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