feat(XLinkSDK): returns swap-possible routes

This commit is contained in:
c4605
2025-04-07 21:02:55 +02:00
parent 9a1c650359
commit e61cb73bed
9 changed files with 240 additions and 65 deletions

View File

@@ -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<void> {
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<string, KnownRoute[]> = {}
for (const route of supportedRoutes) {
@@ -42,11 +68,15 @@ async function print(matchers: {
async function main(command: string[], args: string[]): Promise<void> {
if (args.some(a => a === "-h" || a === "--help")) {
console.log(`Usage: ${command.join(" ")} [chain:<chain>] [token:<token>]`)
console.log(
`Usage: ${command.join(" ")} [op:debug] [op:swap] [chain:<chain>] [token:<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[],
}

View File

@@ -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<KnownRoute[]> {
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<KnownRoute[]> {
const specifiedChain = conditions?.fromChain ?? conditions?.toChain
@@ -318,28 +336,55 @@ export class XLinkSDK {
let resultRoutesPromise: Promise<KnownRoute[]>
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<boolean> {
/**
* 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<boolean> {
const checkingResult = await Promise.all([
isSupportedEVMRoute(this.sdkContext, route),
isSupportedStacksRoute(this.sdkContext, route),

View File

@@ -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<SwapRoute, "via">
@@ -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,

View File

@@ -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

View File

@@ -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<undefined | TransferProphet> => {
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<SwapRoute, "via">
@@ -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<SpecialFeeDetailsForSwapRoute> =>
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,

View File

@@ -167,7 +167,7 @@ export async function getFinalStepStacksTokenAddress(
export async function getAndCheckTransitStacksTokens(
ctx: SDKGlobalContext,
info: KnownRoute & {
swapRoute?: SwapRouteViaALEX | SwapRouteViaEVMDexAggregator
swapRoute?: SwapRoute
},
): Promise<null | {
firstStepToStacksToken: KnownTokenId.StacksToken

View File

@@ -1,13 +1,10 @@
import { ChainId, TokenId } from "../xlinkSdkUtils/types"
import {
SwapRouteViaALEX,
SwapRouteViaEVMDexAggregator,
} from "./SwapRouteHelpers"
import { SDKGlobalContext } from "../xlinkSdkUtils/types.internal"
import { UnsupportedBridgeRouteError } from "./errors"
import { pMemoize } from "./pMemoize"
import { KnownChainId, KnownTokenId } from "./types/knownIds"
import { SwapRoute } from "./SwapRouteHelpers"
import { checkNever } from "./typeHelpers"
import { KnownChainId, KnownTokenId } from "./types/knownIds"
export interface DefinedRoute {
fromChain: ChainId
@@ -245,7 +242,7 @@ export function defineRoute(
export type IsSupportedFn = (
ctx: SDKGlobalContext,
route: DefinedRoute & {
swapRoute?: SwapRouteViaALEX | SwapRouteViaEVMDexAggregator
swapRoute?: SwapRoute
},
) => Promise<boolean>
export const memoizedIsSupportedFactory = (
@@ -279,7 +276,7 @@ export type CheckRouteValidFn = (
ctx: SDKGlobalContext,
isSupported: IsSupportedFn,
route: DefinedRoute & {
swapRoute?: SwapRouteViaALEX | SwapRouteViaEVMDexAggregator
swapRoute?: SwapRoute
},
) => Promise<KnownRoute>
export const checkRouteValid: CheckRouteValidFn = async (
@@ -307,6 +304,7 @@ export interface GetSupportedRoutesFn_Conditions {
fromToken?: TokenId
toChain?: ChainId
toToken?: TokenId
includeUnpredictableSwapPossibilities?: boolean
}
export type GetSupportedRoutesFn = (

View File

@@ -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<KnownRoute[]> {
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,

View File

@@ -29,6 +29,7 @@ export namespace withGlobalContextCache {
}
export interface SDKGlobalContext {
debugLog: boolean
routes: {
detectedCache: SDKGlobalContextCache<"mainnet" | "testnet", KnownRoute[]>
}