Skip to content

Wormhole TypeScript SDK

Introduction

The Wormhole TypeScript SDK is useful for interacting with the chains Wormhole supports and the protocols built on top of Wormhole. This package bundles together functions, definitions, and constants that streamline the process of connecting chains and completing transfers using Wormhole. The SDK also offers targeted sub-packages for Wormhole-connected platforms, which allow you to add multichain support without creating outsized dependencies.

This section covers all you need to know about the functionality and ease of development offered through the Wormhole TypeScript SDK. Take a tour of the package to discover how it helps make integration easier. Learn more about how the SDK abstracts away complexities around concepts like platforms, contexts, and signers. Finally, you'll find guidance on usage, along with code examples, to show you how to use the tools of the SDK.

  • Installation


    Find installation instructions for both the meta package and installing specific, individual packages

    Install the SDK

  • Concepts


    Understand key concepts and how the SDK abstracts them away. Learn more about platforms, chain context, addresses, and signers

    Explore concepts

  • Usage


    Guidance on using the SDK to add seamless interchain messaging to your application, including code examples

    Use the SDK

  • TSdoc for SDK


    Review the TSdoc for the Wormhole TypeScript SDK for a detailed look at availabel methods, classes, interfaces, and definitions

    View the TSdoc on GitHub

Warning

This package is a work in progress. The interface may change, and there are likely bugs. Please report any issues you find.

Installation

Basic

To install the meta package using npm, run the following command in the root directory of your project:

npm install @wormhole-foundation/sdk

This package combines all the individual packages to make setup easier while allowing for tree shaking.

Advanced

Alternatively, you can install a specific set of published packages individually:

sdk-base - exposes constants
npm install @wormhole-foundation/sdk-base
sdk-definitions - exposes contract interfaces, basic types, and VAA payload definitions
npm install @wormhole-foundation/sdk-definitions
sdk-evm - exposes EVM-specific utilities
npm install @wormhole-foundation/sdk-evm
sdk-evm-tokenbridge - exposes the EVM Token Bridge protocol client
npm install @wormhole-foundation/sdk-evm-tokenbridge

Usage

Getting your integration started is simple. First, import Wormhole:

import { wormhole } from '@wormhole-foundation/sdk';

Then, import each of the ecosystem platforms that you wish to support:

import algorand from '@wormhole-foundation/sdk/algorand';
import aptos from '@wormhole-foundation/sdk/aptos';
import cosmwasm from '@wormhole-foundation/sdk/cosmwasm';
import evm from '@wormhole-foundation/sdk/evm';
import solana from '@wormhole-foundation/sdk/solana';
import sui from '@wormhole-foundation/sdk/sui';

To make the platform modules available for use, pass them to the Wormhole constructor:

  const wh = await wormhole('Testnet', [
    evm,
    solana,
    aptos,
    algorand,
    cosmwasm,
    sui,
  ]);

With a configured Wormhole object, you can do things like parse addresses for the provided platforms, get a ChainContext object, or fetch VAAs.

  const ctx = wh.getChain('Solana');

You can retrieve a VAA as follows. In this example, a timeout of 60,000 milliseconds is used. The amount of time required for the VAA to become available will vary by network.

  const vaa = await wh.getVaa(
    // Wormhole Message ID
    whm!,
    // Protocol:Payload name to use for decoding the VAA payload
    'TokenBridge:Transfer',
    // Timeout in milliseconds, depending on the chain and network, the VAA may take some time to be available
    60_000
  );
View the complete script
import { wormhole } from '@wormhole-foundation/sdk';

import { Wormhole, amount, signSendWait } from '@wormhole-foundation/sdk';
import algorand from '@wormhole-foundation/sdk/algorand';
import aptos from '@wormhole-foundation/sdk/aptos';
import cosmwasm from '@wormhole-foundation/sdk/cosmwasm';
import evm from '@wormhole-foundation/sdk/evm';
import solana from '@wormhole-foundation/sdk/solana';
import sui from '@wormhole-foundation/sdk/sui';
import { getSigner } from './helpers/index.js';

(async function () {
  const wh = await wormhole('Testnet', [
    evm,
    solana,
    aptos,
    algorand,
    cosmwasm,
    sui,
  ]);

  const ctx = wh.getChain('Solana');

  const rcv = wh.getChain('Algorand');

  const sender = await getSigner(ctx);
  const receiver = await getSigner(rcv);

  // Get a Token Bridge contract client on the source
  const sndTb = await ctx.getTokenBridge();

  // Send the native token of the source chain
  const tokenId = Wormhole.tokenId(ctx.chain, 'native');

  // Bigint amount using `amount` module
  const amt = amount.units(amount.parse('0.1', ctx.config.nativeTokenDecimals));

  // Create a transaction stream for transfers
  const transfer = sndTb.transfer(
    sender.address.address,
    receiver.address,
    tokenId.address,
    amt
  );

  // Sign and send the transaction
  const txids = await signSendWait(ctx, transfer, sender.signer);
  console.log('Sent: ', txids);

  // Get the Wormhole message ID from the transaction
  const [whm] = await ctx.parseTransaction(txids[txids.length - 1]!.txid);
  console.log('Wormhole Messages: ', whm);

  const vaa = await wh.getVaa(
    // Wormhole Message ID
    whm!,
    // Protocol:Payload name to use for decoding the VAA payload
    'TokenBridge:Transfer',
    // Timeout in milliseconds, depending on the chain and network, the VAA may take some time to be available
    60_000
  );

  // Now get the token bridge on the redeem side
  const rcvTb = await rcv.getTokenBridge();

  // Create a transaction stream for redeeming
  const redeem = rcvTb.redeem(receiver.address.address, vaa!);

  // Sign and send the transaction
  const rcvTxids = await signSendWait(rcv, redeem, receiver.signer);
  console.log('Sent: ', rcvTxids);

  // Now check if the transfer is completed according to
  // the destination token bridge
  const finished = await rcvTb.isTransferCompleted(vaa!);
  console.log('Transfer completed: ', finished);
})();

Optionally, you can override the default configuration with a partial WormholeConfig object to specify particular fields, such as a different RPC endpoint.

const wh = await wormhole('Testnet', [solana], {
  chains: {
    Solana: {
      contracts: {
        coreBridge: '11111111111111111111111111111',
      },
      rpc: 'https://api.devnet.solana.com',
    },
  },
});
View the complete script
import { wormhole } from '@wormhole-foundation/sdk';
import solana from '@wormhole-foundation/sdk/solana';
(async function () {
  const wh = await wormhole('Testnet', [solana], {
    chains: {
      Solana: {
        contracts: {
          coreBridge: '11111111111111111111111111111',
        },
        rpc: 'https://api.devnet.solana.com',
      },
    },
  });
  console.log(wh.config.chains.Solana);
})();

Concepts

Understanding several higher-level Wormhole concepts and how the SDK abstracts them away will help you use the tools most effectively. The following sections will introduce and discuss the concepts of platforms, chain contexts, addresses, signers, and protocols, how they are used in the Wormhole context, and how the SDK helps ease development in each conceptual area.

Platforms

While every chain has unique attributes, chains from the same platform typically have standard functionalities they share. The SDK includes Platform modules, which create a standardized interface for interacting with the chains of a supported platform. The contents of a module vary by platform but can include:

  • Protocols, such as Wormhole core, preconfigured to suit the selected platform
  • Definitions and configurations for types, signers, addresses, and chains
  • Helpers configured for dealing with unsigned transactions on the selected platform

These modules also import and expose essential functions and define types or constants from the chain's native ecosystem to reduce the dependencies needed to interact with a chain using Wormhole. Rather than installing the entire native package for each desired platform, you can install a targeted package of standardized functions and definitions essential to connecting with Wormhole, keeping project dependencies as slim as possible.

Wormhole currently supports the following platforms:

  • EVM
  • Solana
  • Cosmos
  • Sui
  • Aptos
  • Algorand

See the Platforms folder of the TypeScript SDK for an up-to-date list of the platforms supported by the Wormhole TypeScript SDK.

Chain Context

The definitions package of the SDK includes the ChainContext class, which creates an interface for working with connected chains in a standardized way. This class contains the network, chain, and platform configurations for connected chains and cached RPC and protocol clients. The ChainContext class also exposes chain-specific methods and utilities. Much of the functionality comes from the Platform methods but some specific chains may have overridden methods via the context. This is also where the Network, Chain, and Platform type parameters which are used throughout the package are defined.

const srcChain = wh.getChain(senderAddress.chain);
const dstChain = wh.getChain(receiverAddress.chain);

const tb = await srcChain.getTokenBridge(); // => TokenBridge<'Evm'>
srcChain.getRpcClient(); // => RpcClient<'Evm'>

Addresses

The SDK uses the UniversalAddress class to implement the Address interface, which all address types must implement. Addresses from various networks are parsed into their byte representation and modified as needed to ensure they are exactly 32 bytes long. Each platform also has an address type that understands the native address formats, referred to as NativeAddress. These abstractions allow you to work with addresses consistently regardless of the underlying chain.

// It's possible to convert a string address to its Native address
const ethAddr: NativeAddress<'Evm'> = toNative('Ethereum', '0xbeef...');

// A common type in the SDK is the `ChainAddress` which provides
// the additional context of the `Chain` this address is relevant for
const senderAddress: ChainAddress = Wormhole.chainAddress(
  'Ethereum',
  '0xbeef...'
);
const receiverAddress: ChainAddress = Wormhole.chainAddress(
  'Solana',
  'Sol1111...'
);

// Convert the ChainAddress back to its canonical string address format
const strAddress = Wormhole.canonicalAddress(senderAddress); // => '0xbeef...'

// Or if the ethAddr above is for an emitter and you need the UniversalAddress
const emitterAddr = ethAddr.toUniversalAddress().toString();

Tokens

Similar to the ChainAddress type, the TokenId type provides the chain and address of a given token. The following snippet introduces TokenId, a way to uniquely identify any token, whether it's a standard token or a blockchain's native currency (like ETH for Ethereum).

Wormhole uses their contract address to create a TokenId for standard tokens. For native currencies, Wormhole uses the keyword native instead of an address. This makes it easy to work with any type of token consistently.

Finally, the snippet demonstrates how to convert a TokenId back into a regular address format when needed.

const sourceToken: TokenId = Wormhole.tokenId('Ethereum', '0xbeef...');

const gasToken: TokenId = Wormhole.tokenId('Ethereum', 'native');

const strAddress = Wormhole.canonicalAddress(senderAddress); // => '0xbeef...'

Signers

Certain methods of signing transactions require a Signer interface in the SDK. Depending on the specific requirements, this interface can be fulfilled by either a SignOnlySigner or a SignAndSendSigner. A signer can be created by wrapping an offline or web wallet.

A SignOnlySigner is used when the signer isn't connected to the network or prefers not to broadcast transactions themselves. It accepts an array of unsigned transactions and returns an array of signed and serialized transactions. Before signing, the transactions may be inspected or altered. It's important to note that the serialization process is chain-specific. Refer to the testing signers (e.g., EVM or Solana) for an example of how to implement a signer for a specific chain or platform.

Conversely, a SignAndSendSigner is appropriate when the signer is connected to the network and intends to broadcast the transactions. This type of signer also accepts an array of unsigned transactions but returns an array of transaction IDs corresponding to the order of the unsigned transactions.

export type Signer = SignOnlySigner | SignAndSendSigner;

export interface SignOnlySigner {
  chain(): ChainName;
  address(): string;
  // Accept an array of unsigned transactions and return
  // an array of signed and serialized transactions.
  // The transactions may be inspected or altered before
  // signing.
  sign(tx: UnsignedTransaction[]): Promise<SignedTx[]>;
}

export interface SignAndSendSigner {
  chain(): ChainName;
  address(): string;
  // Accept an array of unsigned transactions and return
  // an array of transaction ids in the same order as the
  // unsignedTransactions array.
  signAndSend(tx: UnsignedTransaction[]): Promise<TxHash[]>;
}

Set Up a Signer with Ethers.js

To sign transactions programmatically with the Wormhole SDK, you can use Ethers.js to manage private keys and handle signing. Here's an example of setting up a signer using Ethers.js:

import { ethers } from 'ethers';

// Update the following variables
const rpcUrl = 'INSERT_RPC_URL';
const privateKey = 'INSERT_PRIVATE_KEY';
const toAddress = 'INSERT_RECIPIENT_ADDRESS';

// Set up a provider and signer
const provider = new ethers.JsonRpcProvider(rpcUrl);
const signer = new ethers.Wallet(privateKey, provider);

// Example: Signing and sending a transaction
async function sendTransaction() {
  const tx = {
    to: toAddress,
    value: ethers.parseUnits('0.1'), // Sending 0.1 ETH
    gasPrice: await provider.getGasPrice(),
    gasLimit: ethers.toBeHex(21000),
  };

  const transaction = await signer.sendTransaction(tx);
  console.log('Transaction hash:', transaction.hash);
}
sendTransaction();
  • provider - responsible for connecting to the Ethereum network (or any EVM-compatible network). It acts as a bridge between your application and the blockchain, allowing you to fetch data, check the state of the blockchain, and submit transactions

  • signer - represents the account that will sign the transaction. In this case, you’re creating a signer using the private key associated with the account. The signer is responsible for authorizing transactions by digitally signing them with the private key

  • Wallet - combines both the provider (for blockchain interaction) and the signer (for transaction authorization), allowing you to sign and send transactions programmatically

These components work together to create, sign, and submit a transaction to the blockchain.

Managing Private Keys Securely

Handling private keys is unavoidable, so it’s crucial to manage them securely. Here are some best practices:

  • Use environment variables - avoid hardcoding private keys in your code. Use environment variables or secret management tools to inject private keys securely
  • Hardware wallets - for production environments, consider integrating hardware wallets to keep private keys secure while allowing programmatic access through the SDK

Protocols

While Wormhole is a Generic Message Passing (GMP) protocol, several protocols have been built to provide specific functionality. If available, each protocol will have a platform-specific implementation. These implementations provide methods to generate transactions or read state from the contract on-chain.

Wormhole Core

The core protocol underlies all Wormhole activity. This protocol is responsible for emitting the message containing the information necessary to perform bridging, including the emitter address, the sequence number for the message, and the payload of the message itself.

The following example demonstrates sending and verifying a message using the Wormhole Core protocol on Solana.

First, initialize a Wormhole instance for the Testnet environment, specifically for the Solana chain. Then, obtain a signer and its associated address, which will be used to sign transactions.

Next, get a reference to the core messaging bridge, which is the main interface for interacting with Wormhole's cross-chain messaging capabilities. The code then prepares a message for publication. This message includes:

  • The sender's address
  • The message payload (in this case, the encoded string lol)
  • A nonce (set to 0 here, but can be any user-defined value to uniquely identify the message)
  • A consistency level (set to 0, which determines the finality requirements for the message)

After preparing the message, the next steps are to generate, sign, and send the transaction or transactions required to publish the message on the Solana blockchain. Once the transaction is confirmed, the Wormhole message ID is extracted from the transaction logs. This ID is crucial for tracking the message across chains.

The code then waits for the Wormhole network to process and sign the message, turning it into a Verified Action Approval (VAA). This VAA is retrieved in a Uint8Array format, with a timeout of 60 seconds.

Lastly, the code will demonstrate how to verify the message on the receiving end. A verification transaction is prepared using the original sender's address and the VAA, and finally, this transaction is signed and sent.

View the complete script
import { encoding, signSendWait, wormhole } from '@wormhole-foundation/sdk';
import { getSigner } from './helpers/index.js';
import solana from '@wormhole-foundation/sdk/solana';
import evm from '@wormhole-foundation/sdk/evm';

(async function () {
  const wh = await wormhole('Testnet', [solana, evm]);

  const chain = wh.getChain('Avalanche');
  const { signer, address } = await getSigner(chain);

  // Get a reference to the core messaging bridge
  const coreBridge = await chain.getWormholeCore();

  // Generate transactions, sign and send them
  const publishTxs = coreBridge.publishMessage(
    // Address of sender (emitter in VAA)
    address.address,
    // Message to send (payload in VAA)
    encoding.bytes.encode('lol'),
    // Nonce (user defined, no requirement for a specific value, useful to provide a unique identifier for the message)
    0,
    // ConsistencyLevel (ie finality of the message, see wormhole docs for more)
    0
  );
  // Send the transaction(s) to publish the message
  const txids = await signSendWait(chain, publishTxs, signer);

  // Take the last txid in case multiple were sent
  // The last one should be the one containing the relevant
  // event or log info
  const txid = txids[txids.length - 1];

  // Grab the wormhole message id from the transaction logs or storage
  const [whm] = await chain.parseTransaction(txid!.txid);

  // Wait for the vaa to be signed and available with a timeout
  const vaa = await wh.getVaa(whm!, 'Uint8Array', 60_000);
  console.log(vaa);

  // Note: calling verifyMessage manually is typically not a useful thing to do
  // As the VAA is typically submitted to the counterpart contract for
  // A given protocol and the counterpart contract will verify the VAA
  // This is simply for demo purposes
  const verifyTxs = coreBridge.verifyMessage(address.address, vaa!);
  console.log(await signSendWait(chain, verifyTxs, signer));
})();

The payload contains the information necessary to perform whatever action is required based on the protocol that uses it.

Token Bridge

The most familiar protocol built on Wormhole is the Token Bridge. Every chain has a TokenBridge protocol client that provides a consistent interface for interacting with the Token Bridge, which includes methods to generate the transactions required to transfer tokens and methods to generate and redeem attestations. WormholeTransfer abstractions are the recommended way to interact with these protocols, but it is possible to use them directly.

import { signSendWait } from '@wormhole-foundation/sdk';

const tb = await srcChain.getTokenBridge(); 

const token = '0xdeadbeef...';
const txGenerator = tb.createAttestation(token); 
const txids = await signSendWait(srcChain, txGenerator, src.signer);

Supported protocols are defined in the definitions module.

Transfers

While using the ChainContext and Protocol clients directly is possible, the SDK provides some helpful abstractions for transferring tokens.

The WormholeTransfer interface provides a convenient abstraction to encapsulate the steps involved in a cross-chain transfer.

Token Transfers

Performing a token transfer is trivial for any source and destination chains. You can create a new Wormhole object to make objects like TokenTransfer and CircleTransfer, to transfer tokens between chains.

The following example demonstrates the process of initiating and completing a token transfer. It starts by creating a TokenTransfer object, which tracks the transfer's state throughout its lifecycle. The code then obtains a quote for the transfer, ensuring the amount is sufficient to cover fees and any requested native gas.

The transfer process is divided into three main steps:

  1. Initiating the transfer on the source chain
  2. Waiting for the transfer to be attested (if not automatic)
  3. Completing the transfer on the destination chain

For automatic transfers, the process ends after initiation. The code waits for the transfer to be attested for manual transfers and then completes it on the destination chain.

  // Create a TokenTransfer object to track the state of the transfer over time
  const xfer = await wh.tokenTransfer(
    route.token,
    route.amount,
    route.source.address,
    route.destination.address,
    route.delivery?.automatic ?? false,
    route.payload,
    route.delivery?.nativeGas
  );

  const quote = await TokenTransfer.quoteTransfer(
    wh,
    route.source.chain,
    route.destination.chain,
    xfer.transfer
  );
  console.log(quote);

  if (xfer.transfer.automatic && quote.destinationToken.amount < 0)
    throw 'The amount requested is too low to cover the fee and any native gas requested.';

  // 1) Submit the transactions to the source chain, passing a signer to sign any txns
  console.log('Starting transfer');
  const srcTxids = await xfer.initiateTransfer(route.source.signer);
  console.log(`Started transfer: `, srcTxids);

  // If automatic, we're done
  if (route.delivery?.automatic) return xfer;

  // 2) Wait for the VAA to be signed and ready (not required for auto transfer)
  console.log('Getting Attestation');
  const attestIds = await xfer.fetchAttestation(60_000);
  console.log(`Got Attestation: `, attestIds);

  // 3) Redeem the VAA on the dest chain
  console.log('Completing Transfer');
  const destTxids = await xfer.completeTransfer(route.destination.signer);
  console.log(`Completed Transfer: `, destTxids);
View the complete script
import {
  Chain,
  Network,
  TokenId,
  TokenTransfer,
  Wormhole,
  amount,
  isTokenId,
  wormhole,
} from '@wormhole-foundation/sdk';

import evm from '@wormhole-foundation/sdk/evm';
import solana from '@wormhole-foundation/sdk/solana';
import { SignerStuff, getSigner, waitLog } from './helpers/index.js';

(async function () {
  // Init Wormhole object, passing config for which network
  // to use (e.g. Mainnet/Testnet) and what Platforms to support
  const wh = await wormhole('Testnet', [evm, solana]);

  // Grab chain Contexts -- these hold a reference to a cached rpc client
  const sendChain = wh.getChain('Avalanche');
  const rcvChain = wh.getChain('Solana');

  // Shortcut to allow transferring native gas token
  const token = Wormhole.tokenId(sendChain.chain, 'native');

  // A TokenId is just a `{chain, address}` pair and an alias for ChainAddress
  // The `address` field must be a parsed address.
  // You can get a TokenId (or ChainAddress) prepared for you
  // by calling the static `chainAddress` method on the Wormhole class.
  // e.g.
  // wAvax on Solana
  // const token = Wormhole.tokenId("Solana", "3Ftc5hTz9sG4huk79onufGiebJNDMZNL8HYgdMJ9E7JR");
  // wSol on Avax
  // const token = Wormhole.tokenId("Avalanche", "0xb10563644a6AB8948ee6d7f5b0a1fb15AaEa1E03");

  // Normalized given token decimals later but can just pass bigints as base units
  // Note: The Token bridge will dedust past 8 decimals
  // This means any amount specified past that point will be returned
  // To the caller
  const amt = '0.05';

  // With automatic set to true, perform an automatic transfer. This will invoke a relayer
  // Contract intermediary that knows to pick up the transfers
  // With automatic set to false, perform a manual transfer from source to destination
  // Of the token
  // On the destination side, a wrapped version of the token will be minted
  // To the address specified in the transfer VAA
  const automatic = false;

  // The Wormhole relayer has the ability to deliver some native gas funds to the destination account
  // The amount specified for native gas will be swapped for the native gas token according
  // To the swap rate provided by the contract, denominated in native gas tokens
  const nativeGas = automatic ? '0.01' : undefined;

  // Get signer from local key but anything that implements
  // Signer interface (e.g. wrapper around web wallet) should work
  const source = await getSigner(sendChain);
  const destination = await getSigner(rcvChain);

  // Used to normalize the amount to account for the tokens decimals
  const decimals = isTokenId(token)
    ? Number(await wh.getDecimals(token.chain, token.address))
    : sendChain.config.nativeTokenDecimals;

  // Set this to true if you want to perform a round trip transfer
  const roundTrip: boolean = false;

  // Set this to the transfer txid of the initiating transaction to recover a token transfer
  // And attempt to fetch details about its progress.
  let recoverTxid = undefined;

  // Finally create and perform the transfer given the parameters set above
  const xfer = !recoverTxid
    ? // Perform the token transfer
      await tokenTransfer(
        wh,
        {
          token,
          amount: amount.units(amount.parse(amt, decimals)),
          source,
          destination,
          delivery: {
            automatic,
            nativeGas: nativeGas
              ? amount.units(amount.parse(nativeGas, decimals))
              : undefined,
          },
        },
        roundTrip
      )
    : // Recover the transfer from the originating txid
      await TokenTransfer.from(wh, {
        chain: source.chain.chain,
        txid: recoverTxid,
      });

  const receipt = await waitLog(wh, xfer);

  // Log out the results
  console.log(receipt);
})();

async function tokenTransfer<N extends Network>(
  wh: Wormhole<N>,
  route: {
    token: TokenId;
    amount: bigint;
    source: SignerStuff<N, Chain>;
    destination: SignerStuff<N, Chain>;
    delivery?: {
      automatic: boolean;
      nativeGas?: bigint;
    };
    payload?: Uint8Array;
  },
  roundTrip?: boolean
): Promise<TokenTransfer<N>> {
  // Create a TokenTransfer object to track the state of the transfer over time
  const xfer = await wh.tokenTransfer(
    route.token,
    route.amount,
    route.source.address,
    route.destination.address,
    route.delivery?.automatic ?? false,
    route.payload,
    route.delivery?.nativeGas
  );

  const quote = await TokenTransfer.quoteTransfer(
    wh,
    route.source.chain,
    route.destination.chain,
    xfer.transfer
  );
  console.log(quote);

  if (xfer.transfer.automatic && quote.destinationToken.amount < 0)
    throw 'The amount requested is too low to cover the fee and any native gas requested.';

  // 1) Submit the transactions to the source chain, passing a signer to sign any txns
  console.log('Starting transfer');
  const srcTxids = await xfer.initiateTransfer(route.source.signer);
  console.log(`Started transfer: `, srcTxids);

  // If automatic, we're done
  if (route.delivery?.automatic) return xfer;

  // 2) Wait for the VAA to be signed and ready (not required for auto transfer)
  console.log('Getting Attestation');
  const attestIds = await xfer.fetchAttestation(60_000);
  console.log(`Got Attestation: `, attestIds);

  // 3) Redeem the VAA on the dest chain
  console.log('Completing Transfer');
  const destTxids = await xfer.completeTransfer(route.destination.signer);
  console.log(`Completed Transfer: `, destTxids);

  // If no need to send back, dip
  if (!roundTrip) return xfer;

  const { destinationToken: token } = quote;
  return await tokenTransfer(wh, {
    ...route,
    token: token.token,
    amount: token.amount,
    source: route.destination,
    destination: route.source,
  });
}

Internally, this uses the TokenBridge protocol client to transfer tokens. Like other Protocols, the TokenBridge protocol provides a consistent set of methods across all chains to generate a set of transactions for that specific chain.

Native USDC Transfers

You can also transfer native USDC using Circle's CCTP. Please note that if the transfer is set to Automatic mode, a fee for performing the relay will be included in the quote. This fee is deducted from the total amount requested to be sent. For example, if the user wishes to receive 1.0 on the destination, the amount sent should be adjusted to 1.0 plus the relay fee. The same principle applies to native gas drop offs.

In the following example, the wh.circleTransfer function is called with several parameters to set up the transfer. It takes the amount to be transferred (in the token's base units), the sender's chain and address, and the receiver's chain and address. The function also allows specifying whether the transfer should be automatic, meaning it will be completed without further user intervention.

An optional payload can be included with the transfer, though it's set to undefined in this case . Finally, if the transfer is automatic, you can request that native gas (the blockchain's native currency used for transaction fees) be sent to the receiver along with the transferred tokens.

When waiting for the VAA, a timeout of 60,000 milliseconds is used. The amount of time required for the VAA to become available will vary by network.

  const xfer = await wh.circleTransfer(
    // Amount as bigint (base units)
    req.amount,
    // Sender chain/address
    src.address,
    // Receiver chain/address
    dst.address,
    // Automatic delivery boolean
    req.automatic,
    // Payload to be sent with the transfer
    undefined,
    // If automatic, native gas can be requested to be sent to the receiver
    req.nativeGas
  );

  // Note, if the transfer is requested to be Automatic, a fee for performing the relay
  // will be present in the quote. The fee comes out of the amount requested to be sent.
  // If the user wants to receive 1.0 on the destination, the amount to send should be 1.0 + fee.
  // The same applies for native gas dropoff
  const quote = await CircleTransfer.quoteTransfer(
    src.chain,
    dst.chain,
    xfer.transfer
  );
  console.log('Quote', quote);

  console.log('Starting Transfer');
  const srcTxids = await xfer.initiateTransfer(src.signer);
  console.log(`Started Transfer: `, srcTxids);

  if (req.automatic) {
    const relayStatus = await waitForRelay(srcTxids[srcTxids.length - 1]!);
    console.log(`Finished relay: `, relayStatus);
    return;
  }

  console.log('Waiting for Attestation');
  const attestIds = await xfer.fetchAttestation(60_000);
  console.log(`Got Attestation: `, attestIds);

  console.log('Completing Transfer');
  const dstTxids = await xfer.completeTransfer(dst.signer);
  console.log(`Completed Transfer: `, dstTxids);
}
View the complete script
import {
  Chain,
  CircleTransfer,
  Network,
  Signer,
  TransactionId,
  TransferState,
  Wormhole,
  amount,
  wormhole,
} from '@wormhole-foundation/sdk';
import evm from '@wormhole-foundation/sdk/evm';
import solana from '@wormhole-foundation/sdk/solana';
import { SignerStuff, getSigner, waitForRelay } from './helpers/index.js';

/*
Notes:
Only a subset of chains are supported by Circle for CCTP, see core/base/src/constants/circle.ts for currently supported chains

AutoRelayer takes a 0.1 USDC fee when transferring to any chain beside Goerli, which is 1 USDC
*/
//

(async function () {
  // Init the Wormhole object, passing in the config for which network
  // to use (e.g. Mainnet/Testnet) and what Platforms to support
  const wh = await wormhole('Testnet', [evm, solana]);

  // Grab chain Contexts
  const sendChain = wh.getChain('Avalanche');
  const rcvChain = wh.getChain('Solana');

  // Get signer from local key but anything that implements
  // Signer interface (e.g. wrapper around web wallet) should work
  const source = await getSigner(sendChain);
  const destination = await getSigner(rcvChain);

  // 6 decimals for USDC (except for BSC, so check decimals before using this)
  const amt = amount.units(amount.parse('0.2', 6));

  // Choose whether or not to have the attestation delivered for you
  const automatic = false;

  // If the transfer is requested to be automatic, you can also request that
  // during redemption, the receiver gets some amount of native gas transferred to them
  // so that they may pay for subsequent transactions
  // The amount specified here is denominated in the token being transferred (USDC here)
  const nativeGas = automatic ? amount.units(amount.parse('0.0', 6)) : 0n;

  await cctpTransfer(wh, source, destination, {
    amount: amt,
    automatic,
    nativeGas,
  });

})();

async function cctpTransfer<N extends Network>(
  wh: Wormhole<N>,
  src: SignerStuff<N, any>,
  dst: SignerStuff<N, any>,
  req: {
    amount: bigint;
    automatic: boolean;
    nativeGas?: bigint;
  }
) {

  const xfer = await wh.circleTransfer(
    // Amount as bigint (base units)
    req.amount,
    // Sender chain/address
    src.address,
    // Receiver chain/address
    dst.address,
    // Automatic delivery boolean
    req.automatic,
    // Payload to be sent with the transfer
    undefined,
    // If automatic, native gas can be requested to be sent to the receiver
    req.nativeGas
  );

  // Note, if the transfer is requested to be Automatic, a fee for performing the relay
  // will be present in the quote. The fee comes out of the amount requested to be sent.
  // If the user wants to receive 1.0 on the destination, the amount to send should be 1.0 + fee.
  // The same applies for native gas dropoff
  const quote = await CircleTransfer.quoteTransfer(
    src.chain,
    dst.chain,
    xfer.transfer
  );
  console.log('Quote', quote);

  console.log('Starting Transfer');
  const srcTxids = await xfer.initiateTransfer(src.signer);
  console.log(`Started Transfer: `, srcTxids);

  if (req.automatic) {
    const relayStatus = await waitForRelay(srcTxids[srcTxids.length - 1]!);
    console.log(`Finished relay: `, relayStatus);
    return;
  }

  console.log('Waiting for Attestation');
  const attestIds = await xfer.fetchAttestation(60_000);
  console.log(`Got Attestation: `, attestIds);

  console.log('Completing Transfer');
  const dstTxids = await xfer.completeTransfer(dst.signer);
  console.log(`Completed Transfer: `, dstTxids);
}

export async function completeTransfer(
  wh: Wormhole<Network>,
  txid: TransactionId,
  signer: Signer
): Promise<void> {

  const xfer = await CircleTransfer.from(wh, txid);

  const attestIds = await xfer.fetchAttestation(60 * 60 * 1000);
  console.log('Got attestation: ', attestIds);

  const dstTxIds = await xfer.completeTransfer(signer);
  console.log('Completed transfer: ', dstTxIds);
}

Recovering Transfers

It may be necessary to recover an abandoned transfer before it is completed. To do this, instantiate the Transfer class with the from static method and pass one of several types of identifiers. A TransactionId or WormholeMessageId may be used to recover the transfer.

  const xfer = await CircleTransfer.from(wh, txid);

  const attestIds = await xfer.fetchAttestation(60 * 60 * 1000);
  console.log('Got attestation: ', attestIds);

  const dstTxIds = await xfer.completeTransfer(signer);
  console.log('Completed transfer: ', dstTxIds);
View the complete script
import {
  Chain,
  CircleTransfer,
  Network,
  Signer,
  TransactionId,
  TransferState,
  Wormhole,
  amount,
  wormhole,
} from '@wormhole-foundation/sdk';
import evm from '@wormhole-foundation/sdk/evm';
import solana from '@wormhole-foundation/sdk/solana';
import { SignerStuff, getSigner, waitForRelay } from './helpers/index.js';

/*
Notes:
Only a subset of chains are supported by Circle for CCTP, see core/base/src/constants/circle.ts for currently supported chains

AutoRelayer takes a 0.1 USDC fee when transferring to any chain beside Goerli, which is 1 USDC
*/
//

(async function () {
  // Init the Wormhole object, passing in the config for which network
  // to use (e.g. Mainnet/Testnet) and what Platforms to support
  const wh = await wormhole('Testnet', [evm, solana]);

  // Grab chain Contexts
  const sendChain = wh.getChain('Avalanche');
  const rcvChain = wh.getChain('Solana');

  // Get signer from local key but anything that implements
  // Signer interface (e.g. wrapper around web wallet) should work
  const source = await getSigner(sendChain);
  const destination = await getSigner(rcvChain);

  // 6 decimals for USDC (except for BSC, so check decimals before using this)
  const amt = amount.units(amount.parse('0.2', 6));

  // Choose whether or not to have the attestation delivered for you
  const automatic = false;

  // If the transfer is requested to be automatic, you can also request that
  // during redemption, the receiver gets some amount of native gas transferred to them
  // so that they may pay for subsequent transactions
  // The amount specified here is denominated in the token being transferred (USDC here)
  const nativeGas = automatic ? amount.units(amount.parse('0.0', 6)) : 0n;

  await cctpTransfer(wh, source, destination, {
    amount: amt,
    automatic,
    nativeGas,
  });

})();

async function cctpTransfer<N extends Network>(
  wh: Wormhole<N>,
  src: SignerStuff<N, any>,
  dst: SignerStuff<N, any>,
  req: {
    amount: bigint;
    automatic: boolean;
    nativeGas?: bigint;
  }
) {

  const xfer = await wh.circleTransfer(
    // Amount as bigint (base units)
    req.amount,
    // Sender chain/address
    src.address,
    // Receiver chain/address
    dst.address,
    // Automatic delivery boolean
    req.automatic,
    // Payload to be sent with the transfer
    undefined,
    // If automatic, native gas can be requested to be sent to the receiver
    req.nativeGas
  );

  // Note, if the transfer is requested to be Automatic, a fee for performing the relay
  // will be present in the quote. The fee comes out of the amount requested to be sent.
  // If the user wants to receive 1.0 on the destination, the amount to send should be 1.0 + fee.
  // The same applies for native gas dropoff
  const quote = await CircleTransfer.quoteTransfer(
    src.chain,
    dst.chain,
    xfer.transfer
  );
  console.log('Quote', quote);

  console.log('Starting Transfer');
  const srcTxids = await xfer.initiateTransfer(src.signer);
  console.log(`Started Transfer: `, srcTxids);

  if (req.automatic) {
    const relayStatus = await waitForRelay(srcTxids[srcTxids.length - 1]!);
    console.log(`Finished relay: `, relayStatus);
    return;
  }

  console.log('Waiting for Attestation');
  const attestIds = await xfer.fetchAttestation(60_000);
  console.log(`Got Attestation: `, attestIds);

  console.log('Completing Transfer');
  const dstTxids = await xfer.completeTransfer(dst.signer);
  console.log(`Completed Transfer: `, dstTxids);
}

export async function completeTransfer(
  wh: Wormhole<Network>,
  txid: TransactionId,
  signer: Signer
): Promise<void> {

  const xfer = await CircleTransfer.from(wh, txid);

  const attestIds = await xfer.fetchAttestation(60 * 60 * 1000);
  console.log('Got attestation: ', attestIds);

  const dstTxIds = await xfer.completeTransfer(signer);
  console.log('Completed transfer: ', dstTxIds);
}

Routes

While a specific WormholeTransfer, such as TokenTransfer or CCTPTransfer, may be used, the developer must know exactly which transfer type to use for a given request.

To provide a more flexible and generic interface, the Wormhole class provides a method to produce a RouteResolver that can be configured with a set of possible routes to be supported.

The following section demonstrates setting up and validating a token transfer using Wormhole's routing system.

  // Create new resolver, passing the set of routes to consider
  const resolver = wh.resolver([
    routes.TokenBridgeRoute, // manual token bridge
    routes.AutomaticTokenBridgeRoute, // automatic token bridge
    routes.CCTPRoute, // manual CCTP
    routes.AutomaticCCTPRoute, // automatic CCTP
    routes.AutomaticPorticoRoute, // Native eth transfers
  ]);

Once created, the resolver can be used to provide a list of input and possible output tokens.

  // What tokens are available on the source chain?
  const srcTokens = await resolver.supportedSourceTokens(sendChain);
  console.log(
    'Allowed source tokens: ',
    srcTokens.map((t) => canonicalAddress(t))
  );

  const sendToken = Wormhole.tokenId(sendChain.chain, 'native');

  // Given the send token, what can we possibly get on the destination chain?
  const destTokens = await resolver.supportedDestinationTokens(
    sendToken,
    sendChain,
    destChain
  );
  console.log(
    'For the given source token and routes configured, the following tokens may be receivable: ',
    destTokens.map((t) => canonicalAddress(t))
  );
  // Grab the first one for the example
  const destinationToken = destTokens[0]!;

Once the tokens are selected, a RouteTransferRequest may be created to provide a list of routes that can fulfill the request. Creating a transfer request fetches the token details since all routes will need to know about the tokens.

  // Creating a transfer request fetches token details
  // Since all routes will need to know about the tokens
  const tr = await routes.RouteTransferRequest.create(wh, {
    source: sendToken,
    destination: destinationToken,
  });

  // Resolve the transfer request to a set of routes that can perform it
  const foundRoutes = await resolver.findRoutes(tr);
  console.log(
    'For the transfer parameters, we found these routes: ',
    foundRoutes
  );

Choosing the best route is currently left to the developer, but strategies might include sorting by output amount or expected time to complete the transfer (no estimate is currently provided).

After choosing the best route, extra parameters like amount, nativeGasDropoff, and slippage can be passed, depending on the specific route selected. A quote can be retrieved with the validated request.

After successful validation, the code requests a transfer quote. This quote likely includes important details such as fees, estimated time, and the final amount to be received. If the quote is generated successfully, it's displayed for the user to review and decide whether to proceed with the transfer. This process ensures that all transfer details are properly set up and verified before any actual transfer occurs.

  console.log(
    'This route offers the following default options',
    bestRoute.getDefaultOptions()
  );

  // Specify the amount as a decimal string
  const amt = '0.001';
  // Create the transfer params for this request
  const transferParams = { amount: amt, options: { nativeGas: 0 } };

  // Validate the transfer params passed, this returns a new type of ValidatedTransferParams
  // which (believe it or not) is a validated version of the input params
  // This new var must be passed to the next step, quote
  const validated = await bestRoute.validate(tr, transferParams);
  if (!validated.valid) throw validated.error;
  console.log('Validated parameters: ', validated.params);

  // Get a quote for the transfer, this too returns a new type that must
  // be passed to the next step, execute (if you like the quote)
  const quote = await bestRoute.quote(tr, validated.params);
  if (!quote.success) throw quote.error;
  console.log('Best route quote: ', quote);

Finally, assuming the quote looks good, the route can initiate the request with the quote and the signer.

    const receipt = await bestRoute.initiate(
      tr,
      sender.signer,
      quote,
      receiver.address
    );
    console.log('Initiated transfer with receipt: ', receipt);
View the complete script
import {
  Wormhole,
  canonicalAddress,
  routes,
  wormhole,
} from '@wormhole-foundation/sdk';

import evm from '@wormhole-foundation/sdk/evm';
import solana from '@wormhole-foundation/sdk/solana';
import { getSigner } from './helpers/index.js';

(async function () {
  // Setup
  const wh = await wormhole('Testnet', [evm, solana]);

  // Get chain contexts
  const sendChain = wh.getChain('Avalanche');
  const destChain = wh.getChain('Solana');

  // Get signers from local config
  const sender = await getSigner(sendChain);
  const receiver = await getSigner(destChain);

  // Create new resolver, passing the set of routes to consider
  const resolver = wh.resolver([
    routes.TokenBridgeRoute, // manual token bridge
    routes.AutomaticTokenBridgeRoute, // automatic token bridge
    routes.CCTPRoute, // manual CCTP
    routes.AutomaticCCTPRoute, // automatic CCTP
    routes.AutomaticPorticoRoute, // Native eth transfers
  ]);

  // What tokens are available on the source chain?
  const srcTokens = await resolver.supportedSourceTokens(sendChain);
  console.log(
    'Allowed source tokens: ',
    srcTokens.map((t) => canonicalAddress(t))
  );

  const sendToken = Wormhole.tokenId(sendChain.chain, 'native');

  // Given the send token, what can we possibly get on the destination chain?
  const destTokens = await resolver.supportedDestinationTokens(
    sendToken,
    sendChain,
    destChain
  );
  console.log(
    'For the given source token and routes configured, the following tokens may be receivable: ',
    destTokens.map((t) => canonicalAddress(t))
  );
  // Grab the first one for the example
  const destinationToken = destTokens[0]!;

  // Creating a transfer request fetches token details
  // Since all routes will need to know about the tokens
  const tr = await routes.RouteTransferRequest.create(wh, {
    source: sendToken,
    destination: destinationToken,
  });

  // Resolve the transfer request to a set of routes that can perform it
  const foundRoutes = await resolver.findRoutes(tr);
  console.log(
    'For the transfer parameters, we found these routes: ',
    foundRoutes
  );

  const bestRoute = foundRoutes[0]!;
  console.log('Selected: ', bestRoute);

  console.log(
    'This route offers the following default options',
    bestRoute.getDefaultOptions()
  );

  // Specify the amount as a decimal string
  const amt = '0.001';
  // Create the transfer params for this request
  const transferParams = { amount: amt, options: { nativeGas: 0 } };

  // Validate the transfer params passed, this returns a new type of ValidatedTransferParams
  // which (believe it or not) is a validated version of the input params
  // This new var must be passed to the next step, quote
  const validated = await bestRoute.validate(tr, transferParams);
  if (!validated.valid) throw validated.error;
  console.log('Validated parameters: ', validated.params);

  // Get a quote for the transfer, this too returns a new type that must
  // be passed to the next step, execute (if you like the quote)
  const quote = await bestRoute.quote(tr, validated.params);
  if (!quote.success) throw quote.error;
  console.log('Best route quote: ', quote);

  // If you're sure you want to do this, set this to true
  const imSure = false;
  if (imSure) {
    // Now the transfer may be initiated
    // A receipt will be returned, guess what you gotta do with that?
    const receipt = await bestRoute.initiate(
      tr,
      sender.signer,
      quote,
      receiver.address
    );
    console.log('Initiated transfer with receipt: ', receipt);

    // Kick off a wait log, if there is an opportunity to complete, this function will do it
    // See the implementation for how this works
    await routes.checkAndCompleteTransfer(bestRoute, receipt, receiver.signer);
  } else {
    console.log('Not initiating transfer (set `imSure` to true to do so)');
  }
})();

See the router.ts example in the examples directory for a full working example.

See Also

The TSdoc is available on GitHub.

Got any questions?

Find out more