> ## Documentation Index
> Fetch the complete documentation index at: https://circle-devdocs-test-ai-codegen-component.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Transfer USDC on testnet from Ethereum to Avalanche using CCTP V2

This guide demonstrates how to use the [viem](https://viem.sh/) framework and
the [CCTP V2 API](/cctp/technical-guide#cctp-api-hosts-and-endpoints) in a
simple script that enables a user to transfer USDC from a wallet address on the
**Ethereum Sepolia testnet** to another wallet address on the **Avalanche Fuji
testnet**.

## Prerequisites

Before you start building the sample app to perform a USDC transfer, ensure you
have met the following prerequisites:

1. **Install Node.js and npm**

   * Download and install **Node.js** directly or use a version manager like
     **nvm**.
   * **npm** is included with Node.js.

2. **Set up a non-custodial wallet** (for example, MetaMask)

   * You can download, install, and create a **MetaMask** wallet from its
     [official website](https://metamask.io).
   * During setup, create a wallet on the **Ethereum Sepolia testnet**.
   * Retrieve the **private key** for your wallet, as it will be required in the
     script below.

3. **Fund your wallet with testnet tokens**
   * Obtain **Sepolia ETH** (native token) from a
     [public faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia).
   * Get **Sepolia USDC** from the [Circle Faucet](https://faucet.circle.com).

## Project setup

To build the script, first set up your project environment and install the
required dependencies.

1. **Set up a new project**

Create a new directory and initialize a new `Node.js` project with default
settings:

```shell Shell theme={null}
mkdir cctp-v2-transfer
cd cctp-v2-transfer
npm init -y
```

This also creates a default `package.json` file.

2. **Install dependencies**

In your project directory, install the required dependencies, including `viem`:

```shell Shell theme={null}
npm install axios@^1.7.9 dotenv@^16.4.7 viem@^2.23.4
```

This sets up your development environment with the necessary libraries for
building the script. It also updates the `package.json` file with the
dependencies.

3. **Add module type**

Add `"type": "module"` to the `package.json` file:

```json package.json theme={null}
{
  "name": "cctp-v2-transfer",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node transfer.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "axios": "^1.7.9",
    "dotenv": "^16.4.7",
    "viem": "^2.23.4"
  }
}
```

4. **Configure environment variables**

Create a `.env` file in your project directory and add your wallet private key:

```shell Shell theme={null}
echo "PRIVATE_KEY=your-private-key-here" > .env
```

<Warning>
  **Warning:** This is strictly for testing purposes. **Never share your private
  key**.
</Warning>

## Script setup

This section covers the necessary setup for the [transfer.js](#transferjs)
script, including defining keys and addresses, and configuring the wallet client
for interacting with the source and destination chains.

1. **Replace with your private key and wallet address**

Ensure that this section of the file includes your **private key** and
associated **wallet address**. The script also predefines the **contract
addresses**, the **transfer amount**, and the **max fee**. These definitions are
critical for successfully transferring USDC between the intended wallets.

```javascript Javascript theme={null}
// ============ Configuration Constants ============

// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`);

// Contract Addresses
const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238";
const ETHEREUM_SEPOLIA_TOKEN_MESSENGER =
  "0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa";
const AVALANCHE_FUJI_MESSAGE_TRANSMITTER =
  "0xe737e5cebeeba77efe34d4aa090756590b1ce275";

// Transfer Parameters
const DESTINATION_ADDRESS = "your-wallet-address"; // Address to receive minted tokens on destination chain
const AMOUNT = 1_000_000n; // Set transfer amount in 10^6 subunits (1 USDC; change as needed)
const maxFee = 500n; // Set fast transfer max fee in 10^6 subunits (0.0005 USDC; change as needed)

// Bytes32 Formatted Parameters
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(2)}`; // Destination address in bytes32 format
const DESTINATION_CALLER_BYTES32 =
  "0x0000000000000000000000000000000000000000000000000000000000000000"; // Empty bytes32 allows any address to call MessageTransmitterV2.receiveMessage()

// Chain-specific Parameters
const ETHEREUM_SEPOLIA_DOMAIN = 0; // Source domain ID for Ethereum Sepolia testnet
const AVALANCHE_FUJI_DOMAIN = 1; // Destination domain ID for Avalanche Fuji testnet
```

2. **Set up wallet clients**

The **wallet client** configures the appropriate network settings using `viem`.
In this example, the script connects to the **Ethereum Sepolia testnet** and the
**Avalanche Fuji testnet**.

```javascript Javascript theme={null}
// Set up the wallet clients
const sepoliaClient = createWalletClient({
  chain: sepolia,
  transport: http(),
  account,
});

const avalancheClient = createWalletClient({
  chain: avalancheFuji,
  transport: http(),
  account,
});
```

## CCTP transfer process

The following sections outline the relevant transfer logic of the sample script.
You can view the full source code in the [Build the script](#build-the-script)
section below. To perform the actual transfer of USDC from **Ethereum Sepolia**
to **Avalanche Fuji** using CCTP V2, follow the steps below:

### 1. Approve USDC

The first step is to grant approval for the
[TokenMessengerV2 contract](https://sepolia.etherscan.io/address/0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa)
deployed on the **Ethereum Sepolia testnet** to withdraw USDC from your wallet
on that source chain. This allows the contract to withdraw USDC from the
specified wallet address.

```javascript Javascript theme={null}
async function approveUSDC() {
  console.log("Approving USDC transfer...");
  const approveTx = await sepoliaClient.sendTransaction({
    to: ETHEREUM_SEPOLIA_USDC,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "approve",
          stateMutability: "nonpayable",
          inputs: [
            { name: "spender", type: "address" },
            { name: "amount", type: "uint256" },
          ],
          outputs: [{ name: "", type: "bool" }],
        },
      ],
      functionName: "approve",
      args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, 10_000_000_000n], // Set max allowance in 10^6 subunits (10,000 USDC; change as needed)
    }),
  });
  console.log(`USDC Approval Tx: ${approveTx}`);
}
```

### 2. Burn USDC

In this step, you call the `depositForBurn` function from the
[TokenMessengerV2 contract](https://sepolia.etherscan.io/address/0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa)
deployed on the **Ethereum Sepolia testnet** to burn USDC on that source chain.
You specify the following parameters:

* **Burn amount**: The amount of USDC to burn
* **Destination domain**: the target blockchain for minting USDC
* **Mint recipient**: The wallet address that will receive the minted USDC
* **Burn token**: The contract address of the USDC token being burned on the
  source chain
* **Destination caller**: The address on the target chain to call
  `receiveMessage`
* **Max fee**: The maximum fee allowed for the transfer
* **Finality threshold**: Determines whether it's a **Fast Transfer** or a
  **Standard Transfer**

```javascript Javascript theme={null}
async function burnUSDC() {
  console.log("Burning USDC on Ethereum Sepolia...");
  const burnTx = await sepoliaClient.sendTransaction({
    to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "depositForBurn",
          stateMutability: "nonpayable",
          inputs: [
            { name: "amount", type: "uint256" },
            { name: "destinationDomain", type: "uint32" },
            { name: "mintRecipient", type: "bytes32" },
            { name: "burnToken", type: "address" },
            { name: "destinationCaller", type: "bytes32" },
            { name: "maxFee", type: "uint256" },
            { name: "minFinalityThreshold", type: "uint32" },
          ],
          outputs: [],
        },
      ],
      functionName: "depositForBurn",
      args: [
        AMOUNT,
        AVALANCHE_FUJI_DOMAIN,
        DESTINATION_ADDRESS_BYTES32,
        ETHEREUM_SEPOLIA_USDC,
        DESTINATION_CALLER_BYTES32,
        maxFee,
        1000, // minFinalityThreshold (1000 or less for Fast Transfer)
      ],
    }),
  });
  console.log(`Burn Tx: ${burnTx}`);
  return burnTx;
}
```

### 3. Retrieve attestation

In this step, you retrieve the **attestation** required to complete the CCTP
transfer.

* Call Circle's [GET /v2/messages](/api-reference/cctp/all/get-messages-v-2) API
  endpoint to retrieve the attestation.
  * Pass the `srcDomain` argument from the
    [CCTP Domain](/cctp/cctp-supported-blockchains#cctp-supported-domains) for
    your source chain.
  * Pass `transactionHash` from the value returned by `sendTransaction` within
    the `burnUSDC` function above.

This step is essential for verifying the **burn event** before proceeding with
the transfer.

```javascript Javascript theme={null}
async function retrieveAttestation(transactionHash) {
  console.log("Retrieving attestation...");
  const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`;
  while (true) {
    try {
      const response = await axios.get(url);
      if (response.status === 404) {
        console.log("Waiting for attestation...");
      }
      if (response.data?.messages?.[0]?.status === "complete") {
        console.log("Attestation retrieved successfully!");
        return response.data.messages[0];
      }
      console.log("Waiting for attestation...");
      await new Promise((resolve) => setTimeout(resolve, 5000));
    } catch (error) {
      console.error("Error fetching attestation:", error.message);
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }
}
```

### 4. Mint USDC

In this final step, you call the `receiveMessage` function from the
[MessageTransmitterV2 contract](https://testnet.snowtrace.io/address/0xe737e5cebeeba77efe34d4aa090756590b1ce275)
deployed on the **Avalanche Fuji testnet** to mint USDC on that destination
chain.

* Pass the **signed attestation** and the
  [message data](/cctp/technical-guide#message-header) as parameters.
* The function processes the attestation and mints USDC to the specified
  **Avalanche Fuji wallet address**.

This step finalizes the **CCTP transfer**, making the USDC available on the
destination chain.

```javascript Javascript theme={null}
async function mintUSDC(attestation) {
  console.log("Minting USDC on Avalanche Fuji...");
  const mintTx = await avalancheClient.sendTransaction({
    to: AVALANCHE_FUJI_MESSAGE_TRANSMITTER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "receiveMessage",
          stateMutability: "nonpayable",
          inputs: [
            { name: "message", type: "bytes" },
            { name: "attestation", type: "bytes" },
          ],
          outputs: [],
        },
      ],
      functionName: "receiveMessage",
      args: [attestation.message, attestation.attestation],
    }),
  });
  console.log(`Mint Tx: ${mintTx}`);
}
```

## Build the script

Now that you understand the core steps for programmatically **transferring
USDC** from **Ethereum Sepolia** to **Avalanche Fuji** using CCTP V2, create a
`transfer.js` in your project directory and populate it with the sample code
below.

<Note>
  **Note:** The source wallet must contain **native testnet tokens** (to cover
  gas fees) and **testnet USDC** to complete the transfer.
</Note>

### transfer.js

```javascript Javascript theme={null}
// Import environment variables
import "dotenv/config";
import { createWalletClient, http, encodeFunctionData } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia, avalancheFuji } from "viem/chains";
import axios from "axios";

// ============ Configuration Constants ============

// Authentication
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const account = privateKeyToAccount(`0x${PRIVATE_KEY}`);

// Contract Addresses
const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238";
const ETHEREUM_SEPOLIA_TOKEN_MESSENGER =
  "0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa";
const AVALANCHE_FUJI_MESSAGE_TRANSMITTER =
  "0xe737e5cebeeba77efe34d4aa090756590b1ce275";

// Transfer Parameters
const DESTINATION_ADDRESS = "your-wallet-address"; // Address to receive minted tokens on destination chain
const AMOUNT = 1_000_000n; // Set transfer amount in 10^6 subunits (1 USDC; change as needed)
const maxFee = 500n; // Set fast transfer max fee in 10^6 subunits (0.0005 USDC; change as needed)

// Bytes32 Formatted Parameters
const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice(2)}`; // Destination address in bytes32 format
const DESTINATION_CALLER_BYTES32 =
  "0x0000000000000000000000000000000000000000000000000000000000000000"; // Empty bytes32 allows any address to call MessageTransmitterV2.receiveMessage()

// Chain-specific Parameters
const ETHEREUM_SEPOLIA_DOMAIN = 0; // Source domain ID for Ethereum Sepolia testnet
const AVALANCHE_FUJI_DOMAIN = 1; // Destination domain ID for Avalanche Fuji testnet

// Set up wallet clients
const sepoliaClient = createWalletClient({
  chain: sepolia,
  transport: http(),
  account,
});
const avalancheClient = createWalletClient({
  chain: avalancheFuji,
  transport: http(),
  account,
});

async function approveUSDC() {
  console.log("Approving USDC transfer...");
  const approveTx = await sepoliaClient.sendTransaction({
    to: ETHEREUM_SEPOLIA_USDC,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "approve",
          stateMutability: "nonpayable",
          inputs: [
            { name: "spender", type: "address" },
            { name: "amount", type: "uint256" },
          ],
          outputs: [{ name: "", type: "bool" }],
        },
      ],
      functionName: "approve",
      args: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, 10_000_000_000n], // Set max allowance in 10^6 subunits (10,000 USDC; change as needed)
    }),
  });
  console.log(`USDC Approval Tx: ${approveTx}`);
}

async function burnUSDC() {
  console.log("Burning USDC on Ethereum Sepolia...");
  const burnTx = await sepoliaClient.sendTransaction({
    to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "depositForBurn",
          stateMutability: "nonpayable",
          inputs: [
            { name: "amount", type: "uint256" },
            { name: "destinationDomain", type: "uint32" },
            { name: "mintRecipient", type: "bytes32" },
            { name: "burnToken", type: "address" },
            { name: "destinationCaller", type: "bytes32" },
            { name: "maxFee", type: "uint256" },
            { name: "minFinalityThreshold", type: "uint32" },
          ],
          outputs: [],
        },
      ],
      functionName: "depositForBurn",
      args: [
        AMOUNT,
        AVALANCHE_FUJI_DOMAIN,
        DESTINATION_ADDRESS_BYTES32,
        ETHEREUM_SEPOLIA_USDC,
        DESTINATION_CALLER_BYTES32,
        maxFee,
        1000, // minFinalityThreshold (1000 or less for Fast Transfer)
      ],
    }),
  });
  console.log(`Burn Tx: ${burnTx}`);
  return burnTx;
}

async function retrieveAttestation(transactionHash) {
  console.log("Retrieving attestation...");
  const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`;
  while (true) {
    try {
      const response = await axios.get(url);
      if (response.status === 404) {
        console.log("Waiting for attestation...");
      }
      if (response.data?.messages?.[0]?.status === "complete") {
        console.log("Attestation retrieved successfully!");
        return response.data.messages[0];
      }
      console.log("Waiting for attestation...");
      await new Promise((resolve) => setTimeout(resolve, 5000));
    } catch (error) {
      console.error("Error fetching attestation:", error.message);
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }
}

async function mintUSDC(attestation) {
  console.log("Minting USDC on Avalanche Fuji...");
  const mintTx = await avalancheClient.sendTransaction({
    to: AVALANCHE_FUJI_MESSAGE_TRANSMITTER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "receiveMessage",
          stateMutability: "nonpayable",
          inputs: [
            { name: "message", type: "bytes" },
            { name: "attestation", type: "bytes" },
          ],
          outputs: [],
        },
      ],
      functionName: "receiveMessage",
      args: [attestation.message, attestation.attestation],
    }),
  });
  console.log(`Mint Tx: ${mintTx}`);
}

async function main() {
  await approveUSDC();
  const burnTx = await burnUSDC();
  const attestation = await retrieveAttestation(burnTx);
  await mintUSDC(attestation);
  console.log("USDC transfer completed!");
}

main().catch(console.error);
```

The `transfer.js` script provides a complete **end-to-end** solution for
transfering USDC in CCTP V2 with a **non-custodial wallet**. In the next
section, you can test the script.

## Test the script

To test the script, run the following command:

```shell Shell theme={null}
node transfer.js
```

Once the script runs and the transfer is finalized, a **confirmation receipt**
is logged in the console.

<Note>
  **Rate Limit:**

  The attestation service rate limit is 35 requests per second. If you exceed
  this limit, the service blocks all API requests for the next 5 minutes and
  returns an HTTP 429 (Too Many Requests) response.
</Note>

You have successfully transferred USDC between two EVM-compatible chains using
CCTP end-to-end!

<br />

**WHAT'S NEXT**

* [CCTP Supported Chains and Domains <Icon icon="arrow-right" />](/cctp/cctp-supported-blockchains)
