> ## 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 to and from Stellar

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

Use CCTP to transfer USDC with Stellar Testnet as the source or destination.

<Note>
  On Stellar, USDC precision and address encoding differ from other CCTP-supported
  blockchains. Before you integrate beyond these examples, read
  [CCTP on Stellar](/cctp/references/stellar).
</Note>

Pick the tab that matches the direction of your transfer.

<Tabs>
  <Tab title="Stellar to Arc">
    This quickstart demonstrates how to transfer USDC from Stellar Testnet to Arc
    Testnet using CCTP. You use the
    [@stellar/stellar-sdk](https://github.com/stellar/js-stellar-sdk) library to
    interact with Stellar Soroban contracts, 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 Stellar
    Soroban 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 blockchain](/cctp/concepts/supported-chains-and-domains).

    ## Prerequisites

    Before you begin this tutorial, ensure you have:

    * Installed [Node.js v22+](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 gas fees) from the
        [Circle Faucet](https://faucet.circle.com)
    * Prepared a Stellar Testnet wallet with the secret key (`S...`) available
      * Funded your Stellar wallet with testnet XLM from the
        [Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees
        on Stellar Testnet)
      * Established a
        [USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) on
        your Stellar account so you can hold the testnet USDC you burn
      * Funded your Stellar wallet with Stellar Testnet USDC from the
        [Circle Faucet](https://faucet.circle.com)

    <Tip>
      You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund
      accounts and establish USDC trustlines.
    </Tip>

    ## 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 and install the required dependencies:

    ```bash Shell theme={null}
    # Set up your directory and initialize a Node.js project
    mkdir cctp-stellar-to-arc
    cd cctp-stellar-to-arc
    npm init -y

    # Set up module type and start command
    npm pkg set type=module
    npm pkg set scripts.start="npx tsx --env-file=.env index.ts"

    # Install runtime dependencies
    npm install @stellar/stellar-sdk viem

    # Install dev dependencies
    npm install --save-dev typescript @types/node
    ```

    ### 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}
    STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
    EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
    ```

    * `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban
      transactions on Stellar Testnet.
    * `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use 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 contract addresses, amounts, and clients for Stellar Testnet and Arc
    Testnet.

    ### 2.1. Define configuration constants

    The script predefines the contract addresses, transfer amount, and maximum fee.

    ```ts TypeScript theme={null}
    import {
      Address,
      Contract,
      Keypair,
      nativeToScVal,
      rpc,
      TransactionBuilder,
      xdr,
    } from "@stellar/stellar-sdk";
    import {
      createPublicClient,
      createWalletClient,
      encodeFunctionData,
      http,
      pad,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    interface AttestationMessage {
      message: string;
      attestation: string;
      status: string;
    }

    interface AttestationResponse {
      messages: AttestationMessage[];
    }

    // Contract Addresses
    const STELLAR_TOKEN_MESSENGER_MINTER =
      "CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
    const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
    const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";

    // Transfer Parameters
    const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
    const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)

    // Chain-specific Parameters
    const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
    const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet

    // Stellar Soroban Configuration
    const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
    const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";

    // Authentication
    const stellarKeypair = Keypair.fromSecret(
      process.env.STELLAR_SECRET_KEY as string,
    );
    ```

    ### 2.2. Set up wallet clients

    The wallet client configures the appropriate network settings using `viem`. In
    this example, the script connects to Arc Testnet.

    ```ts TypeScript theme={null}
    const evmAccount = privateKeyToAccount(
      process.env.EVM_PRIVATE_KEY as `0x${string}`,
    );

    const arcWalletClient = createWalletClient({
      chain: arcTestnet,
      transport: http(),
      account: evmAccount,
    });

    const arcPublicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });
    ```

    ### 2.3. Add helper function

    The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban
    contract transaction.

    ```ts TypeScript theme={null}
    async function submitSorobanTx(
      server: rpc.Server,
      contractId: string,
      method: string,
      args: xdr.ScVal[],
    ) {
      const account = await server.getAccount(stellarKeypair.publicKey());
      const contract = new Contract(contractId);

      const tx = new TransactionBuilder(account, {
        fee: "10000000",
        networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
      })
        .addOperation(contract.call(method, ...args))
        .setTimeout(120)
        .build();

      const simulated = await server.simulateTransaction(tx);
      if (rpc.Api.isSimulationError(simulated)) {
        throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
      }

      const prepared = rpc.assembleTransaction(tx, simulated).build();
      prepared.sign(stellarKeypair);

      const sendResult = await server.sendTransaction(prepared);
      if (sendResult.status === "ERROR") {
        throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
      }

      let getResult = await server.getTransaction(sendResult.hash);
      while (getResult.status === "NOT_FOUND") {
        await new Promise((resolve) => setTimeout(resolve, 2000));
        getResult = await server.getTransaction(sendResult.hash);
      }

      if (getResult.status !== "SUCCESS") {
        throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
      }

      return sendResult.hash;
    }
    ```

    ## Step 3: Implement the transfer logic

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

    ### 3.1. Approve USDC on Stellar

    Approve the `TokenMessengerMinterV2` contract to spend your USDC. The
    `submitSorobanTx` helper is used to submit the approve call to the Stellar USDC
    contract.

    ```ts TypeScript theme={null}
    async function approveUSDC() {
      console.log("Approving USDC spend on Stellar...");
      const server = new rpc.Server(STELLAR_RPC_URL);
      const latestLedger = await server.getLatestLedger();
      const expirationLedger = latestLedger.sequence + 100_000;

      const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
        new Address(stellarKeypair.publicKey()).toScVal(),
        new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
        nativeToScVal(AMOUNT, { type: "i128" }),
        nativeToScVal(expirationLedger, { type: "u32" }),
      ]);
      console.log(`Approve Tx: ${approveHash}`);
    }
    ```

    ### 3.2. Burn USDC on Stellar

    Call `deposit_for_burn` to burn USDC on Stellar. The `submitSorobanTx` helper is
    used to submit the burn call with the transfer parameters listed below:

    * **Burn amount**: The amount of USDC to burn (in Stellar subunits, 7 decimals)
    * **Destination domain**: The target blockchain for minting USDC (see
      [supported blockchains and domains](/cctp/concepts/supported-chains-and-domains))
    * **Mint recipient**: The wallet address that receives the minted USDC on Arc
    * **Burn token**: The contract address of the USDC token on Stellar
    * **Destination caller**: The address on the target blockchain that may call
      `receiveMessage`
    * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
      (in Stellar subunits, 7 decimals)
    * **Finality threshold**: Determines whether it's a
      [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
      (1000 or less) or a
      [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
      (2000 or more)

    ```ts TypeScript theme={null}
    async function burnUSDC() {
      console.log("Burning USDC on Stellar...");
      // Bytes32 Formatted Parameters
      const evmAddressBytes32 = pad(evmAccount.address);
      const mintRecipient = xdr.ScVal.scvBytes(
        Buffer.from(evmAddressBytes32.slice(2), "hex"),
      );
      const server = new rpc.Server(STELLAR_RPC_URL);

      const txHash = await submitSorobanTx(
        server,
        STELLAR_TOKEN_MESSENGER_MINTER,
        "deposit_for_burn",
        [
          new Address(stellarKeypair.publicKey()).toScVal(),
          nativeToScVal(AMOUNT, { type: "i128" }),
          nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }),
          mintRecipient,
          new Address(STELLAR_USDC).toScVal(),
          xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller
          nativeToScVal(MAX_FEE, { type: "i128" }),
          nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold
        ],
      );
      console.log(`Burn Tx: ${txHash}`);
      return txHash;
    }
    ```

    ### 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 `STELLAR_DOMAIN` for the `sourceDomain` path parameter, using the
      [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
      for Stellar Testnet (27).
    * Pass `transactionHash` from the value returned by `submitSorobanTx` within the
      `burnUSDC` function above.

    ```ts TypeScript theme={null}
    async function retrieveAttestation(transactionHash: string) {
      console.log("Retrieving attestation...");
      const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`;
      while (true) {
        try {
          const response = await fetch(url, { method: "GET" });

          if (!response.ok) {
            if (response.status !== 404) {
              const text = await response.text().catch(() => "");
              console.error(
                "Error fetching attestation:",
                `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
              );
            }
            await new Promise((resolve) => setTimeout(resolve, 5000));
            continue;
          }

          const data = (await response.json()) as AttestationResponse;

          if (data?.messages?.[0]?.status === "complete") {
            console.log("Attestation retrieved successfully!");
            return data.messages[0];
          }
          console.log("Waiting for attestation...");
          await new Promise((resolve) => setTimeout(resolve, 5000));
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          console.error("Error fetching attestation:", message);
          await new Promise((resolve) => setTimeout(resolve, 5000));
        }
      }
    }
    ```

    ### 3.4. 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 to mint USDC on the destination blockchain.

    * Pass the signed attestation and the 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 mintUSDCOnArc(attestation: AttestationMessage) {
      console.log("Minting USDC on Arc Testnet...");
      const hash = await arcWalletClient.sendTransaction({
        to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
        data: encodeFunctionData({
          abi: [
            {
              type: "function",
              name: "receiveMessage",
              stateMutability: "nonpayable",
              inputs: [
                { name: "message", type: "bytes" },
                { name: "attestation", type: "bytes" },
              ],
              outputs: [],
            },
          ],
          functionName: "receiveMessage",
          args: [
            attestation.message as `0x${string}`,
            attestation.attestation as `0x${string}`,
          ],
        }),
      });
      await arcPublicClient.waitForTransactionReceipt({ hash });
      console.log(`Mint Tx: ${hash}`);
    }
    ```

    ## Step 4: Full script

    Create an `index.ts` file in your project directory and paste the full script
    below so you can run the flow from one file.

    ```ts index.ts expandable theme={null}
    import {
      Address,
      Contract,
      Keypair,
      nativeToScVal,
      rpc,
      TransactionBuilder,
      xdr,
    } from "@stellar/stellar-sdk";
    import {
      createPublicClient,
      createWalletClient,
      encodeFunctionData,
      http,
      pad,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    interface AttestationMessage {
      message: string;
      attestation: string;
      status: string;
    }

    interface AttestationResponse {
      messages: AttestationMessage[];
    }

    // ============ Configuration Constants ============
    // Contract Addresses
    const STELLAR_TOKEN_MESSENGER_MINTER =
      "CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
    const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
    const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";

    // Transfer Parameters
    const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
    const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)

    // Chain-specific Parameters
    const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
    const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet

    // Stellar Soroban Configuration
    const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
    const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";

    // Authentication
    const stellarKeypair = Keypair.fromSecret(
      process.env.STELLAR_SECRET_KEY as string,
    );

    // Set up wallet clients
    const evmAccount = privateKeyToAccount(
      process.env.EVM_PRIVATE_KEY as `0x${string}`,
    );
    const arcWalletClient = createWalletClient({
      chain: arcTestnet,
      transport: http(),
      account: evmAccount,
    });
    const arcPublicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });

    async function submitSorobanTx(
      server: rpc.Server,
      contractId: string,
      method: string,
      args: xdr.ScVal[],
    ) {
      const account = await server.getAccount(stellarKeypair.publicKey());
      const contract = new Contract(contractId);

      const tx = new TransactionBuilder(account, {
        fee: "10000000",
        networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
      })
        .addOperation(contract.call(method, ...args))
        .setTimeout(120)
        .build();

      const simulated = await server.simulateTransaction(tx);
      if (rpc.Api.isSimulationError(simulated)) {
        throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
      }

      const prepared = rpc.assembleTransaction(tx, simulated).build();
      prepared.sign(stellarKeypair);

      const sendResult = await server.sendTransaction(prepared);
      if (sendResult.status === "ERROR") {
        throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
      }

      let getResult = await server.getTransaction(sendResult.hash);
      while (getResult.status === "NOT_FOUND") {
        await new Promise((resolve) => setTimeout(resolve, 2000));
        getResult = await server.getTransaction(sendResult.hash);
      }

      if (getResult.status !== "SUCCESS") {
        throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
      }

      return sendResult.hash;
    }

    // ============ CCTP Flow Functions ============
    async function approveUSDC() {
      console.log("Approving USDC spend on Stellar...");
      const server = new rpc.Server(STELLAR_RPC_URL);
      const latestLedger = await server.getLatestLedger();
      const expirationLedger = latestLedger.sequence + 100_000;

      const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
        new Address(stellarKeypair.publicKey()).toScVal(),
        new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
        nativeToScVal(AMOUNT, { type: "i128" }),
        nativeToScVal(expirationLedger, { type: "u32" }),
      ]);
      console.log(`Approve Tx: ${approveHash}`);
    }

    async function burnUSDC() {
      console.log("Burning USDC on Stellar...");
      // Bytes32 Formatted Parameters
      const evmAddressBytes32 = pad(evmAccount.address);
      const mintRecipient = xdr.ScVal.scvBytes(
        Buffer.from(evmAddressBytes32.slice(2), "hex"),
      );
      const server = new rpc.Server(STELLAR_RPC_URL);

      const txHash = await submitSorobanTx(
        server,
        STELLAR_TOKEN_MESSENGER_MINTER,
        "deposit_for_burn",
        [
          new Address(stellarKeypair.publicKey()).toScVal(),
          nativeToScVal(AMOUNT, { type: "i128" }),
          nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }),
          mintRecipient,
          new Address(STELLAR_USDC).toScVal(),
          xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller
          nativeToScVal(MAX_FEE, { type: "i128" }),
          nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold
        ],
      );
      console.log(`Burn Tx: ${txHash}`);
      return txHash;
    }

    async function retrieveAttestation(transactionHash: string) {
      console.log("Retrieving attestation...");
      const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_DOMAIN}?transactionHash=${transactionHash}`;
      while (true) {
        try {
          const response = await fetch(url, { method: "GET" });

          if (!response.ok) {
            if (response.status !== 404) {
              const text = await response.text().catch(() => "");
              console.error(
                "Error fetching attestation:",
                `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
              );
            }
            await new Promise((resolve) => setTimeout(resolve, 5000));
            continue;
          }

          const data = (await response.json()) as AttestationResponse;

          if (data?.messages?.[0]?.status === "complete") {
            console.log("Attestation retrieved successfully!");
            return data.messages[0];
          }
          console.log("Waiting for attestation...");
          await new Promise((resolve) => setTimeout(resolve, 5000));
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          console.error("Error fetching attestation:", message);
          await new Promise((resolve) => setTimeout(resolve, 5000));
        }
      }
    }

    async function mintUSDCOnArc(attestation: AttestationMessage) {
      console.log("Minting USDC on Arc Testnet...");
      const hash = await arcWalletClient.sendTransaction({
        to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
        data: encodeFunctionData({
          abi: [
            {
              type: "function",
              name: "receiveMessage",
              stateMutability: "nonpayable",
              inputs: [
                { name: "message", type: "bytes" },
                { name: "attestation", type: "bytes" },
              ],
              outputs: [],
            },
          ],
          functionName: "receiveMessage",
          args: [
            attestation.message as `0x${string}`,
            attestation.attestation as `0x${string}`,
          ],
        }),
      });
      await arcPublicClient.waitForTransactionReceipt({ hash });
      console.log(`Mint Tx: ${hash}`);
    }

    // ============ Main Execution ============
    async function main() {
      await approveUSDC();
      const burnTx = await burnUSDC();
      const attestation = await retrieveAttestation(burnTx);
      await mintUSDCOnArc(attestation);
      console.log("USDC transfer from Stellar to Arc completed!");
    }

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

    ## 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}
    Approving USDC spend on Stellar...
    Approve Tx: <stellar-transaction-hash>
    Burning USDC on Stellar...
    Burn Tx: <stellar-transaction-hash>
    Retrieving attestation...
    Waiting for attestation...
    Waiting for attestation...
    Attestation retrieved successfully!
    Minting USDC on Arc Testnet...
    Mint Tx: 0x...
    USDC transfer from Stellar to Arc completed!
    ```

    Attestation polling can take several minutes depending on network conditions and
    the finality threshold you chose. The script retries every 5 seconds with no
    timeout, so if it appears to hang at `Waiting for attestation...`, allow at
    least five minutes before investigating.

    <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>
  </Tab>

  <Tab title="Arc to Stellar">
    This quickstart demonstrates how to transfer USDC from Arc Testnet to Stellar
    Testnet using CCTP. You use the [viem](https://viem.sh/) library to burn USDC on
    Arc, and [`@stellar/stellar-sdk`](https://github.com/stellar/js-stellar-sdk) to
    mint and forward tokens on the Stellar CCTP Forwarder. 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 Stellar Soroban transactions helps you follow and adapt the
    script. Examples use Arc Testnet as the source, but you can use any
    [supported blockchain](/cctp/concepts/supported-chains-and-domains).

    <Warning>
      Always
      [use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
      when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and
      `destinationCaller` to the `CctpForwarder`
      [contract address](/cctp/references/stellar-contracts).

      * If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
      * If `mintRecipient` is set to a user account or muxed address, USDC is not sent
        to the forwarder.

      In either case, funds become permanently stuck and **cannot be recovered**.
    </Warning>

    ## Prerequisites

    Before you begin this tutorial, ensure you have:

    * Installed [Node.js v22+](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 gas fees and the transfer
        amount) from the [Circle Faucet](https://faucet.circle.com)
    * Prepared a Stellar Testnet wallet with the secret key (`S...`) available
      * Funded your Stellar wallet with testnet XLM from the
        [Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees
        on Stellar Testnet)
    * If needed, identified the forward recipient Stellar `strkey` (`G...`, `C...`,
      or `M...`). For `G...` or `M...` recipients, established a
      [USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) before
      receiving minted USDC

    <Tip>
      You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund
      accounts and establish USDC trustlines.
    </Tip>

    ## 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 and install the required dependencies:

    ```bash Shell theme={null}
    # Set up your directory and initialize a Node.js project
    mkdir cctp-arc-to-stellar
    cd cctp-arc-to-stellar
    npm init -y

    # Set up module type and start command
    npm pkg set type=module
    npm pkg set scripts.start="npx tsx --env-file=.env index.ts"

    # Install runtime dependencies
    npm install @stellar/stellar-sdk viem

    # Install dev dependencies
    npm install --save-dev typescript @types/node
    ```

    ### 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
    STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
    # FORWARD_RECIPIENT=G_OR_C_OR_M_STELLAR_STRKEY
    ```

    * `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc
      Testnet.
    * `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban
      transactions on Stellar Testnet.
    * `FORWARD_RECIPIENT` is required when the final recipient is a `G...` or `M...`
      address. When omitted, the script defaults to the public key derived from
      `STELLAR_SECRET_KEY`.

    <EnvFileTip />

    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

    This section covers the necessary setup for the transfer script, including
    defining keys and addresses, and configuring the wallet clients for interacting
    with Arc and Stellar.

    ### 2.1. Define configuration constants

    The script predefines the contract addresses, transfer amount, and other
    parameters.

    ```ts TypeScript theme={null}
    import {
      Contract,
      Keypair,
      StrKey,
      rpc,
      TransactionBuilder,
      xdr,
    } from "@stellar/stellar-sdk";
    import {
      createPublicClient,
      createWalletClient,
      encodeFunctionData,
      http,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    interface AttestationMessage {
      message: string;
      attestation: string;
      status: string;
    }

    interface AttestationResponse {
      messages: AttestationMessage[];
    }

    // ============ Configuration Constants ============
    // Contract Addresses
    const ARC_USDC = "0x3600000000000000000000000000000000000000";
    const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
    const STELLAR_CCTP_FORWARDER =
      "CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ";

    // Transfer Parameters
    const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits)
    const MAX_FEE = 500n; // 0.0005 USDC (500 subunits)

    // Chain-specific Parameters
    const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
    const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar

    // Stellar Soroban Configuration
    const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
    const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
    ```

    ### 2.2. Set up wallet clients

    The wallet clients configure the appropriate network settings using `viem`. In
    this example, the script connects to Arc Testnet.

    ```ts TypeScript theme={null}
    const evmAccount = privateKeyToAccount(
      process.env.EVM_PRIVATE_KEY as `0x${string}`,
    );

    const arcWalletClient = createWalletClient({
      chain: arcTestnet,
      transport: http(),
      account: evmAccount,
    });

    const arcPublicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });
    ```

    ### 2.3. Encode the CCTP Forwarder hook data

    When transferring to Stellar, the Arc burn encodes the forward recipient in the
    `hookData` field. The Stellar CCTP Forwarder contract is set as both the
    `mintRecipient` and `destinationCaller`.

    The hook data encodes the forward recipient in a specific binary format:

    ```text Byte layout theme={null}
      [32-byte header + forward recipient bytes]
      ├─ Bytes 0-23:  Zero padding (0x000...000)
      ├─ Bytes 24-27: Hook version (uint32, currently 0)
      ├─ Bytes 28-31: Forward recipient strkey length (uint32, byte length)
      └─ Bytes 32+:   Forward recipient strkey as UTF-8 bytes
    ```

    Example: For forward recipient "GABC...XYZ" (56 chars):

    * Zero padding: 0x000000000000000000000000000000000000000000000000
    * Hook version: 0x00000000
    * Length: 0x00000038 (56 in hex)
    * Forward recipient: "GABC...XYZ" encoded as UTF-8

    This encoding tells the CCTP Forwarder where to send tokens after minting.

    <Note>
      `G` and `M` strkey forward recipients need an established
      [USDC trustline](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines)
      before receiving funds. Transfers without a trustline fail.
    </Note>

    <Accordion title="How the CCTP Forwarder flow works">
      This path follows the
      [CCTP Forwarder](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
      pattern:

      1. **Burn with hook**: `depositForBurnWithHook` on the Arc
         [`TokenMessengerV2`](/cctp/references/contract-interfaces#depositforburnwithhook)
         contract. `mintRecipient` is the Stellar
         [CCTP Forwarder](/cctp/references/stellar-contracts#cctpforwarder), and
         `hookData` carries the forward recipient strkey.
      2. **Attest**: Circle's attestation service signs the burn event.
      3. **Mint and forward**: `mint_and_forward` on the Stellar CCTP Forwarder calls
         `receive_message` on `MessageTransmitter`, mints tokens, and forwards them
         per the hook data.

      <Note>
        The CCTP Forwarder flow is non-custodial. In one atomic Soroban transaction,
        `mint_and_forward` mints to `CctpForwarder` and pays `forwardRecipient` onchain.
        Circle does not take custody of the minted balance in between.
      </Note>
    </Accordion>

    ```ts TypeScript theme={null}
    // Convert a Stellar contract address (C...) to 0x-prefixed bytes32
    function contractStrkeyToBytes32(strkey: string): `0x${string}` {
      if (!StrKey.isValidContract(strkey)) {
        throw new Error(`Invalid contract strkey: ${strkey}`);
      }
      return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
    }

    // Build hook data encoding the forward recipient
    function buildCctpForwarderHookData(
      forwardRecipientStrkey: string,
    ): `0x${string}` {
      const isValid =
        StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
        StrKey.isValidContract(forwardRecipientStrkey) ||
        StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
      if (!isValid) {
        throw new Error(
          `Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
        );
      }

      const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
      const hookData = Buffer.alloc(32 + recipientBytes.length);
      hookData.writeUInt32BE(0, 24); // hook version = 0
      hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length
      recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8
      return `0x${hookData.toString("hex")}`;
    }

    const stellarKeypair = Keypair.fromSecret(
      process.env.STELLAR_SECRET_KEY as string,
    );
    // Falls back to the Stellar public key derived from STELLAR_SECRET_KEY
    // when FORWARD_RECIPIENT is unset or empty.
    const forwardRecipient =
      process.env.FORWARD_RECIPIENT || stellarKeypair.publicKey();
    const hookData = buildCctpForwarderHookData(forwardRecipient);
    ```

    ## Step 3: Implement the transfer logic

    This step implements the core transfer logic: approve and burn on Arc, poll for
    an attestation, then mint and forward on Stellar. 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 wallet. This allows the contract to burn USDC when you initiate
    the transfer.

    ```ts TypeScript theme={null}
    async function approveUSDC() {
      console.log("Approving USDC spend on Arc...");
      const approveTx = await arcWalletClient.sendTransaction({
        to: ARC_USDC as `0x${string}`,
        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: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT],
        }),
      });
      await arcPublicClient.waitForTransactionReceipt({ hash: approveTx });
      console.log(`Approve Tx: ${approveTx}`);
    }
    ```

    ### 3.2. Burn USDC on Arc

    Call `depositForBurnWithHook` to burn USDC with the CCTP Forwarder hook data.
    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 (see
      [supported blockchains and domains](/cctp/concepts/supported-chains-and-domains))
    * **Mint recipient**: The CCTP Forwarder contract address on Stellar (encoded as
      `bytes32`)
    * **Burn token**: The contract address of the USDC token being burned on Arc
    * **Destination caller**: The CCTP Forwarder contract address on Stellar
      (restricts who can call `receive_message`)
    * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
    * **Finality threshold**: Determines whether it's a
      [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
      (1000 or less) or a
      [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
      (2000 or more)
    * **Hook data**: Encodes the final Stellar recipient address for the CCTP
      Forwarder

    <Warning>
      Always
      [use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
      when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and
      `destinationCaller` to the `CctpForwarder`
      [contract address](/cctp/references/stellar-contracts).

      * If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
      * If `mintRecipient` is set to a user account or muxed address, USDC is not sent
        to the forwarder.

      In either case, funds become permanently stuck and **cannot be recovered**.
    </Warning>

    `mintRecipient` and `destinationCaller` must be the Stellar CCTP Forwarder
    contract address. `TokenMessengerMinter` assumes `mintRecipient` is a contract
    address, so this example validates `STELLAR_CCTP_FORWARDER` as a contract
    `strkey` and uses the final recipient only in `hookData`.

    ```ts TypeScript theme={null}
    async function burnUSDC() {
      console.log("Burning USDC on Arc (with hook)...");
      const cctpForwarderBytes32 = contractStrkeyToBytes32(STELLAR_CCTP_FORWARDER);
      const burnTx = await arcWalletClient.sendTransaction({
        to: ARC_TOKEN_MESSENGER as `0x${string}`,
        data: encodeFunctionData({
          abi: [
            {
              type: "function",
              name: "depositForBurnWithHook",
              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" },
                { name: "hookData", type: "bytes" },
              ],
              outputs: [],
            },
          ],
          functionName: "depositForBurnWithHook",
          args: [
            AMOUNT,
            STELLAR_DOMAIN,
            cctpForwarderBytes32, // mintRecipient = Stellar CCTP Forwarder
            ARC_USDC as `0x${string}`,
            cctpForwarderBytes32, // destinationCaller = Stellar CCTP Forwarder
            MAX_FEE,
            2000, // Standard Transfer finality threshold
            hookData,
          ],
        }),
      });
      await arcPublicClient.waitForTransactionReceipt({ hash: burnTx });
      console.log(`Burn Tx: ${burnTx}`);
      return burnTx;
    }
    ```

    ### 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 `transactionHash` from the value returned by `burnUSDC` above.

    ```ts TypeScript theme={null}
    async function retrieveAttestation(transactionHash: string) {
      console.log("Retrieving attestation...");
      const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
      while (true) {
        try {
          const response = await fetch(url, { method: "GET" });

          if (!response.ok) {
            if (response.status !== 404) {
              const text = await response.text().catch(() => "");
              console.error(
                "Error fetching attestation:",
                `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
              );
            }
            await new Promise((resolve) => setTimeout(resolve, 5000));
            continue;
          }

          const data = (await response.json()) as AttestationResponse;

          if (data?.messages?.[0]?.status === "complete") {
            console.log("Attestation retrieved successfully!");
            return data.messages[0];
          }
          console.log("Waiting for attestation...");
          await new Promise((resolve) => setTimeout(resolve, 5000));
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          console.error("Error fetching attestation:", message);
          await new Promise((resolve) => setTimeout(resolve, 5000));
        }
      }
    }
    ```

    ### 3.4. Mint and forward USDC on Stellar

    The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban
    contract transaction.

    ```ts TypeScript theme={null}
    async function submitSorobanTx(
      server: rpc.Server,
      contractId: string,
      method: string,
      args: xdr.ScVal[],
    ) {
      const account = await server.getAccount(stellarKeypair.publicKey());
      const contract = new Contract(contractId);

      const tx = new TransactionBuilder(account, {
        fee: "10000000",
        networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
      })
        .addOperation(contract.call(method, ...args))
        .setTimeout(120)
        .build();

      const simulated = await server.simulateTransaction(tx);
      if (rpc.Api.isSimulationError(simulated)) {
        throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
      }

      const prepared = rpc.assembleTransaction(tx, simulated).build();
      prepared.sign(stellarKeypair);

      const sendResult = await server.sendTransaction(prepared);
      if (sendResult.status === "ERROR") {
        throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
      }

      let getResult = await server.getTransaction(sendResult.hash);
      while (getResult.status === "NOT_FOUND") {
        await new Promise((resolve) => setTimeout(resolve, 2000));
        getResult = await server.getTransaction(sendResult.hash);
      }

      if (getResult.status !== "SUCCESS") {
        throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
      }

      return sendResult.hash;
    }
    ```

    Use the `submitSorobanTx` helper to call `mint_and_forward` on the Stellar CCTP
    Forwarder. This verifies the CCTP message and attestation, mints USDC through
    the `TokenMessengerMinter`, and forwards it to the recipient encoded in the hook
    data:

    ```ts TypeScript theme={null}
    async function mintAndForwardOnStellar(attestation: AttestationMessage) {
      console.log("Minting and forwarding USDC on Stellar...");
      const server = new rpc.Server(STELLAR_RPC_URL);

      const messageBytes = Buffer.from(
        attestation.message.replace("0x", ""),
        "hex",
      );
      const attestationBytes = Buffer.from(
        attestation.attestation.replace("0x", ""),
        "hex",
      );

      const txHash = await submitSorobanTx(
        server,
        STELLAR_CCTP_FORWARDER,
        "mint_and_forward",
        [xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)],
      );

      console.log(`mint_and_forward Tx: ${txHash}`);
    }
    ```

    ## Step 4: Full script

    Create an `index.ts` file in your project directory and paste the full script
    below so you can run the flow from one file.

    ```ts index.ts expandable theme={null}
    import {
      Contract,
      Keypair,
      StrKey,
      rpc,
      TransactionBuilder,
      xdr,
    } from "@stellar/stellar-sdk";
    import {
      createPublicClient,
      createWalletClient,
      encodeFunctionData,
      http,
    } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { arcTestnet } from "viem/chains";

    interface AttestationMessage {
      message: string;
      attestation: string;
      status: string;
    }

    interface AttestationResponse {
      messages: AttestationMessage[];
    }

    // ============ Configuration Constants ============
    // Contract Addresses
    const ARC_USDC = "0x3600000000000000000000000000000000000000";
    const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
    const STELLAR_CCTP_FORWARDER =
      "CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ";

    // Transfer Parameters
    const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits)
    const MAX_FEE = 500n; // 0.0005 USDC (500 subunits)

    // Chain-specific Parameters
    const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
    const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar

    // Stellar Soroban Configuration
    const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
    const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";

    // Set up wallet clients
    const evmAccount = privateKeyToAccount(
      process.env.EVM_PRIVATE_KEY as `0x${string}`,
    );
    const arcWalletClient = createWalletClient({
      chain: arcTestnet,
      transport: http(),
      account: evmAccount,
    });
    const arcPublicClient = createPublicClient({
      chain: arcTestnet,
      transport: http(),
    });

    // Hook Data — encodes the forward recipient for the CCTP Forwarder contract
    function contractStrkeyToBytes32(strkey: string): `0x${string}` {
      if (!StrKey.isValidContract(strkey)) {
        throw new Error(`Invalid contract strkey: ${strkey}`);
      }
      return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
    }
    function buildCctpForwarderHookData(
      forwardRecipientStrkey: string,
    ): `0x${string}` {
      const isValid =
        StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
        StrKey.isValidContract(forwardRecipientStrkey) ||
        StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
      if (!isValid) {
        throw new Error(
          `Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
        );
      }
      const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
      const hookData = Buffer.alloc(32 + recipientBytes.length);
      hookData.writeUInt32BE(0, 24);
      hookData.writeUInt32BE(recipientBytes.length, 28);
      recipientBytes.copy(hookData, 32);
      return `0x${hookData.toString("hex")}`;
    }
    const stellarKeypair = Keypair.fromSecret(
      process.env.STELLAR_SECRET_KEY as string,
    );
    // Falls back to the Stellar public key derived from STELLAR_SECRET_KEY
    // when FORWARD_RECIPIENT is unset or empty.
    const forwardRecipient =
      process.env.FORWARD_RECIPIENT || stellarKeypair.publicKey();
    const hookData = buildCctpForwarderHookData(forwardRecipient);

    // ============ CCTP Flow Functions ============
    async function approveUSDC() {
      console.log("Approving USDC spend on Arc...");
      const approveTx = await arcWalletClient.sendTransaction({
        to: ARC_USDC as `0x${string}`,
        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: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT],
        }),
      });
      await arcPublicClient.waitForTransactionReceipt({ hash: approveTx });
      console.log(`Approve Tx: ${approveTx}`);
    }

    async function burnUSDC() {
      console.log("Burning USDC on Arc (with hook)...");
      const cctpForwarderBytes32 = contractStrkeyToBytes32(STELLAR_CCTP_FORWARDER);
      const burnTx = await arcWalletClient.sendTransaction({
        to: ARC_TOKEN_MESSENGER as `0x${string}`,
        data: encodeFunctionData({
          abi: [
            {
              type: "function",
              name: "depositForBurnWithHook",
              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" },
                { name: "hookData", type: "bytes" },
              ],
              outputs: [],
            },
          ],
          functionName: "depositForBurnWithHook",
          args: [
            AMOUNT,
            STELLAR_DOMAIN,
            cctpForwarderBytes32, // mintRecipient = Stellar CCTP Forwarder
            ARC_USDC as `0x${string}`,
            cctpForwarderBytes32, // destinationCaller = Stellar CCTP Forwarder
            MAX_FEE,
            2000, // Standard Transfer finality threshold
            hookData,
          ],
        }),
      });
      await arcPublicClient.waitForTransactionReceipt({ hash: burnTx });
      console.log(`Burn Tx: ${burnTx}`);
      return burnTx;
    }

    async function retrieveAttestation(transactionHash: string) {
      console.log("Retrieving attestation...");
      const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
      while (true) {
        try {
          const response = await fetch(url, { method: "GET" });

          if (!response.ok) {
            if (response.status !== 404) {
              const text = await response.text().catch(() => "");
              console.error(
                "Error fetching attestation:",
                `${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
              );
            }
            await new Promise((resolve) => setTimeout(resolve, 5000));
            continue;
          }

          const data = (await response.json()) as AttestationResponse;

          if (data?.messages?.[0]?.status === "complete") {
            console.log("Attestation retrieved successfully!");
            return data.messages[0];
          }
          console.log("Waiting for attestation...");
          await new Promise((resolve) => setTimeout(resolve, 5000));
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          console.error("Error fetching attestation:", message);
          await new Promise((resolve) => setTimeout(resolve, 5000));
        }
      }
    }

    async function submitSorobanTx(
      server: rpc.Server,
      contractId: string,
      method: string,
      args: xdr.ScVal[],
    ) {
      const account = await server.getAccount(stellarKeypair.publicKey());
      const contract = new Contract(contractId);

      const tx = new TransactionBuilder(account, {
        fee: "10000000",
        networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
      })
        .addOperation(contract.call(method, ...args))
        .setTimeout(120)
        .build();

      const simulated = await server.simulateTransaction(tx);
      if (rpc.Api.isSimulationError(simulated)) {
        throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
      }

      const prepared = rpc.assembleTransaction(tx, simulated).build();
      prepared.sign(stellarKeypair);

      const sendResult = await server.sendTransaction(prepared);
      if (sendResult.status === "ERROR") {
        throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
      }

      let getResult = await server.getTransaction(sendResult.hash);
      while (getResult.status === "NOT_FOUND") {
        await new Promise((resolve) => setTimeout(resolve, 2000));
        getResult = await server.getTransaction(sendResult.hash);
      }

      if (getResult.status !== "SUCCESS") {
        throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
      }

      return sendResult.hash;
    }

    async function mintAndForwardOnStellar(attestation: AttestationMessage) {
      console.log("Minting and forwarding USDC on Stellar...");
      const server = new rpc.Server(STELLAR_RPC_URL);

      const messageBytes = Buffer.from(
        attestation.message.replace("0x", ""),
        "hex",
      );
      const attestationBytes = Buffer.from(
        attestation.attestation.replace("0x", ""),
        "hex",
      );

      const txHash = await submitSorobanTx(
        server,
        STELLAR_CCTP_FORWARDER,
        "mint_and_forward",
        [xdr.ScVal.scvBytes(messageBytes), xdr.ScVal.scvBytes(attestationBytes)],
      );

      console.log(`mint_and_forward Tx: ${txHash}`);
    }

    // ============ Main Execution ============
    async function main() {
      await approveUSDC();
      const burnTx = await burnUSDC();
      const attestation = await retrieveAttestation(burnTx);
      await mintAndForwardOnStellar(attestation);
      console.log("USDC transfer from Arc to Stellar completed!");
    }

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

    ## 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}
    Approving USDC spend on Arc...
    Approve Tx: 0x65d6504333ac76cf952975dad29d4a31d8de28c59d7c97ee8ac5d0c360c0e70c
    Burning USDC on Arc (with hook)...
    Burn Tx: 0x73e1eb9224ca5778be763b4e8afc11cfa63e7ae3caa8b2748ce73f4f3d07181a
    Retrieving attestation...
    Waiting for attestation...
    Attestation retrieved successfully!
    Minting and forwarding USDC on Stellar...
    mint_and_forward Tx: <stellar-transaction-hash>
    USDC transfer from Arc to Stellar completed!
    ```

    Attestation polling can take several minutes depending on network conditions and
    the finality threshold you chose. The script retries every 5 seconds with no
    timeout, so if it appears to hang at `Waiting for attestation...`, allow at
    least five minutes before investigating.

    <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>
  </Tab>
</Tabs>
