> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How-to: Transfer USDC from a smart contract account

> Sign a Gateway burn intent with a Circle smart contract account and transfer a unified USDC balance using ERC-1271 programmable authorization.

Once you have established a unified USDC balance in a smart contract account
(SCA), you can transfer it instantly to any supported destination chain using
[ERC-1271](/gateway/references/erc-1271). The SCA authorizes the transfer with
its own signature—no delegate Externally Owned Account (EOA) required.

This guide demonstrates how to transfer your unified balance with a Circle
[Smart Contract Account (SCA) wallet](/wallets/account-types#smart-contract-accounts-sca).
For the EOA path, see
[Transfer a unified USDC balance](/gateway/howtos/transfer-unified-usdc-balance).

<Tip>
  This example uses Circle SCA wallets but the Gateway request is the same for any
  ERC-1271 wallet. Only signing differs: obtain opaque signature bytes that make
  `isValidSignature` return `0x1626ba7e`, then POST
  `{ burnIntent, signature, contractSigner: true }`. Do not split the signature
  into `v`, `r`, `s`.
</Tip>

<Note>
  Nanopayments and x402 batch settlement do not support ERC-1271. See
  [ERC-1271 programmable authorization](/gateway/references/erc-1271).
</Note>

## Prerequisites

Before you begin, ensure that you've:

* Installed [Node.js v22.6+](https://nodejs.org/)
* Created a [Circle Console](https://console.circle.com) account
* Obtained an API key and
  [registered your Entity Secret](/wallets/dev-controlled/register-entity-secret)
* Created SCA wallets via Circle
  [developer-controlled wallets](/wallets/dev-controlled) on the source and
  destination chains (this guide uses Arc Testnet and Base Sepolia)
  * The source SCA holds your Gateway USDC deposits and signs burn intents
  * The destination wallet submits the mint (can be the same address on another
    blockchain)
* [Deposited USDC into the Gateway Wallet](/gateway/howtos/create-unified-usdc-balance)
  from your SCA on Arc Testnet and waited for the deposit to finalize
* Created a TypeScript project with the
  [Developer-Controlled Wallets SDK](/sdks/developer-controlled-wallets-nodejs-sdk)
  installed
* Set up a `.env` file with the following variables:

  ```text .env theme={null}
  CIRCLE_API_KEY={YOUR_API_KEY}
  CIRCLE_ENTITY_SECRET={YOUR_ENTITY_SECRET}
  DEPOSITOR_ADDRESS={YOUR_SCA_WALLET_ADDRESS}
  RECIPIENT_ADDRESS={YOUR_DESTINATION_WALLET_ADDRESS}
  ```

## Steps

Follow these steps to transfer a unified USDC balance from an SCA. This example
transfers 1 USDC from Arc Testnet to Base Sepolia. You can adapt it for any
blockchains where you hold a unified balance.

### Step 1. Create and sign the burn intent with the SCA

Create a new file called `transfer.ts` in the root of your project and add the
following code to it. This code creates a
[burn intent](/gateway/references/technical-guide#burn-intent) for 1 USDC on Arc
Testnet and signs it with your SCA. Set `sourceDepositor` and `sourceSigner` to
the SCA address.

```typescript transfer.ts expandable theme={null}
import { randomBytes } from "node:crypto";
import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";

const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const GATEWAY_MINTER_ADDRESS = "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";

const TRANSFER_VALUE = 1_000000n; // 1 USDC (6 decimals)
const MAX_FEE = 2_010000n;
const MAX_UINT256_DEC = ((1n << 256n) - 1n).toString();

const SOURCE_CHAIN = "ARC-TESTNET";
const DEST_CHAIN = "BASE-SEPOLIA";

const sourceConfig = {
  chainName: "Arc Testnet",
  usdc: "0x3600000000000000000000000000000000000000",
  domain: 26,
  walletChain: SOURCE_CHAIN,
};

const destinationConfig = {
  chainName: "Base Sepolia",
  usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  domain: 6,
  walletChain: DEST_CHAIN,
};

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

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

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" },
];

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

function addressToBytes32(address: string) {
  return ("0x" +
    address
      .toLowerCase()
      .replace(/^0x/, "")
      .padStart(64, "0")) as `0x${string}`;
}

function stringifyTypedData<T>(obj: T) {
  return JSON.stringify(obj, (_key, value) =>
    typeof value === "bigint" ? value.toString() : value,
  );
}

async function waitForTx(
  client: ReturnType<typeof initiateDeveloperControlledWalletsClient>,
  txId: string,
) {
  while (true) {
    const { data } = await client.getTransaction({ id: txId });
    const state = data?.transaction?.state;
    if (["COMPLETE", "CONFIRMED"].includes(state!)) return;
    if (["FAILED", "DENIED", "CANCELLED"].includes(state!))
      throw new Error(`Failed: ${state}`);
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
}

function createBurnIntent(params: {
  depositorAddress: string;
  recipientAddress?: string;
}) {
  const { depositorAddress, recipientAddress = depositorAddress } = params;

  return {
    maxBlockHeight: MAX_UINT256_DEC,
    maxFee: MAX_FEE,
    spec: {
      version: 1,
      sourceDomain: sourceConfig.domain,
      destinationDomain: destinationConfig.domain,
      sourceContract: GATEWAY_WALLET_ADDRESS,
      destinationContract: GATEWAY_MINTER_ADDRESS,
      sourceToken: sourceConfig.usdc,
      destinationToken: destinationConfig.usdc,
      sourceDepositor: depositorAddress,
      destinationRecipient: recipientAddress,
      sourceSigner: depositorAddress,
      destinationCaller: "0x0000000000000000000000000000000000000000",
      value: TRANSFER_VALUE,
      salt: `0x${randomBytes(32).toString("hex")}`,
      hookData: "0x",
    },
  };
}

function burnIntentTypedData(burnIntent: ReturnType<typeof createBurnIntent>) {
  return {
    types: { EIP712Domain, TransferSpec, BurnIntent },
    domain,
    primaryType: "BurnIntent",
    message: {
      ...burnIntent,
      spec: {
        ...burnIntent.spec,
        sourceContract: addressToBytes32(burnIntent.spec.sourceContract),
        destinationContract: addressToBytes32(
          burnIntent.spec.destinationContract,
        ),
        sourceToken: addressToBytes32(burnIntent.spec.sourceToken),
        destinationToken: addressToBytes32(burnIntent.spec.destinationToken),
        sourceDepositor: addressToBytes32(burnIntent.spec.sourceDepositor),
        destinationRecipient: addressToBytes32(
          burnIntent.spec.destinationRecipient,
        ),
        sourceSigner: addressToBytes32(burnIntent.spec.sourceSigner),
        destinationCaller: addressToBytes32(burnIntent.spec.destinationCaller),
      },
    },
  };
}

const client = initiateDeveloperControlledWalletsClient({
  apiKey: process.env.CIRCLE_API_KEY!,
  entitySecret: process.env.CIRCLE_ENTITY_SECRET!,
});

const depositorAddress = process.env.DEPOSITOR_ADDRESS!;
const recipientAddress = process.env.RECIPIENT_ADDRESS!;

console.log(
  `Transferring 1 USDC from ${sourceConfig.chainName} to ${destinationConfig.chainName}`,
);
console.log(`Depositor / signer (SCA): ${depositorAddress}`);
console.log(`Recipient: ${recipientAddress}`);

const burnIntent = createBurnIntent({
  depositorAddress,
  recipientAddress,
});

const typedData = burnIntentTypedData(burnIntent);

const sigResp = await client.signTypedData({
  walletAddress: depositorAddress,
  blockchain: sourceConfig.walletChain,
  data: stringifyTypedData(typedData),
});

const signature = sigResp.data?.signature;
if (!signature) throw new Error("Failed to sign burn intent");
```

<Note>
  For production apps, verifying the balance on each blockchain before creating
  burn intents is best practice. For this how-to, it's assumed that the balances
  are created per the [prerequisites](#prerequisites). For a complete end-to-end
  example that includes checking and error handling, see the Gateway quickstarts
  ([EVM](/gateway/quickstarts/unified-balance-evm),
  [Solana](/gateway/quickstarts/unified-balance-solana)).
</Note>

### Step 2. Submit the burn intent 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`](/api-reference/gateway/all/create-transfer-attestation) endpoint
with `contractSigner: true` so Gateway validates the signature with ERC-1271,
and obtains the attestation from that endpoint.

```typescript transfer.ts theme={null}
const requests = [
  {
    burnIntent: typedData.message,
    signature,
    contractSigner: true,
  },
];

console.log("Submitting to Gateway API...");
const response = await fetch(`${GATEWAY_API_BASE}/v1/transfer`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: stringifyTypedData(requests),
});

if (!response.ok) {
  throw new Error(await response.text());
}

const { attestation, signature: operatorSig } = await response.json();

if (!attestation || !operatorSig) {
  throw new Error("Invalid Gateway API response");
}
```

When `contractSigner` is `true`, Gateway validates via ERC-1271 on
`sourceSigner`. When omitted or `false`, it expects an EOA ECDSA signature. See
[Use ERC-1271 validation](/gateway/references/erc-1271#opting-into-erc-1271-validation).

### 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](/gateway/references/contract-interfaces-and-events#gatewayminter)
on Base Sepolia to instantly mint the USDC to your recipient wallet on that
blockchain.

```typescript transfer.ts theme={null}
console.log(`\nMinting funds on ${destinationConfig.chainName}...`);

const tx = await client.createContractExecutionTransaction({
  walletAddress: recipientAddress,
  blockchain: destinationConfig.walletChain,
  contractAddress: GATEWAY_MINTER_ADDRESS,
  abiFunctionSignature: "gatewayMint(bytes,bytes)",
  abiParameters: [attestation, operatorSig],
  fee: { type: "level", config: { feeLevel: "MEDIUM" } },
});

const txId = tx.data?.id;
if (!txId) throw new Error("Failed to submit mint transaction");

await waitForTx(client, txId);

console.log(`\nMinted 1 USDC on ${destinationConfig.chainName}`);
console.log(`Mint transaction ID:`, txId);
```

<Note>
  `RECIPIENT_ADDRESS` must be a destination chain developer-controlled wallet in
  your Circle account. The script uses `createContractExecutionTransaction()` to
  submit `gatewayMint()`.
</Note>

### Step 4. Run the script

Run the script with the following command:

```shell theme={null}
node --env-file=.env transfer.ts
```

<Note>
  ERC-1271 validation runs offchain as a read-only simulation. Authorization
  logic that modifies onchain state during validation isn't supported. See
  [ERC-1271 programmable
  authorization](/gateway/references/erc-1271#limitations-and-considerations)
  for the full list of limitations.
</Note>
