Full example (EOA)

This is an example using Ethers and Node to collect quotes, approve tokens, build call data, and call authorization using an EOA wallet.

import axios from "axios";
import { ethers } from "ethers";

// ---------- Config ----------
const BASE_URL = process.env.EMIGRO_BASE_URL ?? "https://api.emigro.co";
const API_KEY = process.env.EMIGRO_API_KEY ?? "esk_live.***";
const RPC_URL = process.env.BASE_RPC_URL as string;
const PRIVATE_KEY = process.env.PRIVATE_KEY as string;
const CHAIN_ID = 8453;

// Where the output tokens should be sent (can be the signer itself)
const RECIPIENT = process.env.RECIPIENT ?? "0x000000000000000000000000000000000000dEaD";

// Provider/signer
if (!RPC_URL || !PRIVATE_KEY) {
  throw new Error("Missing BASE_RPC_URL or PRIVATE_KEY env.");
}
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);

// ---------- Types from your API ----------
type Hop = { fromTokenAddress: string; toTokenAddress: string; feeTier?: number };
type Quote = {
  fromTokenAddress: string;
  toTokenAddress: string;
  chainId: number;
  amountOut: string;   // RAW input (from token you're spending)
  minAmountIn: string; // RAW min output (after slippage)
  routerType: string;  // e.g., "uniswapv3", "aerodrome-slipstream"
  feeTier?: number;    // present for v3 single-hop
  multihop: null | Hop[];
  path: string[];      // 2 tokens (single-hop) or 3+ (multi-hop)
};
type BuildTx = {
  success: boolean;
  chainId: number;
  routerAddress: string;
  abiFunctionSignature: string; // swapExactInputVia(...) or swapExactInputPathVia(...)
  abiParameters: any[];         // exact param list for Interface.encodeFunctionData
  routerType: string;
  adapterAddress: string;
  adapterData: string;
  amountOut: string;
  minAmountIn: string;
};

// ---------- Helpers ----------
const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" };

// Read current allowance(owner,spender) for ERC-20
async function readAllowance(token: string, owner: string, spender: string): Promise<bigint> {
  const erc20 = new ethers.Contract(
    token,
    ["function allowance(address,address) view returns (uint256)"],
    provider
  );
  const res = await erc20.allowance(owner, spender);
  return BigInt(res.toString());
}

// Approve if needed: approve(routerAddress, amountOutRAW)
async function approveIfNeeded(tokenIn: string, routerAddress: string, amountOutRAW: string) {
  const owner = await signer.getAddress();
  const current = await readAllowance(tokenIn, owner, routerAddress);
  const needed = BigInt(amountOutRAW);

  if (current >= needed) {
    console.log(`✔ allowance OK for ${tokenIn} → ${routerAddress}: ${current.toString()} >= ${needed.toString()}`);
    return;
  }

  console.log(`ℹ approving ${tokenIn} for router ${routerAddress} to at least ${needed.toString()}`);
  const erc20 = new ethers.Contract(
    tokenIn,
    ["function approve(address spender,uint256 value) returns (bool)"],
    signer
  );
  const tx = await erc20.approve(routerAddress, needed);
  const rc = await tx.wait();
  console.log(`✔ approve tx mined: ${rc?.hash}`);
}

// Encode the router call returned from /buildTransaction
function encodeRouterCall(build: BuildTx) {
  const fnSig = build.abiFunctionSignature;                     // e.g. "swapExactInputVia(address,...)"
  const fnName = fnSig.slice(0, fnSig.indexOf("("));            // "swapExactInputVia" | "swapExactInputPathVia"
  const iface = new ethers.Interface([`function ${fnSig}`]);
  const data = iface.encodeFunctionData(fnName, build.abiParameters);
  return { to: build.routerAddress, data };
}

// ---------- API calls ----------
async function getQuote(params: {
  fromTokenAddress: string;
  toTokenAddress: string;
  amount: string; // HUMAN, e.g. "100.00"
  chainId: number;
  slippage?: number;
}) {
  const { data } = await axios.post<Quote>(`${BASE_URL}/public/emigroswap/quote`, params, { headers });
  return data;
}

async function buildFromQuote(q: Quote) {
  // Base body shared across single-hop and multi-hop
  const body: any = {
    fromTokenAddress: q.fromTokenAddress,
    toTokenAddress: q.toTokenAddress,
    chainId: q.chainId,
    userWallet: RECIPIENT,
    amountOut: q.amountOut,
    minAmountIn: q.minAmountIn,
    routerType: q.routerType,
  };
  // Optional v3 single-hop feeTier
  if (q.routerType.toLowerCase().includes("uniswapv3") && typeof q.feeTier === "number") {
    body.feeTier = q.feeTier;
  }
  // Multi-hop path + per-hop
  if (Array.isArray(q.multihop) && q.path?.length >= 3) {
    body.path = q.path;
    body.multihop = q.multihop;
  }

  const { data } = await axios.post<BuildTx>(`${BASE_URL}/public/emigroswap/buildTransaction`, body, { headers });
  return data;
}

// ---------- End-to-end flows ----------
/**
 * Single-hop demo: USDC -> BRZ (your sample)
 */
async function singleHopFlow() {
  console.log("\n=== SINGLE-HOP FLOW ===");
  const quote = await getQuote({
    fromTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
    toTokenAddress:   "0xE9185Ee218cae427aF7B9764A011bb89FeA761B4", // BRZ
    amount: "100.00",
    chainId: CHAIN_ID,
    slippage: 0.5,
  });

  if (Array.isArray(quote.multihop)) {
    throw new Error("Expected single-hop quote, but got multihop.");
  }
  console.log("quote.path:", quote.path, "routerType:", quote.routerType);
  console.log("amountOut (RAW in):", quote.amountOut, "minAmountIn (RAW out):", quote.minAmountIn);

  // Build calldata using ONLY quote values
  const build = await buildFromQuote(quote);
  console.log("build.router:", build.routerAddress);
  console.log("build.fn:", build.abiFunctionSignature);
  console.log("build.params:", build.abiParameters);

  // Approve input token (first of the path = token you spend)
  const tokenIn = quote.path[0];
  await approveIfNeeded(tokenIn, build.routerAddress, build.amountOut);

  // Encode and send the transaction
  const { to, data } = encodeRouterCall(build);
  const tx = await signer.sendTransaction({ to, data });
  console.log("submitted:", tx.hash);
  const rc = await tx.wait();
  console.log("✔ confirmed:", rc?.hash);
}

/**
 * Multi-hop demo: EURC -> USDC -> BRZ
 * routerType "aerodrome-slipstream", with multihop hops and 3-token path.
 */
async function multiHopFlow() {
  console.log("\n=== MULTI-HOP FLOW ===");
  const quote = await getQuote({
    fromTokenAddress: "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42", // EURC
    toTokenAddress:   "0xE9185Ee218cae427aF7B9764A011bb89FeA761B4", // BRZ
    amount: "100.00",
    chainId: CHAIN_ID,
    slippage: 0.5,
  });

  if (!Array.isArray(quote.multihop) || quote.path.length < 3) {
    throw new Error("Expected multi-hop quote, but got single-hop.");
  }
  console.log("quote.path:", quote.path, "routerType:", quote.routerType, "hops:", quote.multihop.length);
  console.log("amountOut (RAW in):", quote.amountOut, "minAmountIn (RAW out):", quote.minAmountIn);

  // Build calldata using ONLY quote values (includes path + multihop)
  const build = await buildFromQuote(quote);
  console.log("build.router:", build.routerAddress);
  console.log("build.fn:", build.abiFunctionSignature);
  console.log("build.params[0]=adapter:", build.abiParameters[0]); // sanity

  // Approve the first token in the path (EURC) to spend amountOut
  const tokenIn = quote.path[0];
  await approveIfNeeded(tokenIn, build.routerAddress, build.amountOut);

  // Encode and send the transaction
  const { to, data } = encodeRouterCall(build);
  const tx = await signer.sendTransaction({ to, data });
  console.log("submitted:", tx.hash);
  const rc = await tx.wait();
  console.log("✔ confirmed:", rc?.hash);
}

// ---------- Run both demos ----------
(async () => {
  try {
    await singleHopFlow();
    await multiHopFlow();
  } catch (e: any) {
    console.error("Flow error:", e?.response?.data ?? e?.message ?? e);
    process.exit(1);
  }
})();

Additional off-chain and on-chain functions to avoid token allowance rejection or a large number of tokens, at the discretion of the application design.

Last updated