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);
}
})();
Last updated

