feat(swapHelpers): add fetchKyberSwapPossibleRoutesFactory

This commit is contained in:
c4605
2025-03-23 17:31:28 +01:00
parent 40d0846ce7
commit bfe4754a0e
5 changed files with 315 additions and 3 deletions

View File

@@ -201,6 +201,10 @@ export {
fetchMatchaPossibleRoutesFactory,
FetchMatchaPossibleRoutesFailedError,
} from "./utils/swapHelpers/fetchDexAggregatorPossibleRoutes/fetchMatchaPossibleRoutes"
export {
fetchKyberSwapPossibleRoutesFactory,
FetchKyberSwapPossibleRoutesFailedError,
} from "./utils/swapHelpers/fetchDexAggregatorPossibleRoutes/fetchKyberSwapPossibleRoutes"
export interface DexAggregatorRoute
extends Omit<_DexAggregatorRoute, "fromAmount" | "toAmount" | "slippage"> {
fromAmount: SDKNumber

View File

@@ -19,6 +19,7 @@ export const fetchIceScreamSwapPossibleRoutesFactory = (options: {
batchSize?: number
baseUrl?: string
debug?: boolean
onError?: (error: FetchIceScreamSwapPossibleRoutesFailedError) => void
}): FetchRoutesImpl => {
const debugLog: typeof console.log = (...args) => {
if (!options.debug) return
@@ -40,10 +41,18 @@ export const fetchIceScreamSwapPossibleRoutesFactory = (options: {
res.push(
...(await Promise.all(
batch.map(route =>
fetchIceScreamSwapPossibleRouteImpl({ debugLog, baseUrl }, route),
fetchIceScreamSwapPossibleRouteImpl(
{ debugLog, baseUrl },
route,
).catch(e => {
options.onError?.(e)
return []
}),
),
)),
)
if (info.abortSignal?.aborted) break
}
return res.flat()

View File

@@ -0,0 +1,292 @@
import { toSDKNumberOrUndefined } from "../../../xlinkSdkUtils/types"
import { arraySplit } from "../../arrayHelpers"
import { BigNumber } from "../../BigNumber"
import { XLinkSDKErrorBase } from "../../errors"
import { checkNever } from "../../typeHelpers"
import { KnownChainId } from "../../types/knownIds"
import { FetchRoutesImpl, QueryableRoute } from "./helpers"
export class FetchKyberSwapPossibleRoutesFailedError extends XLinkSDKErrorBase {
constructor(message: null | string, options: ErrorConstructorOptions) {
super(message ?? "Request KyberSwap api failed", options)
}
}
export const fetchKyberSwapPossibleRoutesFactory = (options: {
/**
* The maximum number of routes to fetch in same time
*
* @default 1
*/
batchSize?: number
clientId?: string
debug?: boolean
onError?: (error: FetchKyberSwapPossibleRoutesFailedError) => void
}): FetchRoutesImpl => {
const debugLog: typeof console.log = (...args) => {
if (!options.debug) return
console.log("[fetchKyberSwapPossibleRoutesFactory]", ...args)
}
const baseUrl = "https://aggregator-api.kyberswap.com"
const batchSize = options.batchSize ?? 1
return async info => {
const batches = arraySplit(
(_, idx) => Math.floor(idx / batchSize),
info.possibleRoutes,
)
const res: Awaited<ReturnType<FetchRoutesImpl>>[] = []
for (const batch of batches) {
res.push(
...(await Promise.all(
batch.map(route =>
fetchKyberSwapPossibleRouteImpl(
{ debugLog, baseUrl, clientId: options.clientId },
route,
).catch(e => {
options.onError?.(e)
return []
}),
),
)),
)
if (info.abortSignal?.aborted) break
}
return res.flat()
}
}
const fetchKyberSwapPossibleRouteImpl = async (
context: {
debugLog: typeof console.log
baseUrl: string
clientId?: string
},
info: QueryableRoute,
): ReturnType<FetchRoutesImpl> => {
if (BigNumber.isZero(info.amount)) {
context.debugLog("Because of amount is 0, skipping...")
return []
}
const kyberChainId = mapSDKChainIdToKyberChainId(info.chain.chain)
if (!kyberChainId) {
context.debugLog(
`Because of chain ${info.chain.chain} is not supported, skipping...`,
)
return []
}
const querystring = new URLSearchParams({
tokenIn: info.fromEVMToken.address,
tokenOut: info.toEVMToken.address,
amountIn: String(
BigNumber.toBigInt(
{ roundingMode: BigNumber.roundDown },
BigNumber.rightMoveDecimals(info.fromEVMToken.decimals, info.amount),
),
),
})
const fetchUrl =
`${context.baseUrl}/${kyberChainId}/api/v1/routes?${querystring.toString()}`.replace(
/^\/+/,
"/",
)
context.debugLog("fetchUrl", fetchUrl)
const resp = await fetch(fetchUrl, {
headers: {
...(context.clientId ? { "x-client-id": context.clientId } : {}),
},
})
if (!resp.ok) {
context.debugLog("Request failed:", resp)
try {
const respText = await resp.text()
context.debugLog("Response text:", respText)
throw new FetchKyberSwapPossibleRoutesFailedError(null, {
cause: {
response: resp,
data: respText,
},
})
} catch {
throw new FetchKyberSwapPossibleRoutesFailedError(null, {
cause: {
response: resp,
},
})
}
}
const respText = await resp.text()
context.debugLog("Request succeed:", respText)
let respData: MockData
try {
respData = JSON.parse(respText)
} catch (e) {
return []
}
if (respData.code !== 0) {
throw new FetchKyberSwapPossibleRoutesFailedError(
(respData as any).message,
{
cause: {
response: resp,
data: respText,
},
},
)
}
return [
{
provider: "KyberSwap",
evmChain: info.chain.chain,
fromToken: info.fromEVMToken.token,
toToken: info.toEVMToken.token,
fromAmount: info.amount,
toAmount: toSDKNumberOrUndefined(
BigNumber.leftMoveDecimals(
info.toEVMToken.decimals,
respData.data.routeSummary.amountOut,
),
),
slippage: info.slippage,
},
]
}
function mapSDKChainIdToKyberChainId(
chainId: KnownChainId.EVMChain,
): undefined | string {
/**
* https://docs.kyberswap.com/getting-started/supported-exchanges-and-networks
*/
switch (chainId) {
case KnownChainId.EVM.Ethereum:
return "ethereum"
case KnownChainId.EVM.BSC:
return "bsc"
case KnownChainId.EVM.Arbitrum:
return "arbitrum"
case KnownChainId.EVM.Base:
return "base"
case KnownChainId.EVM.Linea:
return "linea"
case KnownChainId.EVM.Sepolia:
case KnownChainId.EVM.BSCTestnet:
case KnownChainId.EVM.CoreDAOTestnet:
case KnownChainId.EVM.BlifeTestnet:
case KnownChainId.EVM.BitboyTestnet:
case KnownChainId.EVM.BeraTestnet:
case KnownChainId.EVM.CoreDAO:
case KnownChainId.EVM.Bsquared:
case KnownChainId.EVM.BOB:
case KnownChainId.EVM.Bitlayer:
case KnownChainId.EVM.Lorenzo:
case KnownChainId.EVM.Merlin:
case KnownChainId.EVM.AILayer:
case KnownChainId.EVM.Mode:
case KnownChainId.EVM.XLayer:
case KnownChainId.EVM.Aurora:
case KnownChainId.EVM.Manta:
return undefined
default:
checkNever(chainId)
return undefined
}
}
type MockData = typeof mockData
const mockData = {
code: 0 as const,
message: "successfully",
data: {
routeSummary: {
tokenIn: "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
amountIn: "1000000000000000000" as `${number}`,
amountInUsd: "1668.95" as `${number}`,
tokenInMarketPriceAvailable: false,
tokenOut: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
amountOut: "1666243758" as `${number}`,
amountOutUsd: "1665.9071767608839" as `${number}`,
tokenOutMarketPriceAvailable: false,
gas: "253000" as `${number}`,
gasPrice: "181968304449" as `${number}`,
gasUsd: "0.06491355324609177" as `${number}`,
extraFee: {
feeAmount: "10" as `${number}`,
chargeFeeBy: "currency_out",
isInBps: true,
feeReceiver: "0x0513c794bC2c65C6f374a86D6ad04425e32Df22e",
},
route: [
[
{
pool: "0x4b543e89351faa242cb0172b2da0cdb52db699b4",
tokenIn: "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
tokenOut: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
limitReturnAmount: "0",
swapAmount: "1000000000000000000",
amountOut: "1667911669",
exchange: "dodo",
poolLength: 2,
poolType: "dodo",
poolExtra: {
type: "DPP",
dodoV1SellHelper: "0xdfaf9584f5d229a9dbe5978523317820a8897c5a",
baseToken: "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619",
quoteToken: "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
},
extra: {
amountIn: "1000000000000000000",
filledOrders: [
{
allowedSenders: "0x0000000000000000000000000000000000000000",
feeAmount: "0",
feeRecipient: "0x0000000000000000000000000000000000000000",
filledMakingAmount: "950000",
filledTakingAmount: "1000000000000000000",
getMakerAmount:
"f4a215c30000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000111e75953102eec1a0",
getTakerAmount:
"296637bf0000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000111e75953102eec1a0",
interaction: "",
isFallback: false,
maker: "0xda060fd9ae5b23cebf8abcb2d19fab152a419d61",
makerAsset: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
makerAssetData: "",
makerTokenFeePercent: 0,
makingAmount: "300000000",
orderId: 9886,
permit: "",
predicate:
"961d5b1e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000227b0c196ea8db17a665ea6824d972a64202e936000000000000000000000000227b0c196ea8db17a665ea6824d972a64202e9360000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000044cf6fc6e3000000000000000000000000da060fd9ae5b23cebf8abcb2d19fab152a419d61000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002463592c2b000000000000000000000000000000000000000000000000000000006453683300000000000000000000000000000000000000000000000000000000",
receiver: "0xda060fd9ae5b23cebf8abcb2d19fab152a419d61",
salt: "202362243813858115557509104206720377774",
signature:
"8fb37c9b14d9ccd7709ccc8289860c24580b69f1ab0e905a7d8c20e2ae5e45c570d33324990afb94a445246872545c5eaf9712b164a90ac7f97502d91a7c27001b",
takerAsset: "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270",
takerAssetData: "",
takingAmount: "315789473684210500000",
},
],
swapSide: "BUY",
},
},
],
],
},
routerAddress: "0x6131B5fae19EA4f9D964eAc0408E4408b66337b5",
},
}

View File

@@ -20,6 +20,7 @@ export const fetchMatchaPossibleRoutesFactory = (options: {
batchSize?: number
baseUrl?: string
debug?: boolean
onError?: (error: FetchMatchaPossibleRoutesFailedError) => void
}): FetchRoutesImpl => {
const debugLog: typeof console.log = (...args) => {
if (!options.debug) return
@@ -44,10 +45,15 @@ export const fetchMatchaPossibleRoutesFactory = (options: {
fetchMatchaPossibleRouteImpl(
{ debugLog, baseUrl, apiKey: options.apiKey },
route,
),
).catch(e => {
options.onError?.(e)
return []
}),
),
)),
)
if (info.abortSignal?.aborted) break
}
return res.flat()

View File

@@ -32,7 +32,7 @@ export interface QueryableRoute {
}
export interface DexAggregatorRoute {
provider: "IceCreamSwap" | "Matcha"
provider: "IceCreamSwap" | "Matcha" | "KyberSwap"
evmChain: KnownChainId.EVMChain
fromToken: KnownTokenId.EVMToken
toToken: KnownTokenId.EVMToken
@@ -43,6 +43,7 @@ export interface DexAggregatorRoute {
export type FetchRoutesImpl = (info: {
possibleRoutes: QueryableRoute[]
abortSignal?: AbortSignal
}) => Promise<DexAggregatorRoute[]>
export async function getQueryableRoutes(