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/pallets that developers can leverage to use XCM features on Polkadot/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¶
- XCM — stands for cross-consensus message. It is a general way for consensus systems to communicate with each other
- VMP — stands for vertical message passing, one of the transport methods for XCMs. It allows parachains to exchange messages with the relay chain. UMP (upward message passing) enables parachains to send messages to their relay chain, while DMP (downward message passing) enables the relay chain to pass messages down to one of their parachains
- XCMP — stands for cross-consensus message passing, one of the transport methods for XCMs. It allows parachains to exchange messages with other parachains on the same relay chain
- HRMP — stands for horizontal relay-routed message passing, a stop-gap protocol while a full XCMP implementation is launched. It has the same interface as XCMP, but messages are stored on the relay chain
- 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, andblake2(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 democracy (technical committee or 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 aninterior
. Parents refers to how many "hops" into a parent blockchain you need to take from a given origin. The interior refers to how many fields you need to define the target point. For example, to target a parachain with ID1000
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, destWeight) — transfer a currency, defined as either the native token (self reserved), or with the asset ID
- transferMultiasset(asset, dest, destWeight) — transfer a fungible asset, defined by its multilocation
- transferMultiassetWithFee(asset, fee, dest, destWeight) — 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, destWeight) — transfer several fungible assets, specifying which is used as the fee. Each asset is defined by its multilocation
- transferMulticurrencies(currencies, feeItem, dest, destWeight) — 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, destWeight) — 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,
SelfReserve
refers to the native token, andForeignAsset
refers to the asset ID of the XC-20 (not to be confused with the XC-20 address) - 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 bytes addresses (Ethereum or Substrate)
- destWeight — 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
Unlimited
option allows for all of the asset used for gas included to be used to pay for weight. TheLimited
option 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 pallet balances 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 be0
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
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 familiar with multilocations.
Note
Each parachain can allow/forbid specific methods from a pallet. Consequently, developers must ensure to use methods that are allowed. On the contrary, the transaction will fail with an error similar to system.CallFiltered
.
You'll be transfering xcUNIT
tokens, which is the XC-20 representation of the Alphanet relay chain token UNIT
. An xcUNIT
is an external XC-20. You can adapt this guide for another external XC-20 or a mintable XC-20.
Checking Prerequisites¶
To be able to send the extrinsics in Polkadot.js Apps, you need to have the following:
- An account loaded in Polkadot.js funded with DEV tokens
- The asset ID of the asset you're transferring
- For external XC-20s, you can get the list of asset IDs from Polkadot.js Apps
- For mintable XC-20s, please refer to the Retrieve List of Mintable XC-20s section of the Mintable XC-20s page
- The number of decimals the asset you're transferring has
- For external XC-20s, please refer to the Retrieve Metadata for External XC-20s section of the External XC-20 page
- For mintable XC-20s, please refer to the Retrieve Metadata for Mintable XC-20s section of the Mintable XC-20s page
- Some
xcUNIT
tokens. You can swapDEV
tokens (Moonbase Alpha's native token) forxcUNIT
s on Moonbeam-Swap, a demo Uniswap-V2 clone on Moonbase Alpha
To check your xcUNIT
balance, you can add the XC-20 to MetaMask with the following address:
0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080
If you're interested in how the precompile address is calculated, you can check out the following guides:
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.
Head to the extrinsic page of Polkadot.js Apps and set the following options (you can adapt for mintable XC-20s):
- Select the account from which you want to send the XCM
- Choose the xTokens pallet
- Choose the transfer extrinsic
- Set the currency ID to ForeignAsset for external XC-20s or LocalAssetReserve for mintable XC-20s. Since
xcUNIT
is an external XC-20, you can choose ForeignAsset - Enter the asset ID. For this example,
xcUNIT
has an asset id of42259045809535163221576417993425387648
- Set the number of tokens to send. For this example, you are sending 1
xcUNIT
, but you have to account for the 12 decimals ofxcUNIT
-
To define the XCM destination multilocation, you have to target an account in the relay chain from Moonbase Alpha as the origin. Therefore, set the following parameters:
Parameter Value Version V1 Parents 1 Interior X1 X1 AccountId32 Network Any Id Target Account -
Set the destination weight as
Limited
, and its value to1000000000
. Note that on Moonbase Alpha, each XCM instruction costs around100000000
weight units. Atransfer
consists of 4 XCM instructions, so a destination weight of400000000
should be enough - Click the Submit Transaction button and sign the transaction
Note
The encoded call data for the extrinsic configured above is 0x1e00018080778c30c20fa2ebc0ed18d2cbca1f0010a5d4e800000000000000000000000101010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a0630102286bee
. It also includes a specific recipient that you'll need to change.
Once the transaction is processed, the TargetAccount should have received the transferred amount minus a small fee that is deducted to execute the XCM on the destination chain. On Polkadot.js Apps, can check the relevant extrinsics and events in Moonbase Alpha and the relay 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.
Head to the extrinsic page of Polkadot.js Apps and set the following options:
- Select the account from which you want to send the XCM
- Choose the xTokens pallet
- Choose the transferMultiasset extrinsic
-
To define the XCM asset multilocation, you have to target
UNIT
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. For this example, the relay chain token can be defined as:Parameter Value Version V1 Parents 1 Interior Here If you're adapting this guide for a mintable XC-20, you have to to specify the pallet where the asset was created and the asset ID. Therefore, you would set the following parameters:
Parameter Value Version V1 Parents 1 Interior X2 PalletInstance 36 GeneralIndex Asset ID -
Set the asset type as Fungible
- Set the number of tokens to send. For this example, you are sending 1
xcUNIT
, but you have to account for the 12 decimals -
To define the XCM destination multilocation, you have to target an account in the relay chain from Moonbase Alpha as the origin. Therefore, set the following parameters:
Parameter Value Version V1 Parents 1 Interior X1 X1 AccountId32 Network Any Id Target Account -
Set the destination weight to
1000000000
. Note that on Moonbase Alpha, each XCM instruction costs around100000000
weight units. AtransferMultiasset
consists of 4 XCM instructions, so a destination weight of400000000
should be enough - Click the Submit Transaction button and sign the transaction
Note
The encoded call data for the extrinsic configured above is 0x1e010100010000070010a5d4e80101010100c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a0630102286bee
. It also includes a specific recipient that you'll need to change.
Once the transaction is processed, the TargetAccount should have received the transferred amount minus a small fee that is deducted to execute the XCM on the destination chain. On Polkadot.js Apps, you can check the relevant extrinsics and events in Moonbase Alpha and the relay chain.
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 assets precompile address for thecurrencyAddress
:- For external XC-20s, provide the XC-20 precompile address
- For mintable XC-20s, you can follow the instructions for calculating the precompile address
- For native tokens (i.e., GLMR, MOVR, and DEV), provide the ERC-20 precompile address, which is
0x0000000000000000000000000000000000000802
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 Any |
AccountKey20 | "0x03+AccountKey20+00" | AccountKey20, Network Any |
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 = 0
// Size of array is 1, meaning is an X1 interior
[
"0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"
// AccountKey32 Selector + Address in hex + Network = Any
]
}
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 PRIVATE_KEY = 'YOUR_PRIVATE_KEY_HERE';
// Create Ethers wallet & contract instance
const provider = new ethers.providers.JsonRpcProvider('https://rpc.api.moonbase.moonbeam.network');
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const xTokens = new ethers.Contract(
'0x0000000000000000000000000000000000000804',
abi,
signer
);
// xcUNIT address in Moonbase Alpha
const xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080';
// Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
const ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']];
// Sends 1 xcUNIT to the relay chain using the transfer function
async function transferToAlice() {
// Creates, signs, and sends the transfer transaction
const transaction = await xTokens.transfer(
xcUNIT_ADDRESS, // Asset
'1000000000000', // Amount
ALICE_RELAY_ACC, // Destination
'1000000000' // Weight
);
// Waits for the transaction to be included in a block
await transaction.wait();
console.log(transaction);
}
// Multilocation targeting the relay chain or its asset from a parachain
const RELAY_CHAIN_ASSET = [1, []];
// Sends 1 xcUNIT to the relay chain using the transferMultiasset function
async function transferMultiassetToAlice() {
const transaction = await xTokens.transferMultiasset(
RELAY_CHAIN_ASSET, // Asset
'1000000000000', // Amount
ALICE_RELAY_ACC, // Destination
'1000000000' // Weight
);
await transaction.wait();
console.log(transaction);}
transferToAlice();
transferMultiassetToAlice();
// Here are some additional multilocations for the Asset multilocation:
const LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]]; // Note that 0x0424 indicates the x-tokens pallet
const DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]]; // Use if you were targeting DEV from a non-Moonbeam network
// Here are some additional multilocations for the Destination multilocation:
const ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]];
const ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]];
import abi from './xtokensABI.js'; // Import the x-tokens ABI
import Web3 from 'web3'; // Import Web3 library
const PRIVATE_KEY = 'YOUR_PRIVATE_KEY_HERE';
// Create Web3 wallet & contract instance
const web3 = new Web3('https://rpc.api.moonbase.moonbeam.network'); // Change to network of choice
const xTokens = new web3.eth.Contract(
abi,
'0x0000000000000000000000000000000000000804',
{ from: web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY).address } // 'from' is necessary for gas estimation
);
// xcUNIT address in Moonbase Alpha
const xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080';
// Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
const ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']];
// Sends 1 xcUNIT to the relay chain using the transfer function
async function transferToAlice() {
// Create transaction
const transferTx = xTokens.methods.transfer(
xcUNIT_ADDRESS, // Asset
'1000000000000', // Amount
ALICE_RELAY_ACC, // Destination
'1000000000' // Weight
);
// Sign transaction
const signedTx = await web3.eth.accounts.signTransaction(
{
to: '0x0000000000000000000000000000000000000804',
data: transferTx.encodeABI(),
gas: await transferTx.estimateGas()
},
PRIVATE_KEY
);
// Send signed transaction
const sendTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
console.log(sendTx);
}
// Multilocation targeting the relay chain or its asset from a parachain
const RELAY_CHAIN_ASSET = [1, []];
// Sends 1 xcUNIT to the relay chain using the transferMultiasset function
async function transferMultiassetToAlice() {
const transferTx = xTokens.methods.transferMultiasset(
RELAY_CHAIN_ASSET, // Asset
'1000000000000', // Amount
ALICE_RELAY_ACC, // Destination
'1000000000' // Weight
);
const signedTx = await web3.eth.accounts.signTransaction(
{
to: '0x0000000000000000000000000000000000000804',
data: transferTx.encodeABI(),
gas: await transferTx.estimateGas()
},
PRIVATE_KEY
);
const sendTx = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
console.log(sendTx);
}
transferToAlice();
transferMultiassetToAlice();
// Here are some additional multilocations for the Asset multilocation:
const LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]]; // Note that 0x0424 indicates the x-tokens pallet
const DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]]; // Use if you were targeting DEV from a non-Moonbeam network
// Here are some additional multilocations for the Destination multilocation:
const ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]];
const ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]];
from web3 import Web3
abi = 'XTOKENS_ABI_HERE' # Paste or import the x-tokens ABI
privateKey = 'YOUR_PRIVATE_KEY_HERE' # This is for demo purposes, never store your private key in plain text
address = 'YOUR_ADDRESS_HERE' # The wallet address that corresponds to your private key
# Create Web3 wallet & contract instance
web3 = Web3(Web3.HTTPProvider('https://rpc.api.moonbase.moonbeam.network'))
xTokens = web3.eth.contract(
address='0x0000000000000000000000000000000000000804',
abi=abi
)
# xcUNIT address in Moonbase Alpha
xcUNIT_ADDRESS = '0xFfFFfFff1FcaCBd218EDc0EbA20Fc2308C778080'
# Multilocation targeting Alice's account on the relay chain from Moonbase Alpha
ALICE_RELAY_ACC = [1, ['0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300']]
# Sends 1 xcUNIT to the relay chain using the transfer function
def transferToAlice():
# Create transaction
transferTx = xTokens.functions.transfer(
xcUNIT_ADDRESS, # Asset
1000000000000, # Amount
ALICE_RELAY_ACC, # Destination
1000000000 # Weight
).buildTransaction(
{
'from': address,
'nonce': web3.eth.get_transaction_count(address),
}
)
# Sign transaction
signedTx = web3.eth.account.sign_transaction(transferTx, privateKey)
# 7. Send tx and wait for receipt
hash = web3.eth.send_raw_transaction(signedTx.rawTransaction)
receipt = web3.eth.wait_for_transaction_receipt(hash)
print(f'Tx successful with hash: { receipt.transactionHash.hex() }')
# Multilocation targeting the relay chain or its asset from a parachain
RELAY_CHAIN_ASSET = [1, []];
# Sends 1 xcUNIT to the relay chain using the transferMultiasset function
def transferMultiassetToAlice():
transferTx = xTokens.functions.transferMultiasset(
RELAY_CHAIN_ASSET, # Asset
1000000000000, # Amount
ALICE_RELAY_ACC, # Destination
1000000000 # Weight
).buildTransaction(
{
'from': address,
'nonce': web3.eth.get_transaction_count(address),
}
)
signedTx = web3.eth.account.sign_transaction(transferTx, privateKey)
hash = web3.eth.send_raw_transaction(signedTx.rawTransaction)
receipt = web3.eth.wait_for_transaction_receipt(hash)
print(f'Tx successful with hash: { receipt.transactionHash.hex() }')
transferToAlice()
transferMultiassetToAlice()
# Here are some additional multilocations for the Asset multilocation:
LOCAL_ASSET = [0, ["0x0424", "0x05FD9D0BF45A2947A519A741C4B9E99EB6"]] # Note that 0x0424 indicates the x-tokens pallet
DEV_FROM_OTHER_PARACHAIN = [1, ["0x00000003E8", "0x0403"]] # Use if you were targeting DEV from a non-Moonbeam network
# Here are some additional multilocations for the Destination multilocation:
ADDRESS32_OF_PARACHAIN = [1, ["0x00000007EF", "0x01c4db7bcb733e117c0b34ac96354b10d47e84a006b9e7e66a229d174e8ff2a06300"]]
ADDRESS20_FROM_PARACHAIN_TO_MOONBASE = [1, ["0x00000003E8", "0x03f24FF3a9CF04c71Dbc94D0b566f7A27B94566cac00"]]
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.