Skip to content

Using the X-Tokens Pallet To Send XC-20s

Introduction

Building an XCM message for fungible asset transfers is not an easy task. Consequently, there are wrapper functions and pallets that developers can leverage to use XCM features on Polkadot and Kusama.

One example of such wrappers is the X-Tokens Pallet, which provides different methods to transfer fungible assets via XCM.

This guide will show you how to leverage the X-Tokens Pallet to send XC-20s from a Moonbeam-based network to other chains in the ecosystem (relay chain/parachains). Moreover, you'll also learn how to use the X-Tokens Precompile to perform the same actions via the Ethereum API.

Developers must understand that sending incorrect XCM messages can result in the loss of funds. Consequently, it is essential to test XCM features on a TestNet before moving to a production environment.

Relevant XCM Definitions

This guide assumes you have a basic understanding of XCM. If not, please take time to review the XCM Overview page.

For this guide specifically, you'll need to have an understanding of the following definitions:

  • Sovereign account — an account each chain in the ecosystem has, one for the relay chain and the other for other parachains. It is calculated as the blake2 hash of a specific word and parachain ID concatenated (blake2(para+ParachainID) for the Sovereign account in the relay chain, and blake2(sibl+ParachainID) for the Sovereign account in other parachains), truncating the hash to the correct length. The account is owned by root and can only be used through SUDO (if available) or governance (referenda). The Sovereign account typically signs XCM messages in other chains in the ecosystem
  • Multilocation — a way to specify a point in the entire relay chain/parachain ecosystem relative to a given origin. For example, it can be used to specify a specific parachain, asset, account, or even a pallet inside a parachain. In general terms, a multilocation is defined with a parents and an interior:

    • parents - refers to how many "hops" into a parent blockchain you need to take from a given origin
    • interior - refers to how many fields you need to define the target point.

    For example, to target a parachain with ID 1000 from another parachain, the multilocation would be { "parents": 1, "interior": { "X1": [{ "Parachain": 1000 }]}}

X-Tokens Pallet Interface

Extrinsics

The X-Tokens Pallet provides the following extrinsics (functions):

  • transfer(currencyId, amount, dest, destWeightLimit) — transfer a currency, defined as either the native token (self reserved), or with the asset ID
  • transferMultiasset(asset, dest, destWeightLimit) — transfer a fungible asset, defined by its multilocation
  • transferMultiassetWithFee(asset, fee, dest, destWeightLimit) — transfer a fungible asset, but it allows the sender to pay the fee with a different asset. Both are defined by their multilocation
  • transferMultiassets(assets, feeItem, dest, destWeightLimit) — transfer several fungible assets, specifying which is used as the fee. Each asset is defined by its multilocation
  • transferMulticurrencies(currencies, feeItem, dest, destWeightLimit) — transfer different currencies, specifying which is used as the fee. Each currency is defined as either the native token (self reserved) or with the asset ID
  • transferWithFee(currencyId, amount, fee, dest, destWeightLimit) — transfer a currency, but it allows the sender to pay the fee with a different asset. Both are defined by their multilocation

Where the inputs that need to be provided can be defined as:

  • currencyId/currencies — the ID/IDs of the currency/currencies being sent via XCM. Different runtimes have different ways to define the IDs. In the case of Moonbeam-based networks, a currency can be defined as one of the following:

    • SelfReserve - refers to the native token
    • ForeignAsset - refers to the asset ID of an External XC-20 (not to be confused with the XC-20 address)
    • LocalAssetReserve - refers to the asset ID of a Mintable XC-20 (not to be confused with the XC-20 address). It is recommended to use Local XC-20s instead via the Erc20 currency type
    • Erc20 - refers to the contract address of a Local XC-20 (ERC-20)
  • amount — the number of tokens that are going to be sent via XCM

  • dest — a multilocation to define the destination address for the tokens being sent via XCM. It supports different address formats, such as 20 or 32-byte addresses (Ethereum or Substrate)
  • destWeightLimit — an enum that represents the maximum amount of execution time you want to provide in the destination chain to execute the XCM message being sent. The enum contains the following options:

    • Unlimited - allows for the entire amount used for gas to be used to pay for weight
    • Limited - limits the amount used for gas to a particular value

    If not enough weight is provided, the execution of the XCM will fail, and funds might get locked in either the Sovereign account or a special pallet. It is important to correctly set the destination weight to avoid failed XCM executions

  • asset/assets — a multilocation to define the asset/assets being sent via XCM. Each parachain has a different way to reference assets. For example, Moonbeam-based networks reference their native tokens with the Balances Pallet index

  • fee — a multilocation to define the asset used to pay for the XCM execution in the target chain
  • feeItem — an index to define the asset position of an array of assets being sent, used to pay for the XCM execution in the target chain. For example, if only one asset is being sent, the feeItem would be 0

Storage Methods

The X-Tokens Pallet includes the following read-only storage method:

  • palletVersion() - provides the version of the X-Tokens Pallet being used

Pallet Constants

The X-Tokens Pallet includes the following read-only functions to obtain pallet constants:

  • baseXcmWeight() - returns the base XCM weight required for execution
  • selfLocation() - returns the multilocation of the chain

XCM Instructions for Transfers via X-Tokens

The XCM instructions used for the X-Tokens Pallet extrinsics are defined in the X-Tokens Open Runtime Module Library repository.

Regardless of which transfer extrinsic is used, the instructions are the same for sending the native asset back to its origin chain, such as xcDOT from Moonbeam back to Polkadot, and sending the native asset from the origin chain to a destination chain, such as DOT from Polkadot to Moonbeam.

For example, to move DOT to Moonbeam, the following XCM instructions are used:

  1. TransferReserveAsset - gets executed in Polkadot. Moves assets from the origin account and deposits them into a destination account. In this case, the destination account is Moonbeam's sovereign account on Polkadot. It then sends an XCM message to the destination, which is Moonbeam, with the XCM instructions that are to be executed
  2. ReserveAssetDeposited - gets executed in Moonbeam. Takes a representation of the assets received in the sovereign account and places them into the holding register, a temporary position in the Cross-Consensus Virtual Machine (XCVM)
  3. ClearOrigin - gets executed in Moonbeam. Ensures that later XCM instructions cannot command the authority of the XCM author
  4. BuyExecution - gets executed in Moonbeam. Takes the assets from holding to pay for execution fees. The fees to pay are determined by the target chain, which in this case is Moonbeam
  5. DepositAsset - gets executed in Moonbeam. Removes the assets from holding and sends them to a destination account on Moonbeam

To check how the instructions for an XCM message are built to transfer self-reserve assets to a target chain, such as DOT to Moonbeam, you can refer to the X-Tokens Open Runtime Module Library repository (as an example). You'll want to take a look at the transfer_self_reserve_asset function. You'll notice it calls TransferReserveAsset and passes in assets, dest, and xcm as parameters. In particular, the xcm parameter includes the BuyExecution and DepositAsset instructions. If you then head over to the Polkadot GitHub repository, you can find the TransferReserveAsset instruction. The XCM message is constructed by combining the ReserveAssetDeposited and ClearOrigin instructions with the xcm parameter, which as mentioned includes the BuyExecution and DepositAsset instructions.

To move xcDOT from Moonbeam back to Polkadot, the instructions that are used are:

  1. WithdrawAsset - gets executed in Moonbeam. Removes assets and places them into the holding register
  2. InitiateReserveWithdraw - gets executed in Moonbeam. Removes the assets from holding (essentially burning them) and sends an XCM message to the destination chain starting with the WithdrawAsset instruction
  3. WithdrawAsset - gets executed in Polkadot. Removes assets and places them into the holding register
  4. ClearOrigin - gets executed in Polkadot. Ensures that later XCM instructions cannot command the authority of the XCM author
  5. BuyExecution - gets executed in Polkadot. Takes the assets from holding to pay for execution fees. The fees to pay are determined by the target chain, which in this case is Polkadot
  6. DepositAsset - gets executed in Polkadot. Removes the assets from holding and sends them to a destination account on Polkadot

To check how the instructions for an XCM message are built to transfer reserve assets to a target chain, such as xcDOT to Polkadot, you can refer to the X-Tokens Open Runtime Module Library repository. You'll want to take a look at the transfer_to_reserve function. You'll notice that it calls WithdrawAsset, then InitiateReserveWithdraw and passes in assets, dest, and xcm as parameters. In particular, the xcm parameter includes the BuyExecution and DepositAsset instructions. If you then head over to the Polkadot GitHub repository, you can find the InitiateReserveWithdraw instruction. The XCM message is constructed by combining the WithdrawAsset and ClearOrigin instructions with the xcm parameter, which as mentioned includes the BuyExecution and DepositAsset instructions.

Building an XCM Message with the X-Tokens Pallet

This guide covers the process of building an XCM message using the X-Tokens Pallet, more specifically, with the transfer and transferMultiasset functions. Nevertheless, these two cases can be extrapolated to the other functions, especially once you become familiar with multilocations.

Note

Each parachain can allow/forbid specific methods from a pallet. Consequently, developers must ensure that they use methods that are allowed. On the contrary, the transaction will fail with an error similar to system.CallFiltered.

You'll be transferring xcUNIT tokens, which are the XC-20 representation of the Alphanet relay chain token, UNIT. You can adapt this guide for any other XC-20.

Checking Prerequisites

To follow along with the examples in this guide, you need to have the following:

  • An account with funds. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
  • Some xcUNIT tokens. You can swap DEV tokens (Moonbase Alpha's native token) for xcUNITs on Moonbeam-Swap, a demo Uniswap-V2 clone on Moonbase Alpha

    Moonbeam Swap xcUNIT

To check your xcUNIT balance, you can add the XC-20 to MetaMask with the following address:

0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080

Note

If you're interested in how the precompile address is calculated, you can check out the Calculate External XC-20 Precompile Addresses guide.

You can adapt this guide for another external XC-20 or a local XC-20. If you're adapting this guide for another external XC-20, you'll need to have the asset ID of the asset you're transferring and the number of decimals the asset has, which you can get by following the Retrieve List of External XC-20s guide. If you're adapting this guide for a local XC-20, you'll need to have the contract address of the XC-20.

X-Tokens Transfer Function

In this example, you'll build an XCM message to transfer xcUNIT from Moonbase Alpha back to its relay chain through the transfer function of the X-Tokens Pallet. To do this, you can use the Polkadot.js API.

Since you'll be interacting with the transfer function of the X-Tokens Pallet, you can take the following steps to gather the arguments for the currencyId, amount, dest, and destWeightLimit:

  1. Define the currencyId. For external XC-20s, you'll use the ForeignAsset currency type and the asset ID of the asset, which in this case is 42259045809535163221576417993425387648. For a local XC-20, you'll need the address of the token. In JavaScript, this translates to:

    const currencyId = { 
      ForeignAsset: { 
        ForeignAsset: 42259045809535163221576417993425387648n 
      } 
    };
    
    const currencyId = { Erc20: { contractAddress: 'INSERT_ERC_20_ADDRESS' } };
    
  2. Specify the amount to transfer. For this example, you are sending 1 xcUNIT, which has 12 decimals:

    const amount = 1000000000000n;
    
  3. Define the multilocation of the destination, which will target an account on the relay chain from Moonbase Alpha. Note that the only asset that the relay chain can receive is its own:

    const dest = { 
      V3: { 
        parents: 1, 
        interior: { X1: { AccountId32: { id: relayAccount } } } 
      } 
    };
    

    Note

    For an AccountId32, AccountIndex64, or AccountKey20, you have the option of specify a network parameter. If you don't specify one, it will default to None.

  4. Set the destWeightLimit to Unlimited. In JavaScript, you'll need to set Unlimited to null (as outlined in the TypeScript interface for XcmV3WeightLimit):

    const destWeightLimit = { Unlimited: null };
    

    Note

    If you wanted to limit the destination weight, you could do so by using Limited, which requires you to enter values for refTime and proofSize. Where refTime is the amount of computational time that can be used for execution and proofSize is the amount of storage in bytes that can be used.

    In JavaScript, this translates to:

    { Limited: { refTime: 'INSERT_ALLOWED_AMOUNT', proofSize: 'INSERT_ALLOWED_AMOUNT' } };
    

Now that you have the values for each of the parameters, you can write the script for the transfer. You'll take the following steps:

  1. Provide the input data for the call. This includes:
    • The Moonbase Alpha endpoint URL to create the provider
    • The values for each of the parameters of the transfer function
  2. Create a Keyring instance that will be used to send the transaction
  3. Create the Polkadot.js API provider
  4. Craft the xTokens.transfer extrinsic with the currencyId, amount, dest, and destWeightLimit
  5. Send the transaction using the signAndSend extrinsic and the Keyring instance you created in the second step

Remember

This is for demo purposes only. Never store your private key in a JavaScript file.

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 relayAccount =
  '0xc4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a063'; // Alice's relay account address
const currencyId = { 
  ForeignAsset: { 
    ForeignAsset: 42259045809535163221576417993425387648n 
  }
};
const amount = 1000000000000n;
const dest = {
  V3: {
    parents: 1,
    interior: { X1: { AccountId32: { id: relayAccount } } },
  },
};
const destWeightLimit = { Unlimited: null };

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

const sendXc20 = 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.xTokens.transfer(currencyId, amount, dest, destWeightLimit);

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

  api.disconnect();
};

sendXc20();

Note

You can view an example of the above script, which sends 1 xcUNIT to Alice's account on the relay chain, on Polkadot.js Apps using the following encoded calldata: 0x1e00018080778c30c20fa2ebc0ed18d2cbca1f0010a5d4e800000000000000000000000301010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300.

Once the transaction is processed, the target account on the relay chain should have received the transferred amount minus a small fee that is deducted to execute the XCM on the destination chain.

X-Tokens Transfer Multiasset Function

In this example, you'll build an XCM message to transfer xcUNIT from Moonbase Alpha back to its relay chain using the transferMultiasset function of the X-Tokens Pallet.

Since you'll be interacting with the transferMultiasset function of the X-Tokens Pallet, you can take the following steps to gather the arguments for the asset, dest, and destWeightLimit:

  1. Define the XCM asset multilocation of the asset, which will target UNIT tokens in the relay chain from Moonbase Alpha as the origin. Each chain sees its own asset differently. Therefore, you will have to set a different asset multilocation for each destination

    // Multilocation for UNIT in the relay chain
    const asset = {
      V3: {
        id: {
          Concrete: {
            parents: 1,
            interior: null,
          },
        },
        fun: {
          Fungible: { Fungible: 1000000000000n }, // 1 token
        },
      },
    };
    
    // Multilocation for a local XC-20 on Moonbeam
    const asset = {
      V3: {
        id: {
          Concrete: {
            parents: 0,
            interior: {
              X2: [
                { PalletInstance: 48 },
                { AccountKey20: { key: 'INSERT_ERC_20_ADDRESS' } },
              ],
            },
          },
        },
        fun: {
          Fungible: { Fungible: 1000000000000000000n }, // 1 token
        },
      },
    };
    

    For information on the default gas limit for local XC-20 transfers and how to override the default, please refer to the following section: Override Local XC-20 Gas Limits.

  2. Define the XCM destination multilocation of the dest, which will target an account in the relay chain from Moonbase Alpha as the origin:

    const dest = {
      V3: {
        parents: 1,
        interior: { X1: { AccountId32: { id: relayAccount } } },
      },
    };
    

    Note

    For an AccountId32, AccountIndex64, or AccountKey20, you have the option of specify a network parameter. If you don't specify one, it will default to None.

  3. Set the destination weight limit to Unlimited. In JavaScript, you'll need to set Unlimited to null (as outlined in the TypeScript interface for XcmV3WeightLimit):

    const destWeightLimit = { Unlimited: null };
    

    Note

    If you wanted to limit the destination weight, you could do so by using Limited, which requires you to enter values for refTime and proofSize. Where refTime is the amount of computational time that can be used for execution and proofSize is the amount of storage in bytes that can be used.

    In JavaScript, this translates to:

    { Limited: { refTime: 'INSERT_ALLOWED_AMOUNT', proofSize: 'INSERT_ALLOWED_AMOUNT' } };
    

Now that you have the values for each of the parameters, you can write the script for the transfer. You'll take the following steps:

  1. Provide the input data for the call. This includes:
    • The Moonbase Alpha endpoint URL to create the provider
    • The values for each of the parameters of the transferMultiasset function
  2. Create a Keyring instance that will be used to send the transaction
  3. Create the Polkadot.js API provider
  4. Craft the xTokens.transferMultiasset extrinsic with the asset, dest, and destWeightLimit
  5. Send the transaction using the signAndSend extrinsic and the Keyring instance you created in the second step

Remember

This is for demo purposes only. Never store your private key in a JavaScript file.

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 relayAccount =
  '0xc4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a063'; // Alice's relay account address
const asset = {
  V3: {
    id: {
      Concrete: {
        parents: 1,
        interior: null,
      },
    },
    fun: {
      Fungible: { Fungible: 1000000000000n },
    },
  },
};
const dest = {
  V3: {
    parents: 1,
    interior: { X1: { AccountId32: { id: relayAccount } } },
  },
};
const destWeightLimit = { Unlimited: null };

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

const sendXc20 = 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.xTokens.transferMultiasset(asset, dest, destWeightLimit);

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

  api.disconnect();
};

sendXc20();

Note

You can view an example of the above script, which sends 1 xcUNIT to Alice's account on the relay chain, on Polkadot.js Apps using the following encoded calldata: 0x1e010300010000070010a5d4e80301010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300

Once the transaction is processed, the account on the relay chain should have received the transferred amount minus a small fee that is deducted to execute the XCM on the destination chain.

Override Local XC-20 Gas Limits

If you are transferring a local XC-20, the default units of gas is as follows for each network:

200,000
200,000
200,000

You can override the default gas limit using an additional junction when you create the multilocation for the local XC-20. To do so, you'll need to use the GeneralKey junction, which accepts two arguments: data and length.

For example, to set the gas limit to 300000, you'll need to set the length to 32, and for the data, you'll need to pass in gas_limit: 300000. However, you can't simply pass in the value for data in text; you'll need to properly format it to a 32-byte zero-padded hex string, where the value for the gas limit is in little-endian format. To properly format the data, you can take the following steps:

  1. Convert gas_limit: to its byte representation
  2. Convert the value for the gas limit into its little-endian byte representation
  3. Concatenate the two byte representations into a single value padded to 32 bytes
  4. Convert the bytes to a hex string

Using the @polkadot/util library, these steps are as follows:

import { numberToU8a, stringToU8a, u8aToHex } from '@polkadot/util';

// 1. Convert `gas_limit:` to bytes
const gasLimitString = 'gas_limit:';
const u8aGasLimit = stringToU8a(gasLimitString);

// 2. Convert the gas value to little-endian formatted bytes
const gasLimitValue = 300000;
const u8aGasLimitValue = numberToU8a(gasLimitValue);
const littleEndianValue = u8aGasLimitValue.reverse();

// 3. Combine and zero pad the gas limit string and the gas limit 
// value to 32 bytes
const u8aCombinedGasLimit = new Uint8Array(32);
u8aCombinedGasLimit.set(u8aGasLimit, 0);
u8aCombinedGasLimit.set(littleEndianValue, u8aGasLimit.length);

// 4. Convert the bytes to a hex string
const data = u8aToHex(u8aCombinedGasLimit);
console.log(`The GeneralKey data is: ${data}`);

The following is an example multilocation with the gas limit set to 300000:

// Multilocation for a local XC-20 on Moonbeam
const asset = {
  V3: {
    id: {
      Concrete: {
        parents: 0,
        interior: {
          X2: [
            { PalletInstance: 48 },
            { AccountKey20: { key: 'INSERT_ERC_20_ADDRESS' } },
            { 
              GeneralKey: {
                // gas_limit: 300000
                data: '0x6761735f6c696d69743ae0930400000000000000000000000000000000000000',
                length: 32,
              },
            },
          ],
        },
      },
    },
    fun: {
      Fungible: { Fungible: 1000000000000000000n }, // 1 token
    },
  },
};

X-Tokens Precompile

The X-Tokens Precompile contract allows developers to access XCM token transfer features through the Ethereum API of Moonbeam-based networks. As with other precompile contracts, the X-Tokens Precompile is located at the following addresses:

0x0000000000000000000000000000000000000804
0x0000000000000000000000000000000000000804
0x0000000000000000000000000000000000000804

Note

There can be some unintended consequences when using the precompiled contracts on Moonbeam. Please refer to the Security Considerations page for more information.

The X-Tokens Solidity Interface

Xtokens.sol is an interface through which developers can interact with the X-Tokens Pallet using the Ethereum API.

The interface includes the following functions:

  • transfer(address currencyAddress, uint256 amount, Multilocation memory destination, uint64 weight) — function that represents the transfer method described in the previous example. Instead of using the currency ID, you'll need to provide the asset's address for the currencyAddress:

    The destination multilocation is built in a particular way that is described in the following section

  • transferMultiasset(Multilocation memory asset, uint256 amount, Multilocation memory destination, uint64 weight) — function that represents the transferMultiasset method described in the previous example. Both multilocations are built in a particular way that is described in the following section

Building the Precompile Multilocation

In the X-Tokens Precompile interface, the Multilocation structure is defined as follows:

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

Note that each multilocation has a parents element, defined in this case by a uint8, and an array of bytes. Parents refer to how many "hops" in the upwards direction you have to do if you are going through the relay chain. Being a uint8, the normal values you would see are:

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

The bytes array (bytes[]) defines the interior and its content within the multilocation. The size of the array defines the interior value as follows:

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

Note

Interior value Here is often used for the relay chain (either as a destination or to target the relay chain asset).

Suppose the bytes array contains data. Each element's first byte (2 hexadecimal numbers) corresponds to the selector of that XN field. For example:

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[]

Next, depending on the selector and its data type, the following bytes correspond to the actual data being provided. Note that for AccountId32, AccountIndex64, and AccountKey20, the network field seen in the Polkadot.js Apps example is appended at the end. For example:

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

Note

The interior data usually needs to be wrapped around quotes. On the contrary, you might get an invalid tuple value error.

The following code snippet goes through some examples of Multilocation structures, as they would need to be fed into the X-Tokens Precompile functions:

// 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 = 1
    // Size of array is 1, meaning is an X1 interior
    [
        '0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300' 
        // AccountKey32 Selector + Address in hex + Network(Option) Null
    ]
}

Using Libraries to Interact with X-Tokens

The Multilocation structs can be formatted like any other struct when using libraries to interact with the Ethereum API. The following code snippet include the previous X-Tokens transfer function, the X-Tokens multiasset transfer function, and sample Multilocation struct examples. You can find the X-Tokens ABI on Github.

import ABI from './xtokensABI.js'; // Import the X-Tokens ABI
import { ethers } from 'ethers'; // Import Ethers library

const privateKey = 'INSERT_PRIVATE_KEY';

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

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

// Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
const aliceRelayAccount = [
  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(
    xcUnitAddress,       // Asset
    '1000000000000',     // Amount
    aliceRelayAccount,   // 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 relayChainAsset = [1, []];

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

transferToAlice();
transferMultiassetToAlice();

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

// Here are some additional multilocations for the Destination multilocation:
const address32OfParachain = [
  1,
  [
    '0x00000007EF',
    '0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300',
  ],
];
const address20FromParachainToMoonbase = [
  1,
  ['0x00000003E8', '0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00'],
];
import ABI from './xtokensABI.js'; // Import the X-Tokens ABI
import Web3 from 'web3'; // Import Web3 library

const privateKey = 'INSERT_PRIVATE_KEY';

// 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(privateKey).address } // 'from' is necessary for gas estimation
);

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

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

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

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

  // 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
    aliceRelayAccount,   // Destination
    '1000000000'         // Weight
  );
  const signedTx = await web3.eth.accounts.signTransaction(
    {
      to: '0x0000000000000000000000000000000000000804',
      data: transferTx.encodeABI(),
      gas: await transferTx.estimateGas(),
    },
    privateKey
  );
  const sendTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
  console.log(sendTx);
}

transferToAlice();
transferMultiassetToAlice();

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

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

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

# Create Web3 wallet & contract instance
web3 = Web3(Web3.HTTPProvider('https://rpc.api.moonbase.moonbeam.network'))
x_tokens = 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_account = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']]

# Sends 1 xcUNIT to the relay chain using the transfer function
def transfer_to_alice():
    # Create transaction
    transferTx = x_tokens.functions.transfer(
        xcUnit_address,         # Asset
        1000000000000,          # Amount
        alice_relay_account,    # Destination
        1000000000              # Weight
    ).build_transaction(
        {
            'from': address,
            'nonce': web3.eth.get_transaction_count(address),
        }
    )

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

    # 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 transfer_multiasset_to_alice():
    transferTx = x_tokens.functions.transferMultiasset(
        relay_chain_asset,      # Asset
        1000000000000,          # Amount
        alice_relay_account,    # Destination
        1000000000              # Weight
    ).build_transaction(
        {
            'from': address,
            'nonce': web3.eth.get_transaction_count(address),
        }
    )
    signedTx = web3.eth.account.sign_transaction(transferTx, private_key)
    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() }')

transfer_to_alice()
transfer_multiasset_to_alice()

# 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']]

Note

To test out the above examples on Moonbeam or Moonriver, you can replace the RPC URL with your own endpoint and API key, which you can get from one of the supported Endpoint Providers.

Last update: September 29, 2023
| Created: March 28, 2022