Full example (SCA Circle)
This is an example using Circle API and Node to collect quotes, approve tokens, build call data, and call authorization using a SCA wallet (dev controlled).
import axios from "axios";
import { Interface } from "ethers";
import {
initiateDeveloperControlledWalletsClient,
Blockchain,
CircleDeveloperControlledWalletsClient,
} from "@circle-fin/developer-controlled-wallets";
// -------------------- Config --------------------
const API_KEY = process.env.EMIGRO_API_KEY ?? "esk_live.***";
const BASE_URL = process.env.EMIGRO_BASE_URL ?? "https://api.emigro.co";
const CHAIN_ID = 8453; // Base
// Circle SCA wallet (already created in your system)
const CIRCLE_API_KEY = process.env.CIRCLE_API_KEY as string;
const CIRCLE_ENTITY_SECRET = process.env.CIRCLE_ENTITY_SECRET as string;
const CIRCLE_WALLET_ID = process.env.CIRCLE_WALLET_ID as string; // <-- your SCA id
const CIRCLE_BLOCKCHAIN: Blockchain = "BASE"; // aligns to chainId 8453
// Approvals: choose exact or a generous allowance
const APPROVAL_AMOUNT = "1000000000000000000000000"; // 1e24 (you may change)
// -------------------- DTOs (subset) --------------------
type QuoteResponse = {
fromTokenAddress: string;
toTokenAddress: string;
chainId: number;
amountOut: string; // RAW tokenIn
minAmountIn: string; // RAW min tokenOut
routerType: string;
feeTier?: number;
multihop: null | Array<{ feeTier?: number; tickSpacing?: number; [k: string]: any }>;
path: string[];
};
type BuildResponse = {
success: boolean;
chainId: number;
routerAddress: string;
abiFunctionSignature: string;
abiParameters: any[];
routerType: string;
adapterAddress: string;
adapterData: string;
};
// -------------------- Circle client --------------------
function circle(): CircleDeveloperControlledWalletsClient {
return initiateDeveloperControlledWalletsClient({
apiKey: CIRCLE_API_KEY,
entitySecret: CIRCLE_ENTITY_SECRET,
});
}
// Simple poller for Circle tx status until CONFIRMED/FAILED
async function waitCircleTx(txId: string, timeoutMs = 90_000) {
const client = circle();
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const res = await client.getTransaction({ id: txId });
const state = res?.data?.transaction?.state?.toUpperCase?.();
if (state === "CONFIRMED") return res.data.transaction;
if (state === "FAILED") throw new Error("Circle tx FAILED");
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("Circle tx TIMEOUT");
}
// -------------------- Emigro API helpers --------------------
async function getQuote(params: {
fromTokenAddress: string;
toTokenAddress: string;
amount: string; // HUMAN, e.g. "100.00"
chainId: number;
slippage?: number;
}): Promise<QuoteResponse> {
const { data } = await axios.post<QuoteResponse>(
`${BASE_URL}/public/emigroswap/quote`,
params,
{ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" } }
);
return data;
}
async function buildFromQuote(quote: QuoteResponse, userWallet: string) {
// base fields required for both single & multi
const body: any = {
fromTokenAddress: quote.fromTokenAddress,
toTokenAddress: quote.toTokenAddress,
chainId: quote.chainId,
userWallet,
amountOut: quote.amountOut, // RAW input (from quote)
minAmountIn: quote.minAmountIn, // RAW min out (from quote)
routerType: quote.routerType,
};
// single-hop v3 feeTier if provided
if (
quote.routerType.toLowerCase().includes("uniswapv3") &&
typeof (quote as any).feeTier === "number"
) {
body.feeTier = (quote as any).feeTier;
}
// multi-hop needs path + multihop from quote
if (Array.isArray(quote.multihop) && quote.path?.length >= 3) {
body.path = quote.path;
body.multihop = quote.multihop;
}
const { data } = await axios.post<BuildResponse>(
`${BASE_URL}/public/emigroswap/buildTransaction`,
body,
{ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" } }
);
return data;
}
// -------------------- Circle allowance + execution --------------------
// Send an ERC20 approve(spender, amount) from the SCA
async function circleApproveERC20({
token,
spender,
amount,
}: {
token: string;
spender: string;
amount: string;
}) {
const client = circle();
const abiSig = "approve(address,uint256)";
const abiParams: [string, string] = [spender, amount];
const send = await client.sendSmartContractTransaction({
walletId: CIRCLE_WALLET_ID,
contractAddress: token,
abiFunctionSignature: abiSig,
abiParameters: abiParams,
// optional:
feeLevel: "HIGH",
});
const approvalTxId = send?.data?.id;
if (!approvalTxId) throw new Error("Circle approval missing tx id");
console.log(" ▸ Sent approval via Circle:", approvalTxId);
const approval = await waitCircleTx(approvalTxId);
console.log(" ✓ Approval confirmed | hash:", approval.txHash);
}
// Execute the router call that the Emigro builder returned
async function circleExecuteBuild(build: BuildResponse) {
const client = circle();
const send = await client.sendSmartContractTransaction({
walletId: CIRCLE_WALLET_ID,
contractAddress: build.routerAddress,
abiFunctionSignature: build.abiFunctionSignature,
abiParameters: build.abiParameters,
feeLevel: "HIGH",
});
const txId = send?.data?.id;
if (!txId) throw new Error("Circle swap missing tx id");
console.log(" ▸ Sent swap via Circle:", txId);
const confirmed = await waitCircleTx(txId);
console.log(" ✓ Swap confirmed | hash:", confirmed.txHash);
return confirmed.txHash;
}
// -------------------- Demo flows --------------------
async function singleHopFlow() {
console.log("\n=== SINGLE-HOP (USDC -> BRZ) with Circle SCA ===");
// 1) quote
const quote = await getQuote({
fromTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
toTokenAddress: "0xE9185Ee218cae427aF7B9764A011bb89FeA761B4", // BRZ
amount: "100.00",
chainId: CHAIN_ID,
slippage: 0.5,
});
console.log(" quote.routerType:", quote.routerType);
console.log(" quote.path:", quote.path);
// 2) build calldata from quote
// Important: userWallet must be the SCA address you want to receive the output into.
// For Circle, the recipient is your SCA's **on-chain address**.
const scaAddress = (await circle().getWallet({ id: CIRCLE_WALLET_ID })).data.wallet?.address!;
const build = await buildFromQuote(quote, scaAddress);
console.log(" build.router:", build.routerAddress);
console.log(" build.func:", build.abiFunctionSignature);
// 3) approve tokenIn to router (amountOut is RAW tokenIn)
await circleApproveERC20({
token: quote.fromTokenAddress,
spender: build.routerAddress,
amount: APPROVAL_AMOUNT, // or quote.amountOut
});
// 4) execute router call
const txHash = await circleExecuteBuild(build);
console.log(" ✅ single-hop done | tx:", txHash);
}
async function multiHopFlow() {
console.log("\n=== MULTI-HOP (EURC -> USDC -> BRZ) with Circle SCA ===");
// 1) quote (multihop path expected)
const quote = await getQuote({
fromTokenAddress: "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42", // EURC
toTokenAddress: "0xE9185Ee218cae427aF7B9764A011bb89FeA761B4", // BRZ
amount: "100.00",
chainId: CHAIN_ID,
slippage: 0.5,
});
console.log(" quote.routerType:", quote.routerType);
console.log(" quote.path:", quote.path);
if (!Array.isArray(quote.multihop) || quote.path.length < 3) {
throw new Error("Expected multihop route in quote");
}
// 2) build (must pass userWallet = SCA address)
const scaAddress = (await circle().getWallet({ id: CIRCLE_WALLET_ID })).data.wallet?.address!;
const build = await buildFromQuote(quote, scaAddress);
console.log(" build.router:", build.routerAddress);
console.log(" build.func:", build.abiFunctionSignature);
// 3) approve tokenIn to router (amountOut is RAW tokenIn)
await circleApproveERC20({
token: quote.fromTokenAddress,
spender: build.routerAddress,
amount: APPROVAL_AMOUNT, // or quote.amountOut
});
// 4) execute router call
const txHash = await circleExecuteBuild(build);
console.log(" ✅ multi-hop done | tx:", txHash);
}
// -------------------- main --------------------
(async () => {
if (!CIRCLE_API_KEY || !CIRCLE_ENTITY_SECRET || !CIRCLE_WALLET_ID) {
throw new Error("Set CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET, CIRCLE_WALLET_ID env vars");
}
// Run one or both:
await singleHopFlow();
await multiHopFlow();
})().catch((e) => {
console.error("\n❌ Error:", e?.response?.data ?? e);
process.exit(1);
});
Last updated

