> ## 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.

# Transfer USDC between Aptos and Arc

> Build scripts to transfer USDC between Aptos Testnet and Arc Testnet using CCTP

Transfer USDC between Aptos Testnet and Arc Testnet using the CCTP
burn-attest-mint flow.

<Note>
  Move scripts for Aptos CCTP transfers are precompiled. They are located in the
  [aptos-cctp GitHub
  repository](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts/testnet/v2).
  You submit these compiled scripts directly instead of compiling Move source
  code.
</Note>

Pick the tab that matches the direction of your transfer.

<Tabs>
  <Tab title="Aptos to Arc">
    This quickstart demonstrates how to transfer USDC from Aptos Testnet to Arc
    Testnet using CCTP. You use the
    [`@aptos-labs/ts-sdk`](https://github.com/aptos-labs/aptos-ts-sdk) library to
    submit the precompiled Aptos Move script that burns USDC, and
    [`viem`](https://viem.sh/) to mint USDC on Arc Testnet. When you finish, you
    will have executed a full burn-attest-mint flow.

    You should be comfortable using a terminal and Node.js. Familiarity with Aptos
    transactions and basic EVM usage helps you follow and adapt the script. Examples
    use Arc Testnet as the destination, but you can use any
    [supported EVM blockchain](/cctp/concepts/supported-chains-and-domains).

    <Note>
      The Aptos burn transaction uses a precompiled Move script. Download
      `deposit_for_burn.mv` from the
      [aptos-cctp repository](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts/testnet/v2)
      and place it in a `precompiled-move-scripts` directory in your project.
    </Note>

    ## Prerequisites

    Before you begin this tutorial, ensure you have:

    * Installed [Node.js v22.6+](https://nodejs.org/)
    * Prepared an Aptos Testnet wallet with the private key available
      * Funded your account with testnet APT from the
        [Aptos Faucet](https://aptos.dev/en/network/faucet) for transaction fees
      * Funded your account with Aptos Testnet USDC from the
        [Circle Faucet](https://faucet.circle.com)
    * Prepared an EVM wallet with the private key available for Arc Testnet
      * Added the Arc Testnet network to your wallet
        ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup))
      * Funded your wallet with Arc Testnet ETH for gas fees

    ## Step 1: Set up the project

    This step shows you how to prepare your project and environment.

    ### 1.1. Set up your development environment

    Create a new directory, initialize and set up a new Node.js project, and install
    the required dependencies:

    ```bash Shell theme={null}
    mkdir cctp-aptos-to-arc
    cd cctp-aptos-to-arc
    npm init -y

    npm pkg set type=module
    npm pkg set scripts.start="node --env-file=.env --import=tsx index.ts"

    npm install @aptos-labs/ts-sdk viem

    npm install --save-dev tsx typescript @types/node
    ```

    Copy
    [`deposit_for_burn.mv`](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts/testnet/v2/deposit_for_burn.mv)
    into `precompiled-move-scripts/` in your project. The full script in Step 4
    reads the bytecode relative to `index.ts`.

    ### 1.2. Initialize and configure the project

    <Tip>
      This step is optional. It helps prevent missing types in your IDE or editor.
    </Tip>

    Create a `tsconfig.json` file:

    ```shell theme={null}
    npx tsc --init
    ```

    Then, update the `tsconfig.json` file:

    ```shell theme={null}
    cat <<'EOF' > tsconfig.json
    {
      "compilerOptions": {
        "target": "ESNext",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "strict": true,
        "types": ["node"]
      }
    }
    EOF
    ```

    ### 1.3. Configure environment variables

    Open `.env` in your editor and add:

    ```text theme={null}
    APTOS_PRIVATE_KEY=YOUR_APTOS_PRIVATE_KEY
    EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
    ```

    * `APTOS_PRIVATE_KEY` is the private key for the Aptos Testnet account used to
      sign the burn transaction.
    * `EVM_PRIVATE_KEY` is the private key for the EVM wallet used to receive USDC
      on Arc Testnet.

    <Tip>
      Open `.env` in your editor rather than writing values with shell commands, and
      add `.env` to your `.gitignore`. This prevents credentials from leaking into
      your shell history or version control.
    </Tip>

    The `npm run start` command loads variables from `.env` using Node.js native
    env-file support.

    <Warning>
      This example uses one or more private keys for local testing. In production,
      use a secure key management solution and never expose or share private keys.
    </Warning>

    ## Step 2: Configure the script

    Define the Aptos and Arc Testnet parameters, then configure the wallet clients
    for each chain.

    ### 2.1. Define configuration constants

    The script predefines the USDC address, CCTP contract address, transfer amount,
    and CCTP domain IDs.

    ```ts TypeScript theme={null}
    import {
      Account,
      AccountAddress,
      Aptos,
      AptosConfig,
      Ed25519PrivateKey,
      Network,
      U32,
      U64,
    } from "@aptos-labs/ts-sdk";
    import { readFile } from "node:fs/promises";
    import { fileURLToPath } from "node:url";
    import {
      createPublicClient,
      createWalletClient,
      http,
      parseAbi,
      type Address,
      type Hex,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    type Attestation = {
      message: string;
      attestation: string;
    };

    const APTOS_DOMAIN = 9; // Source domain ID for Aptos Testnet
    const APTOS_USDC_ADDRESS =
      "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832";
    const BYTECODE_PATH = fileURLToPath(
      new URL("./precompiled-move-scripts/deposit_for_burn.mv", import.meta.url),
    );

    const EVM_DESTINATION_DOMAIN = 26; // Arc Testnet
    const MESSAGE_TRANSMITTER_V2_ADDRESS =
      "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275" as Address;
    const AMOUNT = 1_000_000; // 1 USDC, 6 decimals
    const MAX_FEE = 0;
    const MIN_FINALITY_THRESHOLD = 2_000; // Standard Transfer
    const IRIS_API_URL = "https://iris-api-sandbox.circle.com";
    ```

    `APTOS_DOMAIN` is the source domain used when requesting the attestation. The
    destination domain for Arc Testnet is 26. Aptos Testnet USDC uses 6 decimals, so
    `1_000_000` represents 1 USDC.

    ### 2.2. Set up wallet clients

    The EVM clients connect to Arc Testnet with `viem`. The Aptos client uses the
    Aptos Testnet network and signs transactions with the Aptos account's Ed25519
    private key.

    ```ts TypeScript theme={null}
    const evmPrivateKey = process.env.EVM_PRIVATE_KEY;
    const aptosPrivateKey = process.env.APTOS_PRIVATE_KEY;

    if (!evmPrivateKey || !aptosPrivateKey) {
      throw new Error("Set EVM_PRIVATE_KEY and APTOS_PRIVATE_KEY in .env");
    }

    const account = privateKeyToAccount(evmPrivateKey as Hex);
    const walletClient = createWalletClient({
      account,
      chain: arcTestnet,
      transport: http(),
    });
    const publicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });
    const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET }));
    const aptosAccount = Account.fromPrivateKey({
      privateKey: new Ed25519PrivateKey(aptosPrivateKey),
    });

    const messageTransmitterAbi = parseAbi([
      "function receiveMessage(bytes message, bytes attestation) returns (bool success)",
    ]);
    ```

    ### 2.3. Format the Arc recipient for Aptos

    The Aptos `deposit_for_burn.mv` script accepts the mint recipient as an Aptos
    address. An EVM address is converted to a 32-byte Aptos address by left-padding
    it with zeros.

    ```ts TypeScript theme={null}
    function toAptosAddress(evmAddress: Address): AccountAddress {
      return AccountAddress.from(`0x${evmAddress.slice(2).padStart(64, "0")}`);
    }
    ```

    ## Step 3: Implement the transfer logic

    This step implements the core transfer logic: burn on Aptos, poll for an
    attestation, then mint on Arc. The precompiled Aptos script withdraws USDC from
    the account's primary fungible store, so there is no separate approval
    transaction.

    ### 3.1. Burn USDC on Aptos

    Submit `deposit_for_burn.mv` to burn USDC on Aptos Testnet. The script submits
    the following parameters:

    * **Burn amount**: The amount of USDC to burn in Aptos subunits (6 decimals)
    * **Destination domain**: The target blockchain for minting USDC (26 for Arc
      Testnet)
    * **Mint recipient**: The EVM wallet address that receives minted USDC on Arc,
      represented as a 32-byte Aptos address
    * **Destination caller**: The Aptos zero address, allowing any caller to submit
      the receive transaction on Arc
    * **Burn token**: The Aptos Testnet USDC address
    * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
    * **Finality threshold**: `2000` for a
      [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)

    ```ts TypeScript theme={null}
    async function burnUSDC() {
      console.log(`Burning ${AMOUNT / 1_000_000} USDC on Aptos...`);

      const bytecode = new Uint8Array(await readFile(BYTECODE_PATH));
      const transaction = await aptosClient.transaction.build.simple({
        sender: aptosAccount.accountAddress,
        data: {
          bytecode,
          functionArguments: [
            new U64(AMOUNT),
            new U32(EVM_DESTINATION_DOMAIN),
            toAptosAddress(account.address),
            AccountAddress.from("0x0"),
            AccountAddress.from(APTOS_USDC_ADDRESS),
            new U64(MAX_FEE),
            new U32(MIN_FINALITY_THRESHOLD),
          ],
        },
      });
      const pendingTransaction = await aptosClient.signAndSubmitTransaction({
        signer: aptosAccount,
        transaction,
      });
      const depositForBurnTransaction = await aptosClient.waitForTransaction({
        transactionHash: pendingTransaction.hash,
      });
      console.log(
        `Deposit-for-burn confirmed: https://explorer.aptoslabs.com/txn/${depositForBurnTransaction.hash}`,
      );

      return depositForBurnTransaction;
    }
    ```

    ### 3.2. Retrieve attestation

    Retrieve the attestation required to complete the CCTP transfer by calling
    Circle's attestation API.

    * Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2)
      API endpoint to retrieve the attestation.
    * Pass `APTOS_DOMAIN` for the `sourceDomain` path parameter, using the
      [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
      for Aptos Testnet (9).
    * Pass the Aptos transaction hash returned by `burnUSDC` as `transactionHash`.

    ```ts TypeScript theme={null}
    async function retrieveAttestation(
      transactionHash: string,
    ): Promise<Attestation> {
      const url = `${IRIS_API_URL}/v2/messages/${APTOS_DOMAIN}?transactionHash=${transactionHash}`;
      console.log(`Polling Iris V2 for attestation: ${url}`);

      while (true) {
        const response = await fetch(url);
        if (!response.ok && response.status !== 404) {
          throw new Error(`Iris V2 returned HTTP ${response.status}`);
        }

        if (response.ok) {
          const data = (await response.json()) as {
            messages?: Array<{ message?: string; attestation?: string }>;
          };
          const result = data.messages?.[0];

          if (
            result?.message &&
            result.attestation &&
            result.attestation !== "PENDING"
          ) {
            return {
              message: result.message,
              attestation: result.attestation,
            };
          }
        }

        await new Promise((resolve) => setTimeout(resolve, 2_000));
      }
    }
    ```

    ### 3.3. Mint USDC on Arc

    Call the
    [`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage)
    from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses)
    deployed on Arc Testnet.

    * Pass the signed attestation and message bytes as parameters.
    * The contract verifies the attestation and mints USDC to the recipient encoded
      in the CCTP message.

    ```ts TypeScript theme={null}
    async function mintUSDC({ message, attestation }: Attestation) {
      console.log("Receiving the message on EVM...");

      const receiveTransactionHash = await walletClient.writeContract({
        account,
        chain: arcTestnet,
        address: MESSAGE_TRANSMITTER_V2_ADDRESS,
        abi: messageTransmitterAbi,
        functionName: "receiveMessage",
        args: [message as Hex, attestation as Hex],
      });
      const receiveReceipt = await publicClient.waitForTransactionReceipt({
        hash: receiveTransactionHash,
      });
      if (receiveReceipt.status !== "success") {
        throw new Error(
          `EVM receive transaction reverted: ${receiveTransactionHash}`,
        );
      }
      console.log(`Receive confirmed: ${receiveTransactionHash}`);

      return receiveTransactionHash;
    }
    ```

    ## Step 4: Full script

    Create an `index.ts` file in your project directory and paste the full script
    following. Keep `deposit_for_burn.mv` in the `precompiled-move-scripts`
    directory next to it.

    ```ts index.ts expandable theme={null}
    import {
      Account,
      AccountAddress,
      Aptos,
      AptosConfig,
      Ed25519PrivateKey,
      Network,
      U32,
      U64,
    } from "@aptos-labs/ts-sdk";
    import { readFile } from "node:fs/promises";
    import { resolve } from "node:path";
    import { fileURLToPath, pathToFileURL } from "node:url";
    import {
      createPublicClient,
      createWalletClient,
      http,
      parseAbi,
      type Address,
      type Hex,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    type Attestation = {
      message: string;
      attestation: string;
    };

    const evmPrivateKey = process.env.EVM_PRIVATE_KEY;
    const aptosPrivateKey = process.env.APTOS_PRIVATE_KEY;

    if (!evmPrivateKey || !aptosPrivateKey) {
      throw new Error("Set EVM_PRIVATE_KEY and APTOS_PRIVATE_KEY in .env");
    }

    const APTOS_DOMAIN = 9;
    const APTOS_USDC_ADDRESS =
      "0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832";
    const BYTECODE_PATH = fileURLToPath(
      new URL("./precompiled-move-scripts/deposit_for_burn.mv", import.meta.url),
    );

    const EVM_DESTINATION_DOMAIN = 26; // Arc Testnet
    const MESSAGE_TRANSMITTER_V2_ADDRESS =
      "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275" as Address;
    const AMOUNT = 1_000_000; // 1 USDC, 6 decimals
    const MAX_FEE = 0;
    const MIN_FINALITY_THRESHOLD = 2_000; // finalized transfer
    const IRIS_API_URL = "https://iris-api-sandbox.circle.com";

    const account = privateKeyToAccount(evmPrivateKey as Hex);
    const walletClient = createWalletClient({
      account,
      chain: arcTestnet,
      transport: http(),
    });
    const publicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });
    const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET }));
    const aptosAccount = Account.fromPrivateKey({
      privateKey: new Ed25519PrivateKey(aptosPrivateKey),
    });

    function toAptosAddress(evmAddress: Address): AccountAddress {
      return AccountAddress.from(`0x${evmAddress.slice(2).padStart(64, "0")}`);
    }

    const messageTransmitterAbi = parseAbi([
      "function receiveMessage(bytes message, bytes attestation) returns (bool success)",
    ]);

    async function burnUSDC() {
      console.log(`Burning ${AMOUNT / 1_000_000} USDC on Aptos...`);

      const bytecode = new Uint8Array(await readFile(BYTECODE_PATH));
      const transaction = await aptosClient.transaction.build.simple({
        sender: aptosAccount.accountAddress,
        data: {
          bytecode,
          functionArguments: [
            new U64(AMOUNT),
            new U32(EVM_DESTINATION_DOMAIN),
            toAptosAddress(account.address),
            AccountAddress.from("0x0"),
            AccountAddress.from(APTOS_USDC_ADDRESS),
            new U64(MAX_FEE),
            new U32(MIN_FINALITY_THRESHOLD),
          ],
        },
      });
      const pendingTransaction = await aptosClient.signAndSubmitTransaction({
        signer: aptosAccount,
        transaction,
      });
      const depositForBurnTransaction = await aptosClient.waitForTransaction({
        transactionHash: pendingTransaction.hash,
      });
      console.log(
        `Deposit-for-burn confirmed: https://explorer.aptoslabs.com/txn/${depositForBurnTransaction.hash}`,
      );

      return depositForBurnTransaction;
    }

    async function retrieveAttestation(
      transactionHash: string,
    ): Promise<Attestation> {
      const url = `${IRIS_API_URL}/v2/messages/${APTOS_DOMAIN}?transactionHash=${transactionHash}`;
      console.log(`Polling Iris V2 for attestation: ${url}`);

      while (true) {
        const response = await fetch(url);
        if (!response.ok && response.status !== 404) {
          throw new Error(`Iris V2 returned HTTP ${response.status}`);
        }

        if (response.ok) {
          const data = (await response.json()) as {
            messages?: Array<{ message?: string; attestation?: string }>;
          };
          const result = data.messages?.[0];

          if (
            result?.message &&
            result.attestation &&
            result.attestation !== "PENDING"
          ) {
            return {
              message: result.message,
              attestation: result.attestation,
            };
          }
        }

        await new Promise((resolve) => setTimeout(resolve, 2_000));
      }
    }

    async function mintUSDC({ message, attestation }: Attestation) {
      console.log("Receiving the message on EVM...");

      const receiveTransactionHash = await walletClient.writeContract({
        account,
        chain: arcTestnet,
        address: MESSAGE_TRANSMITTER_V2_ADDRESS,
        abi: messageTransmitterAbi,
        functionName: "receiveMessage",
        args: [message as Hex, attestation as Hex],
      });
      const receiveReceipt = await publicClient.waitForTransactionReceipt({
        hash: receiveTransactionHash,
      });
      if (receiveReceipt.status !== "success") {
        throw new Error(
          `EVM receive transaction reverted: ${receiveTransactionHash}`,
        );
      }
      console.log(`Receive confirmed: ${receiveTransactionHash}`);

      return receiveTransactionHash;
    }

    export default async function main() {
      const depositForBurnTransaction = await burnUSDC();
      const attestation = await retrieveAttestation(depositForBurnTransaction.hash);
      console.log("Attestation received from Iris V2.");
      const receiveTransactionHash = await mintUSDC(attestation);
      console.log("USDC transfer completed.");

      return {
        aptosTransactionHash: depositForBurnTransaction.hash,
        evmTransactionHash: receiveTransactionHash,
      };
    }

    if (
      process.argv[1] &&
      import.meta.url === pathToFileURL(resolve(process.argv[1])).href
    ) {
      console.log("Starting CCTP flow...");
      main().catch((error: unknown) => {
        console.error(error instanceof Error ? error.message : error);
        process.exitCode = 1;
      });
    }
    ```

    ## Step 5: Test the script

    Run the following command to execute the script:

    ```shell Shell theme={null}
    npm run start
    ```

    When the transfer finishes, the console logs a completion message and the
    relevant transaction hashes. Successful output looks similar to the following:

    ```bash Shell theme={null}
    Starting CCTP flow...
    Burning 1 USDC on Aptos...
    Deposit-for-burn confirmed: https://explorer.aptoslabs.com/txn/<aptos-transaction-hash>
    Polling Iris V2 for attestation: https://iris-api-sandbox.circle.com/v2/messages/9?transactionHash=<aptos-transaction-hash>
    Attestation received from Iris V2.
    Receiving the message on EVM...
    Receive confirmed: 0x...
    USDC transfer completed.
    ```

    Attestation polling can take several minutes depending on network conditions and
    the finality threshold you chose. The script retries every 2 seconds with no
    timeout, so allow the process to continue while Iris prepares the attestation.

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

  <Tab title="Arc to Aptos">
    This quickstart demonstrates how to transfer USDC from Arc Testnet to Aptos
    Testnet using CCTP. You use [`viem`](https://viem.sh/) to approve and burn USDC
    on Arc, and the
    [`@aptos-labs/ts-sdk`](https://github.com/aptos-labs/aptos-ts-sdk) library to
    submit the precompiled Aptos Move script that receives and mints USDC. When you
    finish, you will have executed a full burn-attest-mint flow.

    You should be comfortable using a terminal and Node.js. Familiarity with basic
    EVM usage and Aptos transactions helps you follow and adapt the script. Examples
    use Arc Testnet as the source, but you can use any
    [supported EVM blockchain](/cctp/concepts/supported-chains-and-domains).

    <Note>
      The Aptos receive transaction uses a precompiled Move script. Download
      `receive_message.mv` from the
      [aptos-cctp repository](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts/testnet/v2)
      and place it in a `precompiled-move-scripts` directory in your project.
    </Note>

    ## Prerequisites

    Before you begin this tutorial, ensure you have:

    * Installed [Node.js v22.6+](https://nodejs.org/)
    * Prepared an EVM wallet with the private key available for Arc Testnet
      * Added the Arc Testnet network to your wallet
        ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup))
      * Funded your wallet with Arc Testnet USDC for the transfer amount from the
        [Circle Faucet](https://faucet.circle.com)
      * Funded your wallet with Arc Testnet ETH for gas fees
    * Prepared an Aptos Testnet wallet with the private key available
      * Funded your account with testnet APT from the
        [Aptos Faucet](https://aptos.dev/en/network/faucet) for the receive
        transaction fee

    ## Step 1: Set up the project

    This step shows you how to prepare your project and environment.

    ### 1.1. Set up your development environment

    Create a new directory, initialize and set up a new Node.js project, and install
    the required dependencies:

    ```bash Shell theme={null}
    mkdir cctp-arc-to-aptos
    cd cctp-arc-to-aptos
    npm init -y

    npm pkg set type=module
    npm pkg set scripts.start="node --env-file=.env --import=tsx index.ts"

    npm install @aptos-labs/ts-sdk viem

    npm install --save-dev tsx typescript @types/node
    ```

    Copy
    [`receive_message.mv`](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts/testnet/v2/receive_message.mv)
    into `precompiled-move-scripts/` in your project. The full script in Step 4
    reads the bytecode relative to `index.ts`.

    ### 1.2. Initialize and configure the project

    <Tip>
      This step is optional. It helps prevent missing types in your IDE or editor.
    </Tip>

    Create a `tsconfig.json` file:

    ```shell theme={null}
    npx tsc --init
    ```

    Then, update the `tsconfig.json` file:

    ```shell theme={null}
    cat <<'EOF' > tsconfig.json
    {
      "compilerOptions": {
        "target": "ESNext",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "strict": true,
        "types": ["node"]
      }
    }
    EOF
    ```

    ### 1.3. Configure environment variables

    Open `.env` in your editor and add:

    ```text theme={null}
    EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
    APTOS_PRIVATE_KEY=YOUR_APTOS_PRIVATE_KEY
    # FAST_TRANSFER=true
    # FAST_MAX_FEE=100
    ```

    * `EVM_PRIVATE_KEY` is the private key for the EVM wallet used to burn USDC on
      Arc Testnet.
    * `APTOS_PRIVATE_KEY` is the private key for the Aptos Testnet account that
      receives the minted USDC.
    * `FAST_TRANSFER` defaults to `false`. Set it to `true` to attempt Fast Transfer
      (check [supported chains](/cctp/concepts/supported-chains-and-domains) for
      route availability).
    * `FAST_MAX_FEE` is the maximum Fast Transfer fee in USDC subunits. The default
      is `100` (`0.0001 USDC`). Set it to the current route fee when needed.

    <Tip>
      Open `.env` in your editor rather than writing values with shell commands, and
      add `.env` to your `.gitignore`. This prevents credentials from leaking into
      your shell history or version control.
    </Tip>

    The `npm run start` command loads variables from `.env` using Node.js native
    env-file support.

    <Warning>
      This example uses one or more private keys for local testing. In production,
      use a secure key management solution and never expose or share private keys.
    </Warning>

    ## Step 2: Configure the script

    Define the contract addresses, transfer amount, finality settings, and wallet
    clients for Arc Testnet and Aptos Testnet.

    ### 2.1. Define configuration constants

    The script predefines the Arc USDC and TokenMessengerV2 addresses, the CCTP
    domain IDs, and the precompiled Move script path.

    ```ts TypeScript theme={null}
    import {
      Account,
      Aptos,
      AptosConfig,
      Ed25519PrivateKey,
      MoveVector,
      Network,
    } from "@aptos-labs/ts-sdk";
    import { readFile } from "node:fs/promises";
    import { fileURLToPath } from "node:url";
    import {
      createPublicClient,
      createWalletClient,
      http,
      parseAbi,
      type Address,
      type Hex,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    type Attestation = {
      message: string;
      attestation: string;
    };

    const APTOS_DOMAIN = 9; // Destination domain ID for Aptos Testnet
    const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
    const ARC_USDC_ADDRESS =
      "0x3600000000000000000000000000000000000000" as Address;
    const TOKEN_MESSENGER_V2_ADDRESS =
      "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA" as Address;
    const BYTECODE_PATH = fileURLToPath(
      new URL("./precompiled-move-scripts/receive_message.mv", import.meta.url),
    );

    const AMOUNT = 1_000_000n; // 1 USDC, 6 decimals
    const FAST_TRANSFER = process.env.FAST_TRANSFER === "true";
    const MIN_FINALITY_THRESHOLD = FAST_TRANSFER ? 1_000 : 2_000;
    const MAX_FEE = FAST_TRANSFER ? BigInt(process.env.FAST_MAX_FEE ?? "100") : 0n;
    const IRIS_API_URL = "https://iris-api-sandbox.circle.com";
    ```

    `ARC_TESTNET_DOMAIN` is the source domain used when requesting the attestation.
    The destination domain for Aptos Testnet is 9. Fast Transfer uses a finality
    threshold of `1000` and a configurable fee; Standard Transfer uses `2000` and
    sets the maximum fee to zero.

    ### 2.2. Set up wallet clients

    The wallet clients use `viem` for Arc Testnet and the Aptos SDK for Aptos
    Testnet. The Aptos account signs the transaction that submits the receive Move
    script.

    ```ts TypeScript theme={null}
    const evmPrivateKey = process.env.EVM_PRIVATE_KEY;
    const aptosPrivateKey = process.env.APTOS_PRIVATE_KEY;

    if (!evmPrivateKey || !aptosPrivateKey) {
      throw new Error("Set EVM_PRIVATE_KEY and APTOS_PRIVATE_KEY in .env");
    }

    const account = privateKeyToAccount(evmPrivateKey as Hex);
    const walletClient = createWalletClient({
      account,
      chain: arcTestnet,
      transport: http(),
    });
    const publicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });
    const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET }));
    const aptosAccount = Account.fromPrivateKey({
      privateKey: new Ed25519PrivateKey(aptosPrivateKey),
    });

    const usdcAbi = parseAbi([
      "function approve(address spender, uint256 amount) returns (bool)",
    ]);
    const tokenMessengerV2Abi = parseAbi([
      "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold)",
    ]);
    ```

    ### 2.3. Format the Aptos recipient for Arc

    `depositForBurn` accepts the destination recipient as `bytes32`. The Aptos
    account address is already a 32-byte value, so the script passes it as a
    hexadecimal value to `viem`.

    ```ts TypeScript theme={null}
    const mintRecipient = aptosAccount.accountAddress.toString() as Hex;
    const destinationCaller = `0x${"0".repeat(64)}` as Hex;
    ```

    ## Step 3: Implement the transfer logic

    This step implements the core transfer logic: approve and burn on Arc, poll for
    an attestation, then receive and mint on Aptos. A successful run prints
    transaction hashes and a completion message in the console.

    ### 3.1. Approve USDC on Arc

    Grant approval for the
    [`TokenMessengerV2` contract](/cctp/references/contract-addresses) to withdraw
    USDC from your Arc wallet. This allows the contract to burn USDC when you
    initiate the transfer.

    ```ts TypeScript theme={null}
    async function approveUSDC() {
      console.log("Approving Arc USDC transfer...");

      const approvalHash = await walletClient.writeContract({
        account,
        chain: arcTestnet,
        address: ARC_USDC_ADDRESS,
        abi: usdcAbi,
        functionName: "approve",
        args: [TOKEN_MESSENGER_V2_ADDRESS, AMOUNT],
      });
      const approvalReceipt = await publicClient.waitForTransactionReceipt({
        hash: approvalHash,
      });
      if (approvalReceipt.status !== "success") {
        throw new Error(`USDC approval reverted: ${approvalHash}`);
      }

      console.log(`Approval confirmed: ${approvalHash}`);
    }
    ```

    ### 3.2. Burn USDC on Arc

    Call `depositForBurn` to burn USDC on Arc. You specify the following parameters:

    * **Burn amount**: The amount of USDC to burn in Arc subunits (6 decimals)
    * **Destination domain**: The target blockchain for minting USDC (9 for Aptos
      Testnet)
    * **Mint recipient**: The Aptos account address that receives minted USDC
    * **Destination caller**: The zero address, allowing any caller to submit the
      receive transaction on Aptos
    * **Burn token**: The Arc Testnet USDC address
    * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
    * **Finality threshold**: `1000` for a
      [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
      or `2000` for a
      [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)

    ```ts TypeScript theme={null}
    async function burnUSDC() {
      const transferMode = FAST_TRANSFER ? "Fast" : "Standard";
      console.log(
        `Burning ${Number(AMOUNT) / 1_000_000} USDC on Arc (${transferMode} Transfer)...`,
      );

      const mintRecipient = aptosAccount.accountAddress.toString() as Hex;
      const destinationCaller = `0x${"0".repeat(64)}` as Hex;
      const burnHash = await walletClient.writeContract({
        account,
        chain: arcTestnet,
        address: TOKEN_MESSENGER_V2_ADDRESS,
        abi: tokenMessengerV2Abi,
        functionName: "depositForBurn",
        args: [
          AMOUNT,
          APTOS_DOMAIN,
          mintRecipient,
          ARC_USDC_ADDRESS,
          destinationCaller,
          MAX_FEE,
          MIN_FINALITY_THRESHOLD,
        ],
      });
      const burnReceipt = await publicClient.waitForTransactionReceipt({
        hash: burnHash,
      });
      if (burnReceipt.status !== "success") {
        throw new Error(`Deposit-for-burn reverted: ${burnHash}`);
      }

      console.log(`Deposit-for-burn confirmed: ${burnHash}`);
      return burnHash;
    }
    ```

    ### 3.3. Retrieve attestation

    Retrieve the attestation required to complete the CCTP transfer by calling
    Circle's attestation API.

    * Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2)
      API endpoint to retrieve the attestation.
    * Pass `ARC_TESTNET_DOMAIN` for the `sourceDomain` path parameter, using the
      [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
      for Arc Testnet (26).
    * Pass the Arc transaction hash returned by `burnUSDC` as `transactionHash`.

    ```ts TypeScript theme={null}
    async function retrieveAttestation(
      transactionHash: string,
    ): Promise<Attestation> {
      const url = `${IRIS_API_URL}/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
      console.log(`Polling Iris V2 for attestation: ${url}`);

      while (true) {
        const response = await fetch(url);
        if (!response.ok && response.status !== 404) {
          throw new Error(`Iris V2 returned HTTP ${response.status}`);
        }

        if (response.ok) {
          const data = (await response.json()) as {
            messages?: Array<{ message?: string; attestation?: string }>;
          };
          const result = data.messages?.[0];

          if (
            result?.message &&
            result.attestation &&
            result.attestation !== "PENDING"
          ) {
            return {
              message: result.message,
              attestation: result.attestation,
            };
          }
        }

        await new Promise((resolve) => setTimeout(resolve, 2_000));
      }
    }
    ```

    ### 3.4. Receive and mint USDC on Aptos

    Submit `receive_message.mv` to Aptos Testnet with the message and attestation
    returned by Iris V2. The precompiled script calls the Aptos CCTP message
    transmitter, prepares the mint, and deposits USDC into the recipient's primary
    fungible store in one transaction.

    ```ts TypeScript theme={null}
    async function receiveMessage({ message, attestation }: Attestation) {
      console.log("Receiving the message on Aptos...");

      const bytecode = new Uint8Array(await readFile(BYTECODE_PATH));
      const transaction = await aptosClient.transaction.build.simple({
        sender: aptosAccount.accountAddress,
        data: {
          bytecode,
          functionArguments: [
            MoveVector.U8(Buffer.from(message.replace("0x", ""), "hex")),
            MoveVector.U8(Buffer.from(attestation.replace("0x", ""), "hex")),
          ],
        },
      });
      const pendingTransaction = await aptosClient.signAndSubmitTransaction({
        signer: aptosAccount,
        transaction,
      });
      const receiveTransaction = await aptosClient.waitForTransaction({
        transactionHash: pendingTransaction.hash,
      });

      console.log(
        `Receive-message confirmed: https://explorer.aptoslabs.com/txn/${receiveTransaction.hash}`,
      );
      return receiveTransaction.hash;
    }
    ```

    ## Step 4: Full script

    Create an `index.ts` file in your project directory and paste the full script
    following. Keep `receive_message.mv` in the `precompiled-move-scripts` directory
    next to it.

    ```ts index.ts expandable theme={null}
    import {
      Account,
      Aptos,
      AptosConfig,
      Ed25519PrivateKey,
      MoveVector,
      Network,
    } from "@aptos-labs/ts-sdk";
    import { readFile } from "node:fs/promises";
    import { resolve } from "node:path";
    import { fileURLToPath, pathToFileURL } from "node:url";
    import {
      createPublicClient,
      createWalletClient,
      http,
      parseAbi,
      type Address,
      type Hex,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    type Attestation = {
      message: string;
      attestation: string;
    };

    const evmPrivateKey = process.env.EVM_PRIVATE_KEY;
    const aptosPrivateKey = process.env.APTOS_PRIVATE_KEY;

    if (!evmPrivateKey || !aptosPrivateKey) {
      throw new Error("Set EVM_PRIVATE_KEY and APTOS_PRIVATE_KEY in .env");
    }

    const APTOS_DOMAIN = 9;
    const ARC_TESTNET_DOMAIN = 26;
    const ARC_USDC_ADDRESS =
      "0x3600000000000000000000000000000000000000" as Address;
    const TOKEN_MESSENGER_V2_ADDRESS =
      "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA" as Address;
    const BYTECODE_PATH = fileURLToPath(
      new URL("./precompiled-move-scripts/receive_message.mv", import.meta.url),
    );

    const AMOUNT = 1_000_000n; // 1 USDC, 6 decimals
    const FAST_TRANSFER = process.env.FAST_TRANSFER === "true";
    const MIN_FINALITY_THRESHOLD = FAST_TRANSFER ? 1_000 : 2_000;
    const MAX_FEE = FAST_TRANSFER ? BigInt(process.env.FAST_MAX_FEE ?? "100") : 0n;
    const IRIS_API_URL = "https://iris-api-sandbox.circle.com";

    const account = privateKeyToAccount(evmPrivateKey as Hex);
    const walletClient = createWalletClient({
      account,
      chain: arcTestnet,
      transport: http(),
    });
    const publicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });
    const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET }));
    const aptosAccount = Account.fromPrivateKey({
      privateKey: new Ed25519PrivateKey(aptosPrivateKey),
    });

    const usdcAbi = parseAbi([
      "function approve(address spender, uint256 amount) returns (bool)",
    ]);
    const tokenMessengerV2Abi = parseAbi([
      "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold)",
    ]);

    async function approveUSDC() {
      console.log("Approving Arc USDC transfer...");

      const approvalHash = await walletClient.writeContract({
        account,
        chain: arcTestnet,
        address: ARC_USDC_ADDRESS,
        abi: usdcAbi,
        functionName: "approve",
        args: [TOKEN_MESSENGER_V2_ADDRESS, AMOUNT],
      });
      const approvalReceipt = await publicClient.waitForTransactionReceipt({
        hash: approvalHash,
      });
      if (approvalReceipt.status !== "success") {
        throw new Error(`USDC approval reverted: ${approvalHash}`);
      }

      console.log(`Approval confirmed: ${approvalHash}`);
    }

    async function burnUSDC() {
      const transferMode = FAST_TRANSFER ? "Fast" : "Standard";
      console.log(
        `Burning ${Number(AMOUNT) / 1_000_000} USDC on Arc (${transferMode} Transfer)...`,
      );

      const mintRecipient = aptosAccount.accountAddress.toString() as Hex;
      const destinationCaller = `0x${"0".repeat(64)}` as Hex;
      const burnHash = await walletClient.writeContract({
        account,
        chain: arcTestnet,
        address: TOKEN_MESSENGER_V2_ADDRESS,
        abi: tokenMessengerV2Abi,
        functionName: "depositForBurn",
        args: [
          AMOUNT,
          APTOS_DOMAIN,
          mintRecipient,
          ARC_USDC_ADDRESS,
          destinationCaller,
          MAX_FEE,
          MIN_FINALITY_THRESHOLD,
        ],
      });
      const burnReceipt = await publicClient.waitForTransactionReceipt({
        hash: burnHash,
      });
      if (burnReceipt.status !== "success") {
        throw new Error(`Deposit-for-burn reverted: ${burnHash}`);
      }

      console.log(`Deposit-for-burn confirmed: ${burnHash}`);
      return burnHash;
    }

    async function retrieveAttestation(
      transactionHash: string,
    ): Promise<Attestation> {
      const url = `${IRIS_API_URL}/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
      console.log(`Polling Iris V2 for attestation: ${url}`);

      while (true) {
        const response = await fetch(url);
        if (!response.ok && response.status !== 404) {
          throw new Error(`Iris V2 returned HTTP ${response.status}`);
        }

        if (response.ok) {
          const data = (await response.json()) as {
            messages?: Array<{ message?: string; attestation?: string }>;
          };
          const result = data.messages?.[0];

          if (
            result?.message &&
            result.attestation &&
            result.attestation !== "PENDING"
          ) {
            return {
              message: result.message,
              attestation: result.attestation,
            };
          }
        }

        await new Promise((resolve) => setTimeout(resolve, 2_000));
      }
    }

    async function receiveMessage({ message, attestation }: Attestation) {
      console.log("Receiving the message on Aptos...");

      const bytecode = new Uint8Array(await readFile(BYTECODE_PATH));
      const transaction = await aptosClient.transaction.build.simple({
        sender: aptosAccount.accountAddress,
        data: {
          bytecode,
          functionArguments: [
            MoveVector.U8(Buffer.from(message.replace("0x", ""), "hex")),
            MoveVector.U8(Buffer.from(attestation.replace("0x", ""), "hex")),
          ],
        },
      });
      const pendingTransaction = await aptosClient.signAndSubmitTransaction({
        signer: aptosAccount,
        transaction,
      });
      const receiveTransaction = await aptosClient.waitForTransaction({
        transactionHash: pendingTransaction.hash,
      });

      console.log(
        `Receive-message confirmed: https://explorer.aptoslabs.com/txn/${receiveTransaction.hash}`,
      );
      return receiveTransaction.hash;
    }

    export default async function main() {
      await approveUSDC();
      const burnTransactionHash = await burnUSDC();
      const attestation = await retrieveAttestation(burnTransactionHash);
      console.log("Attestation received from Iris V2.");
      const receiveTransactionHash = await receiveMessage(attestation);
      console.log("USDC transfer completed.");

      return {
        evmTransactionHash: burnTransactionHash,
        aptosTransactionHash: receiveTransactionHash,
      };
    }

    if (
      process.argv[1] &&
      import.meta.url === pathToFileURL(resolve(process.argv[1])).href
    ) {
      console.log("Starting CCTP flow...");
      main().catch((error: unknown) => {
        console.error(error instanceof Error ? error.message : error);
        process.exitCode = 1;
      });
    }
    ```

    ## Step 5: Test the script

    Run the following command to execute the script:

    ```shell Shell theme={null}
    npm run start
    ```

    When the transfer finishes, the console logs a completion message and the
    relevant transaction hashes. Successful output looks similar to the following:

    ```bash Shell theme={null}
    Starting CCTP flow...
    Approving Arc USDC transfer...
    Approval confirmed: 0x...
    Burning 1 USDC on Arc (Fast Transfer)...
    Deposit-for-burn confirmed: 0x...
    Polling Iris V2 for attestation: https://iris-api-sandbox.circle.com/v2/messages/26?transactionHash=<arc-transaction-hash>
    Attestation received from Iris V2.
    Receiving the message on Aptos...
    Receive-message confirmed: https://explorer.aptoslabs.com/txn/<aptos-transaction-hash>
    USDC transfer completed.
    ```

    Attestation polling can take several minutes depending on network conditions and
    the finality threshold you chose. The script retries every 2 seconds with no
    timeout, so allow the process to continue while Iris prepares the attestation.

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