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
-
Concepts
Understand key concepts and how the SDK abstracts them away. Learn more about platforms, chain context, addresses, and signers
-
Usage
Guidance on using the SDK to add seamless interchain messaging to your application, including code examples
-
TSdoc for SDK
Review the TSdoc for the Wormhole TypeScript SDK for a detailed look at availabel methods, classes, interfaces, and definitions
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:
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-definitions
- exposes contract interfaces, basic types, and VAA payload definitions
sdk-evm-tokenbridge
- exposes the EVM Token Bridge protocol client
Usage#
Getting your integration started is simple. First, import Wormhole:
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:
With a configured Wormhole object, you can do things like parse addresses for the provided platforms, get a ChainContext
object, or fetch VAAs.
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:
- Initiating the transfer on the source chain
- Waiting for the transfer to be attested (if not automatic)
- 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.
Routes as Plugins#
Routes can be imported from any npm package that exports them and configured with the resolver. Custom routes must extend Route
and implement StaticRouteMethods
.
A noteworthy example of a route exported from a separate npm package is Wormhole Native Token Transfers (NTT). See the NttAutomaticRoute
route implementation.
See Also#
The TSdoc is available on GitHub.