Skip to content

Moonbeam Routed Liquidity

Introduction

Moonbeam Routed Liquidity (MRL) refers to a Moonbeam use case in which liquidity in any blockchain ecosystem that Moonbeam is connected to can be routed to Polkadot parachains. This is possible because of multiple components that work together:

  • General Message Passing (GMP) - technology connecting multiple blockchains together, including Moonbeam. With it, developers can pass messages with arbitrary data, and tokens can be sent across non-parachain blockchains through chain-agnostic GMP protocols
  • Cross-Consensus Message Passing (XCM) - Polkadot's flavor of GMP. Main technology driving cross-chain interactions between Polkadot and its parachains, including Moonbeam
  • XCM-Enabled ERC-20s - also referred to as local XC-20s, are all of the ERC-20 tokens that exist on Moonbeam's EVM that are XCM-enabled out of the box
  • GMP Precompile - a precompiled contract that acts as an interface between a message passed from Wormhole GMP protocol and XCM

These components are combined to offer seamless liquidity routing into parachains through Moonbeam. Liquidity can be routed to parachains using either the GMP Precompile or traditional smart contracts that interact with XCM-related precompiles, like the X-Tokens Precompile.

GMP protocols typically move assets in a lock/mint or burn/mint fashion. This liquidity exists on Moonbeam normally as ERC-20 tokens. All ERC-20s on Moonbeam are now XCM-enabled, meaning they can now exist as XC-20s in any other parachain, as long as they are registered on the other parachain. XCM-enabled ERC-20s are referred to as local XC-20s on Moonbeam.

MRL is currently available through Wormhole-connected chains, but nothing stops a parachain team from implementing a similar pathway through a different GMP provider.

Primarily, this guide will cover the process of integrating with Wormhole's SDKs and interfaces so that your parachain can access liquidity from non-parachain blockchains through Moonbeam. It will also cover the requirements to get started and the tokens available through Wormhole.

Prerequisites

In order to begin an MRL integration with your parachain, you will first need to:

MRL Through Wormhole

While MRL intends to encompass many different GMP providers, Wormhole is the first that has been built for the public. After you have completed all of the prerequisites, to receive liquidity through Wormhole, you'll need to:

  • Notify the Moonbeam team of your desire to integrate into the MRL program so that we can help you with the technical implementation
  • Connect with the Wormhole team and other MRL-dependent frontends to finalize technical details and sync announcements. They will likely need the following information:
    • Parachain ID
    • The account type that your parachain uses (i.e., AccountId32 or AccountKey20)
    • The addresses and names of the tokens that you have registered
    • An endpoint that can be used by a Wormhole Connect frontend
    • Why do you want your parachain to be connected through Wormhole Connect?

Send Tokens Through Wormhole to a Parachain

MRL provides a one-click solution that allows you to define a multilocation as a final destination for your assets to arrive from any Wormhole chain with a Wormhole Connect integration.

To send tokens through Wormhole and MRL, user interfaces will use a mixture of the Wormhole TokenBridge and Moonbeam’s GMP Precompile.

Users transferring liquidity will invoke the transferTokensWithPayload method on the origin chain's deployment of the Wormhole TokenBridge smart contract, which implements the ITokenBridge.sol interface, to send tokens to the GMP Precompile. This function requires a bytes payload, which must be formatted as a SCALE-encoded multilocation object wrapped within another precompile-specific versioned type. To learn how to build this payload, please refer to the Building the Payload for Wormhole section of the GMP Precompile documentation.

Wormhole relies on a set of distributed nodes that monitor the state on several blockchains. In Wormhole, these nodes are referred to as Guardians. It is the Guardian's role to observe messages and sign the corresponding payloads. If 2/3rds of Wormhole's signing Guardians validate a particular message, the message becomes approved and can be received on other chains.

The Guardian signatures combined with the message form a proof called a Verified Action Approval (VAA). These VAAs are delivered to their destinations by relayers within the Wormhole network. On the destination chain, the VAA is used to perform an action. In this case, the VAA is passed into the wormholeTransferERC20 function of the GMP Precompile, which processes the VAA through the Wormhole bridge contract (which mints the tokens) and relays the tokens to a parachain using XCM messages. Please note that as a parachain integrating MRL, you will likely not need to implement or use the GMP Precompile.

A relayer's only job is to pass the transactions approved by Wormhole Guardians to the destination chain. MRL is supported by some relayers already, but anyone can run one. Furthermore, users can manually execute their transaction in the destination chain when bridging through Wormhole and avoid relayers altogether.

Transfering wormhole MRL

Send Tokens From a Parachain Back Through Wormhole

To send tokens from a parachain back through Wormhole to a destination chain, a user will need to send a transaction, preferably using the utility.batchAll extrinsic, which will batch a token transfer and a remote execution action into a single transaction. For example, a batch with a xTokens.transferMultiassets, and polkadotXcm.send with the Transact instruction.

The reason for batching is to offer a one-click solution. Nevertheless, for the time being, it requires the user to also own xcGLMR (representation of GLMR) on the parachain. There are two main reasons as to why:

  • Local XC-20s (XCM-enabled ERC-20s) can't be used to pay for XCM execution on Moonbeam. This was a design decision, as it was preferred to treat them as ERC-20s and utilize the native transfer function of the ERC-20 interface. Consequently, XCM instructions handling the XC-20s are only limited to moving funds from one account to another and don't understand the Holding Register that is inherent to the XCM flow
  • Currently, XCM-related pallets limit the ability of XCM messages to send tokens that have different reserve chains. Consequently, you can't send an XC-20 and set the fee token to be the native parachain token

In the future, the X-Tokens Pallet will be updated, allowing for your native gas currency to be used as a fee token instead. Parachains that use a different pallet will need to implement their own solution to transfer reserve and non-reserve assets in a single message.

As an example, a brief overview of the entire process of sending MRL tokens from a parachain back through Wormhole to a destination chain is as follows:

  1. Send a batch transaction using the batchAll extrinsic of the Utility Pallet that contains the following two calls:
    • xTokens.transferMultiassets - sends xcGLMR and the local XC-20 to the user’s Computed Origin account. The Computed Origin account is a keyless account on Moonbeam that an account on another parachain has control of via XCM
    • polkadotXcm.send - with the Transact instruction. Sends a remote EVM call via XCM to the Batch Precompile on Moonbeam, which batches the following two calls into a single remote EVM transaction using the ethereumXcm.transact extrinsic:
      • approve (of the local XC-20 contract) - approves the Wormhole relayer to transfer the local XC-20
      • transferTokensWithRelay (of the relayer contract) - calls the transferTokensWithPayload function of the Wormhole TokenBridge smart contract on Moonbeam to transfer the tokens cross-chain, which broadcasts the message for the Wormhole Guardians to pick up
  2. The Guardian Network will pick up on the Wormhole transaction and sign it
  3. A Wormhole relayer will relay the tokens to the destination chain and destination account

Transfering Wormhole MRL out

Now that you have a general idea of the game plan, you can begin to implement it. The example in this guide will show you how to transfer assets from a parachain to Moonbase Alpha and back through Wormhole to the destination chain, but this guide can be adapted for Moonbeam.

Calculate the Computed Origin Account

In order to send tokens back through Wormhole, you'll need to calculate the user's Computed Origin account (previously referred to as a multilocation-derivative account) on Moonbeam. This can be done off-chain using the calculate-multilocation-derivative-account.ts script from the xcm-tools repository. For more details, you can refer to the Computed Origins guide.

Alternatively, the multilocationToAddress function of the XCM Utilities Precompile can also be used.

Build the Transfer Multiassets Extrinsic

Once you have the Computed Origin account, you can begin to construct the utility.batchAll transaction. To get started, you'll need to make sure you have a few packages installed:

npm i @polkadot/api ethers

Now you can begin to tackle the xTokens.transferMultiassets extrinsic, which accepts four parameters: assets, feeItem, dest, and destWeightLimit. You can find out more information on each of the parameters in the X-Tokens Pallet Interface documentation.

In short, the assets parameter defines the multilocation and amount of xcDEV (xcGLMR for Moonbeam) and the local XC-20 to send to Moonbase Alpha, with the xcDEV positioned as the first asset and the local XC-20 as the second. The feeItem is set to the index of the xcDEV asset, which in this case is 0, so that DEV is used to pay for the execution fees in Moonbase Alpha. The dest is a multilocation that defines the Computed Origin account that you calculated in the previous section on Moonbase Alpha.

For this example, the xTokens.transferMultiassets extrinsic will look like the following:

Transfer multiassets logic
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { ethers } from 'ethers';

// Input data
const originChainProviderWsURL = 'INSERT_ORIGIN_CHAIN_WSS_URL';
const multilocationDerivativeAccount =
  'INSERT_MULTILOCATION_DERIVATIVE_ADDRESS';
const localXC20Address = 'INSERT_LOCAL_XC20_ADDRESS';
const transferAmount = 'INSERT_AMOUNT_TO_TRANSFER';

// Transfer multiassets parameters
const assets = {
  V3: [
    {
      // xcDEV
      id: {
        Concrete: {
          parents: 1,
          interior: {
            X2: [
              { Parachain: 1000 }, // Parachain ID
              { PalletInstance: 3 }, // Index of the Balances Pallet
            ],
          },
        },
      },
      fun: {
        Fungible: '100000000000000000', // 0.1 DEV as an estimation for XCM and EVM transaction fee
      },
    },
    {
      // Local XC-20 token
      id: {
        Concrete: {
          parents: 1,
          interior: {
            X3: [
              { Parachain: 1000 }, // Parachain ID
              { PalletInstance: 48 }, // Index of the ERC-20 XCM Bridge Pallet
              {
                AccountKey20: {
                  key: localXC20Address,
                },
              },
            ],
          },
        },
      },
      fun: {
        Fungible: transferAmount,
      },
    },
  ],
};
const feeItem = 0;
const destination = {
  V3: {
    parents: 1,
    interior: {
      X2: [
        { Parachain: 1000 },
        { AccountKey20: { key: multilocationDerivativeAccount } },
      ],
    },
  },
};
const weightLimit = 'Unlimited';

const sendBatchTx = async () => {
  // Create origin chain API provider
  const originChainProvider = new WsProvider(originChainProviderWsURL);
  const originChainAPI = await ApiPromise.create({ provider: originChainProvider });

  // Create the transferMultiasset extrinsic
  const transferMultiassets = originChainAPI.tx.xTokens.transferMultiassets(
    assets,
    feeItem,
    destination,
    weightLimit
  );

  // Additional code goes here
};

sendBatchTx();

To modify the script for Moonbeam, you'll use the following configurations:

Parameter Value
Parachain ID 2004
Balances Pallet Index 10
ERC-20 XCM Bridge Pallet Index 110

Build the Remote EVM Call

To generate the second call of the batch transaction, the polkadotXcm.send extrinsic, you'll need to create the EVM transaction and then assemble the XCM instructions that execute said EVM transaction. The EVM transaction can be constructed as a transaction that interacts with the Batch Precompile so that two transactions can happen in one. This is helpful because this EVM transaction has to both approve a Wormhole relayer to relay the local XC-20 token as well as the relay action itself.

To create the batch transaction and wrap it in a remote EVM call to be executed on Moonbeam, you'll need to take the following steps:

  1. Create contract instances of the local XC-20, the Wormhole relayer, and the Batch Precompile. For this, you'll need the ABI for each contract:

    ERC-20 Interface ABI
    export default [
      {
        anonymous: false,
        inputs: [
          {
            indexed: true,
            internalType: 'address',
            name: 'owner',
            type: 'address',
          },
          {
            indexed: true,
            internalType: 'address',
            name: 'spender',
            type: 'address',
          },
          {
            indexed: false,
            internalType: 'uint256',
            name: 'value',
            type: 'uint256',
          },
        ],
        name: 'Approval',
        type: 'event',
      },
      {
        anonymous: false,
        inputs: [
          {
            indexed: true,
            internalType: 'address',
            name: 'from',
            type: 'address',
          },
          {
            indexed: true,
            internalType: 'address',
            name: 'to',
            type: 'address',
          },
          {
            indexed: false,
            internalType: 'uint256',
            name: 'value',
            type: 'uint256',
          },
        ],
        name: 'Transfer',
        type: 'event',
      },
      {
        inputs: [
          {
            internalType: 'address',
            name: 'owner',
            type: 'address',
          },
          {
            internalType: 'address',
            name: 'spender',
            type: 'address',
          },
        ],
        name: 'allowance',
        outputs: [
          {
            internalType: 'uint256',
            name: '',
            type: 'uint256',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'address',
            name: 'spender',
            type: 'address',
          },
          {
            internalType: 'uint256',
            name: 'amount',
            type: 'uint256',
          },
        ],
        name: 'approve',
        outputs: [
          {
            internalType: 'bool',
            name: '',
            type: 'bool',
          },
        ],
        stateMutability: 'nonpayable',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'address',
            name: 'account',
            type: 'address',
          },
        ],
        name: 'balanceOf',
        outputs: [
          {
            internalType: 'uint256',
            name: '',
            type: 'uint256',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
      {
        inputs: [],
        name: 'totalSupply',
        outputs: [
          {
            internalType: 'uint256',
            name: '',
            type: 'uint256',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'address',
            name: 'to',
            type: 'address',
          },
          {
            internalType: 'uint256',
            name: 'amount',
            type: 'uint256',
          },
        ],
        name: 'transfer',
        outputs: [
          {
            internalType: 'bool',
            name: '',
            type: 'bool',
          },
        ],
        stateMutability: 'nonpayable',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'address',
            name: 'from',
            type: 'address',
          },
          {
            internalType: 'address',
            name: 'to',
            type: 'address',
          },
          {
            internalType: 'uint256',
            name: 'amount',
            type: 'uint256',
          },
        ],
        name: 'transferFrom',
        outputs: [
          {
            internalType: 'bool',
            name: '',
            type: 'bool',
          },
        ],
        stateMutability: 'nonpayable',
        type: 'function',
      },
    ];
    
    TokenBridge Relayer ABI
    export default [
      {
        inputs: [
          {
            internalType: 'uint16',
            name: 'targetChainId',
            type: 'uint16',
          },
          {
            internalType: 'address',
            name: 'token',
            type: 'address',
          },
          {
            internalType: 'uint8',
            name: 'decimals',
            type: 'uint8',
          },
        ],
        name: 'calculateRelayerFee',
        outputs: [
          {
            internalType: 'uint256',
            name: 'feeInTokenDenomination',
            type: 'uint256',
          },
        ],
        stateMutability: 'view',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'address',
            name: 'token',
            type: 'address',
          },
          {
            internalType: 'uint256',
            name: 'amount',
            type: 'uint256',
          },
          {
            internalType: 'uint256',
            name: 'toNativeTokenAmount',
            type: 'uint256',
          },
          {
            internalType: 'uint16',
            name: 'targetChain',
            type: 'uint16',
          },
          {
            internalType: 'bytes32',
            name: 'targetRecipient',
            type: 'bytes32',
          },
          {
            internalType: 'uint32',
            name: 'batchId',
            type: 'uint32',
          },
        ],
        name: 'transferTokensWithRelay',
        outputs: [
          {
            internalType: 'uint64',
            name: 'messageSequence',
            type: 'uint64',
          },
        ],
        stateMutability: 'payable',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'uint256',
            name: 'toNativeTokenAmount',
            type: 'uint256',
          },
          {
            internalType: 'uint16',
            name: 'targetChain',
            type: 'uint16',
          },
          {
            internalType: 'bytes32',
            name: 'targetRecipient',
            type: 'bytes32',
          },
          {
            internalType: 'uint32',
            name: 'batchId',
            type: 'uint32',
          },
        ],
        name: 'wrapAndTransferEthWithRelay',
        outputs: [
          {
            internalType: 'uint64',
            name: 'messageSequence',
            type: 'uint64',
          },
        ],
        stateMutability: 'payable',
        type: 'function',
      },
    ];
    
    Batch Precompile ABI
    export default [
      {
        anonymous: false,
        inputs: [
          {
            indexed: false,
            internalType: 'uint256',
            name: 'index',
            type: 'uint256',
          },
        ],
        name: 'SubcallFailed',
        type: 'event',
      },
      {
        anonymous: false,
        inputs: [
          {
            indexed: false,
            internalType: 'uint256',
            name: 'index',
            type: 'uint256',
          },
        ],
        name: 'SubcallSucceeded',
        type: 'event',
      },
      {
        inputs: [
          {
            internalType: 'address[]',
            name: 'to',
            type: 'address[]',
          },
          {
            internalType: 'uint256[]',
            name: 'value',
            type: 'uint256[]',
          },
          {
            internalType: 'bytes[]',
            name: 'callData',
            type: 'bytes[]',
          },
          {
            internalType: 'uint64[]',
            name: 'gasLimit',
            type: 'uint64[]',
          },
        ],
        name: 'batchAll',
        outputs: [],
        stateMutability: 'nonpayable',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'address[]',
            name: 'to',
            type: 'address[]',
          },
          {
            internalType: 'uint256[]',
            name: 'value',
            type: 'uint256[]',
          },
          {
            internalType: 'bytes[]',
            name: 'callData',
            type: 'bytes[]',
          },
          {
            internalType: 'uint64[]',
            name: 'gasLimit',
            type: 'uint64[]',
          },
        ],
        name: 'batchSome',
        outputs: [],
        stateMutability: 'nonpayable',
        type: 'function',
      },
      {
        inputs: [
          {
            internalType: 'address[]',
            name: 'to',
            type: 'address[]',
          },
          {
            internalType: 'uint256[]',
            name: 'value',
            type: 'uint256[]',
          },
          {
            internalType: 'bytes[]',
            name: 'callData',
            type: 'bytes[]',
          },
          {
            internalType: 'uint64[]',
            name: 'gasLimit',
            type: 'uint64[]',
          },
        ],
        name: 'batchSomeUntilFailure',
        outputs: [],
        stateMutability: 'nonpayable',
        type: 'function',
      },
    ];
    

    For this particular example in Moonbase Alpha, you'll also need the address of a Wormhole relayer. You can use the xLabs relayer:

    0xcafd2f0a35a4459fa40c0517e17e6fa2939441ca
    
    0x9563a59c15842a6f322b10f69d1dd88b41f2e97b
    
  2. Use Ether's encodeFunctionData function to get the encoded call data for the two calls in the batch transaction: the approve transaction and the transferTokensWithRelay transaction

  3. Combine the two transactions into a batch transaction and use Ether's encodeFunctionData to get the encoded call data for the batch transaction
  4. Use the encoded call data for the batch transaction to create the remote EVM call via the ethereumXcm.transact extrinsic, which accepts the xcmTransaction as the parameter. For more information, please refer to the Remote EVM Calls documentation
Create remote EVM call logic
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { ethers } from 'ethers';
import batchABI from './abi/Batch.js';
import erc20ABI from './abi/ERC20.js';
import tokenRelayerABI from './abi/TokenRelayer.js';

// Input data
// ...
const localXC20Address = 'INSERT_LOCAL_XC20_ADDRESS';
const transferAmount = 'INSERT_AMOUNT_TO_TRANSFER';
const xLabsRelayer = '0x9563a59c15842a6f322b10f69d1dd88b41f2e97b';
const batchPrecompile = '0x0000000000000000000000000000000000000808';
const destinationChainId = 'INSERT_DESTINATION_CHAIN_ID';
// The recipient address on the destination chain needs to be formatted in 32 bytes
// You'll pad the address to the left with zeroes. Add the destination address below
// without the 0x
const destinationAddress =
  '0x000000000000000000000000' + 'INSERT_DESTINATION_ADDRESS';

// Transfer multiassets parameters
// ...

// Create contract instances
const batchInterface = new ethers.Interface(batchABI);
const localXC20Interface = new ethers.Interface(erc20ABI);
const tokenRelayer = new ethers.Contract(
  xLabsRelayer,
  tokenRelayerABI,
  new ethers.JsonRpcProvider('https://rpc.api.moonbase.moonbeam.network')
);

// Get the encoded call data for the approve transaction
const approve = localXC20Interface.encodeFunctionData('approve', [
  xLabsRelayer, // Spender
  transferAmount, // Amount
]);

// Get the encoded call data for the transferTokensWithRelay transaction.
// Use wrapAndTransferEthWithRelay if the token is GLMR
const transferTokensWithRelay = tokenRelayer.interface.encodeFunctionData(
  'transferTokensWithRelay',
  [
    localXC20Address, // Token
    transferAmount, // Amount to be transferred
    0, // Amount to swap into native assets on the target chain
    destinationChainId, // Target chain ID, like Ethereum MainNet or Fantom
    destinationAddress, // Target recipient address
    0, // Batch ID for Wormhole message batching
  ]
);

const batchAll = batchInterface.encodeFunctionData('batchAll', [
  [localXC20Address, xLabsRelayer], // Addresses to call
  [0, 0], // Value to send for each call
  [approve, transferTokensWithRelay], // Call data for each call
  [], // Gas limit for each call
]);

const sendBatchTx = async () => {
  // Create origin chain API provider
  // ...

  // Create Moonbeam API provider
  const moonbeamProvider = new WsProvider(
    'wss://wss.api.moonbase.moonbeam.network'
  );
  const moonbeamAPI = await ApiPromise.create({ provider: moonbeamProvider });

  // Create the transferMultiasset extrinsic
  // ...

  // Create the ethereumXCM extrinsic that uses the Batch Precompile
  const transact = moonbeamAPI.tx.ethereumXcm.transact({
    V2: {
      gasLimit: 350000n,
      action: {
        Call: batchPrecompile,
      },
      value: 0n,
      input: batchAll,
    },
  });

  // Additional code goes here
};

sendBatchTx();

Next, you'll need to create the extrinsic to send the remote EVM call to Moonbeam. To do so, you'll want to send an XCM message such that the Transact XCM instruction gets successfully executed. The most common method to do this is through polkadotXcm.send and sending the WithdrawAsset, BuyExecution, and Transact instructions. RefundSurplus and DepositAsset can also be used to ensure no assets get trapped, but they are technically optional.

Send remote EVM call logic
// Rest of script
// ...

const sendBatchTx = async () => {
  // Rest of sendBatchTx logic
  // ...

  const txWeight = (await transact.paymentInfo(multilocationDerivativeAccount))
    .weight;

  const sendXCM = originChainAPI.tx.polkadotXcm.send(
    { V3: { parents: 1, interior: { X1: { Parachain: 1000 } } } },
    {
      V3: [
        {
          // Withdraw DEV asset (0.06) from the target account
          WithdrawAsset: [
            {
              id: {
                Concrete: {
                  parents: 0,
                  interior: { X1: { PalletInstance: 3 } },
                },
              },
              fun: { Fungible: 60000000000000000n },
            },
          ],
        },
        {
          // Buy execution with the DEV asset
          BuyExecution: {
            fees: {
              id: {
                Concrete: {
                  parents: 0,
                  interior: { X1: { PalletInstance: 3 } },
                },
              },
              fun: { Fungible: 60000000000000000n },
            },
            weightLimit: 'Unlimited',
          },
        },
        {
          Transact: {
            originKind: 'SovereignAccount',
            requireWeightAtMost: {
              refTime: txWeight.refTime,
              proofSize: txWeight.proofSize,
            },
            call: {
              encoded: transact.method.toHex(),
            },
          },
        },
        {
          RefundSurplus: {},
        },
        {
          DepositAsset: {
            // Note that this must be AllCounted and not All, since All has too high of a gas requirement
            assets: { Wild: { AllCounted: 1 } },
            beneficiary: {
              parents: 0,
              interior: {
                X1: { AccountKey20: { key: multilocationDerivativeAccount } },
              },
            },
          },
        },
      ],
    }
  );
}

sendBatchTx();

Build the Batch Extrinsic

To ensure that both the xTokens.transferMultiassets and the polkadotXcm.send transactions are sent together, you can batch them together using utility.batchAll. At the time of writing, this helps ensure that the asset transfer happens before the EVM transaction, a necessary distinction. Unfortunately, this is subject to change with future XCM updates.

Batch the transfer multiassets and send remote EVM calls
// Imports
// ...

// Input data
// ...
const privateKey = 'INSERT_YOUR_PRIVATE_KEY';

// Rest of script
// ...

// Create a keyring instance
const keyring = new Keyring({ type: 'ethereum' });
const account = keyring.addFromUri(privateKey);

const sendBatchTx = async () => {
  // Rest of sendBatchTx
  // ...

  // Create batch transaction
  const batchExtrinsic = originChainAPI.tx.utility.batchAll([
    transferMultiassets,
    sendXCM,
  ]);

  // Send batch transaction
  return await batchExtrinsic.signAndSend(account, ({ status }) => {
    if (status.isInBlock) console.log(`Transaction sent!`);
  });
};

sendBatchTx();
View the complete script
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { ethers } from 'ethers';
import batchABI from './abi/Batch.js';
import erc20ABI from './abi/ERC20.js';
import tokenRelayerABI from './abi/TokenRelayer.js';

// Input data
const originChainProviderWsURL = 'INSERT_ORIGIN_CHAIN_WSS_URL';
const multilocationDerivativeAccount =
  'INSERT_MULTILOCATION_DERIVATIVE_ADDRESS';
const localXC20Address = 'INSERT_LOCAL_XC20_ADDRESS';
const transferAmount = 'INSERT_AMOUNT_TO_TRANSFER';
const xLabsRelayer = '0x9563a59c15842a6f322b10f69d1dd88b41f2e97b';
const batchPrecompile = '0x0000000000000000000000000000000000000808';
const destinationChainId = 'INSERT_DESTINATION_CHAIN_ID';
// The recipient address on the destination chain needs to be formatted in 32 bytes
// You'll pad the address to the left with zeroes. Add the destination address below
// without the 0x
const destinationAddress =
  '0x000000000000000000000000' + 'INSERT_DESTINATION_ADDRESS';

// Transfer multiassets parameters
const assets = {
  V3: [
    {
      // xcDEV
      id: {
        Concrete: {
          parents: 1,
          interior: {
            X2: [
              { Parachain: 1000 }, // Parachain ID
              { PalletInstance: 3 }, // Index of the Balances Pallet
            ],
          },
        },
      },
      fun: {
        Fungible: '100000000000000000', // 0.1 DEV as an estimation for XCM and EVM transaction fee
      },
    },
    {
      // Local XC-20 token
      id: {
        Concrete: {
          parents: 1,
          interior: {
            X3: [
              { Parachain: 1000 }, // Parachain ID
              { PalletInstance: 48 }, // Index of the ERC-20 XCM Bridge Pallet
              {
                AccountKey20: {
                  key: localXC20Address,
                },
              },
            ],
          },
        },
      },
      fun: {
        Fungible: transferAmount,
      },
    },
  ],
};
const feeItem = 0;
const destination = {
  V3: {
    parents: 1,
    interior: {
      X2: [
        { Parachain: 1000 },
        { AccountKey20: { key: multilocationDerivativeAccount } },
      ],
    },
  },
};
const weightLimit = 'Unlimited';

// Create contract instances
const batchInterface = new ethers.Interface(batchABI);
const localXC20Interface = new ethers.Interface(erc20ABI);
const tokenRelayer = new ethers.Contract(
  xLabsRelayer,
  tokenRelayerABI,
  new ethers.JsonRpcProvider('https://rpc.api.moonbase.moonbeam.network')
);

// Get the encoded call data for the approve transaction
const approve = localXC20Interface.encodeFunctionData('approve', [
  xLabsRelayer, // Spender
  transferAmount, // Amount
]);

// Get the encoded call data for the transferTokensWithRelay transaction.
// Use wrapAndTransferEthWithRelay if the token is GLMR
const transferTokensWithRelay = tokenRelayer.interface.encodeFunctionData(
  'transferTokensWithRelay',
  [
    localXC20Address, // Token
    transferAmount, // Amount to be transferred
    0, // Amount to swap into native assets on the target chain
    destinationChainId, // Target chain ID, like Ethereum MainNet or Fantom
    destinationAddress, // Target recipient address
    0, // Batch ID for Wormhole message batching
  ]
);

const batchAll = batchInterface.encodeFunctionData('batchAll', [
  [localXC20Address, xLabsRelayer], // Addresses to call
  [0, 0], // Value to send for each call
  [approve, transferTokensWithRelay], // Call data for each call
  [], // Gas limit for each call
]);

// Create a keyring instance
const keyring = new Keyring({ type: 'ethereum' });
const account = keyring.addFromUri(privateKey);

const sendBatchTx = async () => {
  // Create origin chain API [rovider
  const originChainProvider = new WsProvider(originChainProviderWsURL);
  const originChainAPI = await ApiPromise.create({
    provider: originChainProvider,
  });

  // Create Moonbeam API provider
  const moonbeamProvider = new WsProvider(
    'wss://wss.api.moonbase.moonbeam.network'
  );
  const moonbeamAPI = await ApiPromise.create({ provider: moonbeamProvider });

  // Create the transferMultiasset extrinsic
  const transferMultiassets = originChainAPI.tx.xTokens.transferMultiassets(
    assets,
    feeItem,
    destination,
    weightLimit
  );

  // Create the ethereumXCM extrinsic that uses the Batch Precompile
  const transact = moonbeamAPI.tx.ethereumXcm.transact({
    V2: {
      gasLimit: 350000n,
      action: {
        Call: batchPrecompile,
      },
      value: 0,
      input: batchAll,
    },
  });

  const txWeight = (await transact.paymentInfo(multilocationDerivativeAccount))
    .weight;

  const sendXCM = originChainAPI.tx.polkadotXcm.send(
    { V3: { parents: 1, interior: { X1: { Parachain: 1000 } } } },
    {
      V3: [
        {
          // Withdraw DEV asset (0.06) from the target account
          WithdrawAsset: [
            {
              id: {
                Concrete: {
                  parents: 0,
                  interior: { X1: { PalletInstance: 3 } },
                },
              },
              fun: { Fungible: 60000000000000000n },
            },
          ],
        },
        {
          // Buy execution with the DEV asset
          BuyExecution: {
            fees: {
              id: {
                Concrete: {
                  parents: 0,
                  interior: { X1: { PalletInstance: 3 } },
                },
              },
              fun: { Fungible: 60000000000000000n },
            },
            weightLimit: 'Unlimited',
          },
        },
        {
          Transact: {
            originKind: 'SovereignAccount',
            requireWeightAtMost: {
              refTime: txWeight.refTime,
              proofSize: txWeight.proofSize,
            },
            call: {
              encoded: transact.method.toHex(),
            },
          },
        },
        {
          RefundSurplus: {},
        },
        {
          DepositAsset: {
            // Note that this must be AllCounted and not All, since All has too high of a gas requirement
            assets: { Wild: { AllCounted: 1 } },
            beneficiary: {
              parents: 0,
              interior: {
                X1: { AccountKey20: { key: multilocationDerivativeAccount } },
              },
            },
          },
        },
      ],
    }
  );

  // Create batch transaction
  const batchExtrinsic = originChainAPI.tx.utility.batchAll([
    transferMultiassets,
    sendXCM,
  ]);

  // Send batch transaction
  return await batchExtrinsic.signAndSend(account, ({ status }) => {
    if (status.isInBlock) console.log(`Transaction sent!`);
  });
};

sendBatchTx();

If you would like to see an example project that fully implements this, an example is available in a GitHub repository.

It’s important to note that not every parachain will have X-Tokens and the other pallets implemented in a way that will allow this path. Substrate-based chains are very flexible, to the point where a standard doesn’t exist. If you believe your parachain does not support this path, please provide an alternative solution in the Moonbeam forum and to the Wormhole team.

Tokens Available Through Wormhole

While Wormhole has the technical capability to bridge any token across chains, relayers will not support every token for fees. The ERC-20 assets that can be bridged through Wormhole's MRL solution are dependent on the tokens that the xLabs relayer takes in. The tokens that are available to Moonbeam abd Moonbase Alpha are listed in the table below:

Token Name Symbol Decimals Address
Wrapped AVAX wAVAX 18 0xd4937A95BeC789CC1AE1640714C61c160279B22F
Wrapped Bitcoin wBTC 8 0xE57eBd2d67B462E9926e04a8e33f01cD0D64346D
Wrapped BNB wBNB 18 0xE3b841C3f96e647E6dc01b468d6D0AD3562a9eeb
Celo Native Asset CELO 18 0xc1a792041985F65c17Eb65E66E254DC879CF380b
Dai Stablecoin DAI 18 0x06e605775296e851FF43b4dAa541Bb0984E9D6fD
Wrapped Ethereum wETH 18 0xab3f0245B83feB11d15AAffeFD7AD465a59817eD
Wrapped Fantom wFTM 18 0x609AedD990bf45926bca9E4eE988b4Fb98587D3A
Wrapped GLMR wGLMR 18 0xAcc15dC74880C9944775448304B263D191c6077F
Wrapped Matic wMATIC 18 0x82DbDa803bb52434B1f4F41A6F0Acb1242A7dFa3
Wrapped SOL SOL 9 0x99Fec54a5Ad36D50A4Bba3a41CAB983a5BB86A7d
Sui SUI 9 0x484eCCE6775143D3335Ed2C7bCB22151C53B9F49
Tether USD USDT 6 0xc30E9cA94CF52f3Bf5692aaCF81353a27052c46f
USDC (Wormhole) USDC 6 0x931715FEE2d06333043d11F658C8CE934aC61D0c
Token Name Symbol Decimals Address
Wrapped Avax wAVAX 18 0x2E8afeCC19842229358f3650cc3F091908dcbaB4
Wrapped BNB wBNB 18 0x6097E80331B0c6aF4F74D7F2363E70Cb2Fd078A5
Celo Native Asset CELO 18 0x3406a9b09adf0cb36DC04c1523C4b294C6b79513
Dai Stablecoin DAI 18 0xc31EC0108D8e886be58808B4C2C53f8365f1885D
Wrapped Ether wETH 18 0xD909178CC99d318e4D46e7E66a972955859670E1
Wrapped Ether (Wormhole) wETH 18 0xd27d8883E31FAA11B2613b14BE83ad8951C8783C
Wrapped Fantom wFTM 18 0x566c1cebc6A4AFa1C122E039C4BEBe77043148Ee
Wrapped Matic wMATIC 18 0xD2888f015BcB76CE3d27b6024cdEFA16836d0dbb
Sui SUI 9 0x2ed4B5B1071A3C676664E9085C0e3826542C1b27
USDC USDC 6 0x6533CE14804D113b1F494dC56c5D60A43cb5C3b5

Please take the time to verify that these assets are still Wormhole assets on Moonbeam by using the Wormhole asset verifier.

The information presented herein has been provided by third parties and is made available solely for general information purposes. Moonbeam does not endorse any project listed and described on the Moonbeam Doc Website (https://docs.moonbeam.network/). Moonbeam Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Moonbeam Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Moonbeam Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Moonbeam Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Moonbeam Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: March 22, 2024
| Created: June 22, 2023