Skip to content

使用XCM Transactor Pallet进行远程执行

概览

XCM消息是由跨共识虚拟机(XCVM)执行的一系列指令组成。这些指令的组合会产生预先确定的操作,例如跨链Token转移,更有趣的是,远程跨链执行。

然而,从头开始构建XCM消息还是比较困难。此外,XCM消息从根账户(即SUDO或通过民主投票)发送给生态系统中的其他参与者,这对于希望通过简单交易实现远程跨链调用的项目来说并不合适。

要克服这些困难,开发者可以利用wrapper函数或pallet来使用波卡或Kusama上的XCM功能,例如XCM Transactor Pallet。另外,XCM Transactor Pallet允许用户从主权账户衍生出来的账户(称为衍生账户)执行远程跨链调用,从而可通过简单的交易轻松执行。

pallet的两个主要extrinsic是通过主权衍生账户或从给定multilocation计算的衍生账户进行交易。每个extrinsic都相应命名。

本教程将向您展示如何使用XCM Transactor Pallet在生态系统(中继链或平行链)中从基于Moonbeam的网络发送XCM消息至其他链。此外,您还将学习到如何使用XCM Transactor预编译通过以太坊API执行同样的操作。

请注意,通过XCM消息进行远程执行仍然有一些限制。

开发者须知悉,若发送不正确的XCM消息可能会导致资金丢失。因此,XCM功能需先在测试网上进行测试后才可移至生产环境。

用于远程执行的XCM指令

通过XCM进行远程执行的相关指令,有但不限于:

  • DescendOrigin - 在目标链中执行。改变将用于执行后续XCM指令的起始地址
  • WithdrawAsset - 在目标链中执行。移除资产并将其放于待使用
  • BuyExecution - 在目标链中执行。从持有资产中提取用于支付执行费用。支付的费用取决于目标链
  • Transact - 在目标链中执行。从给定原始链派遣编码的调用数据

当由XCM Transactor Pallet创建的XCM消息执行后,必须支付费用。所有的相关信息可以在XCM费用页面的XCM Transactor费用部分找到。

相关XCM定义

  • 主权账户 — 生态系统中每条链预设的账户,用于中继链和其他平行链。它的地址为特定单词和平行链ID连接的 blake2 哈希(中继链中的主权账户为blake2(para+ParachainID),其他中的主权账户为blake2(sibl+ParachainID)平行链),将哈希截断为正确的长度。该帐户归root所支配,只能通过SUDO(如果有)或民主(公投)使用。主权账户通常在其它链上签署 XCM 消息
  • Multilocation — 一种指定整个中继链或平行链生态系统中来源点(相对或绝对来源)的方式;例如,它可用于指定特定的平行链、资产、账户,甚至是一个Pallet。一般而言,multilocation定义包含为parentsinterior:

    • parents - 是指需要从来源点“跳跃”多少次可以进入parent区块链
    • interior,是指定义目标点需要多少个字段

    例如,要从另一个平行链中定位ID为1000的平行链,multilocation将是 { "parents": 1, "interior": { "X1": [{ "Parachain": 1000 }]}}

  • Multilocation-derivative account — 这会生产一个从Descend Origin XCM指令和提供的mulitlocation设置的新来源衍生的无私钥账户。对于基于Moonbeam的网络,衍生方法是计算multilocation的blake2哈希,包括原始平行链ID并将哈希截断为正确的长度(以太坊格式的账户为20个字节)。Transact指令执行时会发生XCM调用原始转换。因此,每个平行链可以使用自己想要的程序转换起点,从而发起交易的用户可能在每条平行链上拥有不同的衍生账户。该衍生账户支付交易费用,并设置为调用的派遣员

  • Transact information — 与XCM Transactor extrinsic的XCM远程执行部分的额外权重和费用信息相关。这是必要的,因为XCM交易费用由主权账户进行支付。因此,XCM Transactor计算此费用,并向XCM Transactor extrinsic的发送者收取对应XC-20 token的预估费用来偿还主权账户

XCM Transactor Pallet接口

Extrinsics

XCM Transactor Pallet提供以下extrinsics(函数):

  • hrmpManage(action, fee, weightInfo) - 管理与打开、接受和关闭HRMP通道相关的HRMP操作。给定的操作可以是以下三个操作中的任何一个:InitOpenAcceptClose、和Cancel
  • removeFeePerSecond(assetLocation) — 移除其储备链中给定资产的每秒费用信息。资产定义为multilocation
  • removeTransactInfo(location) — 移除给定链的交易信息,定义为multilocation
  • setFeePerSecond(assetLocation, feePerSecond) — 设置其储备链中给定资产的每秒交易费信息。资产定义为multilocation。feePerSecond是每秒XCM执行的Token单位,将会向XCM Transactor extrinsic的发送者收取费用
  • setTransactInfo(location, transactExtraWeight, maxWeight) — 设置给定链的交易信息,定义为multilocation。交易信息包含:
    • transactExtraWeight — 支付XCM指令执行费用(WithdrawAssetBuyExecutionTransact)的权重,预计至少比移除XCM指令执行使用的费用高出10%以上
    • maxWeight — 允许远程XCM执行的最大权重单位
    • transactExtraWeightSigned — (可选)支付XCM指令执行费用(DescendOriginWithdrawAssetBuyExecutionTransact)的权重,预计至少比移除XCM指令执行使用的费用高出10%以上
  • transactThroughSigned(destination, fee, call, weightInfo) — 发送XCM消息,包含在给定目标链上远程执行特定调用的指令。远程调用将通过目标平行链衍生的新账户签署和执行。对于基于Moonbeam的网络,此账户是继承的multilocation的blake2哈希,截断成正确的长度。XCM Transactor Pallet计算远程执行的费用,并向extrinsic的发送者收取资产ID给出的对应XC-20 token预估费用
  • transactThroughSovereign(destination, feePayer, fee, call, originKind, weightInfo) — 发送XCM消息,包含在给定目标链上远程执行特定调用的指令。程调用将通过支付费用的原始平行链主权账户签署,而交易是从给定起始账户发送。XCM Transactor Pallet计算远程执行的费用,并通过资产multilocation向给定账户收取对应XC-20 token的预估费用

其中需要输入的内容如下:

  • assetLocation — 代表储备链上资产的multilocation,用于设置或获取每秒交易信息的费用
  • location — 代表生态系统中一条链的multilocation,用于设置或获取交易信息
  • destination — 代表生态系统中一条链的multilocation,XCM消息将发送到该位置
  • fee — 一个枚举(enum),为开发者提供两个关于如何定义XCM执行费用项目的选项。两种选项均依赖于feeAmount,即您为执行发送的XCM消息而提供的每秒XCM执行的资产单位。设置费用项目的两种不同方式如下:

    • AsCurrencyID — 用于支付远程调用执行的币种ID。不同的runtime有不同的定义ID的方式。以基于Moonbeam网络为例,SelfReserve指原生Token,ForeignAsset指XC-20资产ID(区别于XC-20地址)

    对于基于Moonbeam的网络,SelfReserve指的是原生Token,ForeignAsset指的是外部 XC-20的资产ID {target=_blank}(不要与XC-20地址混淆),而Erc20指的是原生XC-20

    • AsMultiLocation — 代表用于执行XCM时支付费用的资产multilocation
    • innerCall — 在目标链中执行的调用的编码调用数据。如果通过主权衍生账户进行交易,这将包装在asDerivative选项中
    • weightInfo — 包含所有权重相关信息的结构。若没有提供足够的权重,则XCM执行将失败,资金可能会被锁定在主权账户或特定pallet中。因此,正确设置目标权重以避免XCM执行失败至关重要。结构包含以下两种字段:
    • transactRequiredWeightAtMost — 与Transact调用本身执行相关的权重。对于通过主权衍生的交易,您也需要考虑asDerivative extrinsic的权重。但是,这不会包含在所有XCM指令的成本(权重)当中
    • overallWeight — XCM Transactor extrinsic可以使用的所有权重。这包含所有XCM指令以及调用本身(transactRequiredWeightAtMost)的权重
    • call — 类似于innerCall,但是并没有用asDerivative extrinsic包装
    • feePayer — 将通过主权账户支付远程XCM执行交易费用的地址。费用将根据对应的XC-20 token收取
    • originKind — 在目标链中远程调用的派遣者。目前有四种派遣者类型可使用

存储方法

XCM Transactor Pallet包含以下只读存储方法:

  • destinationAssetFeePerSecond() — 返回给定multilocation资产的每秒费用。这能够将权重转换成费用。如果feeAmount设置为None,pallet extrinsics将读取存储元素
  • palletVersion() — 从存储库返回当前pallet的版本
  • transactInfoWithWeightLimit(location) — 返回给定multilocation的交易信息。如果feeAmount设置为None,pallet extrinsics将读取存储元素

Pallet常量

XCM Transactor Pallet包含以下只读函数以获取pallet常量:

  • baseXcmWeight() — 返回每个XCM指令执行所需的基本XCM权重
  • selfLocation() — 返回链的multilocation

通过签署函数进行XCM Transactor交易

此部分包含使用transactThroughSigned函数通过XCM Transactor Pallet为远程执行构建XCM消息。但是,由于目标平行链暂未公开,您将无法跟进。

注意事项

请确保您已在目标链中允许将要远程执行的调用!

查看先决条件

要在发送extrinsics,您需要准备以下内容:

在本示例中,使用的账户如下:

  • Alice在原始平行链中的地址为0x44236223aB4291b93EEd10E4B511B37a398DEE55
  • 在目标平行链中的multilocation衍生账户地址为0x5c27c4bb7047083420eddff9cddac4a0a120b45c2

构建XCM

由于您将与XCM Transactor Pallet的transactThroughSigned函数交互,您需要组装destfeecallweightInfo参数。为此,您可以执行以下步骤:

  1. 定义目标multilocation,其目标是平行链888:

    const dest = {
      V3: {
        parents: 1,
        interior: { X1: { Parachain: 888 } },
      },
    };
    
  2. 定义fee信息,其将要求您:

    • 定义币种ID并提供资产详情
    • 设置费用金额
    const fee = {
      currency: {
        AsCurrencyId: { 
          ForeignAsset: 3547752324713722007834302681851459189n 
        },
      },
      feeAmount: 50000000000000000n,
     };
    
    const fee = {
      currency: {
        AsCurrencyId: { Erc20: { contractAddress: ERC_20_ADDRESS} },
      },
      feeAmount: 50000000000000000n,
    };
    
  3. 定义将在目标链中执行的call。这里需要pallet、函数和输入值的编码调用数据

    它可以在Polkadot.js Apps中构建(必须连接至目标链)或使用Polkadot.js API。对于本示例而言,内部调用是将目标链的1个Token余额简单转移到Alice的账户:

    const call =
      '0x030044236223ab4291b93eed10e4b511b37a398dee5513000064a7b3b6e00d';
    
  4. 设置weightInfo,其包含所需的transactRequiredWeightAtMost权重和可选的overallWeight参数。两个权重参数都要求您指定refTimeproofSize,其中refTime是可用于执行的计算时间量,proofSize是可使用的存储量(以字节为单位)。对于每个参数,您可以遵循以下准则:

    • 对于transactRequiredAtMost,该值必须包含asDerivative extrinsic。然而,这并不包含XCM指令的权重。在本示例中,将refTime设置为 1000000000的权重单位,将proofSize设置为40000
    • 对于overallWeight,该值必须是transactRequiredWeightAtMost加上在目标链中XCM指令执行成本所需的权重之和。如果您不提供此值,pallet将使用存储中的元素(若有)。在本示例中,将refTime设置为2000000000权重单位,将将proofSize设置为50000
    const weightInfo = {
      transactRequiredWeightAtMost: { refTime: 1000000000n, proofSize: 40000n },
      overallWeight: { refTime: 2000000000n, proofSize: 50000n },
    };
    

    注意事项

    关于refTimeproofSize数值的准确预估,您可以使用在远程EVM调用教程中描述的Polkadot.js API的paymentInfo函数

现在,您已经有了每个参数的值,您可以为交易编写脚本了。为此,您可以执行以下步骤:

  1. 提供调用的输入数据,这包含:

    • 用于创建提供商的Moonbase Alpha端点URL
    • transactThroughSigned函数的每个参数的值
  2. 创建一个用于发送交易的Keyring实例

  3. 创建Polkadot.js API提供商
  4. 使用destfeecallweightInfo值制作xcmTransactor.transactThroughSigned extrinsic
  5. 使用signAndSend extrinsic和在第二个步骤创建的Keyring实例发送交易

请记住

本教程的操作仅用于演示目的,请勿将您的私钥存储至JavaScript文档中。

import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; // Version 9.13.6

// 1. Provide input data
const providerWsURL = 'wss://wss.api.moonbase.moonbeam.network';
const privateKey = 'INSERT_PRIVATE_KEY';
const dest = {
  V3: {
    parents: 1,
    interior: { X1: { Parachain: 888 } },
  },
};
const fee = {
  currency: {
    AsCurrencyId: { ForeignAsset: 35487752324713722007834302681851459189n },
  },
  feeAmount: 50000000000000000n,
};
const call = '0x030044236223ab4291b93eed10e4b511b37a398dee5513000064a7b3b6e00d';
const weightInfo = {
  transactRequiredWeightAtMost: { refTime: 1000000000n, proofSize: 40000n },
  overallWeight: { Unlimited: null },
};
const refund = true;

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

const transactThroughSigned = async () => {
  // 3. Create Substrate API provider
  const substrateProvider = new WsProvider(providerWsURL);
  const api = await ApiPromise.create({ provider: substrateProvider });

  // 4. Craft the extrinsic
  const tx = api.tx.xcmTransactor.transactThroughSigned(
    dest,
    fee,
    call,
    weightInfo,
    refund
  );

  // 5. Send the transaction
  const txHash = await tx.signAndSend(alice);
  console.log(`Submitted with hash ${txHash}`);

  api.disconnect();
};

transactThroughSigned();

注意事项

您可以使用以下编码的调用数据在Polkadot.js Apps上查看上述脚本的示例,该脚本将1个xcUNIT发送给中继链上Alice的账户:0x210603010100e10d00017576e5e612ff054915d426c546b1b21a010000c52ebca2b10000000000000000007c030044236223ab4291b93eed10e4b511b37a398dee5513000064a7b3b6e00d02286bee02710200010300943577420d030000

交易处理后,Alice应该在目标链的地址上收到1个Token。

XCM Transactor预编译

XCM Transactor预编译合约允许开发者通过基于Moonbeam网络的以太坊API访问XCM Transactor Pallet功能。与其他预编译合约相似,XCM Transactor预编译位于以下地址:

0x000000000000000000000000000000000000080d
0x000000000000000000000000000000000000080d
0x000000000000000000000000000000000000080d

XCM Transactor旧版预编译仍可在所有基于Moonbeam网络中使用。但是,旧版本将在不久的将来被弃用,因此所有实现都必须迁移到较新的接口。XCM Transactor旧版预编译位于以下地址:

0x0000000000000000000000000000000000000806
0x0000000000000000000000000000000000000806
0x0000000000000000000000000000000000000806

注意事项

在Moonbeam使用预编译合约时,可能会出现一些意想不到的后果。 请参阅安全注意事项 页面了解更多信息。

XCM Transactor Solidity接口

XcmTransactor.sol是一个接口,开发者可以用其通过以太坊API与XCM Transactor Pallet进行交互。

注意事项

XCM Transactor预编译的旧版本将在不久的将来被弃用,因此所有实现都必须迁移到较新的接口。

此接口包含以下函数:

  • indexToAccount(uint16 index) — 只读函数,返回授权使用给定索引的基于Moonbeam网络主权账户操作的注册地址
  • transactInfoWithSigned(Multilocation memory multilocation) — 只读函数,对于定义为multilocation的给定链,返回考虑与外部调用执行(transactExtraWeight)关联的3个XCM指令的交易消息。这也将返回通过签署extrinsic(transactExtraWeightSigned)交易的DescendOrigin XCM指令关联的额外权重信息
  • feePerSecond(Multilocation memory multilocation) — 只读函数,对于作为multilocation的给定资产,返回每秒XCM执行的Token单位,其作为XCM执行费用收取。这对于给定链有多种资产可以作为手续费进行支付使非常有用
  • transactThroughSignedMultilocation(Multilocation memory dest, Multilocation memory fee_location, uint64 transactRequiredWeightAtMost, bytes memory call, uint256 feeAmount, uint64 overallWeight) — 表示上述示例中描述的transactThroughSigned方法的函数,将fee类型设置为AsMultiLocation。您需要提供Token的资产multilocation来支付费用,而不是XC-20 Token address
  • transactThroughSigned(Multilocation memory dest, address fee_location_address, uint64 transactRequiredWeightAtMost, bytes memory call, uint256 feeAmount, uint64 overallWeight) — 表示上述示例中描述的transactThroughSigned方法的函数,将fee类型设置为AsCurrencyId。您将需要提供用于支付费用的Token的资产XC-20地址,而不是资产ID
  • encodeUtilityAsDerivative(uint8 transactor, uint16 index, bytes memory innerCall) - 给定要使用的交易者(transactor)、衍生账户的索引(index)以及要从衍生地址执行的内部调用(innerCall),对asDerivative包装调用进行编码

构建预编译Multilocation

在XCM Transactor预编译接口中,Multilocation结构定义为如下:

struct Multilocation {
    uint8 parents;
    bytes[] interior;
}

请注意每个multilocation皆有parents元素,请使用uint8和一组字节定义。Parents代表有多少“hops”在通过中继链中您需要执行的传递向上方向。作为uint8,您将会看到以下数值:

Origin Destination Parents Value
Parachain A Parachain A 0
Parachain A Relay Chain 1
Parachain A Parachain B 1

字节阵列(bytes[])定义了内部参数以及其在multilocation的内容。阵列的大小则根据以下定义interior数值:

Array Size Interior Value
[] 0 Here
[XYZ] 1 X1
[XYZ, ABC] 2 X2
[XYZ, ... N] N XN

注意事项

内部值Here通常用于中继链(作为目标链或以中继链资产为目标)。

假设所有字节阵列包含数据。所有元素的首个字节(2个十六进制数值)与XN部分的选择器相关,举例来说:

Byte Value Selector Data Type
0x00 Parachain bytes4
0x01 AccountId32 bytes32
0x02 AccountIndex64 u64
0x03 AccountKey20 bytes20
0x04 PalletInstance byte
0x05 GeneralIndex u128
0x06 GeneralKey bytes[]

接着,根据选择器及其数据类型,以下字节对应于提供的实际数据。请注意在Polkadot.js Apps示例中出现的AccountId32AccountIndex64AccountKey20network将会在最后添加。如下所示:

Selector Data Value Represents
Parachain "0x00+000007E7" Parachain ID 2023
AccountId32 "0x01+AccountId32+00" AccountId32, Network(Option) Null
AccountId32 "0x01+AccountId32+02" AccountId32, Network Polkadot
AccountKey20 "0x03+AccountKey20+00" AccountKey20, Network(Option) Null
PalletInstance "0x04+03" Pallet Instance 3

注意事项

interior数据通常需要使用引号包含。如果您未遵循此规则,您将会获得invalid tuple value错误。

下面的代码片段介绍了Multilocation结构的一些示例,他们需要被输入到XCM Transactor预编译函数中:

// Multilocation targeting the relay chain asset from a parachain
{
    1, // parents = 1
    [] // interior = here
}

// Multilocation targeting Moonbase Alpha DEV token from another parachain
{
    1, // parents = 1
    // Size of array is 2, meaning is an X2 interior
    [
        "0x00000003E8", // Selector Parachain, ID = 1000 (Moonbase Alpha)
        "0x0403" // Pallet Instance = 3
    ]
}

// Multilocation targeting aUSD asset on Acala
{
    1, // parents = 1
    // Size of array is 1, meaning is an X1 interior
    [
        "0x00000007D0", // Selector Parachain, ID = 2000 (Acala)
        "0x060001" // General Key Selector + Asset Key
    ]
}
Last update: January 25, 2024
| Created: July 8, 2022