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);
});

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