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 assetForeignAsset
- uses an external XC-20. It requires you to specify the asset ID of the XC-20LocalAssetReserve
- deprecated - use Local XC-20s instead via theErc20
currency typeErc20
along with the contract address of the local XC-20
amount
- the number of tokens that are going to be sent via XCMdest
- 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 purchasedLimited
- limits the amount of weight that can be purchased by defining the following:refTime
- the amount of computational time that can be used for executionproofSize
- 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 = {
V4: {
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 indexdest
- 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 purchasedLimited
- limits the amount of weight that can be purchased by defining the following:refTime
- the amount of computational time that can be used for executionproofSize
- the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';
const asset = {
V4: {
id: {
parents: INSERT_PARENTS,
interior: INSERT_INTERIOR,
},
fun: {
Fungible: { Fungible: INSERT_AMOUNT_TO_TRANFER },
},
},
};
const dest = {
V4: {
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 indexfee
— the multilocation of the asset used to pay for the XCM execution in the target (destination) chaindest
- 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 purchasedLimited
- limits the amount of weight that can be purchased by defining the following:refTime
- the amount of computational time that can be used for executionproofSize
- the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';
const asset = {
V4: {
id: {
parents: INSERT_PARENTS,
interior: INSERT_INTERIOR,
},
fun: {
Fungible: { Fungible: INSERT_AMOUNT_TO_TRANFER },
},
},
};
const fee = {
V4: {
id: {
parents: INSERT_PARENTS,
interior: INSERT_INTERIOR,
},
fun: {
Fungible: { Fungible: INSERT_AMOUNT_FOR_FEE },
},
},
};
const dest = {
V4: {
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 indexfeeItem
— 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, thefeeItem
would be0
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 purchasedLimited
- limits the amount of weight that can be purchased by defining the following:refTime
- the amount of computational time that can be used for executionproofSize
- the amount of storage in bytes that can be used
import { ApiPromise, WsProvider } from '@polkadot/api';
const assets = {
V4: [
{
id: {
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 = {
V4: {
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 assetForeignAsset
- uses an external XC-20. It requires you to specify the asset ID of the XC-20LocalAssetReserve
- deprecated - use Local XC-20s instead via theErc20
currency typeErc20
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, thefeeItem
would be0
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 purchasedLimited
- limits the amount of weight that can be purchased by defining the following:refTime
- the amount of computational time that can be used for executionproofSize
- 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 = {
V4: {
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 assetForeignAsset
- uses an external XC-20. It requires you to specify the asset ID of the XC-20LocalAssetReserve
- deprecated - use Local XC-20s instead via theErc20
currency typeErc20
along with the contract address of the local XC-20
amount
- the number of tokens that are going to be sent via XCMfee
— 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 chaindest
- 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 purchasedLimited
- limits the amount of weight that can be purchased by defining the following:refTime
- the amount of computational time that can be used for executionproofSize
- the amount of storage in bytes that can be used
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'; // Version 10.13.1
const currencyId = {
ForeignAsset: {
ForeignAsset: INSERT_ASSET_ID,
},
};
const amount = INSERT_AMOUNT_TO_TRANSFER;
const fee = INSERT_AMOUNT_FOR_FEE;
const dest = {
V4: {
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.
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 | Ref Time | Proof Size |
---|---|---|
WithdrawAsset | 3,875,000,000 | 2,480,000 |
ClearOrigin | 5,194,000 | 0 |
BuyExecution | 348,048,000 | 19,056 |
DepositAsset | 3,875,000,000 | 2,480,000 |
TOTAL | 8,103,242,000 | 4,979,056 |
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
:
-
Define the
currencyId
. For external XC-20s, you'll use theForeignAsset
currency type and the asset ID of the asset, which in this case is42259045809535163221576417993425387648
. 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' } };
-
Specify the
amount
to transfer. For this example, you are sending 1 xcUNIT, which has 12 decimals:const amount = 1000000000000n;
-
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 = { V4: { parents: 1, interior: { X1: [{ AccountId32: { id: relayAccount } }] }, }, };
Note
For an
AccountId32
,AccountIndex64
, orAccountKey20
, you have the option of specifying anetwork
parameter. If you don't specify one, it will default toNone
. -
Set the
destWeightLimit
. Since the weight required to execute XCM messages varies for each chain, you can set the weight limit to beUnlimited
, or if you have an estimate of the weight needed, you can useLimited
, but please note that if set below the requirements, the execution may failconst 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 305,986,000 weight units for the XCM execution on Alphanet. You can set the
refTime
to{{ no such element: str object['numbers_only'] }}
. TheproofSize
can be0
, as the Alphanet relay chain does not currently account forproofSize
.
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:
- 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
- Create a Keyring instance that will be used to send the transaction
- Create the Polkadot.js API provider
- Craft the
xTokens.transfer
extrinsic with thecurrencyId
,amount
,dest
, anddestWeightLimit
- 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 10.13.1
import { cryptoWaitReady } from '@polkadot/util-crypto';
// 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 = {
V4: {
parents: 1,
interior: { X1: [{ AccountId32: { id: relayAccount } }] },
},
};
const destWeightLimit = { Unlimited: null };
const sendXc20 = async () => {
// 2. Create Keyring instance
await cryptoWaitReady();
const keyring = new Keyring({ type: 'ethereum' });
const alice = keyring.addFromUri(privateKey);
// 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: 0x1e00018080778c30c20fa2ebc0ed18d2cbca1f0010a5d4e8000000000000000000000004010101000c36e9ba26fa63c60ec728fe75fe57b86a450d94e7fee7f9f9eddd0d3f400d6700
.
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
:
-
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 = { V4: { id: { parents: 1, interior: null, }, fun: { Fungible: { Fungible: 1000000000000n }, }, }, };
// Multilocation for a local XC-20 on Moonbeam const asset = { V4: { id: { 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.
-
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 = { V4: { parents: 1, interior: { X1: [{ AccountId32: { id: relayAccount } }] }, }, };
Note
For an
AccountId32
,AccountIndex64
, orAccountKey20
, you have the option of specifying anetwork
parameter. If you don't specify one, it will default toNone
. -
Set the
destWeightLimit
. Since the weight required to execute XCM messages varies for each chain, you can set the weight limit to beUnlimited
, or if you have an estimate of the weight needed, you can useLimited
, but please note that if set below the requirements, the execution may failconst 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 305,986,000 weight units for the XCM execution on Alphanet. You can set the
refTime
to{{ no such element: str object['numbers_only'] }}
. TheproofSize
can be0
, as the Alphanet relay chain does not currently account forproofSize
.
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:
- 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
- Create a Keyring instance that will be used to send the transaction
- Create the Polkadot.js API provider
- Craft the
xTokens.transferMultiasset
extrinsic with theasset
,dest
, anddestWeightLimit
- 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 10.13.1
import { cryptoWaitReady } from '@polkadot/util-crypto';
// 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 = {
V4: {
id: {
parents: 1,
interior: null,
},
fun: {
Fungible: { Fungible: 1000000000000n },
},
},
};
const dest = {
V4: {
parents: 1,
interior: { X1: [{ AccountId32: { id: relayAccount } }] },
},
};
const destWeightLimit = { Unlimited: null };
const sendXc20 = async () => {
// 2. Create Keyring instance
await cryptoWaitReady();
const keyring = new Keyring({ type: 'ethereum' });
const alice = keyring.addFromUri(privateKey);
// 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: 0x1e0104010000070010a5d4e804010101000c36e9ba26fa63c60ec728fe75fe57b86a450d94e7fee7f9f9eddd0d3f400d6700
.
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:
400,000
800,000
800,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:
- Convert
gas_limit:
to its byte representation - Convert the value for the gas limit into its little-endian byte representation
- Concatenate the two-byte representations into a single value padded to 32 bytes
- 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 = {
V4: {
id: {
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
},
},
};
| Created: October 28, 2023