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

# Quickstart: Create and transfer a unified USDC balance on Solana

> Create and transfer a unified USDC balance on Solana using Circle Gateway.

This guide walks you through the process of creating Unified Crosschain USDC
Balances on Solana using Circle Gateway, and performing transfers from EVM to
Solana and from Solana to Solana.

<Tip>
  **Use
  [Unified Balance Kit](https://www.npmjs.com/package/@circle-fin/unified-balance-kit)
  to simplify this integration.**

  This quickstart uses a manual Gateway integration. It is for learning or for
  developers who need direct control.

  To simplify, use Unified Balance Kit to deposit and spend USDC in just a few
  lines of code.
</Tip>

Select a tab below for the Circle Wallets or self-managed wallet path.

<Tabs>
  <Tab title="Circle Wallets">
    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22.6+](https://nodejs.org/)
    * Obtained a
      [Circle API Key](/w3s/circle-developer-account#creating-an-api-key-for-developer-services)
      and [Entity Secret](/wallets/dev-controlled/register-entity-secret) from the
      [Circle Console](/w3s/circle-developer-account).
    * Created Solana Devnet
      [Developer-Controlled Wallets](/wallets/dev-controlled/create-your-first-wallet)
      for the sender and recipient
    * Funded your wallet with testnet tokens:
      * Get testnet USDC from the [Circle Faucet](https://faucet.circle.com/).
      * Get test native tokens from the
        [Console Faucet](https://console.circle.com/faucet).

    If you want to try the EVM to Solana transfer, ensure that you've:

    * Created an Arc Testnet EVM Developer-Controlled Wallet
    * Created a Solana Devnet Developer-Controlled Wallet to receive the minted USDC
    * Completed the
      [deposit flow](/gateway/quickstarts/unified-balance-evm#step-3-deposit-into-a-unified-crosschain-balance-circle-wallets)
      from the
      [EVM quickstart](/gateway/quickstarts/unified-balance-evm#circle-wallets)
      first

    ### Add testnet funds to your wallet

    To interact with Gateway, you need test USDC and native tokens in your wallet on
    each chain you deposit from. For direct mints on Solana, the wallet that submits
    the Solana transaction also needs SOL to create the recipient Associated Token
    Account and call the Gateway Minter program.

    <Tip>
      You can skip the destination mint transaction by using the [Forwarding
      Service](/gateway/howtos/forwarding-service), which completes the Solana mint
      for you. The recipient ATA still needs to exist for this quickstart.
    </Tip>

    Use the [Circle Faucet](https://faucet.circle.com/) to get test USDC. If you
    have a [Circle Developer Console](https://console.circle.com) account, you can
    use the [Console Faucet](https://console.circle.com/faucet) to get testnet
    native tokens. In addition, the following faucets can also be used to fund your
    wallet with testnet native tokens:

    <Tabs>
      <Tab title="Arc">
        **Faucet:** [Arc Testnet](https://faucet.circle.com) (USDC + native tokens)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `arcTestnet`                                 |
        | USDC address | `0x3600000000000000000000000000000000000000` |
        | Domain ID    | `26`                                         |
      </Tab>

      <Tab title="Avalanche">
        **Faucet:** [Avalanche Fuji](https://core.app/tools/testnet-faucet)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `avalancheFuji`                              |
        | USDC address | `0x5425890298aed601595a70ab815c96711a31bc65` |
        | Domain ID    | `1`                                          |
      </Tab>

      <Tab title="Base">
        **Faucet:** [Base Sepolia](https://www.alchemy.com/faucets/base-sepolia)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `baseSepolia`                                |
        | USDC address | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
        | Domain ID    | `6`                                          |
      </Tab>

      <Tab title="Ethereum">
        **Faucet:** [Ethereum Sepolia](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `sepolia`                                    |
        | USDC address | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
        | Domain ID    | `0`                                          |
      </Tab>

      <Tab title="Hyperliquid">
        **Faucet:** [Hyperliquid EVM Testnet](https://app.hyperliquid-testnet.xyz/drip)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `hyperliquidEvmTestnet`                      |
        | USDC address | `0x2B3370eE501B4a559b57D449569354196457D8Ab` |
        | Domain ID    | `19`                                         |
      </Tab>

      <Tab title="Sei">
        **Faucet:** [Sei Testnet](https://docs.sei.io/learn/faucet)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `seiTestnet`                                 |
        | USDC address | `0x4fCF1784B31630811181f670Aea7A7bEF803eaED` |
        | Domain ID    | `16`                                         |
      </Tab>

      <Tab title="Solana">
        **Faucet:** [Solana Devnet](https://faucet.solana.com/)

        | Property     | Value                                                   |
        | ------------ | ------------------------------------------------------- |
        | Chain name   | `solanaDevnet` (note that Solana is not EVM-compatible) |
        | USDC address | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`          |
        | Domain ID    | `5`                                                     |
      </Tab>

      <Tab title="Sonic">
        **Faucet:** [Sonic Testnet](https://testnet.soniclabs.com/account)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `sonicTestnet`                               |
        | USDC address | `0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51` |
        | Domain ID    | `13`                                         |
      </Tab>

      <Tab title="Worldchain">
        **Faucet:** [Worldchain Sepolia](https://www.l2faucet.com/world)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `worldchainSepolia`                          |
        | USDC address | `0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88` |
        | Domain ID    | `14`                                         |
      </Tab>
    </Tabs>

    ## Step 1. Set up your project

    ### 1.1. Create the project and install dependencies

    ```shell theme={null}
    # Set up your directory and initialize a Node.js project
    mkdir unified-gateway-balance-solana-circle-wallets
    cd unified-gateway-balance-solana-circle-wallets
    npm init -y

    # Set up module type and run scripts
    npm pkg set type=module
    npm pkg set scripts.deposit="node --env-file=.env deposit.ts"
    npm pkg set scripts.balances="node --env-file=.env balances.ts"
    npm pkg set scripts.transfer-from-sol="node --env-file=.env transfer-from-sol.ts"
    npm pkg set scripts.transfer-from-evm="node --env-file=.env transfer-from-evm.ts"

    # Pin bigint-buffer to a patched version
    npm pkg set overrides.bigint-buffer=npm:@trufflesuite/bigint-buffer@1.1.10

    # Install runtime dependencies
    npm install @circle-fin/developer-controlled-wallets @coral-xyz/anchor @solana/buffer-layout @solana/spl-token @solana/web3.js bs58 bn.js typescript

    # Install dev dependencies
    npm install --save-dev @types/node @types/bn.js
    ```

    ### 1.2. Configure TypeScript (optional)

    <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. Set environment variables

    Create a `.env` file in the project directory:

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

    * `CIRCLE_API_KEY` is your Circle API key.
    * `CIRCLE_ENTITY_SECRET` is your Circle entity secret.
    * `DEPOSITOR_ADDRESS` is the source depositor wallet for the script you are
      running.
    * `RECIPIENT_ADDRESS` is the destination wallet that receives the minted USDC.

    For `transfer-from-sol.ts`, both values are Solana addresses.

    For `transfer-from-evm.ts`, `DEPOSITOR_ADDRESS` is an EVM address and
    `RECIPIENT_ADDRESS` is a Solana address.

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

    ## Step 2. Set up the configuration file

    The shared Solana configuration and helpers are used by the deposit and transfer
    scripts.

    ### 2.1. Create the configuration file

    ```shell theme={null}
    touch config.ts
    ```

    ### 2.2. Configure Solana settings and Gateway helpers

    Add the shared Solana RPC configuration, Gateway addresses, IDLs, attestation
    decoding helpers, and Circle Wallets signing helpers to `config.ts`.

    The local IDL fragments in this example are only the subset needed by the sample
    code. For canonical static instruction and account definitions, use the onchain
    IDLs linked from
    [Solana programs and interfaces](/gateway/references/solana-programs#account-structures).

    ```ts config.ts expandable theme={null}
    import { initiateDeveloperControlledWalletsClient } from "@circle-fin/developer-controlled-wallets";
    import { Connection, PublicKey, Transaction } from "@solana/web3.js";
    import {
      u32be,
      nu64be,
      struct,
      seq,
      blob,
      offset,
      Layout,
    } from "@solana/buffer-layout";
    import bs58 from "bs58";

    export const RPC_ENDPOINT = "https://api.devnet.solana.com";
    export const SOLANA_DOMAIN = 5;
    export const SOLANA_ZERO_ADDRESS = "11111111111111111111111111111111";

    export const GATEWAY_WALLET_ADDRESS =
      "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
    export const GATEWAY_MINTER_ADDRESS =
      "GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr";
    export const USDC_ADDRESS = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";

    const API_KEY = process.env.CIRCLE_API_KEY!;
    const ENTITY_SECRET = process.env.CIRCLE_ENTITY_SECRET!;

    if (!API_KEY || !ENTITY_SECRET) {
      console.error(
        "Missing required env vars: CIRCLE_API_KEY, CIRCLE_ENTITY_SECRET",
      );
      process.exit(1);
    }

    export const client = initiateDeveloperControlledWalletsClient({
      apiKey: API_KEY,
      entitySecret: ENTITY_SECRET,
    });

    export class PublicKeyLayout extends Layout<PublicKey> {
      constructor(property: string) {
        super(32, property);
      }
      decode(b: Buffer, offset = 0): PublicKey {
        return new PublicKey(b.subarray(offset, offset + 32));
      }
      encode(src: PublicKey, b: Buffer, offset = 0): number {
        const pubkeyBuffer = src.toBuffer();
        pubkeyBuffer.copy(b, offset);
        return 32;
      }
    }

    export const publicKey = (property: string) => new PublicKeyLayout(property);

    const MintAttestationElementLayout = struct([
      publicKey("destinationToken"),
      publicKey("destinationRecipient"),
      nu64be("value"),
      blob(32, "transferSpecHash"),
      u32be("hookDataLength"),
      blob(offset(u32be(), -4), "hookData"),
    ] as any);

    const MintAttestationSetLayout = struct([
      u32be("magic"),
      u32be("version"),
      u32be("destinationDomain"),
      publicKey("destinationContract"),
      publicKey("destinationCaller"),
      nu64be("maxBlockHeight"),
      u32be("numAttestations"),
      seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
    ] as any);

    // Sample-local IDL subset for this example.
    export const gatewayWalletIdl = {
      address: GATEWAY_WALLET_ADDRESS,
      metadata: {
        name: "gatewayWallet",
        version: "0.1.0",
        spec: "0.1.0",
      },
      instructions: [
        {
          name: "deposit",
          discriminator: [22, 0],
          accounts: [
            { name: "payer", writable: true, signer: true },
            { name: "owner", signer: true },
            { name: "gatewayWallet" },
            { name: "ownerTokenAccount", writable: true },
            { name: "custodyTokenAccount", writable: true },
            { name: "deposit", writable: true },
            { name: "depositorDenylist" },
            { name: "tokenProgram" },
            { name: "systemProgram" },
            { name: "eventAuthority" },
            { name: "program" },
          ],
          args: [{ name: "amount", type: "u64" }],
        },
      ],
    };

    // Sample-local IDL subset for this example.
    export const gatewayMinterIdl = {
      address: GATEWAY_MINTER_ADDRESS,
      metadata: { name: "gatewayMinter", version: "0.1.0", spec: "0.1.0" },
      instructions: [
        {
          name: "gatewayMint",
          discriminator: [12, 0],
          accounts: [
            { name: "payer", writable: true, signer: true },
            { name: "destinationCaller", signer: true },
            { name: "gatewayMinter" },
            { name: "systemProgram" },
            { name: "tokenProgram" },
            { name: "eventAuthority" },
            { name: "program" },
          ],
          args: [
            {
              name: "params",
              type: { defined: { name: "gatewayMintParams" } },
            },
          ],
        },
      ],
      types: [
        {
          name: "gatewayMintParams",
          type: {
            kind: "struct",
            fields: [
              { name: "attestation", type: "bytes" },
              { name: "signature", type: "bytes" },
            ],
          },
        },
      ],
    };

    export function findDepositPDAs(
      programId: PublicKey,
      usdcMint: PublicKey,
      owner: PublicKey,
    ) {
      return {
        wallet: PublicKey.findProgramAddressSync(
          [Buffer.from("gateway_wallet")],
          programId,
        )[0],
        custody: PublicKey.findProgramAddressSync(
          [Buffer.from("gateway_wallet_custody"), usdcMint.toBuffer()],
          programId,
        )[0],
        deposit: PublicKey.findProgramAddressSync(
          [Buffer.from("gateway_deposit"), usdcMint.toBuffer(), owner.toBuffer()],
          programId,
        )[0],
        denylist: PublicKey.findProgramAddressSync(
          [Buffer.from("denylist"), owner.toBuffer()],
          programId,
        )[0],
      };
    }

    export function findCustodyPda(
      mint: PublicKey,
      minterProgramId: PublicKey,
    ): PublicKey {
      return PublicKey.findProgramAddressSync(
        [Buffer.from("gateway_minter_custody"), mint.toBuffer()],
        minterProgramId,
      )[0];
    }

    export function findTransferSpecHashPda(
      transferSpecHash: Uint8Array | Buffer,
      minterProgramId: PublicKey,
    ): PublicKey {
      return PublicKey.findProgramAddressSync(
        [Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
        minterProgramId,
      )[0];
    }

    export function decodeAttestationSet(attestation: string) {
      const buffer = Buffer.from(attestation.slice(2), "hex");
      return MintAttestationSetLayout.decode(buffer) as {
        attestations: Array<{
          destinationToken: PublicKey;
          destinationRecipient: PublicKey;
          transferSpecHash: Uint8Array;
        }>;
      };
    }

    export function solanaAddressToBytes32(address: string): string {
      const decoded = Buffer.from(bs58.decode(address));
      return `0x${decoded.toString("hex")}`;
    }

    export function hexToPublicKey(hex: string): PublicKey {
      return new PublicKey(Buffer.from(hex.slice(2), "hex"));
    }

    export async function signAndBroadcast(
      circleClient: ReturnType<typeof initiateDeveloperControlledWalletsClient>,
      connection: Connection,
      transaction: Transaction,
      walletAddress: string,
      label: string,
    ): Promise<string> {
      const serialized = transaction.serialize({
        requireAllSignatures: false,
        verifySignatures: false,
      });

      console.log(`Signing ${label} via Circle Wallets...`);
      const signResult = await circleClient.signTransaction({
        walletAddress,
        blockchain: "SOL-DEVNET",
        rawTransaction: serialized.toString("base64"),
      });

      const signedTxBase64 = signResult.data?.signedTransaction;
      if (!signedTxBase64) throw new Error(`Failed to sign ${label}`);

      console.log(`Broadcasting ${label}...`);
      const signedTxBytes = Buffer.from(signedTxBase64, "base64");
      return connection.sendRawTransaction(signedTxBytes);
    }

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

    ## Step 3. Deposit into a unified crosschain balance (Circle Wallets)

    The deposit script submits a Gateway deposit on Solana Devnet. You can skip to
    the [full deposit script](#3-5-full-deposit-script-circle-wallets) if you
    prefer.

    <Warning>
      Do not send USDC directly to the Gateway Wallet address or custody account.
      You must use a Gateway deposit instruction for the funds to be credited to
      your unified balance.
    </Warning>

    ### 3.1. Create the deposit script

    ```shell theme={null}
    touch deposit.ts
    ```

    ### 3.2. Define constants and helpers

    Set the deposit amount near the top of the file, then derive the owner ATA and
    load the account so the script can validate balance before it builds the Gateway
    instruction.

    ```ts theme={null}
    const DEPOSIT_AMOUNT = new BN(1_000_000);
    ```

    ### 3.3. Initialize connection, Anchor client, and validate balance

    Initialize the connection, check the source wallet balance, then set up the
    Anchor client and derive the Gateway PDAs.

    ```ts theme={null}
    const connection = new Connection(RPC_ENDPOINT, "confirmed");
    const usdcMint = new PublicKey(USDC_ADDRESS);
    const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
    const owner = new PublicKey(DEPOSITOR_ADDRESS);

    const userTokenAccount = getAssociatedTokenAddressSync(usdcMint, owner);
    const tokenAccountInfo = await getAccount(connection, userTokenAccount);

    const dummyWallet = new Wallet(Keypair.generate());
    const provider = new AnchorProvider(
      connection,
      dummyWallet,
      AnchorProvider.defaultOptions(),
    );
    ```

    ### 3.4. Execute the deposit

    After the balance check and PDA derivation, build the Gateway deposit
    instruction, sign it with Circle Wallets, broadcast it, and wait for Solana
    confirmation.

    ```ts theme={null}
    const depositIx = await program.methods
      .deposit(DEPOSIT_AMOUNT)
      .accountsPartial({
        payer: owner,
        owner: owner,
        gatewayWallet: pdas.wallet,
        ownerTokenAccount: userTokenAccount,
        custodyTokenAccount: pdas.custody,
        deposit: pdas.deposit,
        depositorDenylist: pdas.denylist,
        tokenProgram: TOKEN_PROGRAM_ID,
        systemProgram: SystemProgram.programId,
      })
      .instruction();

    const txSignature = await signAndBroadcast(
      client,
      connection,
      transaction,
      DEPOSITOR_ADDRESS,
      "deposit",
    );
    ```

    ### 3.5. Full deposit script (Circle Wallets)

    The script validates the source balance, builds the Gateway deposit instruction,
    and confirms the deposit on Solana Devnet. Inline comments explain each stage.

    ```ts deposit.ts expandable theme={null}
    import {
      Wallet,
      AnchorProvider,
      setProvider,
      Program,
    } from "@coral-xyz/anchor";
    import {
      Connection,
      Keypair,
      PublicKey,
      SystemProgram,
      Transaction,
    } from "@solana/web3.js";
    import {
      getAssociatedTokenAddressSync,
      getAccount,
      TOKEN_PROGRAM_ID,
    } from "@solana/spl-token";
    import BN from "bn.js";
    import {
      RPC_ENDPOINT,
      GATEWAY_WALLET_ADDRESS,
      USDC_ADDRESS,
      client,
      gatewayWalletIdl,
      findDepositPDAs,
      signAndBroadcast,
    } from "./config.js";

    const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;

    if (!DEPOSITOR_ADDRESS) {
      console.error("Missing required env var: DEPOSITOR_ADDRESS");
      process.exit(1);
    }

    const DEPOSIT_AMOUNT = new BN(1_000_000);

    async function main() {
      // Set up the Solana connection and core account addresses.
      const connection = new Connection(RPC_ENDPOINT, "confirmed");
      const usdcMint = new PublicKey(USDC_ADDRESS);
      const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
      const owner = new PublicKey(DEPOSITOR_ADDRESS);

      console.log(`Using account: ${owner.toBase58()}`);
      console.log(`\n=== Processing Solana Devnet ===`);

      // [1] Check the depositor's current USDC balance.
      const userTokenAccount = getAssociatedTokenAddressSync(usdcMint, owner);
      const tokenAccountInfo = await getAccount(connection, userTokenAccount);
      const currentBalance = Number(tokenAccountInfo.amount) / 1_000_000;
      console.log(`Current balance: ${currentBalance} USDC`);

      if (tokenAccountInfo.amount < BigInt(DEPOSIT_AMOUNT.toString())) {
        throw new Error(
          "Insufficient USDC balance. Please top up at https://faucet.circle.com",
        );
      }

      // [2] Set up the Anchor client and derive the Gateway deposit PDAs.
      const dummyWallet = new Wallet(Keypair.generate());
      const provider = new AnchorProvider(
        connection,
        dummyWallet,
        AnchorProvider.defaultOptions(),
      );
      setProvider(provider);
      const program = new Program(gatewayWalletIdl, provider);
      const pdas = findDepositPDAs(programId, usdcMint, owner);

      // [3] Build, sign, and confirm the Gateway deposit transaction.
      const depositIx = await program.methods
        .deposit(DEPOSIT_AMOUNT)
        .accountsPartial({
          payer: owner,
          owner: owner,
          gatewayWallet: pdas.wallet,
          ownerTokenAccount: userTokenAccount,
          custodyTokenAccount: pdas.custody,
          deposit: pdas.deposit,
          depositorDenylist: pdas.denylist,
          tokenProgram: TOKEN_PROGRAM_ID,
          systemProgram: SystemProgram.programId,
        })
        .instruction();

      const { blockhash, lastValidBlockHeight } =
        await connection.getLatestBlockhash();
      const transaction = new Transaction();
      transaction.add(depositIx);
      transaction.recentBlockhash = blockhash;
      transaction.feePayer = owner;

      const txSignature = await signAndBroadcast(
        client,
        connection,
        transaction,
        DEPOSITOR_ADDRESS,
        "deposit",
      );

      await connection.confirmTransaction(
        { signature: txSignature, blockhash, lastValidBlockHeight },
        "confirmed",
      );

      console.log(`Done on Solana Devnet. Deposit tx: ${txSignature}`);
    }

    /* Main invocation */
    main().catch((error) => {
      console.error("\nError:", error);
      process.exit(1);
    });
    ```

    ### 3.6. Run the deposit script

    ```shell theme={null}
    npm run deposit
    ```

    Wait for the required number of block confirmations. Once the deposit
    transaction is final, your Gateway balance on Solana Devnet will be updated.
    Solana Devnet transactions typically reach finality in seconds.

    ### 3.7. Check the balances on the Gateway Wallet

    Create a new file called `balances.ts`, and add the following code. This script
    retrieves the USDC balances available from your Gateway Wallet for the
    `DEPOSITOR_ADDRESS` currently set in `.env`.

    ```ts balances.ts expandable theme={null}
    interface GatewayBalancesResponse {
      balances: Array<{
        domain: number;
        balance: string;
      }>;
    }

    const EVM_DOMAINS = {
      ethereum: 0,
      avalanche: 1,
      optimism: 2,
      arbitrum: 3,
      base: 6,
      polygon: 7,
      unichain: 10,
      arc: 26,
    };

    const SOLANA_DOMAINS = {
      solana: 5,
    };

    const DOMAINS = { ...EVM_DOMAINS, ...SOLANA_DOMAINS };

    const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;

    if (!DEPOSITOR_ADDRESS) {
      console.error("Missing required env var: DEPOSITOR_ADDRESS");
      process.exit(1);
    }

    const isEvmAddress = DEPOSITOR_ADDRESS.startsWith("0x");

    async function main() {
      console.log(`Depositor address: ${DEPOSITOR_ADDRESS}`);
      console.log(`Address type: ${isEvmAddress ? "EVM" : "Solana"}\n`);

      const activeDomains = isEvmAddress ? EVM_DOMAINS : SOLANA_DOMAINS;
      const domainIds = Object.values(activeDomains);
      const body = {
        token: "USDC",
        sources: domainIds.map((domain) => ({
          domain,
          depositor: DEPOSITOR_ADDRESS,
        })),
      };

      const res = await fetch(
        "https://gateway-api-testnet.circle.com/v1/balances",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        },
      );

      const result = (await res.json()) as GatewayBalancesResponse;

      let total = 0;
      for (const balance of result.balances) {
        const chain =
          Object.keys(DOMAINS).find(
            (k) => DOMAINS[k as keyof typeof DOMAINS] === balance.domain,
          ) || `Domain ${balance.domain}`;
        const amount = parseFloat(balance.balance);
        console.log(`${chain}: ${amount.toFixed(6)} USDC`);
        total += amount;
      }

      console.log(`\nTotal: ${total.toFixed(6)} USDC`);
    }

    main().catch((error) => {
      console.error("\nError:", error);
      process.exit(1);
    });
    ```

    You can run it to verify your balance on Gateway.

    ```shell theme={null}
    npm run balances
    ```

    <Tabs>
      <Tab title="Transfer from Solana">
        ## Step 4. Transfer USDC from Solana to Solana

        This step transfers USDC from your Solana Devnet Gateway balance to a recipient
        on Solana Devnet. Both paths create and sign the Solana burn intent first.
        Direct mint then retrieves the Gateway attestation and calls `gatewayMint(...)`
        on Solana Devnet from your wallet, while Forwarding Service lets Circle complete
        the Solana mint for you.

        <Tabs>
          <Tab title="Direct Mint">
            ### 4.1. Create the Solana transfer script

            You can skip to the
            [full transfer script](#4-9-full-solana-transfer-script-circle-wallets) if you
            prefer.

            ```shell theme={null}
            touch transfer-from-sol.ts
            ```

            ### 4.2. Define constants and types

            This flow uses the same Solana burn intent layout as the standard Gateway
            quickstart, but swaps in Circle Wallet signing for both the burn intent and the
            mint transaction.

            ```ts theme={null}
            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;
            ```

            ### 4.3. Add helper functions

            The helper layer encodes the Solana burn intent, creates a lightweight Anchor
            provider, and exposes the address conversion utilities required for Gateway
            minting.

            ```ts theme={null}
            function createProvider(connection: Connection) {
              const dummyWallet = new Wallet(Keypair.generate());
              const provider = new AnchorProvider(
                connection,
                dummyWallet,
                AnchorProvider.defaultOptions(),
              );
              setProvider(provider);
              return provider;
            }
            ```

            ### 4.4. Initialize connection and create recipient ATA

            Before minting to the destination wallet, derive the recipient Associated Token
            Account and create it idempotently. In this script, the source
            Developer-Controlled Wallet pays for the account creation.

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            ```ts theme={null}
            const provider = createProvider(connection);

            const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
            const ataTx = new Transaction();
            ataTx.add(
              createAssociatedTokenAccountIdempotentInstruction(
                owner,
                recipientAta,
                recipientPubkey,
                usdcMint,
              ),
            );
            ```

            ### 4.5. Create and sign burn intent

            Encode the Solana burn intent, prefix the payload, and sign it with the source
            Developer-Controlled Wallet.

            ```ts theme={null}
            const burnIntent = createBurnIntent({
              sourceDepositor: owner.toBase58(),
              destinationRecipient: recipientAta.toBase58(),
              sourceSigner: owner.toBase58(),
            });

            const sigResult = await client.signMessage({
              walletAddress: DEPOSITOR_ADDRESS,
              blockchain: "SOL-DEVNET",
              encodedByHex: true,
              message: "0x" + prefixed.toString("hex"),
            });
            ```

            ### 4.6. Request attestation from Gateway API

            Submit the signed burn intent to the Gateway API and decode the attestation set
            that comes back from the response.

            ```ts theme={null}
            const response = await fetch(
              "https://gateway-api-testnet.circle.com/v1/transfer",
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyTypedData(request),
              },
            );
            ```

            ### 4.7. Set up minter client

            Once the API returns the attestation, initialize the Gateway Minter program and
            derive the PDA accounts needed for the Solana mint.

            <Info>
              The ordered remaining-account list and PDA derivations are documented in
              [Solana Programs and
              Interfaces](/gateway/references/solana-programs#gatewaymint). For static
              instruction definitions, use the onchain IDLs linked from that page.
            </Info>

            ```ts theme={null}
            const minterProgram = new Program(gatewayMinterIdl, provider);
            const [minterPda] = PublicKey.findProgramAddressSync(
              [Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
              minterProgramId,
            );
            ```

            ### 4.8. Mint on Solana

            Create the mint instruction, sign the transaction with the source
            Developer-Controlled Wallet, and confirm it on Solana Devnet.

            This script uses the source Solana wallet as both `payer` and
            `destinationCaller`. The recipient wallet owns the recipient ATA that receives
            the minted USDC.

            In this quickstart's client shape, the instruction accounts are assembled as the
            fixed `gatewayMint` accounts first, followed by one ordered triplet per
            attestation: `custody_token_account`, `destination_recipient`, and
            `transfer_spec_hash_account`.

            ```ts theme={null}
            const mintIx = await minterProgram.methods
              .gatewayMint({
                attestation: attestationBytes,
                signature: signatureBytes,
              })
              .accountsPartial({
                gatewayMinter: minterPda,
                destinationCaller: owner,
                payer: owner,
                systemProgram: SystemProgram.programId,
                tokenProgram: TOKEN_PROGRAM_ID,
              })
              .remainingAccounts(remainingAccounts)
              .instruction();
            ```

            ### 4.9. Full Solana transfer script (Circle Wallets)

            The script creates the recipient ATA, signs a Solana burn intent, requests a
            Gateway attestation, and mints on Solana Devnet. Inline comments explain each
            stage.

            ```ts transfer-from-sol.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import {
              Wallet,
              AnchorProvider,
              setProvider,
              Program,
              utils,
            } from "@coral-xyz/anchor";
            import {
              Connection,
              Keypair,
              PublicKey,
              SystemProgram,
              Transaction,
            } from "@solana/web3.js";
            import {
              TOKEN_PROGRAM_ID,
              getAssociatedTokenAddressSync,
              createAssociatedTokenAccountIdempotentInstruction,
            } from "@solana/spl-token";
            import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
            import {
              RPC_ENDPOINT,
              GATEWAY_WALLET_ADDRESS,
              GATEWAY_MINTER_ADDRESS,
              USDC_ADDRESS,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              client,
              gatewayMinterIdl,
              publicKey,
              hexToPublicKey,
              solanaAddressToBytes32,
              decodeAttestationSet,
              findCustodyPda,
              findTransferSpecHashPda,
              signAndBroadcast,
              stringifyTypedData,
            } from "./config.js";

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

            if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
              console.error(
                "Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
              );
              process.exit(1);
            }

            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;

            const TRANSFER_SPEC_MAGIC = 0xca85def7;
            const BURN_INTENT_MAGIC = 0x070afbc2;

            // Custom layout for 256-bit unsigned integers.
            class UInt256BE extends Layout<bigint> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0) {
                const buffer = b.subarray(offset, offset + 32);
                return buffer.readBigUInt64BE(24);
              }
              encode(src: bigint, b: Buffer, offset = 0) {
                const buffer = Buffer.alloc(32);
                buffer.writeBigUInt64BE(BigInt(src), 24);
                buffer.copy(b, offset);
                return 32;
              }
            }

            const uint256be = (property: string) => new UInt256BE(property);

            const BurnIntentLayout = struct([
              u32be("magic"),
              uint256be("maxBlockHeight"),
              uint256be("maxFee"),
              u32be("transferSpecLength"),
              struct(
                [
                  u32be("magic"),
                  u32be("version"),
                  u32be("sourceDomain"),
                  u32be("destinationDomain"),
                  publicKey("sourceContract"),
                  publicKey("destinationContract"),
                  publicKey("sourceToken"),
                  publicKey("destinationToken"),
                  publicKey("sourceDepositor"),
                  publicKey("destinationRecipient"),
                  publicKey("sourceSigner"),
                  publicKey("destinationCaller"),
                  uint256be("value"),
                  blob(32, "salt"),
                  u32be("hookDataLength"),
                  blob(offset(u32be(), -4), "hookData"),
                ] as any,
                "spec",
              ),
            ] as any);

            function createBurnIntent(params: {
              sourceDepositor: string;
              destinationRecipient: string;
              sourceSigner: string;
            }) {
              const { sourceDepositor, destinationRecipient, sourceSigner } = params;

              return {
                maxBlockHeight: MAX_UINT64,
                maxFee: MAX_FEE,
                spec: {
                  version: 1,
                  sourceDomain: SOLANA_DOMAIN,
                  destinationDomain: SOLANA_DOMAIN,
                  sourceContract: solanaAddressToBytes32(GATEWAY_WALLET_ADDRESS),
                  destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                  sourceToken: solanaAddressToBytes32(USDC_ADDRESS),
                  destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                  sourceDepositor: solanaAddressToBytes32(sourceDepositor),
                  destinationRecipient: solanaAddressToBytes32(destinationRecipient),
                  sourceSigner: solanaAddressToBytes32(sourceSigner),
                  destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                  value: TRANSFER_VALUE,
                  salt: "0x" + randomBytes(32).toString("hex"),
                  hookData: "0x",
                },
              };
            }

            // Encode the burn intent into the binary layout expected by Gateway.
            function encodeBurnIntent(bi: ReturnType<typeof createBurnIntent>): Buffer {
              const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
              const prepared = {
                magic: BURN_INTENT_MAGIC,
                maxBlockHeight: bi.maxBlockHeight,
                maxFee: bi.maxFee,
                transferSpecLength: 340 + hookData.length,
                spec: {
                  magic: TRANSFER_SPEC_MAGIC,
                  version: bi.spec.version,
                  sourceDomain: bi.spec.sourceDomain,
                  destinationDomain: bi.spec.destinationDomain,
                  sourceContract: hexToPublicKey(bi.spec.sourceContract),
                  destinationContract: hexToPublicKey(bi.spec.destinationContract),
                  sourceToken: hexToPublicKey(bi.spec.sourceToken),
                  destinationToken: hexToPublicKey(bi.spec.destinationToken),
                  sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
                  destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
                  sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
                  destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
                  value: bi.spec.value,
                  salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
                  hookDataLength: hookData.length,
                  hookData,
                },
              };
              const buffer = Buffer.alloc(72 + 340 + hookData.length);
              const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
              return buffer.subarray(0, bytesWritten);
            }

            // Create a lightweight Anchor provider for PDA derivation and instruction building.
            function createProvider(connection: Connection) {
              const dummyWallet = new Wallet(Keypair.generate());
              const provider = new AnchorProvider(
                connection,
                dummyWallet,
                AnchorProvider.defaultOptions(),
              );
              setProvider(provider);
              return provider;
            }

            async function main() {
              // Set up the Solana connection and destination accounts.
              const connection = new Connection(RPC_ENDPOINT, "confirmed");
              const usdcMint = new PublicKey(USDC_ADDRESS);
              const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
              const owner = new PublicKey(DEPOSITOR_ADDRESS);
              const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);

              console.log(`Using account: ${owner.toBase58()}`);
              console.log(`Transferring from: Solana Devnet -> Solana Devnet`);

              const provider = createProvider(connection);

              // [1] Create the recipient ATA if it does not already exist.
              const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
              const { blockhash: ataBlockhash, lastValidBlockHeight: ataBlockHeight } =
                await connection.getLatestBlockhash();
              const ataTx = new Transaction();
              ataTx.add(
                createAssociatedTokenAccountIdempotentInstruction(
                  owner,
                  recipientAta,
                  recipientPubkey,
                  usdcMint,
                ),
              );
              ataTx.recentBlockhash = ataBlockhash;
              ataTx.feePayer = owner;

              const ataSig = await signAndBroadcast(
                client,
                connection,
                ataTx,
                DEPOSITOR_ADDRESS,
                "ATA creation",
              );
              await connection.confirmTransaction(
                {
                  signature: ataSig,
                  blockhash: ataBlockhash,
                  lastValidBlockHeight: ataBlockHeight,
                },
                "confirmed",
              );

              // [2] Create and sign the Solana burn intent.
              const burnIntent = createBurnIntent({
                sourceDepositor: owner.toBase58(),
                destinationRecipient: recipientAta.toBase58(),
                sourceSigner: owner.toBase58(),
              });

              const encoded = encodeBurnIntent(burnIntent);
              const prefixed = Buffer.concat([
                Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
                encoded,
              ]);

              const sigResult = await client.signMessage({
                walletAddress: DEPOSITOR_ADDRESS,
                blockchain: "SOL-DEVNET",
                encodedByHex: true,
                message: "0x" + prefixed.toString("hex"),
              });

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

              const formattedSignature = burnIntentSignature.startsWith("0x")
                ? burnIntentSignature
                : `0x${burnIntentSignature}`;

              const request = [{ burnIntent, signature: formattedSignature }];
              console.log("Signed burn intent.");

              // [3] Request the attestation set from Gateway API.
              const response = await fetch(
                "https://gateway-api-testnet.circle.com/v1/transfer",
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyTypedData(request),
                },
              );

              const json = (await response.json()) as {
                attestation: string;
                signature: string;
                success?: boolean;
                message?: string;
              };
              if (json.success === false) {
                throw new Error(`Gateway API error: ${json.message}`);
              }
              console.log("Gateway API response:", JSON.stringify(json, null, 2));

              const { attestation, signature: mintSignature } = json;
              const decoded = decodeAttestationSet(attestation);

              // [4] Set up the Gateway Minter client and remaining accounts.
              const minterProgram = new Program(gatewayMinterIdl, provider);
              const [minterPda] = PublicKey.findProgramAddressSync(
                [Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
                minterProgramId,
              );

              const remainingAccounts = decoded.attestations.flatMap((e) => [
                {
                  pubkey: findCustodyPda(e.destinationToken, minterProgramId),
                  isWritable: true,
                  isSigner: false,
                },
                { pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
                {
                  pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
                  isWritable: true,
                  isSigner: false,
                },
              ]);

              const attestationBytes = Buffer.from(attestation.slice(2), "hex");
              const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");

              // [5] Mint on Solana with the returned attestation.
              console.log("Minting funds on Solana Devnet...");
              const mintIx = await minterProgram.methods
                .gatewayMint({
                  attestation: attestationBytes,
                  signature: signatureBytes,
                })
                .accountsPartial({
                  gatewayMinter: minterPda,
                  destinationCaller: owner,
                  payer: owner,
                  systemProgram: SystemProgram.programId,
                  tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(remainingAccounts)
                .instruction();

              const { blockhash, lastValidBlockHeight } =
                await connection.getLatestBlockhash();
              const mintTx = new Transaction();
              mintTx.add(mintIx);
              mintTx.recentBlockhash = blockhash;
              mintTx.feePayer = owner;

              const mintSig = await signAndBroadcast(
                client,
                connection,
                mintTx,
                DEPOSITOR_ADDRESS,
                "mint",
              );

              await connection.confirmTransaction(
                { signature: mintSig, blockhash, lastValidBlockHeight },
                "confirmed",
              );

              console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
              console.log(`Mint transaction hash (Solana Devnet):`, mintSig);
            }

            /* Main invocation */
            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.10. Run the Solana direct-mint script

            Run the script to burn from your Solana Devnet Gateway balance and mint to the
            recipient ATA on Solana Devnet.

            Confirm these values before running:

            * `DEPOSITOR_ADDRESS` is the source Solana Devnet wallet
            * `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Solana Devnet
            * the source Solana wallet can submit the ATA creation and mint transactions on
              Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-sol
            ```

            When the transfer succeeds, the script logs the minted amount and Solana Devnet
            mint transaction hash.
          </Tab>

          <Tab title="Forwarding Service">
            ### 4.1. Create the Solana forwarding script

            You can skip to the
            [full transfer script](#4-7-full-solana-forwarding-script-circle-wallets) if you
            prefer.

            ```shell theme={null}
            touch transfer-from-sol.ts
            ```

            ### 4.2. Define constants and response types

            This forwarding flow signs the burn intent on Solana Devnet, submits it with
            forwarding enabled, and lets Circle complete the Solana Devnet mint.

            ```ts transfer-from-sol.ts expandable theme={null}
            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            type TransferSpec = ReturnType<typeof createTransferSpec>;

            type BurnIntent = {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            };

            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;
            ```

            ### 4.3. Add Solana burn intent helpers

            The forwarding flow first estimates the burn-intent limits for the Solana Devnet
            to Solana Devnet route, then signs the estimate-provided burn intent with the
            source Developer-Controlled Wallet.

            ```ts transfer-from-sol.ts expandable theme={null}
            function createTransferSpec(params: {
              sourceDepositor: string;
              destinationRecipient: string;
              sourceSigner: string;
            }) {
              const { sourceDepositor, destinationRecipient, sourceSigner } = params;

              return {
                version: 1,
                sourceDomain: SOLANA_DOMAIN,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: solanaAddressToBytes32(GATEWAY_WALLET_ADDRESS),
                destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: solanaAddressToBytes32(USDC_ADDRESS),
                destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                sourceDepositor: solanaAddressToBytes32(sourceDepositor),
                destinationRecipient: solanaAddressToBytes32(destinationRecipient),
                sourceSigner: solanaAddressToBytes32(sourceSigner),
                destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: "0x" + randomBytes(32).toString("hex"),
                hookData: "0x",
              };
            }

            function createBurnIntent(params: {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            }): BurnIntent {
              return params;
            }
            ```

            ### 4.4. Estimate forwarding fees

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            Derive the recipient ATA, build the transfer spec, then call the Gateway
            estimate endpoint with `enableForwarder=true`. The response returns the `maxFee`
            and `maxBlockHeight` values to include in the signed burn intent.

            ```ts transfer-from-sol.ts theme={null}
            const owner = new PublicKey(DEPOSITOR_ADDRESS);
            const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
            const recipientAta = getAssociatedTokenAddressSync(
              new PublicKey(USDC_ADDRESS),
              recipientPubkey,
            );

            const spec = createTransferSpec({
              sourceDepositor: owner.toBase58(),
              destinationRecipient: recipientAta.toBase58(),
              sourceSigner: owner.toBase58(),
            });

            const estimateResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyTypedData([{ spec }]),
              },
            );
            ```

            ### 4.5. Sign and submit the forwarding burn intent

            Create the burn intent with the estimate-provided limits, sign it with the
            source Developer-Controlled Wallet, then submit it to the Gateway transfer
            endpoint with `enableForwarder=true`.

            ```ts transfer-from-sol.ts theme={null}
            const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
            const signature = await signBurnIntent(burnIntent);

            const transferResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyTypedData([{ burnIntent, signature }]),
              },
            );
            ```

            ### 4.6. Poll for forwarding completion

            The Forwarding Service completes the Solana Devnet mint for you. Poll the
            transfer status endpoint until the transfer is confirmed, finalized, failed, or
            expired.

            ```ts transfer-from-sol.ts theme={null}
            const pollResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
            );
            ```

            ### 4.7. Full Solana forwarding script (Circle Wallets)

            The complete script estimates forwarding fees, signs the burn intent on Solana
            Devnet, submits it with forwarding enabled, and polls until Circle completes the
            Solana Devnet mint.

            ```ts transfer-from-sol.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import { getAssociatedTokenAddressSync } from "@solana/spl-token";
            import { PublicKey } from "@solana/web3.js";
            import { u32be, struct, blob, offset, Layout } from "@solana/buffer-layout";
            import {
              GATEWAY_WALLET_ADDRESS,
              GATEWAY_MINTER_ADDRESS,
              USDC_ADDRESS,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              client,
              publicKey,
              hexToPublicKey,
              solanaAddressToBytes32,
              stringifyTypedData,
            } from "./config.js";

            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            type TransferSpec = ReturnType<typeof createTransferSpec>;

            type BurnIntent = {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            };

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

            if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
              console.error(
                "Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
              );
              process.exit(1);
            }

            /* Transfer parameters */
            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;

            const TRANSFER_SPEC_MAGIC = 0xca85def7;
            const BURN_INTENT_MAGIC = 0x070afbc2;

            /* Custom layout for 256-bit unsigned integers (burn intent encoding only) */
            // @solana/buffer-layout's TypeScript definitions are incomplete (archived Jan 2025)
            class UInt256BE extends Layout<bigint> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0) {
                return b.subarray(offset, offset + 32).readBigUInt64BE(24);
              }
              encode(src: bigint, b: Buffer, offset = 0) {
                const buffer = Buffer.alloc(32);
                buffer.writeBigUInt64BE(BigInt(src), 24);
                buffer.copy(b, offset);
                return 32;
              }
            }

            const uint256be = (property: string) => new UInt256BE(property);

            /* Burn intent binary layout */
            // 'as any' needed due to @solana/buffer-layout's incomplete TypeScript definitions (archived Jan 2025)
            const BurnIntentLayout = struct([
              u32be("magic"),
              uint256be("maxBlockHeight"),
              uint256be("maxFee"),
              u32be("transferSpecLength"),
              struct(
                [
                  u32be("magic"),
                  u32be("version"),
                  u32be("sourceDomain"),
                  u32be("destinationDomain"),
                  publicKey("sourceContract"),
                  publicKey("destinationContract"),
                  publicKey("sourceToken"),
                  publicKey("destinationToken"),
                  publicKey("sourceDepositor"),
                  publicKey("destinationRecipient"),
                  publicKey("sourceSigner"),
                  publicKey("destinationCaller"),
                  uint256be("value"),
                  blob(32, "salt"),
                  u32be("hookDataLength"),
                  blob(offset(u32be(), -4), "hookData"),
                ] as any,
                "spec",
              ),
            ] as any);

            /* Helpers */
            function createTransferSpec(params: {
              sourceDepositor: string;
              destinationRecipient: string;
              sourceSigner: string;
            }) {
              const { sourceDepositor, destinationRecipient, sourceSigner } = params;

              return {
                version: 1,
                sourceDomain: SOLANA_DOMAIN,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: solanaAddressToBytes32(GATEWAY_WALLET_ADDRESS),
                destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: solanaAddressToBytes32(USDC_ADDRESS),
                destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                sourceDepositor: solanaAddressToBytes32(sourceDepositor),
                destinationRecipient: solanaAddressToBytes32(destinationRecipient),
                sourceSigner: solanaAddressToBytes32(sourceSigner),
                destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: "0x" + randomBytes(32).toString("hex"),
                hookData: "0x",
              };
            }

            function createBurnIntent(params: {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            }): BurnIntent {
              return params;
            }

            function encodeBurnIntent(burnIntent: BurnIntent): Buffer {
              const hookData = Buffer.from(burnIntent.spec.hookData.slice(2), "hex");
              const prepared = {
                magic: BURN_INTENT_MAGIC,
                maxBlockHeight: burnIntent.maxBlockHeight,
                maxFee: burnIntent.maxFee,
                transferSpecLength: 340 + hookData.length,
                spec: {
                  magic: TRANSFER_SPEC_MAGIC,
                  version: burnIntent.spec.version,
                  sourceDomain: burnIntent.spec.sourceDomain,
                  destinationDomain: burnIntent.spec.destinationDomain,
                  sourceContract: hexToPublicKey(burnIntent.spec.sourceContract),
                  destinationContract: hexToPublicKey(burnIntent.spec.destinationContract),
                  sourceToken: hexToPublicKey(burnIntent.spec.sourceToken),
                  destinationToken: hexToPublicKey(burnIntent.spec.destinationToken),
                  sourceDepositor: hexToPublicKey(burnIntent.spec.sourceDepositor),
                  destinationRecipient: hexToPublicKey(
                    burnIntent.spec.destinationRecipient,
                  ),
                  sourceSigner: hexToPublicKey(burnIntent.spec.sourceSigner),
                  destinationCaller: hexToPublicKey(burnIntent.spec.destinationCaller),
                  value: burnIntent.spec.value,
                  salt: Buffer.from(burnIntent.spec.salt.slice(2), "hex"),
                  hookDataLength: hookData.length,
                  hookData,
                },
              };
              const buffer = Buffer.alloc(72 + 340 + hookData.length);
              const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
              return buffer.subarray(0, bytesWritten);
            }

            async function signBurnIntent(burnIntent: BurnIntent): Promise<string> {
              const encoded = encodeBurnIntent(burnIntent);
              const prefixed = Buffer.concat([
                Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
                encoded,
              ]);

              const sigResult = await client.signMessage({
                walletAddress: DEPOSITOR_ADDRESS,
                blockchain: "SOL-DEVNET",
                encodedByHex: true,
                message: "0x" + prefixed.toString("hex"),
              });

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

              return signature.startsWith("0x") ? signature : `0x${signature}`;
            }

            function formatUsdc(value: bigint): string {
              const whole = value / 1_000_000n;
              const fraction = (value % 1_000_000n).toString().padStart(6, "0");
              return `${whole}.${fraction}`.replace(/\.?0+$/, "");
            }

            /* Main logic */
            async function main() {
              const owner = new PublicKey(DEPOSITOR_ADDRESS);
              const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
              const recipientAta = getAssociatedTokenAddressSync(
                new PublicKey(USDC_ADDRESS),
                recipientPubkey,
              );

              console.log(`Sender (Solana Devnet): ${owner.toBase58()}`);
              console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
              console.log(`Amount: ${formatUsdc(TRANSFER_VALUE)} USDC`);
              console.log("Forwarding: enabled\n");

              // [1] Create transfer spec and estimate forwarding fees
              const spec = createTransferSpec({
                sourceDepositor: owner.toBase58(),
                destinationRecipient: recipientAta.toBase58(),
                sourceSigner: owner.toBase58(),
              });

              const estimateResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyTypedData([{ spec }]),
                },
              );

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

              const estimateResult = (await estimateResponse.json()) as EstimateResponse;
              const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
              if (!estimatedBurnIntent) {
                throw new Error("Estimate API response missing burnIntent");
              }

              const estimatedMaxFee = BigInt(estimatedBurnIntent.maxFee);
              const maxFee = estimatedMaxFee + MAX_FEE_BUFFER;
              const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);

              if (estimateResult.fees.forwardingFee) {
                console.log(
                  `Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
                );
              }
              console.log(
                `Estimated maxFee: ${formatUsdc(estimatedMaxFee)} ${estimateResult.fees.token}`,
              );
              console.log(
                `Signed maxFee: ${formatUsdc(maxFee)} ${estimateResult.fees.token}`,
              );

              // [2] Create, sign, and submit burn intent
              const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
              const signature = await signBurnIntent(burnIntent);

              const transferResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyTypedData([{ burnIntent, signature }]),
                },
              );

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

              const transferResult = (await transferResponse.json()) as TransferResponse;
              if (!transferResult.transferId) {
                throw new Error("Gateway API response missing transferId");
              }
              console.log(`Transfer ID: ${transferResult.transferId}`);

              // [3] Poll for forwarding completion
              const pollStart = Date.now();
              while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
                const pollResponse = await fetch(
                  `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
                );

                if (!pollResponse.ok) {
                  console.error(`Poll error: ${pollResponse.status}`);
                  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
                  continue;
                }

                const details = (await pollResponse.json()) as TransferStatusResponse;
                console.log(`Status: ${details.status}`);

                if (details.status === "finalized" || details.status === "confirmed") {
                  console.log(
                    `Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
                  );
                  console.log(`Forwarded ${formatUsdc(TRANSFER_VALUE)} USDC`);
                  return;
                }

                if (details.status === "failed") {
                  const reason = details.forwardingDetails?.failureReason ?? "unknown";
                  throw new Error(`Transfer failed: ${reason}`);
                }

                if (details.status === "expired") {
                  throw new Error("Transfer attestation expired before forwarding");
                }

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

              throw new Error("Polling timed out waiting for transfer completion");
            }

            /* Main invocation */
            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.8. Run the Solana forwarding script

            Run the script to burn from your Solana Devnet Gateway balance and let the
            Forwarding Service mint to the recipient ATA on Solana Devnet.

            Confirm these values before running:

            * `DEPOSITOR_ADDRESS` is the source Solana Devnet wallet
            * `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Solana Devnet
            * the recipient ATA exists on Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-sol
            ```

            When the transfer succeeds, the script logs the transfer ID, status, Solana
            Devnet mint transaction hash, and forwarded amount.
          </Tab>
        </Tabs>
      </Tab>

      <Tab title="Transfer from EVM">
        ## Step 4. Transfer USDC from EVM to Solana

        This step transfers USDC from your Arc Testnet Gateway balance to Solana Devnet.
        Both paths create and submit the burn intent from Arc Testnet first. Direct mint
        then retrieves the Gateway attestation and calls `gatewayMint(...)` on Solana
        Devnet from your Solana wallet, while Forwarding Service lets Circle complete
        the Solana mint for you.

        <Tabs>
          <Tab title="Direct Mint">
            ### 4.1. Create the EVM to Solana transfer script

            You can skip to the
            [full transfer script](#4-9-full-evm-to-solana-transfer-script-circle-wallets)
            if you prefer.

            ```shell theme={null}
            touch transfer-from-evm.ts
            ```

            ### 4.2. Define constants and types

            This flow signs an EIP-712 burn intent on Arc Testnet, requests a Gateway
            attestation, and then calls `gatewayMint(...)` on Solana Devnet.

            ```ts theme={null}
            const SOURCE_CHAIN = {
              chainName: "Arc Testnet",
              gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9",
              usdc: "0x3600000000000000000000000000000000000000",
              domain: 26,
              walletChain: "ARC-TESTNET" as const,
            };

            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;
            const MAX_UINT64_DEC = MAX_UINT64.toString();
            ```

            ### 4.3. Create the burn intent helpers

            The burn intent uses EVM `bytes32` values for the Arc Testnet source fields and
            Solana `bytes32` values for the destination Gateway Minter, USDC mint, and
            recipient ATA.

            ```ts theme={null}
            function createBurnIntent(params: {
              depositorAddress: string;
              recipientAta: string;
            }) {
              const { depositorAddress, recipientAta } = params;

              return {
                maxBlockHeight: MAX_UINT64_DEC,
                maxFee: MAX_FEE,
                spec: {
                  version: 1,
                  sourceDomain: SOURCE_CHAIN.domain,
                  destinationDomain: SOLANA_DOMAIN,
                  sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                  destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                  sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                  destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                  sourceDepositor: evmAddressToBytes32(depositorAddress),
                  destinationRecipient: solanaAddressToBytes32(recipientAta),
                  sourceSigner: evmAddressToBytes32(depositorAddress),
                  destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                  value: TRANSFER_VALUE,
                  salt: "0x" + randomBytes(32).toString("hex"),
                  hookData: "0x",
                },
              };
            }
            ```

            ### 4.4. Initialize connection and create recipient ATA

            Set up the Solana connection, derive the recipient associated token account
            (ATA), and create it idempotently before you request a Gateway attestation.

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            ```ts theme={null}
            const connection = new Connection(RPC_ENDPOINT, "confirmed");
            const usdcMint = new PublicKey(USDC_ADDRESS);
            const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
            const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
            ```

            ### 4.5. Create and sign the burn intent

            Create one burn intent for the Arc Testnet Gateway balance and sign it with the
            source Developer-Controlled Wallet.

            ```ts theme={null}
            const burnIntent = createBurnIntent({
              depositorAddress: DEPOSITOR_ADDRESS,
              recipientAta: recipientAta.toBase58(),
            });

            const typedData = burnIntentTypedData(burnIntent);

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

            ### 4.6. Request the Gateway attestation

            Submit the signed burn intent to the Gateway API and validate that the response
            includes both the attestation and operator signature needed for the Solana mint.

            ```ts theme={null}
            const response = await fetch(
              "https://gateway-api-testnet.circle.com/v1/transfer",
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
              },
            );

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

            const json = (await response.json()) as {
              attestation?: string;
              signature?: string;
            };

            const { attestation, signature: mintSignature } = json;
            if (!attestation || !mintSignature) {
              throw new Error("Gateway API response missing attestation or signature");
            }
            ```

            ### 4.7. Set up minter client

            After decoding the attestation set, initialize the Gateway Minter program and
            build the remaining account list expected by the Solana mint instruction.

            <Info>
              The ordered remaining-account list and PDA derivations are documented in
              [Solana Programs and
              Interfaces](/gateway/references/solana-programs#gatewaymint). For static
              instruction definitions, use the onchain IDLs linked from that page.
            </Info>

            ```ts theme={null}
            const decoded = decodeAttestationSet(attestation);
            const minterProgram = new Program(gatewayMinterIdl, provider);

            const remainingAccounts = decoded.attestations.flatMap((e) => [
              {
                pubkey: findCustodyPda(e.destinationToken, minterProgramId),
                isWritable: true,
                isSigner: false,
              },
              { pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
              {
                pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
                isWritable: true,
                isSigner: false,
              },
            ]);
            ```

            ### 4.8. Mint on Solana

            Create the Solana mint instruction, sign it with the destination
            Developer-Controlled Wallet, then confirm the transaction on Solana Devnet.

            This script sets `destinationCaller` to the Solana zero address in the burn
            intent, so the recipient wallet can submit the mint as both `payer` and
            `destinationCaller`.

            ```ts theme={null}
            const mintIx = await minterProgram.methods
              .gatewayMint({
                attestation: Buffer.from(attestation.slice(2), "hex"),
                signature: Buffer.from(mintSignature.slice(2), "hex"),
              })
              .accountsPartial({
                gatewayMinter: minterPda,
                destinationCaller: recipientPubkey,
                payer: recipientPubkey,
                systemProgram: SystemProgram.programId,
                tokenProgram: TOKEN_PROGRAM_ID,
              })
              .remainingAccounts(remainingAccounts)
              .instruction();
            ```

            ### 4.9. Full EVM to Solana transfer script (Circle Wallets)

            The script creates the recipient ATA, signs one Arc Testnet burn intent,
            requests a Gateway attestation, and mints on Solana Devnet. Inline comments
            explain each stage.

            ```ts transfer-from-evm.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import {
              Wallet,
              AnchorProvider,
              setProvider,
              Program,
              utils,
            } from "@coral-xyz/anchor";
            import {
              createAssociatedTokenAccountIdempotentInstruction,
              getAssociatedTokenAddressSync,
              TOKEN_PROGRAM_ID,
            } from "@solana/spl-token";
            import {
              Connection,
              Keypair,
              PublicKey,
              SystemProgram,
              Transaction,
            } from "@solana/web3.js";
            import {
              RPC_ENDPOINT,
              GATEWAY_MINTER_ADDRESS,
              USDC_ADDRESS,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              client,
              gatewayMinterIdl,
              solanaAddressToBytes32,
              decodeAttestationSet,
              findCustodyPda,
              findTransferSpecHashPda,
              signAndBroadcast,
              stringifyTypedData,
            } from "./config.js";

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

            if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
              console.error(
                "Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
              );
              process.exit(1);
            }

            const SOURCE_CHAIN = {
              chainName: "Arc Testnet",
              gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9",
              usdc: "0x3600000000000000000000000000000000000000",
              domain: 26,
              walletChain: "ARC-TESTNET" as const,
            };

            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;
            const MAX_UINT64_DEC = MAX_UINT64.toString();

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

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

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

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

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

              return {
                maxBlockHeight: MAX_UINT64_DEC,
                maxFee: MAX_FEE,
                spec: {
                  version: 1,
                  sourceDomain: SOURCE_CHAIN.domain,
                  destinationDomain: SOLANA_DOMAIN,
                  sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                  destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                  sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                  destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                  sourceDepositor: evmAddressToBytes32(depositorAddress),
                  destinationRecipient: solanaAddressToBytes32(recipientAta),
                  sourceSigner: evmAddressToBytes32(depositorAddress),
                  destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                  value: TRANSFER_VALUE,
                  salt: "0x" + randomBytes(32).toString("hex"),
                  hookData: "0x",
                },
              };
            }

            function burnIntentTypedData(burnIntent: ReturnType<typeof createBurnIntent>) {
              return {
                types: { EIP712Domain, TransferSpec, BurnIntent },
                domain: eip712Domain,
                primaryType: "BurnIntent",
                message: burnIntent,
              };
            }

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

            function createProvider(connection: Connection) {
              const dummyWallet = new Wallet(Keypair.generate());
              const provider = new AnchorProvider(
                connection,
                dummyWallet,
                AnchorProvider.defaultOptions(),
              );
              setProvider(provider);
              return provider;
            }

            async function main() {
              console.log(`Sender (${SOURCE_CHAIN.chainName}): ${DEPOSITOR_ADDRESS}`);
              console.log(`Recipient (Solana Devnet): ${RECIPIENT_ADDRESS}`);

              const connection = new Connection(RPC_ENDPOINT, "confirmed");
              const usdcMint = new PublicKey(USDC_ADDRESS);
              const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
              const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);

              const provider = createProvider(connection);

              // [1] Create recipient's Associated Token Account
              const recipientAta = getAssociatedTokenAddressSync(usdcMint, recipientPubkey);
              console.log(`Recipient ATA: ${recipientAta.toBase58()}`);

              const { blockhash: ataBlockhash, lastValidBlockHeight: ataBlockHeight } =
                await connection.getLatestBlockhash();
              const ataTx = new Transaction();
              ataTx.add(
                createAssociatedTokenAccountIdempotentInstruction(
                  recipientPubkey,
                  recipientAta,
                  recipientPubkey,
                  usdcMint,
                ),
              );
              ataTx.recentBlockhash = ataBlockhash;
              ataTx.feePayer = recipientPubkey;

              const ataSig = await signAndBroadcast(
                client,
                connection,
                ataTx,
                RECIPIENT_ADDRESS,
                "ATA creation",
              );
              await connection.confirmTransaction(
                {
                  signature: ataSig,
                  blockhash: ataBlockhash,
                  lastValidBlockHeight: ataBlockHeight,
                },
                "confirmed",
              );

              // [2] Create and sign burn intent
              const burnIntent = createBurnIntent({
                depositorAddress: DEPOSITOR_ADDRESS,
                recipientAta: recipientAta.toBase58(),
              });
              const typedData = burnIntentTypedData(burnIntent);

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

              const signature = sigResp.data?.signature;
              if (!signature) {
                throw new Error("Failed to sign burn intent");
              }
              console.log("Signed burn intent.");

              // [3] Request attestation from Gateway API
              const response = await fetch(
                "https://gateway-api-testnet.circle.com/v1/transfer",
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
                },
              );

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

              const json = (await response.json()) as {
                attestation?: string;
                signature?: string;
              };
              console.log("Gateway API response:", JSON.stringify(json, null, 2));

              const { attestation, signature: mintSignature } = json;
              if (!attestation || !mintSignature) {
                throw new Error("Gateway API response missing attestation or signature");
              }

              // [4] Set up the minter client
              const decoded = decodeAttestationSet(attestation);
              const minterProgram = new Program(gatewayMinterIdl, provider);
              const [minterPda] = PublicKey.findProgramAddressSync(
                [Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
                minterProgramId,
              );

              const remainingAccounts = decoded.attestations.flatMap((attestationItem) => [
                {
                  pubkey: findCustodyPda(attestationItem.destinationToken, minterProgramId),
                  isWritable: true,
                  isSigner: false,
                },
                {
                  pubkey: attestationItem.destinationRecipient,
                  isWritable: true,
                  isSigner: false,
                },
                {
                  pubkey: findTransferSpecHashPda(
                    attestationItem.transferSpecHash,
                    minterProgramId,
                  ),
                  isWritable: true,
                  isSigner: false,
                },
              ]);

              const attestationBytes = Buffer.from(attestation.slice(2), "hex");
              const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");

              // [5] Mint on Solana
              console.log("Minting funds on Solana Devnet...");
              const mintIx = await minterProgram.methods
                .gatewayMint({
                  attestation: attestationBytes,
                  signature: signatureBytes,
                })
                .accountsPartial({
                  gatewayMinter: minterPda,
                  destinationCaller: recipientPubkey,
                  payer: recipientPubkey,
                  systemProgram: SystemProgram.programId,
                  tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(remainingAccounts)
                .instruction();

              const { blockhash, lastValidBlockHeight } =
                await connection.getLatestBlockhash();
              const mintTx = new Transaction();
              mintTx.add(mintIx);
              mintTx.recentBlockhash = blockhash;
              mintTx.feePayer = recipientPubkey;

              const mintSig = await signAndBroadcast(
                client,
                connection,
                mintTx,
                RECIPIENT_ADDRESS,
                "mint",
              );

              // [6] Wait for confirmation
              await connection.confirmTransaction(
                { signature: mintSig, blockhash, lastValidBlockHeight },
                "confirmed",
              );

              console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
              console.log(`Mint transaction hash (Solana Devnet):`, mintSig);
            }

            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.10. Run the EVM to Solana direct-mint script

            Run the script to burn from your Arc Testnet Gateway balance and mint to the
            recipient ATA on Solana Devnet.

            Confirm these values before running:

            * `DEPOSITOR_ADDRESS` is the source Arc Testnet wallet
            * `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Arc Testnet
            * the destination Solana wallet can submit the ATA creation and mint
              transactions on Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-evm
            ```

            When the transfer succeeds, the script logs the minted amount and Solana Devnet
            mint transaction hash.
          </Tab>

          <Tab title="Forwarding Service">
            ### 4.1. Create the EVM to Solana forwarding script

            You can skip to the
            [full transfer script](#4-7-full-evm-to-solana-forwarding-script-circle-wallets)
            if you prefer.

            ```shell theme={null}
            touch transfer-from-evm.ts
            ```

            ### 4.2. Define constants and response types

            This forwarding flow signs the burn intent on Arc Testnet, submits it with
            forwarding enabled, and lets Circle complete the Solana Devnet mint.

            ```ts theme={null}
            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;
            ```

            ### 4.3. Create the transfer spec helpers

            The forwarding flow estimates the burn-intent limits for the Arc Testnet to
            Solana Devnet route before signing. The transfer spec still identifies the
            recipient's Solana ATA as the destination recipient.

            ```ts theme={null}
            function createTransferSpec(params: {
              depositorAddress: string;
              recipientAta: string;
            }) {
              const { depositorAddress, recipientAta } = params;

              return {
                version: 1,
                sourceDomain: SOURCE_CHAIN.domain,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                sourceDepositor: evmAddressToBytes32(depositorAddress),
                destinationRecipient: solanaAddressToBytes32(recipientAta),
                sourceSigner: evmAddressToBytes32(depositorAddress),
                destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: "0x" + randomBytes(32).toString("hex"),
                hookData: "0x",
              };
            }
            ```

            ### 4.4. Estimate forwarding fees

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            Call the Gateway estimate endpoint with `enableForwarder=true`. The response
            returns the `maxFee` and `maxBlockHeight` values to include in the signed burn
            intent.

            ```ts theme={null}
            const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
            const recipientAta = getAssociatedTokenAddressSync(
              new PublicKey(USDC_ADDRESS),
              recipientPubkey,
            );

            const spec = createTransferSpec({
              depositorAddress: DEPOSITOR_ADDRESS,
              recipientAta: recipientAta.toBase58(),
            });

            const estimateResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyTypedData([{ spec }]),
              },
            );

            const estimateResult = (await estimateResponse.json()) as EstimateResponse;
            const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
            if (!estimatedBurnIntent) {
              throw new Error("Estimate API response missing burnIntent");
            }

            const estimatedMaxFee = BigInt(estimatedBurnIntent.maxFee);
            const maxFee = estimatedMaxFee + MAX_FEE_BUFFER;
            const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);
            ```

            ### 4.5. Sign and submit the forwarding burn intent

            Sign the estimated burn intent with the Arc Testnet Developer-Controlled Wallet,
            then submit it to the Gateway transfer endpoint with `enableForwarder=true`.

            ```ts theme={null}
            const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });

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

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

            const transferResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
              },
            );
            ```

            ### 4.6. Poll for forwarding completion

            The Forwarding Service completes the Solana Devnet mint for you. Poll the
            transfer status endpoint until the transfer is confirmed, finalized, failed, or
            expired.

            ```ts theme={null}
            const pollResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
            );
            ```

            ### 4.7. Full EVM to Solana forwarding script (Circle Wallets)

            The script estimates forwarding fees, signs the burn intent on Arc Testnet,
            submits it with forwarding enabled, and polls until Circle completes the Solana
            Devnet mint. Inline comments explain each stage.

            ```ts transfer-from-evm.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import { getAssociatedTokenAddressSync } from "@solana/spl-token";
            import { PublicKey } from "@solana/web3.js";
            import {
              GATEWAY_MINTER_ADDRESS,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              USDC_ADDRESS,
              client,
              solanaAddressToBytes32,
              stringifyTypedData,
            } from "./config.js";

            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            /* Environment */
            const DEPOSITOR_ADDRESS = process.env.DEPOSITOR_ADDRESS!;
            const RECIPIENT_ADDRESS = process.env.RECIPIENT_ADDRESS!;

            if (!DEPOSITOR_ADDRESS || !RECIPIENT_ADDRESS) {
              console.error(
                "Missing required env vars: DEPOSITOR_ADDRESS, RECIPIENT_ADDRESS",
              );
              process.exit(1);
            }

            /* Constants */
            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";

            const SOURCE_CHAIN = {
              chainName: "Arc Testnet",
              gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9",
              usdc: "0x3600000000000000000000000000000000000000",
              domain: 26,
              walletChain: "ARC-TESTNET" as const,
            };

            const TRANSFER_AMOUNT = 0.1;
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE_BUFFER = 10_000n; // 0.01 USDC fee buffer
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;

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

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

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

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

            /* Helpers */
            function createTransferSpec(params: {
              depositorAddress: string;
              recipientAta: string;
            }) {
              const { depositorAddress, recipientAta } = params;

              return {
                version: 1,
                sourceDomain: SOURCE_CHAIN.domain,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                sourceDepositor: evmAddressToBytes32(depositorAddress),
                destinationRecipient: solanaAddressToBytes32(recipientAta),
                sourceSigner: evmAddressToBytes32(depositorAddress),
                destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: "0x" + randomBytes(32).toString("hex"),
                hookData: "0x",
              };
            }

            function burnIntentTypedData(params: {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: ReturnType<typeof createTransferSpec>;
            }) {
              return {
                types: { EIP712Domain, TransferSpec, BurnIntent },
                domain: eip712Domain,
                primaryType: "BurnIntent",
                message: params,
              };
            }

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

            function formatUsdc(value: bigint): string {
              const whole = value / 1_000_000n;
              const fraction = (value % 1_000_000n).toString().padStart(6, "0");
              return `${whole}.${fraction}`.replace(/\.?0+$/, "");
            }

            /* Main logic */
            async function main() {
              const recipientPubkey = new PublicKey(RECIPIENT_ADDRESS);
              const recipientAta = getAssociatedTokenAddressSync(
                new PublicKey(USDC_ADDRESS),
                recipientPubkey,
              );

              console.log(`Sender (${SOURCE_CHAIN.chainName}): ${DEPOSITOR_ADDRESS}`);
              console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
              console.log(`Amount: ${formatUsdc(TRANSFER_VALUE)} USDC`);
              console.log("Forwarding: enabled\n");

              // [1] Create transfer spec and estimate forwarding fees
              const spec = createTransferSpec({
                depositorAddress: DEPOSITOR_ADDRESS,
                recipientAta: recipientAta.toBase58(),
              });

              const estimateResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyTypedData([{ spec }]),
                },
              );

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

              const estimateResult = (await estimateResponse.json()) as EstimateResponse;
              const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
              if (!estimatedBurnIntent) {
                throw new Error("Estimate API response missing burnIntent");
              }

              const estimatedMaxFee = BigInt(estimatedBurnIntent.maxFee);
              const maxFee = estimatedMaxFee + MAX_FEE_BUFFER;
              const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);

              if (estimateResult.fees.forwardingFee) {
                console.log(
                  `Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
                );
              }
              console.log(
                `Estimated maxFee: ${formatUsdc(estimatedMaxFee)} ${estimateResult.fees.token}`,
              );
              console.log(
                `Signed maxFee: ${formatUsdc(maxFee)} ${estimateResult.fees.token}`,
              );

              // [2] Create, sign, and submit burn intent
              const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });

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

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

              const transferResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyTypedData([{ burnIntent: typedData.message, signature }]),
                },
              );

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

              const transferResult = (await transferResponse.json()) as TransferResponse;
              if (!transferResult.transferId) {
                throw new Error("Gateway API response missing transferId");
              }
              console.log(`Transfer ID: ${transferResult.transferId}`);

              // [3] Poll for forwarding completion
              const pollStart = Date.now();
              while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
                const pollResponse = await fetch(
                  `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
                );

                if (!pollResponse.ok) {
                  console.error(`Poll error: ${pollResponse.status}`);
                  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
                  continue;
                }

                const details = (await pollResponse.json()) as TransferStatusResponse;
                console.log(`Status: ${details.status}`);

                if (details.status === "finalized" || details.status === "confirmed") {
                  console.log(
                    `Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
                  );
                  console.log(`Forwarded ${formatUsdc(TRANSFER_VALUE)} USDC`);
                  return;
                }

                if (details.status === "failed") {
                  const reason = details.forwardingDetails?.failureReason ?? "unknown";
                  throw new Error(`Transfer failed: ${reason}`);
                }

                if (details.status === "expired") {
                  throw new Error("Transfer attestation expired before forwarding");
                }

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

              throw new Error("Polling timed out waiting for transfer completion");
            }

            /* Main invocation */
            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.8. Run the EVM to Solana forwarding script

            Run the script to burn from your Arc Testnet Gateway balance and let the
            Forwarding Service mint on Solana Devnet.

            Confirm these values before running:

            * `DEPOSITOR_ADDRESS` is the source Arc Testnet wallet
            * `RECIPIENT_ADDRESS` is the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Arc Testnet
            * the recipient ATA exists on Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-evm
            ```

            When the transfer succeeds, the script logs the transfer ID, status, Solana
            Devnet mint transaction hash, and forwarded amount.
          </Tab>
        </Tabs>
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Self-managed">
    ## Prerequisites

    Before you begin, ensure that you've:

    * Installed [Node.js v22.6+](https://nodejs.org/)
    * Prepared Solana Devnet wallets (sender and recipient) and have the private key
      pairs exported as JSON arrays

    If you want to try the EVM to Solana transfer, ensure that you've:

    * Prepared an EVM testnet wallet with the private key available
      * Added Arc Testnet to your wallet
    * Completed
      [Step 3: Deposit into a unified crosschain balance](/gateway/quickstarts/unified-balance-evm#step-3-deposit-into-a-unified-crosschain-balance-self-managed)
      from the
      [EVM quickstart](/gateway/quickstarts/unified-balance-evm#self-managed)

    ### Add testnet funds to your wallet

    To interact with Gateway, you need test USDC and native tokens in your wallet on
    each chain you deposit from. For direct mints on Solana, the wallet that submits
    the Solana transaction also needs SOL to create the recipient Associated Token
    Account and call the Gateway Minter program.

    Use the [Circle Faucet](https://faucet.circle.com/) to get testnet USDC. If you
    have a [Circle Developer Console](https://console.circle.com) account, you can
    use the [Console Faucet](https://console.circle.com/faucet) to get testnet
    native tokens. In addition, the following faucets can also be used to fund your
    wallet with testnet native tokens:

    <Tabs>
      <Tab title="Arc">
        **Faucet:** [Arc Testnet](https://faucet.circle.com) (USDC + native tokens)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `arcTestnet`                                 |
        | USDC address | `0x3600000000000000000000000000000000000000` |
        | Domain ID    | `26`                                         |
      </Tab>

      <Tab title="Avalanche">
        **Faucet:** [Avalanche Fuji](https://core.app/tools/testnet-faucet)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `avalancheFuji`                              |
        | USDC address | `0x5425890298aed601595a70ab815c96711a31bc65` |
        | Domain ID    | `1`                                          |
      </Tab>

      <Tab title="Base">
        **Faucet:** [Base Sepolia](https://www.alchemy.com/faucets/base-sepolia)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `baseSepolia`                                |
        | USDC address | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
        | Domain ID    | `6`                                          |
      </Tab>

      <Tab title="Ethereum">
        **Faucet:** [Ethereum Sepolia](https://cloud.google.com/application/web3/faucet/ethereum/sepolia)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `sepolia`                                    |
        | USDC address | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
        | Domain ID    | `0`                                          |
      </Tab>

      <Tab title="Hyperliquid">
        **Faucet:** [Hyperliquid EVM Testnet](https://app.hyperliquid-testnet.xyz/drip)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `hyperliquidEvmTestnet`                      |
        | USDC address | `0x2B3370eE501B4a559b57D449569354196457D8Ab` |
        | Domain ID    | `19`                                         |
      </Tab>

      <Tab title="Sei">
        **Faucet:** [Sei Testnet](https://docs.sei.io/learn/faucet)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `seiTestnet`                                 |
        | USDC address | `0x4fCF1784B31630811181f670Aea7A7bEF803eaED` |
        | Domain ID    | `16`                                         |
      </Tab>

      <Tab title="Solana">
        **Faucet:** [Solana Devnet](https://faucet.solana.com/)

        | Property     | Value                                                   |
        | ------------ | ------------------------------------------------------- |
        | Chain name   | `solanaDevnet` (note that Solana is not EVM-compatible) |
        | USDC address | `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`          |
        | Domain ID    | `5`                                                     |
      </Tab>

      <Tab title="Sonic">
        **Faucet:** [Sonic Testnet](https://testnet.soniclabs.com/account)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `sonicTestnet`                               |
        | USDC address | `0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51` |
        | Domain ID    | `13`                                         |
      </Tab>

      <Tab title="Worldchain">
        **Faucet:** [Worldchain Sepolia](https://www.l2faucet.com/world)

        | Property     | Value                                        |
        | ------------ | -------------------------------------------- |
        | Chain name   | `worldchainSepolia`                          |
        | USDC address | `0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88` |
        | Domain ID    | `14`                                         |
      </Tab>
    </Tabs>

    ## Step 1. Set up your project

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

    ### 1.1. Create the project and install dependencies

    Create a new directory and install the required dependencies:

    ```shell theme={null}
    # Set up your directory and initialize a Node.js project
    mkdir unified-gateway-balance-sol
    cd unified-gateway-balance-sol
    npm init -y

    # Set up module type and run scripts
    npm pkg set type=module
    npm pkg set scripts.deposit="node --env-file=.env deposit.ts"
    npm pkg set scripts.transfer-from-sol="node --env-file=.env transfer-from-sol.ts"
    npm pkg set scripts.balances="node --env-file=.env balances.ts"

    # Install dependencies
    npm pkg set overrides.bigint-buffer=npm:@trufflesuite/bigint-buffer@1.1.10
    npm install @coral-xyz/anchor @solana/buffer-layout @solana/spl-token @solana/web3.js bn.js bs58 typescript
    npm install --save-dev @types/bn.js @types/node
    ```

    If you want to try the EVM to Solana transfer, add the run script and install
    [Viem](https://viem.sh/) :

    ```shell theme={null}
    npm pkg set scripts.transfer-from-evm="node --env-file=.env transfer-from-evm.ts"
    npm install viem
    ```

    ### 1.2. Configure TypeScript (optional)

    <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. Set environment variables

    Open `.env` in your editor and add:

    ```text theme={null}
    SOLANA_PRIVATE_KEYPAIR=YOUR_SOLANA_KEYPAIR_ARRAY
    RECIPIENT_KEYPAIR=YOUR_RECIPIENT_KEYPAIR_ARRAY
    ```

    * `SOLANA_PRIVATE_KEYPAIR` is the primary Solana wallet keypair as a JSON array.
      In `transfer-from-sol.ts`, this is the source sender. In
      `transfer-from-evm.ts`, this is the Solana recipient and mint signer.
    * `RECIPIENT_KEYPAIR` is the Solana recipient wallet keypair as a JSON array for
      `transfer-from-sol.ts`.

    <Note>
      If your wallet exports a private key hash instead, you can use `bs58` to convert it:

      ```ts TypeScript theme={null}
      const bytes = bs58.decode("YOUR_PRIVATE_KEY_HASH");
      console.log(JSON.stringify(Array.from(bytes)));
      ```
    </Note>

    If you want to try the EVM to Solana transfer, also add:

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

    * `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use for the EVM
      side of the transfer.

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

    <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. Set up the configuration file

    This section covers the shared configuration file will be used by both the
    deposit and transfer scripts.

    ### 2.1. Create the configuration file

    ```shell theme={null}
    touch config.ts
    ```

    ### 2.2. Configure Solana settings and Gateway addresses

    Add the Solana-specific configuration, Gateway contract addresses, and account
    setup helper to your `config.ts` file. This includes the RPC endpoint, USDC
    address, domain ID, and the IDL definitions for interacting with Gateway Wallet
    and Gateway Minter programs on Solana Devnet.

    The local IDL fragments in this example are only the subset needed by the sample
    code. For canonical static instruction and account definitions, use the onchain
    IDLs linked from
    [Solana programs and interfaces](/gateway/references/solana-programs#account-structures).

    ```ts config.ts expandable theme={null}
    import { Keypair } from "@solana/web3.js";

    /* Solana Configuration */
    export const RPC_ENDPOINT = "https://api.devnet.solana.com";
    export const SOLANA_DOMAIN = 5;
    export const SOLANA_ZERO_ADDRESS = "11111111111111111111111111111111";

    /* Gateway Contract Addresses */
    export const GATEWAY_WALLET_ADDRESS =
      "GATEwdfmYNELfp5wDmmR6noSr2vHnAfBPMm2PvCzX5vu";
    export const GATEWAY_MINTER_ADDRESS =
      "GATEmKK2ECL1brEngQZWCgMWPbvrEYqsV6u29dAaHavr";
    export const USDC_ADDRESS = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";

    /* Account Setup Helper */
    export function createKeypairFromEnv(privateKey: string | undefined): Keypair {
      if (!privateKey) {
        throw new Error("Missing required Solana keypair environment variable");
      }

      const secretKey = JSON.parse(privateKey);
      return Keypair.fromSecretKey(Uint8Array.from(secretKey));
    }

    /* Sample-local Gateway Wallet IDL subset used by this runnable example. */
    export const gatewayWalletIdl = {
      address: GATEWAY_WALLET_ADDRESS,
      metadata: {
        name: "gatewayWallet",
        version: "0.1.0",
        spec: "0.1.0",
      },
      instructions: [
        {
          name: "deposit",
          discriminator: [22, 0],
          accounts: [
            { name: "payer", writable: true, signer: true },
            { name: "owner", signer: true },
            { name: "gatewayWallet" },
            { name: "ownerTokenAccount", writable: true },
            { name: "custodyTokenAccount", writable: true },
            { name: "deposit", writable: true },
            { name: "depositorDenylist" },
            { name: "tokenProgram" },
            { name: "systemProgram" },
            { name: "eventAuthority" },
            { name: "program" },
          ],
          args: [{ name: "amount", type: "u64" }],
        },
      ],
    };

    /* Sample-local Gateway Minter IDL subset used by this runnable example. */
    export const gatewayMinterIdl = {
      address: GATEWAY_MINTER_ADDRESS,
      metadata: { name: "gatewayMinter", version: "0.1.0", spec: "0.1.0" },
      instructions: [
        {
          name: "gatewayMint",
          discriminator: [12, 0],
          accounts: [
            { name: "payer", writable: true, signer: true },
            { name: "destinationCaller", signer: true },
            { name: "gatewayMinter" },
            { name: "systemProgram" },
            { name: "tokenProgram" },
            { name: "eventAuthority" },
            { name: "program" },
          ],
          args: [
            { name: "params", type: { defined: { name: "gatewayMintParams" } } },
          ],
        },
      ],
      types: [
        {
          name: "gatewayMintParams",
          type: {
            kind: "struct",
            fields: [
              { name: "attestation", type: "bytes" },
              { name: "signature", type: "bytes" },
            ],
          },
        },
      ],
    };
    ```

    ## Step 3. Deposit into a unified crosschain balance (self-managed)

    This section explains parts of the deposit script that allows you to deposit
    USDC into the Gateway Wallet contract on Solana Devnet. You can skip to the
    [full deposit script](#3-5-full-deposit-script-self-managed) if you prefer.

    ### 3.1. Create the script file

    ```shell theme={null}
    touch deposit.ts
    ```

    ### 3.2. Define constants and helpers

    You can adjust the `DEPOSIT_AMOUNT` to a different value. For now, it is set to
    10 USDC.

    ```ts deposit.ts theme={null}
    const DEPOSIT_AMOUNT = new BN(10000000); // 10 USDC (6 decimals)

    /* Helpers */
    function findPDAs(programId: PublicKey, usdcMint: PublicKey, owner: PublicKey) {
      return {
        wallet: PublicKey.findProgramAddressSync(
          [Buffer.from(utils.bytes.utf8.encode("gateway_wallet"))],
          programId,
        )[0],
        custody: PublicKey.findProgramAddressSync(
          [
            Buffer.from(utils.bytes.utf8.encode("gateway_wallet_custody")),
            usdcMint.toBuffer(),
          ],
          programId,
        )[0],
        deposit: PublicKey.findProgramAddressSync(
          [Buffer.from("gateway_deposit"), usdcMint.toBuffer(), owner.toBuffer()],
          programId,
        )[0],
        denylist: PublicKey.findProgramAddressSync(
          [Buffer.from("denylist"), owner.toBuffer()],
          programId,
        )[0],
      };
    }
    ```

    ### 3.3. Initialize connection, Anchor client, and validate balance

    Initialize the Solana connection and keypair, set up the Anchor client with
    Program Derived Addresses (PDAs) for interacting with the Gateway Wallet
    contract, then verify sufficient USDC balance before depositing.

    ```ts deposit.ts theme={null}
    const keypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
    const connection = new Connection(RPC_ENDPOINT, "confirmed");
    const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
    const usdcMint = new PublicKey(USDC_ADDRESS);

    console.log(`Using account: ${keypair.publicKey.toBase58()}`);

    // Check USDC balance
    const userAta = await getAssociatedTokenAddress(usdcMint, keypair.publicKey);
    const ataInfo = await getAccount(connection, userAta);
    const currentBalance = ataInfo.amount;

    console.log(
      `Current balance: ${Number(currentBalance.toString()) / 1_000_000} USDC`,
    );

    if (currentBalance < BigInt(DEPOSIT_AMOUNT.toString())) {
      throw new Error(
        `Insufficient USDC balance! Please top up at https://faucet.circle.com`,
      );
    }

    const pdas = findPDAs(programId, usdcMint, keypair.publicKey);

    const anchorWallet = new Wallet(keypair);
    const provider = new AnchorProvider(
      connection,
      anchorWallet,
      AnchorProvider.defaultOptions(),
    );
    setProvider(provider);
    const program = new Program(gatewayWalletIdl, provider);
    ```

    ### 3.4. Execute the deposit

    ```ts deposit.ts theme={null}
    const txHash = await program.methods
      .deposit(DEPOSIT_AMOUNT)
      .accountsPartial({
        payer: keypair.publicKey,
        owner: keypair.publicKey,
        gatewayWallet: pdas.wallet,
        ownerTokenAccount: userAta,
        custodyTokenAccount: pdas.custody,
        deposit: pdas.deposit,
        depositorDenylist: pdas.denylist,
        tokenProgram: TOKEN_PROGRAM_ID,
        systemProgram: SystemProgram.programId,
      })
      .signers([keypair])
      .rpc();

    console.log(`Done on Solana Devnet. Deposit tx: ${txHash}`);
    ```

    ### 3.5. Full deposit script (Self-managed)

    The complete deposit script initializes the Solana connection and Anchor client,
    validates the USDC balance, and deposits funds into the Gateway Wallet contract
    on Solana Devnet. The script includes inline comments to explain what each
    function does, making it easier to follow and modify if needed.

    ```ts deposit.ts expandable theme={null}
    import {
      Wallet,
      AnchorProvider,
      setProvider,
      Program,
      utils,
    } from "@coral-xyz/anchor";
    import { Connection, PublicKey, SystemProgram } from "@solana/web3.js";
    import {
      getAssociatedTokenAddress,
      getAccount,
      TOKEN_PROGRAM_ID,
    } from "@solana/spl-token";
    import BN from "bn.js";
    import {
      RPC_ENDPOINT,
      GATEWAY_WALLET_ADDRESS,
      USDC_ADDRESS,
      createKeypairFromEnv,
      gatewayWalletIdl,
    } from "./config.js";

    const DEPOSIT_AMOUNT = new BN(10000000); // 10 USDC (6 decimals)

    /* Helpers */
    function findPDAs(programId: PublicKey, usdcMint: PublicKey, owner: PublicKey) {
      return {
        wallet: PublicKey.findProgramAddressSync(
          [Buffer.from(utils.bytes.utf8.encode("gateway_wallet"))],
          programId,
        )[0],
        custody: PublicKey.findProgramAddressSync(
          [
            Buffer.from(utils.bytes.utf8.encode("gateway_wallet_custody")),
            usdcMint.toBuffer(),
          ],
          programId,
        )[0],
        deposit: PublicKey.findProgramAddressSync(
          [Buffer.from("gateway_deposit"), usdcMint.toBuffer(), owner.toBuffer()],
          programId,
        )[0],
        denylist: PublicKey.findProgramAddressSync(
          [Buffer.from("denylist"), owner.toBuffer()],
          programId,
        )[0],
      };
    }

    /* Main logic */
    async function main() {
      if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
        throw new Error("SOLANA_PRIVATE_KEYPAIR not set in environment");
      }

      const keypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
      const connection = new Connection(RPC_ENDPOINT, "confirmed");
      const programId = new PublicKey(GATEWAY_WALLET_ADDRESS);
      const usdcMint = new PublicKey(USDC_ADDRESS);

      console.log(`Using account: ${keypair.publicKey.toBase58()}`);

      console.log(`\n=== Processing Solana Devnet ===`);

      // Check USDC balance
      const userAta = await getAssociatedTokenAddress(usdcMint, keypair.publicKey);
      const ataInfo = await getAccount(connection, userAta);
      const currentBalance = ataInfo.amount;

      console.log(
        `Current balance: ${Number(currentBalance.toString()) / 1_000_000} USDC`,
      );

      if (currentBalance < BigInt(DEPOSIT_AMOUNT.toString())) {
        throw new Error(
          `Insufficient USDC balance! Please top up at https://faucet.circle.com`,
        );
      }

      console.log(
        `Depositing ${Number(DEPOSIT_AMOUNT.toString()) / 1_000_000} USDC to Gateway Wallet`,
      );

      // Set up Anchor client
      const pdas = findPDAs(programId, usdcMint, keypair.publicKey);

      const anchorWallet = new Wallet(keypair);
      const provider = new AnchorProvider(
        connection,
        anchorWallet,
        AnchorProvider.defaultOptions(),
      );
      setProvider(provider);
      const program = new Program(gatewayWalletIdl, provider);

      // Execute deposit
      const txHash = await program.methods
        .deposit(DEPOSIT_AMOUNT)
        .accountsPartial({
          payer: keypair.publicKey,
          owner: keypair.publicKey,
          gatewayWallet: pdas.wallet,
          ownerTokenAccount: userAta,
          custodyTokenAccount: pdas.custody,
          deposit: pdas.deposit,
          depositorDenylist: pdas.denylist,
          tokenProgram: TOKEN_PROGRAM_ID,
          systemProgram: SystemProgram.programId,
        })
        .signers([keypair])
        .rpc();

      console.log(`Done on Solana Devnet. Deposit tx: ${txHash}`);
    }

    main().catch((error) => {
      console.error("\nError:", error);
      process.exit(1);
    });
    ```

    ### 3.6. Run the script to create a crosschain balance

    Run the deposit script to deposit USDC into your Gateway balance on Solana
    Devnet.

    ```shell theme={null}
    npm run deposit
    ```

    Wait for the required number of block confirmations. Once the deposit
    transaction is final, your Gateway balance on Solana Devnet will be updated.
    Solana Devnet transactions typically reach finality in seconds.

    ### 3.7. Check the balances on the Gateway Wallet

    Create a new file called `balances.ts`, and add the following code. This script
    retrieves the USDC balances available from your Gateway Wallet on Solana Devnet.

    ```ts balances.ts expandable theme={null}
    import { Keypair } from "@solana/web3.js";

    /* Constants */
    const SOLANA_DOMAIN = 5;

    /* Helpers */
    function createKeypairFromEnv(privateKey: string | undefined): Keypair {
      if (!privateKey) {
        throw new Error("Missing required Solana keypair environment variable");
      }

      const secretKey = JSON.parse(privateKey);
      return Keypair.fromSecretKey(Uint8Array.from(secretKey));
    }

    async function main() {
      if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
        throw new Error("SOLANA_PRIVATE_KEYPAIR not set in environment");
      }

      const keypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
      const depositor = keypair.publicKey.toBase58();

      console.log(`Depositor address: ${depositor}\n`);

      const body = {
        token: "USDC",
        sources: [{ domain: SOLANA_DOMAIN, depositor }],
      };

      const res = await fetch(
        "https://gateway-api-testnet.circle.com/v1/balances",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        },
      );

      const result = await res.json();

      for (const balance of result.balances) {
        const amount = parseFloat(balance.balance);
        console.log(`Solana Devnet: ${amount.toFixed(6)} USDC`);
      }
    }

    main().catch((error) => {
      console.error("\nError:", error);
      process.exit(1);
    });
    ```

    You can run it to verify your balance on Gateway.

    ```shell theme={null}
    npm run balances
    ```

    <Tabs>
      <Tab title="Transfer from Solana">
        ## Step 4. Transfer USDC from Solana to Solana

        This step transfers USDC from your Solana Devnet Gateway balance to a recipient
        on Solana Devnet. Both paths create and sign the Solana burn intent first.
        Direct mint then retrieves the Gateway attestation and calls `gatewayMint(...)`
        on Solana Devnet from your self-managed Solana wallet, while Forwarding Service
        lets Circle complete the Solana mint for you.

        <Tabs>
          <Tab title="Direct Mint">
            ### 4.1. Create the script file

            You can skip to the
            [full transfer script](#4-9-full-solana-transfer-script-self-managed) if you
            prefer.

            ```shell theme={null}
            touch transfer-from-sol.ts
            ```

            ### 4.2. Define constants and types

            You can set the amount to be transferred from your Gateway balance by changing
            the `TRANSFER_AMOUNT`. For now, it is set to 1 USDC.

            ```ts transfer-from-sol.ts expandable theme={null}
            const TRANSFER_AMOUNT = 1; // 1 USDC
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;

            const TRANSFER_SPEC_MAGIC = 0xca85def7;
            const BURN_INTENT_MAGIC = 0x070afbc2;

            /* Type definitions */
            // Custom layout for Solana PublicKey (32 bytes)
            class PublicKeyLayout extends Layout<PublicKey> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0): PublicKey {
                return new PublicKey(b.subarray(offset, offset + 32));
              }
              encode(src: PublicKey, b: Buffer, offset = 0): number {
                const pubkeyBuffer = src.toBuffer();
                pubkeyBuffer.copy(b, offset);
                return 32;
              }
            }

            const publicKey = (property: string) => new PublicKeyLayout(property);

            // Custom layout for 256-bit unsigned integers
            class UInt256BE extends Layout<bigint> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0) {
                const buffer = b.subarray(offset, offset + 32);
                return buffer.readBigUInt64BE(24);
              }
              encode(src: bigint, b: Buffer, offset = 0) {
                const buffer = Buffer.alloc(32);
                buffer.writeBigUInt64BE(BigInt(src), 24);
                buffer.copy(b, offset);
                return 32;
              }
            }

            const uint256be = (property: string) => new UInt256BE(property);

            // Type 'as any' used due to @solana/buffer-layout's incomplete TypeScript definitions (archived Jan 2025)
            const BurnIntentLayout = struct([
              u32be("magic"),
              uint256be("maxBlockHeight"),
              uint256be("maxFee"),
              u32be("transferSpecLength"),
              struct(
                [
                  u32be("magic"),
                  u32be("version"),
                  u32be("sourceDomain"),
                  u32be("destinationDomain"),
                  publicKey("sourceContract"),
                  publicKey("destinationContract"),
                  publicKey("sourceToken"),
                  publicKey("destinationToken"),
                  publicKey("sourceDepositor"),
                  publicKey("destinationRecipient"),
                  publicKey("sourceSigner"),
                  publicKey("destinationCaller"),
                  uint256be("value"),
                  blob(32, "salt"),
                  u32be("hookDataLength"),
                  blob(offset(u32be(), -4), "hookData"),
                ] as any,
                "spec",
              ),
            ] as any);

            const MintAttestationElementLayout = struct([
              publicKey("destinationToken"),
              publicKey("destinationRecipient"),
              nu64be("value"),
              blob(32, "transferSpecHash"),
              u32be("hookDataLength"),
              blob(offset(u32be(), -4), "hookData"),
            ] as any);

            const MintAttestationSetLayout = struct([
              u32be("magic"),
              u32be("version"),
              u32be("destinationDomain"),
              publicKey("destinationContract"),
              publicKey("destinationCaller"),
              nu64be("maxBlockHeight"),
              u32be("numAttestations"),
              seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
            ] as any);
            ```

            ### 4.3. Add helper functions

            ```ts transfer-from-sol.ts expandable theme={null}
            // Construct burn intent for a given source
            function createBurnIntent(params: {
              sourceDepositor: string;
              destinationRecipient: string;
              sourceSigner: string;
            }) {
              const { sourceDepositor, destinationRecipient, sourceSigner } = params;

              return {
                maxBlockHeight: MAX_UINT64,
                maxFee: MAX_FEE,
                spec: {
                  version: 1,
                  sourceDomain: SOLANA_DOMAIN,
                  destinationDomain: SOLANA_DOMAIN,
                  sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
                  destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
                  sourceToken: addressToBytes32(USDC_ADDRESS),
                  destinationToken: addressToBytes32(USDC_ADDRESS),
                  sourceDepositor: addressToBytes32(sourceDepositor),
                  destinationRecipient: addressToBytes32(destinationRecipient),
                  sourceSigner: addressToBytes32(sourceSigner),
                  destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
                  value: TRANSFER_VALUE,
                  salt: "0x" + randomBytes(32).toString("hex"),
                  hookData: "0x",
                },
              };
            }

            // Encode burn intent as binary layout for signing
            function encodeBurnIntent(bi: any): Buffer {
              const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
              const prepared = {
                magic: BURN_INTENT_MAGIC,
                maxBlockHeight: bi.maxBlockHeight,
                maxFee: bi.maxFee,
                transferSpecLength: 340 + hookData.length,
                spec: {
                  magic: TRANSFER_SPEC_MAGIC,
                  version: bi.spec.version,
                  sourceDomain: bi.spec.sourceDomain,
                  destinationDomain: bi.spec.destinationDomain,
                  sourceContract: hexToPublicKey(bi.spec.sourceContract),
                  destinationContract: hexToPublicKey(bi.spec.destinationContract),
                  sourceToken: hexToPublicKey(bi.spec.sourceToken),
                  destinationToken: hexToPublicKey(bi.spec.destinationToken),
                  sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
                  destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
                  sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
                  destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
                  value: bi.spec.value,
                  salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
                  hookDataLength: hookData.length,
                  hookData,
                },
              };
              const buffer = Buffer.alloc(72 + 340 + hookData.length);
              const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
              return buffer.subarray(0, bytesWritten);
            }

            // Sign burn intent with Ed25519 keypair
            function signBurnIntent(keypair: Keypair, payload: any): string {
              const encoded = encodeBurnIntent(payload);
              const prefixed = Buffer.concat([
                Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
                encoded,
              ]);
              const privateKey = crypto.createPrivateKey({
                key: Buffer.concat([
                  Buffer.from("302e020100300506032b657004220420", "hex"),
                  Buffer.from(keypair.secretKey.slice(0, 32)),
                ]),
                format: "der",
                type: "pkcs8",
              });
              return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
            }

            // Convert Solana address to 32-byte hex string
            function addressToBytes32(address: string): string {
              const decoded = Buffer.from(bs58.decode(address));
              return `0x${decoded.toString("hex")}`;
            }

            // Convert hex string to Solana PublicKey
            function hexToPublicKey(hex: string): PublicKey {
              return new PublicKey(Buffer.from(hex.slice(2), "hex"));
            }

            // Decode attestation set from Gateway API response
            function decodeAttestationSet(attestation: string) {
              const buffer = Buffer.from(attestation.slice(2), "hex");
              return MintAttestationSetLayout.decode(buffer) as {
                attestations: Array<{
                  destinationToken: PublicKey;
                  destinationRecipient: PublicKey;
                  transferSpecHash: Uint8Array;
                }>;
              };
            }

            // Find PDA for token custody account
            function findCustodyPda(
              mint: PublicKey,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("gateway_minter_custody"), mint.toBuffer()],
                minterProgramId,
              )[0];
            }

            // Find PDA for transfer spec hash tracking
            function findTransferSpecHashPda(
              transferSpecHash: Uint8Array | Buffer,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
                minterProgramId,
              )[0];
            }
            ```

            ### 4.4. Initialize connection and create recipient ATA

            Initialize the Solana connection and keypairs, then create the recipient's
            Associated Token Account (ATA) for receiving USDC on the destination chain.

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            ```ts transfer-from-sol.ts theme={null}
            const senderKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
            const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
            const connection = new Connection(RPC_ENDPOINT, "confirmed");
            const usdcMint = new PublicKey(USDC_ADDRESS);

            console.log(`Using account: ${senderKeypair.publicKey.toBase58()}`);
            console.log(`Transferring balances from: Solana Devnet`);

            // Create recipient's Associated Token Account
            const recipientAta = getAssociatedTokenAddressSync(
              usdcMint,
              recipientKeypair.publicKey,
            );

            const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
              senderKeypair.publicKey,
              recipientAta,
              recipientKeypair.publicKey,
              usdcMint,
            );
            const tx = new Transaction().add(createAtaIx);
            await sendAndConfirmTransaction(connection, tx, [senderKeypair]);
            ```

            ### 4.5. Create and sign burn intent

            ```ts transfer-from-sol.ts theme={null}
            const burnIntent = createBurnIntent({
              sourceDepositor: senderKeypair.publicKey.toBase58(),
              destinationRecipient: recipientAta.toBase58(),
              sourceSigner: senderKeypair.publicKey.toBase58(),
            });

            const burnIntentSignature = signBurnIntent(senderKeypair, burnIntent);
            const request = [{ burnIntent, signature: burnIntentSignature }];
            console.log("Signed burn intent.");
            ```

            ### 4.6. Request attestation from Gateway API

            ```ts transfer-from-sol.ts theme={null}
            const response = await fetch(
              "https://gateway-api-testnet.circle.com/v1/transfer",
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify(request, (_key, value) =>
                  typeof value === "bigint" ? value.toString() : value,
                ),
              },
            );

            const json = await response.json();
            if (json.success === false) {
              throw new Error(`Gateway API error: ${json.message}`);
            }
            console.log("Gateway API response:", JSON.stringify(json, null, 2));

            const { attestation, signature: mintSignature } = json;
            const decoded = decodeAttestationSet(attestation);
            ```

            ### 4.7. Set up minter client

            <Info>
              The ordered remaining-account list and PDA derivations are documented in
              [Solana Programs and
              Interfaces](/gateway/references/solana-programs#gatewaymint). For static
              instruction definitions, use the onchain IDLs linked from that page.
            </Info>

            ```ts transfer-from-sol.ts theme={null}
            const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
            const anchorWallet = new Wallet(senderKeypair);
            const provider = new AnchorProvider(
              connection,
              anchorWallet,
              AnchorProvider.defaultOptions(),
            );
            setProvider(provider);
            const minterProgram = new Program(gatewayMinterIdl, provider);

            const [minterPda] = PublicKey.findProgramAddressSync(
              [Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
              minterProgramId,
            );
            ```

            ### 4.8. Mint on Solana

            ```ts transfer-from-sol.ts expandable theme={null}
            const remainingAccounts = decoded.attestations.flatMap((e) => [
              {
                pubkey: findCustodyPda(e.destinationToken, minterProgramId),
                isWritable: true,
                isSigner: false,
              },
              { pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
              {
                pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
                isWritable: true,
                isSigner: false,
              },
            ]);

            const attestationBytes = Buffer.from(attestation.slice(2), "hex");
            const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");

            console.log("Minting funds on Solana Devnet...");
            const mintTx = await minterProgram.methods
              .gatewayMint({ attestation: attestationBytes, signature: signatureBytes })
              .accountsPartial({
                gatewayMinter: minterPda,
                destinationCaller: senderKeypair.publicKey,
                payer: senderKeypair.publicKey,
                systemProgram: SystemProgram.programId,
                tokenProgram: TOKEN_PROGRAM_ID,
              })
              .remainingAccounts(remainingAccounts)
              .signers([senderKeypair])
              .rpc();

            // [6] Wait for confirmation
            const latest = await connection.getLatestBlockhash();
            await connection.confirmTransaction(
              {
                signature: mintTx,
                blockhash: latest.blockhash,
                lastValidBlockHeight: latest.lastValidBlockHeight,
              },
              "confirmed",
            );

            console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
            console.log(`Mint transaction hash (Solana Devnet):`, mintTx);
            ```

            ### 4.9. Full Solana transfer script (Self-managed)

            The complete transfer script creates and signs a burn intent on Solana Devnet,
            submits it to the Gateway API for attestation, and mints USDC on Solana Devnet
            for the recipient. The script includes inline comments to explain what each
            function does, making it easier to follow and modify if needed.

            ```ts transfer-from-sol.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import * as crypto from "crypto";
            import {
              Wallet,
              AnchorProvider,
              setProvider,
              Program,
              utils,
            } from "@coral-xyz/anchor";
            import {
              Connection,
              Keypair,
              PublicKey,
              SystemProgram,
              Transaction,
              sendAndConfirmTransaction,
            } from "@solana/web3.js";
            import {
              TOKEN_PROGRAM_ID,
              getAssociatedTokenAddressSync,
              createAssociatedTokenAccountIdempotentInstruction,
            } from "@solana/spl-token";
            import {
              u32be,
              nu64be,
              struct,
              seq,
              blob,
              offset,
              Layout,
            } from "@solana/buffer-layout";
            import bs58 from "bs58";
            import {
              RPC_ENDPOINT,
              GATEWAY_WALLET_ADDRESS,
              GATEWAY_MINTER_ADDRESS,
              USDC_ADDRESS,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              createKeypairFromEnv,
              gatewayMinterIdl,
            } from "./config.js";

            const TRANSFER_AMOUNT = 1; // 1 USDC
            const TRANSFER_VALUE = BigInt(Math.floor(TRANSFER_AMOUNT * 1e6));
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;

            const TRANSFER_SPEC_MAGIC = 0xca85def7;
            const BURN_INTENT_MAGIC = 0x070afbc2;

            /* Type definitions */
            // Custom layout for Solana PublicKey (32 bytes)
            class PublicKeyLayout extends Layout<PublicKey> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0): PublicKey {
                return new PublicKey(b.subarray(offset, offset + 32));
              }
              encode(src: PublicKey, b: Buffer, offset = 0): number {
                const pubkeyBuffer = src.toBuffer();
                pubkeyBuffer.copy(b, offset);
                return 32;
              }
            }

            const publicKey = (property: string) => new PublicKeyLayout(property);

            // Custom layout for 256-bit unsigned integers
            class UInt256BE extends Layout<bigint> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0) {
                const buffer = b.subarray(offset, offset + 32);
                return buffer.readBigUInt64BE(24);
              }
              encode(src: bigint, b: Buffer, offset = 0) {
                const buffer = Buffer.alloc(32);
                buffer.writeBigUInt64BE(BigInt(src), 24);
                buffer.copy(b, offset);
                return 32;
              }
            }

            const uint256be = (property: string) => new UInt256BE(property);

            // Type 'as any' used due to @solana/buffer-layout's incomplete TypeScript definitions (archived Jan 2025)
            const BurnIntentLayout = struct([
              u32be("magic"),
              uint256be("maxBlockHeight"),
              uint256be("maxFee"),
              u32be("transferSpecLength"),
              struct(
                [
                  u32be("magic"),
                  u32be("version"),
                  u32be("sourceDomain"),
                  u32be("destinationDomain"),
                  publicKey("sourceContract"),
                  publicKey("destinationContract"),
                  publicKey("sourceToken"),
                  publicKey("destinationToken"),
                  publicKey("sourceDepositor"),
                  publicKey("destinationRecipient"),
                  publicKey("sourceSigner"),
                  publicKey("destinationCaller"),
                  uint256be("value"),
                  blob(32, "salt"),
                  u32be("hookDataLength"),
                  blob(offset(u32be(), -4), "hookData"),
                ] as any,
                "spec",
              ),
            ] as any);

            const MintAttestationElementLayout = struct([
              publicKey("destinationToken"),
              publicKey("destinationRecipient"),
              nu64be("value"),
              blob(32, "transferSpecHash"),
              u32be("hookDataLength"),
              blob(offset(u32be(), -4), "hookData"),
            ] as any);

            const MintAttestationSetLayout = struct([
              u32be("magic"),
              u32be("version"),
              u32be("destinationDomain"),
              publicKey("destinationContract"),
              publicKey("destinationCaller"),
              nu64be("maxBlockHeight"),
              u32be("numAttestations"),
              seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
            ] as any);

            /* Helpers */
            // Construct burn intent for a given source
            function createBurnIntent(params: {
              sourceDepositor: string;
              destinationRecipient: string;
              sourceSigner: string;
            }) {
              const { sourceDepositor, destinationRecipient, sourceSigner } = params;

              return {
                maxBlockHeight: MAX_UINT64,
                maxFee: MAX_FEE,
                spec: {
                  version: 1,
                  sourceDomain: SOLANA_DOMAIN,
                  destinationDomain: SOLANA_DOMAIN,
                  sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
                  destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
                  sourceToken: addressToBytes32(USDC_ADDRESS),
                  destinationToken: addressToBytes32(USDC_ADDRESS),
                  sourceDepositor: addressToBytes32(sourceDepositor),
                  destinationRecipient: addressToBytes32(destinationRecipient),
                  sourceSigner: addressToBytes32(sourceSigner),
                  destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
                  value: TRANSFER_VALUE,
                  salt: "0x" + randomBytes(32).toString("hex"),
                  hookData: "0x",
                },
              };
            }

            // Encode burn intent as binary layout for signing
            function encodeBurnIntent(bi: any): Buffer {
              const hookData = Buffer.from((bi.spec.hookData || "0x").slice(2), "hex");
              const prepared = {
                magic: BURN_INTENT_MAGIC,
                maxBlockHeight: bi.maxBlockHeight,
                maxFee: bi.maxFee,
                transferSpecLength: 340 + hookData.length,
                spec: {
                  magic: TRANSFER_SPEC_MAGIC,
                  version: bi.spec.version,
                  sourceDomain: bi.spec.sourceDomain,
                  destinationDomain: bi.spec.destinationDomain,
                  sourceContract: hexToPublicKey(bi.spec.sourceContract),
                  destinationContract: hexToPublicKey(bi.spec.destinationContract),
                  sourceToken: hexToPublicKey(bi.spec.sourceToken),
                  destinationToken: hexToPublicKey(bi.spec.destinationToken),
                  sourceDepositor: hexToPublicKey(bi.spec.sourceDepositor),
                  destinationRecipient: hexToPublicKey(bi.spec.destinationRecipient),
                  sourceSigner: hexToPublicKey(bi.spec.sourceSigner),
                  destinationCaller: hexToPublicKey(bi.spec.destinationCaller),
                  value: bi.spec.value,
                  salt: Buffer.from(bi.spec.salt.slice(2), "hex"),
                  hookDataLength: hookData.length,
                  hookData,
                },
              };
              const buffer = Buffer.alloc(72 + 340 + hookData.length);
              const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
              return buffer.subarray(0, bytesWritten);
            }

            // Sign burn intent with Ed25519 keypair
            function signBurnIntent(keypair: Keypair, payload: any): string {
              const encoded = encodeBurnIntent(payload);
              const prefixed = Buffer.concat([
                Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
                encoded,
              ]);
              const privateKey = crypto.createPrivateKey({
                key: Buffer.concat([
                  Buffer.from("302e020100300506032b657004220420", "hex"),
                  Buffer.from(keypair.secretKey.slice(0, 32)),
                ]),
                format: "der",
                type: "pkcs8",
              });
              return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
            }

            // Convert Solana address to 32-byte hex string
            function addressToBytes32(address: string): string {
              const decoded = Buffer.from(bs58.decode(address));
              return `0x${decoded.toString("hex")}`;
            }

            // Convert hex string to Solana PublicKey
            function hexToPublicKey(hex: string): PublicKey {
              return new PublicKey(Buffer.from(hex.slice(2), "hex"));
            }

            // Decode attestation set from Gateway API response
            function decodeAttestationSet(attestation: string) {
              const buffer = Buffer.from(attestation.slice(2), "hex");
              return MintAttestationSetLayout.decode(buffer) as {
                attestations: Array<{
                  destinationToken: PublicKey;
                  destinationRecipient: PublicKey;
                  transferSpecHash: Uint8Array;
                }>;
              };
            }

            // Find PDA for token custody account
            function findCustodyPda(
              mint: PublicKey,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("gateway_minter_custody"), mint.toBuffer()],
                minterProgramId,
              )[0];
            }

            // Find PDA for transfer spec hash tracking
            function findTransferSpecHashPda(
              transferSpecHash: Uint8Array | Buffer,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
                minterProgramId,
              )[0];
            }

            /* Main logic */
            async function main() {
              if (!process.env.SOLANA_PRIVATE_KEYPAIR || !process.env.RECIPIENT_KEYPAIR) {
                throw new Error("SOLANA_PRIVATE_KEYPAIR and RECIPIENT_KEYPAIR must be set");
              }

              const senderKeypair = createKeypairFromEnv(
                process.env.SOLANA_PRIVATE_KEYPAIR,
              );
              const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
              const connection = new Connection(RPC_ENDPOINT, "confirmed");
              const usdcMint = new PublicKey(USDC_ADDRESS);

              console.log(`Using account: ${senderKeypair.publicKey.toBase58()}`);
              console.log(`Transferring balances from: Solana Devnet`);

              // [1] Create recipient's Associated Token Account
              const recipientAta = getAssociatedTokenAddressSync(
                usdcMint,
                recipientKeypair.publicKey,
              );

              const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
                senderKeypair.publicKey,
                recipientAta,
                recipientKeypair.publicKey,
                usdcMint,
              );
              const tx = new Transaction().add(createAtaIx);
              await sendAndConfirmTransaction(connection, tx, [senderKeypair]);

              // [2] Create and sign burn intent
              console.log(`Creating burn intent from Solana Devnet → Solana Devnet...`);
              const burnIntent = createBurnIntent({
                sourceDepositor: senderKeypair.publicKey.toBase58(),
                destinationRecipient: recipientAta.toBase58(),
                sourceSigner: senderKeypair.publicKey.toBase58(),
              });

              const burnIntentSignature = signBurnIntent(senderKeypair, burnIntent);
              const request = [{ burnIntent, signature: burnIntentSignature }];
              console.log("Signed burn intent.");

              // [3] Request attestation from Gateway API
              const response = await fetch(
                "https://gateway-api-testnet.circle.com/v1/transfer",
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: JSON.stringify(request, (_key, value) =>
                    typeof value === "bigint" ? value.toString() : value,
                  ),
                },
              );

              const json = await response.json();
              if (json.success === false) {
                throw new Error(`Gateway API error: ${json.message}`);
              }
              console.log("Gateway API response:", JSON.stringify(json, null, 2));

              const { attestation, signature: mintSignature } = json;
              const decoded = decodeAttestationSet(attestation);

              // [4] Set up the minter client
              const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
              const anchorWallet = new Wallet(senderKeypair);
              const provider = new AnchorProvider(
                connection,
                anchorWallet,
                AnchorProvider.defaultOptions(),
              );
              setProvider(provider);
              const minterProgram = new Program(gatewayMinterIdl, provider);

              const [minterPda] = PublicKey.findProgramAddressSync(
                [Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
                minterProgramId,
              );

              // [5] Mint on Solana
              const remainingAccounts = decoded.attestations.flatMap((e) => [
                {
                  pubkey: findCustodyPda(e.destinationToken, minterProgramId),
                  isWritable: true,
                  isSigner: false,
                },
                { pubkey: e.destinationRecipient, isWritable: true, isSigner: false },
                {
                  pubkey: findTransferSpecHashPda(e.transferSpecHash, minterProgramId),
                  isWritable: true,
                  isSigner: false,
                },
              ]);

              const attestationBytes = Buffer.from(attestation.slice(2), "hex");
              const signatureBytes = Buffer.from(mintSignature.slice(2), "hex");

              console.log("Minting funds on Solana Devnet...");
              const mintTx = await minterProgram.methods
                .gatewayMint({ attestation: attestationBytes, signature: signatureBytes })
                .accountsPartial({
                  gatewayMinter: minterPda,
                  destinationCaller: senderKeypair.publicKey,
                  payer: senderKeypair.publicKey,
                  systemProgram: SystemProgram.programId,
                  tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(remainingAccounts)
                .signers([senderKeypair])
                .rpc();

              // [6] Wait for confirmation
              const latest = await connection.getLatestBlockhash();
              await connection.confirmTransaction(
                {
                  signature: mintTx,
                  blockhash: latest.blockhash,
                  lastValidBlockHeight: latest.lastValidBlockHeight,
                },
                "confirmed",
              );

              console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
              console.log(`Mint transaction hash (Solana Devnet):`, mintTx);
            }

            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.10. Run the Solana direct-mint script

            Run the script to burn from your Solana Devnet Gateway balance and mint to the
            recipient ATA on Solana Devnet.

            Confirm these values before running:

            * `SOLANA_PRIVATE_KEYPAIR` controls the source Solana Devnet wallet
            * `RECIPIENT_KEYPAIR` controls the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Solana Devnet
            * the source Solana wallet can submit the ATA creation and mint transactions on
              Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-sol
            ```

            When the transfer succeeds, the script logs the minted amount and Solana Devnet
            mint transaction hash.
          </Tab>

          <Tab title="Forwarding Service">
            ### 4.1. Create the Solana forwarding script

            You can skip to the
            [full transfer script](#4-7-full-solana-forwarding-script-self-managed) if you
            prefer.

            ```shell theme={null}
            touch transfer-from-sol.ts
            ```

            ### 4.2. Define constants and response types

            This forwarding flow signs the burn intent on Solana Devnet, submits it with
            forwarding enabled, and lets Circle complete the Solana Devnet mint.

            ```ts transfer-from-sol.ts expandable theme={null}
            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            type TransferSpec = ReturnType<typeof createTransferSpec>;

            type BurnIntent = {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            };

            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
            const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;
            ```

            ### 4.3. Add Solana burn intent helpers

            The forwarding flow first estimates the burn-intent limits for the Solana Devnet
            to Solana Devnet route, then signs the estimate-provided burn intent with the
            source Solana keypair.

            ```ts transfer-from-sol.ts expandable theme={null}
            // Construct transfer spec for Solana to Solana forwarding transfer
            function createTransferSpec(params: {
              sourceDepositor: string;
              destinationRecipient: string;
              sourceSigner: string;
            }) {
              const { sourceDepositor, destinationRecipient, sourceSigner } = params;

              return {
                version: 1,
                sourceDomain: SOLANA_DOMAIN,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
                destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: addressToBytes32(USDC_ADDRESS),
                destinationToken: addressToBytes32(USDC_ADDRESS),
                sourceDepositor: addressToBytes32(sourceDepositor),
                destinationRecipient: addressToBytes32(destinationRecipient),
                sourceSigner: addressToBytes32(sourceSigner),
                destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: `0x${randomBytes(32).toString("hex")}`,
                hookData: "0x",
              };
            }

            // Construct burn intent with estimate-provided limits
            function createBurnIntent(params: {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            }): BurnIntent {
              return params;
            }
            ```

            ### 4.4. Estimate forwarding fees

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            Derive the recipient ATA, build the transfer spec, then call the Gateway
            estimate endpoint with `enableForwarder=true`. The response returns the `maxFee`
            and `maxBlockHeight` values to include in the signed burn intent.

            ```ts transfer-from-sol.ts theme={null}
            const senderKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
            const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
            const recipientAta = getAssociatedTokenAddressSync(
              new PublicKey(USDC_ADDRESS),
              recipientKeypair.publicKey,
            );

            const spec = createTransferSpec({
              sourceDepositor: senderKeypair.publicKey.toBase58(),
              destinationRecipient: recipientAta.toBase58(),
              sourceSigner: senderKeypair.publicKey.toBase58(),
            });

            const estimateResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyBigInts([{ spec }]),
              },
            );
            ```

            ### 4.5. Sign and submit the forwarding burn intent

            Create the burn intent with the estimate-provided limits, sign it with the
            source Solana keypair, then submit it to the Gateway transfer endpoint with
            `enableForwarder=true`.

            ```ts transfer-from-sol.ts theme={null}
            const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
            const signature = signBurnIntent(senderKeypair, burnIntent);

            const transferResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyBigInts([{ burnIntent, signature }]),
              },
            );
            ```

            ### 4.6. Poll for forwarding completion

            The Forwarding Service completes the Solana Devnet mint for you. Poll the
            transfer status endpoint until the transfer is confirmed, finalized, failed, or
            expired.

            ```ts transfer-from-sol.ts theme={null}
            const pollResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
            );
            ```

            ### 4.7. Full Solana forwarding script (Self-managed)

            The complete script estimates forwarding fees, signs the burn intent on Solana
            Devnet, submits it with forwarding enabled, and polls until Circle completes the
            Solana Devnet mint.

            ```ts transfer-from-sol.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import * as crypto from "crypto";
            import { getAssociatedTokenAddressSync } from "@solana/spl-token";
            import { PublicKey, Keypair } from "@solana/web3.js";
            import { blob, Layout, offset, struct, u32be } from "@solana/buffer-layout";
            import bs58 from "bs58";
            import { formatUnits } from "viem";
            import {
              GATEWAY_MINTER_ADDRESS,
              GATEWAY_WALLET_ADDRESS,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              USDC_ADDRESS,
              createKeypairFromEnv,
            } from "./config.js";

            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            type TransferSpec = ReturnType<typeof createTransferSpec>;

            type BurnIntent = {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            };

            /* Constants */
            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
            const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;

            const TRANSFER_SPEC_MAGIC = 0xca85def7;
            const BURN_INTENT_MAGIC = 0x070afbc2;

            /* Type definitions */
            class PublicKeyLayout extends Layout<PublicKey> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0): PublicKey {
                return new PublicKey(b.subarray(offset, offset + 32));
              }
              encode(src: PublicKey, b: Buffer, offset = 0): number {
                src.toBuffer().copy(b, offset);
                return 32;
              }
            }

            const publicKey = (property: string) => new PublicKeyLayout(property);

            class UInt256BE extends Layout<bigint> {
              constructor(property: string) {
                super(32, property);
              }
              decode(b: Buffer, offset = 0) {
                return b.subarray(offset, offset + 32).readBigUInt64BE(24);
              }
              encode(src: bigint, b: Buffer, offset = 0) {
                const buffer = Buffer.alloc(32);
                buffer.writeBigUInt64BE(BigInt(src), 24);
                buffer.copy(b, offset);
                return 32;
              }
            }

            const uint256be = (property: string) => new UInt256BE(property);

            const BurnIntentLayout = struct([
              u32be("magic"),
              uint256be("maxBlockHeight"),
              uint256be("maxFee"),
              u32be("transferSpecLength"),
              struct(
                [
                  u32be("magic"),
                  u32be("version"),
                  u32be("sourceDomain"),
                  u32be("destinationDomain"),
                  publicKey("sourceContract"),
                  publicKey("destinationContract"),
                  publicKey("sourceToken"),
                  publicKey("destinationToken"),
                  publicKey("sourceDepositor"),
                  publicKey("destinationRecipient"),
                  publicKey("sourceSigner"),
                  publicKey("destinationCaller"),
                  uint256be("value"),
                  blob(32, "salt"),
                  u32be("hookDataLength"),
                  blob(offset(u32be(), -4), "hookData"),
                ] as any,
                "spec",
              ),
            ] as any);

            /* Helpers */
            // Construct transfer spec for Solana to Solana forwarding transfer
            function createTransferSpec(params: {
              sourceDepositor: string;
              destinationRecipient: string;
              sourceSigner: string;
            }) {
              const { sourceDepositor, destinationRecipient, sourceSigner } = params;

              return {
                version: 1,
                sourceDomain: SOLANA_DOMAIN,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: addressToBytes32(GATEWAY_WALLET_ADDRESS),
                destinationContract: addressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: addressToBytes32(USDC_ADDRESS),
                destinationToken: addressToBytes32(USDC_ADDRESS),
                sourceDepositor: addressToBytes32(sourceDepositor),
                destinationRecipient: addressToBytes32(destinationRecipient),
                sourceSigner: addressToBytes32(sourceSigner),
                destinationCaller: addressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: `0x${randomBytes(32).toString("hex")}`,
                hookData: "0x",
              };
            }

            // Construct burn intent with estimate-provided limits
            function createBurnIntent(params: {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: TransferSpec;
            }): BurnIntent {
              return params;
            }

            // Encode burn intent as binary layout for signing
            function encodeBurnIntent(burnIntent: BurnIntent): Buffer {
              const hookData = Buffer.from(burnIntent.spec.hookData.slice(2), "hex");
              const prepared = {
                magic: BURN_INTENT_MAGIC,
                maxBlockHeight: burnIntent.maxBlockHeight,
                maxFee: burnIntent.maxFee,
                transferSpecLength: 340 + hookData.length,
                spec: {
                  magic: TRANSFER_SPEC_MAGIC,
                  version: burnIntent.spec.version,
                  sourceDomain: burnIntent.spec.sourceDomain,
                  destinationDomain: burnIntent.spec.destinationDomain,
                  sourceContract: hexToPublicKey(burnIntent.spec.sourceContract),
                  destinationContract: hexToPublicKey(burnIntent.spec.destinationContract),
                  sourceToken: hexToPublicKey(burnIntent.spec.sourceToken),
                  destinationToken: hexToPublicKey(burnIntent.spec.destinationToken),
                  sourceDepositor: hexToPublicKey(burnIntent.spec.sourceDepositor),
                  destinationRecipient: hexToPublicKey(
                    burnIntent.spec.destinationRecipient,
                  ),
                  sourceSigner: hexToPublicKey(burnIntent.spec.sourceSigner),
                  destinationCaller: hexToPublicKey(burnIntent.spec.destinationCaller),
                  value: burnIntent.spec.value,
                  salt: Buffer.from(burnIntent.spec.salt.slice(2), "hex"),
                  hookDataLength: hookData.length,
                  hookData,
                },
              };
              const buffer = Buffer.alloc(72 + 340 + hookData.length);
              const bytesWritten = BurnIntentLayout.encode(prepared, buffer);
              return buffer.subarray(0, bytesWritten);
            }

            // Sign burn intent with Ed25519 keypair
            function signBurnIntent(keypair: Keypair, burnIntent: BurnIntent): string {
              const encoded = encodeBurnIntent(burnIntent);
              const prefixed = Buffer.concat([
                Buffer.from([0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
                encoded,
              ]);
              const privateKey = crypto.createPrivateKey({
                key: Buffer.concat([
                  Buffer.from("302e020100300506032b657004220420", "hex"),
                  Buffer.from(keypair.secretKey.slice(0, 32)),
                ]),
                format: "der",
                type: "pkcs8",
              });
              return `0x${crypto.sign(null, prefixed, privateKey).toString("hex")}`;
            }

            // Convert Solana address to 32-byte hex string
            function addressToBytes32(address: string): string {
              return `0x${Buffer.from(bs58.decode(address)).toString("hex")}`;
            }

            // Convert hex string to Solana PublicKey
            function hexToPublicKey(hex: string): PublicKey {
              return new PublicKey(Buffer.from(hex.slice(2), "hex"));
            }

            // Serialize API payloads with bigint values
            function stringifyBigInts<T>(obj: T): string {
              return JSON.stringify(obj, (_key, value) =>
                typeof value === "bigint" ? value.toString() : value,
              );
            }

            /* Main logic */
            async function main() {
              if (!process.env.SOLANA_PRIVATE_KEYPAIR || !process.env.RECIPIENT_KEYPAIR) {
                throw new Error("SOLANA_PRIVATE_KEYPAIR and RECIPIENT_KEYPAIR must be set");
              }

              const senderKeypair = createKeypairFromEnv(
                process.env.SOLANA_PRIVATE_KEYPAIR,
              );
              const recipientKeypair = createKeypairFromEnv(process.env.RECIPIENT_KEYPAIR);
              const recipientAta = getAssociatedTokenAddressSync(
                new PublicKey(USDC_ADDRESS),
                recipientKeypair.publicKey,
              );

              console.log(`Sender (Solana Devnet): ${senderKeypair.publicKey.toBase58()}`);
              console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
              console.log(`Amount: ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
              console.log("Forwarding: enabled\n");

              // [1] Create transfer spec and estimate forwarding fees
              const spec = createTransferSpec({
                sourceDepositor: senderKeypair.publicKey.toBase58(),
                destinationRecipient: recipientAta.toBase58(),
                sourceSigner: senderKeypair.publicKey.toBase58(),
              });

              const estimateResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyBigInts([{ spec }]),
                },
              );

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

              const estimateResult = (await estimateResponse.json()) as EstimateResponse;
              const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
              if (!estimatedBurnIntent) {
                throw new Error("Estimate API response missing burnIntent");
              }

              const maxFee = BigInt(estimatedBurnIntent.maxFee);
              const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);

              if (estimateResult.fees.forwardingFee) {
                console.log(
                  `Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
                );
              }
              console.log(
                `Estimated maxFee: ${formatUnits(maxFee, 6)} ${estimateResult.fees.token}`,
              );

              // [2] Create, sign, and submit burn intent
              const burnIntent = createBurnIntent({ maxBlockHeight, maxFee, spec });
              const signature = signBurnIntent(senderKeypair, burnIntent);

              const transferResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyBigInts([{ burnIntent, signature }]),
                },
              );

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

              const transferResult = (await transferResponse.json()) as TransferResponse;
              if (!transferResult.transferId) {
                throw new Error("Gateway API response missing transferId");
              }
              console.log(`Transfer ID: ${transferResult.transferId}`);

              // [3] Poll for forwarding completion
              const pollStart = Date.now();
              while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
                const pollResponse = await fetch(
                  `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
                );

                if (!pollResponse.ok) {
                  console.error(`Poll error: ${pollResponse.status}`);
                  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
                  continue;
                }

                const details = (await pollResponse.json()) as TransferStatusResponse;
                console.log(`Status: ${details.status}`);

                if (details.status === "finalized" || details.status === "confirmed") {
                  console.log(
                    `Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
                  );
                  console.log(`Forwarded ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
                  return;
                }

                if (details.status === "failed") {
                  const reason = details.forwardingDetails?.failureReason ?? "unknown";
                  throw new Error(`Transfer failed: ${reason}`);
                }

                if (details.status === "expired") {
                  throw new Error("Transfer attestation expired before forwarding");
                }

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

              throw new Error("Polling timed out waiting for transfer completion");
            }

            /* Main invocation */
            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.8. Run the Solana forwarding script

            Run the script to burn from your Solana Devnet Gateway balance and let the
            Forwarding Service mint to the recipient ATA on Solana Devnet.

            Confirm these values before running:

            * `SOLANA_PRIVATE_KEYPAIR` for the source Solana Devnet wallet
            * `RECIPIENT_KEYPAIR` for the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Solana Devnet
            * the recipient ATA exists on Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-sol
            ```

            When the transfer succeeds, the script logs the transfer ID, status, Solana
            Devnet mint transaction hash, and forwarded amount.
          </Tab>
        </Tabs>
      </Tab>

      <Tab title="Transfer from EVM">
        ## Step 4. Transfer USDC from EVM to Solana

        This step transfers USDC from your Arc Testnet Gateway balance to Solana Devnet.
        Both paths create and submit the burn intent from Arc Testnet first. Direct mint
        then retrieves the Gateway attestation and calls `gatewayMint(...)` on Solana
        Devnet from your self-managed Solana wallet, while Forwarding Service lets
        Circle complete the Solana mint for you.

        <Tabs>
          <Tab title="Direct Mint">
            ### 4.1. Create the script file

            You can skip to the
            [full transfer script](#4-9-full-evm-to-solana-transfer-script-self-managed) if
            you prefer.

            ```shell theme={null}
            touch transfer-from-evm.ts
            ```

            ### 4.2. Define constants and types

            The validated script uses Arc Testnet as the source chain and Solana Devnet as
            the destination chain. For now, the transfer amount is set to `0.1` USDC.

            ```ts transfer-from-evm.ts expandable theme={null}
            type MintAttestation = {
              destinationToken: PublicKey;
              destinationRecipient: PublicKey;
              transferSpecHash: Uint8Array;
            };

            type MintAttestationSet = {
              attestations: MintAttestation[];
            };

            /* Constants */
            const SOURCE_CHAIN = {
              chain: arcTestnet,
              gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9" as Hex,
              usdc: "0x3600000000000000000000000000000000000000" as Hex,
              domain: 26,
            } as const;

            const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;

            const eip712Domain = { name: "GatewayWallet", version: "1" } as const;
            ```

            ### 4.3. Add EIP-712 and attestation layouts

            EVM source transfers use EIP-712 typed data for the burn intent. The Solana
            destination mint still needs to decode the Gateway attestation set so the script
            can build the remaining accounts for `gatewayMint`.

            ```ts transfer-from-evm.ts expandable theme={null}
            const EIP712Domain = [
              { name: "name", type: "string" },
              { name: "version", type: "string" },
            ] as const;

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

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

            class PublicKeyLayout extends Layout<PublicKey> {
              constructor(property: string) {
                super(32, property);
              }

              decode(b: Buffer, offset = 0): PublicKey {
                return new PublicKey(b.subarray(offset, offset + 32));
              }

              encode(src: PublicKey, b: Buffer, offset = 0): number {
                src.toBuffer().copy(b, offset);
                return 32;
              }
            }

            const publicKey = (property: string) => new PublicKeyLayout(property);

            const MintAttestationElementLayout = struct([
              publicKey("destinationToken"),
              publicKey("destinationRecipient"),
              nu64be("value"),
              blob(32, "transferSpecHash"),
              u32be("hookDataLength"),
              blob(offset(u32be(), -4), "hookData"),
            ] as any);

            const MintAttestationSetLayout = struct([
              u32be("magic"),
              u32be("version"),
              u32be("destinationDomain"),
              publicKey("destinationContract"),
              publicKey("destinationCaller"),
              nu64be("maxBlockHeight"),
              u32be("numAttestations"),
              seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
            ] as any);
            ```

            ### 4.4. Add helper functions

            The burn intent uses EVM `bytes32` values for Arc Testnet and Solana `bytes32`
            values for the destination Gateway Minter, USDC mint, recipient ATA, and zero
            destination caller.

            ```ts transfer-from-evm.ts expandable theme={null}
            // Construct burn intent for EVM to Solana transfer
            function createBurnIntent(params: {
              depositorAddress: Hex;
              recipientAta: string;
            }) {
              const { depositorAddress, recipientAta } = params;

              return {
                maxBlockHeight: MAX_UINT64,
                maxFee: MAX_FEE,
                spec: {
                  version: 1,
                  sourceDomain: SOURCE_CHAIN.domain,
                  destinationDomain: SOLANA_DOMAIN,
                  sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                  destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                  sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                  destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                  sourceDepositor: evmAddressToBytes32(depositorAddress),
                  destinationRecipient: solanaAddressToBytes32(recipientAta),
                  sourceSigner: evmAddressToBytes32(depositorAddress),
                  destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                  value: TRANSFER_VALUE,
                  salt: `0x${randomBytes(32).toString("hex")}` as Hex,
                  hookData: "0x" as Hex,
                },
              };
            }

            // Format burn intent as EIP-712 typed data for signing
            function burnIntentTypedData(burnIntent: ReturnType<typeof createBurnIntent>) {
              return {
                types: { EIP712Domain, TransferSpec, BurnIntent },
                domain: eip712Domain,
                primaryType: "BurnIntent",
                message: burnIntent,
              } as const;
            }

            // Decode attestation set from Gateway API response
            function decodeAttestationSet(attestation: Hex): MintAttestationSet {
              const buffer = Buffer.from(attestation.slice(2), "hex");
              return MintAttestationSetLayout.decode(buffer) as MintAttestationSet;
            }

            // Find PDA for token custody account
            function findCustodyPda(
              mint: PublicKey,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("gateway_minter_custody"), mint.toBuffer()],
                minterProgramId,
              )[0];
            }

            // Find PDA for transfer spec hash tracking
            function findTransferSpecHashPda(
              transferSpecHash: Uint8Array | Buffer,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
                minterProgramId,
              )[0];
            }

            // Pad EVM address to 32 bytes
            function evmAddressToBytes32(address: Hex): Hex {
              return pad(address.toLowerCase() as Hex, { size: 32 });
            }

            // Convert Solana address to 32-byte hex string
            function solanaAddressToBytes32(address: string): Hex {
              const decoded = Buffer.from(bs58.decode(address));
              return `0x${decoded.toString("hex")}`;
            }

            // Serialize typed data (convert bigints to strings)
            function stringifyBigInts<T>(obj: T): string {
              return JSON.stringify(obj, (_key, value) =>
                typeof value === "bigint" ? value.toString() : value,
              );
            }

            // Get EVM keypair from environment variable
            function createEvmAccount() {
              if (!process.env.EVM_PRIVATE_KEY) {
                throw new Error("EVM_PRIVATE_KEY must be set");
              }

              const privateKey = process.env.EVM_PRIVATE_KEY.startsWith("0x")
                ? (process.env.EVM_PRIVATE_KEY as Hex)
                : (`0x${process.env.EVM_PRIVATE_KEY}` as Hex);

              return privateKeyToAccount(privateKey);
            }
            ```

            ### 4.5. Initialize wallets and create recipient ATA

            Use `EVM_PRIVATE_KEY` as the Arc Testnet source signer and
            `SOLANA_PRIVATE_KEYPAIR` as the Solana Devnet recipient and mint signer. The
            recipient must receive USDC in an associated token account (ATA), not directly
            in a wallet account.

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            ```ts transfer-from-evm.ts theme={null}
            const evmAccount = createEvmAccount();
            const solanaKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);

            const connection = new Connection(RPC_ENDPOINT, "confirmed");
            const usdcMint = new PublicKey(USDC_ADDRESS);

            const recipientAta = getAssociatedTokenAddressSync(
              usdcMint,
              solanaKeypair.publicKey,
            );
            const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
              solanaKeypair.publicKey,
              recipientAta,
              solanaKeypair.publicKey,
              usdcMint,
            );
            const createAtaTx = new Transaction().add(createAtaIx);
            await sendAndConfirmTransaction(connection, createAtaTx, [solanaKeypair]);
            ```

            ### 4.6. Create and sign burn intent

            Create one burn intent for Arc Testnet and sign it with the self-managed EVM
            wallet.

            ```ts transfer-from-evm.ts theme={null}
            const burnIntent = createBurnIntent({
              depositorAddress: evmAccount.address,
              recipientAta: recipientAta.toBase58(),
            });
            const typedData = burnIntentTypedData(burnIntent);
            const signature = await evmAccount.signTypedData(typedData);
            ```

            ### 4.7. Request attestation from Gateway API

            ```ts transfer-from-evm.ts theme={null}
            const response = await fetch(
              "https://gateway-api-testnet.circle.com/v1/transfer",
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
              },
            );

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

            const json = (await response.json()) as {
              attestation?: Hex;
              signature?: Hex;
            };

            const { attestation, signature: mintSignature } = json;
            if (!attestation || !mintSignature) {
              throw new Error("Gateway API response missing attestation or signature");
            }
            ```

            ### 4.8. Set up minter client and mint on Solana

            After Gateway returns the attestation and operator signature, use them to call
            `gatewayMint` on Solana Devnet.

            <Info>
              The ordered remaining-account list and PDA derivations are documented in
              [Solana Programs and
              Interfaces](/gateway/references/solana-programs#gatewaymint). For static
              instruction definitions, use the onchain IDLs linked from that page.
            </Info>

            ```ts transfer-from-evm.ts expandable theme={null}
            const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
            const anchorWallet = new Wallet(solanaKeypair);
            const provider = new AnchorProvider(
              connection,
              anchorWallet,
              AnchorProvider.defaultOptions(),
            );
            setProvider(provider);
            const minterProgram = new Program(gatewayMinterIdl, provider);

            const [minterPda] = PublicKey.findProgramAddressSync(
              [Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
              minterProgramId,
            );

            const decoded = decodeAttestationSet(attestation);
            const remainingAccounts = decoded.attestations.flatMap((attestationItem) => [
              {
                pubkey: findCustodyPda(attestationItem.destinationToken, minterProgramId),
                isWritable: true,
                isSigner: false,
              },
              {
                pubkey: attestationItem.destinationRecipient,
                isWritable: true,
                isSigner: false,
              },
              {
                pubkey: findTransferSpecHashPda(
                  attestationItem.transferSpecHash,
                  minterProgramId,
                ),
                isWritable: true,
                isSigner: false,
              },
            ]);

            const mintTx = await minterProgram.methods
              .gatewayMint({
                attestation: Buffer.from(attestation.slice(2), "hex"),
                signature: Buffer.from(mintSignature.slice(2), "hex"),
              })
              .accountsPartial({
                gatewayMinter: minterPda,
                destinationCaller: solanaKeypair.publicKey,
                payer: solanaKeypair.publicKey,
                systemProgram: SystemProgram.programId,
                tokenProgram: TOKEN_PROGRAM_ID,
              })
              .remainingAccounts(remainingAccounts)
              .signers([solanaKeypair])
              .rpc();
            ```

            ### 4.9. Full EVM to Solana transfer script (Self-managed)

            The complete transfer script creates the Solana recipient ATA, signs an Arc
            Testnet burn intent, submits it to Gateway for attestation, and mints USDC on
            Solana Devnet.

            ```ts transfer-from-evm.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import {
              Wallet,
              AnchorProvider,
              Program,
              setProvider,
              utils,
            } from "@coral-xyz/anchor";
            import {
              createAssociatedTokenAccountIdempotentInstruction,
              getAssociatedTokenAddressSync,
              TOKEN_PROGRAM_ID,
            } from "@solana/spl-token";
            import {
              Connection,
              PublicKey,
              sendAndConfirmTransaction,
              SystemProgram,
              Transaction,
            } from "@solana/web3.js";
            import {
              blob,
              Layout,
              nu64be,
              offset,
              seq,
              struct,
              u32be,
            } from "@solana/buffer-layout";
            import bs58 from "bs58";
            import { pad, type Hex } from "viem";
            import { privateKeyToAccount } from "viem/accounts";
            import { arcTestnet } from "viem/chains";
            import {
              GATEWAY_MINTER_ADDRESS,
              RPC_ENDPOINT,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              USDC_ADDRESS,
              createKeypairFromEnv,
              gatewayMinterIdl,
            } from "./config.js";

            type MintAttestation = {
              destinationToken: PublicKey;
              destinationRecipient: PublicKey;
              transferSpecHash: Uint8Array;
            };

            type MintAttestationSet = {
              attestations: MintAttestation[];
            };

            /* Constants */
            const SOURCE_CHAIN = {
              chain: arcTestnet,
              gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9" as Hex,
              usdc: "0x3600000000000000000000000000000000000000" as Hex,
              domain: 26,
            } as const;

            const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
            const MAX_FEE = 2_010000n;
            const MAX_UINT64 = 2n ** 64n - 1n;

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

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

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

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

            /* Type definitions */
            class PublicKeyLayout extends Layout<PublicKey> {
              constructor(property: string) {
                super(32, property);
              }

              decode(b: Buffer, offset = 0): PublicKey {
                return new PublicKey(b.subarray(offset, offset + 32));
              }

              encode(src: PublicKey, b: Buffer, offset = 0): number {
                src.toBuffer().copy(b, offset);
                return 32;
              }
            }

            const publicKey = (property: string) => new PublicKeyLayout(property);

            const MintAttestationElementLayout = struct([
              publicKey("destinationToken"),
              publicKey("destinationRecipient"),
              nu64be("value"),
              blob(32, "transferSpecHash"),
              u32be("hookDataLength"),
              blob(offset(u32be(), -4), "hookData"),
            ] as any);

            const MintAttestationSetLayout = struct([
              u32be("magic"),
              u32be("version"),
              u32be("destinationDomain"),
              publicKey("destinationContract"),
              publicKey("destinationCaller"),
              nu64be("maxBlockHeight"),
              u32be("numAttestations"),
              seq(MintAttestationElementLayout, offset(u32be(), -4), "attestations"),
            ] as any);

            /* Helpers */
            // Construct burn intent for EVM to Solana transfer
            function createBurnIntent(params: {
              depositorAddress: Hex;
              recipientAta: string;
            }) {
              const { depositorAddress, recipientAta } = params;

              return {
                maxBlockHeight: MAX_UINT64,
                maxFee: MAX_FEE,
                spec: {
                  version: 1,
                  sourceDomain: SOURCE_CHAIN.domain,
                  destinationDomain: SOLANA_DOMAIN,
                  sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                  destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                  sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                  destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                  sourceDepositor: evmAddressToBytes32(depositorAddress),
                  destinationRecipient: solanaAddressToBytes32(recipientAta),
                  sourceSigner: evmAddressToBytes32(depositorAddress),
                  destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                  value: TRANSFER_VALUE,
                  salt: `0x${randomBytes(32).toString("hex")}` as Hex,
                  hookData: "0x" as Hex,
                },
              };
            }

            // Format burn intent as EIP-712 typed data for signing
            function burnIntentTypedData(burnIntent: ReturnType<typeof createBurnIntent>) {
              return {
                types: { EIP712Domain, TransferSpec, BurnIntent },
                domain: eip712Domain,
                primaryType: "BurnIntent",
                message: burnIntent,
              } as const;
            }

            // Decode attestation set from Gateway API response
            function decodeAttestationSet(attestation: Hex): MintAttestationSet {
              const buffer = Buffer.from(attestation.slice(2), "hex");
              return MintAttestationSetLayout.decode(buffer) as MintAttestationSet;
            }

            // Find PDA for token custody account
            function findCustodyPda(
              mint: PublicKey,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("gateway_minter_custody"), mint.toBuffer()],
                minterProgramId,
              )[0];
            }

            // Find PDA for transfer spec hash tracking
            function findTransferSpecHashPda(
              transferSpecHash: Uint8Array | Buffer,
              minterProgramId: PublicKey,
            ): PublicKey {
              return PublicKey.findProgramAddressSync(
                [Buffer.from("used_transfer_spec_hash"), Buffer.from(transferSpecHash)],
                minterProgramId,
              )[0];
            }

            // Pad EVM address to 32 bytes
            function evmAddressToBytes32(address: Hex): Hex {
              return pad(address.toLowerCase() as Hex, { size: 32 });
            }

            // Convert Solana address to 32-byte hex string
            function solanaAddressToBytes32(address: string): Hex {
              const decoded = Buffer.from(bs58.decode(address));
              return `0x${decoded.toString("hex")}`;
            }

            // Serialize typed data (convert bigints to strings)
            function stringifyBigInts<T>(obj: T): string {
              return JSON.stringify(obj, (_key, value) =>
                typeof value === "bigint" ? value.toString() : value,
              );
            }

            // Get EVM keypair from environment variable
            function createEvmAccount() {
              if (!process.env.EVM_PRIVATE_KEY) {
                throw new Error("EVM_PRIVATE_KEY must be set");
              }

              const privateKey = process.env.EVM_PRIVATE_KEY.startsWith("0x")
                ? (process.env.EVM_PRIVATE_KEY as Hex)
                : (`0x${process.env.EVM_PRIVATE_KEY}` as Hex);

              return privateKeyToAccount(privateKey);
            }

            /* Main logic */
            async function main() {
              if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
                throw new Error("SOLANA_PRIVATE_KEYPAIR must be set");
              }

              const evmAccount = createEvmAccount();
              const solanaKeypair = createKeypairFromEnv(
                process.env.SOLANA_PRIVATE_KEYPAIR,
              );

              console.log(`Sender (${SOURCE_CHAIN.chain.name}): ${evmAccount.address}`);
              console.log(
                `Recipient (Solana Devnet): ${solanaKeypair.publicKey.toBase58()}`,
              );

              const connection = new Connection(RPC_ENDPOINT, "confirmed");
              const usdcMint = new PublicKey(USDC_ADDRESS);

              // [1] Create recipient's Associated Token Account
              const recipientAta = getAssociatedTokenAddressSync(
                usdcMint,
                solanaKeypair.publicKey,
              );
              const createAtaIx = createAssociatedTokenAccountIdempotentInstruction(
                solanaKeypair.publicKey,
                recipientAta,
                solanaKeypair.publicKey,
                usdcMint,
              );
              const createAtaTx = new Transaction().add(createAtaIx);
              await sendAndConfirmTransaction(connection, createAtaTx, [solanaKeypair]);

              // [2] Create and sign burn intent
              const burnIntent = createBurnIntent({
                depositorAddress: evmAccount.address,
                recipientAta: recipientAta.toBase58(),
              });
              const typedData = burnIntentTypedData(burnIntent);
              const signature = await evmAccount.signTypedData(typedData);

              // [3] Request attestation from Gateway API
              const response = await fetch(
                "https://gateway-api-testnet.circle.com/v1/transfer",
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
                },
              );

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

              const json = (await response.json()) as {
                attestation?: Hex;
                signature?: Hex;
              };
              console.log("Gateway API response:", JSON.stringify(json, null, 2));

              const { attestation, signature: mintSignature } = json;
              if (!attestation || !mintSignature) {
                throw new Error("Gateway API response missing attestation or signature");
              }

              // [4] Set up the minter client
              const minterProgramId = new PublicKey(GATEWAY_MINTER_ADDRESS);
              const anchorWallet = new Wallet(solanaKeypair);
              const provider = new AnchorProvider(
                connection,
                anchorWallet,
                AnchorProvider.defaultOptions(),
              );
              setProvider(provider);
              const minterProgram = new Program(gatewayMinterIdl, provider);

              const [minterPda] = PublicKey.findProgramAddressSync(
                [Buffer.from(utils.bytes.utf8.encode("gateway_minter"))],
                minterProgramId,
              );

              const decoded = decodeAttestationSet(attestation);
              const remainingAccounts = decoded.attestations.flatMap((attestationItem) => [
                {
                  pubkey: findCustodyPda(attestationItem.destinationToken, minterProgramId),
                  isWritable: true,
                  isSigner: false,
                },
                {
                  pubkey: attestationItem.destinationRecipient,
                  isWritable: true,
                  isSigner: false,
                },
                {
                  pubkey: findTransferSpecHashPda(
                    attestationItem.transferSpecHash,
                    minterProgramId,
                  ),
                  isWritable: true,
                  isSigner: false,
                },
              ]);

              // [5] Mint on Solana
              console.log("Minting funds on Solana Devnet...");
              const mintTx = await minterProgram.methods
                .gatewayMint({
                  attestation: Buffer.from(attestation.slice(2), "hex"),
                  signature: Buffer.from(mintSignature.slice(2), "hex"),
                })
                .accountsPartial({
                  gatewayMinter: minterPda,
                  destinationCaller: solanaKeypair.publicKey,
                  payer: solanaKeypair.publicKey,
                  systemProgram: SystemProgram.programId,
                  tokenProgram: TOKEN_PROGRAM_ID,
                })
                .remainingAccounts(remainingAccounts)
                .signers([solanaKeypair])
                .rpc();

              // [6] Wait for confirmation
              const latest = await connection.getLatestBlockhash();
              await connection.confirmTransaction(
                {
                  signature: mintTx,
                  blockhash: latest.blockhash,
                  lastValidBlockHeight: latest.lastValidBlockHeight,
                },
                "confirmed",
              );

              console.log(`Minted ${Number(TRANSFER_VALUE) / 1_000_000} USDC`);
              console.log(`Mint transaction hash (Solana Devnet):`, mintTx);
            }

            /* Main invocation */
            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.10. Run the EVM to Solana direct-mint script

            Run the script to burn from your Arc Testnet Gateway balance and mint to the
            recipient ATA on Solana Devnet.

            Confirm these values before running:

            * `EVM_PRIVATE_KEY` controls the source Arc Testnet wallet
            * `SOLANA_PRIVATE_KEYPAIR` controls the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Arc Testnet
            * the destination Solana wallet can submit the ATA creation and mint
              transactions on Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-evm
            ```

            When the transfer succeeds, the script logs the minted amount and Solana Devnet
            mint transaction hash.
          </Tab>

          <Tab title="Forwarding Service">
            ### 4.1. Create the EVM to Solana forwarding script

            You can skip to the
            [full transfer script](#4-8-full-evm-to-solana-forwarding-script-self-managed)
            if you prefer.

            ```shell theme={null}
            touch transfer-from-evm.ts
            ```

            ### 4.2. Define constants and response types

            This forwarding flow signs the burn intent on Arc Testnet, submits it with
            forwarding enabled, and lets Circle complete the Solana Devnet mint.

            ```ts transfer-from-evm.ts expandable theme={null}
            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
            const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;
            ```

            ### 4.3. Add EIP-712 helpers

            The forwarding flow first estimates the burn-intent limits for the Arc Testnet
            to Solana Devnet route, then signs the estimated burn intent with your
            self-managed EVM wallet.

            ```ts transfer-from-evm.ts expandable theme={null}
            const EIP712Domain = [
              { name: "name", type: "string" },
              { name: "version", type: "string" },
            ] as const;

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

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

            ### 4.4. Create the transfer spec helpers

            The transfer spec uses EVM `bytes32` values for Arc Testnet and Solana `bytes32`
            values for the destination Gateway Minter, USDC mint, and recipient ATA.

            ```ts transfer-from-evm.ts expandable theme={null}
            // Construct transfer spec for EVM to Solana forwarding transfer
            function createTransferSpec(params: {
              depositorAddress: Hex;
              recipientAta: string;
            }) {
              const { depositorAddress, recipientAta } = params;

              return {
                version: 1,
                sourceDomain: SOURCE_CHAIN.domain,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                sourceDepositor: evmAddressToBytes32(depositorAddress),
                destinationRecipient: solanaAddressToBytes32(recipientAta),
                sourceSigner: evmAddressToBytes32(depositorAddress),
                destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: `0x${randomBytes(32).toString("hex")}` as Hex,
                hookData: "0x" as Hex,
              };
            }

            // Format burn intent as EIP-712 typed data for signing
            function burnIntentTypedData(params: {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: ReturnType<typeof createTransferSpec>;
            }) {
              return {
                types: { EIP712Domain, TransferSpec, BurnIntent },
                domain: eip712Domain,
                primaryType: "BurnIntent",
                message: params,
              } as const;
            }
            ```

            ### 4.5. Estimate forwarding fees

            <Info>
              For transfers to Solana, the `destinationRecipient` must be an initialized
              USDC Token Account. If the intended recipient is a standard wallet address,
              use its Associated Token Account (ATA), not the recipient wallet address
              itself.
            </Info>

            Derive the recipient ATA, build the transfer spec, then call the Gateway
            estimate endpoint with `enableForwarder=true`. The response returns the `maxFee`
            and `maxBlockHeight` values to include in the signed burn intent.

            ```ts transfer-from-evm.ts theme={null}
            const evmAccount = createEvmAccount();
            const solanaKeypair = createKeypairFromEnv(process.env.SOLANA_PRIVATE_KEYPAIR);
            const recipientAta = getAssociatedTokenAddressSync(
              new PublicKey(USDC_ADDRESS),
              solanaKeypair.publicKey,
            );

            const spec = createTransferSpec({
              depositorAddress: evmAccount.address,
              recipientAta: recipientAta.toBase58(),
            });

            const estimateResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyBigInts([{ spec }]),
              },
            );
            ```

            ### 4.6. Sign and submit the forwarding burn intent

            Sign the estimated burn intent with the self-managed Arc Testnet wallet, then
            submit it to the Gateway transfer endpoint with `enableForwarder=true`.

            ```ts transfer-from-evm.ts theme={null}
            const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });
            const signature = await evmAccount.signTypedData(typedData);

            const transferResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
              {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
              },
            );
            ```

            ### 4.7. Poll for forwarding completion

            The Forwarding Service completes the Solana Devnet mint for you. Poll the
            transfer status endpoint until the transfer is confirmed, finalized, failed, or
            expired.

            ```ts transfer-from-evm.ts theme={null}
            const pollResponse = await fetch(
              `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
            );
            ```

            ### 4.8. Full EVM to Solana forwarding script (Self-managed)

            The complete script estimates forwarding fees, signs the burn intent on Arc
            Testnet, submits it with forwarding enabled, and polls until Circle completes
            the Solana Devnet mint.

            ```ts transfer-from-evm.ts expandable theme={null}
            import { randomBytes } from "node:crypto";
            import { getAssociatedTokenAddressSync } from "@solana/spl-token";
            import { PublicKey } from "@solana/web3.js";
            import bs58 from "bs58";
            import { formatUnits, pad, type Hex } from "viem";
            import { privateKeyToAccount } from "viem/accounts";
            import { arcTestnet } from "viem/chains";
            import {
              GATEWAY_MINTER_ADDRESS,
              SOLANA_DOMAIN,
              SOLANA_ZERO_ADDRESS,
              USDC_ADDRESS,
              createKeypairFromEnv,
            } from "./config.js";

            type EstimateResponse = {
              body: Array<{
                burnIntent: {
                  maxFee: string;
                  maxBlockHeight: string;
                };
              }>;
              fees: {
                forwardingFee?: string;
                token: string;
              };
            };

            type TransferResponse = {
              transferId?: string;
            };

            type TransferStatusResponse = {
              status: string;
              transactionHash?: string;
              forwardingDetails?: {
                failureReason?: string;
              };
            };

            /* Constants */
            const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";

            const SOURCE_CHAIN = {
              chain: arcTestnet,
              gatewayWallet: "0x0077777d7EBA4688BDeF3E311b846F25870A19B9" as Hex,
              usdc: "0x3600000000000000000000000000000000000000" as Hex,
              domain: 26,
            } as const;

            const TRANSFER_VALUE = 100_000n; // 0.1 USDC (6 decimals)
            const POLL_INTERVAL_MS = 5_000;
            const POLL_TIMEOUT_MS = 300_000;

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

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

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

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

            /* Helpers */
            // Construct transfer spec for EVM to Solana forwarding transfer
            function createTransferSpec(params: {
              depositorAddress: Hex;
              recipientAta: string;
            }) {
              const { depositorAddress, recipientAta } = params;

              return {
                version: 1,
                sourceDomain: SOURCE_CHAIN.domain,
                destinationDomain: SOLANA_DOMAIN,
                sourceContract: evmAddressToBytes32(SOURCE_CHAIN.gatewayWallet),
                destinationContract: solanaAddressToBytes32(GATEWAY_MINTER_ADDRESS),
                sourceToken: evmAddressToBytes32(SOURCE_CHAIN.usdc),
                destinationToken: solanaAddressToBytes32(USDC_ADDRESS),
                sourceDepositor: evmAddressToBytes32(depositorAddress),
                destinationRecipient: solanaAddressToBytes32(recipientAta),
                sourceSigner: evmAddressToBytes32(depositorAddress),
                destinationCaller: solanaAddressToBytes32(SOLANA_ZERO_ADDRESS),
                value: TRANSFER_VALUE,
                salt: `0x${randomBytes(32).toString("hex")}` as Hex,
                hookData: "0x" as Hex,
              };
            }

            // Format burn intent as EIP-712 typed data for signing
            function burnIntentTypedData(params: {
              maxBlockHeight: bigint;
              maxFee: bigint;
              spec: ReturnType<typeof createTransferSpec>;
            }) {
              return {
                types: { EIP712Domain, TransferSpec, BurnIntent },
                domain: eip712Domain,
                primaryType: "BurnIntent",
                message: params,
              } as const;
            }

            // Pad EVM address to 32 bytes
            function evmAddressToBytes32(address: Hex): Hex {
              return pad(address.toLowerCase() as Hex, { size: 32 });
            }

            // Convert Solana address to 32-byte hex string
            function solanaAddressToBytes32(address: string): Hex {
              const decoded = Buffer.from(bs58.decode(address));
              return `0x${decoded.toString("hex")}`;
            }

            // Serialize typed data and convert bigints to strings
            function stringifyBigInts<T>(obj: T): string {
              return JSON.stringify(obj, (_key, value) =>
                typeof value === "bigint" ? value.toString() : value,
              );
            }

            // Get EVM keypair from environment variable
            function createEvmAccount() {
              if (!process.env.EVM_PRIVATE_KEY) {
                throw new Error("EVM_PRIVATE_KEY must be set");
              }

              const privateKey = process.env.EVM_PRIVATE_KEY.startsWith("0x")
                ? (process.env.EVM_PRIVATE_KEY as Hex)
                : (`0x${process.env.EVM_PRIVATE_KEY}` as Hex);

              return privateKeyToAccount(privateKey);
            }

            /* Main logic */
            async function main() {
              if (!process.env.SOLANA_PRIVATE_KEYPAIR) {
                throw new Error("SOLANA_PRIVATE_KEYPAIR must be set");
              }

              const evmAccount = createEvmAccount();
              const solanaKeypair = createKeypairFromEnv(
                process.env.SOLANA_PRIVATE_KEYPAIR,
              );
              const recipientAta = getAssociatedTokenAddressSync(
                new PublicKey(USDC_ADDRESS),
                solanaKeypair.publicKey,
              );

              console.log(`Sender (${SOURCE_CHAIN.chain.name}): ${evmAccount.address}`);
              console.log(`Recipient ATA (Solana Devnet): ${recipientAta.toBase58()}`);
              console.log(`Amount: ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
              console.log("Forwarding: enabled\n");

              // [1] Create transfer spec and estimate forwarding fees
              const spec = createTransferSpec({
                depositorAddress: evmAccount.address,
                recipientAta: recipientAta.toBase58(),
              });

              const estimateResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyBigInts([{ spec }]),
                },
              );

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

              const estimateResult = (await estimateResponse.json()) as EstimateResponse;
              const estimatedBurnIntent = estimateResult.body[0]?.burnIntent;
              if (!estimatedBurnIntent) {
                throw new Error("Estimate API response missing burnIntent");
              }

              const maxFee = BigInt(estimatedBurnIntent.maxFee);
              const maxBlockHeight = BigInt(estimatedBurnIntent.maxBlockHeight);

              if (estimateResult.fees.forwardingFee) {
                console.log(
                  `Forwarding fee: ${estimateResult.fees.forwardingFee} ${estimateResult.fees.token}`,
                );
              }
              console.log(
                `Estimated maxFee: ${formatUnits(maxFee, 6)} ${estimateResult.fees.token}`,
              );

              // [2] Create, sign, and submit burn intent
              const typedData = burnIntentTypedData({ maxBlockHeight, maxFee, spec });
              const signature = await evmAccount.signTypedData(typedData);

              const transferResponse = await fetch(
                `${GATEWAY_API_BASE}/v1/transfer?enableForwarder=true`,
                {
                  method: "POST",
                  headers: { "Content-Type": "application/json" },
                  body: stringifyBigInts([{ burnIntent: typedData.message, signature }]),
                },
              );

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

              const transferResult = (await transferResponse.json()) as TransferResponse;
              if (!transferResult.transferId) {
                throw new Error("Gateway API response missing transferId");
              }
              console.log(`Transfer ID: ${transferResult.transferId}`);

              // [3] Poll for forwarding completion
              const pollStart = Date.now();
              while (Date.now() - pollStart < POLL_TIMEOUT_MS) {
                const pollResponse = await fetch(
                  `${GATEWAY_API_BASE}/v1/transfer/${transferResult.transferId}`,
                );

                if (!pollResponse.ok) {
                  console.error(`Poll error: ${pollResponse.status}`);
                  await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
                  continue;
                }

                const details = (await pollResponse.json()) as TransferStatusResponse;
                console.log(`Status: ${details.status}`);

                if (details.status === "finalized" || details.status === "confirmed") {
                  console.log(
                    `Mint transaction hash (Solana Devnet): ${details.transactionHash}`,
                  );
                  console.log(`Forwarded ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
                  return;
                }

                if (details.status === "failed") {
                  const reason = details.forwardingDetails?.failureReason ?? "unknown";
                  throw new Error(`Transfer failed: ${reason}`);
                }

                if (details.status === "expired") {
                  throw new Error("Transfer attestation expired before forwarding");
                }

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

              throw new Error("Polling timed out waiting for transfer completion");
            }

            /* Main invocation */
            main().catch((error) => {
              console.error("\nError:", error);
              process.exit(1);
            });
            ```

            ### 4.9. Run the EVM to Solana forwarding script

            Run the script to burn from your Arc Testnet Gateway balance and let the
            Forwarding Service mint to the recipient ATA on Solana Devnet.

            Confirm these values before running:

            * `EVM_PRIVATE_KEY` for the source Arc Testnet wallet
            * `SOLANA_PRIVATE_KEYPAIR` for the destination Solana Devnet wallet
            * the source depositor has a Gateway balance on Arc Testnet
            * the recipient ATA exists on Solana Devnet

            <Note>
              Gateway fees are charged per burn intent and are based on the source
              blockchain you burn from. Choosing where to hold and burn Gateway balances can
              affect transfer costs. For fee details, see [Gateway
              Fees](/gateway/references/fees).
            </Note>

            ```shell theme={null}
            npm run transfer-from-evm
            ```

            When the transfer succeeds, the script logs the transfer ID, status, Solana
            Devnet mint transaction hash, and forwarded amount.
          </Tab>
        </Tabs>
      </Tab>
    </Tabs>
  </Tab>
</Tabs>
