From e61cb73bed571598b9cdf220e5112dc982004cd0 Mon Sep 17 00:00:00 2001 From: c4605 Date: Mon, 7 Apr 2025 21:02:55 +0200 Subject: [PATCH] feat(XLinkSDK): returns swap-possible routes --- scripts/printAllSupportedRoutes.ts | 36 ++++++++- src/XLinkSDK.ts | 77 +++++++++++++++---- src/bitcoinUtils/peggingHelpers.ts | 33 +++++++- src/evmUtils/peggingHelpers.ts | 60 ++++++++++++--- src/metaUtils/peggingHelpers.ts | 61 ++++++++++----- src/utils/SwapRouteHelpers.ts | 2 +- src/utils/buildSupportedRoutes.ts | 12 ++- ...ortedRoutes.ts => detectPossibleRoutes.ts} | 23 ++++-- src/xlinkSdkUtils/types.internal.ts | 1 + 9 files changed, 240 insertions(+), 65 deletions(-) rename src/utils/{detectSupportedRoutes.ts => detectPossibleRoutes.ts} (91%) diff --git a/scripts/printAllSupportedRoutes.ts b/scripts/printAllSupportedRoutes.ts index 5f64b17..6dd3217 100644 --- a/scripts/printAllSupportedRoutes.ts +++ b/scripts/printAllSupportedRoutes.ts @@ -2,11 +2,37 @@ import { XLinkSDK } from "../src" import { KnownRoute } from "../src/utils/buildSupportedRoutes" async function print(matchers: { + debug: boolean + op: "swap" | "bridge" chain: string[] token: string[] }): Promise { - const sdk = new XLinkSDK() - const supportedRoutes = await sdk.getSupportedRoutes() + const sdk = new XLinkSDK({ + debugLog: matchers.debug, + }) + const supportedRoutes = await sdk + .getSupportedRoutes({ + includeUnpredictableSwapPossibilities: matchers.op === "swap", + }) + .then(routes => { + const isChainMatch: (r: KnownRoute) => boolean = + matchers.chain.length === 0 + ? () => true + : r => + matchers.chain.some( + c => r.fromChain.includes(c) || r.toChain.includes(c), + ) + + const isTokenMatch: (r: KnownRoute) => boolean = + matchers.token.length === 0 + ? () => true + : r => + matchers.token.some( + t => r.fromToken.includes(t) || r.toToken.includes(t), + ) + + return routes.filter(r => isChainMatch(r) && isTokenMatch(r)) + }) const group: Record = {} for (const route of supportedRoutes) { @@ -42,11 +68,15 @@ async function print(matchers: { async function main(command: string[], args: string[]): Promise { if (args.some(a => a === "-h" || a === "--help")) { - console.log(`Usage: ${command.join(" ")} [chain:] [token:]`) + console.log( + `Usage: ${command.join(" ")} [op:debug] [op:swap] [chain:] [token:]`, + ) process.exit(0) } const matchers = { + debug: args.includes("op:debug"), + op: args.includes("op:swap") ? ("swap" as const) : ("bridge" as const), chain: [] as string[], token: [] as string[], } diff --git a/src/XLinkSDK.ts b/src/XLinkSDK.ts index 750c8f5..f86c8f2 100644 --- a/src/XLinkSDK.ts +++ b/src/XLinkSDK.ts @@ -30,7 +30,7 @@ import { GetSupportedRoutesFn_Conditions, KnownRoute, } from "./utils/buildSupportedRoutes" -import { detectSupportedRoutes } from "./utils/detectSupportedRoutes" +import { detectPossibleRoutes } from "./utils/detectPossibleRoutes" import { TooFrequentlyError } from "./utils/errors" import { KnownChainId, @@ -119,6 +119,8 @@ import { } from "./xlinkSdkUtils/types" import { SDKGlobalContext } from "./xlinkSdkUtils/types.internal" import { DumpableCache, getCacheInside } from "./utils/DumpableCache" +import { isNotNull } from "./utils/typeHelpers" +import { SwapRoute } from "./utils/SwapRouteHelpers" export { GetSupportedRoutesFn_Conditions, @@ -190,6 +192,7 @@ export { export type { DumpableCache } from "./utils/DumpableCache" export interface XLinkSDKOptions { + debugLog?: boolean __experimental?: { backendAPI?: { runtimeEnv?: "prod" | "dev" @@ -251,6 +254,7 @@ export class XLinkSDK { } this.sdkContext = { + debugLog: options.debugLog ?? false, routes: { detectedCache: new Map(), }, @@ -291,19 +295,33 @@ export class XLinkSDK { } /** - * This function retrieves the list of supported routes for token transfers between blockchain - * networks, filtered based on optional conditions. It aggregates the results from different - * blockchain networks (Stacks, EVM, Bitcoin) to return a list of possible routes. - * @param conditions - An optional object containing the conditions for filtering the supported routes: + * @deprecated Use `getPossibleRoutes` instead + */ + async getSupportedRoutes( + conditions?: GetSupportedRoutesFn_Conditions, + ): Promise { + return this.getPossibleRoutes(conditions) + } + + /** + * This function roughly returns a list of possible routes supported by the + * SDK. It aggregates the results from different blockchain networks (Stacks, + * EVM, Bitcoin). + * + * @param conditions - An optional object containing the conditions for filtering the possible routes: * - `fromChain?: ChainId` - The ID of the source blockchain (optional). * - `toChain?: ChainId` - The ID of the destination blockchain (optional). * - `fromToken?: TokenId` - The ID of the token being transferred from the source blockchain (optional). * - `toToken?: TokenId` - The ID of the token expected on the destination blockchain (optional). + * - `includeUnpredictableSwapPossibilities?: boolean` - Whether to include + * routes that require token swaps to complete. Note that the ability to perform these swaps + * cannot be determined at this point, so enabling this option may return routes that cannot + * actually be completed (optional). * * @returns A promise that resolves with an array of `KnownRoute` objects, each representing a * possible route for the token transfer. */ - async getSupportedRoutes( + async getPossibleRoutes( conditions?: GetSupportedRoutesFn_Conditions, ): Promise { const specifiedChain = conditions?.fromChain ?? conditions?.toChain @@ -318,28 +336,55 @@ export class XLinkSDK { let resultRoutesPromise: Promise if (networkType == null) { resultRoutesPromise = Promise.all([ - detectSupportedRoutes(this.sdkContext, "mainnet"), - detectSupportedRoutes(this.sdkContext, "testnet"), - ]).then(res => res.flat()) + detectPossibleRoutes(this.sdkContext, { + networkType: "mainnet", + swapEnabled: + conditions?.includeUnpredictableSwapPossibilities ?? false, + }), + detectPossibleRoutes(this.sdkContext, { + networkType: "testnet", + swapEnabled: + conditions?.includeUnpredictableSwapPossibilities ?? false, + }), + ]).then(res => res.flat().filter(isNotNull)) } else { - resultRoutesPromise = detectSupportedRoutes(this.sdkContext, networkType) + resultRoutesPromise = detectPossibleRoutes(this.sdkContext, { + networkType, + swapEnabled: conditions?.includeUnpredictableSwapPossibilities ?? false, + }) } const resultRoutes = await resultRoutesPromise - if (conditions == null || Object.keys(conditions).length === 0) { + const routeConditions = { + fromChain: conditions?.fromChain, + fromToken: conditions?.fromToken, + toChain: conditions?.toChain, + toToken: conditions?.toToken, + } + if (Object.values(routeConditions).filter(isNotNull).length === 0) { return resultRoutes } return resultRoutes.filter( r => - r.fromChain === conditions.fromChain && - r.toChain === conditions.toChain && - r.fromToken === conditions.fromToken && - r.toToken === conditions.toToken, + (routeConditions.fromChain == null || + r.fromChain === routeConditions.fromChain) && + (routeConditions.fromToken == null || + r.fromToken === routeConditions.fromToken) && + (routeConditions.toChain == null || + r.toChain === routeConditions.toChain) && + (routeConditions.toToken == null || + r.toToken === routeConditions.toToken), ) } - async isSupportedRoute(route: DefinedRoute): Promise { + /** + * different from `getPossibleRoutes`, this function is designed to further + * determine if the route is supported by the SDK + */ + async isSupportedRoute( + route: DefinedRoute & { swapRoute?: SwapRoute }, + ): Promise { const checkingResult = await Promise.all([ isSupportedEVMRoute(this.sdkContext, route), isSupportedStacksRoute(this.sdkContext, route), diff --git a/src/bitcoinUtils/peggingHelpers.ts b/src/bitcoinUtils/peggingHelpers.ts index 5c2814b..b4cf98b 100644 --- a/src/bitcoinUtils/peggingHelpers.ts +++ b/src/bitcoinUtils/peggingHelpers.ts @@ -49,10 +49,11 @@ export const getBtc2StacksFeeInfo = async ( return withGlobalContextCache( ctx.btc.feeRateCache, `${withGlobalContextCache.cacheKeyFromRoute(route)}:${options.swapRoute?.via ?? ""}`, - () => _getBtc2StacksFeeInfo(route, options), + () => _getBtc2StacksFeeInfo(ctx, route, options), ) } const _getBtc2StacksFeeInfo = async ( + ctx: SDKGlobalContext, route: KnownRoute_FromBitcoin_ToStacks, options: { swapRoute: null | Pick @@ -104,7 +105,20 @@ const _getBtc2StacksFeeInfo = async ( {}, contractCallInfo.executeOptions, ).then(numberFromStacksContractNumber), - }) + }).then( + resp => { + if (ctx.debugLog) { + console.log("[getBtc2StacksFeeInfo]", route, resp) + } + return resp + }, + err => { + if (ctx.debugLog) { + console.log("[getBtc2StacksFeeInfo]", route, err) + } + throw err + }, + ) return { isPaused: resp.isPaused, @@ -214,7 +228,20 @@ const _getStacks2BtcFeeInfo = async ( {}, stacksContractCallInfo.executeOptions, ), - }) + }).then( + resp => { + if (ctx.debugLog) { + console.log("[getStacks2BtcFeeInfo]", route, resp) + } + return resp + }, + err => { + if (ctx.debugLog) { + console.log("[getStacks2BtcFeeInfo]", route, err) + } + throw err + }, + ) return { isPaused: resp.isPaused, diff --git a/src/evmUtils/peggingHelpers.ts b/src/evmUtils/peggingHelpers.ts index 20e9c11..50b463e 100644 --- a/src/evmUtils/peggingHelpers.ts +++ b/src/evmUtils/peggingHelpers.ts @@ -148,7 +148,20 @@ const _getEvm2StacksFeeInfo = async ( {}, stacksContractCallInfo.executeOptions, ), - }) + }).then( + resp => { + if (ctx.debugLog) { + console.log("[getEvm2StacksFeeInfo]", route, resp) + } + return resp + }, + err => { + if (ctx.debugLog) { + console.log("[getEvm2StacksFeeInfo]", route, err) + } + throw err + }, + ) if (!resp.isApprovedOnEVMSide) return undefined @@ -194,7 +207,20 @@ const getEvm2StacksNativeBridgeFeeInfo = async ( {}, stacksContractCallInfo.executeOptions, ), - }) + }).then( + resp => { + if (ctx.debugLog) { + console.log("[getEvm2StacksNativeBridgeFeeInfo]", route, resp) + } + return resp + }, + err => { + if (ctx.debugLog) { + console.log("[getEvm2StacksNativeBridgeFeeInfo]", route, err) + } + throw err + }, + ) return { isPaused: resp.isPaused, @@ -279,6 +305,10 @@ const _getStacks2EvmFeeInfo = async ( }, }) + if (ctx.debugLog) { + console.log("[getStacks2EvmFeeInfo/specialFeeInfo]", route, specialFeeInfo) + } + const tokenConf = await Promise.all([ executeReadonlyCallXLINK( stacksContractCallInfo.contractName, @@ -297,14 +327,26 @@ const _getStacks2EvmFeeInfo = async ( {}, stacksContractCallInfo.executeOptions, ), - ]).then(([resp, isPaused]) => { - if (resp.type !== "success") return undefined + ]).then( + ([resp, isPaused]) => { + if (ctx.debugLog) { + console.log("[getStacks2EvmFeeInfo]", route, resp, isPaused) + } - return { - ...unwrapResponse(resp), - isPaused, - } - }) + if (resp.type !== "success") return undefined + + return { + ...unwrapResponse(resp), + isPaused, + } + }, + err => { + if (ctx.debugLog) { + console.log("[getStacks2EvmFeeInfo]", route, err) + } + throw err + }, + ) if (tokenConf == null) return undefined diff --git a/src/metaUtils/peggingHelpers.ts b/src/metaUtils/peggingHelpers.ts index 0d3fe7d..d7b91e1 100644 --- a/src/metaUtils/peggingHelpers.ts +++ b/src/metaUtils/peggingHelpers.ts @@ -9,7 +9,6 @@ import { BigNumber } from "../utils/BigNumber" import { getAndCheckTransitStacksTokens, getSpecialFeeDetailsForSwapRoute, - SpecialFeeDetailsForSwapRoute, SwapRoute, } from "../utils/SwapRouteHelpers" import { @@ -96,7 +95,7 @@ const _getMeta2StacksFeeInfo = async ( }, ): Promise => { if (options.swapRoute != null) { - return getMeta2StacksSwapFeeInfo(route, { + return getMeta2StacksSwapFeeInfo(ctx, route, { swapRoute: options.swapRoute, }) } else { @@ -126,6 +125,10 @@ const getMeta2StacksBaseFeeInfo = async ( const filteredRoute = filteredRoutes[0] if (filteredRoute == null) return + if (ctx.debugLog) { + console.log("[getMeta2StacksBaseFeeInfo]", route, filteredRoute) + } + return { isPaused: filteredRoute.pegInPaused, bridgeToken: route.fromToken, @@ -150,6 +153,7 @@ const getMeta2StacksBaseFeeInfo = async ( } const getMeta2StacksSwapFeeInfo = async ( + ctx: SDKGlobalContext, route1: KnownRoute_FromBRC20_ToStacks | KnownRoute_FromRunes_ToStacks, options: { swapRoute: Pick @@ -187,7 +191,20 @@ const getMeta2StacksSwapFeeInfo = async ( {}, contractCallInfo.executeOptions, ).then(numberFromStacksContractNumber), - }) + }).then( + resp => { + if (ctx.debugLog) { + console.log("[getMeta2StacksSwapFeeInfo]", route1, resp) + } + return resp + }, + err => { + if (ctx.debugLog) { + console.log("[getMeta2StacksSwapFeeInfo]", route1, err) + } + throw err + }, + ) return { isPaused: resp.isPaused, @@ -262,24 +279,30 @@ const _getStacks2MetaFeeInfo = async ( const filteredRoute = filteredRoutes[0] if (filteredRoute == null) return - const feeDetails = await getSpecialFeeDetailsForSwapRoute(ctx, route, { + const specialFeeInfo = await getSpecialFeeDetailsForSwapRoute(ctx, route, { initialRoute: options.initialRoute, swapRoute: options.swapRoute, - }).then( - async (info): Promise => - info ?? - props({ - feeRate: filteredRoute.pegOutFeeRate, - minFeeAmount: BigNumber.ZERO, - gasFee: - filteredRoute.pegOutFeeBitcoinAmount == null - ? undefined - : props({ - token: KnownTokenId.Stacks.aBTC, - amount: filteredRoute.pegOutFeeBitcoinAmount, - }), - }), - ) + }) + if (ctx.debugLog) { + console.log("[getStacks2MetaFeeInfo/specialFeeInfo]", route, specialFeeInfo) + } + + const feeDetails = + specialFeeInfo ?? + (await props({ + feeRate: filteredRoute.pegOutFeeRate, + minFeeAmount: BigNumber.ZERO, + gasFee: + filteredRoute.pegOutFeeBitcoinAmount == null + ? undefined + : props({ + token: KnownTokenId.Stacks.aBTC, + amount: filteredRoute.pegOutFeeBitcoinAmount, + }), + })) + if (ctx.debugLog) { + console.log("[getStacks2MetaFeeInfo]", route, feeDetails) + } return { isPaused: filteredRoute.pegOutPaused, diff --git a/src/utils/SwapRouteHelpers.ts b/src/utils/SwapRouteHelpers.ts index f4bfdd8..0205e84 100644 --- a/src/utils/SwapRouteHelpers.ts +++ b/src/utils/SwapRouteHelpers.ts @@ -167,7 +167,7 @@ export async function getFinalStepStacksTokenAddress( export async function getAndCheckTransitStacksTokens( ctx: SDKGlobalContext, info: KnownRoute & { - swapRoute?: SwapRouteViaALEX | SwapRouteViaEVMDexAggregator + swapRoute?: SwapRoute }, ): Promise Promise export const memoizedIsSupportedFactory = ( @@ -279,7 +276,7 @@ export type CheckRouteValidFn = ( ctx: SDKGlobalContext, isSupported: IsSupportedFn, route: DefinedRoute & { - swapRoute?: SwapRouteViaALEX | SwapRouteViaEVMDexAggregator + swapRoute?: SwapRoute }, ) => Promise export const checkRouteValid: CheckRouteValidFn = async ( @@ -307,6 +304,7 @@ export interface GetSupportedRoutesFn_Conditions { fromToken?: TokenId toChain?: ChainId toToken?: TokenId + includeUnpredictableSwapPossibilities?: boolean } export type GetSupportedRoutesFn = ( diff --git a/src/utils/detectSupportedRoutes.ts b/src/utils/detectPossibleRoutes.ts similarity index 91% rename from src/utils/detectSupportedRoutes.ts rename to src/utils/detectPossibleRoutes.ts index f5932b5..29799af 100644 --- a/src/utils/detectSupportedRoutes.ts +++ b/src/utils/detectPossibleRoutes.ts @@ -5,9 +5,12 @@ import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal" import { KnownRoute } from "./buildSupportedRoutes" import { KnownChainId, KnownTokenId } from "./types/knownIds" -export async function detectSupportedRoutes( +export async function detectPossibleRoutes( ctx: SDKGlobalContext, - networkType: "mainnet" | "testnet", + conditions: { + networkType: "mainnet" | "testnet" + swapEnabled: boolean + }, ): Promise { type FetchedRoute = { baseStacksChain: KnownChainId.StacksChain @@ -16,7 +19,9 @@ export async function detectSupportedRoutes( pairedToken: KnownTokenId.KnownToken } - if (ctx.routes.detectedCache.get(networkType) !== null) { + const { networkType, swapEnabled } = conditions + + if (ctx.routes.detectedCache.get(networkType) != null) { return ctx.routes.detectedCache.get(networkType)! } @@ -120,7 +125,8 @@ export async function detectSupportedRoutes( const isSameBaseStacksToken = routeFrom.baseStacksChain === routeTo.baseStacksChain && routeFrom.baseStacksToken === routeTo.baseStacksToken - if (isSameRoute || !isSameBaseStacksToken) continue + if (isSameRoute) continue + if (!swapEnabled && !isSameBaseStacksToken) continue result.push({ fromChain: routeFrom.pairedTokenChain, fromToken: routeFrom.pairedToken, @@ -146,7 +152,8 @@ export async function detectSupportedRoutes( const isSameBaseStacksToken = routeFrom.baseStacksChain === routeTo.baseStacksChain && routeFrom.baseStacksToken === routeTo.baseStacksToken - if (isSameRoute || !isSameBaseStacksToken) continue + if (isSameRoute) continue + if (!swapEnabled && !isSameBaseStacksToken) continue result.push({ fromChain: routeFrom.pairedTokenChain, fromToken: routeFrom.pairedToken, @@ -172,7 +179,8 @@ export async function detectSupportedRoutes( const isSameBaseStacksToken = routeFrom.baseStacksChain === routeTo.baseStacksChain && routeFrom.baseStacksToken === routeTo.baseStacksToken - if (isSameRoute || !isSameBaseStacksToken) continue + if (isSameRoute) continue + if (!swapEnabled && !isSameBaseStacksToken) continue result.push({ fromChain: routeFrom.pairedTokenChain, fromToken: routeFrom.pairedToken, @@ -198,7 +206,8 @@ export async function detectSupportedRoutes( const isSameBaseStacksToken = routeFrom.baseStacksChain === routeTo.baseStacksChain && routeFrom.baseStacksToken === routeTo.baseStacksToken - if (isSameRoute || !isSameBaseStacksToken) continue + if (isSameRoute) continue + if (!swapEnabled && !isSameBaseStacksToken) continue result.push({ fromChain: routeFrom.pairedTokenChain, fromToken: routeFrom.pairedToken, diff --git a/src/xlinkSdkUtils/types.internal.ts b/src/xlinkSdkUtils/types.internal.ts index 8d83e54..835b6c5 100644 --- a/src/xlinkSdkUtils/types.internal.ts +++ b/src/xlinkSdkUtils/types.internal.ts @@ -29,6 +29,7 @@ export namespace withGlobalContextCache { } export interface SDKGlobalContext { + debugLog: boolean routes: { detectedCache: SDKGlobalContextCache<"mainnet" | "testnet", KnownRoute[]> }