Wormhole TypeScript SDK Reference#
This page covers all you need to know about the functionality offered through the Wormhole TypeScript SDK.
-
Installation
Find installation instructions for both the meta package and installing specific, individual packages.
-
TSdoc for SDK
Review the TSdoc for the Wormhole TypeScript SDK for a detailed look at available methods, classes, interfaces, and definitions.
-
Source Code
Want to go straight to the source? Check out the TypeScript SDK GitHub repository.
Warning
This package is a work in progress. The interface may change, and there are likely bugs. Please report any issues you find.
Concepts#
Understanding key Wormhole concepts—and how the SDK abstracts them—will help you use the tools more effectively. The following sections cover platforms, chain contexts, addresses, signers, and protocols, explaining their roles in Wormhole and how the SDK simplifies working with them.
Platforms#
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 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 expose key functions and types from the native ecosystem, reducing the need for full packages and keeping dependencies lightweight.
Supported platform modules
Platform | Installation Command |
---|---|
EVM |
|
Solana |
|
Algorand |
|
Aptos |
|
Cosmos |
|
Sui |
|
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#
ChainContext
(from the @wormhole-foundation/sdk-definitions
package) provides a unified interface for interacting with connected chains. It:
- Holds network, chain, and platform configurations
- Caches RPC and protocol clients
- Exposes both platform-inherited and chain-specific methods
- Defines the core types used across the SDK:
Network
,Chain
, andPlatform
// Get the chain context for the source and destination chains
// This is useful to grab direct clients for the protocols
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, standardizing address handling across chains. All addresses are parsed into a 32-byte format. Each platform also defines a NativeAddress
type that understands its native format. These abstractions ensure consistent cross-chain address handling.
// 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#
The TokenId
type identifies any token by its chain and address. For standardized tokens, Wormhole uses the token's contract address. For native currencies (e.g., ETH on Ethereum), it uses the keyword native
. This ensures consistent handling of all tokens.
// Get the TokenId for an ERC-20 token
const sourceToken: TokenId = Wormhole.tokenId('Ethereum', '0xbeef...');
// Get the TokenId for native ETH
const gasToken: TokenId = Wormhole.tokenId('Ethereum', 'native');
// Convert a TokenId back to a string
const strAddress = Wormhole.canonicalAddress(senderAddress); // => '0xbeef...'
Signers#
The SDK's Signer
interface can be implemented as either a SignOnlySigner
or a SignAndSendSigner
, created by wrapping an offline or web wallet:
SignOnlySigner
: Signs and serializes unsigned transactions without broadcasting them. Transactions can be inspected or modified before signing. Serialization is chain-specific. See testing signers (e.g., EVM, Solana) for implementation examples.SignAndSendSigner
: Signs and broadcasts transactions, returning their transaction IDs in order.
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();
These components work together to create, sign, and submit a transaction to the blockchain:
provider
: Connects to the Ethereum or EVM-compatible network, enabling data access and transaction submission.signer
: Represents the account that signs transactions using a private key.Wallet
: Combines provider and signer to create, sign, and send transactions programmatically.
Protocols#
Wormhole is a Generic Message Passing (GMP) protocol with several specialized protocols built on top. Each protocol has platform-specific implementations providing methods to generate transactions or read on-chain state.
Supported protocol modules
Protocol | Installation Command |
---|---|
EVM Core |
|
EVM Token Bridge |
|
EVM CCTP |
|
EVM Portico |
|
EVM TBTC |
|
Solana Core |
|
Solana Token Bridge |
|
Solana CCTP |
|
Solana TBTC |
|
Algorand Core |
|
Algorand Token Bridge |
|
Aptos Core |
|
Aptos Token Bridge |
|
Aptos CCTP |
|
Cosmos Core |
|
Cosmos Token Bridge |
|
Sui Core |
|
Sui Token Bridge |
|
Sui CCTP |
|
Wormhole Core#
The core protocol powers all Wormhole activity by emitting messages containing the emitter address, sequence number, and payload needed for bridging.
Example workflow on Solana Testnet:
- Initialize a Wormhole instance for Solana.
- Obtain a signer and its address.
- Access the core messaging bridge for cross-chain messaging.
-
Prepare a message with:
- Sender's address
- Encoded payload (e.g., "lol")
- Nonce (e.g., 0)
- Consistency level (e.g., 0)
-
Generate, sign, and send the transaction to publish the message.
- Extract the Wormhole message ID from transaction logs for tracking.
- Wait (up to 60s) to receive the Verified Action Approval (VAA) (in
Uint8Array
format) from the Wormhole network. - Prepare and send a verification transaction on the receiving chain using the sender's address and the VAA.
Example workflow
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. Each supported chain has a TokenBridge
client that provides a consistent interface for transferring tokens and handling attestations. While WormholeTransfer
abstractions are recommended, direct interaction with the protocol is also supported.
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);
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#
Token transfers between chains are straightforward using Wormhole. Create a Wormhole
instance and use it to initialize a TokenTransfer
or CircleTransfer
object.
The example below shows how to initiate and complete a TokenTransfer
. After creating the transfer object and retrieving a quote (to verify sufficient amount and fees), the process involves:
- Initiating the transfer on the source chain.
- Waiting for attestation (if required).
- Completing the transfer on the destination chain.
For automatic transfers, the process ends after initiation. Manual transfers require attestation before completion.
// 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.
Native USDC Transfers#
You can transfer native USDC using Circle's CCTP. If the transfer is set to automatic
, the quote will include a relay fee, which is deducted from the total amount sent. For example, to receive 1.0 USDC on the destination chain, the sender must cover both the 1.0 and the relay fee. The same applies when including a native gas drop-off.
In the example below, the wh.circleTransfer
function is used to initiate the transfer. It accepts the amount (in base units), sender and receiver chains and addresses, and an optional automatic flag to enable hands-free completion. You can also include an optional payload (set to undefined
here) and specify a native gas drop-off if desired.
When waiting for the VAA, a timeout of 60,000
milliseconds is used. The actual wait time varies 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 up to the developer and may involve sorting by output amount or estimated completion time (though no estimate is currently provided).
Once a route is selected, parameters like amount
, nativeGasDropoff
, and slippage
can be set. After validation, a transfer quote is requested, including fees, estimated time, and final amount. If successful, the quote is shown to the user for review before proceeding, ensuring all details are verified prior to transfer.
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
.
import { Network, routes } from '@wormhole-foundation/sdk-connect';
export class CustomRoute<N extends Network>
extends routes.Route<N>
implements routes.StaticRouteMethods<typeof CustomRoute>
{
static meta = {
name: 'CustomRoute',
};
// implementation...
}
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.