跳转到主要内容

交换示例

一个使用 ethers.js v6 在 Kroko DEX 上执行代币交换的最小但完整的示例。

完整代码

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. 步骤 1 和 2 仅需一次 — 每个代币只需执行一次,后续交换可跳过
  2. 原生 KAS:使用 WKAS 地址作为 tokenIn,并跳过步骤 1 和 2。API 会自动设置 value
  3. 错误处理:在继续之前始终检查 quote.errorswapData.error
  4. 滑点保护:链上的 minAmountOut / maxAmountIn 保护已编码在 calldata 中