Get Started with CCTP#
Introduction#
Wormhole CCTP enables native USDC transfers between supported chains by burning tokens on the source chain and minting them on the destination. This provides native, canonical USDC movement without the need for wrapped tokens.
In this guide, you will use the Wormhole TypeScript SDK to perform a manual cross-chain USDC transfer using Circle's CCTP protocol.
You will initiate the transfer on the source chain, wait for Circle's attestation, and redeem the USDC on the destination chain.
Prerequisites#
Before you begin, make sure you have the following:
- Node.js and npm
- Wallets funded with native tokens and USDC on two supported CCTP chains
This example uses an Avalanche Fuji wallet with USDC and AVAX, as well as a Sepolia wallet with testnet ETH, to pay the transaction fees. You can adapt the steps to work with any supported EVM chains that support CCTP.
Configure Your Token Transfer Environment#
-
Create a new directory and initialize a Node.js project:
-
Install the required dependencies:
-
Create a
transfer.ts
file to handle the multichain transfer logic and ahelper.ts
file to manage wallet signers: -
Set up secure access to your wallets. This guide assumes you are loading your
EVM_PRIVATE_KEY
from a secure keystore of your choice, such as a secrets manager or a CLI-based tool likecast wallet
.Warning
If you use a
.env
file during development, add it to your.gitignore
to exclude it from version control. Never commit private keys or mnemonics to your repository.
Perform a CCTP Transfer#
This section walks you through a complete manual USDC transfer using Wormhole's CCTP integration. You will initiate the transfer on Avalanche Fuji, wait for the Circle attestation, and complete the redemption on Sepolia.
Start by defining utility functions for signer and token setup:
-
In
helper.ts
, define functions to load private keys and instantiate EVM signers:helper.tsimport { ChainAddress, ChainContext, Network, Signer, Wormhole, Chain, } from '@wormhole-foundation/sdk'; import solana from '@wormhole-foundation/sdk/solana'; import sui from '@wormhole-foundation/sdk/sui'; import evm from '@wormhole-foundation/sdk/evm'; /** * Returns a signer for the given chain using locally scoped credentials. * The required values (EVM_PRIVATE_KEY, SOL_PRIVATE_KEY, SUI_MNEMONIC) must * be loaded securely beforehand, for example via a keystore, secrets * manager, or environment variables (not recommended). */ export async function getSigner<N extends Network, C extends Chain>( chain: ChainContext<N, C> ): Promise<{ chain: ChainContext<N, C>; signer: Signer<N, C>; address: ChainAddress<C>; }> { let signer: Signer; const platform = chain.platform.utils()._platform; switch (platform) { case 'Evm': signer = await ( await evm() ).getSigner(await chain.getRpc(), EVM_PRIVATE_KEY!); break; case 'Solana': signer = await ( await solana() ).getSigner(await chain.getRpc(), SOL_PRIVATE_KEY!); break; case 'Sui': signer = await ( await sui() ).getSigner(await chain.getRpc(), SUI_MNEMONIC!); break; default: throw new Error(`Unsupported platform: ${platform}`); } return { chain, signer: signer as Signer<N, C>, address: Wormhole.chainAddress(chain.chain, signer.address()), }; }
-
In
transfer.ts
, add the script to perform the manual transfer using CCTP:transfer.tsimport { CircleTransfer, wormhole } from '@wormhole-foundation/sdk'; import evm from '@wormhole-foundation/sdk/evm'; import solana from '@wormhole-foundation/sdk/solana'; import sui from '@wormhole-foundation/sdk/sui'; import { getSigner } from './helper'; (async function () { // Initialize the Wormhole object for the Testnet environment and add supported chains (evm, solana and sui) const wh = await wormhole('Testnet', [evm, solana, sui]); // Grab chain Contexts -- these hold a reference to a cached rpc client const sendChain = wh.getChain('Avalanche'); const rcvChain = wh.getChain('Sepolia'); // Get signer from local key const source = await getSigner(sendChain); const destination = await getSigner(rcvChain); // Define the amount of USDC to transfer (in the smallest unit, so 0.1 USDC = 100,000 units assuming 6 decimals) const amt = 100_000n; const automatic = false; // Create the circleTransfer transaction (USDC-only) const xfer = await wh.circleTransfer( amt, source.address, destination.address, automatic ); const quote = await CircleTransfer.quoteTransfer( sendChain, rcvChain, xfer.transfer ); console.log('Quote: ', quote); // Step 1: Initiate the transfer on the source chain (Avalanche) console.log('Starting Transfer'); const srcTxids = await xfer.initiateTransfer(source.signer); console.log(`Started Transfer: `, srcTxids); // Step 2: Wait for Circle Attestation (VAA) const timeout = 120 * 1000; // Timeout in milliseconds (120 seconds) console.log('Waiting for Attestation'); const attestIds = await xfer.fetchAttestation(timeout); console.log(`Got Attestation: `, attestIds); // Step 3: Complete the transfer on the destination chain (Sepolia) console.log('Completing Transfer'); const dstTxids = await xfer.completeTransfer(destination.signer); console.log(`Completed Transfer: `, dstTxids); process.exit(0); })();
-
Run the script to execute the transfer:
You will see terminal output similar to the following:
npx tsx transfer.ts Starting Transfer Started Transfer: [ '0xdedbf496a1e658efb15bc57f120122b38a3714a560892be7a8c0a7f23c44aca2', '0x9a8e41837e225edfa62d1913f850c01bd0552e55bf082fd9225df789455a465a' ] Waiting for Attestation Retrying Circle:GetAttestation, attempt 0/60 Retrying Circle:GetAttestation, attempt 1/60 Got Attestation: [{hash: '0x89f8651bf94cfd932ba5bcd2f7795a3fabc6a7c602075fa712c9c55022f5cca8'}] Completing Transfer Completed Transfer: [ '0x9b81bb30d2a68aa2ecc707a8a1b5af63448223a69b2ead6cf6d172ab880ad0c9']
To verify the transaction and view its details, paste the transaction hash into Wormholescan.
Next Steps#
Now that you've completed a CCTP USDC transfer using the Wormhole SDK, you're ready to explore more advanced features and expand your integration:
- Circle CCTP Documentation: Learn how USDC cross-chain transfers work and explore advanced CCTP features.