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).

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.

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 the asset ID
  • currencyId - the ID of the currency 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 - uses the native asset
    • ForeignAsset - uses an external XC-20. It requires you to specify the asset ID of the XC-20
    • LocalAssetReserve - deprecated - use Local XC-20s instead via the Erc20 currency type
    • Erc20 along with the contract address of the local XC-20
  • amount - the number of tokens that are going to be sent via XCM
  • dest - the multilocation of 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 - the weight to be purchased to pay for XCM execution on the destination chain, which is charged from the transferred asset. The weight limit can be defined as either:
    • Unlimited - allows an unlimited amount of weight that can be purchased
    • Limited - limits the amount of weight that can be purchased by defining the following:
      • refTime - the amount of computational time that can be used for execution
      • proofSize - the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';

const currencyId = {
  ForeignAsset: {
    ForeignAsset: INSERT_ASSET_ID,
  },
};
const amount = INSERT_AMOUNT_TO_TRANFER;
const dest = {
  V3: {
    parents: INSERT_PARENTS,
    interior: INSERT_INTERIOR,
  },
};
const destWeightLimit = { Unlimited: null };

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });
  const tx = api.tx.xTokens.transfer(currencyId, amount, dest, destWeightLimit);
  const txHash = await tx.signAndSend('INSERT_ACCOUNT_OR_KEYRING');
};

main();
transferMultiasset(asset, dest, destWeightLimit) — transfer a fungible asset, defined by its multilocation
  • asset - the multilocation of the asset 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
  • dest - the multilocation of 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 - the weight to be purchased to pay for XCM execution on the destination chain, which is charged from the transferred asset. The weight limit can be defined as either:
    • Unlimited - allows an unlimited amount of weight that can be purchased
    • Limited - limits the amount of weight that can be purchased by defining the following:
      • refTime - the amount of computational time that can be used for execution
      • proofSize - the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';

const asset = {
  V3: {
    id: {
      Concrete: {
        parents: INSERT_PARENTS,
        interior: INSERT_INTERIOR,
      },
    },
    fun: {
      Fungible: { Fungible: INSERT_AMOUNT_TO_TRANFER },
    },
  },
};
const dest = {
  V3: {
    parents: INSERT_PARENTS,
    interior: INSERT_INTERIOR,
  },
};
const destWeightLimit = { Unlimited: null };

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });
  const tx = api.tx.xTokens.transferMultiasset(asset, dest, destWeightLimit);
  const txHash = await tx.signAndSend('INSERT_ACCOUNT_OR_KEYRING');
};

main();
transferMultiassetWithFee(asset, fee, dest, destWeightLimit) — transfer a fungible asset, defined by its multilocation, and pay the fee with a different asset, also defined by its multilocation
  • asset - the multilocation of the asset 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 — the multilocation of the asset used to pay for the XCM execution in the target (destination) chain
  • dest - the multilocation of 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 - the weight to be purchased to pay for XCM execution on the destination chain, which is charged from the transferred asset. The weight limit can be defined as either:
    • Unlimited - allows an unlimited amount of weight that can be purchased
    • Limited - limits the amount of weight that can be purchased by defining the following:
      • refTime - the amount of computational time that can be used for execution
      • proofSize - the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';

const asset = {
  V3: {
    id: {
      Concrete: {
        parents: INSERT_PARENTS,
        interior: INSERT_INTERIOR,
      },
    },
    fun: {
      Fungible: { Fungible: INSERT_AMOUNT_TO_TRANFER },
    },
  },
};
const fee = {
  V3: {
    id: {
      Concrete: {
        parents: INSERT_PARENTS,
        interior: INSERT_INTERIOR,
      },
    },
    fun: {
      Fungible: { Fungible: INSERT_AMOUNT_FOR_FEE },
    },
  },
};
const dest = {
  V3: {
    parents: INSERT_PARENTS,
    interior: INSERT_INTERIOR,
  },
};
const destWeightLimit = { Unlimited: null };

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });
  const tx = api.tx.xTokens.transferMultiassetWithFee(
    asset,
    fee,
    dest,
    destWeightLimit
  );
  const txHash = await tx.signAndSend('INSERT_ACCOUNT_OR_KEYRING');
};

main();
transferMultiassets(assets, feeItem, dest, destWeightLimit) — transfer several fungible assets, defined by their multilocation, and pay the fee with one of the assets, also defined by its multilocation
  • assets - the multilocation of the 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
  • 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
  • dest - the multilocation of 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 - the weight to be purchased to pay for XCM execution on the destination chain, which is charged from the transferred asset. The weight limit can be defined as either:
    • Unlimited - allows an unlimited amount of weight that can be purchased
    • Limited - limits the amount of weight that can be purchased by defining the following:
      • refTime - the amount of computational time that can be used for execution
      • proofSize - the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';

const assets = {
  V3: [
    {
      id: {
        Concrete: {
          parents: INSERT_PARENTS,
          interior: INSERT_INTERIOR,
        },
      },
      fun: {
        Fungible: { Fungible: INSERT_AMOUNT_TO_TRANFER },
      },
    },
    // Insert additional assets here
  ],
};
const feeItem = INSERT_ASSET_INDEX_FOR_FEE;
const dest = {
  V3: {
    parents: INSERT_PARENTS,
    interior: INSERT_INTERIOR,
  },
};
const destWeightLimit = { Unlimited: null };

const main = async () => {
  // 3. Create Substrate API provider
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });

  const tx = api.tx.xTokens.transferMultiassets(
    assets,
    feeItem,
    dest,
    destWeightLimit
  );

  const txHash = await tx.signAndSend('INSERT_ACCOUNT_OR_KEYRING');
};

main();
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
  • currencies - the IDs of the 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 - uses the native asset
    • ForeignAsset - uses an external XC-20. It requires you to specify the asset ID of the XC-20
    • LocalAssetReserve - deprecated - use Local XC-20s instead via the Erc20 currency type
    • Erc20 along with the contract address of the local XC-20
  • 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
  • dest - the multilocation of 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 - the weight to be purchased to pay for XCM execution on the destination chain, which is charged from the transferred asset. The weight limit can be defined as either:
    • Unlimited - allows an unlimited amount of weight that can be purchased
    • Limited - limits the amount of weight that can be purchased by defining the following:
      • refTime - the amount of computational time that can be used for execution
      • proofSize - the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';

const currencies = [
  [
    {
      ForeignAsset: {
        ForeignAsset: INSERT_ASSET_ID,
      },
    },
    INSERT_AMOUNT_TO_TRANSFER,
  ],
  // Insert additional currencies
];
const feeItem = INSERT_ASSET_INDEX_FOR_FEE;
const dest = {
  V3: {
    parents: INSERT_PARENTS,
    interior: INSERT_INTERIOR,
  },
};
const destWeightLimit = { Unlimited: null };

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });
  const tx = api.tx.xTokens.transferMulticurrencies(
    currencies,
    feeItem,
    dest,
    destWeightLimit
  );

  const txHash = await tx.signAndSend('INSERT_ACCOUNT_OR_KEYRING');
};

main();
transferWithFee(currencyId, amount, fee, dest, destWeightLimit) — transfer a currency, defined as either the native token (self-reserved) or the asset ID, and specify the fee separately from the amount
  • currencyId - the ID of the currency 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 - uses the native asset
    • ForeignAsset - uses an external XC-20. It requires you to specify the asset ID of the XC-20
    • LocalAssetReserve - deprecated - use Local XC-20s instead via the Erc20 currency type
    • Erc20 along with the contract address of the local XC-20
  • amount - the number of tokens that are going to be sent via XCM
  • fee — the amount to be spent to pay for the XCM execution in the target (destination) chain. If this value is not high enough to cover execution costs, the assets will be trapped in the destination chain
  • dest - the multilocation of 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 - the weight to be purchased to pay for XCM execution on the destination chain, which is charged from the transferred asset. The weight limit can be defined as either:
    • Unlimited - allows an unlimited amount of weight that can be purchased
    • Limited - limits the amount of weight that can be purchased by defining the following:
      • refTime - the amount of computational time that can be used for execution
      • proofSize - the amount of storage in bytes that can be used
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; // Version 9.13.6

const currencyId = {
  ForeignAsset: {
    ForeignAsset: INSERT_ASSET_ID,
  },
};
const amount = INSERT_AMOUNT_TO_TRANSFER;
const fee = INSERT_AMOUNT_FOR_FEE;
const dest = {
  V3: {
    parents: INSERT_PARENTS,
    interior: INSERT_INTERIOR,
  },
};
const destWeightLimit = { Unlimited: null };

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });
  const tx = api.tx.xTokens.transferWithFee(
    currencyId,
    amount,
    fee,
    dest,
    destWeightLimit
  );
  const txHash = await tx.signAndSend('INSERT_ACCOUNT_OR_KEYRING');
};

main();

Storage Methods

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

palletVersion() — returns current pallet version from storage

None

A number representing the current version of the pallet.

// If using Polkadot.js API and calling toJSON() on the unwrapped value
0
import { ApiPromise, WsProvider } from '@polkadot/api';

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });
  const palletVersion = await api.query.xTokens.palletVersion();
};

main();

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, per XCM instruction

The base XCM weight object.

// If using Polkadot.js API and calling toJSON() on the unwrapped value
{ refTime: 200000000, proofSize: 0 }
import { ApiPromise, WsProvider } from '@polkadot/api';

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });

  const baseXcmWeight = api.consts.xTokens.baseXcmWeight;
  console.log(baseXcmWeight.toJSON());
};

main();
selfLocation() - returns the multilocation of the chain

The self-location multilocation object.

// If using Polkadot.js API and calling toJSON() on the unwrapped value
{ parents: 0, interior: { here: null } }
import { ApiPromise, WsProvider } from '@polkadot/api';

const main = async () => {
  const api = await ApiPromise.create({
    provider: new WsProvider('INSERT_WSS_ENDPOINT'),
  });

  const selfLocation = api.consts.xTokens.selfLocation;
  console.log(selfLocation.toJSON());
};

main();

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 in the pallet, especially once you become familiar with multilocations.

Note

Each parachain can allow and forbid specific methods from a pallet. Consequently, developers must ensure that they use methods that are allowed, or 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

    Note

    You can adapt this guide to transfer another external XC-20 or a local XC-20. For external XC-20s, you'll need the asset ID and the number of decimals the asset has. For local XC-20s, you'll need the contract address.

    Moonbeam Swap xcUNIT

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

0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080

Determining Weight Needed for XCM Execution

To determine the weight needed for XCM execution on the destination chain, you'll need to know which XCM instructions are executed on the destination chain. You can find an overview of the XCM instructions used in the XCM Instructions for Transfers via X-Tokens guide.

In this example, where you're transferring xcUNIT from Moonbase Alpha to the Alphanet relay chain, the instructions that are executed on Alphanet are:

Instruction Weight
WithdrawAsset 145,308,000
ClearOrigin 5,725,000
BuyExecution 5,751,000
DepositAsset 147,433,000
TOTAL 304,217,000

Note

Some weights include database reads and writes; for example, the WithdrawAsset and DepositAsset instructions include both one database read and one write. To get the total weight, you'll need to add the weight of any required database reads or writes to the base weight of the given instruction.

For Westend-based relay chains, like Alphanet, you can get the weight cost for read and write database operations for Rocks DB (which is the default database) in the polkadot-sdk repository on GitHub.

Since Alphanet is a Westend-based relay chain, you can refer to the instruction weights defined in the Westend runtime code, which are broken up into two types of instructions: fungible and generic.

It's important to note that each chain defines its own weight requirements. To determine the weight required for each XCM instruction on a given chain, please refer to the chain's documentation or reach out to a member of their team. To learn how to find the weights required by Moonbeam, Polkadot, or Kusama, you can refer to our documentation on Weights and Fees.

X-Tokens Transfer Function

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

Since you'll be interacting with the transfer function, 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 specifying a network parameter. If you don't specify one, it will default to None.

  4. Set the destWeightLimit. Since the weight required to execute XCM messages varies for each chain, you can set the weight limit to be Unlimited, or if you have an estimate of the weight needed, you can use Limited, but please note that if set below the requirements, the execution may fail

    const destWeightLimit = { Unlimited: null };
    
    const destWeightLimit = {
      Limited: {
        refTime: 'INSERT_ALLOWED_AMOUNT',
        proofSize: 'INSERT_ALLOWED_AMOUNT',
      },
    };
    

    As outlined in the Determining Weight Needed for XCM Execution section, you'll need 304,217,000 weight units for the XCM execution on Alphanet. You can set the refTime to {{ no such element: str object['numbers_only'] }}. The proofSize can be 0, as the Alphanet relay chain does not currently account for proofSize.

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 the Alphanet relay chain using the transferMultiasset function of the X-Tokens Pallet.

Since you'll be interacting with the transferMultiasset function, 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 specifying a network parameter. If you don't specify one, it will default to None.

  3. Set the destWeightLimit. Since the weight required to execute XCM messages varies for each chain, you can set the weight limit to be Unlimited, or if you have an estimate of the weight needed, you can use Limited, but please note that if set below the requirements, the execution may fail

    const destWeightLimit = { Unlimited: null };
    
    const destWeightLimit = {
      Limited: {
        refTime: 'INSERT_ALLOWED_AMOUNT',
        proofSize: 'INSERT_ALLOWED_AMOUNT',
      },
    };
    

    As outlined in the Determining Weight Needed for XCM Execution section, you'll need 304,217,000 weight units for the XCM execution on Alphanet. You can set the refTime to {{ no such element: str object['numbers_only'] }}. The proofSize can be 0, as the Alphanet relay chain does not currently account for proofSize.

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 are 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 of a multilocation with the gas limit set to 300000:

// Multilocation for a local XC-20 on Moonbeam
const asset = {
  V3: {
    id: {
      Concrete: {
        parents: 0,
        interior: {
          X3: [
            { PalletInstance: 48 },
            { AccountKey20: { key: 'INSERT_ERC_20_ADDRESS' } },
            { 
              GeneralKey: {
                // gas_limit: 300000
                data: '0x6761735f6c696d69743ae0930400000000000000000000000000000000000000',
                length: 32,
              },
            },
          ],
        },
      },
    },
    fun: {
      Fungible: { Fungible: 1000000000000000000n }, // 1 token
    },
  },
};
Last update: January 25, 2024
| Created: October 28, 2023