交换示例
一个使用 ethers.js v6 在 Kroko DEX 上执行代币交换的最小但完整的示例。完整代码
复制
询问AI
import { ethers } from 'ethers';
// === 配置 ===
const RPC_URL = 'https://evmrpc.kasplex.org';
const API_BASE = 'https://dex.kasplex.org/swap-api';
const PERMIT2 = '0x2E1987F680FD7Bc8B33d3Bf94f12B988A0B50034';
const UNIVERSAL_ROUTER = '0xefeCc1c2dE3BfE4C6D43030F2AcDD5C3cE279024';
// === ABIs ===
const ERC20_ABI = [
'function allowance(address,address) view returns (uint256)',
'function approve(address,uint256) returns (bool)',
'function balanceOf(address) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
];
const PERMIT2_ABI = [
'function approve(address token, address spender, uint160 amount, uint48 expiration)',
'function allowance(address owner, address token, address spender) view returns (uint160, uint48, uint48)',
];
// === 交易类型 ===
enum TradeType {
EXACT_INPUT = 0,
EXACT_OUTPUT = 1,
}
/**
* 在 Kroko DEX 上执行完整的交换。
*
* @param signer - 已连接的 ethers Signer
* @param tokenIn - 输入代币地址(原生 KAS 使用 WKAS 地址)
* @param tokenOut - 输出代币地址
* @param amount - 原始单位的金额(EXACT_INPUT 为 amountIn,EXACT_OUTPUT 为 amountOut)
* @param tradeType - 0 为精确输入,1 为精确输出
* @param slippage - 滑点容差百分比(例如 0.5 表示 0.5%)
*/
async function executeSwap(
signer: ethers.Signer,
tokenIn: string,
tokenOut: string,
amount: string,
tradeType: TradeType = TradeType.EXACT_INPUT,
slippage: number = 0.5
): Promise<ethers.TransactionReceipt> {
const userAddress = await signer.getAddress();
const token = new ethers.Contract(tokenIn, ERC20_ABI, signer);
const permit2 = new ethers.Contract(PERMIT2, PERMIT2_ABI, signer);
// --- 步骤 1:授权代币 → Permit2 ---
const tokenAllowance = await token.allowance(userAddress, PERMIT2);
if (tokenAllowance < BigInt(amount)) {
console.log('步骤 1:正在授权代币给 Permit2...');
const tx = await token.approve(PERMIT2, ethers.MaxUint256);
await tx.wait();
console.log(' 完成。');
} else {
console.log('步骤 1:代币已授权给 Permit2。');
}
// --- 步骤 2:Permit2 → 授权 Universal Router ---
const [p2Amount, p2Expiration] = await permit2.allowance(
userAddress,
tokenIn,
UNIVERSAL_ROUTER
);
const now = Math.floor(Date.now() / 1000);
if (p2Amount < BigInt(amount) || Number(p2Expiration) < now) {
console.log('步骤 2:正在通过 Permit2 授权 Universal Router...');
const tx = await permit2.approve(
tokenIn,
UNIVERSAL_ROUTER,
ethers.MaxUint160,
now + 365 * 24 * 60 * 60
);
await tx.wait();
console.log(' 完成。');
} else {
console.log('步骤 2:Universal Router 已通过 Permit2 授权。');
}
// --- 步骤 3:获取报价 ---
console.log('步骤 3:正在获取报价...');
const quoteParams = new URLSearchParams({
tokenIn,
tokenOut,
tradeType: tradeType.toString(),
});
if (tradeType === TradeType.EXACT_INPUT) {
quoteParams.set('amountIn', amount);
} else {
quoteParams.set('amountOut', amount);
}
const quoteRes = await fetch(`${API_BASE}/api/v1/quote?${quoteParams}`);
const quote = await quoteRes.json();
if (quote.error) throw new Error(`报价失败:${quote.error}`);
console.log(` 路由:${quote.route.protocol}(${quote.route.hops} 跳)`);
console.log(` 输入金额:${quote.amountIn}`);
console.log(` 输出金额:${quote.amountOut}`);
console.log(` 价格影响:${quote.priceImpact}%`);
// --- 步骤 4:获取交换 calldata ---
console.log('步骤 4:正在生成 calldata...');
const swapBody: Record<string, unknown> = {
tokenIn,
tokenOut,
tradeType,
slippage,
recipient: userAddress,
deadline: 1200,
};
if (tradeType === TradeType.EXACT_INPUT) {
swapBody.amountIn = amount;
} else {
swapBody.amountOut = amount;
}
const swapRes = await fetch(`${API_BASE}/api/v1/swap`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(swapBody),
});
const swapData = await swapRes.json();
if (swapData.error) throw new Error(`Swap API 失败:${swapData.error}`);
if (tradeType === TradeType.EXACT_INPUT) {
console.log(` 最小产出:${swapData.quote.minAmountOut}`);
} else {
console.log(` 最大输入:${swapData.quote.maxAmountIn}`);
}
// --- 步骤 5:执行 ---
console.log('步骤 5:正在发送交易...');
const tx = await signer.sendTransaction({
to: swapData.to,
data: swapData.data,
value: swapData.value,
});
console.log(` 交易哈希:${tx.hash}`);
const receipt = await tx.wait();
console.log(` 已在区块 ${receipt!.blockNumber} 中确认`);
return receipt!;
}
// === 使用示例 ===
async function main() {
// 连接钱包(此处使用私钥示例 — 生产环境请使用安全的方式)
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);
const TOKEN_A = '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630';
const TOKEN_B = '0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220';
// 精确输入:卖出 1 个 TOKEN_A 换取 TOKEN_B
await executeSwap(
signer,
TOKEN_A,
TOKEN_B,
ethers.parseEther('1').toString(),
TradeType.EXACT_INPUT,
0.5
);
}
main().catch(console.error);
要点
- 步骤 1 和 2 仅需一次 — 每个代币只需执行一次,后续交换可跳过
- 原生 KAS:使用 WKAS 地址作为
tokenIn,并跳过步骤 1 和 2。API 会自动设置value。 - 错误处理:在继续之前始终检查
quote.error和swapData.error - 滑点保护:链上的
minAmountOut/maxAmountIn保护已编码在 calldata 中