kirapay-axelar-sdk
Version:
TypeScript SDK for cross-chain swaps with CCIP and Axelar bridges
540 lines (539 loc) • 23.1 kB
JavaScript
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,
};
}
}