Portal Bridge#
Introduction#
This tutorial guides you through building a cross-chain token transfer application using the Wormhole TypeScript SDK and its Portal Bridge method. The Portal Bridge method enables secure and efficient cross-chain asset transfers across different blockchain networks, allowing users to move tokens seamlessly.
By leveraging Wormhole’s Portal Bridge, this guide shows you how to build an application that supports multiple transfer types:
- EVM to EVM (e.g., Ethereum to Avalanche)
- EVM to non-EVM chains (e.g., Ethereum to Solana)
- Non-EVM to EVM chains (e.g., Sui to Avalanche)
- Non-EVM to non-EVM chains (e.g., Solana to Sui)
Existing solutions for cross-chain transfers can be complex and inefficient, requiring multiple steps and transaction fees. However, the Portal Bridge method from Wormhole simplifies the process by handling the underlying attestation, transaction validation, and message passing across blockchains.
At the end of this guide, you’ll have a fully functional setup for transferring assets across chains using Wormhole’s Portal Bridge method.
Prerequisites#
Before you begin, ensure you have the following:
- Node.js and npm installed on your machine
- TypeScript installed globally
- Native tokens (testnet or mainnet) in Solana and Sui wallets
- A wallet with a private key, funded with native tokens (testnet or mainnet) for gas fees
Supported Chains#
The Wormhole SDK supports a wide range of EVM and non-EVM chains, allowing you to facilitate cross-chain transfers efficiently. You can find a complete list of supported chains in the Wormhole SDK GitHub repository, which covers both testnet and mainnet environments.
Project Setup#
In this section, we’ll guide you through initializing the project, installing dependencies, and preparing your environment for cross-chain transfers.
-
Initialize the project - start by creating a new directory for your project and initializing it with
npm
, which will create thepackage.json
file for your project -
Install dependencies - install the required dependencies, including the Wormhole SDK and helper libraries
-
Set up environment variables - to securely store your private key, create a
.env
file in the root of your projectInside the
.env
file, add your private keys.ETH_PRIVATE_KEY="INSERT_YOUR_PRIVATE_KEY" SOL_PRIVATE_KEY="INSERT_YOUR_PRIVATE_KEY" SUI_PRIVATE_KEY="INSERT_SUI_MNEMONIC"
Note
Ensure your private key contains native tokens for gas on both the source and destination chains. For Sui, you must provide a mnemonic instead of a private key.
-
Create a
helpers.ts
file - to simplify the interaction between chains, create a file to store utility functions for fetching your private key, set up signers for different chains, and manage transaction relays-
Create the helpers file
-
Open the
helpers.ts
file and add the following codeimport { ChainAddress, ChainContext, Network, Signer, Wormhole, Chain, TokenId, isTokenId, } from '@wormhole-foundation/sdk'; import evm from '@wormhole-foundation/sdk/evm'; import solana from '@wormhole-foundation/sdk/solana'; import sui from '@wormhole-foundation/sdk/sui'; import { config } from 'dotenv'; config(); export interface SignerStuff<N extends Network, C extends Chain> { chain: ChainContext<N, C>; signer: Signer<N, C>; address: ChainAddress<C>; } // Function to fetch environment variables (like your private key) function getEnv(key: string): string { const val = process.env[key]; if (!val) throw new Error(`Missing environment variable: ${key}`); return val; } // Signer setup function for different blockchain platforms export async function getSigner<N extends Network, C extends Chain>( chain: ChainContext<N, C> ): Promise<{ chain: ChainContext<N, C>; signer: Signer<N, C>; address: ChainAddress<C>; }> { let signer: Signer; const platform = chain.platform.utils()._platform; switch (platform) { case 'Solana': signer = await ( await solana() ).getSigner(await chain.getRpc(), getEnv('SOL_PRIVATE_KEY')); break; case 'Evm': signer = await ( await evm() ).getSigner(await chain.getRpc(), getEnv('ETH_PRIVATE_KEY')); break; case 'Sui': signer = await ( await sui() ).getSigner(await chain.getRpc(), getEnv('SUI_PRIVATE_KEY')); break; default: throw new Error('Unsupported platform: ' + platform); } return { chain, signer: signer as Signer<N, C>, address: Wormhole.chainAddress(chain.chain, signer.address()), }; } export async function getTokenDecimals< N extends 'Mainnet' | 'Testnet' | 'Devnet' >( wh: Wormhole<N>, token: TokenId, sendChain: ChainContext<N, any> ): Promise<number> { return isTokenId(token) ? Number(await wh.getDecimals(token.chain, token.address)) : sendChain.config.nativeTokenDecimals; }
getEnv
- this function fetches environment variables like your private key from the.env
filegetSigner
- based on the chain you're working with (EVM, Solana, Sui, etc.), this function retrieves a signer for that specific platform. The signer is responsible for signing transactions and interacting with the blockchain. It securely uses the private key stored in your.env
filegetTokenDecimals
- this function fetches the number of decimals for a token on a specific chain. It helps handle token amounts accurately during transfers
-
Native Token Transfers#
In this section, you'll create a script to transfer native tokens across chains using Wormhole's Portal Bridge method. The script will handle the transfer of Sui native tokens to Solana, demonstrating the seamless cross-chain transfer capabilities of the Wormhole SDK. Since both chains are non-EVM compatible, you'll need to manually handle the attestation and finalization steps.
Configure Transfer Details#
Before initiating a cross-chain transfer, you must set up the chain context and signers for both the source and destination chains.
-
Create the
native-transfer.ts
file in thesrc
directory to hold your script for transferring native tokens across chains -
Open the
native-transfer.ts
file and begin by importing the necessary modules from the SDK and helper filesimport { Chain, Network, Wormhole, amount, wormhole, TokenId, TokenTransfer, } from '@wormhole-foundation/sdk'; import evm from '@wormhole-foundation/sdk/evm'; import solana from '@wormhole-foundation/sdk/solana'; import sui from '@wormhole-foundation/sdk/sui'; import { SignerStuff, getSigner, getTokenDecimals } from '../helpers/helpers';
evm
- this import is for working with EVM-compatible chains, like Avalanche, Ethereum, Base Sepolia, and moresolana
- this adds support for Solana, a non-EVM chainsui
- support for Sui chain, another non-EVM chainhelpers.ts
methods defined in the previous section
-
Initialize the Wormhole SDK - initialize the
wormhole
function for theTestnet
environment and specify the platforms (EVM, Solana, and Sui) to support. This allows us to interact with both EVM-compatible chains like Ethereum and non-EVM chains like Solana and SuiNote
You can replace
'Testnet'
with'Mainnet'
if you want to perform transfers on Mainnet. -
Set up source and destination chains - specify the source chain (Sui) and the destination chain (Solana) using the
getChain
method. This allows us to define where to send the native tokens and where to receive them -
Configure the signers - use the
getSigner
function to retrieve the signers responsible for signing transactions on the respective chains. This ensures that transactions are correctly authorized on both the source and destination chains -
Define the token to transfer - specify the native token on the source chain (Sui in this example) by creating a
TokenId
object -
Define the transfer amount - the amount of native tokens to transfer is specified. In this case, we're transferring 1 unit
-
Set transfer mode - specify that the transfer should be manual by setting
automatic = false
. This means you will need to handle the attestation and finalization steps yourselfNote
Automatic transfers are only supported for EVM chains. For non-EVM chains like Solana and Sui, you must manually handle the attestation and finalization steps.
-
Define decimals - fetch the number of decimals for the token on the source chain (Sui) using the
getTokenDecimals
function -
Perform the token transfer and exit the process - initiate the transfer by calling the
tokenTransfer
function, which we’ll define in the next step. This function takes an object containing all required details for executing the transfer, including thesource
anddestination
chains,token
,mode
, and transferamount
const xfer = await tokenTransfer(wh, { token, amount: amount.units(amount.parse(amt, decimals)), source, destination, automatic, });
Finally, we use
process.exit(0);
to close the script once the transfer completes
Token Transfer Logic#
This section defines the tokenTransfer
function, which manages the core steps for cross-chain transfer execution. This function will handle initiating the transfer on the source chain, retrieving the attestation, and completing the transfer on the destination chain.
Defining the Token Transfer Function#
The tokenTransfer
function initiates and manages the transfer process, handling all necessary steps to move tokens across chains with the Wormhole SDK. This function uses types from the SDK and our helpers.ts
file to ensure chain compatibility.
async function tokenTransfer<N extends Network>(
wh: Wormhole<N>,
route: {
token: TokenId;
amount: bigint;
source: SignerStuff<N, Chain>;
destination: SignerStuff<N, Chain>;
automatic: boolean;
payload?: Uint8Array;
}
) {
// Token Transfer Logic
}
Steps to Transfer Tokens#
The tokenTransfer
function consists of several key steps to facilitate the cross-chain transfer. Let’s break down each step:
-
Initialize the transfer object - the
tokenTransfer
function begins by creating aTokenTransfer
object,xfer
, which tracks the state of the transfer process and provides access to relevant methods for each transfer step -
Estimate transfer fees and validate amount - we obtain a fee quote for the transfer before proceeding. This step is significant in automatic mode (
automatic = true
), where the quote will include additional fees for relaying -
Submit the transaction to the source chain - initiate the transfer on the source chain by submitting the transaction using
route.source.signer
, starting the token transfer processconst srcTxids = await xfer.initiateTransfer(route.source.signer); console.log(`Source Trasaction ID: ${srcTxids[0]}`);
srcTxids
- the resulting transaction IDs are printed to the console. These IDs can be used to track the transfer’s progress on the source chain and Wormhole network
How Cross-Chain Transfers Work in the Background
When
xfer.initiateTransfer(route.source.signer)
is called, it initiates the transfer on the source chain. Here’s what happens in the background:- Token lock or burn - tokens are either locked in a smart contract or burned on the source chain, representing the transfer amount
- VAA creation - Wormhole’s network of Guardians generates a Verifiable Action Approval (VAA)—a signed proof of the transaction, which ensures it’s recognized across chains
- Tracking the transfer - the returned transaction IDs allow you to track the transfer's progress both on the source chain and within Wormhole’s network
- Redemption on destination - once detected, the VAA is used to release or mint the corresponding token amount on the destination chain, completing the transfer
This process ensures a secure and verifiable transfer across chains, from locking tokens on the source chain to redeeming them on the destination chain.
-
Wait for the attestation - retrieve the Wormhole attestation (VAA), which serves as cryptographic proof of the transfer. In manual mode, you must wait for the VAA before redeeming the transfer on the destination chain
-
Complete the transfer on the destination chain - redeem the VAA on the destination chain to finalize the transfer
You can find the complete native token transfer script below:
native-transfer.ts
import {
Chain,
Network,
Wormhole,
amount,
wormhole,
TokenId,
TokenTransfer,
} from '@wormhole-foundation/sdk';
import evm from '@wormhole-foundation/sdk/evm';
import solana from '@wormhole-foundation/sdk/solana';
import sui from '@wormhole-foundation/sdk/sui';
import { SignerStuff, getSigner, getTokenDecimals } from '../helpers/helpers';
(async function () {
const wh = await wormhole('Testnet', [evm, solana, sui]);
// Set up source and destination chains
const sendChain = wh.getChain('Sui');
const rcvChain = wh.getChain('Solana');
// Get signer from local key but anything that implements
const source = await getSigner(sendChain);
const destination = await getSigner(rcvChain);
// Shortcut to allow transferring native gas token
const token = Wormhole.tokenId(sendChain.chain, 'native');
// Define the amount of tokens to transfer
const amt = '1';
// Set automatic transfer to false for manual transfer
const automatic = false;
// Used to normalize the amount to account for the tokens decimals
const decimals = await getTokenDecimals(wh, token, sendChain);
// Perform the token transfer if no recovery transaction ID is provided
const xfer = await tokenTransfer(wh, {
token,
amount: amount.units(amount.parse(amt, decimals)),
source,
destination,
automatic,
});
process.exit(0);
})();
async function tokenTransfer<N extends Network>(
wh: Wormhole<N>,
route: {
token: TokenId;
amount: bigint;
source: SignerStuff<N, Chain>;
destination: SignerStuff<N, Chain>;
automatic: boolean;
payload?: Uint8Array;
}
) {
// Token Transfer Logic
// 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.automatic ?? false,
route.payload
);
const quote = await TokenTransfer.quoteTransfer(
wh,
route.source.chain,
route.destination.chain,
xfer.transfer
);
if (xfer.transfer.automatic && quote.destinationToken.amount < 0)
throw 'The amount requested is too low to cover the fee and any native gas requested.';
// 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(`Source Trasaction ID: ${srcTxids[0]}`);
console.log(`Wormhole Trasaction ID: ${srcTxids[1] ?? srcTxids[0]}`);
// Wait for the VAA to be signed and ready (not required for auto transfer)
console.log('Getting Attestation');
await xfer.fetchAttestation(60_000);
// Redeem the VAA on the dest chain
console.log('Completing Transfer');
const destTxids = await xfer.completeTransfer(route.destination.signer);
console.log(`Completed Transfer: `, destTxids);
}
Run the Native Token Transfer#
Now that you’ve set up the project and defined the transfer logic, you can execute the script to transfer native tokens from the Sui chain to Solana. You can use tsx
to run the TypeScript file directly:
This initiates the native token transfer from the source chain (Sui) and completes it on the destination chain (Solana).
You can monitor the status of the transaction on the Wormhole explorer.
Resources#
If you'd like to explore the complete project or need a reference while following this tutorial, you can find the complete codebase in Wormhole's demo GitHub repository. The repository includes all the example scripts and configurations needed to perform native token cross-chain transfers, including manual, automatic, and partial transfers using the Wormhole SDK.
Conclusion#
You've successfully built a cross-chain token transfer application using Wormhole's TypeScript SDK and the Portal Bridge method. This guide took you through the setup, configuration, and transfer logic required to move native tokens across non-EVM chains like Sui and Solana.
The same transfer logic will apply if you’d like to extend this application to different chain combinations, including EVM-compatible chains.