Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gobob/bob-sdk",
"version": "4.2.10",
"version": "4.2.11",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
Expand Down
6 changes: 2 additions & 4 deletions sdk/src/gateway/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddressType, Network } from 'bitcoin-address-validation';

Check warning on line 1 in sdk/src/gateway/client.ts

View workflow job for this annotation

GitHub Actions / Tests

'AddressType' is defined but never used
import * as bitcoin from 'bitcoinjs-lib';
import {
Account,
Expand All @@ -17,7 +17,7 @@
import { bob, bobSepolia } from 'viem/chains';
import { EsploraClient } from '../esplora';
import { bigIntToFloatingNumber } from '../utils';
import { createBitcoinPsbt, getAddressInfo } from '../wallet';

Check warning on line 20 in sdk/src/gateway/client.ts

View workflow job for this annotation

GitHub Actions / Tests

'getAddressInfo' is defined but never used
import { claimDelayAbi, offrampCaller, strategyCaller } from './abi';
import StrategyClient from './strategy';
import { ADDRESS_LOOKUP, getTokenAddress, getTokenDecimals } from './tokens';
Expand All @@ -43,6 +43,7 @@
OnchainOfframpOrderDetails,
OnrampExecuteQuoteParams,
OnrampOrder,
OnrampOrderResponse,
OrderStatus,
StrategyParams,
Token,
Expand Down Expand Up @@ -421,9 +422,6 @@
bitcoinNetwork = bitcoin.networks.testnet;
}

if (!params.toUserAddress || getAddressInfo(params.toUserAddress, this.isSignet).type === AddressType.p2tr) {
throw new Error('Only following bitcoin address types are supported P2PKH, P2WPKH, P2SH or P2WSH.');
}
const receiverAddress = toHexScriptPubKey(params.toUserAddress, bitcoinNetwork);

return {
Expand Down Expand Up @@ -1017,7 +1015,7 @@
undefined,
'Failed to fetch onramp orders'
);
const orders = await response.json();
const orders: OnrampOrderResponse[] = await response.json();
return orders.map((order) => {
function getFinal<L, R>(base?: L, output?: R): NonNullable<L | R> {
return order.status
Expand Down
89 changes: 70 additions & 19 deletions sdk/src/gateway/layerzero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,43 @@
oftCmd: Hex;
};

type LayerZeroChainInfo = {
name: string;
eid?: string;
oftAddress: string;
nativeChainId?: number;
};

export class LayerZeroClient {
private basePath: string;

constructor() {
this.basePath = 'https://metadata.layerzero-api.com/v1/metadata';
}

async getSupportedChains(): Promise<Array<string>> {
const params = new URLSearchParams({
symbols: 'WBTC',
});

const data = await this.getJson<{
WBTC: [{ deployments: { [chainKey: string]: { address: string } } }];
}>(`${this.basePath}/experiment/ofts/list?${params.toString()}`);

return Object.keys(data.WBTC[0].deployments);
}

async getEidForChain(chainKey: string) {
const data = await this.getJson<{
private async getChainDeployments() {
return this.getJson<{
[chainKey: string]: {
deployments: [
deployments?: [
{
version: number;
eid: string;
},
];
chainKey: string;
chainDetails?: {
nativeChainId: number;
};
};
}>(`${this.basePath}`);
}

return data[chainKey]?.deployments.find((item) => item.version === 2)?.eid || null;
async getEidForChain(chainKey: string) {
const data = await this.getChainDeployments();
return data[chainKey]?.deployments?.find((item) => item.version === 2)?.eid || null;
}

async getOftAddressForChain(chainKey: string): Promise<string | null> {
private async getWbtcDeployments() {
const params = new URLSearchParams({
symbols: 'WBTC',
});
Expand All @@ -67,7 +69,41 @@
WBTC: [{ deployments: { [chainKey: string]: { address: string } } }];
}>(`${this.basePath}/experiment/ofts/list?${params.toString()}`);

return data.WBTC[0].deployments[chainKey]?.address || null;
return data.WBTC[0].deployments;
}

async getSupportedChains(): Promise<Array<string>> {
const deployments = await this.getWbtcDeployments();
return Object.keys(deployments);
}

async getOftAddressForChain(chainKey: string): Promise<string | null> {
const deployments = await this.getWbtcDeployments();
return deployments[chainKey]?.address || null;
}

async getSupportedChainsInfo(): Promise<Array<LayerZeroChainInfo>> {
const chains = await this.getChainDeployments();
const chainLookup = Object.fromEntries(
Object.entries(chains).map(([_, chainData]) => [

Check warning on line 88 in sdk/src/gateway/layerzero.ts

View workflow job for this annotation

GitHub Actions / Tests

'_' is defined but never used
chainData.chainKey,
{
eid: chainData.deployments?.find((item) => item.version === 2)?.eid,
nativeChainId: chainData.chainDetails?.nativeChainId,
},
])
);

const deployments = await this.getWbtcDeployments();
return Object.entries(deployments).map(([chainKey, deployment]) => {
const chainInfo = chainLookup[chainKey];
return {
name: chainKey,
eid: chainInfo?.eid,
oftAddress: deployment.address,
nativeChainId: chainInfo?.nativeChainId,
};
});
}

private async getJson<T>(url: string): Promise<T> {
Expand Down Expand Up @@ -105,14 +141,27 @@
this.l0Client = new LayerZeroClient();
}

async getSupportedChainsInfo(): Promise<Array<LayerZeroChainInfo>> {
return this.l0Client.getSupportedChainsInfo();
}

/**
* @deprecated Use getSupportedChainsInfo() instead
*/
async getSupportedChains(): Promise<Array<string>> {
return this.l0Client.getSupportedChains();
}

/**
* @deprecated Use getSupportedChainsInfo() instead
*/
async getEidForChain(chainKey: string): Promise<string | null> {
return this.l0Client.getEidForChain(chainKey);
}

/**
* @deprecated Use getSupportedChainsInfo() instead
*/
async getOftAddressForChain(chainKey: string): Promise<string | null> {
return this.l0Client.getOftAddressForChain(chainKey);
}
Expand Down Expand Up @@ -315,7 +364,9 @@
};

// we're quoting on the origin chain, so public client must be configured correctly
if (publicClient.chain?.name.toLowerCase() !== fromChain) {
const maybeFromChainId = executeQuoteParams.params.fromChain;
if (typeof maybeFromChainId === 'number' && publicClient.chain?.id !== maybeFromChainId) {
// avoid matching on name since L0 and viem may have different naming conventions
throw new Error(`Public client must be origin chain`);
}
Comment on lines 366 to 371
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Strengthen origin‑chain check when fromChain is a string.

Currently only enforced for numeric IDs. If a string slug is provided, publicClient could point to the wrong chain and the tx would be simulated/submitted on the wrong network.

Apply:

-            const maybeFromChainId = executeQuoteParams.params.fromChain;
-            if (typeof maybeFromChainId === 'number' && publicClient.chain?.id !== maybeFromChainId) {
-                // avoid matching on name since L0 and viem may have different naming conventions
-                throw new Error(`Public client must be origin chain`);
-            }
+            const fromParam = executeQuoteParams.params.fromChain;
+            if (typeof fromParam === 'number') {
+                if (publicClient.chain?.id !== fromParam) {
+                    throw new Error(`Public client must be origin chain`);
+                }
+            } else {
+                // Resolve slug -> nativeChainId via metadata and verify by numeric id to avoid name mismatches.
+                const info = await this.l0Client.getSupportedChainsInfo();
+                const nativeId = info.find((c) => c.name === fromParam)?.nativeChainId;
+                if (nativeId && publicClient.chain?.id !== nativeId) {
+                    throw new Error(`Public client must be origin chain`);
+                }
+            }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In sdk/src/gateway/layerzero.ts around lines 366 to 371, the origin-chain guard
only checks numeric IDs; extend it to also validate when
executeQuoteParams.params.fromChain is a string by performing a case-insensitive
comparison against the publicClient.chain's identifying fields (e.g., chain.name
and chain.network or chain.slug if available) and throw the same error if none
match; keep the existing numeric ID check, ensure null/undefined
publicClient.chain is handled safely, and normalize strings before comparing.


Expand Down
15 changes: 7 additions & 8 deletions sdk/src/gateway/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export interface OnrampOrderResponse {
/** @description The tx hash on the EVM chain */
txHash?: string;
/** @description V4 order details */
orderDetails?: OrderDetails;
orderDetails?: OrderDetailsRaw;
}

export type OrderStatusData = {
Expand Down Expand Up @@ -343,13 +343,12 @@ export type OrderStatus =
};

/** Order given by the Gateway API once the bitcoin tx is submitted */
export type OnrampOrder = Omit<
OnrampOrderResponse & {
/** @description The gas refill in satoshis */
gasRefill: number;
},
'satsToConvertToEth'
> & {
export type OnrampOrder = Omit<OnrampOrderResponse, 'satsToConvertToEth' | 'orderDetails'> & {
/** @description The gas refill in satoshis */
gasRefill: number;
/** @description V4 order details */
orderDetails?: OrderDetails;
} & {
/** @description Get the actual token address received */
getTokenAddress(): string | undefined;
/** @description Get the actual token received */
Expand Down
45 changes: 0 additions & 45 deletions sdk/test/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,51 +539,6 @@ describe('Gateway Tests', () => {
expect(scriptPubKey).to.deep.equal('0x1976a914bba505f3a2730254053081318cade0672ffe31d888ac');
});

it('should return error for taproot address', async () => {
const gatewaySDK = new GatewaySDK(bobSepolia.id);
nock(`${SIGNET_GATEWAY_BASE_URL}`)
.get('/offramp-quote')
.query(true)
.reply(200, {
amountLockInSat: 10000000000000,
registryAddress: '0xd7b27b178f6bf290155201109906ad203b6d99b1',
feeBreakdown: {
overallFeeSats: 385,
inclusionFeeSats: 384,
protocolFeeSats: 1,
affiliateFeeSats: 0,
fastestFeeRate: 1,
},
});

await expect(
gatewaySDK.createOfframpOrder(
{
amountLockInSat: 0,
deadline: 0,
registryAddress: '0x',
token: '0x',
feeBreakdown: {
overallFeeSats: 0,
inclusionFeeSats: 0,
protocolFeeSats: 0,
affiliateFeeSats: 0,
fastestFeeRate: 0,
},
},
{
fromToken: '0xda472456b1a6a2fc9ae7edb0e007064224d4284c',
amount: 100000000000000,
fromUserAddress: '0xFAEe001465dE6D7E8414aCDD9eF4aC5A35B2B808',
toUserAddress: 'tb1p5d2m6d7yje35xqnk2wczghak6q20c6rqw303p58wrlzhue8t4z9s9y304z', // P2TR taproot address
fromChain: 'bob',
toChain: 'bitcoin',
toToken: 'bitcoin',
}
)
).rejects.toThrowError('Only following bitcoin address types are supported P2PKH, P2WPKH, P2SH or P2WSH.');
});

it('fetches the correct offramp registry address', async () => {
const gatewaySDK = new GatewaySDK(bobSepolia.id);

Expand Down
66 changes: 35 additions & 31 deletions sdk/test/layerzero.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import { privateKeyToAccount } from 'viem/accounts';
describe('LayerZero Tests', () => {
it('should get chains', async () => {
const client = new LayerZeroClient();
const chains = await client.getSupportedChains();
assert.containSubset(chains, [
const chainsInfo = await client.getSupportedChainsInfo();

const chainNames = chainsInfo.map((chain) => chain.name);
assert.containSubset(chainNames, [
'ethereum',
'bob',
'base',
Expand All @@ -31,38 +33,40 @@ describe('LayerZero Tests', () => {
'sei',
]);

assert.equal(await client.getEidForChain('ethereum'), '30101');
assert.equal(await client.getEidForChain('bob'), '30279');
assert.equal(await client.getEidForChain('base'), '30184');
assert.equal(await client.getEidForChain('bera'), '30362');
assert.equal(await client.getEidForChain('unichain'), '30320');
assert.equal(await client.getEidForChain('avalanche'), '30106');
assert.equal(await client.getEidForChain('sonic'), '30332');
assert.equal(await client.getEidForChain('aptos'), '30108');
assert.equal(await client.getEidForChain('bsc'), '30102');
assert.equal(await client.getEidForChain('soneium'), '30340');
assert.equal(await client.getEidForChain('telos'), '30199');
assert.equal(await client.getEidForChain('swell'), '30335');
assert.equal(await client.getEidForChain('optimism'), '30111');
assert.equal(await client.getEidForChain('sei'), '30280');

assert.equal(await client.getOftAddressForChain('ethereum'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('bob'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('base'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('bera'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('unichain'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('avalanche'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('sonic'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
const findChain = (name: string) => chainsInfo.find((chain) => chain.name === name);

assert.equal(findChain('ethereum')?.eid, '30101');
assert.equal(findChain('bob')?.eid, '30279');
assert.equal(findChain('base')?.eid, '30184');
assert.equal(findChain('bera')?.eid, '30362');
assert.equal(findChain('unichain')?.eid, '30320');
assert.equal(findChain('avalanche')?.eid, '30106');
assert.equal(findChain('sonic')?.eid, '30332');
assert.equal(findChain('aptos')?.eid, '30108');
assert.equal(findChain('bsc')?.eid, '30102');
assert.equal(findChain('soneium')?.eid, '30340');
assert.equal(findChain('telos')?.eid, '30199');
assert.equal(findChain('swell')?.eid, '30335');
assert.equal(findChain('optimism')?.eid, '30111');
assert.equal(findChain('sei')?.eid, '30280');

assert.equal(findChain('ethereum')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('bob')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('base')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('bera')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('unichain')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('avalanche')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('sonic')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(
await client.getOftAddressForChain('aptos'),
findChain('aptos')?.oftAddress,
'0xef3a1c7d6aa1a531336433e48d7b1d9b46c5bedc69f3db291df4e39bef4708e2'
);
assert.equal(await client.getOftAddressForChain('bsc'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('soneium'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('telos'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('swell'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(await client.getOftAddressForChain('optimism'), '0xc3f854b2970f8727d28527ece33176fac67fef48');
assert.equal(await client.getOftAddressForChain('sei'), '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('bsc')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('soneium')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('telos')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('swell')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
assert.equal(findChain('optimism')?.oftAddress, '0xc3f854b2970f8727d28527ece33176fac67fef48');
assert.equal(findChain('sei')?.oftAddress, '0x0555e30da8f98308edb960aa94c0db47230d2b9c');
}, 120000);

it.skip('should get an onramp quote and execute it', async () => {
Expand Down
Loading