Skip to main content

Swap Example

A minimal but complete example of executing a token swap on Kroko DEX using ethers.js v6.

Full Code

import { ethers } from 'ethers';

// === Configuration ===
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)',
];

// === Trade Types ===
enum TradeType {
  EXACT_INPUT = 0,
  EXACT_OUTPUT = 1,
}

/**
 * Execute a complete swap on Kroko DEX.
 *
 * @param signer     - Connected ethers Signer
 * @param tokenIn    - Input token address (use WKAS address for native KAS)
 * @param tokenOut   - Output token address
 * @param amount     - Amount in raw units (amountIn for EXACT_INPUT, amountOut for EXACT_OUTPUT)
 * @param tradeType  - 0 for Exact Input, 1 for Exact Output
 * @param slippage   - Slippage tolerance as percentage (e.g., 0.5 for 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);

  // --- Step 1: Approve token → Permit2 ---
  const tokenAllowance = await token.allowance(userAddress, PERMIT2);
  if (tokenAllowance < BigInt(amount)) {
    console.log('Step 1: Approving token to Permit2...');
    const tx = await token.approve(PERMIT2, ethers.MaxUint256);
    await tx.wait();
    console.log('  Done.');
  } else {
    console.log('Step 1: Token already approved to Permit2.');
  }

  // --- Step 2: Permit2 → approve 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('Step 2: Approving Universal Router via Permit2...');
    const tx = await permit2.approve(
      tokenIn,
      UNIVERSAL_ROUTER,
      ethers.MaxUint160,
      now + 365 * 24 * 60 * 60
    );
    await tx.wait();
    console.log('  Done.');
  } else {
    console.log('Step 2: Universal Router already approved via Permit2.');
  }

  // --- Step 3: Get quote ---
  console.log('Step 3: Fetching quote...');
  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 failed: ${quote.error}`);

  console.log(`  Route: ${quote.route.protocol} (${quote.route.hops} hop)`);
  console.log(`  Amount in:  ${quote.amountIn}`);
  console.log(`  Amount out: ${quote.amountOut}`);
  console.log(`  Price impact: ${quote.priceImpact}%`);

  // --- Step 4: Get swap calldata ---
  console.log('Step 4: Generating 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 failed: ${swapData.error}`);

  if (tradeType === TradeType.EXACT_INPUT) {
    console.log(`  Min output: ${swapData.quote.minAmountOut}`);
  } else {
    console.log(`  Max input: ${swapData.quote.maxAmountIn}`);
  }

  // --- Step 5: Execute ---
  console.log('Step 5: Sending transaction...');
  const tx = await signer.sendTransaction({
    to: swapData.to,
    data: swapData.data,
    value: swapData.value,
  });

  console.log(`  Tx hash: ${tx.hash}`);
  const receipt = await tx.wait();
  console.log(`  Confirmed in block ${receipt!.blockNumber}`);

  return receipt!;
}

// === Usage ===
async function main() {
  // Connect wallet (example using private key — use a secure method in production)
  const provider = new ethers.JsonRpcProvider(RPC_URL);
  const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);

  const TOKEN_A = '0xB190a6A7fC2873f1Abf145279eD664348d5Ef630';
  const TOKEN_B = '0x3Ac3B30b7f18AEFD4590D7FE4d9C5944aaeB7220';

  // Exact Input: sell 1 TOKEN_A for TOKEN_B
  await executeSwap(
    signer,
    TOKEN_A,
    TOKEN_B,
    ethers.parseEther('1').toString(),
    TradeType.EXACT_INPUT,
    0.5
  );
}

main().catch(console.error);

Key Points

  1. Steps 1 & 2 are one-time per token — you can skip them for subsequent swaps
  2. Native KAS: Use the WKAS address as tokenIn, and skip Steps 1 & 2. The API sets value automatically.
  3. Error handling: Always check for quote.error and swapData.error before proceeding
  4. Slippage: The on-chain minAmountOut / maxAmountIn protection is encoded in the calldata