Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add enoughRouterLiquidity #6018

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
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
29 changes: 28 additions & 1 deletion packages/agents/sdk-wrapper/src/sdkUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber } from "ethers";
import { BigNumber, BigNumberish } from "ethers";
import { Logger, ChainData, XTransferStatus, XTransferErrorStatus } from "@connext/nxtp-utils";

import type { SdkConfig, RouterBalance, Transfer } from "./sdk-types";
Expand All @@ -24,6 +24,7 @@ export class SdkUtils extends SdkShared {

async getRoutersData(params?: {
order?: { orderBy?: string; ascOrDesc?: "asc" | "desc" };
limit?: number;
}): Promise<RouterBalance[]> {
const response = await axiosPost(`${this.baseUri}/getRoutersData`, params ?? {});
return response.data;
Expand Down Expand Up @@ -59,6 +60,32 @@ export class SdkUtils extends SdkShared {
return BigNumber.from(response.data);
}

async enoughRouterLiquidity(
domainId: string,
asset: string,
minLiquidity: BigNumberish,
maxN?: number,
bufferPercentage?: number
): Promise<BigNumber> {
const params: {
domainId: string;
asset: string;
minLiquidity: BigNumberish,
maxN?: number,
bufferPercentage?: number
} =
{
domainId,
asset,
minLiquidity,
maxN,
bufferPercentage
};
const response = await axiosPost(`${this.baseUri}/enoughRouterLiquidity`, params);

return BigNumber.from(response.data);
}

async getLatestAssetPrice(domainId: string, asset: string): Promise<BigNumber> {
const params: { domainId: string; asset: string } = {
domainId,
Expand Down
32 changes: 32 additions & 0 deletions packages/agents/sdk-wrapper/test/sdkUtils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as MockableFns from "../src/mockable";

import { expect } from "@connext/nxtp-utils";
import {
SdkEnoughRouterLiquidityParams,
SdkCheckRouterLiquidityParams,
SdkGetRouterLiquidityParams,
SdkGetRoutersDataParams,
Expand Down Expand Up @@ -185,6 +186,37 @@ describe("#SDKUtils", () => {
});
});

describe("#enoughRouterLiquidity", async () => {
it("happy: should send request with correct params", async () => {
const expectedEndpoint = "/enoughRouterLiquidity";
const expectedArgs: SdkEnoughRouterLiquidityParams = {
domainId: mock.domain.A,
asset: mock.asset.A.address,
minLiquidity: 100,
maxN: undefined,
};
const mockServerRes = {
type: "BigNumber",
hex: "0x1",
};
const expectedRes = BigNumber.from(mockServerRes);

axiosPostStub.resolves({
data: mockServerRes,
status: 200,
});

const res = await sdkUtils.enoughRouterLiquidity(
expectedArgs.domainId,
expectedArgs.asset,
expectedArgs.minLiquidity
);

expect(axiosPostStub).to.have.been.calledWithExactly(expectedBaseUri + expectedEndpoint, expectedArgs);
expect(res).to.be.deep.eq(expectedRes);
});
});

describe("#getLatestAssetPrice", async () => {
it("happy: should send request with correct params", async () => {
const expectedEndpoint = "/getLatestAssetPrice";
Expand Down
6 changes: 3 additions & 3 deletions packages/agents/sdk/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export const domainsToChainNames: Record<string, string> = {
"1935897199": "scroll",
"1936027759": "sepolia",
"1869640549": "optimism-sepolia",
"1633842021": "arbitrum-sepolia"
"1633842021": "arbitrum-sepolia",
};

// Need to add more domains here.
Expand All @@ -216,7 +216,7 @@ export const XERC20REGISTRY_DOMAIN_ADDRESS: Record<string, string> = {
"1935897199": "0x397aEEEDd44f40326f9eB583a1DFB8A7A673C40B", // scroll
"1936027759": "0x2a3fe9a49fb50536f1ed099192c2ae2404de7bb5", // sepolia
"1869640549": "0x18b5b08b10a2e351180f07e31f4fef94d14e28f6", // op-sepolia
"1633842021": "0x343d827d5109e8038bbb71e9ba4f3fd0d546b9ff" // arb-sepolia
"1633842021": "0x343d827d5109e8038bbb71e9ba4f3fd0d546b9ff", // arb-sepolia
};

// Need to add more domains here.
Expand All @@ -234,5 +234,5 @@ export const LOCKBOX_ADAPTER_DOMAIN_ADDRESS: Record<string, string> = {
"1935897199": "", // scroll (TODO)
"1936027759": "0xcF021fCFB9bd72E5aA7ab390cFA4fCfDF895c7Cf", // sepolia
"1869640549": "0x20b4789065DE09c71848b9A4FcAABB2c10006FA2", // op-sepolia
"1633842021": "0x0f4Fe4903d01E0deb067A7297453fBEFdC36D189" // arb-sepolia
"1633842021": "0x0f4Fe4903d01E0deb067A7297453fBEFdC36D189", // arb-sepolia
};
11 changes: 11 additions & 0 deletions packages/agents/sdk/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,7 @@ export const TRange = Type.Object({
export const SdkGetRoutersDataParamsSchema = Type.Optional(
Type.Object({
order: Type.Optional(TOrderBy),
limit: Type.Optional(Type.Number()),
}),
);
export type SdkGetRoutersDataParams = Static<typeof SdkGetRoutersDataParamsSchema>;
Expand Down Expand Up @@ -844,6 +845,16 @@ export const SdkCheckRouterLiquidityParamsSchema = Type.Object({
});
export type SdkCheckRouterLiquidityParams = Static<typeof SdkCheckRouterLiquidityParamsSchema>;

// enoughRouterLiquidity
export const SdkEnoughRouterLiquidityParamsSchema = Type.Object({
domainId: Type.String(),
asset: Type.String(),
minLiquidity: Type.Number(),
maxN: Type.Optional(Type.Number()),
bufferPercentage: Type.Optional(Type.Number()),
});
export type SdkEnoughRouterLiquidityParams = Static<typeof SdkEnoughRouterLiquidityParamsSchema>;

// getLatestAssetPrice
export const SdkGetLatestAssetPriceParamsSchema = Type.Object({
domainId: Type.String(),
Expand Down
30 changes: 17 additions & 13 deletions packages/agents/sdk/src/sdkPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { validateUri, axiosGetRequest } from "./lib/helpers";
import { Pool, PoolAsset, AssetData, Options } from "./interfaces";
import { PriceFeed } from "./lib/priceFeed";
import { SdkShared } from "./sdkShared";
import { SdkUtils } from "./sdkUtils";

/**
* @classdesc SDK class encapsulating stableswap pool functions.
Expand Down Expand Up @@ -244,13 +245,14 @@ export class SdkPool extends SdkShared {
amount,
signerAddress,
});
const [originPool, [canonicalDomain, canonicalId]] = await Promise.all([
this.getPool(originDomain, _originTokenAddress),
this.getCanonicalTokenId(originDomain, _originTokenAddress),
]);

const originPool = await this.getPool(originDomain, _originTokenAddress);
const isNextAsset = originPool ? utils.getAddress(originPool.local.address) === _originTokenAddress : undefined;
const key = this.calculateCanonicalKey(canonicalDomain, canonicalId);
const destinationAssetData = await this.getAssetsDataByDomainAndKey(destinationDomain, key);
const destinationAssetData = (await this.getAssetsData({
domain: originDomain,
localAsset: _originTokenAddress,
limit: 1
}))[0];
if (!destinationAssetData) {
throw new Error("Origin token cannot be bridged to any token on this destination domain");
}
Expand Down Expand Up @@ -308,14 +310,16 @@ export class SdkPool extends SdkShared {
*/

// Determine if fast liquidity is available (pre-destination-swap amount)
let isFastPath = true;
let isFastPath = false;
if (checkFastLiquidity) {
const activeLiquidity = await this.getActiveLiquidity(destinationDomain, destinationAssetData.local);
this.logger.info("Active router liquidity", requestContext, methodContext, { signerAddress, activeLiquidity });
if (activeLiquidity?.length > 0) {
const total_balance: string = activeLiquidity[0].total_balance.toString();
isFastPath = BigNumber.from(this.scientificToBigInt(total_balance)).mul(70).div(100).gt(originAmountReceived);
}
const sdkUtils = await SdkUtils.create(this.config);
isFastPath = await sdkUtils.enoughRouterLiquidity(
destinationDomain,
destinationAssetData.local,
originAmountReceived,
4, // default top 4 routers, this is enforced by protocol
30 // apply 1.3x router liquidity buffer
);
}

// Subtract router fee if fast liquidity is available
Expand Down
43 changes: 38 additions & 5 deletions packages/agents/sdk/src/sdkShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,14 +403,47 @@ export class SdkShared {
* ```
*/
getAssetsData = memoize(
async (): Promise<AssetData[]> => {
const uri = formatUrl(this.config.cartographerUrl!, "assets");
// Validate uri
async (params?: {
domain?: string;
localAsset?: string;
adoptedAsset?: string;
canonicalId?: string;
order?: { orderBy: string; ascOrDesc: "asc" | "desc" };
limit?: number;
}): Promise<AssetData[]> => {
const { domain, localAsset, adoptedAsset, canonicalId, order, limit } = params ?? {};

const domainIdentifier = domain ? `domain=eq.${domain.toString()}&` : "";
const localAssetIdentifier = localAsset ? `local=eq.${localAsset.toLowerCase()}&` : "";
const adoptedAssetIdentifier = adoptedAsset ? `adopted=eq.${adoptedAsset.toLowerCase()}&` : "";
const canonicalIdIdentifier = canonicalId ? `canonical_id=eq.${canonicalId.toLowerCase()}&` : "";

const searchIdentifier =
domainIdentifier +
localAssetIdentifier +
adoptedAssetIdentifier +
canonicalIdIdentifier;

const orderBy = order?.orderBy || "";
const ascOrDesc = order?.ascOrDesc ? `.${order.ascOrDesc}` : "";
const orderIdentifier = orderBy ? `order=${orderBy}${ascOrDesc}&` : "";
const limitIdentifier = limit ? `limit=${limit}` : "";

const uri = formatUrl(
this.config.cartographerUrl!,
"assets?",
searchIdentifier + orderIdentifier + limitIdentifier
);
validateUri(uri);

return await axiosGetRequest(uri);
},
{ promise: true, maxAge: 5 * 60 * 1000 }, // 5 min
}, {
promise: true,
maxAge: 5 * 60 * 1000, // 5 minutes
normalizer: function(args) {
return JSON.stringify(args[0]);
}
}
);

getActiveLiquidity = memoize(
Expand Down
93 changes: 86 additions & 7 deletions packages/agents/sdk/src/sdkUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { utils, BigNumber } from "ethers";
import { utils, BigNumber, BigNumberish } from "ethers";
import {
Logger,
ChainData,
Expand Down Expand Up @@ -70,9 +70,14 @@ export class SdkUtils extends SdkShared {
* Fetches a list of router liquidity data.
*
* @param params - (optional) Parameters object.
* @param params.domain - (optional) The domain to filter against.
* @param params.localAsset - (optional) The local asset address to filter against.
* @param params.adoptedAsset - (optional) The adopted asset address to filter against.
* @param params.canonicalId - (optional) The canonical ID to filter against.
* @param params.order - (optional) The object with orderBy and ascOrDesc options.
* @param params.order.orderBy - (optional) Field to order by.
* @param params.order.ascOrDesc - (optional) Sort order, either "asc" or "desc".
* @param params.limit - (optional) The number of results to get.
* @returns Array of objects containing the router address and liquidity information, in the form of:
*
* ```ts
Expand All @@ -94,16 +99,36 @@ export class SdkUtils extends SdkShared {
* ```
*/
async getRoutersData(params?: {
domain?: string;
localAsset?: string;
adoptedAsset?: string;
canonicalId?: string;
order?: { orderBy?: string; ascOrDesc?: "asc" | "desc" };
limit?: number;
}): Promise<RouterBalance[]> {
const { order } = params ?? {};
const { domain, localAsset, adoptedAsset, canonicalId, order, limit } = params ?? {};

const orderBy = order?.orderBy ? order.orderBy : "";
const ascOrDesc = order?.ascOrDesc ? "." + order.ascOrDesc : "";
const orderIdentifier = orderBy ? `order=${orderBy}${ascOrDesc}` : "";
const domainIdentifier = domain ? `domain=eq.${domain.toString()}&` : "";
const localAssetIdentifier = localAsset ? `local=eq.${localAsset.toLowerCase()}&` : "";
const adoptedAssetIdentifier = adoptedAsset ? `adopted=eq.${adoptedAsset.toLowerCase()}&` : "";
const canonicalIdIdentifier = canonicalId ? `canonical_id=eq.${canonicalId.toLowerCase()}&` : "";

const uri = formatUrl(this.config.cartographerUrl!, "routers_with_balances?", orderIdentifier);
// Validate uri
const searchIdentifier =
domainIdentifier +
localAssetIdentifier +
adoptedAssetIdentifier +
canonicalIdIdentifier;

const orderBy = order?.orderBy || "";
const ascOrDesc = order?.ascOrDesc ? `.${order.ascOrDesc}` : "";
const orderIdentifier = orderBy ? `order=${orderBy}${ascOrDesc}&` : "";
const limitIdentifier = limit ? `limit=${limit}` : "";

const uri = formatUrl(
this.config.cartographerUrl!,
"routers_with_balances?",
searchIdentifier + orderIdentifier + limitIdentifier
);
validateUri(uri);

return await axiosGetRequest(uri);
Expand Down Expand Up @@ -309,6 +334,60 @@ export class SdkUtils extends SdkShared {
.reduce((acc, router) => acc.add(BigNumber.from(router.balance.toString())), BigNumber.from(0));
}

/**
* Checks if enough router liquidity is available for a specific asset.
*
* @param domainId - The domain ID where the asset exists.
* @param asset - The address of the local asset.
* @param minLiquidity - The minimum liquidity to check against the sum of max N routers.
* @param maxN - (optional) The max N routers, should match the auction round depth (N = 2^(depth-1).
* @param bufferPercentage - (optional) The buffer percentage to apply on top of the minimum liquidity.
* @returns The total router liquidity available for the asset.
*
*/
async enoughRouterLiquidity(
domainId: string,
asset: string,
minLiquidity: BigNumberish,
maxN?: number,
bufferPercentage?: number
): Promise<boolean> {
const _asset = asset.toLowerCase();
const _maxN = maxN ?? 4;
const _minLiquidityBN = BigNumber.from(this.scientificToBigInt(minLiquidity.toString()));
const _bufferPercentage = bufferPercentage ?? 0;

const routersByLargestBalance = await this.getRoutersData({
domain: domainId,
localAsset: _asset,
order: { orderBy: "balance", ascOrDesc: "desc" },
limit: _maxN,
});

let totalLiquidity = BigNumber.from(0);
for (const routerBalance of routersByLargestBalance) {
const balanceBN = BigNumber.from(this.scientificToBigInt(routerBalance.balance.toString()));
totalLiquidity = totalLiquidity.add(balanceBN);
}

const totalLiquidityWithBuffer = _minLiquidityBN.mul(BigNumber.from(100 + _bufferPercentage)).div(100);
return totalLiquidity.gte(totalLiquidityWithBuffer);
}

scientificToBigInt(scientificNotationString: string) {
const parts = scientificNotationString.split("e");
const coeff = parseFloat(parts[0]);
const exp = parts.length > 1 ? parseFloat(parts[1]) : 0;

const decimalParts = coeff.toString().split(".");
const numDecimals = decimalParts[1]?.length || 0;

const bigIntCoeff = BigInt(decimalParts.join(""));
const bigIntExp = BigInt(exp - numDecimals);

return bigIntCoeff * BigInt(10) ** bigIntExp;
}

/**
* Fetches asset prices that match filter criteria from Cartographer.
*
Expand Down
Loading
Loading