Skip to content

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:

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

  1. Create a new directory and initialize a Node.js project:

    mkdir cctp-bridge
    cd cctp-bridge
    npm init -y
    
  2. Install the required dependencies:

    npm install @wormhole-foundation/sdk
    npm install -D tsx typescript
    
  3. Create a transfer.ts file to handle the multichain transfer logic and a helper.ts file to manage wallet signers:

    touch transfer.ts helper.ts
    
  4. 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 like cast 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:

  1. In helper.ts, define functions to load private keys and instantiate EVM signers:

    helper.ts
    import {
      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()),
      };
    }
    
  2. In transfer.ts, add the script to perform the manual transfer using CCTP:

    transfer.ts
    import { 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);
    })();
    
  3. Run the script to execute the transfer:

    npx tsx transfer.ts
    

    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: