Skip to content

Moonbeam路由流动性 - Moonbeam Routed Liquidity(MRL)

概览

Moonbeam路由流动性(MRL)是Moonbeam的多种用例之一:与Moonbeam连接的任何区块链都能够通过Moonbeam将流动性导入波卡的其它平行链。而这是由多个组件协同工作来实现的:

通过组合这些组件,Web3的流动性能通过Moonbeam无缝导入其他平行链。流动性的传递既可以通过GMP预编译也可以通过传统智能合约,Moonbeam支持智能合约与XCM预编译之间的互相交互,比如X-Tokens

GMP协议通常以锁定/铸造或销毁/铸造方式移动资产。这种流动性通常以ERC-20代币的形式存在于Moonbeam上。现在Moonbeam上所有ERC-20代币都已支持XCM,只要代币在其他平行链上注册过,它们就可以在平行链上被认证。Moonbeam上启用XCM的ERC-20代币被称为本地XC-20代币

现在MRL仅支持与Wormhole相连的区块链,但任何平行链团队都能用其他GMP提供商来搭建类似路径。

本指南将主要解释一个平行链应该如何与Wormhole SDK和接口集成,以通过Moonbeam从非平行链区块链获得流动性。它也会解释使用时需要的基本配置以及如何有哪些是Wormhole协议支持的代币。

先决条件

要开始与您的平行链进行MRL集成,您首先需要:

Wormhole MRL

Wormhole是第一个为公众搭建的GMP平台,虽然MRL计划支持多种不同GMP提供商,但现在仍以Wormhole为主。在完成所有先决条件之后,您还需要完成以下步骤来通过Wormhole接收流动性:

  • 通知Moonbeam团队您希望加入MRL计划,以便我们为您提供技术实施方面的帮助。
  • 与Wormhole团队和其他依赖MRL的前端联系以敲定技术细节并同步公告。他们可能会需要以下信息:
    • 平行链ID
    • 平行链使用的账户类型(例如,AccountId32 或 AccountKey20)
    • 已注册代币的地址和名称
    • Wormhole Connect前台可用的端点
    • 希望您的平行链通过Wormhole Connect连接的理由

通过Wormhole将代币发送至平行链

MRL提供了一个一键式解决方案,允许您将Multilocation定义为最终目的地,以便资产从任何与Wormhole Connect集成的Wormhole链中转移。

要通过Wormhole和MRL发送代币,用户界面将使用Wormhole TokenBridgeMoonbeam GMP预编译的混合体。

转移流动性的用户将调用起始链上部署的Wormhole TokenBridge智能合约中的transferTokensWithPayload函数,该方法通过 ITokenBridge.sol接口来将代币发送给GMP Precompile。此函数要求一个bytes负载,该负载必须格式化为SCALE编码的Multilocation对象,并包装在另一个预编译特定版本化的类型中。要了解如何构建此负载,请参阅GMP Precompile文档中的搭建Wormhole负载部分。

Wormhole使用一套分布式节点来监视多个区块链的状态。Wormhole将这些节点称为守护者节点(Guardians)。Guardian节点的职责是监控消息并签署相应的负载。如果2/3 Wormhole参与签署的Guardian节点者验证了某个消息,则该消息将被批准并可以在其他链上接收。

Guardian节点的签名与消息组合成为一个叫做已验证操作批准(Verified Action Approval (VAA))的证明。这些VAA将由Wormhole网络中的中继器(Relayer)传递至其目的地,然后在目的地链上VAA将被用于执行某项操作。在本文中,VAA会被传递给GMP Precompile的wormholeTransferERC20函数,该函数通过Wormhole桥接合约(该合约铸造代币,处理VAA,并使用XCM消息将代币中继到平行链。请注意,作为集成MRL的平行链,您大概率不需要部署或使用GMP预编译合约。

中继器的唯一工作是将Wormhole Guardian节点批准的交易传递到目的地链。部分中继器已经支持MRL但任何人都可以独立运行专属中继器。此外,用户在通过Wormhole进行桥接时可以在目的地链上手动执行该交易,并完全避免使用中继器。

Transfering wormhole MRL

通过Wormhole将代币从平行链发送回原链

要通过Wormhole将代币从平行链发送回原链,用户需要发送一笔交易。推荐使用utility.batchAll批处理将代币转账交易和远程执行操作交易打包到同一个extrinsic中。例如同一个extrinsic中可以包含一个xTokens.transferMultiassets交易加上带有Transact指令的 polkadotXcm.send交易。

批处理能提供一个一键式解决方案。然而目前而言,这个操作需要用户在平行链上拥有xcGLMR(GLMR的外部代币)。这主要是因为两个原因:

  • 本地XC-20(启用XCM的ERC-20)不能用于支付Moonbeam上的XCM执行费用。这是一个设计决策,因为XC-20代币的设计需要贴近传统的ERC-20代币,使用ERC-20接口中的transfer函数来转移资产。处理XC-20的XCM指令仅限于将资金从一个账户转移到另一个账户,而XCM流程中需要资产寄存机制,这两者并不兼容。
  • 目前,XCM相关pallet的限制让我们无法使用XCM来转移具有不同储备链的代币。这也导致了在发送XC-20时无法将费用代币设置为本地平行链代币。

将来,X-Tokens Pallet将得到更新,允许使用本地gas货币为XCM付费。使用不同pallet的平行链需要自己实现在一个消息中转移储备与非储备资产的解决方案。

举个例子,下面是将MRL代币通过Wormhole从平行链发送回原链过程的简要概述:

  1. 使用Utility PalletbatchAllextrinsic发送一个批处理交易,其中包含以下两个调用:
    • xTokens.transferMultiassets - 将xcGLMR和本地XC-20发送到用户的Multilocation衍生账户(multilocation-derivative account)。Multilocation衍生账户是Moonbeam上的一个无密钥账户,另一个平行链上的账户可以通过XCM来控制该账户。
    • polkadotXcm.send - 带有Transact指令。通过XCM向Moonbeam上的Batch预编译合同发送远程EVM调用,该Batch预编译使用 ethereumXcm.transact将以下两个调用批处理到一个远程EVM交易中:
      • approve(本地 XC-20 合约)- 授权Wormhole中继器转移本地 XC-20
      • transferTokensWithRelay(中继器合约)- 调用Moonbeam上Wormhole TokenBridge智能合约上的transferTokensWithPayload函数来跨链转移代币,该函数广播消息供Wormhole Guardian节点拾取。
  2. Guardian节点网络将拾取Wormhole交易并对其签名
  3. Wormhole中继器将代币中继到原链和目标账户

Transfering Wormhole MRL out

现在我们已对MRL有了大致的了解,接下来我们将通过实例实现上述功能。以下的示例将向您展示如何将资产从平行链转移到Moonbase Alpha,并通过Wormhole返回原链,本指南也同样适用于Moonbeam。

计算Multilocation衍生账户(Multilocation-Derivative Account)

要通过Wormhole发送代币回原链,您需要计算用户在Moonbeam上的Multilocation衍生账户。您可以使用xcm-tools repository中的calculate-multilocation-derivative-account.ts脚本在链外完成此步骤。有关更多详细信息,您可以参考远程EVM调用文档中的计算Multilocation衍生账户部分。

除此以外,您还可以使用XCM Utilities 预编译中的multilocationToAddress函数。

构建Transfer Multiassets Extrinsic

当有了multilocation衍生账户,您就可以开始构建utility.batchAll交易。开始之前,您需要确认以下包已被安装:

npm i @polkadot/api ethers

现在您可以开始构建xTokens.transferMultiassets交易,该交易接受四个参数:assets, feeItem, dest, 和 destWeightLimit。您可以在X-Tokens Pallet接口文档中找到这些参数的更多信息。

简而言之,assets参数定义了xcDEV(Moonbeam的xcGLMR)和本地XC-20的multilocation和数量,并将xcDEV定位为第一资产,本地XC-20定位为第二资产。feeItem被设置为xcDEV资产的索引,在本例中为0feeItem用DEV来支付Moonbase Alpha的执行费用。dest是一个multilocation,它用于定义我们在前一小节里计算得到的Moonbase Alpha multilocation衍生账户。

此示例的xTokens.transferMultiassets如下所示:

multiassets转账逻辑
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();

在Moonbeam上使用,您需要修改以下参数:

参数
Parachain ID 2004
Balances Pallet Index 10
ERC-20 XCM Bridge Pallet Index 110

构建远程EVM调用

要生成batch交易的第二个调用polkadotXcm.send,您需要先创建一个EVM交易,然后再拼装执行该EVM交易的XCM指令。这个EVM交易可以构建为一个与Batch预编译交互的交易,以便在同一交易中执行两个交易。这很是个很有用的技巧,因为这个EVM交易必须同时批准Wormhole中继器来中继本地XC-20代币以及这个中继操作本身。

要创建batch交易并将其包装在远程EVM调用中以便在Moonbeam上执行,您需要执行以下步骤:

  1. 创建本地XC-20、Wormhole中继器Batch预编译合约的实例您需要每个合约的ABI:

    ERC-20接口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中继器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预编译合约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',
      },
    ];
    

    在Moonbase Alpha这个特定示例中,您还需要一个Wormhole中继器的地址。您可以使用:

    0x9563a59c15842a6f322b10f69d1dd88b41f2e97b
    
  2. 使用Ethers的encodeFunctionData函数来encode batch交易中使用的approve交易和transferTokensWithRelay交易并得到encoded call data

  3. 将两个交易合并为一个batch交易,并再次使用Ethers的encodeFunctionData函数来encode该交易的call data
  4. 使用ethereumXcm.transactextrinsic为batch交易创建远程EVM调用,该extrinsic使用上一步得到的encoded call data作为xcmTransaction参数。有关更多信息,请参阅远程EVM调用文档
创建远程EVM调用逻辑
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();

接下来,您需要创建extrinsic以将远程EVM调用发送到Moonbeam。您需要发送一个XCM消息来执行TransactXCM指令。最常见的做法是通过polkadotXcm.send发送WithdrawAsset, BuyExecution, 和 Transact指令。也可用 RefundSurplusDepositAsset来确保资产不会被锁,但技术上它们不是必须的。

发送远程EVM调用逻辑
// 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();

构建Batch Extrinsic

为确保xTokens.transferMultiassetspolkadotXcm.send交易同时发送,您可以使用utility.batchAll将它们批量处理在一起。至少在现在,这有助于确保资产转移在EVM交易之前发生,这是一个十分必要的特质。但是这在以后可能会因为XCM升级而发生变化。

将multilocation资产交易与远程EVM调用发送打包
// 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();
查看完整脚本
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();

如果您想查看一个完全实现了此功能的示例项目,可以访问这个GitHub repository

请注意,并非每个平行链都包括X-Tokens和其他必要Pallet。基于Substrate的链都非常灵活,甚至没有一个固定标准。如果您认为您的平行链不支持此路径,请在Moonbeam论坛上和Wormhole团队中提供替代解决方案。

Wormhole支持的代币

虽然Wormhole在技术上支持跨链转移任何代币,但中继器并不支持所有代币的费用支付。基于Wormhole MRL的解决方案可转移的ERC-20资产取决于xLabs 中继器能接受的代币种类。以下表格中列出了Moonbeam支持的代币:

代币名称 地址
WMATIC 0x82DbDa803bb52434B1f4F41A6F0Acb1242A7dFa3
WGLMR 0xAcc15dC74880C9944775448304B263D191c6077F
WFTM 0x609AedD990bf45926bca9E4eE988b4Fb98587D3A
WETH 0xab3f0245B83feB11d15AAffeFD7AD465a59817eD
WBTC 0xE57eBd2d67B462E9926e04a8e33f01cD0D64346D
wTBTC 0xeCd65E4B89495Ae63b4f11cA872a23680A7c419c
WBNB 0xE3b841C3f96e647E6dc01b468d6D0AD3562a9eeb
WAVAX 0xd4937A95BeC789CC1AE1640714C61c160279B22F
USDT 0xc30E9cA94CF52f3Bf5692aaCF81353a27052c46f
USDC 0x931715FEE2d06333043d11F658C8CE934aC61D0c
SUI 0x484eCCE6775143D3335Ed2C7bCB22151C53B9F49
CELO 0xc1a792041985F65c17Eb65E66E254DC879CF380b

使用前请花时间用Wormhole资产验证器验证这些代币在Moonbeam上仍然是有效的Wormhole资产。

Last update: January 25, 2024
| Created: October 30, 2023