Skip to main content
Once you have established a unified USDC balance, you can transfer it instantly to any supported destination chain. This guide demonstrates how to transfer your unified balance. Select a tab below for EVM or Solana-specific instructions.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+
  • Prepared an EVM testnet wallet with the private key available
    • Added the supported Testnets of your choice to your wallet (this guide uses Arc Testnet, Avalanche Fuji and Sei Testnet)
  • Funded your testnet wallet with native tokens on the destination chain (this guide uses Sei Testnet)
  • Deposited 10 USDC into the Gateway Wallet contracts on Arc Testnet and Avalanche Fuji (creating a unified balance of 20 USDC)
  • Created a TypeScript project and have viem installed
  • You’ve set up a .env file with the following variables:
    .env
    EVM_PRIVATE_KEY={YOUR_PRIVATE_KEY}
    

Steps

Follow these steps to transfer a unified USDC balance. This example uses a unified balance split between Arc Testnet and Avalanche Fuji. You can adapt it for any chains where you hold a unified balance.

Step 1. Create and sign burn intents for the source chains

Create a new file called transfer.ts in the root of your project and add the following code to it. This code creates and signs burn intents for 5 USDC on Arc Testnet and 5 USDC on Avalanche Fuji.
transfer.ts
import { randomBytes } from "node:crypto";
import {
  http,
  maxUint256,
  zeroAddress,
  pad,
  createPublicClient,
  getContract,
  createWalletClient,
  formatUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { avalancheFuji, arcTestnet, seiTestnet } from "viem/chains";

/* Constants */
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const GATEWAY_MINTER_ADDRESS = "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";

const TRANSFER_VALUE = 5_000000n; // 5 USDC (6 decimals)
const MAX_FEE = 2_010000n;

// Source chains configuration
const sourceChains = [
  {
    name: "arcTestnet",
    chain: arcTestnet,
    usdcAddress: "0x3600000000000000000000000000000000000000",
    domainId: 26,
  },
  {
    name: "avalancheFuji",
    chain: avalancheFuji,
    usdcAddress: "0x5425890298aed601595a70ab815c96711a31bc65",
    domainId: 1,
  },
];

// Destination chain configuration
const destinationChain = {
  name: "seiTestnet",
  chain: seiTestnet,
  usdcAddress: "0x4fCF1784B31630811181f670Aea7A7bEF803eaED",
  domainId: 16,
};

const domain = { name: "GatewayWallet", version: "1" };

const EIP712Domain = [
  { name: "name", type: "string" },
  { name: "version", type: "string" },
] as const;

const TransferSpec = [
  { name: "version", type: "uint32" },
  { name: "sourceDomain", type: "uint32" },
  { name: "destinationDomain", type: "uint32" },
  { name: "sourceContract", type: "bytes32" },
  { name: "destinationContract", type: "bytes32" },
  { name: "sourceToken", type: "bytes32" },
  { name: "destinationToken", type: "bytes32" },
  { name: "sourceDepositor", type: "bytes32" },
  { name: "destinationRecipient", type: "bytes32" },
  { name: "sourceSigner", type: "bytes32" },
  { name: "destinationCaller", type: "bytes32" },
  { name: "value", type: "uint256" },
  { name: "salt", type: "bytes32" },
  { name: "hookData", type: "bytes" },
] as const;

const BurnIntent = [
  { name: "maxBlockHeight", type: "uint256" },
  { name: "maxFee", type: "uint256" },
  { name: "spec", type: "TransferSpec" },
] as const;

const gatewayMinterAbi = [
  {
    type: "function",
    name: "gatewayMint",
    inputs: [
      { name: "attestationPayload", type: "bytes" },
      { name: "signature", type: "bytes" },
    ],
    outputs: [],
    stateMutability: "nonpayable",
  },
] as const;

// Get account from environment
if (!process.env.EVM_PRIVATE_KEY) throw new Error("EVM_PRIVATE_KEY not set");
const account = privateKeyToAccount(
  process.env.EVM_PRIVATE_KEY as `0x${string}`,
);

console.log(`Using account: ${account.address}`);
console.log(`Transferring from: ${sourceChains.map((c) => c.name).join(", ")}`);
console.log(`Transferring to: ${destinationChain.name}\n`);

// Create and sign burn intents for each source chain
const requests = [];

for (const sourceChain of sourceChains) {
  console.log(
    `Creating burn intent from ${sourceChain.name}${destinationChain.name}...`,
  );

  const burnIntent = {
    maxBlockHeight: maxUint256,
    maxFee: MAX_FEE,
    spec: {
      version: 1,
      sourceDomain: sourceChain.domainId,
      destinationDomain: destinationChain.domainId,
      sourceContract: GATEWAY_WALLET_ADDRESS,
      destinationContract: GATEWAY_MINTER_ADDRESS,
      sourceToken: sourceChain.usdcAddress,
      destinationToken: destinationChain.usdcAddress,
      sourceDepositor: account.address,
      destinationRecipient: account.address,
      sourceSigner: account.address,
      destinationCaller: zeroAddress,
      value: TRANSFER_VALUE,
      salt: "0x" + randomBytes(32).toString("hex"),
      hookData: "0x",
    },
  };

  const typedData = {
    types: { EIP712Domain, TransferSpec, BurnIntent },
    domain,
    primaryType: "BurnIntent" as const,
    message: {
      ...burnIntent,
      spec: {
        ...burnIntent.spec,
        sourceContract: pad(
          burnIntent.spec.sourceContract.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
        destinationContract: pad(
          burnIntent.spec.destinationContract.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
        sourceToken: pad(
          burnIntent.spec.sourceToken.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
        destinationToken: pad(
          burnIntent.spec.destinationToken.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
        sourceDepositor: pad(
          burnIntent.spec.sourceDepositor.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
        destinationRecipient: pad(
          burnIntent.spec.destinationRecipient.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
        sourceSigner: pad(
          burnIntent.spec.sourceSigner.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
        destinationCaller: pad(
          burnIntent.spec.destinationCaller.toLowerCase() as `0x${string}`,
          { size: 32 },
        ),
      },
    },
  };

  const signature = await account.signTypedData(
    typedData as Parameters<typeof account.signTypedData>[0],
  );
  requests.push({ burnIntent: typedData.message, signature });
}

console.log("Signed burn intents.\n");
Note: For production apps, verifying the balance on each chain before creating burn intents is best practice. For this how-to, it’s assumed that the balances are created per the prerequisites. For a complete end-to-end example that includes checking and error handling, see the Gateway quickstarts (EVM, Solana).

Step 2. Submit the burn intents to the Gateway API to obtain an attestation

Add the following code to transfer.ts. This code constructs a Gateway API request to the /transfer endpoint and obtains the attestation from that endpoint.
transfer.ts
console.log("Submitting to Gateway API...");
const response = await fetch(
  "https://gateway-api-testnet.circle.com/v1/transfer",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(requests, (_key, value) =>
      typeof value === "bigint" ? value.toString() : value,
    ),
  },
);

if (!response.ok) {
  console.error("Gateway API error status:", response.status);
  console.error(await response.text());
  throw new Error("Gateway API request failed");
}

const json = await response.json();
console.log("Gateway API response:", JSON.stringify(json, null, 2));

const { attestation, signature } = json;

Step 3. Transfer USDC to the destination chain

Add the following code to transfer.ts. This code performs a call to the Gateway Minter contract on Sei Testnet to instantly mint the USDC to your account on that chain.
transfer.ts
console.log(`\nMinting funds on ${destinationChain.chain.name}...`);

const publicClient = createPublicClient({
  chain: destinationChain.chain,
  transport: http(),
});

const walletClient = createWalletClient({
  account,
  chain: destinationChain.chain,
  transport: http(),
});

const gatewayMinter = getContract({
  address: GATEWAY_MINTER_ADDRESS,
  abi: gatewayMinterAbi,
  client: walletClient,
});

const mintTx = await gatewayMinter.write.gatewayMint([attestation, signature], {
  account,
});
await publicClient.waitForTransactionReceipt({ hash: mintTx });

const totalMinted = BigInt(requests.length) * TRANSFER_VALUE;
console.log(`\nMinted ${formatUnits(totalMinted, 6)} USDC`);
console.log(`Mint transaction hash:`, mintTx);

Step 4. Run the script

Run the script with the following command:
npx tsx --env-file=.env transfer.ts