Set Up Your Token Transfer Environment

## Set Up Your Token Transfer Environment

Follow these steps to initialize your project, install dependencies, and prepare your developer environment for multichain token transfers.

1. Create a new directory and initialize a Node.js project using the following commands:
   ```bash
   mkdir wtt-demo
   cd wtt-demo
   npm init -y
   ```

2. Install dependencies, including the Wormhole TypeScript SDK. This example uses the SDK version `4.14.1`:

   ```bash
   npm install @wormhole-foundation/sdk@4.14.1 -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://getfoundry.sh/cast/reference/wallet#cast-wallet){target=\_blank}.

    !!! warning
        If you use a `.env` file during development, add it to your `.gitignore` to exclude it from version control. Never commit private keys or mnemonics to your repository.

4. Create a new file named `helpers.ts` to hold signer and decimal functions:
   ```bash
   touch helpers.ts
   ```

5. Open `helpers.ts` and add the following code:
    ```typescript title="helpers.ts"
    import {
      Chain,
      ChainAddress,
      ChainContext,
      isTokenId,
      Wormhole,
      Network,
      Signer,
      TokenId,
    } from '@wormhole-foundation/sdk';
    import type { SignAndSendSigner } from '@wormhole-foundation/sdk';
    import evm from '@wormhole-foundation/sdk/evm';
    import solana from '@wormhole-foundation/sdk/solana';
    import sui from '@wormhole-foundation/sdk/sui';

    /**
     * Returns a signer for the given chain using locally scoped credentials.
     * The required values (EVM_PRIVATE_KEY, SOL_PRIVATE_KEY, SUI_MNEMONIC) must
     * be loaded securely beforehand, for example via a keystore, secrets
     * manager, or environment variables (not recommended).
     */
    export async function getSigner<N extends Network, C extends Chain>(
      chain: ChainContext<N, C>,
      gasLimit?: bigint
    ): Promise<{
      chain: ChainContext<N, C>;
      signer: SignAndSendSigner<N, C>;
      address: ChainAddress<C>;
    }> {
      let signer: Signer<any, any>;
      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':
          const evmSignerOptions = gasLimit ? { gasLimit } : {};
          (signer = await (
            await evm()
          ).getSigner(await chain.getRpc(), EVM_PRIVATE_KEY!)),
            evmSignerOptions;
          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<N, C>;

      return {
        chain,
        signer: typedSigner,
        address: Wormhole.chainAddress(chain.chain, signer.address()),
      };
    }

    /**
     * Get the number of decimals for the token on the source chain.
     * This helps convert a user-friendly amount (e.g., '1') into raw units.
     */
    export async function getTokenDecimals<N extends Network>(
      wh: Wormhole<N>,
      token: TokenId,
      chain: ChainContext<N, any>
    ): Promise<number> {
      return isTokenId(token)
        ? Number(await wh.getDecimals(token.chain, token.address))
        : chain.config.nativeTokenDecimals;
    }
    ```

    You can view the [constants for platform names](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/3eae2e91fc3a6fec859eb87cfa85a4c92c65466f/core/base/src/constants/platforms.ts#L6){target=\_blank} in the GitHub repo for a list of supported platforms


Verify Token Registration (Attestation)

## Verify Token Registration (Attestation)

Tokens must be registered on the destination chain before they can be bridged. This process involves submitting an attestation with the native token metadata to the destination chain, which enables the destination chain's WTT contract to create a corresponding wrapped version with the same attributes as the native token.

Registration via attestation is only required the first time a given token is sent to that specific destination chain. Follow these steps to check the registration status of a token:

1. Create a new file named `transfer.ts`:
   ```bash
   touch transfer.ts
   ```

2. Open your `transfer.ts` file and add the following code:
    ```typescript title="transfer.ts"
    import { wormhole, Wormhole, TokenId } from '@wormhole-foundation/sdk';
    import evm from '@wormhole-foundation/sdk/evm';
    import solana from '@wormhole-foundation/sdk/solana';
    import { getSigner, getTokenDecimals } from './helpers';

    async function transferTokens() {
      // Initialize wh instance
      const wh = await wormhole('Testnet', [evm, solana]);
      // Define sourceChain and destinationChain, get chain contexts
      const sourceChain = wh.getChain('Moonbeam');
      const destinationChain = wh.getChain('Solana');
      // Load signers for both chains
      const sourceSigner = await getSigner(sourceChain);
      const destinationSigner = await getSigner(destinationChain);

      // Define token and amount to transfer
      const tokenId: TokenId = Wormhole.tokenId(
        sourceChain.chain,
        'INSERT_TOKEN_CONTRACT_ADDRESS'
      );
      // Replace with amount you want to transfer
      // This is a human-readable number, e.g., 0.2 for 0.2 tokens
      const amount = INSERT_AMOUNT;
      // Convert to raw units based on token decimals
      const decimals = await getTokenDecimals(wh, tokenId, sourceChain);
      const transferAmount = BigInt(Math.floor(amount * 10 ** decimals));

      // Check if the token is registered with destinationChain WTT (Token Bridge) contract
      // Registered = returns the wrapped token ID, continues with transfer
      // Not registered = runs the attestation flow to register the token
      let wrappedToken: TokenId;
      try {
        wrappedToken = await wh.getWrappedAsset(destinationChain.chain, tokenId);
        console.log(
          '✅ Token already registered on destination:',
          wrappedToken.address
        );
      } catch (e) {
        console.log(
          '⚠ Token is NOT registered on destination. Attestation required before transfer can proceed...'
        );
      }
      // Insert Initiate Transfer on Source Chain code
    }

    transferTokens().catch((e) => {
      console.error('❌ Error in transferTokens', e);
      process.exit(1);
    });
    ```

    This code does the following:

    - Initializes a `wormhole` instance and defines the source and destination chains.
    - Imports the signer and decimal functions from `helpers.ts`.
    - Identifies the token and amount to transfer.
    - Checks to see if a wrapped version of the ERC-20 token to transfer exists on the destination chain.

3. Run the script using the following command:

    ```bash
    npx tsx transfer.ts
    ```

    If the token is registered on the destination chain, the address of the existing wrapped asset is returned, and you can continue to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain. If the token is not registered, you will see a message similar to the following advising the attestation flow will run:

      npx tsx transfer.ts
      ⚠ Token is NOT registered on destination. Running attestation flow...

    If you see this message, follow the steps under "Need to register a token?" before continuing with the rest of the transfer flow code.

    ??? example "Need to register a token?"
        Token attestation is a one-time process to register a token on a destination chain. You should only follow these steps if your token registration check indicates a wrapped version does not exist on the destination chain.

        1. Create a new file called `attestToken.ts`:
            ```bash
            touch attestToken.ts
            ```

        2. Open `attestToken.ts` and add the following code to create the attestation for token registration:
            ```typescript title="attestToken.ts"
            import {
              wormhole,
              Wormhole,
              TokenId,
              TokenAddress,
            } from '@wormhole-foundation/sdk';
            import evm from '@wormhole-foundation/sdk/evm';
            import solana from '@wormhole-foundation/sdk/solana';
            import { signSendWait, toNative } from '@wormhole-foundation/sdk-connect';
            import { getSigner } from './helpers';

            async function attestToken() {
              // Initialize wh instance
              const wh = await wormhole('Testnet', [evm, solana]);
              // Define sourceChain and destinationChain, get chain contexts
              const sourceChain = wh.getChain('Moonbeam');
              const destinationChain = wh.getChain('Solana');

              // Define gas limit for EVM chains (optional)
              const gasLimit = BigInt(2_500_000);

              // Load signers for both chains
              const sourceSigner = await getSigner(sourceChain);
              const destinationSigner = await getSigner(destinationChain, gasLimit);

              // Retrieve the WTT (Token Bridge) context for the source chain
              // This is where you will send the transaction to attest the token
              const tb = await sourceChain.getTokenBridge();
              // Define the token to attest
              const tokenId: TokenId = Wormhole.tokenId(
                sourceChain.chain,
                'INSERT_TOKEN_CONTRACT_ADDRESS'
              );
              // Define the token to attest and a payer address
              const token: TokenAddress<typeof sourceChain.chain> = 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 WTT (Token Bridge) context for the source chaindestination chain
                // and submit the attestation VAA
                const destTb = await destinationChain.getTokenBridge();
                const payer = toNative(
                  destinationChain.chain,
                  destinationSigner.signer.address()
                );
                const destTxids = await signSendWait(
                  destinationChain,
                  destTb.submitAttestation(vaa, payer),
                  destinationSigner.signer
                );
                console.log('✅ Attestation submitted on destination:', destTxids);
              }
              // Poll for the wrapped token to appear on the destination chain
              // before proceeding with the transfer
              const maxAttempts = 50; // ~5 minutes with 6s interval
              const interval = 6000;
              let attempt = 0;
              let registered = false;

              while (attempt < maxAttempts && !registered) {
                attempt++;
                try {
                  const wrapped = await wh.getWrappedAsset(destinationChain.chain, tokenId);
                  console.log(
                    `✅ Wrapped token is now available on ${destinationChain.chain}:`,
                    wrapped.address
                  );
                  registered = true;
                } catch {
                  console.log(
                    `⏳ Waiting for wrapped token to register on ${destinationChain.chain}...`
                  );
                  await new Promise((res) => setTimeout(res, interval));
                }
              }

              if (!registered) {
                throw new Error(
                  `❌ Token attestation did not complete in time on ${destinationChain.chain}`
                );
              }
              console.log('🚀 Token attestation complete! Proceed with transfer...');
            }
            ```

            This code does the following:

            - Gets the WTT protocol for the source chain.
            - Defines the token to attest for registration on the destination chain and the payer to sign for the transaction.
            - Calls `createAttestation`, signs, and then sends the transaction.
            - Waits for the signed VAA confirming the attestation creation.
            - Sends the VAA to the destination chain to complete registration.
            - Polls for the wrapped token to be available on the destination chain before continuing the transfer process.

        3. Run the script with the following command:

            ```bash
            npx tsx attestToken.ts
            ```

            When the attestation and registration are complete, you will see terminal output similar to the following:

              npx tsx transfer.ts
              ⚠ Token is NOT registered on destination. Running attestation flow...
              ✅ Attestation transaction sent: [
              {
                chain: 'Moonbeam',
                txid: '0x2b9878e6d8e92d8ecc96d663904312c18a827ccf0b02380074fdbc0fba7e6b68'
              }
            ]
              ✅ Attestation messages: [
              {
                chain: 'Moonbeam',
                emitter: UniversalAddress { address: [Uint8Array] },
                sequence: 1505n
              }
            ]

              Retrying Wormholescan:GetVaaBytes, attempt 0/750
              Retrying Wormholescan:GetVaaBytes, attempt 1/750
              ....
              Retrying Wormholescan:GetVaaBytes, attempt 10/750
              ✅ Attestation submitted on destination: [
              {
                chain: 'Solana',
                txid: '3R4oF5P85jK3wKgkRs5jmE8BBLoM4wo2hWSgXXL6kA8efbj2Vj9vfuFSb53xALqYZuv3FnXDwJNuJfiKKDwpDH1r'
              }
            ]
              ✅ Wrapped token is now available on Solana: SolanaAddress {
              type: 'Native',
              address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] {
                _bn: <BN: 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca>
              }
            }
              🚀 Token attestation complete! Proceeding with transfer...

        You can now go on to [initiate the transfer](#initiate-transfer-on-source-chain) on the source chain.


Initiate Transfer on Source Chain

## Initiate Transfer on Source Chain

Before initializing the token transfer, decide whether to use an automatic or manual transaction. Refer to the [Automatic vs. Manual Transfers](/docs/products/token-transfers/wrapped-token-transfers/concepts/transfer-flow/#automatic-vs-manual-transfers){target=\_blank} section for a comparison of both options.

Follow these steps to add the remaining logic to initiate the token transfer on the source chain. Add the below code where the comment says `// Insert Initiate Transfer on Source Chain code` in your `transfer.ts` file:

1. Open your `transfer.ts` file and add the following code:

    === "Manual Transfer"

        ```typescript title="transfer.ts"
          // Build the token transfer object
          const xfer = await wh.tokenTransfer(
            tokenId,
            transferAmount,
            sourceSigner.address,
            destinationSigner.address,
            'TokenBridge',
            undefined // no payload
          );
          console.log('🚀 Built transfer object:', xfer.transfer);

          // Initiate, sign, and send the token transfer
          const srcTxs = await xfer.initiateTransfer(sourceSigner.signer);
          console.log('🔗 Source chain tx sent:', srcTxs);

          // For manual transfers, wait for VAA
          console.log('⏳ Waiting for attestation (VAA) for manual transfer...');
          const timeout = 10 * 60 * 1000; // 10 minutes timeout
          const attIds = await xfer.fetchAttestation(timeout);
          console.log('✅ Got attestation ID(s):', attIds);

          // Complete the manual transfer on the destination chain
          console.log('↪ Redeeming transfer on destination...');
          const destTxs = await xfer.completeTransfer(destinationSigner.signer);
          console.log('🎉 Destination tx(s) submitted:', destTxs);
        ```

    === "Automatic Transfer"

        ```ts title="transfer.ts"
          // Optional native gas amount for automatic transfers only
          const nativeGasAmount = '0.001'; // 0.001 of native gas in human-readable format
          // Get the decimals for the source chain
          const nativeGasDecimals = destinationChain.config.nativeTokenDecimals;
          // Convert to raw units, otherwise set to 0n
          const nativeGas = BigInt(Number(nativeGasAmount) * 10 ** nativeGasDecimals);

          // Build the token transfer object
          const xfer = await wh.tokenTransfer(
            tokenId,
            transferAmount,
            sourceSigner.address,
            destinationSigner.address,
            'AutomaticTokenBridge',
            nativeGas
          );
          console.log('🚀 Built transfer object:', xfer.transfer);

          // Initiate, sign, and send the token transfer
          const srcTxs = await xfer.initiateTransfer(sourceSigner.signer);
          console.log('🔗 Source chain tx sent:', srcTxs);

          // If automatic, no further action is required. The relayer completes the transfer.
          console.log('✅ Automatic transfer: relayer is handling redemption.');

          process.exit(0);
        ```

    This code does the following:

    - Defines the transfer as automatic or manual. For automatic transfers, both the source and destination chain must have an existing `TokenBridgeRelayer` contract, which listens for and completes transfers on your behalf. You can check the list of [deployed `TokenBridgeRelayer` contracts](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/a48c9132015279ca6a2d3e9c238a54502b16fc7e/core/base/src/constants/contracts/tokenBridgeRelayer.ts){target=\_blank} in the Wormhole SDK repo to see if your desired chains are supported.
    - Sets an optional amount for [native gas drop-off](/docs/products/token-transfers/wrapped-token-transfers/concepts/transfer-flow/#flow-of-an-automatic-transfer-via-tbr){target=\_blank}. This option allows you to send a small amount of the destination chain's native token to cover gas fees. Native gas drop-off is currently only supported for automatic transfers.
    - Builds the transfer object, initiates the transfer, signs the transaction, and sends it.
    - If the transfer is automatic, the flow ends. Otherwise, the script waits for the signed VAA confirming the transaction on the source chain. The signed VAA is then submitted to the destination chain to claim the tokens and complete the manual transfer.

2. Run the script with the following command:
    ```bash
    npx tsx transfer.ts
    ```

3. You will see terminal output similar to the following:

    === "Manual Transfer"

          npx tsx transfer.ts
          ✅ Token already registered on destination: SolanaAddress {
          type: 'Native',
          address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] {
            _bn: <BN: 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca>
          }
        }
          🚀 Built transfer object: {
          token: {
            chain: 'Moonbeam',
            address: EvmAddress {
              type: 'Native',
              address: '0x39F2f26f247CcC223393396755bfde5ecaeb0648'
            }
          },
          amount: 200000000000000000n,
          from: {
            chain: 'Moonbeam',
            address: EvmAddress {
              type: 'Native',
              address: '0xCD8Bcd9A793a7381b3C66C763c3f463f70De4e12'
            }
          },
          to: {
            chain: 'Solana',
            address: SolanaAddress {
              type: 'Native',
              address: [PublicKey [PublicKey(21dmEFTFGBEVoUNjmrxumN6A2xFxNBQXTkK7AmMqNmqD)]]
            }
          },
          protocol: 'TokenBridge',
          payload: undefined
        }
          🔗 Source chain tx sent: [
          '0xf318a1098a81063ac8acc9ca117eeb41ae9abfd9cb550a976721d2fa978f313a'
        ]
          ⏳ Waiting for attestation (VAA) for manual transfer...
          Retrying Wormholescan:GetVaaBytes, attempt 0/30
          Retrying Wormholescan:GetVaaBytes, attempt 1/30
          .....
          Retrying Wormholescan:GetVaaBytes, attempt 15/30
          ✅ Got attestation ID(s): [
          {
            chain: 'Moonbeam',
            emitter: UniversalAddress { address: [Uint8Array] },
            sequence: 1506n
          }
        ]
          ↪ Redeeming transfer on destination...
          🎉 Destination tx(s) submitted: [
          '23NRfFZyKJTDLppJF4GovdegxYAuW2HeXTEFSKKNeA7V82aqTVYTkKeM8sCHCDWe7gWooLAPHARjbAheXoxbbwPk'
        ]

    === "Automatic Transfer"

          npx tsx transfer.ts
          ✅ Token already registered on destination: SolanaAddress {
          type: 'Native',
          address: PublicKey [PublicKey(2qjSAGrpT2eTb673KuGAR5s6AJfQ1X5Sg177Qzuqt7yB)] {
            _bn: <BN: 1b578bb9b7a04a1aab3b5b64b550d8fc4f73ab343c9cf8532d2976b77ec4a8ca>
          }
        }
          🚀 Built transfer object: {
          token: {
            chain: 'Moonbeam',
            address: EvmAddress {
              type: 'Native',
              address: '0x39F2f26f247CcC223393396755bfde5ecaeb0648'
            }
          },
          amount: 200000000000000000n,
          from: {
            chain: 'Moonbeam',
            address: EvmAddress {
              type: 'Native',
              address: '0xCD8Bcd9A793a7381b3C66C763c3f463f70De4e12'
            }
          },
          to: {
            chain: 'Solana',
            address: SolanaAddress {
              type: 'Native',
              address: [PublicKey [PublicKey(21dmEFTFGBEVoUNjmrxumN6A2xFxNBQXTkK7AmMqNmqD)]]
            }
          },
          protocol: 'AutomaticTokenBridge',
          nativeGas: 10000000000000000n
        }
          🔗 Source chain tx sent: [
          '0xf318a1098a81063ac8acc9ca117eeb41ae9abfd9cb550a976721d2fa978f313a'
        ]
          ✅ Automatic transfer: relayer is handling redemption.

Congratulations! You've now used WTT to transfer wrapped assets using the Wormhole TypeScript SDK. Consider the following options to build upon what you've achieved.


Next Steps

## Next Steps

<div class="grid cards" markdown>

-   :octicons-tools-16:{ .lg .middle } **Portal Bridge**

    ---

    Visit this site to interact with Wormhole's Portal Bridge, featuring a working WTT integration.

    [:custom-arrow: Check out the Portal Bridge](https://portalbridge.com/){target=\_blank}

-   :octicons-tools-16:{ .lg .middle } **Interact with WTT Contracts**

    ---

    This guide explores the Solidity functions used in WTT contracts.

    [:custom-arrow: Get Started](/docs/products/token-transfers/wrapped-token-transfers/guides/wtt-contracts/)

-   :octicons-tools-16:{ .lg .middle } **Reference Interfaces**

    ---

    View the source code defining the `TokenBridge` and `AutomaticTokenBridge` interfaces and their associated namespaces.

    [:custom-arrow: See Interfaces](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/tokenBridge/tokenBridge.ts){target=\_blank}

</div>


Recovering Stuck Transfers

## Recovering Stuck Transfers

If your transfer appears stuck or failed to complete automatically, you can recover it manually using the transaction hash from the source chain.


Using the Recovery Script

### Using the Recovery Script

1. **Get your source transaction hash**: This is the transaction ID from when you initiated the transfer

2. **Clone the recovery script**:

    ```bash
    git clone https://github.com/wormhole-foundation/demo-basic-ts-sdk
    cd demo-basic-ts-sdk
    ```

3. **Run the recovery**:

    ```bash
    # Edit src/tx-recover.ts and set recoverTxid to your transaction hash
    npm run transfer:recover
    ```


When Recovery Might Not Work

### When Recovery Might Not Work

- **Wrong destination address**: If tokens were sent to a contract that can't release them
- **Finality not reached**: Wait up to 20 minutes for Ethereum finality before attempting recovery


Get Started with Wrapped Token Transfers (WTT)

Prerequisites

## Prerequisites

To interact with the Wormhole WTT, you'll need the following:

- [The address of the WTT contract](/docs/products/reference/contract-addresses/#wrapped-token-transfers-wtt){target=\_blank} on the chains you're working with.
- [The Wormhole chain ID](/docs/products/reference/chain-ids/){target=\_blank} of the chains you're targeting for token transfers.


How to Interact with WTT Contracts

## How to Interact with WTT Contracts

The primary functions of the WTT contracts revolve around:

- **Attesting a token**: Registering a new token for cross-chain transfers.
- **Transferring tokens**: Locking and minting tokens across chains.
- **Transferring tokens with a payload**: Including additional data with transfers.


Attest a Token

### Attest a Token

Suppose a token has never been transferred to the target chain before transferring it cross-chain. In that case, its metadata must be registered so WTT can recognize it and create a wrapped version if necessary.

The attestation process doesn't require you to manually input token details, such as name, symbol, or decimals. Instead, the WTT contract retrieves these values from the token contract itself when you call the `attestToken()` method.

```solidity
function attestToken(
    address tokenAddress,
    uint32 nonce
) external payable returns (uint64 sequence);
```

??? interface "Parameters"

    `tokenAddress` ++"address"++

    The contract address of the token to be attested.

    ---

    `nonce` ++"uint32"++

    An arbitrary value provided by the caller to ensure uniqueness.

??? interface "Returns"

    `sequence` ++"uint64"++

    A unique identifier for the attestation transaction.

??? interface "Example"

    ```solidity
    IWormhole wormhole = IWormhole(wormholeAddr);
    ITokenBridge tokenBridge = ITokenBridge(tokenBridgeAddr);

    uint256 wormholeFee = wormhole.messageFee();

    tokenBridge.attestToken{value: wormholeFee}(
        address(tokenImpl), // the token contract to attest
        234                 // nonce for the transfer
    );
    ```

When `attestToken()` is called, the contract emits a Verifiable Action Approval (VAA) containing the token's metadata, which the Guardians sign and publish.

You must ensure the token is ERC-20 compliant. If it does not implement the standard functions, the attestation may fail or produce incomplete metadata.


Transfer Tokens

### Transfer Tokens

Once a token is attested, a cross-chain token transfer can be initiated following the lock-and-mint mechanism. On the source chain, tokens are locked (or burned if they're already a wrapped asset), and a VAA is emitted. On the destination chain, the VAA is used to mint or release the corresponding amount of wrapped tokens.

Call `transferTokens()` to lock/burn tokens and produce a VAA with transfer details.

```solidity
function transferTokens(
    address token,
    uint256 amount,
    uint16 recipientChain,
    bytes32 recipient,
    uint256 arbiterFee,
    uint32 nonce
) external payable returns (uint64 sequence);
```

??? interface "Parameters"

    `token` ++"address"++

    The address of the token being transferred.

    ---

    `amount` ++"uint256"++

    The amount of tokens to be transferred.

    ---

    `recipientChain` ++"uint16"++

    The Wormhole chain ID of the destination chain.

    ---

    `recipient` ++"bytes32"++

    The recipient's address on the destination chain.

    ---

    `arbiterFee` ++"uint256"++

    Optional fee to be paid to an arbiter for relaying the transfer.

    ---

    `nonce` ++"uint32"++

    A unique identifier for the transaction.

??? interface "Returns"

    `sequence` ++"uint64"++

    A unique identifier for the transfer transaction.

??? interface "Example"

    ```solidity
    IWormhole wormhole = IWormhole(wormholeAddr);
    ITokenBridge tokenBridge = ITokenBridge(tokenBridgeAddr);

    // Get the fee for publishing a message
    uint256 wormholeFee = wormhole.messageFee();

    tokenBridge.transferTokens{value: wormholeFee}(
        token,           // address of the ERC-20 token to transfer
        amount,          // amount of tokens to transfer
        recipientChain,  // Wormhole chain ID of the destination chain
        recipient,       // recipient address on the destination chain (as bytes32)
        arbiterFee,      // fee for relayer
        nonce            // nonce for this transfer
    );
    ```

Once a transfer VAA is obtained from the Wormhole Guardian network, the final step is to redeem the tokens on the destination chain. Redemption verifies the VAA's authenticity and releases (or mints) tokens to the specified recipient. To redeem the tokens, call `completeTransfer()`.

```solidity
function completeTransfer(bytes memory encodedVm) external;
```

??? interface "Parameters"

    `encodedVm` ++"bytes memory"++

    The signed VAA containing the transfer details.

!!!note
    - WTT normalizes token amounts to 8 decimals when passing them between chains. Make sure your application accounts for potential decimal truncation.
    - The VAA ensures the integrity of the message. Only after the Guardians sign the VAA can it be redeemed on the destination chain.


Transfer Tokens with Payload

### Transfer Tokens with Payload

While a standard token transfer moves tokens between chains, a transfer with a payload allows you to embed arbitrary data in the VAA. This data can be used on the destination chain to execute additional logic—such as automatically depositing tokens into a DeFi protocol, initiating a swap on a DEX, or interacting with a custom smart contract.

Call `transferTokensWithPayload()` instead of `transferTokens()` to include a custom payload (arbitrary bytes) with the token transfer.

```solidity
function transferTokensWithPayload(
    address token,
    uint256 amount,
    uint16 recipientChain,
    bytes32 recipient,
    uint32 nonce,
    bytes memory payload
) external payable returns (uint64 sequence);
```

??? interface "Parameters"

    `token` ++"address"++

    The address of the token being transferred.

    ---

    `amount` ++"uint256"++

    The amount of tokens to be transferred.

    ---

    `recipientChain` ++"uint16"++

    The Wormhole chain ID of the destination chain.

    ---

    `recipient` ++"bytes32"++

    The recipient's address on the destination chain.

    ---

    `nonce` ++"uint32"++

    A unique identifier for the transaction.

    ---

    `payload` ++"bytes memory"++

    Arbitrary data payload attached to the transaction.

??? interface "Returns"

    `sequence` ++"uint64"++

    A unique identifier for the transfer transaction.

??? interface "Example"

    ```solidity
    IWormhole wormhole = IWormhole(wormholeAddr);
    ITokenBridge tokenBridge = ITokenBridge(tokenBridgeAddr);

    // Get the fee for publishing a message
    uint256 wormholeFee = wormhole.messageFee();

    tokenBridge.transferTokensWithPayload{value: wormholeFee}(
        token,           // address of the ERC-20 token to transfer
        amount,          // amount of tokens to transfer
        recipientChain,  // Wormhole chain ID of the destination chain
        recipient,       // recipient address on the destination chain (as bytes32)
        nonce,           // nonce for this transfer
        additionalPayload // additional payload data
    );
    ```

After initiating a transfer on the source chain, the Wormhole Guardian network observes and signs the resulting message, creating a Verifiable Action Approval (VAA). You'll need to fetch this VAA and then call `completeTransferWithPayload()`.

Only the designated recipient contract can redeem tokens. This ensures that the intended contract securely handles the attached payload. On successful redemption, the tokens are minted (if foreign) or released (if native) to the recipient address on the destination chain. For payload transfers, the designated contract can execute the payload's logic at this time.

```solidity
function completeTransferWithPayload(bytes memory encodedVm) external returns (bytes memory);
```

??? interface "Parameters"

    `encodedVm` ++"bytes memory"++

    The signed VAA containing the transfer details.

??? interface "Returns"

    `bytes memory`

    The extracted payload data.


Source Code References

## Source Code References

For a deeper understanding of WTT implementation and to review the actual source code, please refer to the following links:

- [WTT contract](https://github.com/wormhole-foundation/wormhole/blob/main/ethereum/contracts/bridge/Bridge.sol){target=\_blank}
- [WTT interface](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/interfaces/ITokenBridge.sol){target=\_blank}


Portal Bridge

## Portal Bridge

A practical implementation of the Wormhole WTT can be seen in [Portal Bridge](https://portalbridge.com/){target=\_blank}, which provides an easy-to-use interface for transferring tokens across multiple blockchain networks. It leverages the Wormhole infrastructure to handle cross-chain asset transfers seamlessly, offering users a convenient way to bridge their assets while ensuring security and maintaining token integrity.


Wrapped Token Transfers Overview

## Wrapped Token Transfers Overview

Wrapped Token Transfers (WTT) is a Wormhole module for bridging wrapped tokens across various blockchain networks. Locking assets on one network and minting corresponding wrapped tokens on another facilitates secure, efficient, and composable multichain token movement.

This overview covers WTT's main features, general processes, and possible next steps to begin building a cross-chain application.


Key Features

## Key Features

WTT is built to solve interoperability problems in multichain token transfers. Key features include:

- **Interoperability**: Transfer standards-compliant tokens (e.g., ERC-20, SPL) across over 30 [supported chains](/docs/products/reference/supported-networks/#wtt){target=\_blank}.
- **Lock-and-mint mechanism**: Mint wrapped tokens backed 1:1 by locked assets on the source chain.
- **Preserved metadata**: Ensure that token properties like name, symbol, and decimals persist across chains.
- **Transfer with payload**: Attach arbitrary data to token transfers, enabling the triggering of specific actions.
- **Decentralized security**: Verified by the [Guardian Network](/docs/protocol/infrastructure/guardians/){target=\_blank}, ensuring cross-chain consistency and message authenticity.


How It Works

## How It Works

WTT provides a reliable foundation for multichain interoperability at scale. The transfer process follows these key steps:

1. **Attestation**: The token’s metadata (e.g., symbol, name, decimals) is registered on the destination chain. This step is only required once per token.
2. **Locking**: On the source chain, the native token is locked in a custody account.
3. **Message emission**: The [Guardian Network](/docs/protocol/infrastructure/guardians/){target=\_blank} verifies and emits a [VAA](/docs/protocol/infrastructure/vaas/){target=\_blank}.
4. **Verification**: The VAA is submitted and verified on the destination chain to confirm authenticity.
5. **Minting**: A wrapped version of the token is minted (or the native token is released) to the recipient on the destination chain.

This diagram showcases a simplified flow of Alice bridging ETH from Ethereum to her account on Solana.

```mermaid
sequenceDiagram
    participant Alice
    participant Ethereum
    participant GuardianNetwork
    participant Solana

    Alice->>Ethereum: Lock ETH in WTT contract
    Ethereum->>GuardianNetwork: Emit transfer message
    GuardianNetwork->>GuardianNetwork: Verify and sign message

    GuardianNetwork->>Solana: Submit signed message
    Solana->>Solana: Verify message and mint wrapped ETH (WETH)

    Solana->>Alice: Deliver wrapped ETH on Solana
```

For a more in-depth understanding of how WTT works, see the [Flow of a Transfer](/docs/products/token-transfers/wrapped-token-transfers/concepts/transfer-flow/){target=\_blank} page.


Use Cases

## Use Cases

Here are key use cases that highlight the power and versatility of WTT.

- **Multichain Rewards and Token Utility in Decentralized Platforms (e.g., [Chingari](https://chingari.io/){target=\_blank})**

    - **[WTT](/docs/products/token-transfers/wrapped-token-transfers/get-started/)**: Transfer tokens between chains.
    - **[Messaging](/docs/products/messaging/overview/)**: Facilitate the distribution and claiming processes of rewards.

- **Tokenized Gaming Rewards**

    - **[WTT](/docs/products/token-transfers/wrapped-token-transfers/get-started/)**: Handle the underlying lock-and-mint logic securely.
    - **[Connect](/docs/products/connect/overview/)**: Provide a user-friendly way to move game tokens across chains.

- **Multichain DeFi Arbitrage**

    - **[WTT](/docs/products/token-transfers/wrapped-token-transfers/get-started/)**: Enables rapid and secure movement of DeFi assets.
    - **[Connect](/docs/products/connect/overview/)**: Provides a UI widget to onboard users and facilitate seamless multichain swaps within DeFi aggregator platforms.


Next Steps

## Next Steps

If you are looking for more guided practice, take a look at the following guides.

<div class="grid cards" markdown>

-   :octicons-tools-16:{ .lg .middle } **Get Started with WTT**

    ---

    Perform token transfers using WTT, including manual and automatic transfers.

    [:custom-arrow: Get Started](/docs/products/token-transfers/wrapped-token-transfers/get-started/)

-   :octicons-tools-16:{ .lg .middle } **Complete Token Transfer Workflow**

    ---

    Build a cross-chain native token transfer app using Wormhole’s TypeScript SDK, supporting native token transfers across EVM and non-EVM chains.

    [:custom-arrow: Get Started](/docs/products/token-transfers/wrapped-token-transfers/tutorials/transfer-workflow/)

-   :octicons-tools-16:{ .lg .middle } **Create Multichain Tokens**

    ---

    Craft a multichain token using Wormhole's Portal Bridge.

    [:custom-arrow: Get Started](/docs/products/token-transfers/wrapped-token-transfers/tutorials/multichain-token/)

</div>


How do I use deep-linking with Portal?

## How do I use deep-linking with Portal?

You can create a direct link to pre-fill chain and asset selections on [Portal Bridge](https://portalbridge.com){target=\_blank} using URL parameters.

| Parameter     | Description                                                |
|---------------|------------------------------------------------------------|
| `sourceChain` | A source chain that will be pre-selected.                   |
| `targetChain` | A target chain that will be pre-selected.                   |
| `asset`       | The asset key on the source chain (e.g., SOL, USDC, etc.).  |
| `targetAsset` | The asset key on the destination chain.                     |

Example:

```bash
https://portalbridge.com/?sourceChain=solana&targetChain=ethereum&asset=SOL&targetAsset=WSOL
```

This link will open Portal with:

 - **`sourceChain`** pre-selected as `solana`.
 - **`targetChain`** pre-selected as `ethereum`.
 - **`asset`** pre-selected as `SOL`.
 - **`targetAsset`** pre-selected as `WSOL`.

!!! note
    For [**NTT tokens**](/docs/products/token-transfers/native-token-transfers/overview/){target=\_blank}, you can define just one asset if the same token exists across chains.

    Example: [https://portalbridge.com/?sourceChain=ethereum&targetChain=solana&asset=W](https://portalbridge.com/?sourceChain=ethereum&targetChain=solana&asset=W){target=\_blank}


## What does the "Send to a wallet address" field do?

After selecting your tokens and connecting your source wallet on [Portal](https://portalbridge.com/){target=\_blank}, you'll be prompted to connect your destination wallet. At this step, alongside wallet options like MetaMask or Phantom, you'll also see an option labeled "Send to a wallet address".
This flexibility allows you to enter any wallet address as the recipient rather than connecting a destination wallet, enabling you to send tokens to a predefined recipient, such as a team wallet, treasury address, or cold storage wallet.

![](/docs/images/products/wrapped-token-transfers/portal-bridge/faqs/portal-wallet-address.webp){.half}

This field is optional. If left empty, the tokens will be sent to your connected wallet.


## Why is the token that I deployed using the WTT / NTT framework not showing up in the Portal Bridge UI?

While deploying tokens to new chains via Wormhole is fully permissionless, these tokens do not automatically show up on the [Portal](https://portalbridge.com/){target=\_blank} UI. Wormhole Contributors maintain discretion in configuring tokens for Portal to prevent abuse. You can quickly spin up your own UI for token transfers using Wormhole Connect. See the demo repositories to get started:

- [**Basic Connect Demo**](https://github.com/wormhole-foundation/demo-basic-connect){target=\_blank}
- [**NTT Connect Demo**](https://github.com/wormhole-foundation/demo-ntt-connect){target=\_blank}


## Why is my CCTP transfer stuck and can't be redeemed?

If your transfer is stuck and you're unable to complete it through Portal, it may have been initiated through a third-party app or smart contract that reserved the right to finalize it. In that case, Portal is unable to complete the redemption on your behalf. Only the original app can.

To resolve this, go back to the app or platform you used to start the transfer and look for an option to complete or claim it there.

If you want to investigate further, check the events of your originating transaction for a `destination_caller` field. If it's set to a non-zero address, that's the address exclusively authorized to redeem the message and the reason Portal can't complete it.


Register the Token on the Source Chain

## Register the Token on the Source Chain

The first step in creating a multichain token is registering your token on its source chain. This ensures the token is prepared for bridging across blockchains. Follow these steps:

1. Open the [Portal Bridge](https://portalbridge.com/legacy-tools/#/register){target=\_blank}.
2. Select the blockchain where your token is currently deployed (source chain).
3. Connect your wallet by following the on-screen instructions.
4. Locate the **Asset** field and paste the token contract address.
5. Click **Next** to proceed.

![Source Chain Registration Screen](/docs/images/products/wrapped-token-transfers/tutorials/multichain-tokens/multichain-token-1.webp)


Register the Token on the Target Chain

## Register the Token on the Target Chain

After registering your token on the source chain, the next step is to select the target chain—the blockchain where you want the wrapped version of your token to exist. This step connects your token to its destination network.

1. Choose the blockchain where you want the token to be bridged (target chain).
2. Connect your wallet to the target chain.
3. Click **Next** to finalize the registration process.

![Target Chain Registration Screen](/docs/images/products/wrapped-token-transfers/tutorials/multichain-tokens/multichain-token-2.webp)


Send an Attestation

## Send an Attestation

Attestation is a key step in the process. It verifies your token’s metadata, ensuring it is correctly recognized on the target chain’s blockchain explorer (e.g., [Etherscan](https://etherscan.io/){target=\_blank}).

1. Click **Attest** to initiate the attestation process.
2. Approve the transaction in your wallet when prompted.

![Send Attestation Screen](/docs/images/products/wrapped-token-transfers/tutorials/multichain-tokens/multichain-token-3.webp)

!!! note
    - Attestation is crucial for token metadata to appear correctly on blockchain explorers like Etherscan, allowing users to identify and trust your token.
    - Ensure you have sufficient funds to cover transaction fees on the target chain.


Create a Wrapped Token

## Create a Wrapped Token

The final step is to create the wrapped token on the target chain. This token represents the original asset and enables its use within the target blockchain.

1. Click **Create** to generate the wrapped token.
2. Approve the transaction in your wallet when prompted.

![Create Wrapped Token Screen](/docs/images/products/wrapped-token-transfers/tutorials/multichain-tokens/multichain-token-4.webp)

Upon successful creation, you will see a confirmation screen displaying key details such as the source chain, target chain, and transaction status. This helps verify that the process was completed correctly. Refer to the image below as an example:

![Confirmation Screen](/docs/images/products/wrapped-token-transfers/tutorials/multichain-tokens/multichain-token-5.webp)


Additional Steps and Recommendations

## Additional Steps and Recommendations

After creating your multichain token, there are a few optional but highly recommended steps to ensure the best experience for users interacting with your token.


Update Metadata on Blockchain Explorers

### Update Metadata on Blockchain Explorers

It is recommended that you update your token’s metadata on blockchain explorers such as Etherscan. This includes adding details like the token logo, price, and contract verification.

1. Create an account on the relevant scanner and go to the [token update section](https://etherscan.io/tokenupdate){target=\_blank} (or the relevant scanner that you would like to update metadata on).
2. Copy and paste the wrapped contract address in the **Token Update Application Form**.
3. Before proceeding to the next step, you will need to verify as the contract address owner on [Etherscan’s address verification tool](https://etherscan.io/verifyAddress/){target=\_blank}.
4. Follow the directions to verify contract address ownership via MetaMask by reviewing the [guide on verifying address ownership](https://info.etherscan.com/how-to-verify-address-ownership/){target=\_blank}.
   - Given that Wormhole may be the contract owner, use the manual verification process by reaching out through the [Etherscan contact form](https://etherscan.io/contactus){target=\_blank}. The team will provide support as needed.
5. Once the step above is completed, follow the [instructions to update token information](https://info.etherscan.com/how-to-update-token-information-on-token-page/){target=\_blank}.


Next Steps

## Next Steps

<div class="grid cards" markdown>

-   :octicons-tools-16:{ .lg .middle } **Demo Tutorials Repository**

    ---

    Looking for more hands-on tutorials? Check out the Wormhole Tutorial Demo repository on GitHub for additional examples.

    [:custom-arrow: Explore the Demo Repository](https://github.com/wormhole-foundation/demo-tutorials){target=\_blank}

-   :octicons-tools-16:{ .lg .middle } **Wormhole Dev Arena**

    ---

    A structured learning hub with hands-on tutorials across the Wormhole ecosystem.

    [:custom-arrow: Explore the Dev Arena](https://arena.wormhole.com/){target=\_blank}

</div>


Transfer Tokens via Wrapped Token Transfers (WTT) Tutorial

Prerequisites

## 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.
 - Native tokens (testnet or mainnet) in Solana and Sui wallets.
 - A wallet with a private key, funded with native tokens (testnet or mainnet) for gas fees.
 - **Sui token compatibility**: If you're working with custom Sui tokens, ensure they are created with the legacy `CoinMetadata` type for WTT. Once created, the token can be migrated to the `Currency` standard, but the legacy `CoinMetadata` type must exist initially.


Supported Chains

## Supported Chains

The Wormhole SDK supports a wide range of EVM and non-EVM chains, allowing you to facilitate cross-chain transfers efficiently. You can find a complete list of supported chains on the [Supported Networks](/docs/products/reference/supported-networks/#wtt){target=\_blank} page, which includes every network where WTT is supported, across both mainnet and testnet.


Project Setup

## Project Setup

In this section, we’ll guide you through initializing the project, installing dependencies, and preparing your environment for cross-chain transfers.

1. **Initialize the project**: Start by creating a new directory for your project and initializing it with `npm`, which will create the `package.json` file for your project.

    ```bash
    mkdir native-transfers
    cd native-transfers
    npm init -y
    ```

2. **Install dependencies**: Install the required dependencies. This tutorial uses the SDK version `4.14.1`:

    ```bash
    npm install @wormhole-foundation/sdk@4.14.1 tsx
    ```

3. **Set up secure access to your wallets**: This guide assumes you are loading your `SOL_PRIVATE_KEY`, `EVM_PRIVATE_KEY` and `SUI_MNEMONIC` from a secure keystore of your choice, such as a secrets manager or a CLI-based tool like [`cast wallet`](https://getfoundry.sh/cast/reference/wallet/#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 `helpers.ts` file**: To simplify the interaction between chains, create a file to store utility functions for fetching your private key, setting up signers for different chains, and managing transaction relays.

    1. Create the helpers file.

        ```bash
        mkdir -p src/helpers
        touch src/helpers/helpers.ts
        ```

    2. Open the `helpers.ts` file and add the following code.

        ```typescript
        import {
          ChainAddress,
          ChainContext,
          Network,
          Signer,
          Wormhole,
          Chain,
          TokenId,
          isTokenId,
        } from '@wormhole-foundation/sdk';
        import evm from '@wormhole-foundation/sdk/evm';
        import solana from '@wormhole-foundation/sdk/solana';
        import sui from '@wormhole-foundation/sdk/sui';
        import aptos from '@wormhole-foundation/sdk/aptos';
        import { config } from 'dotenv';
        config();

        export interface SignerStuff<N extends Network, C extends Chain> {
          chain: ChainContext<N, C>;
          signer: Signer<N, C>;
          address: ChainAddress<C>;
        }

        // Signer setup function for different blockchain platforms
        export async function getSigner<N extends Network, C extends Chain>(
          chain: ChainContext<N, C>,
          gasLimit?: bigint
        ): Promise<{
          chain: ChainContext<N, C>;
          signer: Signer<N, C>;
          address: ChainAddress<C>;
        }> {
          let signer: Signer;
          const platform = chain.platform.utils()._platform;

          switch (platform) {
            case 'Solana':
              signer = await (
                await solana()
              ).getSigner(await chain.getRpc(), 'SOL_PRIVATE_KEY');
              break;
            case 'Evm':
              const evmSignerOptions = gasLimit ? { gasLimit } : {};
              signer = await (
                await evm()
              ).getSigner(await chain.getRpc(), 'ETH_PRIVATE_KEY', evmSignerOptions);
              break;
            case 'Sui':
              signer = await (
                await sui()
              ).getSigner(await chain.getRpc(), 'SUI_MNEMONIC');
              break;
            case 'Aptos':
              signer = await (
                await aptos()
              ).getSigner(await chain.getRpc(), 'APTOS_PRIVATE_KEY');
              break;
            default:
              throw new Error('Unsupported platform: ' + platform);
          }

          return {
            chain,
            signer: signer as Signer<N, C>,
            address: Wormhole.chainAddress(chain.chain, signer.address()),
          };
        }

        export async function getTokenDecimals<
          N extends 'Mainnet' | 'Testnet' | 'Devnet'
        >(
          wh: Wormhole<N>,
          token: TokenId,
          sendChain: ChainContext<N, any>
        ): Promise<number> {
          return isTokenId(token)
            ? Number(await wh.getDecimals(token.chain, token.address))
            : sendChain.config.nativeTokenDecimals;
        }
        ```

        - **`getSigner`**: Based on the chain you're working with (EVM, Solana, Sui, etc.), this function retrieves a signer for that specific platform. The signer is responsible for signing transactions and interacting with the blockchain. It securely uses the private key stored in your `.env` file.
        - **`getTokenDecimals`**: Fetches the number of decimals for a token on a specific chain. It helps handle token amounts accurately during transfers.


Check and Create Wrapped Tokens

## Check and Create Wrapped Tokens

Before tokens are transferred across chains, it should be checked whether a wrapped version exists on the destination chain. If not, an attestation must be generated to wrap it so it can be sent and received on that chain.

In this section, you'll create a script that automates this process by checking whether Arbitrum Sepolia has a wrapped version on Base Sepolia and registering it if needed.


Configure the Wrapped Token Script

### Configure the Wrapped Token Script

1. **Create the `create-wrapped.ts` file**: Set up the script file that will handle checking and wrapping tokens in the `src` directory.

    ```bash
    mkdir -p src/scripts
    touch src/scripts/create-wrapped.ts
    ```

2. **Open `create-wrapped.ts` and import the required modules**: Import the necessary SDK modules to interact with Wormhole, EVM, Solana, and Sui chains, as well as helper functions for signing and sending transactions.

    ```typescript
    import { Wormhole, signSendWait, wormhole } from '@wormhole-foundation/sdk';
    import evm from '@wormhole-foundation/sdk/evm';
    import solana from '@wormhole-foundation/sdk/solana';
    import sui from '@wormhole-foundation/sdk/sui';
    import { inspect } from 'util';
    import { getSigner } from '../helpers/helpers';
    ```

3. **Initialize the Wormhole SDK**: Initialize the `wormhole` function for the `Testnet` environment and specify the platforms (EVM, Solana, and Sui) to support.

    ```typescript
    (async function () {
      const wh = await wormhole('Testnet', [evm, solana, sui]);
    ```

    !!! note
        You can replace `'Testnet'` with `'Mainnet'` if you want to perform transfers on mainnet.

4. **Configure transfer parameters**: Specify Arbitrum Sepolia as the source chain and Base Sepolia as the destination, retrieve the token ID from the source chain for transfer, and set the gas limit (optional).

    ```typescript
      const srcChain = wh.getChain('ArbitrumSepolia');
      const destChain = wh.getChain('BaseSepolia');
      const token = await srcChain.getNativeWrappedTokenId();
      const gasLimit = BigInt(2_500_000);
    ```

5. **Set up the destination chain signer**: The signer authorizes transactions, such as submitting the attestation.

    ```typescript
      const { signer: destSigner } = await getSigner(destChain, gasLimit);
    ```

6. **Check if the token is wrapped on the destination chain**: Verify if the token already exists as a wrapped asset before creating an attestation.

    ```typescript
      const tbDest = await destChain.getTokenBridge();

      try {
        const wrapped = await tbDest.getWrappedAsset(token);
        console.log(
          `Token already wrapped on ${destChain.chain}. Skipping attestation.`
        );
        return { chain: destChain.chain, address: wrapped };
      } catch (e) {
        console.log(
          `No wrapped token found on ${destChain.chain}. Proceeding with attestation.`
        );
      }
    ```

    If the token is already wrapped, the script exits, and you may proceed to the [next section](/docs/products/token-transfers/wrapped-token-transfers/tutorials/transfer-workflow/#token-transfers). Otherwise, an attestation must be generated.

7. **Set up the source chain signer**: The signer creates and submits the attestation transaction.

    ```typescript
      const { signer: origSigner } = await getSigner(srcChain);
    ```

8. **Create an attestation transaction**: Generate and send an attestation for the token on the source chain to register it on the destination chain, then save the transaction ID to verify the attestation in the next step.

    ```typescript
      const tbOrig = await srcChain.getTokenBridge();
      const attestTxns = tbOrig.createAttestation(
        token.address,
        Wormhole.parseAddress(origSigner.chain(), origSigner.address())
      );

      const txids = await signSendWait(srcChain, attestTxns, origSigner);
      console.log('txids: ', inspect(txids, { depth: null }));
      const txid = txids[0]!.txid;
      console.log('Created attestation (save this): ', txid);
    ```

9. **Retrieve the signed VAA**: Once the attestation transaction is confirmed, use `parseTransaction(txid)` to extract Wormhole messages, then retrieve the signed VAA from the messages. The timeout defines how long to wait for the VAA before failure.

    ```typescript
      const msgs = await srcChain.parseTransaction(txid);
      console.log('Parsed Messages:', msgs);

      const timeout = 25 * 60 * 1000;
      const vaa = await wh.getVaa(msgs[0]!, 'TokenBridge:AttestMeta', timeout);
      if (!vaa) {
        throw new Error(
          'VAA not found after retries exhausted. Try extending the timeout.'
        );
      }
    ```

10. **Submit the attestation on the destination chain**: Submit the signed VAA using `submitAttestation(vaa, recipient)` to create the wrapped token on the destination chain, then send the transaction and await confirmation.

    ```typescript
      const subAttestation = tbDest.submitAttestation(
        vaa,
        Wormhole.parseAddress(destSigner.chain(), destSigner.address())
      );

      const tsx = await signSendWait(destChain, subAttestation, destSigner);
    ```

11. **Wait for the wrapped asset to be available**: Poll until the wrapped token is available on the destination chain.

    ```typescript
      async function waitForIt() {
        do {
          try {
            const wrapped = await tbDest.getWrappedAsset(token);
            return { chain: destChain.chain, address: wrapped };
          } catch (e) {
            console.error('Wrapped asset not found yet. Retrying...');
          }
          console.log('Waiting before checking again...');
          await new Promise((r) => setTimeout(r, 2000));
        } while (true);
      }

      console.log('Wrapped Asset: ', await waitForIt());
    })().catch((e) => console.error(e));
    ```

    If the token is not found, it logs a message and retries after a short delay. Once the wrapped asset is detected, its address is returned.

??? code "Complete script"
    ```typescript
    import { Wormhole, signSendWait, wormhole } from '@wormhole-foundation/sdk';
    import evm from '@wormhole-foundation/sdk/evm';
    import solana from '@wormhole-foundation/sdk/solana';
    import sui from '@wormhole-foundation/sdk/sui';
    import { inspect } from 'util';
    import { getSigner } from '../helpers/helpers';

    (async function () {
      const wh = await wormhole('Testnet', [evm, solana, sui]);

      // Define the source and destination chains
      const srcChain = wh.getChain('ArbitrumSepolia');
      const destChain = wh.getChain('BaseSepolia');
      const token = await srcChain.getNativeWrappedTokenId();
      const gasLimit = BigInt(2_500_000);

      // Destination chain signer setup
      const { signer: destSigner } = await getSigner(destChain, gasLimit);
      const tbDest = await destChain.getTokenBridge();

      try {
        const wrapped = await tbDest.getWrappedAsset(token);
        console.log(
          `Token already wrapped on ${destChain.chain}. Skipping attestation.`
        );
        return { chain: destChain.chain, address: wrapped };
      } catch (e) {
        console.log(
          `No wrapped token found on ${destChain.chain}. Proceeding with attestation.`
        );
      }

      // Source chain signer setup
      const { signer: origSigner } = await getSigner(srcChain);

      // Create an attestation transaction on the source chain
      const tbOrig = await srcChain.getTokenBridge();
      const attestTxns = tbOrig.createAttestation(
        token.address,
        Wormhole.parseAddress(origSigner.chain(), origSigner.address())
      );

      const txids = await signSendWait(srcChain, attestTxns, origSigner);
      console.log('txids: ', inspect(txids, { depth: null }));
      const txid = txids[0]!.txid;
      console.log('Created attestation (save this): ', txid);

      // Retrieve the Wormhole message ID from the attestation transaction
      const msgs = await srcChain.parseTransaction(txid);
      console.log('Parsed Messages:', msgs);

      const timeout = 25 * 60 * 1000;
      const vaa = await wh.getVaa(msgs[0]!, 'TokenBridge:AttestMeta', timeout);
      if (!vaa) {
        throw new Error(
          'VAA not found after retries exhausted. Try extending the timeout.'
        );
      }

      console.log('Token Address: ', vaa.payload.token.address);

      // Submit the attestation on the destination chain
      console.log('Attesting asset on destination chain...');

      const subAttestation = tbDest.submitAttestation(
        vaa,
        Wormhole.parseAddress(destSigner.chain(), destSigner.address())
      );

      const tsx = await signSendWait(destChain, subAttestation, destSigner);
      console.log('Transaction hash: ', tsx);

      // Poll for the wrapped asset until it's available
      async function waitForIt() {
        do {
          try {
            const wrapped = await tbDest.getWrappedAsset(token);
            return { chain: destChain.chain, address: wrapped };
          } catch (e) {
            console.error('Wrapped asset not found yet. Retrying...');
          }
          console.log('Waiting before checking again...');
          await new Promise((r) => setTimeout(r, 2000));
        } while (true);
      }

      console.log('Wrapped Asset: ', await waitForIt());
    })().catch((e) => console.error(e));
    ```


Run the Wrapped Token Creation

### Run the Wrapped Token Creation

Once the script is ready, execute it with:

```bash
npx tsx src/scripts/create-wrapped.ts
```

If the token is already wrapped, the script exits. Otherwise, it generates an attestation and submits it. Once complete, you’re ready to transfer tokens across chains.


Token Transfers

## Token Transfers

In this section, you'll create a script to transfer native tokens across chains using Wormhole's WTT protocol. The script will handle the transfer of Sui native tokens to Solana, demonstrating the seamless cross-chain transfer capabilities of the Wormhole SDK. Since both chains are non-EVM compatible, you'll need to manually handle the attestation and finalization steps.


Configure Transfer Details

### Configure Transfer Details

Before initiating a cross-chain transfer, you must set up the chain context and signers for both the source and destination chains.

1. Create the `native-transfer.ts` file in the `src` directory to hold your script for transferring native tokens across chains.

    ```bash
    touch src/scripts/native-transfer.ts
    ```

2. Open the `native-transfer.ts` file and begin by importing the necessary modules from the SDK and helper files.

    ```typescript
    import {
      Chain,
      Network,
      Wormhole,
      amount,
      wormhole,
      TokenId,
      TokenTransfer,
    } from '@wormhole-foundation/sdk';
    import evm from '@wormhole-foundation/sdk/evm';
    import solana from '@wormhole-foundation/sdk/solana';
    import sui from '@wormhole-foundation/sdk/sui';
    import { SignerStuff, getSigner, getTokenDecimals } from '../helpers/helpers';
    ```

3. **Initialize the Wormhole SDK**: Initialize the `wormhole` function for the `Testnet` environment and specify the platforms (EVM, Solana, and Sui) to support.

    ```typescript
    (async function () {
      const wh = await wormhole('Testnet', [evm, solana, sui]);
    ```

4. **Set up source and destination chains**: Specify the source chain (Sui) and the destination chain (Solana) using the `getChain` method. This allows us to define where to send the native tokens and where to receive them.

    ```typescript
      const sendChain = wh.getChain('Sui');
      const rcvChain = wh.getChain('Solana');
    ```

5. **Configure the signers**: Use the `getSigner` function to retrieve the signers responsible for signing transactions on the respective chains. This ensures that transactions are correctly authorized on both the source and destination chains.

    ```typescript
      const source = await getSigner(sendChain);
      const destination = await getSigner(rcvChain);
    ```

6. **Define the token to transfer**: Specify the native token on the source chain (Sui in this example) by creating a `TokenId` object.

    ```typescript
      const token = Wormhole.tokenId(sendChain.chain, 'native');
    ```

7. **Define the transfer amount**: The amount of native tokens to transfer is specified. In this case, we're transferring 1 unit.

    ```typescript
      const amt = '1';
    ```

8. **Set transfer mode**: Specify manual or automatic transfer using `route`. Set `route  = 'TokenBridge'` for manual transfers, where you will handle the attestation and finalization steps yourself. To use automatic relaying on EVM chains, set `route = 'AutomaticTokenBridge'`.

    ```typescript
      const route = 'TokenBridge';
    ```

    !!! note
        Automatic transfers are only supported for EVM chains. For non-EVM chains, such as Solana and Sui, you must manually handle the attestation and finalization steps.

9. **Define decimals**: Fetch the number of decimals for the token on the source chain (Sui) using the `getTokenDecimals` function.

    ```typescript
      const decimals = await getTokenDecimals(wh, token, sendChain);
    ```

10. **Perform the token transfer and exit the process**: Initiate the transfer by calling the `tokenTransfer` function, which we’ll define in the next step. This function takes an object containing all required details for executing the transfer, including the `source` and `destination` chains, `token`, `mode`, and transfer `amount`.

    ```typescript
      const xfer = await tokenTransfer(wh, {
        token,
        amount: amount.units(amount.parse(amt, decimals)),
        source,
        destination,
        route,
      });
    ```

    Finally, we use `process.exit(0);` to close the script once the transfer completes.

    ```typescript
      process.exit(0);
    })();
    ```


Token Transfer Logic

### Token Transfer Logic

This section defines the `tokenTransfer` function, which manages the core steps for executing cross-chain transfers. This function will handle initiating the transfer on the source chain, retrieving the attestation, and completing the transfer on the destination chain.

#### Defining the Token Transfer Function

The `tokenTransfer` function initiates and manages the transfer process, handling all necessary steps to move tokens across chains with the Wormhole SDK. This function uses types from the SDK and our `helpers.ts` file to ensure chain compatibility.

```typescript
async function tokenTransfer<N extends Network>(
  wh: Wormhole<N>,
  route: {
    token: TokenId;
    amount: bigint;
    source: SignerStuff<N, Chain>;
    destination: SignerStuff<N, Chain>;
    route: string;
    payload?: Uint8Array;
  }
) {
  // Token Transfer Logic
}
```

#### Steps to Transfer Tokens

The `tokenTransfer` function comprises several key steps to facilitate cross-chain transfers. Let’s break down each step:

1. **Initialize the transfer object**: The `tokenTransfer` function begins by creating a `TokenTransfer` object, `xfer`, which tracks the state of the transfer process and provides access to relevant methods for each transfer step.

    ```typescript
      const xfer = await wh.tokenTransfer(
        route.token,
        route.amount,
        route.source.address,
        route.destination.address,
        route.route,
        route.payload
      );
    ```

2. **Estimate transfer fees and validate amount**: We obtain a fee quote for the transfer before proceeding. This step is significant in automatic mode (`automatic = true`), where the quote will include additional fees for relaying.

    ```typescript
      const quote = await TokenTransfer.quoteTransfer(
        wh,
        route.source.chain,
        route.destination.chain,
        xfer.transfer
      );

      if (xfer.transfer.route === 'AutomaticTokenBridge' && quote.destinationToken.amount < 0)
        throw 'The amount requested is too low to cover the fee and any native gas requested.';
    ```

3. **Submit the transaction to the source chain**: Initiate the transfer on the source chain by submitting the transaction using `route.source.signer`, starting the token transfer process.

    ```typescript
      const srcTxids = await xfer.initiateTransfer(route.source.signer);
      console.log(`Source Trasaction ID: ${srcTxids[0]}`);
    ```

     - **`srcTxids`**: The resulting transaction IDs are printed to the console. These IDs can be used to track the transfer’s progress on the source chain and [Wormhole network](https://wormholescan.io/#/?network=Testnet){target=\_blank}.

    ???- note "How Cross-Chain Transfers Work in the Background"
        When `xfer.initiateTransfer(route.source.signer)` is called, it initiates the transfer on the source chain. Here’s what happens in the background:

         - **Token lock or burn**: Tokens are either locked in a smart contract or burned on the source chain, representing the transfer amount.
         - **VAA creation**: Wormhole’s network of Guardians generates a Verifiable Action Approval (VAA)—a signed proof of the transaction, which ensures it’s recognized across chains.
         - **Tracking the transfer**: The returned transaction IDs allow you to track the transfer's progress both on the source chain and within Wormhole’s network.
         - **Redemption on destination**: Once detected, the VAA is used to release or mint the corresponding token amount on the destination chain, completing the transfer.

        This process ensures a secure and verifiable transfer across chains, from locking tokens on the source chain to redeeming them on the destination chain.

4. **Wait for the attestation**: Retrieve the Wormhole attestation (VAA), which serves as cryptographic proof of the transfer. In manual mode, you must wait for the VAA before redeeming the transfer on the destination chain.

    ```typescript
      await xfer.fetchAttestation(60_000);
    ```

5. **Complete the transfer on the destination chain**: Redeem the VAA on the destination chain to finalize the transfer.

    ```typescript
      const destTxids = await xfer.completeTransfer(route.destination.signer);
      console.log(`Completed Transfer: `, destTxids);
    ```

??? code "Complete script"
    ```typescript
    import {
      Chain,
      Network,
      Wormhole,
      amount,
      wormhole,
      TokenId,
      TokenTransfer,
    } from '@wormhole-foundation/sdk';
    import evm from '@wormhole-foundation/sdk/evm';
    import solana from '@wormhole-foundation/sdk/solana';
    import sui from '@wormhole-foundation/sdk/sui';
    import { SignerStuff, getSigner, getTokenDecimals } from '../helpers/helpers';

    (async function () {
      const wh = await wormhole('Testnet', [evm, solana, sui]);

      // Set up source and destination chains
      const sendChain = wh.getChain('Sui');
      const rcvChain = wh.getChain('Solana');

      // Get signer from local key but anything that implements
      const source = await getSigner(sendChain);
      const destination = await getSigner(rcvChain);

      // Shortcut to allow transferring native gas token
      const token = Wormhole.tokenId(sendChain.chain, 'native');

      // Define the amount of tokens to transfer
      const amt = '1';

      // Set route for manual transfers
      const route = 'TokenBridge';

      // Used to normalize the amount to account for the tokens decimals
      const decimals = await getTokenDecimals(wh, token, sendChain);

      // Perform the token transfer if no recovery transaction ID is provided
      const xfer = await tokenTransfer(wh, {
        token,
        amount: amount.units(amount.parse(amt, decimals)),
        source,
        destination,
        route,
      });

      process.exit(0);
    })();

    async function tokenTransfer<N extends Network>(
      wh: Wormhole<N>,
      route: {
        token: TokenId;
        amount: bigint;
        source: SignerStuff<N, Chain>;
        destination: SignerStuff<N, Chain>;
        route: string;
        payload?: Uint8Array;
      }
    ) {
      // Token Transfer Logic
      // Create a TokenTransfer object to track the state of the transfer over time
      const xfer = await wh.tokenTransfer(
        route.token,
        route.amount,
        route.source.address,
        route.destination.address,
        route.route,
        route.payload
      );

      const quote = await TokenTransfer.quoteTransfer(
        wh,
        route.source.chain,
        route.destination.chain,
        xfer.transfer
      );

      if (xfer.transfer.route === 'AutomaticTokenBridge' && quote.destinationToken.amount < 0)
        throw 'The amount requested is too low to cover the fee and any native gas requested.';

      // Submit the transactions to the source chain, passing a signer to sign any txns
      console.log('Starting transfer');
      const srcTxids = await xfer.initiateTransfer(route.source.signer);
      console.log(`Source Trasaction ID: ${srcTxids[0]}`);
      console.log(`Wormhole Trasaction ID: ${srcTxids[1] ?? srcTxids[0]}`);

      // Wait for the VAA to be signed and ready (not required for auto transfer)
      console.log('Getting Attestation');
      await xfer.fetchAttestation(60_000);

      // Redeem the VAA on the dest chain
      console.log('Completing Transfer');
      const destTxids = await xfer.completeTransfer(route.destination.signer);
      console.log(`Completed Transfer: `, destTxids);
    }
    ```


Run the Native Token Transfer

### Run the Native Token Transfer

Now that you’ve set up the project and defined the transfer logic, you can execute the script to transfer native tokens from the Sui chain to Solana. You can use `tsx` to run the TypeScript file directly:

```bash
npx tsx src/scripts/native-transfer.ts
```

This initiates the native token transfer from the source chain (Sui) and completes it on the destination chain (Solana).

You can monitor the status of the transaction on the [Wormhole explorer](https://wormholescan.io/#/?network=Testnet){target=\_blank}.


Resources

## Resources

If you'd like to explore the complete project or need a reference while following this tutorial, you can find the complete codebase in [Wormhole's demo GitHub repository](https://github.com/wormhole-foundation/demo-basic-ts-sdk/){target=\_blank}. The repository includes all the example scripts and configurations needed to perform native token cross-chain transfers, including manual, automatic, and partial transfers using the Wormhole SDK.


Conclusion

## Conclusion

You've successfully built a cross-chain token transfer application using Wormhole's TypeScript SDK and the WTT protocol. This guide walks you through the setup, configuration, and transfer logic required to move native tokens across non-EVM chains, such as Sui and Solana.

The same transfer logic will apply if you’d like to extend this application to different chain combinations, including EVM-compatible chains.


Next Steps

## Next Steps

<div class="grid cards" markdown>

-   :octicons-tools-16:{ .lg .middle } **Build a Staking and Lending Protocol**

    ---

    Master the core features of Wrapped Token Transfers (WTT) by building a real-world staking and lending protocol on the Wormhole Dev Arena, a structured learning hub with hands-on tutorials across the Wormhole ecosystem.

    [:custom-arrow: Explore the Dev Arena](https://arena.wormhole.com/courses/1bee7446-5ed5-8188-92ae-c13ee2f78b1c){target=\_blank}

-   :octicons-tools-16:{ .lg .middle } **Demo Tutorials Repository**

    ---

    Looking for more hands-on tutorials? Check out the Wormhole Tutorial Demo repository on GitHub for additional examples.

    [:custom-arrow: Explore the Demo Repository](https://github.com/wormhole-foundation/demo-tutorials){target=\_blank}

</div>


On-Chain Components

## On-Chain Components

- **Emitter**: A contract that calls the publish message method on the Core Contract. To identify the message, the Core Contract will write an event to the transaction logs with details about the emitter and sequence number. This may be your cross-chain dApp or an existing ecosystem protocol.
- **[Wormhole Core Contract](/docs/protocol/infrastructure/core-contracts/){target=\_blank}**: Primary contract, this is the contract which the Guardians observe and which fundamentally allows for multichain communication.
- **Transaction logs**: Blockchain-specific logs that allow the Guardians to observe messages emitted by the Core Contract.


Off-Chain Components

## Off-Chain Components

- **Guardian Network**: Validators that exist in their own P2P network. Guardians observe and validate messages emitted by the Core Contract to produce VAAs. For full Guardian Set chains, all Guardians perform direct on-chain observation. For delegated chains, a delegated subset performs observation and broadcasts signed delegate observations to the rest of the network, after which Canonical Guardians wait for delegate quorum before signing.
- **[Guardian](/docs/protocol/infrastructure/guardians/){target=\_blank}**: One of 19 validators in the Guardian Network that contributes to the VAA multisig.
- **[Spy](/docs/protocol/infrastructure/spy/){target=\_blank}**: A daemon that subscribes to messages published within the Guardian Network. A Spy can observe and forward network traffic, which helps scale up VAA distribution.
- **[API](https://docs.wormholescan.io/){target=\_blank}**: A REST server to retrieve details for a VAA or the Guardian Network.
- **[VAAs](/docs/protocol/infrastructure/vaas/){target=\_blank}**: Verifiable Action Approvals (VAAs) are the signed attestation of an observed message from the Wormhole Core Contract.
- **[Relayer](/docs/protocol/infrastructure/relayers/relayer/){target=\_blank}**: Any off-chain process that relays a VAA to the target chain. Wormhole provides the [Executor framework](/docs/protocol/infrastructure/relayers/executor-framework/){target=\_blank}, a shared execution framework. This framework enables permissionless message delivery through an open marketplace where relay providers compete using a request-and-quote model.


Next Steps

## Next Steps

<div class="grid cards" markdown>

-   :octicons-book-16:{ .lg .middle } **Core Contracts**

    ---

    Discover Wormhole's Core Contracts, enabling multichain communication with message sending, receiving, and multicast features for efficient synchronization.

    [:custom-arrow: Explore Core Contracts](/docs/protocol/infrastructure/core-contracts/)

-   :octicons-tools-16:{ .lg .middle } **Wormhole Dev Arena**

    ---

    A structured learning hub with hands-on tutorials across the Wormhole ecosystem.

    [:custom-arrow: Explore the Dev Arena](https://arena.wormhole.com/ecosystem){target=\_blank}

</div>


Ecosystem Overview

## Ecosystem Overview

The diagram shows a high-level view of Wormhole’s modular stack, illustrating how different tools are grouped into four layers:

- **Application and user-facing products**: The top layer includes user-centric solutions such as [Connect](/docs/products/connect/overview/){target=\_blank} (a simple bridging interface).
- **Asset and data transfer layer**: Below it sits the core bridging and data solutions—[NTT](/docs/products/token-transfers/native-token-transfers/overview/){target=\_blank}, [WTT](/docs/products/token-transfers/wrapped-token-transfers/overview/){target=\_blank}, [Queries](/docs/products/queries/overview/){target=\_blank}, [Settlement](/docs/products/settlement/overview/){target=\_blank}, and [MultiGov](/docs/products/multigov/overview/){target=\_blank}—that handle the movement of tokens, real-time data fetching, advanced cross-chain settlements, and cross-chain governance.
- **Integration layer**: The [TypeScript SDK](/docs/tools/typescript-sdk/get-started/){target=\_blank} and [WormholeScan API](https://wormholescan.io/#/){target=\_blank} provide developer-friendly libraries and APIs to integrate cross-chain capabilities into applications.
- **Foundation layer**: At the base, the [Wormhole messaging](/docs/products/messaging/overview/){target=\_blank} system and the [core contracts](/docs/protocol/infrastructure/core-contracts/){target=\_blank} secure the entire network, providing essential verification and cross-chain message delivery.

![Wormhole ecosystem diagram](/docs/images/protocol/ecosystem/ecosystem-1.webp)


## Bringing It All Together: Interoperability in Action

Wormhole’s modularity makes it easy to adopt just the pieces you need. If you want to quickly add bridging to a dApp, use Connect at the top layer while relying on the Foundation Layer behind the scenes. Or if your app needs to send raw messages between chains, integrate the Messaging layer directly via the Integration Layer (TypeScript or Solidity SDK). You can even layer on additional features—like real-time data calls from Queries or more flexible bridging flows with Native Token Transfers.

Ultimately, these components aren’t siloed but designed to be combined. You could, for instance, fetch a balance from one chain using Queries and then perform an on-chain swap on another chain using Settlement. Regardless of your approach, each Wormhole product is powered by the same Guardian-secured messaging backbone, ensuring all cross-chain interactions remain reliable and secure.


Next Steps

## Next Steps

<div class="grid cards" markdown>

-   :octicons-book-16:{ .lg .middle } **Visit the Product Comparison**

    ---

    Unsure which bridging solution you need? Match your requirements with the right Wormhole tool.

    [:custom-arrow: Compare Products](/docs/products/overview/)

-   :octicons-tools-16:{ .lg .middle } **Wormhole Dev Arena**

    ---

    A structured learning hub with hands-on tutorials across the Wormhole ecosystem.

    [:custom-arrow: Explore the Dev Arena](https://arena.wormhole.com/ecosystem){target=\_blank}

</div>


Prerequisites

## Prerequisites

Before integrating CCTP with Executor, ensure that:

- Both the source and destination chains are supported.
- The required CCTP relay type (CCTPv1 `ERC1` or CCTPv2 `ERC2`) is enabled for the destination chain.

!!! note
    Circle’s Cross-Chain Transfer Protocol supports two versions: CCTPv1 and CCTPv2. These versions differ in how transfers are finalized and how execution can be composed on the destination chain. For a detailed explanation of the differences between CCTPv1 and CCTPv2, see [Circle’s overview](https://www.circle.com/blog/cctp-v2-the-future-of-cross-chain){target=\_blank}.

??? info "How to verify chain and relay type support"

    You can confirm chain and relay type support using the capabilities endpoint:

    ```sh
    GET https://executor-testnet.labsapis.com/v0/capabilities
    ```
    The response includes:

      - Supported source and destination chains
      - Enabled CCTP relay types (`ERC1` or `ERC2`) for the destination chain
      - Gas drop-off limits, which define the maximum gas the relay provider can allocate

    The relay provider will only respect the first `GasDropOffInstruction` and will drop off the lesser of the requested amount and the configured limit.


References

## References

Use the following resources throughout this guide:

- [**CCTP with Executor addresses**](/docs/products/messaging/reference/executor-addresses/#cctp-with-executor){target=\_blank}: List of deployed contracts for CCTP with Executor.
- **Executor endpoints**: Used for quote requests, transaction status checks, and capability queries.

    | Environment | URL                                                                            |
    |-------------|--------------------------------------------------------------------------------|
    | **Mainnet** | <pre>```https://executor.labsapis.com```</pre> |
    | **Testnet** | <pre>```https://executor-testnet.labsapis.com```</pre> |

    For development and testing, use the testnet endpoint. The mainnet relay provider is reserved for production-ready deployments.


Generate Relay Instructions

## Generate Relay Instructions

Relay instructions define how the Executor should perform the relay on the destination chain, including gas limits and optional native token drop-offs. They are serialized into a compact byte format and passed to the Executor contract when submitting a transfer. Before generating relay instructions, install the SDK [Definitions](https://github.com/wormhole-foundation/native-token-transfers/blob/main/sdk/definitions/src/nttWithExecutor.ts){target=\_blank} package:

```sh
npm i @wormhole-foundation/sdk-definitions
```

[Layouts](https://github.com/wormhole-foundation/wormhole-sdk-ts/blob/main/core/definitions/src/protocols/executor/relayInstruction.ts){target=\_blank} for the Executor `relayInstructions` are provided by the Wormhole TypeScript SDK. Once installed, use the `serializeLayout` helper to construct and encode your relay instructions:

```tsx
import {
  encoding,
  serializeLayout,
  UniversalAddress,
} from "@wormhole-foundation/sdk-connect";
import { relayInstructionsLayout } from "@wormhole-foundation/sdk-definitions";

const relayInstructions = serializeLayout(relayInstructionsLayout, {
  requests: [
    {
      request: {
	      type: "GasInstruction",
	      gasLimit: 250000n,
	      msgValue: 0n,
	    },
	  }
  ],
});
```

??? interface "Parameters"

    `type` ++"GasInstruction"++

    Defines the instruction to allocate gas for the relay.

    ---

    `gasLimit` ++"uint"++

    Specifies the maximum gas available for executing the redeem transaction on the destination chain.

    ---

    `msgValue` ++"uint"++

    Represents the amount of native token (e.g., ETH, SOL) to forward with the transaction; this should typically be set to 0 for NTT transfers.

Relay instructions are encoded using the `relayInstructionsLayout`, which always expects an array of instruction objects. Each array element is a `RelayInstruction` whose `request.type` determines the specific variant:

| Instruction             | Description                                                               | Fields                 |
| ----------------------- | ------------------------------------------------------------------------- | ---------------------- |
| `GasInstruction`        | Defines gas allocation for relay execution                                | `gasLimit`, `msgValue` |
| `GasDropOffInstruction` | Drops native tokens to a wallet on the destination chain                  | `dropOff`, `recipient` |

Relay instructions can include multiple requests (e.g., for gas, value transfer, or drop-off). For most CCTP with Executor flows, a single `GasInstruction` is sufficient.


EVM

### EVM

For EVM destinations:

- `gasLimit` is the gas limit set on the redeeming transaction. Actual gas consumption depends on whether a gas drop-off instruction is included (in addition to the normal differences across various EVM chains).
- `msgValue` is not used by CCTP’s `receiveMessage` entrypoints and should be set to zero for standard CCTP flows.


SVM

### SVM

For Solana and other SVM chains:

- `gasLimit` represents the number of compute units to allocate to the transaction.
- The total relay cost is determined by:
    - The CUs consumed by the transaction
    - The [prioritization fee](https://solana.com/docs/core/fees#prioritization-fee){target=\_blank} used by the relay provider
- `msgValue` must cover all lamports required for:
    - Transaction fees
    - Priority fees
    - Any rent required for new accounts

CCTP transfers to Solana are redeemed into a USDC token account that must exist before redemption. If the recipient's associated token account (ATA) does not exist, the relayer can create it, but this increases the rent and `msgValue` requirements. To allow the relayer to create the ATA automatically:

1. Target the associated token account for the recipient.
2. Before sending, check whether the ATA exists.
3. If it does not exist, include a zero-value `GasDropOffInstruction` for the wallet owner (not the ATA). This gives the relayer enough information to re-derive and create the ATA.

!!!note
    If a non-zero `GasDropOffInstruction` is used for a new wallet, the drop-off amount must be greater than `getMinimumBalanceForRentExemption` for the token account. Drop-offs below this threshold for new accounts are ignored to avoid guaranteed transaction failure.


Sui

### Sui

For Sui:

- `gasLimit` represents the gas budget for the transaction.
- As with native Sui transactions, the budget often needs to exceed the actual cost to account for variable execution and storage usage.
- A direct gas budget is used instead of a simulated CU-style model due to the [non-linear gas cost structure](https://docs.sui.io/concepts/tokenomics/gas-in-sui#gas-prices){target=\_blank} on Sui.


Request a Signed Quote

## Request a Signed Quote

Once you have your relay instructions ready, request a `SignedQuote` from the Executor Relay Provider. The quote authorizes a provider to perform the relay and includes an estimated cost. The example below requests a quote from Sepolia to Base Sepolia:

```ts
const EXECUTOR_URL = 'https://executor-testnet.labsapis.com';
const { signedQuote: quote, estimatedCost: estimate } = (
  await axios.post(`${EXECUTOR_URL}/v0/quote`, {
    srcChain: 10002,
    dstChain: 10004,
    relayInstructions,
  })
).data;
```

??? interface "Parameters"

    `srcChain` ++"uint16"++

    Specify the Wormhole chain IDs for the source networks.

    ---

    `dstChain` ++"uint16"++

    Specify the Wormhole chain IDs for the destination networks.

    ---

    `relayInstructions` ++"Uint8Array"++

    Encodes the execution parameters you generated in the previous step.

Example response:

```bash
{
  "signedQuote": "0x455130315241c9276698439fef2780dbab76fec90b633fbd000000000000000000000000f7122c001b3e07d7fafd8be3670545135859954a271227140000000067dd750f00000000000003e80000000000514b7c000011bbaf716200000011bbaf716200f86edc3960908d257472836d5b1c33c457bf17af67a758d9984356e7166bec8162faa0e07f991d061b93e4f033895c71134a30d9ca369c606fcabba0b742d2431c",
  "estimatedCost": "1431935000000"
}
```

Signed Quotes have an expiry time and must be generated for each request. The Executor contract will revert if the quote expires before on-chain submission.


Call Sending Contract

## Call Sending Contract

With relay instructions and a signed quote, the sending transaction can initiate both the CCTP burn and the Executor request, which instructs the relay provider to redeem and optionally execute on the destination chain.


EVM

### EVM

For EVM chains, helper contracts wrap the CCTP calls and the Executor request into a single entry point. These helpers perform the CCTP burn via `depositForBurn`, followed by a `requestExecution` through the Executor using the signed quote and relay instructions you generated earlier. A version specific helper contract is used depending on whether your integration relies on CCTPv1 (`CCTPv1WithExecutor`) or CCTPv2 (`CCTPv2WithExecutor`).

Both versions share the same `ExecutorArgs` and `FeeArgs` structs:

```sol
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.19;

struct ExecutorArgs {
    // The refund address used by the Executor.
    address refundAddress;
    // The signed quote to be passed into the Executor.
    bytes signedQuote;
    // The relay instructions to be passed into the Executor.
    bytes instructions;
}

struct FeeArgs {
    // The fee in tenths of basis points.
    uint16 dbps;
    // To whom the fee should be paid (the "referrer").
    address payee;
}
```

The helper interfaces are as follows:

??? interface "ICCTPv1WithExecutor"

    ```sol
    interface ICCTPv1WithExecutor {
        /// @notice Deposits and burns tokens from sender to be minted on destination domain using the Executor for relaying.
        /// @param amount amount of tokens to burn
        /// @param destinationChain destination chain ID
        /// @param destinationDomain destination domain (ETH = 0, AVAX = 1)
        /// @param mintRecipient address of mint recipient on destination domain
        /// @param burnToken address of contract to burn deposited tokens, on local domain
        /// @param executorArgs The arguments to be passed into the Executor.
        /// @param feeArgs The arguments used to compute and pay the referrer fee.
        /// @return nonce Circle nonce reserved by message
        ///
        function depositForBurn(
            uint256 amount,
            uint16 destinationChain,
            uint32 destinationDomain,
            bytes32 mintRecipient,
            address burnToken,
            ExecutorArgs calldata executorArgs,
            FeeArgs calldata feeArgs
        ) external payable returns (uint64 nonce);
    }
    ```

??? interface "ICCTPv2WithExecutor"

    ```sol
    interface ICCTPv2WithExecutor {
        /**
         * @notice Deposits and burns tokens from sender to be minted on destination domain.
         * Emits a `DepositForBurn` event.
         * @dev reverts if:
         * - given burnToken is not supported
         * - given destinationDomain has no TokenMessenger registered
         * - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance
         * to this contract is less than `amount`.
         * - burn() reverts. For example, if `amount` is 0.
         * - maxFee is greater than or equal to `amount`.
         * - MessageTransmitterV2#sendMessage reverts.
         * @param amount amount of tokens to burn
         * @param destinationChain destination chain ID
         * @param destinationDomain destination domain to receive message on
         * @param mintRecipient address of mint recipient on destination domain
         * @param burnToken token to burn `amount` of, on local domain
         * @param destinationCaller authorized caller on the destination domain, as bytes32. If equal to bytes32(0),
         * any address can broadcast the message.
         * @param maxFee maximum fee to pay on the destination domain, specified in units of burnToken
         * @param minFinalityThreshold the minimum finality at which a burn message will be attested to.
         * @param executorArgs The arguments to be passed into the Executor.
         * @param feeArgs The arguments used to compute and pay the referrer fee.
         */
        function depositForBurn(
            uint256 amount,
            uint16 destinationChain,
            uint32 destinationDomain,
            bytes32 mintRecipient,
            address burnToken,
            bytes32 destinationCaller,
            uint256 maxFee,
            uint32 minFinalityThreshold,
            ExecutorArgs calldata executorArgs,
            FeeArgs calldata feeArgs
        ) external payable;
    }
    ```

In both cases, you pass:

- `executorArgs.signedQuote`: The `signedQuote` returned by the Executor `/v0/quote` endpoint.
- `executorArgs.instructions`: The serialized relay instructions from the previous step.
- `executorArgs.refundAddress`: The address that should receive any unused funds refunded by the Executor.
- `feeArgs`: Optional referrer fee configuration, if your integration charges a fee on transfers.