Skip to content
Open
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.3.12",
"version": "4.4.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
Expand Down
70 changes: 59 additions & 11 deletions sdk/src/gateway/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
OfframpRawOrder,
OnchainOfframpOrderDetails,
OnrampFeeBreakdownRaw,
OnrampLiquidity,
OnrampOrder,
OnrampOrderResponse,
OnrampOrderStatus,
Expand Down Expand Up @@ -448,33 +449,80 @@ export class GatewayApiClient extends BaseClient {
}

/**
* Fetches available offramp liquidity for a specific token.
* Fetches available offramp liquidity.
*
* @param token Token symbol or address
* @param userAddress User address to get liquidity for
* @returns Promise resolving to liquidity information
* @throws {Error} If API request fails
*/
async fetchOfframpLiquidity(token: string): Promise<OfframpLiquidity> {
async fetchOfframpLiquidity(token: string, userAddress: Address): Promise<OfframpLiquidity> {
const tokenAddress = getTokenAddress(this.chainId, token.toLowerCase());

const response = await this.safeFetch(
`${this.baseUrl}/offramp-liquidity/${tokenAddress}`,
undefined,
'Failed to get offramp liquidity'
);
const queryParams = new URLSearchParams({
tokenAddress: tokenAddress,
userAddress: userAddress,
});

const requestUrl = `${this.baseUrl}/v2/offramp-liquidity?${queryParams.toString()}`;
const response = await this.safeFetch(requestUrl, undefined, 'Failed to get offramp v2 liquidity');

if (!response.ok) {
const errorData = await response.json().catch(() => null);
const errorMessage = errorData?.message || 'Failed to get offramp liquidity';
const errorMessage = errorData?.message || 'Failed to get offramp v2 liquidity';
throw new Error(errorMessage);
}

const rawLiquidity = await response.json();

return {
tokenAddress: rawLiquidity.tokenAddress as Address,
maxOrderAmountInSats: BigInt(rawLiquidity.maxOrderAmountInSats),
totalOfframpLiquidityInSats: BigInt(rawLiquidity.totalOfframpLiquidityInSats),
minimumOfframpQuote: {
minimumAmountInSats: BigInt(rawLiquidity.minimumOfframpQuote.minimumAmountInSats),
calculatedForFeeRate: BigInt(rawLiquidity.minimumOfframpQuote.calculatedForFeeRate),
},
};
}

/**
* Fetches available onramp liquidity.
*
* @param token Token symbol or address
* @param userAddress User address to get liquidity for
* @param gasRefill The amount of gas refill user wants in wei
* @returns Promise resolving to liquidity information
* @throws {Error} If API request fails
*/
async fetchOnrampLiquidity(token: string, userAddress: Address, gasRefill?: bigint): Promise<OnrampLiquidity> {
const tokenAddress = getTokenAddress(this.chainId, token.toLowerCase());

const queryParams = new URLSearchParams({
tokenAddress: tokenAddress,
userAddress: userAddress,
});

if (gasRefill) {
queryParams.append('gasRefill', gasRefill.toString());
}

const requestUrl = `${this.baseUrl}/onramp-liquidity?${queryParams.toString()}`;
const response = await this.safeFetch(requestUrl, undefined, 'Failed to get onramp liquidity');

if (!response.ok) {
const errorData = await response.json().catch(() => null);
const errorMessage = errorData?.message || 'Failed to get onramp liquidity';
throw new Error(errorMessage);
}

const rawLiquidity = await response.json();

return {
token: rawLiquidity.tokenAddress as Address,
maxOrderAmount: BigInt(rawLiquidity.maxOrderAmount),
totalOfframpLiquidity: BigInt(rawLiquidity.totalOfframpLiquidity),
tokenAddress: rawLiquidity.tokenAddress as Address,
maxOrderAmountInSats: BigInt(rawLiquidity.maxOrderAmountInSats),
totalOnrampLiquidityInSats: BigInt(rawLiquidity.totalOnrampLiquidityInSats),
minSatsAmount: BigInt(rawLiquidity.minSatsAmount),
};
}

Expand Down
36 changes: 29 additions & 7 deletions sdk/src/gateway/types/offramp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,36 @@ export interface OfframpQuote {
feeBreakdown: OfframpFeeBreakdown;
}

/** @dev Offramp Available Liquidity */
/** @dev Offramp available liquidity details */
export interface OfframpLiquidity {
/** @dev Token address used for payment */
token: Address;
/** @dev Max token amount a *single* order can be served with (in token decimals) */
maxOrderAmount: bigint;
/** @dev Total liquidity across all solver addresses (in token decimals) */
totalOfframpLiquidity: bigint;
/** @dev Address of the token accepted for offramp payments */
tokenAddress: Address;
/** @dev Maximum sats amount that a *single* order can be served with for a specific user address */
maxOrderAmountInSats: bigint;
/** @dev Total available offramp liquidity across all solver addresses */
totalOfframpLiquidityInSats: bigint;
/** @dev Details about the minimum offramp quote, including fee rate */
minimumOfframpQuote: MinimumOfframpQuote;
}

/** @dev Minimum offramp quote information */
export interface MinimumOfframpQuote {
/** @dev Minimum sats amount that can be processed for a single offramp order */
minimumAmountInSats: bigint;
/** @dev Fee rate used to calculate the minimum offramp quote */
calculatedForFeeRate: bigint;
}

/** @dev Onramp available liquidity details */
export interface OnrampLiquidity {
/** @dev Address of the token accepted for onramp payments */
tokenAddress: Address;
/** @dev Maximum sats amount that a *single* order can be served with for a specific user address */
maxOrderAmountInSats: bigint;
/** @dev Total available onramp liquidity across all gateway addresses */
totalOnrampLiquidityInSats: bigint;
/** @dev Minimum sats amount that can be processed for an onramp order */
minSatsAmount: bigint;
}

/** @dev Params used for createOrder call on the off-ramp contract */
Expand Down
146 changes: 120 additions & 26 deletions sdk/test/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,32 +767,6 @@ describe('Gateway Tests', () => {
expect(result).toBe(true);
});

it('fetches the correct offramp liquidity', async () => {
const gatewaySDK = new GatewaySDK(bobSepolia.id);
const tokenAddress = '0x4496ebE7C8666a8103713EE6e0c08cA0cD25b888'.toLowerCase();
nock(SIGNET_GATEWAY_BASE_URL).persist().get(`/offramp-liquidity/${tokenAddress}`).reply(200, {
tokenAddress,
maxOrderAmount: '861588',
totalOfframpLiquidity: '861588',
});

const offrampLiquidityTokenAddressAsParam = await gatewaySDK.fetchOfframpLiquidity(tokenAddress);

expect(offrampLiquidityTokenAddressAsParam).toEqual({
token: tokenAddress,
maxOrderAmount: BigInt('861588'),
totalOfframpLiquidity: BigInt('861588'),
});

const offrampLiquidityTokenSymbolAsParam = await gatewaySDK.fetchOfframpLiquidity('bobBTC');

expect(offrampLiquidityTokenSymbolAsParam).toEqual({
token: tokenAddress,
maxOrderAmount: BigInt('861588'),
totalOfframpLiquidity: BigInt('861588'),
});
});

it('should return btc txid for onramp', async () => {
const gatewaySDK = new GatewaySDK(bob.id);
const mockBtcSigner = {
Expand Down Expand Up @@ -1417,4 +1391,124 @@ describe('Gateway Tests', () => {

expect(orders).toEqual([]);
});

it('should return mocked offramp liquidity', async () => {
const gatewaySDK = new GatewaySDK(bob.id);
const tokenAddress = SYMBOL_LOOKUP[bob.id]['wbtc (oft)'].address;
const userAddress = zeroAddress;

const mock_offramp_liquidity = {
tokenAddress,
maxOrderAmountInSats: 50000000,
totalOfframpLiquidityInSats: 53304097,
minimumOfframpQuote: {
minimumAmountInSats: 666,
calculatedForFeeRate: 1,
},
};

nock(MAINNET_GATEWAY_BASE_URL)
.get('/v2/offramp-liquidity')
.query({
tokenAddress,
userAddress,
})
.reply(200, mock_offramp_liquidity);

const offrampLiquidity = await gatewaySDK.fetchOfframpLiquidity(tokenAddress, userAddress);

const normalized = JSON.parse(
JSON.stringify(offrampLiquidity, (_, v) => (typeof v === 'bigint' ? Number(v) : v))
);

// Match only the liquidity details, skip tokenAddress
expect(normalized).toMatchObject(mock_offramp_liquidity);
});

it('should return mocked onramp liquidity', async () => {
const gatewaySDK = new GatewaySDK(bob.id);
const tokenAddress = SYMBOL_LOOKUP[bob.id]['wbtc (oft)'].address as Address;
const userAddress = '0xFAEe001465dE6D7E8414aCDD9eF4aC5A35B2B808' as Address;

const mock_onramp_liquidity = {
tokenAddress,
maxOrderAmountInSats: 50000000,
totalOnrampLiquidityInSats: 53408586,
minSatsAmount: 86,
};

nock(MAINNET_GATEWAY_BASE_URL)
.get('/onramp-liquidity')
.query({
tokenAddress,
userAddress,
})
.reply(200, mock_onramp_liquidity);

const onrampLiquidity = await gatewaySDK.fetchOnrampLiquidity(tokenAddress, userAddress, undefined);

const normalized = JSON.parse(
JSON.stringify(onrampLiquidity, (_, v) => (typeof v === 'bigint' ? Number(v) : v))
);
expect(normalized).toMatchObject(mock_onramp_liquidity);
});

it.skip(
'get offramp liquidity and get quote e2e',
async () => {
const gatewaySDK = new GatewaySDK(bob.id);
const tokenAddress = SYMBOL_LOOKUP[bob.id]['wbtc (oft)'].address as Address;
const offrampLiquidity = await gatewaySDK.fetchOfframpLiquidity(tokenAddress, zeroAddress);

const minGatewayQuoteOnly = await gatewaySDK.fetchOfframpQuote(
tokenAddress,
offrampLiquidity.minimumOfframpQuote.minimumAmountInSats,
zeroAddress
);
expect(BigInt(minGatewayQuoteOnly.amountLockInSat)).toEqual(
offrampLiquidity.minimumOfframpQuote.minimumAmountInSats
);

const maxGatewayQuoteOnly = await gatewaySDK.fetchOfframpQuote(
tokenAddress,
offrampLiquidity.maxOrderAmountInSats,
zeroAddress
);
expect(BigInt(maxGatewayQuoteOnly.amountLockInSat)).toEqual(offrampLiquidity.maxOrderAmountInSats);
},
{ timeout: 30000 } // 30 seconds
);

it.skip(
'get onramp liquidity and get quote e2e',
async () => {
const gatewaySDK = new GatewaySDK(bob.id);
const tokenAddress = SYMBOL_LOOKUP[bob.id]['wbtc (oft)'].address as Address;
const userAddress = '0xFAEe001465dE6D7E8414aCDD9eF4aC5A35B2B808' as Address;
const onrampLiquidity = await gatewaySDK.fetchOnrampLiquidity(tokenAddress, userAddress, undefined);

const minGatewayQuoteOnly = await gatewaySDK.getOnrampQuote({
fromUserAddress: userAddress,
fromChain: 'Bitcoin',
fromToken: 'bitcoin',
toChain: bob.id,
toToken: 'WBTC (OFT)',
toUserAddress: tokenAddress,
amount: onrampLiquidity.minSatsAmount,
});
expect(BigInt(minGatewayQuoteOnly.satoshis)).toEqual(onrampLiquidity.minSatsAmount);

const maxGatewayQuoteOnly = await gatewaySDK.getOnrampQuote({
fromUserAddress: userAddress,
fromChain: 'Bitcoin',
fromToken: 'bitcoin',
toChain: bob.id,
toToken: 'WBTC (OFT)',
toUserAddress: tokenAddress,
amount: onrampLiquidity.maxOrderAmountInSats,
});
expect(BigInt(maxGatewayQuoteOnly.satoshis)).toEqual(onrampLiquidity.maxOrderAmountInSats);
},
{ timeout: 30000 } // 30 seconds
);
});