UNPKG

kirapay-axelar-sdk

Version:

TypeScript SDK for cross-chain swaps with CCIP and Axelar bridges

540 lines (539 loc) 23.1 kB
import { RouteNotFoundError, } from "./types"; import { ConfigManager } from "./config"; import { BridgeService } from "../bridges/BridgeService"; import { DexService } from "../dex/DexService"; import { GasService } from "../gas/GasService"; import { TrackingService } from "../tracking/TrackingService"; import { zeroAddress, encodeFunctionData } from "viem"; import { USDC_BY_CHAIN, AXELAR_WRAPPED_TOKENS_BY_CHAIN, WETH_BY_CHAIN, UNIFIED_CCIP_BRIDGE_WITH_SWAPS_PROXY, DEFAULT_CCIP_CONFIG, WRAPPED_NATIVE_BY_CHAIN, } from "../config"; import { UNIFIED_AXELAR_BRIDGE_WITH_SWAPS_PROXY, AXELAR_CHAIN_NAME_BY_ID, } from "../config"; import { UNIFIED_BRIDGE_WITH_SWAPS_ABI } from "./abis"; import { createDebugger } from "../status"; import { randomBytes } from "crypto"; /** * KiraSdk is the main entry point to the SDK * It provides a clean API for cross-chain swaps using Smart Order Router */ export class KiraSdk { debugLog(...args) { this.logger(...args); } /** * Create a new KiraSdk instance * @param config SDK configuration */ constructor(config = {}) { this.logger = createDebugger("KiraSdk"); this.configManager = new ConfigManager(config); this.gasService = new GasService(); this.bridgeService = new BridgeService(this.configManager, this.gasService); this.dexService = new DexService(this.configManager); this.trackingService = new TrackingService(this.configManager, this.bridgeService); } /** * Get the proxy contract address for a chain pair * @param srcChainId Source chain ID * @param dstChainId Destination chain ID * @param bridge Bridge type ("AXELAR" or "CCIP") * @returns Proxy contract address */ getBridgeContract(srcChainId, dstChainId, bridge) { let srcProxy; let dstProxy; if (bridge === "AXELAR") { srcProxy = UNIFIED_AXELAR_BRIDGE_WITH_SWAPS_PROXY[srcChainId]; dstProxy = UNIFIED_AXELAR_BRIDGE_WITH_SWAPS_PROXY[dstChainId]; } else { srcProxy = UNIFIED_CCIP_BRIDGE_WITH_SWAPS_PROXY[srcChainId]; dstProxy = UNIFIED_CCIP_BRIDGE_WITH_SWAPS_PROXY[dstChainId]; } if (!srcProxy || !dstProxy || srcProxy === "0x0000000000000000000000000000000000000000" || dstProxy === "0x0000000000000000000000000000000000000000") { throw new Error(`Proxy bridge not available for chain pair ${srcChainId} to ${dstChainId}`); } return { srcContract: srcProxy, dstContract: dstProxy, }; } /** * Get a default swap deadline based on current time and configured window. */ getSwapDeadline() { const seconds = this.configManager.getDefaultSwapDeadlineSeconds(); return BigInt(Math.floor(Date.now() / 1000) + seconds); } /** * Generate a random 32-byte routeId as 0x-prefixed hex string. */ generateRouteId() { return `0x${randomBytes(32).toString("hex")}`; } /** * Get a route for a cross-chain swap using Smart Order Router * @param input Quote input parameters * @returns Route quote */ async getRoute(input) { this.debugLog("getRoute input", { srcChainId: input.srcChainId, dstChainId: input.dstChainId, tokenIn: input.tokenIn, tokenOut: input.tokenOut, amountIn: input.amountIn, preferredBridge: input.preferredBridge, }); const handledInput = { ...input, maxSlippageBps: input.maxSlippageBps ?? 5000, ttlSeconds: input.ttlSeconds ?? this.configManager.getDefaultTtlSeconds(), preferredBridge: input.preferredBridge ?? "CCIP", }; // Validate inputs and get RPCs const { rpcSrc, rpcDst } = this.validateAndGetRpcs(handledInput); const bridgeContract = this.validateAndGetBridgeContract(handledInput); // Determine bridge asset const bridgeAsset = this.determineBridgeAsset(handledInput); // Get source leg quote using Smart Order Router const sourceLeg = await this.quoteSourceLeg(handledInput, bridgeAsset.srcBridgeAsset, rpcSrc); // Get destination leg quote using Smart Order Router const destinationLeg = await this.quoteDestinationLeg(handledInput, bridgeAsset.dstBridgeAsset, sourceLeg.minAmountOut, rpcDst); return { srcDex: sourceLeg, dstDex: destinationLeg, bridgeAsset: bridgeAsset.srcBridgeAsset, bridgeContract, dstDexGasEstimate: destinationLeg?.gasEstimate ?? 0n, }; } /** * Validate if the bridge is supported for the chain pair */ validateAndGetBridgeContract(input) { if (!this.bridgeService.isBridgeSupported(input.srcChainId, input.dstChainId, input.preferredBridge)) { throw new Error(`Bridge ${input.preferredBridge} not supported for chain pair ${input.srcChainId} to ${input.dstChainId}`); } const bridgeContract = this.getBridgeContract(input.srcChainId, input.dstChainId, input.preferredBridge); return bridgeContract; } /** * Validate inputs and get RPC URLs */ validateAndGetRpcs(input) { const rpcSrc = this.configManager.getRpcUrl(input.srcChainId); const rpcDst = this.configManager.getRpcUrl(input.dstChainId); if (!rpcSrc || !rpcDst) { throw new Error("Missing RPCs for source or destination chain. Make sure the chain is supported."); } return { rpcSrc, rpcDst }; } /** * Determine the appropriate bridge asset based on input and bridge preference */ determineBridgeAsset(input) { const preferredBridge = input.preferredBridge ?? "CCIP"; let bridgeAsset; if (preferredBridge === "AXELAR") { bridgeAsset = this.getAxelarBridgeAssetForChain(input.srcChainId, input.dstChainId); } else { bridgeAsset = this.getCcipBridgeAssetForChain(input.srcChainId, input.dstChainId); } this.debugLog("Final bridge asset after bridge-specific override", bridgeAsset); if (!bridgeAsset) { throw new Error("No viable bridge asset found for source chain"); } return { srcBridgeAsset: bridgeAsset.srcBridgeAsset, dstBridgeAsset: bridgeAsset.dstBridgeAsset, }; } /** * Quote the source leg (tokenIn -> bridge asset) using Smart Order Router */ async quoteSourceLeg(input, bridgeAsset, rpcSrc) { if (input.tokenIn.address.toLowerCase() === bridgeAsset.address.toLowerCase()) { // No swap needed const amountOut = input.amountIn; this.debugLog("Source leg: no swap needed, already bridge asset", bridgeAsset); return { kind: "V3", gasEstimate: 0n, path: [bridgeAsset.address], feeTiers: [], poolParams: [], amountIn: input.amountIn, minAmountOut: amountOut, }; } // Get candidate assets for source leg const candidateAssets = this.getSourceLegCandidates(input, bridgeAsset); // Quote all candidates using Smart Order Router const candidateQuotes = await this.quoteSourceCandidates(input, candidateAssets, rpcSrc); // Find best viable quote const viable = candidateQuotes.filter((r) => r && r.quote && r.quote.value > 0n); if (!viable.length) { throw new RouteNotFoundError("SRC", `Quoted amount is zero for ${input.tokenIn.symbol} -> any bridge asset candidate. Check if pools exist and have liquidity.`, { candidates: candidateAssets.map((c) => ({ symbol: c.symbol, address: c.address, })), }); } // Pick best quote const best = viable.reduce((a, b) => b.quote.value > a.quote.value ? b : a); this.debugLog("Source best route", { candidate: { symbol: best.candidate.symbol, address: best.candidate.address, }, out: best.quote.value.toString(), version: best.quote.route.version, path: best.quote.route.path, fees: best.quote.route.fees, poolParams: best.quote.route.poolParams, }); return { gasEstimate: 0n, // gasEstimate: best.quote!.route.gasEstimate, kind: best.quote.route.version, path: best.quote.route.path, feeTiers: best.quote.route.fees, amountIn: input.amountIn, poolParams: best.quote.route.poolParams, minAmountOut: best.quote.value, // No slippage calculation needed - handled by Smart Order Router }; } /** * Get candidate assets for source leg */ getSourceLegCandidates(input, bridgeAsset) { return [bridgeAsset]; // const preferredBridge = input.preferredBridge; // const candidates = ( // preferredBridge === "AXELAR" // ? this.getAxelarSourceBridgeCandidates(input.srcChainId) // : [bridgeAsset] // ).filter((t) => !(preferredBridge === "AXELAR" && t.symbol === "USDC")); // return candidates; } /** * Quote all source leg candidates using Smart Order Router */ async quoteSourceCandidates(input, candidates, rpcSrc) { const buildQuotes = async (candidate) => { this.debugLog("Building quotes for candidate", { symbol: candidate.symbol, address: candidate.address, }); try { // Use Smart Order Router to find optimal route with slippage const quote = await this.dexService.quoteExactIn({ rpc: rpcSrc, chainId: input.srcChainId, tokenIn: input.tokenIn.address, tokenOut: candidate.address, amountIn: input.amountIn, isSrcSwap: true, }, input.maxSlippageBps || 100); // Use maxSlippageBps from input, default to 1% this.debugLog("Smart Order Router quote result", { candidate: candidate.symbol, value: quote.value.toString(), version: quote.route.version, path: quote.route.path, fees: quote.route.fees, poolParams: quote.route.poolParams, }); return { candidate, quote }; } catch (error) { this.debugLog("Smart Order Router quote failed", { candidate: candidate.symbol, error: error.shortMessage, }); return { candidate, quote: null }; } }; return Promise.all(candidates.map(buildQuotes)); } /** * Quote the destination leg (bridge asset -> tokenOut) using Smart Order Router */ async quoteDestinationLeg(input, dstBridgeAsset, destinationBridgeAssetAmount, rpcDst) { // Resolve destination bridge asset representation this.debugLog("Destination leg routing started", { bridgeAsset: dstBridgeAsset.symbol, outputToken: input.tokenOut.symbol, bridgeAssetAmount: destinationBridgeAssetAmount.toString(), }); // Check if destination swap is needed if (input.tokenOut.address.toLowerCase() === dstBridgeAsset.address.toLowerCase()) { this.debugLog("Destination leg: no swap needed, output is already bridge asset"); return undefined; // No swap needed } try { // Use Smart Order Router to find optimal route with slippage const quote = await this.dexService.quoteExactIn({ rpc: rpcDst, chainId: input.dstChainId, tokenIn: dstBridgeAsset.address, tokenOut: input.tokenOut.address, amountIn: destinationBridgeAssetAmount, }, input.maxSlippageBps); // Use maxSlippageBps from input, default to 1% if (quote.value > 0n) { this.debugLog("Destination leg quote successful", { value: quote.value.toString(), version: quote.route.version, path: quote.route.path, fees: quote.route.fees, }); return { kind: quote.route.version, path: quote.route.path, poolParams: quote.route.poolParams, feeTiers: quote.route.fees, amountIn: destinationBridgeAssetAmount, minAmountOut: quote.value, // No slippage calculation needed - handled by Smart Order Router gasEstimate: quote.route.gasEstimate, }; } else { throw new RouteNotFoundError("DST", `Quoted amount is zero for ${dstBridgeAsset.symbol} -> ${input.tokenOut.symbol}`, {}); } } catch (e) { this.debugLog("Destination quote error", e); if (e instanceof RouteNotFoundError) throw e; throw new RouteNotFoundError("DST", `Failed to quote destination swap for ${dstBridgeAsset.symbol} -> ${input.tokenOut.symbol}`, { cause: e?.message }); } } /** * Estimate bridge fee */ async estimateBridgeFee(input, bridgeAsset, sourceLeg, destinationLeg, dstDexGasEstimate) { // Make sure bridge is supported const preferredBridge = input.preferredBridge; // Create payload for fee estimation // Estimate fee const gasLimit = dstDexGasEstimate ?? 600000n; // Default gas limit swap at destination const bridgeFee = await this.bridgeService.estimateFee({ srcChainId: input.srcChainId, dstChainId: input.dstChainId, bridgeAsset: bridgeAsset.address, bridgeAssetAmount: sourceLeg.minAmountOut, recipient: input.recipient, gasLimit, }, preferredBridge); this.debugLog("Bridge fee", bridgeFee); return bridgeFee; } /** * Build a cross-chain swap transaction * @param input Quote input parameters * @param precomputedRoute Optional precomputed route to avoid recomputation * @returns Prepared transaction data */ async build(input, precomputedRoute) { // Get route const route = precomputedRoute ?? (await this.getRoute(input)); // Get bridge asset const bridgeAsset = route.bridgeAsset; const bridgeFee = await this.estimateBridgeFee(input, bridgeAsset, route.srcDex, route.dstDex, route.dstDexGasEstimate); // Get proxy addresses from config const bridgeContract = route.bridgeContract.srcContract; const destinationContract = route.bridgeContract.dstContract; const swapDeadline = this.getSwapDeadline(); const routeId = this.generateRouteId(); const built = this.buildUnifiedSwapParams({ input, prepared: { route, deadline: swapDeadline, routeId, bridgeFee, }, destinationChainName: AXELAR_CHAIN_NAME_BY_ID[input.dstChainId], destinationContract, }); // Encode function data using provided ABI to avoid ABI drift const data = encodeFunctionData({ abi: UNIFIED_BRIDGE_WITH_SWAPS_ABI, functionName: "bridgeWithSwap", args: [built.params], }); return { to: bridgeContract, data, value: built.nativeValueForNativeFlow, metadata: { params: built.params, bridgeFee: bridgeFee, }, }; } /** * Get the CCIP bridge asset for a chain * Falls back to alternative assets like WETH if USDC is not available * * @param chainId The chain ID * @returns The best bridge asset for the chain, or undefined if not found */ getCcipBridgeAssetForChain(srcChainId, dstChainId) { // First try to get USDC for this chain const srcUsdc = USDC_BY_CHAIN[srcChainId]; const dstUsdc = USDC_BY_CHAIN[dstChainId]; if (srcUsdc && dstUsdc) { return { srcBridgeAsset: srcUsdc, dstBridgeAsset: dstUsdc, }; } const srcWeth = WETH_BY_CHAIN[srcChainId]; const dstWeth = WETH_BY_CHAIN[dstChainId]; if (srcWeth && dstWeth) { return { srcBridgeAsset: srcWeth, dstBridgeAsset: dstWeth, }; } return undefined; } /** * Get the CCIP bridge asset for a chain * Falls back to alternative assets like WETH if USDC is not available * * @param chainId The chain ID * @returns The best bridge asset for the chain, or undefined if not found */ getAxelarBridgeAssetForChain(srcChainId, dstChainId) { const axlOnSrc = AXELAR_WRAPPED_TOKENS_BY_CHAIN[srcChainId]; const axlOnDst = AXELAR_WRAPPED_TOKENS_BY_CHAIN[dstChainId]; if (axlOnSrc?.usdc && axlOnDst?.usdc) { return { srcBridgeAsset: axlOnSrc.usdc, dstBridgeAsset: axlOnDst.usdc, }; } if (axlOnSrc?.weth && axlOnDst?.weth) { return { srcBridgeAsset: axlOnSrc.weth, dstBridgeAsset: axlOnDst.weth, }; } return undefined; } /** * Track a transaction through the entire cross-chain flow * @param txHash Source transaction hash * @param srcChainId Source chain ID * @param dstChainId Destination chain ID * @param options Tracking options * @returns An object with methods to stop tracking and get status */ trackTransaction(txHash, srcChainId, dstChainId, options) { return this.trackingService.trackTransaction(txHash, srcChainId, dstChainId, options); } /** * Track a cross-chain message directly * @param messageId Message ID for CCIP or Axelar * @param srcChainId Source chain ID * @param dstChainId Destination chain ID * @param bridge Bridge type * @param options Tracking options * @returns An object with methods to stop tracking and get status */ trackCrossChainMessage(messageId, srcChainId, dstChainId, bridge, options) { return this.trackingService.trackCrossChainMessage(messageId, srcChainId, dstChainId, bridge, options); } /** * Track token transfers on the destination chain * Useful for situations where direct message tracking isn't possible * @param tokenAddress Token address to track * @param recipient Recipient address * @param chainId Chain ID * @param options Tracking options * @returns An object with methods to stop tracking and get status */ trackTokenTransfer(tokenAddress, recipient, chainId, options) { return this.trackingService.trackTokenTransfer(tokenAddress, recipient, chainId, options); } mapUniswapPoolVersion(version) { if (version === "V2") return 2; if (version === "V4") return 1; return 0; // V3 } /** * Build a cross-chain swap transaction for unified bridge contracts * This method only requires approval to the proxy contract, which handles all swaps internally * @param input Quote input parameters * @returns Prepared transaction data */ buildUnifiedSwapParams(params) { const { input, prepared, destinationChainName, destinationContract } = params; const srcPath = [...prepared.route.srcDex.path]; // if (tokenInIsNative && srcPath.length > 0) // srcPath[0] = getAddress("0x0000000000000000000000000000000000000000"); const preferredBridge = input.preferredBridge; // if (preferredBridge === "AXELAR" && srcPath.length >= 1) { // const lastIdx = srcPath.length - 1; // const axl = // AXELAR_WRAPPED_TOKENS_BY_CHAIN[input.srcChainId]?.usdc?.address; // if (axl && srcPath[lastIdx].toLowerCase() !== axl.toLowerCase()) // srcPath[lastIdx] = axl as Address; // } // const srcFees: number[] = prepared.route.srcDex.feeTiers || []; const srcPoolParams = prepared.route.srcDex.poolParams || []; const destPoolParams = prepared.route.dstDex?.poolParams || []; // while (srcFees.length < Math.max(0, srcPath.length - 1)) srcFees.push(500); // if (srcFees.length >= 1 && preferredBridge === "AXELAR") // srcFees[srcFees.length - 1] = 100; const destPath = (prepared.route.dstDex?.path || []); const destFees = (prepared.route.dstDex?.feeTiers || []); const shouldSwapOnDest = Boolean(destPath.length > 1); const srcSwapVersion = this.mapUniswapPoolVersion(prepared.route.srcDex.kind); const destSwapVersion = this.mapUniswapPoolVersion(prepared.route.dstDex?.kind || ""); const minBridgeAmount = 0n; const minAmountOut = 0n; // prepared.route.dstDex?.minAmountOut ?? 0n; const deadline = prepared.deadline; const routeId = prepared.routeId; const tokenInIsNative = input.tokenIn.address.toLowerCase() === zeroAddress || input.tokenIn.address.toLowerCase() === WRAPPED_NATIVE_BY_CHAIN[input.srcChainId].address.toLowerCase(); // const multiplier = preferredBridge === "AXELAR" ? 50n : 12n; const bridgeFeeNative = prepared.bridgeFee.feeAmount; const nativeValueForNativeFlow = (tokenInIsNative ? input.amountIn : 0n) + prepared.bridgeFee.feeAmount; return { params: { destinationChain: destinationChainName, destinationChainSelector: preferredBridge === "AXELAR" ? 0n : DEFAULT_CCIP_CONFIG[input.dstChainId].selector, destinationContract: destinationContract.toLowerCase(), recipient: input.recipient, routeId, deadline, srcPath, srcPoolParams, srcSwapVersion, inputAmount: input.amountIn, minBridgeAmount, shouldSwapOnDest, destPath, destPoolParams, destSwapVersion, minAmountOut, }, bridgeFeeNative, nativeValueForNativeFlow, }; } }