Skip to main content
This guide demonstrates how to transfer USDC from Solana Devnet to Arc Testnet using CCTP. You use the Solana Kit library to interact with Solana CCTP programs, and viem to mint USDC on Arc Testnet.
Tip: Use Bridge Kit to simplify crosschain transfers with CCTP.This guide demonstrates how to transfer USDC from to using a manual integration with CCTP. This example is provided for understanding and for developers who may need to implement a manual integration.For a more streamlined experience, use Bridge Kit to transfer USDC between blockchains in just a few lines of code.

Prerequisites

Before you begin, ensure that you’ve:
  • Installed Node.js v22+
  • Prepared a Solana wallet and have the private key array available
  • Funded your Solana wallet with the following testnet tokens:
  • Prepared an EVM testnet wallet with the private key available
  • Funded your EVM wallet with Arc Testnet USDC (for gas fees) from the Circle Faucet

Step 1. Set up the project

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

1.1. Set up your development environment

Create a new directory and install the required dependencies:
Shell
# Set up your directory and initialize a Node.js project
mkdir cctp-solana-transfer
cd cctp-solana-transfer
npm init -y

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

# Install runtime dependencies
npm install @solana/kit @solana-program/system @solana-program/token viem tsx

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

1.2. Initialize and configure the project

This command creates a tsconfig.json file:
Shell
npx tsc --init
Then, edit the tsconfig.json file:
Shell
# Replace the contents of the generated file
cat <<'EOF' > tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["node"]
  }
}
EOF

1.3. Configure environment variables

Create a .env file in your project directory and add your wallet private keys, replacing {YOUR_EVM_PRIVATE_KEY} with the private key from your EVM wallet and {YOUR_SOLANA_PRIVATE_KEY_ARRAY} with the private key array from your Solana wallet.
Shell
echo "SOLANA_PRIVATE_KEY={YOUR_SOLANA_PRIVATE_KEY_ARRAY}
EVM_PRIVATE_KEY={YOUR_EVM_PRIVATE_KEY}" > .env
Warning: This is strictly for testing purposes. Never share your private keys.

Step 2: Configure the script

Define the configuration constants for interacting with Solana and Arc Testnet.

2.1. Setup chains and wallets

The script predefines the program addresses, transfer amount, and other parameters:
TypeScript
// Solana Configuration
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!);
const solanaKeypair = await createKeyPairSignerFromBytes(
  Uint8Array.from(solanaPrivateKey),
);

// Solana CCTP Program Addresses (Devnet)
const TOKEN_MESSENGER_MINTER_PROGRAM = address(
  "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe",
);
const MESSAGE_TRANSMITTER_PROGRAM = address(
  "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC",
);
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const ASSOCIATED_TOKEN_PROGRAM = address(
  "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);

// Arc Testnet Configuration
const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!;
const ethAccount = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`);
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
const arcClient = createWalletClient({
  chain: arcTestnet,
  transport: http(),
  account: ethAccount,
});

// Transfer Parameters
const AMOUNT = 1_000_000n;
const DESTINATION_DOMAIN = 26;
const ARC_DESTINATION_ADDRESS = ethAccount.address;
const MAX_FEE = 500n;

Step 3: Implement the transfer logic

The following sections outline the core transfer logic from Solana to Arc.

3.1. Burn USDC on Solana

Call the depositForBurn instruction from the TokenMessengerMinterV2 program to burn USDC on Solana:
TypeScript
async function burnUSDCOnSolana() {
  console.log("Burning USDC on Solana...");

  const addressEncoder = getAddressEncoder();

  // Get the sender's USDC token account (Associated Token Account PDA)
  const [senderUsdcAccount] = await getProgramDerivedAddress({
    programAddress: ASSOCIATED_TOKEN_PROGRAM,
    seeds: [
      addressEncoder.encode(solanaKeypair.address),
      addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
      addressEncoder.encode(USDC_MINT),
    ],
  });

  const destAddressBytes32 = Buffer.concat([
    Buffer.alloc(12),
    Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"),
  ]);

  // Derive PDAs (Program Derived Addresses)
  const [senderAuthorityPda] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("sender_authority")],
  });

  const [denylistPda] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [
      new TextEncoder().encode("denylist_account"),
      addressEncoder.encode(solanaKeypair.address),
    ],
  });

  const [messageTransmitter] = await getProgramDerivedAddress({
    programAddress: MESSAGE_TRANSMITTER_PROGRAM,
    seeds: [new TextEncoder().encode("message_transmitter")],
  });

  const [tokenMessenger] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("token_messenger")],
  });

  // NOTE: Domain is converted to string for PDA derivation in V2
  const [remoteTokenMessenger] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [
      new TextEncoder().encode("remote_token_messenger"),
      new TextEncoder().encode(DESTINATION_DOMAIN.toString()),
    ],
  });

  const [tokenMinter] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("token_minter")],
  });

  const [localToken] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [
      new TextEncoder().encode("local_token"),
      addressEncoder.encode(USDC_MINT),
    ],
  });

  // Derive event authority PDAs for Anchor CPI events
  const [eventAuthority] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("__event_authority")],
  });

  const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({
    programAddress: MESSAGE_TRANSMITTER_PROGRAM,
    seeds: [new TextEncoder().encode("__event_authority")],
  });

  const messageSentEventAccount = await generateKeyPairSigner();

  const discriminator = crypto
    .createHash("sha256")
    .update("global:deposit_for_burn")
    .digest()
    .slice(0, 8);

  const amountBuffer = Buffer.alloc(8);
  amountBuffer.writeBigUInt64LE(AMOUNT);

  const domainBuffer = Buffer.alloc(4);
  domainBuffer.writeUInt32LE(DESTINATION_DOMAIN);

  const maxFeeBuffer = Buffer.alloc(8);
  maxFeeBuffer.writeBigUInt64LE(MAX_FEE);

  const finalityBuffer = Buffer.alloc(4);
  finalityBuffer.writeUInt32LE(1000);

  const instructionData = new Uint8Array(
    Buffer.concat([
      discriminator,
      amountBuffer,
      domainBuffer,
      destAddressBytes32,
      Buffer.alloc(32),
      maxFeeBuffer,
      finalityBuffer,
    ]),
  );

  // Note: For accounts that need to sign, pass the signer object directly
  const depositForBurnIx = {
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    accounts: [
      { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, // owner (WRITABLE_SIGNER)
      { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, // event_rent_payer (WRITABLE_SIGNER)
      { address: senderAuthorityPda, role: 0 }, // sender_authority_pda (READONLY)
      { address: senderUsdcAccount, role: 1 }, // burn_token_account (WRITABLE)
      { address: denylistPda, role: 0 }, // denylist_account (READONLY)
      { address: messageTransmitter, role: 1 }, // message_transmitter (WRITABLE)
      { address: tokenMessenger, role: 0 }, // token_messenger (READONLY)
      { address: remoteTokenMessenger, role: 0 }, // remote_token_messenger (READONLY)
      { address: tokenMinter, role: 0 }, // token_minter (READONLY)
      { address: localToken, role: 1 }, // local_token (WRITABLE)
      { address: USDC_MINT, role: 1 }, // burn_token_mint (WRITABLE)
      {
        address: messageSentEventAccount.address,
        role: 3,
        signer: messageSentEventAccount,
      }, // message_sent_event_data (WRITABLE_SIGNER)
      { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, // message_transmitter_program (READONLY)
      { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, // token_messenger_minter_program (READONLY)
      { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, // token_program (READONLY)
      { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, // system_program (READONLY)
      { address: eventAuthority, role: 0 }, // event_authority (READONLY)
      { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, // program (READONLY)
      { address: messageTransmitterEventAuthority, role: 0 }, // event_authority (READONLY)
      { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, // program (READONLY)
    ],
    data: instructionData,
  };

  // Get latest blockhash for transaction lifetime
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  // Build and sign transaction
  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstruction(depositForBurnIx, tx),
  );

  const signedTransaction =
    await signTransactionMessageWithSigners(transactionMessage);

  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  await sendAndConfirmTransaction(signedTransaction as any, {
    commitment: "confirmed",
  });

  const signature = getSignatureFromTransaction(signedTransaction);
  console.log(`Burn transaction signature: ${signature}`);

  return signature;
}

3.2. Retrieve attestation

Retrieve the attestation required to complete the CCTP transfer by calling Circle’s attestation API:
TypeScript
async function retrieveAttestation(transactionSignature: string) {
  console.log("Retrieving attestation...");
  const url = `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`;

  while (true) {
    try {
      const response = await fetch(url, { method: "GET" });

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

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

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

      if (data?.messages?.[0]?.status === "complete") {
        console.log("Attestation retrieved successfully!");
        return data.messages[0];
      }

      console.log("Waiting for attestation...");
      await new Promise((resolve) => setTimeout(resolve, 5000));
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      console.error("Error fetching attestation:", message);
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }
}

3.3. Mint USDC on Arc Testnet

Call the receiveMessage function from the MessageTransmitterV2 contract on Arc Testnet to mint USDC:
TypeScript
async function mintUSDCOnArc(attestation: AttestationMessage) {
  console.log("Minting USDC on Arc testnet...");

  const mintTx = await arcClient.sendTransaction({
    to: ARC_MESSAGE_TRANSMITTER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "receiveMessage",
          stateMutability: "nonpayable",
          inputs: [
            { name: "message", type: "bytes" },
            { name: "attestation", type: "bytes" },
          ],
          outputs: [],
        },
      ],
      functionName: "receiveMessage",
      args: [
        attestation.message as `0x${string}`,
        attestation.attestation as `0x${string}`,
      ],
    }),
  });

  console.log(`Mint transaction hash: ${mintTx}`);
}

Step 4: Complete script

Create a index.ts file in your project directory and populate it with the complete code below.
index.ts
import crypto from "crypto";
import {
  address,
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  generateKeyPairSigner,
  getAddressEncoder,
  getProgramDerivedAddress,
  getSignatureFromTransaction,
  pipe,
  sendAndConfirmTransactionFactory,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstruction,
  signTransactionMessageWithSigners,
} from "@solana/kit";
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import { createWalletClient, http, encodeFunctionData } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";

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

interface AttestationResponse {
  messages: AttestationMessage[];
}

// ============ Configuration & Setup ============
// Solana Configuration
const SOLANA_RPC = "https://api.devnet.solana.com";
const SOLANA_WS = "wss://api.devnet.solana.com";
const rpc = createSolanaRpc(SOLANA_RPC);
const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS);
const solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!);
const solanaKeypair = await createKeyPairSignerFromBytes(
  Uint8Array.from(solanaPrivateKey),
);

// Solana CCTP Program Addresses (Devnet)
const TOKEN_MESSENGER_MINTER_PROGRAM = address(
  "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe",
);
const MESSAGE_TRANSMITTER_PROGRAM = address(
  "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC",
);
const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU");
const ASSOCIATED_TOKEN_PROGRAM = address(
  "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
);

// Arc Testnet Configuration
const EVM_PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!;
const ethAccount = privateKeyToAccount(EVM_PRIVATE_KEY as `0x${string}`);
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
const arcClient = createWalletClient({
  chain: arcTestnet,
  transport: http(),
  account: ethAccount,
});

// Transfer Parameters
const AMOUNT = 1_000_000n;
const DESTINATION_DOMAIN = 26;
const ARC_DESTINATION_ADDRESS = ethAccount.address;
const MAX_FEE = 500n;

// ============ CCTP Flow Functions ============
async function burnUSDCOnSolana() {
  console.log("Burning USDC on Solana...");

  const addressEncoder = getAddressEncoder();

  // Get the sender's USDC token account (Associated Token Account PDA)
  const [senderUsdcAccount] = await getProgramDerivedAddress({
    programAddress: ASSOCIATED_TOKEN_PROGRAM,
    seeds: [
      addressEncoder.encode(solanaKeypair.address),
      addressEncoder.encode(TOKEN_PROGRAM_ADDRESS),
      addressEncoder.encode(USDC_MINT),
    ],
  });

  const destAddressBytes32 = Buffer.concat([
    Buffer.alloc(12),
    Buffer.from(ARC_DESTINATION_ADDRESS.slice(2), "hex"),
  ]);

  // Derive PDAs (Program Derived Addresses)
  const [senderAuthorityPda] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("sender_authority")],
  });

  const [denylistPda] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [
      new TextEncoder().encode("denylist_account"),
      addressEncoder.encode(solanaKeypair.address),
    ],
  });

  const [messageTransmitter] = await getProgramDerivedAddress({
    programAddress: MESSAGE_TRANSMITTER_PROGRAM,
    seeds: [new TextEncoder().encode("message_transmitter")],
  });

  const [tokenMessenger] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("token_messenger")],
  });

  // NOTE: Domain is converted to string for PDA derivation in V2
  const [remoteTokenMessenger] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [
      new TextEncoder().encode("remote_token_messenger"),
      new TextEncoder().encode(DESTINATION_DOMAIN.toString()),
    ],
  });

  const [tokenMinter] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("token_minter")],
  });

  const [localToken] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [
      new TextEncoder().encode("local_token"),
      addressEncoder.encode(USDC_MINT),
    ],
  });

  // Derive event authority PDAs for Anchor CPI events
  const [eventAuthority] = await getProgramDerivedAddress({
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    seeds: [new TextEncoder().encode("__event_authority")],
  });

  const [messageTransmitterEventAuthority] = await getProgramDerivedAddress({
    programAddress: MESSAGE_TRANSMITTER_PROGRAM,
    seeds: [new TextEncoder().encode("__event_authority")],
  });

  const messageSentEventAccount = await generateKeyPairSigner();

  const discriminator = crypto
    .createHash("sha256")
    .update("global:deposit_for_burn")
    .digest()
    .slice(0, 8);

  const amountBuffer = Buffer.alloc(8);
  amountBuffer.writeBigUInt64LE(AMOUNT);

  const domainBuffer = Buffer.alloc(4);
  domainBuffer.writeUInt32LE(DESTINATION_DOMAIN);

  const maxFeeBuffer = Buffer.alloc(8);
  maxFeeBuffer.writeBigUInt64LE(MAX_FEE);

  const finalityBuffer = Buffer.alloc(4);
  finalityBuffer.writeUInt32LE(1000);

  const instructionData = new Uint8Array(
    Buffer.concat([
      discriminator,
      amountBuffer,
      domainBuffer,
      destAddressBytes32,
      Buffer.alloc(32),
      maxFeeBuffer,
      finalityBuffer,
    ]),
  );

  // Note: For accounts that need to sign, pass the signer object directly
  const depositForBurnIx = {
    programAddress: TOKEN_MESSENGER_MINTER_PROGRAM,
    accounts: [
      { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, // owner (WRITABLE_SIGNER)
      { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, // event_rent_payer (WRITABLE_SIGNER)
      { address: senderAuthorityPda, role: 0 }, // sender_authority_pda (READONLY)
      { address: senderUsdcAccount, role: 1 }, // burn_token_account (WRITABLE)
      { address: denylistPda, role: 0 }, // denylist_account (READONLY)
      { address: messageTransmitter, role: 1 }, // message_transmitter (WRITABLE)
      { address: tokenMessenger, role: 0 }, // token_messenger (READONLY)
      { address: remoteTokenMessenger, role: 0 }, // remote_token_messenger (READONLY)
      { address: tokenMinter, role: 0 }, // token_minter (READONLY)
      { address: localToken, role: 1 }, // local_token (WRITABLE)
      { address: USDC_MINT, role: 1 }, // burn_token_mint (WRITABLE)
      {
        address: messageSentEventAccount.address,
        role: 3,
        signer: messageSentEventAccount,
      }, // message_sent_event_data (WRITABLE_SIGNER)
      { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, // message_transmitter_program (READONLY)
      { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, // token_messenger_minter_program (READONLY)
      { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, // token_program (READONLY)
      { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, // system_program (READONLY)
      { address: eventAuthority, role: 0 }, // event_authority (READONLY)
      { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, // program (READONLY)
      { address: messageTransmitterEventAuthority, role: 0 }, // event_authority (READONLY)
      { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, // program (READONLY)
    ],
    data: instructionData,
  };

  // Get latest blockhash for transaction lifetime
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

  // Build and sign transaction
  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) => appendTransactionMessageInstruction(depositForBurnIx, tx),
  );

  const signedTransaction =
    await signTransactionMessageWithSigners(transactionMessage);

  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  await sendAndConfirmTransaction(signedTransaction as any, {
    commitment: "confirmed",
  });

  const signature = getSignatureFromTransaction(signedTransaction);
  console.log(`Burn transaction signature: ${signature}`);

  return signature;
}

async function retrieveAttestation(transactionSignature: string) {
  console.log("Retrieving attestation...");
  const url = `https://iris-api-sandbox.circle.com/v2/messages/5?transactionHash=${transactionSignature}`;

  while (true) {
    try {
      const response = await fetch(url, { method: "GET" });

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

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

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

      if (data?.messages?.[0]?.status === "complete") {
        console.log("Attestation retrieved successfully!");
        return data.messages[0];
      }

      console.log("Waiting for attestation...");
      await new Promise((resolve) => setTimeout(resolve, 5000));
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      console.error("Error fetching attestation:", message);
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }
}

async function mintUSDCOnArc(attestation: AttestationMessage) {
  console.log("Minting USDC on Arc testnet...");

  const mintTx = await arcClient.sendTransaction({
    to: ARC_MESSAGE_TRANSMITTER,
    data: encodeFunctionData({
      abi: [
        {
          type: "function",
          name: "receiveMessage",
          stateMutability: "nonpayable",
          inputs: [
            { name: "message", type: "bytes" },
            { name: "attestation", type: "bytes" },
          ],
          outputs: [],
        },
      ],
      functionName: "receiveMessage",
      args: [
        attestation.message as `0x${string}`,
        attestation.attestation as `0x${string}`,
      ],
    }),
  });

  console.log(`Mint transaction hash: ${mintTx}`);
}

// ============ Main Execution ============
async function main() {
  const burnSignature = await burnUSDCOnSolana();
  const attestation = await retrieveAttestation(burnSignature);
  await mintUSDCOnArc(attestation);
  console.log("USDC transfer from Solana Devnet to Arc Testnet completed!");
}

main().catch(console.error);

Step 5: Test the script

Run the following command to execute the script:
Shell
npm run start
Once the script runs and the transfer is finalized, a confirmation message is logged in the console.
Rate limit:The attestation service rate limit is 35 requests per second. If you exceed this limit, the service blocks all API requests for the next 5 minutes and returns an HTTP 429 (Too Many Requests) response.