diff --git a/.snippets/code/products/token-bridge/guides/attest-tokens/attest.ts b/.snippets/code/products/token-bridge/guides/attest-tokens/attest.ts new file mode 100644 index 000000000..9fcd3c3bd --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/attest-tokens/attest.ts @@ -0,0 +1,126 @@ +import { + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { getSigner } from './helper'; + +async function attestToken() { + // Initialize wormhole instance, define the network, platforms, and chains + const wh = await wormhole('Testnet', [evm, solana]); + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + + // Define the token to check for a wrapped version + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID + // 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...' + ); + // Attestation flow code + // 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(); + // Get the signer for the source chain + const sourceSigner = await getSigner(sourceChain); + // 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(); + // Get the signer for the destination chain + const destinationSigner = await getSigner(destinationChain); + 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 + 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! Token registered with ${destinationChain.chain}.` + ); + } +} + +attestToken().catch((e) => { + console.error('❌ Error in attestToken', e); + process.exit(1); +}); \ No newline at end of file diff --git a/.snippets/code/products/token-bridge/guides/attest-tokens/helper.ts b/.snippets/code/products/token-bridge/guides/attest-tokens/helper.ts new file mode 100644 index 000000000..b377050ce --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/attest-tokens/helper.ts @@ -0,0 +1,59 @@ +import { + Chain, + ChainAddress, + ChainContext, + Wormhole, + Network, + Signer, +} 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()), + }; +} diff --git a/.snippets/code/products/token-bridge/guides/attest-tokens/terminal01.html b/.snippets/code/products/token-bridge/guides/attest-tokens/terminal01.html new file mode 100644 index 000000000..259aa8f8f --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/attest-tokens/terminal01.html @@ -0,0 +1,10 @@ +
+ npx tsx attest.ts + ✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: BN: 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca + } + } + +
\ No newline at end of file diff --git a/.snippets/code/products/token-bridge/guides/attest-tokens/terminal02.html b/.snippets/code/products/token-bridge/guides/attest-tokens/terminal02.html new file mode 100644 index 000000000..ab81d4920 --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/attest-tokens/terminal02.html @@ -0,0 +1,5 @@ +
+ npx tsx attest.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/attest-tokens/terminal03.html b/.snippets/code/products/token-bridge/guides/attest-tokens/terminal03.html new file mode 100644 index 000000000..7edb4604b --- /dev/null +++ b/.snippets/code/products/token-bridge/guides/attest-tokens/terminal03.html @@ -0,0 +1,24 @@ +
+ npx tsx attest.ts + ⚠️ Token is NOT registered on destination. Running attestation + flow... + ✅ Attestation transaction sent: [ { chain: 'Moonbeam', txid: + '0xbaf7429e1099cac6f39ef7e3c30e38776cfb5b6be837dcd8793374c8ee491799' } + ] + ✅ Attestation messages: [ { chain: 'Moonbeam', emitter: UniversalAddress { + address: [Uint8Array] }, sequence: 1507n } ] + Retrying Wormholescan:GetVaaBytes, attempt 0/750 + Retrying Wormholescan:GetVaaBytes, attempt 1/750 + ..... + Retrying Wormholescan:GetVaaBytes, attempt 10/750 + 📨 Submitting attestation VAA to Solana... + ✅ Attestation submitted on destination: [ { chain: 'Solana', txid: + '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } ] + ✅ Wrapped token is now available on Solana: SolanaAddress { type: + 'Native', address: PublicKey + [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { _bn: BN: + 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca } } + 🚀 Token attestation complete! + +
\ No newline at end of file diff --git a/llms-files/llms-token-bridge.txt b/llms-files/llms-token-bridge.txt index d26dc6cf1..6b7c5eb9b 100644 --- a/llms-files/llms-token-bridge.txt +++ b/llms-files/llms-token-bridge.txt @@ -17,6 +17,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/concepts/transfer-flow.md [type: other] 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/attest-tokens.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/overview.md [type: other] @@ -837,6 +838,506 @@ Now that you've completed a manual multichain token transfer, explore these guid - [Create Multichain Tokens](/docs/products/token-bridge/tutorials/multichain-token){target=\_blank}: Learn how to issue tokens that work across chains. --- END CONTENT --- +Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/attest-tokens/ +--- BEGIN CONTENT --- +--- +title: Token Attestation +description: Create and submit a token attestation to register a token for transfer with Token Bridge using the TypeScript SDK. Required before first-time transfers. +categories: Token-Bridge, Transfer +--- + +# Token Attestation + +## Introduction + +This guide demonstrates token attestation for registering a token for transfer using the [Token Bridge](/docs/products/token-bridge/overview) protocol. An attestation of the token's metadata (e.g., symbol, name, decimals) ensures consistent handling by the destination chain for ease of multichain interoperability. These steps are only required the first time a token is sent to a particular destination chain. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create and submit token attestation to register a wrapped version of a token on a destination chain +- Check for the wrapped version to become available on the destination chain and return the wrapped token address + +The example will register an arbitrary ERC-20 token deployed to Moonbase Alpha for transfer to Solana but can be adapted for any supported chains. + +## 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 token you wish to register +- 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 + +## Set Up Your Developer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for token attestation. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir attest-token + cd attest-token + npm init -y + ``` + +2. Install dependencies, including the [Wormhole TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}: + ```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 `helper.ts` to hold signer functions: + ```bash + touch helper.ts + ``` + +5. Open `helper.ts` and add the following code: + ```typescript title="helper.ts" + import { + Chain, + ChainAddress, + ChainContext, + Wormhole, + Network, + Signer, +} 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()), + }; +} + + ``` + + 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 + +## Check for Wrapped Version + +If you are working with a newly created token that you know has never been transferred to the destination chain, you can continue to the [Create Attestation on the Source Chain](#create-attestation-on-the-source-chain) section. + +Since attestation is a one-time process, it is good practice when working with existing tokens to incorporate a check for wrapped versions into your Token Bridge transfer flow. Follow these steps to check for a wrapped version of a token: + +1. Create a new file called `attest.ts` to hold the wrapped version check and attestation logic: + ```bash + touch attest.ts + ``` + +2. Open `attest.ts` and add the following code: + ```typescript title="attest.ts" + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { getSigner } from './helper'; + +async function attestToken() { + // Initialize wormhole instance, define the network, platforms, and chains + const wh = await wormhole('Testnet', [evm, solana]); + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + + // Define the token to check for a wrapped version + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID + // 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...' + ); + // Attestation flow code + } + +attestToken().catch((e) => { + console.error('❌ Error in attestToken', e); + process.exit(1); +});
+ ``` + + After initializing a Wormhole instance and defining the source and destination chains, this code does the following: + + - **Defines the token to check**: use the contract address on the source chain for this value. + - **Calls `getWrappedAsset`**: part of the [`Wormhole` class](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L47){target=\_blank}, the [`getWrappedAsset`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L205){target=\_blank} method: + - Accepts a [`TokenId`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/platforms/aptos/protocols/tokenBridge/src/types.ts#L12){target=\_blank} representing a token on the source chain. + - Checks for a corresponding wrapped version of the destination chain's Token Bridge contract. + - Returns the `TokenId` for the wrapped token on the destination chain if a wrapped version exists. + +3. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +4. If the token has a wrapped version registered with the destination chain Token Bridge contract, you will see terminal output similar to the following: + +
+npx tsx attest.ts +✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: BN: 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca + } + } + +
+ + You can safely use Token Bridge to transfer this token to the destination chain. + + If a wrapped version isn't found on the destination chain, your terminal output will be similar to the following and you must attest the token before transfer: + +
+npx tsx attest.ts +⚠️ Token is NOT registered on destination. Running attestation flow... + +
+ +## Create Attestation on the Source Chain + +To create the attestation transaction on the source chain, open `attest.ts` and replace the "// Attestation flow code" comment with the following code: +```typescript title="attest.ts" +const tb = await sourceChain.getTokenBridge(); + // Get the signer for the source chain + const sourceSigner = await getSigner(sourceChain); + // 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); +``` + +This code does the following: + +- **Gets the source chain Token Bridge context**: this is where the transaction is sent to create the attestation. +- Defines the token to attest and the payer. +- **Calls `createAttestation`**: defined in the [`TokenBridge` interface](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L123){target=\_blank}, the [`createAttestation`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L188){target=\_blank} method does the following: + - Accepts a `TokenAddress` representing the token on its native chain. + - Accepts an optional `payer` address to cover the transaction fees for the attestation transaction. + - Prepares an attestation for the token including metadata such as address, symbol, and decimals. + - Returns an `AsyncGenerator` that yields unsigned transactions, which are then signed and sent to initiate the attestation process on the source chain. + +## Submit Attestation on Destination Chain + +The attestation flow finishes with the following: + +- Using the transaction ID returned from the `createAttestation` transaction on the source chain to retrieve the associated signed `TokenBridge:AttestMeta` VAA. +- Submitting the signed VAA to the destination chain to provide Guardian-backed verification of the attestation transaction on the source chain. +- The destination chain uses the attested metadata to create the wrapped version of the token and register it with its Token Bridge contract. + +Follow these steps to complete your attestation flow logic: + +1. Add the following code to `attest.ts`: + ```typescript title="attest.ts" + 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(); + // Get the signer for the destination chain + const destinationSigner = await getSigner(destinationChain); + 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 + 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! Token registered with ${destinationChain.chain}.` + ); + } +} + ``` + +2. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +3. You will see terminal output similar to the following: + +
+npx tsx attest.ts +⚠️ Token is NOT registered on destination. Running attestation + flow... +✅ Attestation transaction sent: [ { chain: 'Moonbeam', txid: + '0xbaf7429e1099cac6f39ef7e3c30e38776cfb5b6be837dcd8793374c8ee491799' } + ] +✅ Attestation messages: [ { chain: 'Moonbeam', emitter: UniversalAddress { + address: [Uint8Array] }, sequence: 1507n } ] +Retrying Wormholescan:GetVaaBytes, attempt 0/750 +Retrying Wormholescan:GetVaaBytes, attempt 1/750 +..... +Retrying Wormholescan:GetVaaBytes, attempt 10/750 +📨 Submitting attestation VAA to Solana... +✅ Attestation submitted on destination: [ { chain: 'Solana', txid: + '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } ] +✅ Wrapped token is now available on Solana: SolanaAddress { type: + 'Native', address: PublicKey + [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { _bn: BN: + 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca } } +🚀 Token attestation complete! + +
+ + ??? example "View complete script" + ```typescript title="attest.ts" + import { + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { getSigner } from './helper'; + +async function attestToken() { + // Initialize wormhole instance, define the network, platforms, and chains + const wh = await wormhole('Testnet', [evm, solana]); + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + + // Define the token to check for a wrapped version + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID + // 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...' + ); + // Attestation flow code + // 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(); + // Get the signer for the source chain + const sourceSigner = await getSigner(sourceChain); + // 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(); + // Get the signer for the destination chain + const destinationSigner = await getSigner(destinationChain); + 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 + 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! Token registered with ${destinationChain.chain}.` + ); + } +} + +attestToken().catch((e) => { + console.error('❌ Error in attestToken', e); + process.exit(1); +}); + ``` + +Congratulations! You've successfully created and submitted an attestation to register a token for transfer via Token Bridge. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Transfer Wrapped Assets**](/docs/products/token-bridge/guides/attest-tokens): follow this guide to incorporate token attestation and registration into an end-to-end Token Bridge transfer flow. + +TODO: What else should be here? +--- END CONTENT --- + Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/token-bridge-contracts/ --- BEGIN CONTENT --- --- diff --git a/llms-files/llms-transfer.txt b/llms-files/llms-transfer.txt index 93c101ab3..5ee93d0cd 100644 --- a/llms-files/llms-transfer.txt +++ b/llms-files/llms-transfer.txt @@ -47,6 +47,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/settlement/overview.md [type: other] Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/concepts/transfer-flow.md [type: other] 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/guides/attest-tokens.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/overview.md [type: other] @@ -7529,6 +7530,506 @@ Updating the metadata (such as the token image, name, or symbol) of a wrapped to To request an update, contact Solscan via [support@solscan.io](mailto:support@solscan.io) or their [contact form](https://solscan.io/contactus){target=\_blank}. --- END CONTENT --- +Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/attest-tokens/ +--- BEGIN CONTENT --- +--- +title: Token Attestation +description: Create and submit a token attestation to register a token for transfer with Token Bridge using the TypeScript SDK. Required before first-time transfers. +categories: Token-Bridge, Transfer +--- + +# Token Attestation + +## Introduction + +This guide demonstrates token attestation for registering a token for transfer using the [Token Bridge](/docs/products/token-bridge/overview) protocol. An attestation of the token's metadata (e.g., symbol, name, decimals) ensures consistent handling by the destination chain for ease of multichain interoperability. These steps are only required the first time a token is sent to a particular destination chain. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create and submit token attestation to register a wrapped version of a token on a destination chain +- Check for the wrapped version to become available on the destination chain and return the wrapped token address + +The example will register an arbitrary ERC-20 token deployed to Moonbase Alpha for transfer to Solana but can be adapted for any supported chains. + +## 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 token you wish to register +- 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 + +## Set Up Your Developer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for token attestation. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir attest-token + cd attest-token + npm init -y + ``` + +2. Install dependencies, including the [Wormhole TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}: + ```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 `helper.ts` to hold signer functions: + ```bash + touch helper.ts + ``` + +5. Open `helper.ts` and add the following code: + ```typescript title="helper.ts" + import { + Chain, + ChainAddress, + ChainContext, + Wormhole, + Network, + Signer, +} 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()), + }; +} + + ``` + + 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 + +## Check for Wrapped Version + +If you are working with a newly created token that you know has never been transferred to the destination chain, you can continue to the [Create Attestation on the Source Chain](#create-attestation-on-the-source-chain) section. + +Since attestation is a one-time process, it is good practice when working with existing tokens to incorporate a check for wrapped versions into your Token Bridge transfer flow. Follow these steps to check for a wrapped version of a token: + +1. Create a new file called `attest.ts` to hold the wrapped version check and attestation logic: + ```bash + touch attest.ts + ``` + +2. Open `attest.ts` and add the following code: + ```typescript title="attest.ts" + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { getSigner } from './helper'; + +async function attestToken() { + // Initialize wormhole instance, define the network, platforms, and chains + const wh = await wormhole('Testnet', [evm, solana]); + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + + // Define the token to check for a wrapped version + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID + // 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...' + ); + // Attestation flow code + } + +attestToken().catch((e) => { + console.error('❌ Error in attestToken', e); + process.exit(1); +});
+ ``` + + After initializing a Wormhole instance and defining the source and destination chains, this code does the following: + + - **Defines the token to check**: use the contract address on the source chain for this value. + - **Calls `getWrappedAsset`**: part of the [`Wormhole` class](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L47){target=\_blank}, the [`getWrappedAsset`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L205){target=\_blank} method: + - Accepts a [`TokenId`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/platforms/aptos/protocols/tokenBridge/src/types.ts#L12){target=\_blank} representing a token on the source chain. + - Checks for a corresponding wrapped version of the destination chain's Token Bridge contract. + - Returns the `TokenId` for the wrapped token on the destination chain if a wrapped version exists. + +3. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +4. If the token has a wrapped version registered with the destination chain Token Bridge contract, you will see terminal output similar to the following: + +
+npx tsx attest.ts +✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: BN: 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca + } + } + +
+ + You can safely use Token Bridge to transfer this token to the destination chain. + + If a wrapped version isn't found on the destination chain, your terminal output will be similar to the following and you must attest the token before transfer: + +
+npx tsx attest.ts +⚠️ Token is NOT registered on destination. Running attestation flow... + +
+ +## Create Attestation on the Source Chain + +To create the attestation transaction on the source chain, open `attest.ts` and replace the "// Attestation flow code" comment with the following code: +```typescript title="attest.ts" +const tb = await sourceChain.getTokenBridge(); + // Get the signer for the source chain + const sourceSigner = await getSigner(sourceChain); + // 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); +``` + +This code does the following: + +- **Gets the source chain Token Bridge context**: this is where the transaction is sent to create the attestation. +- Defines the token to attest and the payer. +- **Calls `createAttestation`**: defined in the [`TokenBridge` interface](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L123){target=\_blank}, the [`createAttestation`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L188){target=\_blank} method does the following: + - Accepts a `TokenAddress` representing the token on its native chain. + - Accepts an optional `payer` address to cover the transaction fees for the attestation transaction. + - Prepares an attestation for the token including metadata such as address, symbol, and decimals. + - Returns an `AsyncGenerator` that yields unsigned transactions, which are then signed and sent to initiate the attestation process on the source chain. + +## Submit Attestation on Destination Chain + +The attestation flow finishes with the following: + +- Using the transaction ID returned from the `createAttestation` transaction on the source chain to retrieve the associated signed `TokenBridge:AttestMeta` VAA. +- Submitting the signed VAA to the destination chain to provide Guardian-backed verification of the attestation transaction on the source chain. +- The destination chain uses the attested metadata to create the wrapped version of the token and register it with its Token Bridge contract. + +Follow these steps to complete your attestation flow logic: + +1. Add the following code to `attest.ts`: + ```typescript title="attest.ts" + 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(); + // Get the signer for the destination chain + const destinationSigner = await getSigner(destinationChain); + 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 + 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! Token registered with ${destinationChain.chain}.` + ); + } +} + ``` + +2. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +3. You will see terminal output similar to the following: + +
+npx tsx attest.ts +⚠️ Token is NOT registered on destination. Running attestation + flow... +✅ Attestation transaction sent: [ { chain: 'Moonbeam', txid: + '0xbaf7429e1099cac6f39ef7e3c30e38776cfb5b6be837dcd8793374c8ee491799' } + ] +✅ Attestation messages: [ { chain: 'Moonbeam', emitter: UniversalAddress { + address: [Uint8Array] }, sequence: 1507n } ] +Retrying Wormholescan:GetVaaBytes, attempt 0/750 +Retrying Wormholescan:GetVaaBytes, attempt 1/750 +..... +Retrying Wormholescan:GetVaaBytes, attempt 10/750 +📨 Submitting attestation VAA to Solana... +✅ Attestation submitted on destination: [ { chain: 'Solana', txid: + '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } ] +✅ Wrapped token is now available on Solana: SolanaAddress { type: + 'Native', address: PublicKey + [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { _bn: BN: + 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca } } +🚀 Token attestation complete! + +
+ + ??? example "View complete script" + ```typescript title="attest.ts" + import { + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { getSigner } from './helper'; + +async function attestToken() { + // Initialize wormhole instance, define the network, platforms, and chains + const wh = await wormhole('Testnet', [evm, solana]); + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + + // Define the token to check for a wrapped version + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID + // 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...' + ); + // Attestation flow code + // 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(); + // Get the signer for the source chain + const sourceSigner = await getSigner(sourceChain); + // 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(); + // Get the signer for the destination chain + const destinationSigner = await getSigner(destinationChain); + 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 + 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! Token registered with ${destinationChain.chain}.` + ); + } +} + +attestToken().catch((e) => { + console.error('❌ Error in attestToken', e); + process.exit(1); +}); + ``` + +Congratulations! You've successfully created and submitted an attestation to register a token for transfer via Token Bridge. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Transfer Wrapped Assets**](/docs/products/token-bridge/guides/attest-tokens): follow this guide to incorporate token attestation and registration into an end-to-end Token Bridge transfer flow. + +TODO: What else should be here? +--- END CONTENT --- + Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/token-bridge-contracts/ --- BEGIN CONTENT --- --- diff --git a/llms-full.txt b/llms-full.txt index 5f385fe86..792c449b2 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -74,6 +74,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/concepts/transfer-flow.md 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/attest-tokens.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/overview.md Doc-Page: https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/tutorials/multichain-token.md @@ -16805,6 +16806,506 @@ Now that you've completed a manual multichain token transfer, explore these guid - [Create Multichain Tokens](/docs/products/token-bridge/tutorials/multichain-token){target=\_blank}: Learn how to issue tokens that work across chains. --- END CONTENT --- +Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/attest-tokens/ +--- BEGIN CONTENT --- +--- +title: Token Attestation +description: Create and submit a token attestation to register a token for transfer with Token Bridge using the TypeScript SDK. Required before first-time transfers. +categories: Token-Bridge, Transfer +--- + +# Token Attestation + +## Introduction + +This guide demonstrates token attestation for registering a token for transfer using the [Token Bridge](/docs/products/token-bridge/overview) protocol. An attestation of the token's metadata (e.g., symbol, name, decimals) ensures consistent handling by the destination chain for ease of multichain interoperability. These steps are only required the first time a token is sent to a particular destination chain. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create and submit token attestation to register a wrapped version of a token on a destination chain +- Check for the wrapped version to become available on the destination chain and return the wrapped token address + +The example will register an arbitrary ERC-20 token deployed to Moonbase Alpha for transfer to Solana but can be adapted for any supported chains. + +## 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 token you wish to register +- 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 + +## Set Up Your Developer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for token attestation. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir attest-token + cd attest-token + npm init -y + ``` + +2. Install dependencies, including the [Wormhole TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}: + ```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 `helper.ts` to hold signer functions: + ```bash + touch helper.ts + ``` + +5. Open `helper.ts` and add the following code: + ```typescript title="helper.ts" + import { + Chain, + ChainAddress, + ChainContext, + Wormhole, + Network, + Signer, +} 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()), + }; +} + + ``` + + 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 + +## Check for Wrapped Version + +If you are working with a newly created token that you know has never been transferred to the destination chain, you can continue to the [Create Attestation on the Source Chain](#create-attestation-on-the-source-chain) section. + +Since attestation is a one-time process, it is good practice when working with existing tokens to incorporate a check for wrapped versions into your Token Bridge transfer flow. Follow these steps to check for a wrapped version of a token: + +1. Create a new file called `attest.ts` to hold the wrapped version check and attestation logic: + ```bash + touch attest.ts + ``` + +2. Open `attest.ts` and add the following code: + ```typescript title="attest.ts" + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { getSigner } from './helper'; + +async function attestToken() { + // Initialize wormhole instance, define the network, platforms, and chains + const wh = await wormhole('Testnet', [evm, solana]); + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + + // Define the token to check for a wrapped version + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID + // 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...' + ); + // Attestation flow code + } + +attestToken().catch((e) => { + console.error('❌ Error in attestToken', e); + process.exit(1); +});
+ ``` + + After initializing a Wormhole instance and defining the source and destination chains, this code does the following: + + - **Defines the token to check**: use the contract address on the source chain for this value. + - **Calls `getWrappedAsset`**: part of the [`Wormhole` class](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L47){target=\_blank}, the [`getWrappedAsset`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L205){target=\_blank} method: + - Accepts a [`TokenId`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/platforms/aptos/protocols/tokenBridge/src/types.ts#L12){target=\_blank} representing a token on the source chain. + - Checks for a corresponding wrapped version of the destination chain's Token Bridge contract. + - Returns the `TokenId` for the wrapped token on the destination chain if a wrapped version exists. + +3. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +4. If the token has a wrapped version registered with the destination chain Token Bridge contract, you will see terminal output similar to the following: + +
+npx tsx attest.ts +✅ Token already registered on destination: SolanaAddress { + type: 'Native', + address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { + _bn: BN: 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca + } + } + +
+ + You can safely use Token Bridge to transfer this token to the destination chain. + + If a wrapped version isn't found on the destination chain, your terminal output will be similar to the following and you must attest the token before transfer: + +
+npx tsx attest.ts +⚠️ Token is NOT registered on destination. Running attestation flow... + +
+ +## Create Attestation on the Source Chain + +To create the attestation transaction on the source chain, open `attest.ts` and replace the "// Attestation flow code" comment with the following code: +```typescript title="attest.ts" +const tb = await sourceChain.getTokenBridge(); + // Get the signer for the source chain + const sourceSigner = await getSigner(sourceChain); + // 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); +``` + +This code does the following: + +- **Gets the source chain Token Bridge context**: this is where the transaction is sent to create the attestation. +- Defines the token to attest and the payer. +- **Calls `createAttestation`**: defined in the [`TokenBridge` interface](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L123){target=\_blank}, the [`createAttestation`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L188){target=\_blank} method does the following: + - Accepts a `TokenAddress` representing the token on its native chain. + - Accepts an optional `payer` address to cover the transaction fees for the attestation transaction. + - Prepares an attestation for the token including metadata such as address, symbol, and decimals. + - Returns an `AsyncGenerator` that yields unsigned transactions, which are then signed and sent to initiate the attestation process on the source chain. + +## Submit Attestation on Destination Chain + +The attestation flow finishes with the following: + +- Using the transaction ID returned from the `createAttestation` transaction on the source chain to retrieve the associated signed `TokenBridge:AttestMeta` VAA. +- Submitting the signed VAA to the destination chain to provide Guardian-backed verification of the attestation transaction on the source chain. +- The destination chain uses the attested metadata to create the wrapped version of the token and register it with its Token Bridge contract. + +Follow these steps to complete your attestation flow logic: + +1. Add the following code to `attest.ts`: + ```typescript title="attest.ts" + 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(); + // Get the signer for the destination chain + const destinationSigner = await getSigner(destinationChain); + 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 + 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! Token registered with ${destinationChain.chain}.` + ); + } +} + ``` + +2. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +3. You will see terminal output similar to the following: + +
+npx tsx attest.ts +⚠️ Token is NOT registered on destination. Running attestation + flow... +✅ Attestation transaction sent: [ { chain: 'Moonbeam', txid: + '0xbaf7429e1099cac6f39ef7e3c30e38776cfb5b6be837dcd8793374c8ee491799' } + ] +✅ Attestation messages: [ { chain: 'Moonbeam', emitter: UniversalAddress { + address: [Uint8Array] }, sequence: 1507n } ] +Retrying Wormholescan:GetVaaBytes, attempt 0/750 +Retrying Wormholescan:GetVaaBytes, attempt 1/750 +..... +Retrying Wormholescan:GetVaaBytes, attempt 10/750 +📨 Submitting attestation VAA to Solana... +✅ Attestation submitted on destination: [ { chain: 'Solana', txid: + '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r' + } ] +✅ Wrapped token is now available on Solana: SolanaAddress { type: + 'Native', address: PublicKey + [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] { _bn: BN: + 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca } } +🚀 Token attestation complete! + +
+ + ??? example "View complete script" + ```typescript title="attest.ts" + import { + wormhole, + Wormhole, + TokenId, + TokenAddress, +} from '@wormhole-foundation/sdk'; +import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect'; +import evm from '@wormhole-foundation/sdk/evm'; +import solana from '@wormhole-foundation/sdk/solana'; +import { getSigner } from './helper'; + +async function attestToken() { + // Initialize wormhole instance, define the network, platforms, and chains + const wh = await wormhole('Testnet', [evm, solana]); + const sourceChain = wh.getChain('Moonbeam'); + const destinationChain = wh.getChain('Solana'); + + // Define the token to check for a wrapped version + const tokenId: TokenId = Wormhole.tokenId( + sourceChain.chain, + 'INSERT_TOKEN_CONTRACT_ADDRESS' + ); + // Check if the token is registered with destinationChain token bridge contract + // Registered = returns the wrapped token ID + // 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...' + ); + // Attestation flow code + // 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(); + // Get the signer for the source chain + const sourceSigner = await getSigner(sourceChain); + // 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(); + // Get the signer for the destination chain + const destinationSigner = await getSigner(destinationChain); + 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 + 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! Token registered with ${destinationChain.chain}.` + ); + } +} + +attestToken().catch((e) => { + console.error('❌ Error in attestToken', e); + process.exit(1); +}); + ``` + +Congratulations! You've successfully created and submitted an attestation to register a token for transfer via Token Bridge. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Transfer Wrapped Assets**](/docs/products/token-bridge/guides/attest-tokens): follow this guide to incorporate token attestation and registration into an end-to-end Token Bridge transfer flow. + +TODO: What else should be here? +--- END CONTENT --- + Doc-Content: https://wormhole.com/docs/products/token-bridge/guides/token-bridge-contracts/ --- BEGIN CONTENT --- --- diff --git a/llms.txt b/llms.txt index 3c2fa636c..12d2428f9 100644 --- a/llms.txt +++ b/llms.txt @@ -72,6 +72,7 @@ - [Flow of a Token Bridge Transfer](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/concepts/transfer-flow.md): Learn how the Wormhole Token Bridge enables secure, cross-chain token transfers by combining token-specific logic with Wormhole's core message-passing layer. - [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). +- [Token Attestation](https://raw.githubusercontent.com/wormhole-foundation/wormhole-docs/refs/heads/main/products/token-bridge/guides/attest-tokens.md): Create and submit a token attestation to register a token for transfer with Token Bridge using the TypeScript SDK. Required before first-time transfers. - [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. - [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. diff --git a/products/token-bridge/guides/.pages b/products/token-bridge/guides/.pages index 52178f6c6..daf168a97 100644 --- a/products/token-bridge/guides/.pages +++ b/products/token-bridge/guides/.pages @@ -1,3 +1,3 @@ title: Guides nav: -- 'Interact with Contracts': token-bridge-contracts.md +- 'Token Attestation': attest-tokens.md diff --git a/products/token-bridge/guides/attest-tokens.md b/products/token-bridge/guides/attest-tokens.md new file mode 100644 index 000000000..e0dc8672f --- /dev/null +++ b/products/token-bridge/guides/attest-tokens.md @@ -0,0 +1,157 @@ +--- +title: Token Attestation +description: Create and submit a token attestation to register a token for transfer with Token Bridge using the TypeScript SDK. Required before first-time transfers. +categories: Token-Bridge, Transfer +--- + +# Token Attestation + +## Introduction + +This guide demonstrates token attestation for registering a token for transfer using the [Token Bridge](/docs/products/token-bridge/overview) protocol. An attestation of the token's metadata (e.g., symbol, name, decimals) ensures consistent handling by the destination chain for ease of multichain interoperability. These steps are only required the first time a token is sent to a particular destination chain. + +Completing this guide will help you to accomplish the following: + +- Verify if a wrapped version of a token exists on a destination chain +- Create and submit token attestation to register a wrapped version of a token on a destination chain +- Check for the wrapped version to become available on the destination chain and return the wrapped token address + +The example will register an arbitrary ERC-20 token deployed to Moonbase Alpha for transfer to Solana but can be adapted for any supported chains. + +## 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 token you wish to register +- 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 + +## Set Up Your Developer Environment + +Follow these steps to initialize your project, install dependencies, and prepare your developer environment for token attestation. + +1. Create a new directory and initialize a Node.js project using the following commands: + ```bash + mkdir attest-token + cd attest-token + npm init -y + ``` + +2. Install dependencies, including the [Wormhole TypeScript SDK](https://github.com/wormhole-foundation/wormhole-sdk-ts){target=\_blank}: + ```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 `helper.ts` to hold signer functions: + ```bash + touch helper.ts + ``` + +5. Open `helper.ts` and add the following code: + ```typescript title="helper.ts" + --8<-- 'code/products/token-bridge/guides/attest-tokens/helper.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 + +## Check for Wrapped Version + +If you are working with a newly created token that you know has never been transferred to the destination chain, you can continue to the [Create Attestation on the Source Chain](#create-attestation-on-the-source-chain) section. + +Since attestation is a one-time process, it is good practice when working with existing tokens to incorporate a check for wrapped versions into your Token Bridge transfer flow. Follow these steps to check for a wrapped version of a token: + +1. Create a new file called `attest.ts` to hold the wrapped version check and attestation logic: + ```bash + touch attest.ts + ``` + +2. Open `attest.ts` and add the following code: + ```typescript title="attest.ts" + --8<-- 'code/products/token-bridge/guides/attest-tokens/attest.ts:1:37' + --8<-- 'code/products/token-bridge/guides/attest-tokens/attest.ts:120:126' + ``` + + After initializing a Wormhole instance and defining the source and destination chains, this code does the following: + + - **Defines the token to check**: use the contract address on the source chain for this value. + - **Calls `getWrappedAsset`**: part of the [`Wormhole` class](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L47){target=\_blank}, the [`getWrappedAsset`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/connect/src/wormhole.ts#L205){target=\_blank} method: + - Accepts a [`TokenId`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/platforms/aptos/protocols/tokenBridge/src/types.ts#L12){target=\_blank} representing a token on the source chain. + - Checks for a corresponding wrapped version of the destination chain's Token Bridge contract. + - Returns the `TokenId` for the wrapped token on the destination chain if a wrapped version exists. + +3. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +4. If the token has a wrapped version registered with the destination chain Token Bridge contract, you will see terminal output similar to the following: + + --8<-- 'code/products/token-bridge/guides/attest-tokens/terminal01.html' + + You can safely use Token Bridge to transfer this token to the destination chain. + + If a wrapped version isn't found on the destination chain, your terminal output will be similar to the following and you must attest the token before transfer: + + --8<-- 'code/products/token-bridge/guides/attest-tokens/terminal02.html' + +## Create Attestation on the Source Chain + +To create the attestation transaction on the source chain, open `attest.ts` and replace the "// Attestation flow code" comment with the following code: +```typescript title="attest.ts" +--8<-- 'code/products/token-bridge/guides/attest-tokens/attest.ts:39:57' +``` + +This code does the following: + +- **Gets the source chain Token Bridge context**: this is where the transaction is sent to create the attestation. +- Defines the token to attest and the payer. +- **Calls `createAttestation`**: defined in the [`TokenBridge` interface](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L123){target=\_blank}, the [`createAttestation`](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/definitions/src/protocols/tokenBridge/tokenBridge.ts#L188){target=\_blank} method does the following: + - Accepts a `TokenAddress` representing the token on its native chain. + - Accepts an optional `payer` address to cover the transaction fees for the attestation transaction. + - Prepares an attestation for the token including metadata such as address, symbol, and decimals. + - Returns an `AsyncGenerator` that yields unsigned transactions, which are then signed and sent to initiate the attestation process on the source chain. + +## Submit Attestation on Destination Chain + +The attestation flow finishes with the following: + +- Using the transaction ID returned from the `createAttestation` transaction on the source chain to retrieve the associated signed `TokenBridge:AttestMeta` VAA. +- Submitting the signed VAA to the destination chain to provide Guardian-backed verification of the attestation transaction on the source chain. +- The destination chain uses the attested metadata to create the wrapped version of the token and register it with its Token Bridge contract. + +Follow these steps to complete your attestation flow logic: + +1. Add the following code to `attest.ts`: + ```typescript title="attest.ts" + --8<-- 'code/products/token-bridge/guides/attest-tokens/attest.ts:58:122' + ``` + +2. Run the script using the following command: + ```bash + npx tsx attest.ts + ``` + +3. You will see terminal output similar to the following: + + --8<-- 'code/products/token-bridge/guides/attest-tokens/terminal03.html' + + ??? example "View complete script" + ```typescript title="attest.ts" + --8<-- 'code/products/token-bridge/guides/attest-tokens/attest.ts' + ``` + +Congratulations! You've successfully created and submitted an attestation to register a token for transfer via Token Bridge. Consider the following options to build upon what you've achieved. + +## Next Steps + +- [**Transfer Wrapped Assets**](/docs/products/token-bridge/guides/attest-tokens): follow this guide to incorporate token attestation and registration into an end-to-end Token Bridge transfer flow. + +TODO: What else should be here? \ No newline at end of file