Skip to content

使用X-Tokens Pallet发送XC-20s

x-tokens Precompile Contracts Banner

概览

为同质化资产转移构建XCM信息通道并非一件易事。因此,开发者可以通过利用wrapper函数/pallet在Polkadot/Kusama上使用XCM功能。

此类包装器的一个示例是x-tokens pallet,用于提供通过XCM转移同质化资产的不同方法。

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

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

相关XCM定义

  • XCM — 代表跨共识信息,是共识系统相互通信的信息格式
  • VMP — 代表垂直消息传递,它允许平行链与中继链交换消息。UMP(向上消息传递)允许平行链将消息发送到它们的中继链,而 DMP(向下消息传递)允许中继链将消息向下传递到它们的平行链之一
  • XCMP — 代表跨共识消息传递,它允许平行链与同一中继链上的其他平行链交换消息
  • HRMP — 代表平行中继路由消息传递,是启动完整XCMP实现前的过度实现方式。与XCMP具有相同的接口,但消息存储在中继链上
  • 主权账户 — 生态系统中每条链预设的账户,用于中继链和其他平行链。它的地址为特定单词和平行链ID连接的 blake2 哈希(中继链中的主权账户为blake2(para+ParachainID),其他中的主权账户为blake2(sibl+ParachainID)平行链),将哈希截断为正确的长度。该帐户归root所支配,只能通过SUDO(如果有)或民主(技术委员会或公民投票)使用。主权账户通常在其它链上签署 XCM 消息
  • Multilocation — 一种指定整个中继链或平行链生态系统中来源点(相对或绝对来源)的方式;例如,它可用于指定特定的平行链、资产、账户,甚至是一个Pallet。一般而言,multilocation定义包含为parentsinteriorparents是指需要从来源点“跳跃”多少次可以进入parent区块链。interior,是指定义目标点需要多少个字段。例如,要从另一个平行链中定位ID为1000的平行链,multilocation将是 { "parents": 1, "interior": { "X1": [{ "Parachain": 1000 }]}}

X-tokens Pallet接口

Extrinsics

X-tokens pallet提供以下extrinsics(函数):

  • transfer(currencyId, amount, dest, destWeight) — 转移一个币种,根据原生Token(自身储备)或是资产ID定义
  • transferMultiasset(asset, dest, destWeight) — 转移一种可替代资产,根据其multilocation定义
  • transferMultiassetWithFee(asset, fee, dest, destWeight) — 转移一种可替代资产,但其能够让资产的发送者使用另外一种资产支付转移费用。两者皆根据其multilocation定义
  • transferMultiassets(assets, feeItem, dest, destWeight) — 转移多种可替代资产,并定义其中哪种资产将会被用于支付转移费用。所有资产皆由其multilocation定义
  • transferMulticurrencies(currencies, feeItem, dest, destWeight) — 转移多个币种,并定义其中哪种币种将会被用于支付转移费用。所有币种都将通过原生Token(自身储备)或是资产ID定义
  • transferWithFee(currencyId, amount, fee, dest, destWeight) — 转移一个币种,但允许资产发送者使用不同的资产支付转移费用。两者皆由其multilocation定义

其中需要提供信息输入的函数定义如下:

  • currencyId/currencies — 将通过XCM转移的币种ID。不同runtime有不同的方法定义ID。以基于Moonbeam的网络为例子,SelfReserve代表原生Token,ForeignAsset代表其XC-20资产ID(而不是其XC-20地址)
  • amount — 将通过XCM转移的Token数量
  • dest — 一个multilocation,用于定义将通过XCM转移Token的目标地址。其支持不同地址格式,如20或32字节的地址(以太坊或是Substrate格式)
  • destWeight — 您提供给目标链希望其执行XCM信息的最大执行时间。如果您提供的信息不足,XCM将会执行失败,且资金将会被锁定在主权账户或是特定的pallet中。设置目标权重非常重要,这将避免XCM失败
  • asset/assets — 一个用于定义将通过XCM转移资产的multilocation。每条平行链将会有不同定义资产的方式。举例而言,基于Moonbeam的网络将会经由其原生Token的pallet余额索引定义
  • fee — 一个用于定义支付XCM在目标链上执行的multilocation
  • feeItem — 一个用于定义多样资产发送地点的索引,将用于支付XCM在目标链上的执行。举例而言,如果仅有一种资产被发送,feeItem将会是0

存储方法

X-tokens pallet包括以下只读存储方式:

  • palletVersion() - 提供正在使用的x-tokens pallet的版本

Pallet常量

X-tokens pallet包括以下用于获取pallet常量的只读函数:

  • baseXcmWeight() - 返回执行所需的基本XCM重量
  • selfLocation() - 返回本地的multilocation

使用x-tokens Pallet构建XCM信息

此教程将会包含使用x-tokens pallet构建XCM信息的过程,更详细来说为使用transfertransferMultiasset函数。然而,这两种情况仍然可以外推至其他函数,特别是当您熟悉了多重地点的使用之后。

注意事项

每条平行链皆能够通过pallet允许/禁止特定函数。因此,开发者需要确认使用的函数是被平行链允许的。相反来说,如果使用了被禁止的函数,交易将会如同system.CallFiltered显示一般失败。

本教程将以转移xcUNIT token为例。xcUNIT是Alphanet中继链Token UNITXC-20形式,也是外部XC-20。本教程也同样适用于其他外部XC-20或可铸造XC-20

查看先决条件

要在Polikadot.js Apps上发送extrinsics,您需要准备以下内容:

Moonbeam Swap xcUNIT

要查看您的xcUNIT余额,您可以使用以下地址将XC-20添加至MetaMask:

0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080

您可以通过以下教程查看如何计算预编译地址:

X-Tokens转移函数

在本示例中,您将会构建一个XCM信息,通过x-tokens pallet的transfer函数将xcUNIT从Moonbase Alpha转移回其中继链上。

导航至Polkadot.js Apps的extrinsic页面,并设定以下选项(也可以适用于可铸造XC-20s):

  1. 选取您希望转移XCM的账户
  2. 选择xTokens pallet
  3. 选择transfer extrinsic
  4. 将外部XC-20的币种ID设置为ForeignAsset或将可铸造XC-20的币种ID设置为LocalAssetReserve。因为xcUNIT是外部XC-20,所以您需要选择ForeignAsset
  5. 输入资产ID。在本示例中,xcUNIT的资产ID为42259045809535163221576417993425387648
  6. 设置需要转移的Token数量。在本示例中,您将转移1个xcUNIT,但您需要注意xcUNIT后有12位小数位
  7. 定义XCM目标multilocation,您需要将Moonbase Alpha的中继链中的一个账户作为初始点。因此,您需要设置以下参数:

    参数 数值
    Version V1
    Parents 1
    Interior X1
    X1 AccountId32
    Network Any
    Id Target Account
  8. 将目标权重设置为1000000000。请注意,在Moonbase Alpha上,每个XCM操作需要大概100000000权重单位。一个transfer包含4个XCM操作,因此目标权重应当设置为400000000左右

  9. 点击Submit Transaction按钮并签署交易

注意事项

以上extrinsict配置的编码调用数据为0x1e00018080778c30c20fa2ebc0ed18d2cbca1f0010a5d4e800000000000000000000000101010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300ca9a3b00000000,这同样包含一个您需要改变的接收者函数。

XCM x-tokens Transfer Extrinsic

当交易正在处理中,TargetAccount将会获取设定的转移数量并扣除用于在目标链上执行XCM的小额费用。在Polkadot.js Apps,您可以查看Moonbase Alpha以及中继链的相关extrinsics和事件。

X-Tokens转移多种资产函数

在本示例中,您将会构建一个XCM信息,通过x-tokens pallet的transferMultiasset函数将xcUNIT从Moonbase Alpha转移回其中继链上。

导航至Polkadot.js Apps的extrinsic页面,并设定以下选项:

  1. 选取您希望转移XCM的账户

  2. 选择xTokens pallet

  3. 选择transferMultiasset extrinsic

  4. 定义XCM资产multilocation,您需要将Moonbase Alpha的中继链中的UNIT作为初始点。每条链皆有不同的定义方法。因此,您需要为各个目标设置不同的资产multilocation。在此例而言,中继链Token将会通过以下函数定义:

    参数 数值
    Version V1
    Parents 1
    Interior Here

    如果您要将本教程修改成适应可铸造XC-20资产,您需要指定创建资产和资产ID的pallet。因此,您需要设置以下参数:

    参数 数值
    Version V1
    Parents 1
    Interior X2
    PalletInstance 36
    GeneralIndex Asset ID
  5. 将资产类别设置为Fungible

  6. 设置需要转移的Token数量。在本示例中,您将转移1个xcUNIT,但您需要注意xcUNIT后有12位小数位

  7. 定义XCM目标multilocation,您需要将Moonbase Alpha的中继链中的一个账户作为初始点。因此,您需要设置以下参数:

    参数 数值
    Version V1
    Parents 1
    Interior X1
    X1 AccountId32
    Network Any
    Id Target Account
  8. 将目标权重设置为1000000000。请注意,在Moonbase Alpha上,每个XCM操作需要大概100000000权重单位。一个transferMultiasset包含4个XCM操作,因此目标权重应当设置为400000000左右

  9. 点击Submit Transaction按钮并签署交易

注意事项

以上extrinsict配置的编码调用数据为0x1e010100010000070010a5d4e80101010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300ca9a3b00000000,这同样包含一个您需要改变的接收者函数。

XCM x-tokens Transfer Extrinsic

当交易正在处理中,TargetAccount将会获取设定的转移数量并扣取用于在目标链上执行XCM的小额费用。在Polkadot.js Apps,您可以查看Moonbase Alpha以及中继链的相关extrinsics和事件。

X-Tokens预编译

X-tokens预编译合约将会允许开发者通过基于Moonbeam网络的以太坊API访问XCM Token转移功能。如同其他预编译合约,x-tokens预编译位于以下地址:

0x0000000000000000000000000000000000000804
0x0000000000000000000000000000000000000804
0x0000000000000000000000000000000000000804

注意事项

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

X-Tokens Solidity接口

Xtokens.sol是一个开发者能够使用以太坊API与x-tokens pallet交互的接口。

此接口包含以下函数:

  • transfer(address currencyAddress, uint256 amount, Multilocation memory destination, uint64 weight) —— 用于表示先前示例中提及的transfer函数。然而,在使用币种ID之外,您需要为currencyAddress提供资产预编译地址:

    destination multilocation将会以一种特殊形式构建(我们将在下一部分提及)

  • transferMultiasset(Multilocation memory asset, uint256 amount, Multilocation memory destination, uint64 weight) —— 用于表示先前示例中提及的transferMultiasset函数。两种multilocation将会以一种特殊形式构建(我们将在下一部分提及)

构建预编译Multilocation

在x-tokens预编译接口中,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 Any
AccountKey20 "0x03+AccountKey20+00" AccountKey20, Network Any
PalletInstance "0x04+03" Pallet Instance 3

注意事项

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

以下代码片段包含Multilocation架构的部分示例,因为其将会在x-tokens预编译函数中使用:

// Multilocation targeting the relay chain or its 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 Alice's account on the Relay Chain from Moonbase Alpha
{
    1, // parents = 0
    // Size of array is 1, meaning is an X1 interior
    [
        "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300" 
        // AccountKey32 Selector + Address in hex + Network = Any
    ]
}

使用库与X-Token交互

当使用库与Ethereum API交互时,Multilocation结构可以像任何其他结构一样格式化。以下代码片段包括前面的x-tokens 传输函数x-tokens 多资产传输函数和示例Multilocation结构示例。您可以在Github上找到x-tokens的合约ABI

import abi from './xtokensABI.js'; // Import the x-tokens ABI
import { ethers } from 'ethers'; // Import Ethers library
const PRIVATE_KEY = 'YOUR_PRIVATE_KEY_HERE';

// Create Ethers wallet & contract instance
const provider = new ethers.providers.JsonRpcProvider('https://rpc.api.moonbase.moonbeam.network');
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const xTokens = new ethers.Contract(
    '0x0000000000000000000000000000000000000804', 
    abi, 
    signer
);

// xcUNIT address in Moonbase Alpha
const xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080';

// Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
const ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']];

// Sends 1 xcUNIT to the relay chain using the transfer function
async function transferToAlice() {
    // Creates, signs, and sends the transfer transaction
    const transaction = await xTokens.transfer(
        xcUNIT_ADDRESS,         // Asset
        '1000000000000',        // Amount
        ALICE_RELAY_ACC,        // Destination
        '1000000000'            // Weight
    );

    // Waits for the transaction to be included in a block
    await transaction.wait();
    console.log(transaction);
}

// Multilocation targeting the relay chain or its asset from a parachain
const RELAY_CHAIN_ASSET = [1, []];

// Sends 1 xcUNIT to the relay chain using the transferMultiasset function
async function transferMultiassetToAlice() {
    const transaction = await xTokens.transferMultiasset(
        RELAY_CHAIN_ASSET,      // Asset
        '1000000000000',        // Amount
        ALICE_RELAY_ACC,        // Destination
        '1000000000'            // Weight
    );
    await transaction.wait();
    console.log(transaction);}

transferToAlice();
transferMultiassetToAlice();

// Here are some additional multilocations for the Asset multilocation:
const LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]]; // Note that 0x0424 indicates the x-tokens pallet
const DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]]; // Use if you were targeting DEV from a non-Moonbeam network

// Here are some additional multilocations for the Destination multilocation:
const ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]];
const ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]];
import abi from './xtokensABI.js'; // Import the x-tokens ABI
import Web3 from 'web3'; // Import Web3 library
const PRIVATE_KEY = 'YOUR_PRIVATE_KEY_HERE';

// Create Web3 wallet & contract instance
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network'); // Change to network of choice
const xTokens = new web3.eth.Contract(
  abi,
  '0x0000000000000000000000000000000000000804',
  { from: web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY).address } // 'from' is necessary for gas estimation
);

// xcUNIT address in Moonbase Alpha
const xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080';

// Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
const ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']];

// Sends 1 xcUNIT to the relay chain using the transfer function
async function transferToAlice() {
  // Create transaction
  const transferTx = xTokens.methods.transfer(
    xcUNIT_ADDRESS,         // Asset
    '1000000000000',        // Amount
    ALICE_RELAY_ACC,        // Destination
    '1000000000'            // Weight
  );

  // Sign transaction
  const signedTx = await web3.eth.accounts.signTransaction(
    {
      to: '0x0000000000000000000000000000000000000804',
      data: transferTx.encodeABI(),
      gas: await transferTx.estimateGas()
    },
    PRIVATE_KEY
  );

  // Send signed transaction
  const sendTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log(sendTx);
}

// Multilocation targeting the relay chain or its asset from a parachain
const RELAY_CHAIN_ASSET = [1, []];

// Sends 1 xcUNIT to the relay chain using the transferMultiasset function
async function transferMultiassetToAlice() {
  const transferTx = xTokens.methods.transferMultiasset(
    RELAY_CHAIN_ASSET,      // Asset
    '1000000000000',        // Amount
    ALICE_RELAY_ACC,        // Destination
    '1000000000'            // Weight
  );
  const signedTx = await web3.eth.accounts.signTransaction(
    {
      to: '0x0000000000000000000000000000000000000804',
      data: transferTx.encodeABI(),
      gas: await transferTx.estimateGas()
    },
    PRIVATE_KEY
  );
  const sendTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log(sendTx);
}

transferToAlice();
transferMultiassetToAlice();

// Here are some additional multilocations for the Asset multilocation:
const LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]]; // Note that 0x0424 indicates the x-tokens pallet
const DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]]; // Use if you were targeting DEV from a non-Moonbeam network

// Here are some additional multilocations for the Destination multilocation:
const ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]];
const ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]];
from web3 import Web3

abi = 'XTOKENS_ABI_HERE' # Paste or import the x-tokens ABI
privateKey = 'YOUR_PRIVATE_KEY_HERE' # This is for demo purposes, never store your private key in plain text
address = 'YOUR_ADDRESS_HERE' # The wallet address that corresponds to your private key

# Create Web3 wallet & contract instance
web3 = Web3(Web3.HTTPProvider('https://rpc.api.moonbase.moonbeam.network'))
xTokens = web3.eth.contract(
    address='0x0000000000000000000000000000000000000804',
    abi=abi
)

# xcUNIT address in Moonbase Alpha
xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080'

# Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']]

# Sends 1 xcUNIT to the relay chain using the transfer function
def transferToAlice():
    # Create transaction
    transferTx = xTokens.functions.transfer(
        xcUNIT_ADDRESS,         # Asset
        1000000000000,          # Amount
        ALICE_RELAY_ACC,        # Destination
        1000000000              # Weight
    ).buildTransaction(
        {
            'from': address,
            'nonce': web3.eth.get_transaction_count(address),
        }
    )

    # Sign transaction
    signedTx = web3.eth.account.sign_transaction(transferTx, privateKey)

    # 7. Send tx and wait for receipt
    hash = web3.eth.send_raw_transaction(signedTx.rawTransaction)
    receipt = web3.eth.wait_for_transaction_receipt(hash)
    print(f'Tx successful with hash: { receipt.transactionHash.hex() }')

# Multilocation targeting the relay chain or its asset from a parachain
RELAY_CHAIN_ASSET = [1, []];

# Sends 1 xcUNIT to the relay chain using the transferMultiasset function
def transferMultiassetToAlice():
    transferTx = xTokens.functions.transferMultiasset(
        RELAY_CHAIN_ASSET,      # Asset
        1000000000000,          # Amount
        ALICE_RELAY_ACC,        # Destination
        1000000000              # Weight
    ).buildTransaction(
        {
            'from': address,
            'nonce': web3.eth.get_transaction_count(address),
        }
    )
    signedTx = web3.eth.account.sign_transaction(transferTx, privateKey)
    hash = web3.eth.send_raw_transaction(signedTx.rawTransaction)
    receipt = web3.eth.wait_for_transaction_receipt(hash)
    print(f'Tx successful with hash: { receipt.transactionHash.hex() }')

transferToAlice()
transferMultiassetToAlice()

# Here are some additional multilocations for the Asset multilocation:
LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]] # Note that 0x0424 indicates the x-tokens pallet
DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]] # Use if you were targeting DEV from a non-Moonbeam network

# Here are some additional multilocations for the Destination multilocation:
ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]]
ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]]

注意事项

在Moonbeam 或 Moonriver上测试上述示例时,您可以将RPC URL替换为您自己的私有端点和API密钥。