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:
- Solana Devnet SOL (native token) from a public faucet
- Solana Devnet USDC from the Circle Faucet
- Prepared an EVM testnet wallet with the private key available
- Added Arc testnet network to your wallet (network details)
- 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
Report incorrect code
Copy
# 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 atsconfig.json file:
Shell
Report incorrect code
Copy
npx tsc --init
tsconfig.json file:
Shell
Report incorrect code
Copy
# 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
Report incorrect code
Copy
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
Report incorrect code
Copy
// 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 thedepositForBurn instruction from the TokenMessengerMinterV2 program
to burn USDC on Solana:
TypeScript
Report incorrect code
Copy
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
Report incorrect code
Copy
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 thereceiveMessage function from the MessageTransmitterV2 contract on
Arc Testnet to mint USDC:
TypeScript
Report incorrect code
Copy
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 aindex.ts file in your project directory and populate it with the
complete code below.
index.ts
Report incorrect code
Copy
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
Report incorrect code
Copy
npm run start
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.