diff --git a/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/helpers.ts b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/helpers.ts new file mode 100644 index 000000000..a266bee2b --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/helpers.ts @@ -0,0 +1,75 @@ +import { + Chain, + ChainAddress, + ChainContext, + isTokenId, + Wormhole, + Network, + Signer, + TokenId, +} from '@wormhole-foundation/sdk'; +import type { SignAndSendSigner } 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'; + +/** + * Returns a signer for the given chain using locally scoped credentials. + * The required values (EVM_PRIVATE_KEY, SOL_PRIVATE_KEY, SUI_MNEMONIC) must + * be loaded securely beforehand, for example via a keystore, secrets + * manager, or environment variables (not recommended). + */ +export async function getSigner( + chain: ChainContext +): Promise<{ + chain: ChainContext; + signer: SignAndSendSigner; + address: ChainAddress; +}> { + let signer: Signer; + const platform = chain.platform.utils()._platform; + + // Customize the signer by adding or removing platforms as needed + // Be sure to import the necessary packages for the platforms you want to support + switch (platform) { + case 'Evm': + signer = await ( + await evm() + ).getSigner(await chain.getRpc(), EVM_PRIVATE_KEY!); + break; + case 'Solana': + signer = await ( + await solana() + ).getSigner(await chain.getRpc(), SOL_PRIVATE_KEY!); + break; + case 'Sui': + signer = await ( + await sui() + ).getSigner(await chain.getRpc(), SUI_MNEMONIC!); + break; + default: + throw new Error(`Unsupported platform: ${platform}`); + } + + const typedSigner = signer as SignAndSendSigner; + + return { + chain, + signer: typedSigner, + address: Wormhole.chainAddress(chain.chain, signer.address()), + }; +} + +/** + * Get the number of decimals for the token on the source chain. + * This helps convert a user-friendly amount (e.g., '1') into raw units. + */ +export async function getTokenDecimals( + wh: Wormhole, + token: TokenId, + chain: ChainContext +): Promise { + return isTokenId(token) + ? Number(await wh.getDecimals(token.chain, token.address)) + : chain.config.nativeTokenDecimals; +} diff --git a/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal01.html b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal01.html new file mode 100644 index 000000000..cc9622543 --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal01.html @@ -0,0 +1,5 @@ +
+ npx tsx transfer.ts + ⚠️ Token is NOT registered on destination. Running attestation flow... + +
\ No newline at end of file diff --git a/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal02.html b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal02.html new file mode 100644 index 000000000..bc64fe86f --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal02.html @@ -0,0 +1,36 @@ +
+ npx tsx transfer.ts + ⚠️ Token is NOT registered on destination. Running attestation flow... + ✅ Attestation transaction sent: [ + { + chain: 'Moonbeam', + txid: '0x2b9878e6d8e92d8ecc96d663904312c18a827ccf0b02380074fdbc0fba7e6b68' + } +] + ✅ Attestation messages: [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1505n + } +] + + Retrying Wormholescan:GetVaaBytes, attempt 0/750 + Retrying Wormholescan:GetVaaBytes, attempt 1/750 + .... + Retrying Wormholescan:GetVaaBytes, attempt 10/750 + ✅ Attestation submitted on destination: [ + { + chain: 'Solana', + txid: '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } +] + ✅ Wrapped token is now available on Solana: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} + 🚀 Token attestation complete! Proceeding with transfer... + +
\ No newline at end of file diff --git a/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal03.html b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal03.html new file mode 100644 index 000000000..2ea4e2769 --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/terminal03.html @@ -0,0 +1,56 @@ +
+ npx tsx transfer.ts + ✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} + 🚀 Built transfer object: { + token: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0x39F2f26f247CcC223393396755bfde5ecaeb0648' + } + }, + amount: 200000000000000000n, + from: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0xCD8Bcd9A793a7381b3C66C763c3f463f70De4e12' + } + }, + to: { + chain: 'Solana', + address: SolanaAddress { + type: 'Native', + address: [PublicKey [PublicKey(21dmEFTFGBEVoUNjmrxumN6A2xFxNBQXTkK7AmMqNmqD)]] + } + }, + automatic: false, + payload: undefined, + nativeGas: 0n +} + 🔗 Source chain tx sent: [ + '0xf318a1098a81063ac8acc9ca117eeb41ae9abfd9cb550a976721d2fa978f313a' +] + ⏳ Waiting for attestation (VAA) for manual transfer... + Retrying Wormholescan:GetVaaBytes, attempt 0/30 + Retrying Wormholescan:GetVaaBytes, attempt 1/30 + ..... + Retrying Wormholescan:GetVaaBytes, attempt 15/30 + ✅ Got attestation ID(s): [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1506n + } +] + ↪️ Redeeming transfer on destination... + 🎉 Destination tx(s) submitted: [ + '23NRfFZyKJTDLppJF4GovdegxYAuW2HeXTEFSKKNeA7V82aqTVYTkKeM8sCHCDWe7gWooLAPHARjbAheXoxbbwPk' +] + +
\ No newline at end of file diff --git a/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts new file mode 100644 index 000000000..6322277d5 --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts @@ -0,0 +1,174 @@ +import { + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import { getSigner, getTokenDecimals } from './helpers'; + +async function transferTokens() { + // Initialize wh instance + const wh = await wormhole('Testnet', [evm, solana]); + // Define sourceChain and destinationChain, get chain contexts + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + // Load signers for both chains + const sourceSigner = await getSigner(sourceChain); + const destinationSigner = await getSigner(destinationChain); + + // Define token and amount to transfer + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Replace with amount you want to transfer + // This is a human-readable number, e.g., 0.2 for 0.2 tokens + const amount = 0; + // Convert to raw units based on token decimals + const decimals = await getTokenDecimals(wh, tokenId, sourceChain); + const transferAmount = BigInt(Math.floor(amount * 10 ** decimals)); + + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID, continues with transfer + // Not registered = runs the attestation flow to register the token + let wrappedToken: TokenId; + try { + wrappedToken = await wh.getWrappedAsset(destinationChain.chain, tokenId); + console.log( + '✅ Token already registered on destination:', + wrappedToken.address + ); + } catch (e) { + console.log( + '⚠️ Token is NOT registered on destination. Running attestation flow...' + ); + // Retrieve the token bridge context for the source chain + // This is where you will send the transaction to attest the token + const tb = await sourceChain.getTokenBridge(); + // Define the token to attest and a payer address + const token: TokenAddress = toNative( + sourceChain.chain, + tokenId.address.toString() + ); + const payer = toNative(sourceChain.chain, sourceSigner.signer.address()); + // Call the `createAttestation` method to create a new attestation + // and sign and send the transaction + for await (const tx of tb.createAttestation(token, payer)) { + const txids = await signSendWait( + sourceChain, + tb.createAttestation(token), + sourceSigner.signer + ); + console.log('✅ Attestation transaction sent:', txids); + // Parse the transaction to get Wormhole message ID + const messages = await sourceChain.parseTransaction(txids[0].txid); + console.log('✅ Attestation messages:', messages); + // Set a timeout for fetching the VAA, this can take several minutes + // depending on the source chain network and finality + const timeout = 25 * 60 * 1000; + // Fetch the VAA for the attestation message + const vaa = await wh.getVaa( + messages[0]!, + 'TokenBridge:AttestMeta', + timeout + ); + if (!vaa) throw new Error('❌ VAA not found before timeout.'); + // Get the token bridge context for the destination chain + // and submit the attestation VAA + const destTb = await destinationChain.getTokenBridge(); + const payer = toNative( + destinationChain.chain, + destinationSigner.signer.address() + ); + const destTxids = await signSendWait( + destinationChain, + destTb.submitAttestation(vaa, payer), + destinationSigner.signer + ); + console.log('✅ Attestation submitted on destination:', destTxids); + } + // Poll for the wrapped token to appear on the destination chain + // before proceeding with the transfer + const maxAttempts = 50; // ~5 minutes with 6s interval + const interval = 6000; + let attempt = 0; + let registered = false; + + while (attempt < maxAttempts && !registered) { + attempt++; + try { + const wrapped = await wh.getWrappedAsset( + destinationChain.chain, + tokenId + ); + console.log( + `✅ Wrapped token is now available on ${destinationChain.chain}:`, + wrapped.address + ); + registered = true; + } catch { + console.log( + `⏳ Waiting for wrapped token to register on ${destinationChain.chain}...` + ); + await new Promise((res) => setTimeout(res, interval)); + } + } + + if (!registered) { + throw new Error( + `❌ Token attestation did not complete in time on ${destinationChain.chain}` + ); + } + + console.log('🚀 Token attestation complete! Proceeding with transfer...'); + } + + // Define whether the transfer is automatic or manual + const automatic = false; + // Optional native gas amount for automatic transfers only + const nativeGasAmount = '0.001'; // 0.001 of native gas in human-readable format + // Get the decimals for the source chain + const nativeGasDecimals = destinationChain.config.nativeTokenDecimals; + // If automatic, convert to raw units, otherwise set to 0n + const nativeGas = automatic + ? BigInt(Number(nativeGasAmount) * 10 ** nativeGasDecimals) + : 0n; + // Build the token transfer object + const xfer = await wh.tokenTransfer( + tokenId, + transferAmount, + sourceSigner.address, + destinationSigner.address, + automatic, + undefined, // no payload + nativeGas + ); + console.log('🚀 Built transfer object:', xfer.transfer); + + // Initiate, sign, and send the token transfer + const srcTxs = await xfer.initiateTransfer(sourceSigner.signer); + console.log('🔗 Source chain tx sent:', srcTxs); + + // If automatic, no further action is required. The relayer completes the transfer. + if (automatic) { + console.log('✅ Automatic transfer: relayer is handling redemption.'); + return; + } + // For manual transfers, wait for VAA + console.log('⏳ Waiting for attestation (VAA) for manual transfer...'); + const attIds = await xfer.fetchAttestation(120_000); // 2 minutes timeout + console.log('✅ Got attestation ID(s):', attIds); + + // Complete the manual transfer on the destination chain + console.log('↪️ Redeeming transfer on destination...'); + const destTxs = await xfer.completeTransfer(destinationSigner.signer); + console.log('🎉 Destination tx(s) submitted:', destTxs); +} + +transferTokens().catch((e) => { + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); \ No newline at end of file diff --git a/llms-files/llms-token-bridge.txt b/llms-files/llms-token-bridge.txt index e82929d67..41ccba349 100644 --- a/llms-files/llms-token-bridge.txt +++ b/llms-files/llms-token-bridge.txt @@ -18,6 +18,7 @@ Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/re Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/faqs.md [type: other] Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/get-started.md [type: other] Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/token-bridge-contracts.md [type: other] +Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/transfer-wrapped-assets.md [type: other] Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/overview.md [type: other] ## Full content for each doc page @@ -1111,6 +1112,521 @@ For a deeper understanding of the Token Bridge implementation and to review the A practical implementation of the Wormhole Token Bridge can be seen in [Portal Bridge](https://portalbridge.com/){target=\_blank}, which provides an easy-to-use interface for transferring tokens across multiple blockchain networks. It leverages the Wormhole infrastructure to handle cross-chain asset transfers seamlessly, offering users a convenient way to bridge their assets while ensuring security and maintaining token integrity. --- END CONTENT --- +Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/transfer-wrapped-assets/ +--- BEGIN CONTENT --- +--- +title: Transfer Wrapped Assets +description: Follow this guide to use Token Bridge to transfer wrapped assets. Includes automatic and manual flows, token attestation, VAA fetching, and manual redemption. +categories: Token-Bridge, Transfers, Typescript-SDK +--- + +# Transfer Wrapped Assets + +## Introduction + +This guide demonstrates the transfer of wrapped assets using the Token Bridge protocol via the [TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}. This example will transfer an arbitrary ERC-20 token from Moonbase Alpha to Solana but can be adapted for any supported chains. View this list of chains with [deployed Token Bridge contracts](/docs/products/reference/contract-addresses/#token-bridge){target=\_blank} to verify if your desired source and destination chains are supported. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create a token attestation to register a wrapped version of a token on a destination chain +- Transfer wrapped assets using Token Bridge automatic or manual transfers +- Fetch a signed Verified Action Approval (VAA) +- Manually redeem a signed VAA to claim tokens on a destination chain + +## Prerequisites + +Before you begin, ensure you have the following: + +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm){target=\_blank} installed on your machine +- [TypeScript](https://www.typescriptlang.org/download/){target=\_blank} installed globally +- The contract address for the ERC-20 token you wish to transfer +- A wallet setup with the following: + - Private keys for your source and destination chains + - A small amount of gas tokens on your source and destination chains + - A balance on your source chain of the ERC-20 token you want to transfer + +## Set Up Your Token Transfer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for multichain token transfers. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir token-bridge-demo + cd token-bridge-demo + npm init -y + ``` + +2. Install dependencies, including the Wormhole TypeScript SDK: + ```bash + npm install @wormhole-foundation/sdk -D tsx typescript + ``` + +3. Set up secure access to your wallets. This guide assumes you are loading your private key values from a secure keystore of your choice, such as a secrets manager or a CLI-based tool like [`cast wallet`](https://book.getfoundry.sh/reference/cast/cast-wallet){target=\_blank}. + + !!! warning + If you use a `.env` file during development, add it to your `.gitignore` to exclude it from version control. Never commit private keys or mnemonics to your repository. + +4. Create a new file named `helpers.ts` to hold signer and decimal functions: + ```bash + touch helpers.ts + ``` + +5. Open `helpers.ts` and add the following code: + ```typescript title="helpers.ts" + import { + Chain, + ChainAddress, + ChainContext, + isTokenId, + Wormhole, + Network, + Signer, + TokenId, +} from '@wormhole-foundation/sdk'; +import type { SignAndSendSigner } 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'; + +/** + * Returns a signer for the given chain using locally scoped credentials. + * The required values (EVM_PRIVATE_KEY, SOL_PRIVATE_KEY, SUI_MNEMONIC) must + * be loaded securely beforehand, for example via a keystore, secrets + * manager, or environment variables (not recommended). + */ +export async function getSigner( + chain: ChainContext +): Promise<{ + chain: ChainContext; + signer: SignAndSendSigner; + address: ChainAddress; +}> { + let signer: Signer; + const platform = chain.platform.utils()._platform; + + // Customize the signer by adding or removing platforms as needed + // Be sure to import the necessary packages for the platforms you want to support + switch (platform) { + case 'Evm': + signer = await ( + await evm() + ).getSigner(await chain.getRpc(), EVM_PRIVATE_KEY!); + break; + case 'Solana': + signer = await ( + await solana() + ).getSigner(await chain.getRpc(), SOL_PRIVATE_KEY!); + break; + case 'Sui': + signer = await ( + await sui() + ).getSigner(await chain.getRpc(), SUI_MNEMONIC!); + break; + default: + throw new Error(`Unsupported platform: ${platform}`); + } + + const typedSigner = signer as SignAndSendSigner; + + return { + chain, + signer: typedSigner, + address: Wormhole.chainAddress(chain.chain, signer.address()), + }; +} + +/** + * Get the number of decimals for the token on the source chain. + * This helps convert a user-friendly amount (e.g., '1') into raw units. + */ +export async function getTokenDecimals( + wh: Wormhole, + token: TokenId, + chain: ChainContext +): Promise { + return isTokenId(token) + ? Number(await wh.getDecimals(token.chain, token.address)) + : chain.config.nativeTokenDecimals; +} + + ``` + + You can view the [constants for platform names](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/3eae2e91fc3a6fec859eb87cfa85a4c92c65466f/core/base/src/constants/platforms.ts#L6){target=\_blank} in the GitHub repo for a list of supported platforms + +## Verify Token Registration (Attestation) + +Tokens must be registered on the destination chain before they can be bridged. This process involves submitting an attestation with the native token metadata to the destination chain, which enables the destination chain's Token Bridge contract to create a corresponding wrapped version with the same attributes as the native token. + +Registration via attestation is only required the first time a given token is sent to that specific destination chain. Follow these steps to check the registration status of a token: + +1. Create a new file named `transfer.ts`: + ```bash + touch transfer.ts + ``` + +2. Open your `transfer.ts` file and add the following code: + ```typescript title="transfer.ts" + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import { getSigner, getTokenDecimals } from './helpers'; + +async function transferTokens() { + // Initialize wh instance + const wh = await wormhole('Testnet', [evm, solana]); + // Define sourceChain and destinationChain, get chain contexts + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + // Load signers for both chains + const sourceSigner = await getSigner(sourceChain); + const destinationSigner = await getSigner(destinationChain); + + // Define token and amount to transfer + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Replace with amount you want to transfer + // This is a human-readable number, e.g., 0.2 for 0.2 tokens + const amount = 0; + // Convert to raw units based on token decimals + const decimals = await getTokenDecimals(wh, tokenId, sourceChain); + const transferAmount = BigInt(Math.floor(amount * 10 ** decimals)); + + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID, continues with transfer + // Not registered = runs the attestation flow to register the token + let wrappedToken: TokenId; + try { + wrappedToken = await wh.getWrappedAsset(destinationChain.chain, tokenId); + console.log( + '✅ Token already registered on destination:', + wrappedToken.address + ); + } catch (e) { + console.log( + '⚠️ Token is NOT registered on destination. Running attestation flow...' + ); + // Token attestation and registration flow here if needed + + transferTokens().catch((e) => { + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + ``` + + This code does the following: + + - Initializes a `wormhole` instance and defines the source and destination chains + - Imports the signer and decimal functions from `helpers.ts` + - Identifies the token and amount to transfer + - Checks to see if a wrapped version of the ERC-20 token to transfer exists on the destination chain + +3. Run the script using the following command: + + ```bash + npx tsx transfer.ts + ``` + + If the token is registered on the destination chain, the address of the existing wrapped asset is returned, and you can continue to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. If the token is not registered, you will see a message similar to the following advising the attestation flow will run: + +
+npx tsx transfer.ts +⚠️ Token is NOT registered on destination. Running attestation flow... + +
+ + If you see this message, follow the steps under "Need to register a token?" before continuing with the rest of the transfer flow code. + + ??? example "Need to register a token?" + Token attestation is a one-time process to register a token on a destination chain. You should only follow these steps if your token registration check indicates a wrapped version does not exist on the destination chain. + + 1. Add the following code to `transfer.ts` to create the attestation for token registration: + ```typescript title="transfer.ts" + // Token attestation and registration flow here if needed + // This is where you will send the transaction to attest the token + const tb = await sourceChain.getTokenBridge(); + // Define the token to attest and a payer address + const token: TokenAddress = toNative( + sourceChain.chain, + tokenId.address.toString() + ); + const payer = toNative(sourceChain.chain, sourceSigner.signer.address()); + // Call the `createAttestation` method to create a new attestation + // and sign and send the transaction + for await (const tx of tb.createAttestation(token, payer)) { + const txids = await signSendWait( + sourceChain, + tb.createAttestation(token), + sourceSigner.signer + ); + console.log('✅ Attestation transaction sent:', txids); + // Parse the transaction to get Wormhole message ID + const messages = await sourceChain.parseTransaction(txids[0].txid); + console.log('✅ Attestation messages:', messages); + // Set a timeout for fetching the VAA, this can take several minutes + // depending on the source chain network and finality + const timeout = 25 * 60 * 1000; + // Fetch the VAA for the attestation message + const vaa = await wh.getVaa( + messages[0]!, + 'TokenBridge:AttestMeta', + timeout + ); + if (!vaa) throw new Error('❌ VAA not found before timeout.'); + // Get the token bridge context for the destination chain + // and submit the attestation VAA + const destTb = await destinationChain.getTokenBridge(); + const payer = toNative( + destinationChain.chain, + destinationSigner.signer.address() + ); + const destTxids = await signSendWait( + destinationChain, + destTb.submitAttestation(vaa, payer), + destinationSigner.signer + ); + console.log('✅ Attestation submitted on destination:', destTxids); + } + // Poll for the wrapped token to appear on the destination chain + // before proceeding with the transfer + const maxAttempts = 50; // ~5 minutes with 6s interval + const interval = 6000; + let attempt = 0; + let registered = false; + + while (attempt < maxAttempts && !registered) { + attempt++; + try { + const wrapped = await wh.getWrappedAsset( + destinationChain.chain, + tokenId + ); + console.log( + `✅ Wrapped token is now available on ${destinationChain.chain}:`, + wrapped.address + ); + registered = true; + } catch { + console.log( + `⏳ Waiting for wrapped token to register on ${destinationChain.chain}...` + ); + await new Promise((res) => setTimeout(res, interval)); + } + } + + if (!registered) { + throw new Error( + `❌ Token attestation did not complete in time on ${destinationChain.chain}` + ); + } + + console.log('🚀 Token attestation complete! Proceeding with transfer...'); + } + // Remainder of transfer code + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + ``` + + This code does the following: + + - Gets the Token Bridge protocol for the source chain + - Defines the token to attest for registration on the destination chain and the payer to sign for the transaction + - Calls `createAttestation`, signs, and then sends the transaction + - Waits for the signed VAA confirming the attestation creation + - Sends the VAA to the destination chain to complete registration + - Polls for the wrapped token to be available on the destination chain before continuing the transfer process + + 3. Run the script with the following command: + + ```bash + npx tsx transfer.ts + ``` + + When the attestation and registration are complete, you will see terminal output similar to the following: + +
+npx tsx transfer.ts +⚠️ Token is NOT registered on destination. Running attestation flow... +✅ Attestation transaction sent: [ + { + chain: 'Moonbeam', + txid: '0x2b9878e6d8e92d8ecc96d663904312c18a827ccf0b02380074fdbc0fba7e6b68' + } +] +✅ Attestation messages: [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1505n + } +] + +Retrying Wormholescan:GetVaaBytes, attempt 0/750 +Retrying Wormholescan:GetVaaBytes, attempt 1/750 +.... +Retrying Wormholescan:GetVaaBytes, attempt 10/750 +✅ Attestation submitted on destination: [ + { + chain: 'Solana', + txid: '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } +] +✅ Wrapped token is now available on Solana: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} +🚀 Token attestation complete! Proceeding with transfer... + +
+ + You can now go on to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. + +## Initiate Transfer on Source Chain + +Follow these steps to add the rest of the logic to initiate the token transfer on the source chain: + +1. Open your `transfer.ts` file and add the following code: + + ```typescript title="transfer.ts" + // Remainder of transfer code + const automatic = false; + // Optional native gas amount for automatic transfers only + const nativeGasAmount = '0.001'; // 0.001 of native gas in human-readable format + // Get the decimals for the source chain + const nativeGasDecimals = destinationChain.config.nativeTokenDecimals; + // If automatic, convert to raw units, otherwise set to 0n + const nativeGas = automatic + ? BigInt(Number(nativeGasAmount) * 10 ** nativeGasDecimals) + : 0n; + // Build the token transfer object + const xfer = await wh.tokenTransfer( + tokenId, + transferAmount, + sourceSigner.address, + destinationSigner.address, + automatic, + undefined, // no payload + nativeGas + ); + console.log('🚀 Built transfer object:', xfer.transfer); + + // Initiate, sign, and send the token transfer + const srcTxs = await xfer.initiateTransfer(sourceSigner.signer); + console.log('🔗 Source chain tx sent:', srcTxs); + + // If automatic, no further action is required. The relayer completes the transfer. + if (automatic) { + console.log('✅ Automatic transfer: relayer is handling redemption.'); + return; + } + // For manual transfers, wait for VAA + console.log('⏳ Waiting for attestation (VAA) for manual transfer...'); + const attIds = await xfer.fetchAttestation(120_000); // 2 minutes timeout + console.log('✅ Got attestation ID(s):', attIds); + + // Complete the manual transfer on the destination chain + console.log('↪️ Redeeming transfer on destination...'); + const destTxs = await xfer.completeTransfer(destinationSigner.signer); + console.log('🎉 Destination tx(s) submitted:', destTxs); +} + +transferTokens().catch((e) => { + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + + ``` + + This code does the following: + + - Defines the transfer as automatic or manual. To use automatic transfer, both the source and destination chain must have an existing `tokenBridgeRelayer` contract. You can check the list of [deployed `tokenBridgeRelayer` contracts](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/base/src/constants/contracts/tokenBridgeRelayer.ts){target=\_blank} in the Wormhole SDK repo to see if your desired chains are supported + - Sets an optional amount for native gas drop-off. This option allows you to send a small amount of the destination chain's native token for gas fees. Native gas drop-off is currently only supported for automatic transfers + - Builds the transfer object, initiates the transfer, signs and sends the transaction + - If the transfer is automatic, the flow ends. Otherwise, the script waits for the signed VAA confirming the transaction on the source chain. The signed VAA is then submitted to the destination chain to claim the tokens and complete the manual transfer + +2. Run the script with the following command: + ```bash + npx tsx transfer.ts + ``` + +3. You will see terminal output similar to the following: + +
+npx tsx transfer.ts +✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} +🚀 Built transfer object: { + token: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0x39F2f26f247CcC223393396755bfde5ecaeb0648' + } + }, + amount: 200000000000000000n, + from: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0xCD8Bcd9A793a7381b3C66C763c3f463f70De4e12' + } + }, + to: { + chain: 'Solana', + address: SolanaAddress { + type: 'Native', + address: [PublicKey [PublicKey(21dmEFTFGBEVoUNjmrxumN6A2xFxNBQXTkK7AmMqNmqD)]] + } + }, + automatic: false, + payload: undefined, + nativeGas: 0n +} +🔗 Source chain tx sent: [ + '0xf318a1098a81063ac8acc9ca117eeb41ae9abfd9cb550a976721d2fa978f313a' +] +⏳ Waiting for attestation (VAA) for manual transfer... +Retrying Wormholescan:GetVaaBytes, attempt 0/30 +Retrying Wormholescan:GetVaaBytes, attempt 1/30 +..... +Retrying Wormholescan:GetVaaBytes, attempt 15/30 +✅ Got attestation ID(s): [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1506n + } +] +↪️ Redeeming transfer on destination... +🎉 Destination tx(s) submitted: [ + '23NRfFZyKJTDLppJF4GovdegxYAuW2HeXTEFSKKNeA7V82aqTVYTkKeM8sCHCDWe7gWooLAPHARjbAheXoxbbwPk' +] + +
+ +Congratulations! You've now used Token Bridge to transfer wrapped assets using the Wormhole TypeScript SDK. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Portal Bridge**](https://portalbridge.com/){target=\_blank}: visit this site to interact with Wormhole's Portal Bridge featuring a working Token Bridge integration. +- [**Interact with Token Bridge Contracts**](/docs/products/token-bridge/guides/token-bridge-contracts/): this guide explores the Solidity functions used in Token Bridge contracts. +- [**`TokenBridge` and `AutomaticTokenBridge` interfaces**](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/tokenBridge/tokenBridge.ts){target=\_blank}: view the source code defining these key interfaces and their associated namespaces. +--- END CONTENT --- + Doc-Content: https://wormhole.com/docs/products/token-bridge/overview/ --- BEGIN CONTENT --- --- diff --git a/llms-files/llms-typescript-sdk.txt b/llms-files/llms-typescript-sdk.txt index 799700644..dee947c52 100644 --- a/llms-files/llms-typescript-sdk.txt +++ b/llms-files/llms-typescript-sdk.txt @@ -14,6 +14,7 @@ You are an AI developer assistant for Wormhole (https://wormhole.com). Your task ## List of doc pages: Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/messaging/get-started.md [type: other] +Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/transfer-wrapped-assets.md [type: other] Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/tools/cli/get-started.md [type: other] Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/tools/dev-env.md [type: other] Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/tools/faqs.md [type: other] @@ -331,6 +332,521 @@ Congratulations! You've published your first multichain message using Wormhole's - [**Get Started with the Solidity SDK**](/docs/tools/solidity-sdk/get-started/){target=\_blank}: Smart contract developers can follow this on-chain integration guide to use Wormhole Solidity SDK-based sender and receiver contracts to send testnet USDC across chains. --- END CONTENT --- +Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/transfer-wrapped-assets/ +--- BEGIN CONTENT --- +--- +title: Transfer Wrapped Assets +description: Follow this guide to use Token Bridge to transfer wrapped assets. Includes automatic and manual flows, token attestation, VAA fetching, and manual redemption. +categories: Token-Bridge, Transfers, Typescript-SDK +--- + +# Transfer Wrapped Assets + +## Introduction + +This guide demonstrates the transfer of wrapped assets using the Token Bridge protocol via the [TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}. This example will transfer an arbitrary ERC-20 token from Moonbase Alpha to Solana but can be adapted for any supported chains. View this list of chains with [deployed Token Bridge contracts](/docs/products/reference/contract-addresses/#token-bridge){target=\_blank} to verify if your desired source and destination chains are supported. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create a token attestation to register a wrapped version of a token on a destination chain +- Transfer wrapped assets using Token Bridge automatic or manual transfers +- Fetch a signed Verified Action Approval (VAA) +- Manually redeem a signed VAA to claim tokens on a destination chain + +## Prerequisites + +Before you begin, ensure you have the following: + +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm){target=\_blank} installed on your machine +- [TypeScript](https://www.typescriptlang.org/download/){target=\_blank} installed globally +- The contract address for the ERC-20 token you wish to transfer +- A wallet setup with the following: + - Private keys for your source and destination chains + - A small amount of gas tokens on your source and destination chains + - A balance on your source chain of the ERC-20 token you want to transfer + +## Set Up Your Token Transfer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for multichain token transfers. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir token-bridge-demo + cd token-bridge-demo + npm init -y + ``` + +2. Install dependencies, including the Wormhole TypeScript SDK: + ```bash + npm install @wormhole-foundation/sdk -D tsx typescript + ``` + +3. Set up secure access to your wallets. This guide assumes you are loading your private key values from a secure keystore of your choice, such as a secrets manager or a CLI-based tool like [`cast wallet`](https://book.getfoundry.sh/reference/cast/cast-wallet){target=\_blank}. + + !!! warning + If you use a `.env` file during development, add it to your `.gitignore` to exclude it from version control. Never commit private keys or mnemonics to your repository. + +4. Create a new file named `helpers.ts` to hold signer and decimal functions: + ```bash + touch helpers.ts + ``` + +5. Open `helpers.ts` and add the following code: + ```typescript title="helpers.ts" + import { + Chain, + ChainAddress, + ChainContext, + isTokenId, + Wormhole, + Network, + Signer, + TokenId, +} from '@wormhole-foundation/sdk'; +import type { SignAndSendSigner } 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'; + +/** + * Returns a signer for the given chain using locally scoped credentials. + * The required values (EVM_PRIVATE_KEY, SOL_PRIVATE_KEY, SUI_MNEMONIC) must + * be loaded securely beforehand, for example via a keystore, secrets + * manager, or environment variables (not recommended). + */ +export async function getSigner( + chain: ChainContext +): Promise<{ + chain: ChainContext; + signer: SignAndSendSigner; + address: ChainAddress; +}> { + let signer: Signer; + const platform = chain.platform.utils()._platform; + + // Customize the signer by adding or removing platforms as needed + // Be sure to import the necessary packages for the platforms you want to support + switch (platform) { + case 'Evm': + signer = await ( + await evm() + ).getSigner(await chain.getRpc(), EVM_PRIVATE_KEY!); + break; + case 'Solana': + signer = await ( + await solana() + ).getSigner(await chain.getRpc(), SOL_PRIVATE_KEY!); + break; + case 'Sui': + signer = await ( + await sui() + ).getSigner(await chain.getRpc(), SUI_MNEMONIC!); + break; + default: + throw new Error(`Unsupported platform: ${platform}`); + } + + const typedSigner = signer as SignAndSendSigner; + + return { + chain, + signer: typedSigner, + address: Wormhole.chainAddress(chain.chain, signer.address()), + }; +} + +/** + * Get the number of decimals for the token on the source chain. + * This helps convert a user-friendly amount (e.g., '1') into raw units. + */ +export async function getTokenDecimals( + wh: Wormhole, + token: TokenId, + chain: ChainContext +): Promise { + return isTokenId(token) + ? Number(await wh.getDecimals(token.chain, token.address)) + : chain.config.nativeTokenDecimals; +} + + ``` + + You can view the [constants for platform names](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/3eae2e91fc3a6fec859eb87cfa85a4c92c65466f/core/base/src/constants/platforms.ts#L6){target=\_blank} in the GitHub repo for a list of supported platforms + +## Verify Token Registration (Attestation) + +Tokens must be registered on the destination chain before they can be bridged. This process involves submitting an attestation with the native token metadata to the destination chain, which enables the destination chain's Token Bridge contract to create a corresponding wrapped version with the same attributes as the native token. + +Registration via attestation is only required the first time a given token is sent to that specific destination chain. Follow these steps to check the registration status of a token: + +1. Create a new file named `transfer.ts`: + ```bash + touch transfer.ts + ``` + +2. Open your `transfer.ts` file and add the following code: + ```typescript title="transfer.ts" + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import { getSigner, getTokenDecimals } from './helpers'; + +async function transferTokens() { + // Initialize wh instance + const wh = await wormhole('Testnet', [evm, solana]); + // Define sourceChain and destinationChain, get chain contexts + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + // Load signers for both chains + const sourceSigner = await getSigner(sourceChain); + const destinationSigner = await getSigner(destinationChain); + + // Define token and amount to transfer + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Replace with amount you want to transfer + // This is a human-readable number, e.g., 0.2 for 0.2 tokens + const amount = 0; + // Convert to raw units based on token decimals + const decimals = await getTokenDecimals(wh, tokenId, sourceChain); + const transferAmount = BigInt(Math.floor(amount * 10 ** decimals)); + + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID, continues with transfer + // Not registered = runs the attestation flow to register the token + let wrappedToken: TokenId; + try { + wrappedToken = await wh.getWrappedAsset(destinationChain.chain, tokenId); + console.log( + '✅ Token already registered on destination:', + wrappedToken.address + ); + } catch (e) { + console.log( + '⚠️ Token is NOT registered on destination. Running attestation flow...' + ); + // Token attestation and registration flow here if needed + + transferTokens().catch((e) => { + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + ``` + + This code does the following: + + - Initializes a `wormhole` instance and defines the source and destination chains + - Imports the signer and decimal functions from `helpers.ts` + - Identifies the token and amount to transfer + - Checks to see if a wrapped version of the ERC-20 token to transfer exists on the destination chain + +3. Run the script using the following command: + + ```bash + npx tsx transfer.ts + ``` + + If the token is registered on the destination chain, the address of the existing wrapped asset is returned, and you can continue to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. If the token is not registered, you will see a message similar to the following advising the attestation flow will run: + +
+npx tsx transfer.ts +⚠️ Token is NOT registered on destination. Running attestation flow... + +
+ + If you see this message, follow the steps under "Need to register a token?" before continuing with the rest of the transfer flow code. + + ??? example "Need to register a token?" + Token attestation is a one-time process to register a token on a destination chain. You should only follow these steps if your token registration check indicates a wrapped version does not exist on the destination chain. + + 1. Add the following code to `transfer.ts` to create the attestation for token registration: + ```typescript title="transfer.ts" + // Token attestation and registration flow here if needed + // This is where you will send the transaction to attest the token + const tb = await sourceChain.getTokenBridge(); + // Define the token to attest and a payer address + const token: TokenAddress = toNative( + sourceChain.chain, + tokenId.address.toString() + ); + const payer = toNative(sourceChain.chain, sourceSigner.signer.address()); + // Call the `createAttestation` method to create a new attestation + // and sign and send the transaction + for await (const tx of tb.createAttestation(token, payer)) { + const txids = await signSendWait( + sourceChain, + tb.createAttestation(token), + sourceSigner.signer + ); + console.log('✅ Attestation transaction sent:', txids); + // Parse the transaction to get Wormhole message ID + const messages = await sourceChain.parseTransaction(txids[0].txid); + console.log('✅ Attestation messages:', messages); + // Set a timeout for fetching the VAA, this can take several minutes + // depending on the source chain network and finality + const timeout = 25 * 60 * 1000; + // Fetch the VAA for the attestation message + const vaa = await wh.getVaa( + messages[0]!, + 'TokenBridge:AttestMeta', + timeout + ); + if (!vaa) throw new Error('❌ VAA not found before timeout.'); + // Get the token bridge context for the destination chain + // and submit the attestation VAA + const destTb = await destinationChain.getTokenBridge(); + const payer = toNative( + destinationChain.chain, + destinationSigner.signer.address() + ); + const destTxids = await signSendWait( + destinationChain, + destTb.submitAttestation(vaa, payer), + destinationSigner.signer + ); + console.log('✅ Attestation submitted on destination:', destTxids); + } + // Poll for the wrapped token to appear on the destination chain + // before proceeding with the transfer + const maxAttempts = 50; // ~5 minutes with 6s interval + const interval = 6000; + let attempt = 0; + let registered = false; + + while (attempt < maxAttempts && !registered) { + attempt++; + try { + const wrapped = await wh.getWrappedAsset( + destinationChain.chain, + tokenId + ); + console.log( + `✅ Wrapped token is now available on ${destinationChain.chain}:`, + wrapped.address + ); + registered = true; + } catch { + console.log( + `⏳ Waiting for wrapped token to register on ${destinationChain.chain}...` + ); + await new Promise((res) => setTimeout(res, interval)); + } + } + + if (!registered) { + throw new Error( + `❌ Token attestation did not complete in time on ${destinationChain.chain}` + ); + } + + console.log('🚀 Token attestation complete! Proceeding with transfer...'); + } + // Remainder of transfer code + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + ``` + + This code does the following: + + - Gets the Token Bridge protocol for the source chain + - Defines the token to attest for registration on the destination chain and the payer to sign for the transaction + - Calls `createAttestation`, signs, and then sends the transaction + - Waits for the signed VAA confirming the attestation creation + - Sends the VAA to the destination chain to complete registration + - Polls for the wrapped token to be available on the destination chain before continuing the transfer process + + 3. Run the script with the following command: + + ```bash + npx tsx transfer.ts + ``` + + When the attestation and registration are complete, you will see terminal output similar to the following: + +
+npx tsx transfer.ts +⚠️ Token is NOT registered on destination. Running attestation flow... +✅ Attestation transaction sent: [ + { + chain: 'Moonbeam', + txid: '0x2b9878e6d8e92d8ecc96d663904312c18a827ccf0b02380074fdbc0fba7e6b68' + } +] +✅ Attestation messages: [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1505n + } +] + +Retrying Wormholescan:GetVaaBytes, attempt 0/750 +Retrying Wormholescan:GetVaaBytes, attempt 1/750 +.... +Retrying Wormholescan:GetVaaBytes, attempt 10/750 +✅ Attestation submitted on destination: [ + { + chain: 'Solana', + txid: '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } +] +✅ Wrapped token is now available on Solana: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} +🚀 Token attestation complete! Proceeding with transfer... + +
+ + You can now go on to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. + +## Initiate Transfer on Source Chain + +Follow these steps to add the rest of the logic to initiate the token transfer on the source chain: + +1. Open your `transfer.ts` file and add the following code: + + ```typescript title="transfer.ts" + // Remainder of transfer code + const automatic = false; + // Optional native gas amount for automatic transfers only + const nativeGasAmount = '0.001'; // 0.001 of native gas in human-readable format + // Get the decimals for the source chain + const nativeGasDecimals = destinationChain.config.nativeTokenDecimals; + // If automatic, convert to raw units, otherwise set to 0n + const nativeGas = automatic + ? BigInt(Number(nativeGasAmount) * 10 ** nativeGasDecimals) + : 0n; + // Build the token transfer object + const xfer = await wh.tokenTransfer( + tokenId, + transferAmount, + sourceSigner.address, + destinationSigner.address, + automatic, + undefined, // no payload + nativeGas + ); + console.log('🚀 Built transfer object:', xfer.transfer); + + // Initiate, sign, and send the token transfer + const srcTxs = await xfer.initiateTransfer(sourceSigner.signer); + console.log('🔗 Source chain tx sent:', srcTxs); + + // If automatic, no further action is required. The relayer completes the transfer. + if (automatic) { + console.log('✅ Automatic transfer: relayer is handling redemption.'); + return; + } + // For manual transfers, wait for VAA + console.log('⏳ Waiting for attestation (VAA) for manual transfer...'); + const attIds = await xfer.fetchAttestation(120_000); // 2 minutes timeout + console.log('✅ Got attestation ID(s):', attIds); + + // Complete the manual transfer on the destination chain + console.log('↪️ Redeeming transfer on destination...'); + const destTxs = await xfer.completeTransfer(destinationSigner.signer); + console.log('🎉 Destination tx(s) submitted:', destTxs); +} + +transferTokens().catch((e) => { + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + + ``` + + This code does the following: + + - Defines the transfer as automatic or manual. To use automatic transfer, both the source and destination chain must have an existing `tokenBridgeRelayer` contract. You can check the list of [deployed `tokenBridgeRelayer` contracts](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/base/src/constants/contracts/tokenBridgeRelayer.ts){target=\_blank} in the Wormhole SDK repo to see if your desired chains are supported + - Sets an optional amount for native gas drop-off. This option allows you to send a small amount of the destination chain's native token for gas fees. Native gas drop-off is currently only supported for automatic transfers + - Builds the transfer object, initiates the transfer, signs and sends the transaction + - If the transfer is automatic, the flow ends. Otherwise, the script waits for the signed VAA confirming the transaction on the source chain. The signed VAA is then submitted to the destination chain to claim the tokens and complete the manual transfer + +2. Run the script with the following command: + ```bash + npx tsx transfer.ts + ``` + +3. You will see terminal output similar to the following: + +
+npx tsx transfer.ts +✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} +🚀 Built transfer object: { + token: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0x39F2f26f247CcC223393396755bfde5ecaeb0648' + } + }, + amount: 200000000000000000n, + from: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0xCD8Bcd9A793a7381b3C66C763c3f463f70De4e12' + } + }, + to: { + chain: 'Solana', + address: SolanaAddress { + type: 'Native', + address: [PublicKey [PublicKey(21dmEFTFGBEVoUNjmrxumN6A2xFxNBQXTkK7AmMqNmqD)]] + } + }, + automatic: false, + payload: undefined, + nativeGas: 0n +} +🔗 Source chain tx sent: [ + '0xf318a1098a81063ac8acc9ca117eeb41ae9abfd9cb550a976721d2fa978f313a' +] +⏳ Waiting for attestation (VAA) for manual transfer... +Retrying Wormholescan:GetVaaBytes, attempt 0/30 +Retrying Wormholescan:GetVaaBytes, attempt 1/30 +..... +Retrying Wormholescan:GetVaaBytes, attempt 15/30 +✅ Got attestation ID(s): [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1506n + } +] +↪️ Redeeming transfer on destination... +🎉 Destination tx(s) submitted: [ + '23NRfFZyKJTDLppJF4GovdegxYAuW2HeXTEFSKKNeA7V82aqTVYTkKeM8sCHCDWe7gWooLAPHARjbAheXoxbbwPk' +] + +
+ +Congratulations! You've now used Token Bridge to transfer wrapped assets using the Wormhole TypeScript SDK. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Portal Bridge**](https://portalbridge.com/){target=\_blank}: visit this site to interact with Wormhole's Portal Bridge featuring a working Token Bridge integration. +- [**Interact with Token Bridge Contracts**](/docs/products/token-bridge/guides/token-bridge-contracts/): this guide explores the Solidity functions used in Token Bridge contracts. +- [**`TokenBridge` and `AutomaticTokenBridge` interfaces**](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/tokenBridge/tokenBridge.ts){target=\_blank}: view the source code defining these key interfaces and their associated namespaces. +--- END CONTENT --- + Doc-Content: https://wormhole.com/docs/tools/cli/get-started/ --- BEGIN CONTENT --- --- diff --git a/llms-full.txt b/llms-full.txt index 1f7af2f22..74ca3f60e 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -75,6 +75,7 @@ Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/re Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/faqs.md Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/get-started.md Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/token-bridge-contracts.md +Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/transfer-wrapped-assets.md Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/overview.md Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/tutorials/multichain-token.md Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/tutorials/transfer-workflow.md @@ -17063,6 +17064,521 @@ For a deeper understanding of the Token Bridge implementation and to review the A practical implementation of the Wormhole Token Bridge can be seen in [Portal Bridge](https://portalbridge.com/){target=\_blank}, which provides an easy-to-use interface for transferring tokens across multiple blockchain networks. It leverages the Wormhole infrastructure to handle cross-chain asset transfers seamlessly, offering users a convenient way to bridge their assets while ensuring security and maintaining token integrity. --- END CONTENT --- +Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/transfer-wrapped-assets/ +--- BEGIN CONTENT --- +--- +title: Transfer Wrapped Assets +description: Follow this guide to use Token Bridge to transfer wrapped assets. Includes automatic and manual flows, token attestation, VAA fetching, and manual redemption. +categories: Token-Bridge, Transfers, Typescript-SDK +--- + +# Transfer Wrapped Assets + +## Introduction + +This guide demonstrates the transfer of wrapped assets using the Token Bridge protocol via the [TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}. This example will transfer an arbitrary ERC-20 token from Moonbase Alpha to Solana but can be adapted for any supported chains. View this list of chains with [deployed Token Bridge contracts](/docs/products/reference/contract-addresses/#token-bridge){target=\_blank} to verify if your desired source and destination chains are supported. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create a token attestation to register a wrapped version of a token on a destination chain +- Transfer wrapped assets using Token Bridge automatic or manual transfers +- Fetch a signed Verified Action Approval (VAA) +- Manually redeem a signed VAA to claim tokens on a destination chain + +## Prerequisites + +Before you begin, ensure you have the following: + +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm){target=\_blank} installed on your machine +- [TypeScript](https://www.typescriptlang.org/download/){target=\_blank} installed globally +- The contract address for the ERC-20 token you wish to transfer +- A wallet setup with the following: + - Private keys for your source and destination chains + - A small amount of gas tokens on your source and destination chains + - A balance on your source chain of the ERC-20 token you want to transfer + +## Set Up Your Token Transfer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for multichain token transfers. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir token-bridge-demo + cd token-bridge-demo + npm init -y + ``` + +2. Install dependencies, including the Wormhole TypeScript SDK: + ```bash + npm install @wormhole-foundation/sdk -D tsx typescript + ``` + +3. Set up secure access to your wallets. This guide assumes you are loading your private key values from a secure keystore of your choice, such as a secrets manager or a CLI-based tool like [`cast wallet`](https://book.getfoundry.sh/reference/cast/cast-wallet){target=\_blank}. + + !!! warning + If you use a `.env` file during development, add it to your `.gitignore` to exclude it from version control. Never commit private keys or mnemonics to your repository. + +4. Create a new file named `helpers.ts` to hold signer and decimal functions: + ```bash + touch helpers.ts + ``` + +5. Open `helpers.ts` and add the following code: + ```typescript title="helpers.ts" + import { + Chain, + ChainAddress, + ChainContext, + isTokenId, + Wormhole, + Network, + Signer, + TokenId, +} from '@wormhole-foundation/sdk'; +import type { SignAndSendSigner } 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'; + +/** + * Returns a signer for the given chain using locally scoped credentials. + * The required values (EVM_PRIVATE_KEY, SOL_PRIVATE_KEY, SUI_MNEMONIC) must + * be loaded securely beforehand, for example via a keystore, secrets + * manager, or environment variables (not recommended). + */ +export async function getSigner( + chain: ChainContext +): Promise<{ + chain: ChainContext; + signer: SignAndSendSigner; + address: ChainAddress; +}> { + let signer: Signer; + const platform = chain.platform.utils()._platform; + + // Customize the signer by adding or removing platforms as needed + // Be sure to import the necessary packages for the platforms you want to support + switch (platform) { + case 'Evm': + signer = await ( + await evm() + ).getSigner(await chain.getRpc(), EVM_PRIVATE_KEY!); + break; + case 'Solana': + signer = await ( + await solana() + ).getSigner(await chain.getRpc(), SOL_PRIVATE_KEY!); + break; + case 'Sui': + signer = await ( + await sui() + ).getSigner(await chain.getRpc(), SUI_MNEMONIC!); + break; + default: + throw new Error(`Unsupported platform: ${platform}`); + } + + const typedSigner = signer as SignAndSendSigner; + + return { + chain, + signer: typedSigner, + address: Wormhole.chainAddress(chain.chain, signer.address()), + }; +} + +/** + * Get the number of decimals for the token on the source chain. + * This helps convert a user-friendly amount (e.g., '1') into raw units. + */ +export async function getTokenDecimals( + wh: Wormhole, + token: TokenId, + chain: ChainContext +): Promise { + return isTokenId(token) + ? Number(await wh.getDecimals(token.chain, token.address)) + : chain.config.nativeTokenDecimals; +} + + ``` + + You can view the [constants for platform names](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/3eae2e91fc3a6fec859eb87cfa85a4c92c65466f/core/base/src/constants/platforms.ts#L6){target=\_blank} in the GitHub repo for a list of supported platforms + +## Verify Token Registration (Attestation) + +Tokens must be registered on the destination chain before they can be bridged. This process involves submitting an attestation with the native token metadata to the destination chain, which enables the destination chain's Token Bridge contract to create a corresponding wrapped version with the same attributes as the native token. + +Registration via attestation is only required the first time a given token is sent to that specific destination chain. Follow these steps to check the registration status of a token: + +1. Create a new file named `transfer.ts`: + ```bash + touch transfer.ts + ``` + +2. Open your `transfer.ts` file and add the following code: + ```typescript title="transfer.ts" + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import { getSigner, getTokenDecimals } from './helpers'; + +async function transferTokens() { + // Initialize wh instance + const wh = await wormhole('Testnet', [evm, solana]); + // Define sourceChain and destinationChain, get chain contexts + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + // Load signers for both chains + const sourceSigner = await getSigner(sourceChain); + const destinationSigner = await getSigner(destinationChain); + + // Define token and amount to transfer + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Replace with amount you want to transfer + // This is a human-readable number, e.g., 0.2 for 0.2 tokens + const amount = 0; + // Convert to raw units based on token decimals + const decimals = await getTokenDecimals(wh, tokenId, sourceChain); + const transferAmount = BigInt(Math.floor(amount * 10 ** decimals)); + + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID, continues with transfer + // Not registered = runs the attestation flow to register the token + let wrappedToken: TokenId; + try { + wrappedToken = await wh.getWrappedAsset(destinationChain.chain, tokenId); + console.log( + '✅ Token already registered on destination:', + wrappedToken.address + ); + } catch (e) { + console.log( + '⚠️ Token is NOT registered on destination. Running attestation flow...' + ); + // Token attestation and registration flow here if needed + + transferTokens().catch((e) => { + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + ``` + + This code does the following: + + - Initializes a `wormhole` instance and defines the source and destination chains + - Imports the signer and decimal functions from `helpers.ts` + - Identifies the token and amount to transfer + - Checks to see if a wrapped version of the ERC-20 token to transfer exists on the destination chain + +3. Run the script using the following command: + + ```bash + npx tsx transfer.ts + ``` + + If the token is registered on the destination chain, the address of the existing wrapped asset is returned, and you can continue to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. If the token is not registered, you will see a message similar to the following advising the attestation flow will run: + +
+npx tsx transfer.ts +⚠️ Token is NOT registered on destination. Running attestation flow... + +
+ + If you see this message, follow the steps under "Need to register a token?" before continuing with the rest of the transfer flow code. + + ??? example "Need to register a token?" + Token attestation is a one-time process to register a token on a destination chain. You should only follow these steps if your token registration check indicates a wrapped version does not exist on the destination chain. + + 1. Add the following code to `transfer.ts` to create the attestation for token registration: + ```typescript title="transfer.ts" + // Token attestation and registration flow here if needed + // This is where you will send the transaction to attest the token + const tb = await sourceChain.getTokenBridge(); + // Define the token to attest and a payer address + const token: TokenAddress = toNative( + sourceChain.chain, + tokenId.address.toString() + ); + const payer = toNative(sourceChain.chain, sourceSigner.signer.address()); + // Call the `createAttestation` method to create a new attestation + // and sign and send the transaction + for await (const tx of tb.createAttestation(token, payer)) { + const txids = await signSendWait( + sourceChain, + tb.createAttestation(token), + sourceSigner.signer + ); + console.log('✅ Attestation transaction sent:', txids); + // Parse the transaction to get Wormhole message ID + const messages = await sourceChain.parseTransaction(txids[0].txid); + console.log('✅ Attestation messages:', messages); + // Set a timeout for fetching the VAA, this can take several minutes + // depending on the source chain network and finality + const timeout = 25 * 60 * 1000; + // Fetch the VAA for the attestation message + const vaa = await wh.getVaa( + messages[0]!, + 'TokenBridge:AttestMeta', + timeout + ); + if (!vaa) throw new Error('❌ VAA not found before timeout.'); + // Get the token bridge context for the destination chain + // and submit the attestation VAA + const destTb = await destinationChain.getTokenBridge(); + const payer = toNative( + destinationChain.chain, + destinationSigner.signer.address() + ); + const destTxids = await signSendWait( + destinationChain, + destTb.submitAttestation(vaa, payer), + destinationSigner.signer + ); + console.log('✅ Attestation submitted on destination:', destTxids); + } + // Poll for the wrapped token to appear on the destination chain + // before proceeding with the transfer + const maxAttempts = 50; // ~5 minutes with 6s interval + const interval = 6000; + let attempt = 0; + let registered = false; + + while (attempt < maxAttempts && !registered) { + attempt++; + try { + const wrapped = await wh.getWrappedAsset( + destinationChain.chain, + tokenId + ); + console.log( + `✅ Wrapped token is now available on ${destinationChain.chain}:`, + wrapped.address + ); + registered = true; + } catch { + console.log( + `⏳ Waiting for wrapped token to register on ${destinationChain.chain}...` + ); + await new Promise((res) => setTimeout(res, interval)); + } + } + + if (!registered) { + throw new Error( + `❌ Token attestation did not complete in time on ${destinationChain.chain}` + ); + } + + console.log('🚀 Token attestation complete! Proceeding with transfer...'); + } + // Remainder of transfer code + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + ``` + + This code does the following: + + - Gets the Token Bridge protocol for the source chain + - Defines the token to attest for registration on the destination chain and the payer to sign for the transaction + - Calls `createAttestation`, signs, and then sends the transaction + - Waits for the signed VAA confirming the attestation creation + - Sends the VAA to the destination chain to complete registration + - Polls for the wrapped token to be available on the destination chain before continuing the transfer process + + 3. Run the script with the following command: + + ```bash + npx tsx transfer.ts + ``` + + When the attestation and registration are complete, you will see terminal output similar to the following: + +
+npx tsx transfer.ts +⚠️ Token is NOT registered on destination. Running attestation flow... +✅ Attestation transaction sent: [ + { + chain: 'Moonbeam', + txid: '0x2b9878e6d8e92d8ecc96d663904312c18a827ccf0b02380074fdbc0fba7e6b68' + } +] +✅ Attestation messages: [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1505n + } +] + +Retrying Wormholescan:GetVaaBytes, attempt 0/750 +Retrying Wormholescan:GetVaaBytes, attempt 1/750 +.... +Retrying Wormholescan:GetVaaBytes, attempt 10/750 +✅ Attestation submitted on destination: [ + { + chain: 'Solana', + txid: '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } +] +✅ Wrapped token is now available on Solana: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} +🚀 Token attestation complete! Proceeding with transfer... + +
+ + You can now go on to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. + +## Initiate Transfer on Source Chain + +Follow these steps to add the rest of the logic to initiate the token transfer on the source chain: + +1. Open your `transfer.ts` file and add the following code: + + ```typescript title="transfer.ts" + // Remainder of transfer code + const automatic = false; + // Optional native gas amount for automatic transfers only + const nativeGasAmount = '0.001'; // 0.001 of native gas in human-readable format + // Get the decimals for the source chain + const nativeGasDecimals = destinationChain.config.nativeTokenDecimals; + // If automatic, convert to raw units, otherwise set to 0n + const nativeGas = automatic + ? BigInt(Number(nativeGasAmount) * 10 ** nativeGasDecimals) + : 0n; + // Build the token transfer object + const xfer = await wh.tokenTransfer( + tokenId, + transferAmount, + sourceSigner.address, + destinationSigner.address, + automatic, + undefined, // no payload + nativeGas + ); + console.log('🚀 Built transfer object:', xfer.transfer); + + // Initiate, sign, and send the token transfer + const srcTxs = await xfer.initiateTransfer(sourceSigner.signer); + console.log('🔗 Source chain tx sent:', srcTxs); + + // If automatic, no further action is required. The relayer completes the transfer. + if (automatic) { + console.log('✅ Automatic transfer: relayer is handling redemption.'); + return; + } + // For manual transfers, wait for VAA + console.log('⏳ Waiting for attestation (VAA) for manual transfer...'); + const attIds = await xfer.fetchAttestation(120_000); // 2 minutes timeout + console.log('✅ Got attestation ID(s):', attIds); + + // Complete the manual transfer on the destination chain + console.log('↪️ Redeeming transfer on destination...'); + const destTxs = await xfer.completeTransfer(destinationSigner.signer); + console.log('🎉 Destination tx(s) submitted:', destTxs); +} + +transferTokens().catch((e) => { + console.error('❌ Error in transferTokens', e); + process.exit(1); +}); + + ``` + + This code does the following: + + - Defines the transfer as automatic or manual. To use automatic transfer, both the source and destination chain must have an existing `tokenBridgeRelayer` contract. You can check the list of [deployed `tokenBridgeRelayer` contracts](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/base/src/constants/contracts/tokenBridgeRelayer.ts){target=\_blank} in the Wormhole SDK repo to see if your desired chains are supported + - Sets an optional amount for native gas drop-off. This option allows you to send a small amount of the destination chain's native token for gas fees. Native gas drop-off is currently only supported for automatic transfers + - Builds the transfer object, initiates the transfer, signs and sends the transaction + - If the transfer is automatic, the flow ends. Otherwise, the script waits for the signed VAA confirming the transaction on the source chain. The signed VAA is then submitted to the destination chain to claim the tokens and complete the manual transfer + +2. Run the script with the following command: + ```bash + npx tsx transfer.ts + ``` + +3. You will see terminal output similar to the following: + +
+npx tsx transfer.ts +✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: + } +} +🚀 Built transfer object: { + token: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0x39F2f26f247CcC223393396755bfde5ecaeb0648' + } + }, + amount: 200000000000000000n, + from: { + chain: 'Moonbeam', + address: EvmAddress { + type: 'Native', + address: '0xCD8Bcd9A793a7381b3C66C763c3f463f70De4e12' + } + }, + to: { + chain: 'Solana', + address: SolanaAddress { + type: 'Native', + address: [PublicKey [PublicKey(21dmEFTFGBEVoUNjmrxumN6A2xFxNBQXTkK7AmMqNmqD)]] + } + }, + automatic: false, + payload: undefined, + nativeGas: 0n +} +🔗 Source chain tx sent: [ + '0xf318a1098a81063ac8acc9ca117eeb41ae9abfd9cb550a976721d2fa978f313a' +] +⏳ Waiting for attestation (VAA) for manual transfer... +Retrying Wormholescan:GetVaaBytes, attempt 0/30 +Retrying Wormholescan:GetVaaBytes, attempt 1/30 +..... +Retrying Wormholescan:GetVaaBytes, attempt 15/30 +✅ Got attestation ID(s): [ + { + chain: 'Moonbeam', + emitter: UniversalAddress { address: [Uint8Array] }, + sequence: 1506n + } +] +↪️ Redeeming transfer on destination... +🎉 Destination tx(s) submitted: [ + '23NRfFZyKJTDLppJF4GovdegxYAuW2HeXTEFSKKNeA7V82aqTVYTkKeM8sCHCDWe7gWooLAPHARjbAheXoxbbwPk' +] + +
+ +Congratulations! You've now used Token Bridge to transfer wrapped assets using the Wormhole TypeScript SDK. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Portal Bridge**](https://portalbridge.com/){target=\_blank}: visit this site to interact with Wormhole's Portal Bridge featuring a working Token Bridge integration. +- [**Interact with Token Bridge Contracts**](/docs/products/token-bridge/guides/token-bridge-contracts/): this guide explores the Solidity functions used in Token Bridge contracts. +- [**`TokenBridge` and `AutomaticTokenBridge` interfaces**](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/tokenBridge/tokenBridge.ts){target=\_blank}: view the source code defining these key interfaces and their associated namespaces. +--- END CONTENT --- + Doc-Content: https://wormhole.com/docs/products/token-bridge/overview/ --- BEGIN CONTENT --- --- diff --git a/llms.txt b/llms.txt index b96b1956f..450071266 100644 --- a/llms.txt +++ b/llms.txt @@ -73,6 +73,7 @@ - [Token Bridge FAQs](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/faqs.md): Find answers to common questions about the Wormhole Token Bridge, including managing wrapped assets and understanding gas fees. - [Get Started with Token Bridge](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/get-started.md): Perform token transfers using Wormhole’s Token Bridge with the TypeScript SDK, including manual (Solana–Sepolia) and automatic (Fuji–Alfajores). - [Get Started with Token Bridge](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/token-bridge-contracts.md): Learn how to integrate Wormhole's Token Bridge for seamless multichain token transfers with a lock-and-mint mechanism and cross-chain asset management. +- [Transfer Wrapped Assets](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/transfer-wrapped-assets.md): Follow this guide to use Token Bridge to transfer wrapped assets. Includes automatic and manual flows, token attestation, VAA fetching, and manual redemption. - [Token Bridge Overview](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/overview.md): With Wormhole Token Bridge, you can enable secure, multichain communication, build multichain apps, sync data, and coordinate actions across blockchains. - [Create Multichain Tokens](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/tutorials/multichain-token.md): Learn how to create a multichain token, bridge tokens across blockchains, and update metadata for seamless multichain interoperability. - [Transfer Tokens via Token Bridge Tutorial](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/tutorials/transfer-workflow.md): Learn to build a cross-chain native token transfer app using Wormhole’s TypeScript SDK, supporting native token transfers across EVM and non-EVM chains diff --git a/products/token-bridge/guides/.pages b/products/token-bridge/guides/.pages index 52178f6c6..f8303de3a 100644 --- a/products/token-bridge/guides/.pages +++ b/products/token-bridge/guides/.pages @@ -1,3 +1,4 @@ title: Guides nav: +- 'Transfer Wrapped Assets': transfer-wrapped-assets.md - 'Interact with Contracts': token-bridge-contracts.md diff --git a/products/token-bridge/guides/transfer-wrapped-assets.md b/products/token-bridge/guides/transfer-wrapped-assets.md new file mode 100644 index 000000000..de03981ed --- /dev/null +++ b/products/token-bridge/guides/transfer-wrapped-assets.md @@ -0,0 +1,170 @@ +--- +title: Transfer Wrapped Assets +description: Follow this guide to use Token Bridge to transfer wrapped assets. Includes automatic and manual flows, token attestation, VAA fetching, and manual redemption. +categories: Token-Bridge, Transfers, Typescript-SDK +--- + +# Transfer Wrapped Assets + +## Introduction + +This guide demonstrates the transfer of wrapped assets using the Token Bridge protocol via the [TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}. This example will transfer an arbitrary ERC-20 token from Moonbase Alpha to Solana but can be adapted for any supported chains. View this list of chains with [deployed Token Bridge contracts](/docs/products/reference/contract-addresses/#token-bridge){target=\_blank} to verify if your desired source and destination chains are supported. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create a token attestation to register a wrapped version of a token on a destination chain +- Transfer wrapped assets using Token Bridge automatic or manual transfers +- Fetch a signed Verified Action Approval (VAA) +- Manually redeem a signed VAA to claim tokens on a destination chain + +## Prerequisites + +Before you begin, ensure you have the following: + +- [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm){target=\_blank} installed on your machine +- [TypeScript](https://www.typescriptlang.org/download/){target=\_blank} installed globally +- The contract address for the ERC-20 token you wish to transfer +- A wallet setup with the following: + - Private keys for your source and destination chains + - A small amount of gas tokens on your source and destination chains + - A balance on your source chain of the ERC-20 token you want to transfer + +## Set Up Your Token Transfer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for multichain token transfers. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir token-bridge-demo + cd token-bridge-demo + npm init -y + ``` + +2. Install dependencies, including the Wormhole TypeScript SDK: + ```bash + npm install @wormhole-foundation/sdk -D tsx typescript + ``` + +3. Set up secure access to your wallets. This guide assumes you are loading your private key values from a secure keystore of your choice, such as a secrets manager or a CLI-based tool like [`cast wallet`](https://book.getfoundry.sh/reference/cast/cast-wallet){target=\_blank}. + + !!! warning + If you use a `.env` file during development, add it to your `.gitignore` to exclude it from version control. Never commit private keys or mnemonics to your repository. + +4. Create a new file named `helpers.ts` to hold signer and decimal functions: + ```bash + touch helpers.ts + ``` + +5. Open `helpers.ts` and add the following code: + ```typescript title="helpers.ts" + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/helpers.ts' + ``` + + You can view the [constants for platform names](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/3eae2e91fc3a6fec859eb87cfa85a4c92c65466f/core/base/src/constants/platforms.ts#L6){target=\_blank} in the GitHub repo for a list of supported platforms + +## Verify Token Registration (Attestation) + +Tokens must be registered on the destination chain before they can be bridged. This process involves submitting an attestation with the native token metadata to the destination chain, which enables the destination chain's Token Bridge contract to create a corresponding wrapped version with the same attributes as the native token. + +Registration via attestation is only required the first time a given token is sent to that specific destination chain. Follow these steps to check the registration status of a token: + +1. Create a new file named `transfer.ts`: + ```bash + touch transfer.ts + ``` + +2. Open your `transfer.ts` file and add the following code: + ```typescript title="transfer.ts" + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts:1:47' + // Token attestation and registration flow here if needed + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts:127:127' + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts:169:174' + ``` + + This code does the following: + + - Initializes a `wormhole` instance and defines the source and destination chains + - Imports the signer and decimal functions from `helpers.ts` + - Identifies the token and amount to transfer + - Checks to see if a wrapped version of the ERC-20 token to transfer exists on the destination chain + +3. Run the script using the following command: + + ```bash + npx tsx transfer.ts + ``` + + If the token is registered on the destination chain, the address of the existing wrapped asset is returned, and you can continue to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. If the token is not registered, you will see a message similar to the following advising the attestation flow will run: + + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/terminal01.html' + + If you see this message, follow the steps under "Need to register a token?" before continuing with the rest of the transfer flow code. + + ??? example "Need to register a token?" + Token attestation is a one-time process to register a token on a destination chain. You should only follow these steps if your token registration check indicates a wrapped version does not exist on the destination chain. + + 1. Add the following code to `transfer.ts` to create the attestation for token registration: + ```typescript title="transfer.ts" + // Token attestation and registration flow here if needed + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts:48:127' + // Remainder of transfer code + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts:171:174' + ``` + + This code does the following: + + - Gets the Token Bridge protocol for the source chain + - Defines the token to attest for registration on the destination chain and the payer to sign for the transaction + - Calls `createAttestation`, signs, and then sends the transaction + - Waits for the signed VAA confirming the attestation creation + - Sends the VAA to the destination chain to complete registration + - Polls for the wrapped token to be available on the destination chain before continuing the transfer process + + 3. Run the script with the following command: + + ```bash + npx tsx transfer.ts + ``` + + When the attestation and registration are complete, you will see terminal output similar to the following: + + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/terminal02.html' + + You can now go on to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. + +## Initiate Transfer on Source Chain + +Follow these steps to add the rest of the logic to initiate the token transfer on the source chain: + +1. Open your `transfer.ts` file and add the following code: + + ```typescript title="transfer.ts" + // Remainder of transfer code + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/transfer01.ts:129:174' + + ``` + + This code does the following: + + - Defines the transfer as automatic or manual. To use automatic transfer, both the source and destination chain must have an existing `tokenBridgeRelayer` contract. You can check the list of [deployed `tokenBridgeRelayer` contracts](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/base/src/constants/contracts/tokenBridgeRelayer.ts){target=\_blank} in the Wormhole SDK repo to see if your desired chains are supported + - Sets an optional amount for native gas drop-off. This option allows you to send a small amount of the destination chain's native token for gas fees. Native gas drop-off is currently only supported for automatic transfers + - Builds the transfer object, initiates the transfer, signs and sends the transaction + - If the transfer is automatic, the flow ends. Otherwise, the script waits for the signed VAA confirming the transaction on the source chain. The signed VAA is then submitted to the destination chain to claim the tokens and complete the manual transfer + +2. Run the script with the following command: + ```bash + npx tsx transfer.ts + ``` + +3. You will see terminal output similar to the following: + + --8<-- 'code/products/token-bridge/guides/transfer-wrapped-assets/terminal03.html' + +Congratulations! You've now used Token Bridge to transfer wrapped assets using the Wormhole TypeScript SDK. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Portal Bridge**](https://portalbridge.com/){target=\_blank}: visit this site to interact with Wormhole's Portal Bridge featuring a working Token Bridge integration. +- [**Interact with Token Bridge Contracts**](/docs/products/token-bridge/guides/token-bridge-contracts/): this guide explores the Solidity functions used in Token Bridge contracts. +- [**`TokenBridge` and `AutomaticTokenBridge` interfaces**](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/tokenBridge/tokenBridge.ts){target=\_blank}: view the source code defining these key interfaces and their associated namespaces. \ No newline at end of file