> ## Documentation Index
> Fetch the complete documentation index at: https://docs.krokoswap.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 交换示例

> 完整的、可运行的交换集成示例

# 交换示例

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

## 完整代码

```typescript theme={null}
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.error` 和 `swapData.error`
4. **滑点保护**：链上的 `minAmountOut` / `maxAmountIn` 保护已编码在 calldata 中
