This can all be done in a simple, pure JavaScript example.
Start with a minimal HTML page with the necessary Polkadot JS imports:
index.html
<!doctype html>
<html lang="en">
<body>
<h1>Simple XCM Example</h1>
<button onclick="submitXcm()">Submit an XCM Transaction</button>
<script src="//unpkg.com/@polkadot/util/bundle-polkadot-util.js"></script>
<script src="//unpkg.com/@polkadot/util-crypto/bundle-polkadot-util-crypto.js"></script>
<script src="//unpkg.com/@polkadot/types/bundle-polkadot-types.js"></script>
<script src="//unpkg.com/@polkadot/api/bundle-polkadot-api.js"></script>
<script src="//unpkg.com/@polkadot/extension-dapp/bundle-polkadot-extension-dapp.js"></script>
<!-- Our Custom JavaScript -->
<script src="./index.js"></script>
</body>
</html>
Then create an index.js
file where we will do most of our work.
Connect to Polkadot JS API
To establish a basic connection to Polkadot JS, we can follow the templates found in the docs: https://polkadot.js.org/docs/
index.js
const { WsProvider, ApiPromise } = polkadotApi;
// We will use these later to access the Polkadot JS Extension
const { web3FromAddress, web3Enable } = polkadotExtensionDapp;
async function submitXcm() {
const provider = new WsProvider('wss://kusama-rpc.polkadot.io');
const api = await ApiPromise.create({ provider });
console.log('connected:', api.genesisHash.toHex());
// We will add the code described below here...
}
Create MultiLocation for Parachain Destination
The first trick to make this work is using api.createType
to allow Polkadot JS API to create all the concrete types you need.
Another trick is to make this all work is to use the XcmVersioned...
types, not the direct MultiLocation
type. In this case, you have to control which version of MultiLocation
you want to use, but this is better.
Make sure that all of your constructed types use the same XCM version.
const parachainId = 3338;
const destination = api.createType('XcmVersionedMultiLocation', {
V2: {
parents: '0',
interior: {
X1: { Parachain: parachainId },
},
},
});
From the perspective of the relay chain, you locate a parachain with parent: 0
and a single junction (X1
) directly to the Parachain
with parachainId
.
Create a Beneficiary Location (Receiving Address)
In this scenario, we are reserve transferring KSM from the Kusama relay chain to a Parachain. We want to receive the credit for the reserve backed KSM on the parachain, so we need to figure out OUR MultiLocation.
let address = 'EGVQCe73TpFyAZx5uKfE1222XfkT3BSKozjgcqzLBnc5eYo';
let accountId = api.createType('AccountId', address);
const beneficiary = api.createType('XcmVersionedMultiLocation', {
V2: {
parents: '0',
interior: {
X1: {
AccountId32: {
network: 'Any',
id: accountId.toHex(),
},
},
},
},
});
Some tricks here are:
- Again, using
createType
and XcmVersioned...
with a specific version number for constructing the types.
- Passing in the
address
as a bytes represented by Hex. This is easy to do by creating an AccountId
type, and calling toHex()
.
I believe this object is from the perspective of the parachain, thus the benificiary has 0 parents (since we are already in the perspective of the parachain) and directly references the AccountId32
from a single junction (X1
).
Create the KSM MultiAsset
We need to also specify which asset we are transferring, and for this we need to create a MultiAssets
type. Note that this is plural, so it expects a vector of MultiAsset
, but we are only doing one here anyway.
// 1 KSM = 10^12, this is .1 KSM
const amount = 100000000000;
const assets = api.createType('XcmVersionedMultiAssets', {
V2: [
{
id: {
Concrete: {
parents: 0,
interior: {
Here: '',
},
},
},
fun: {
Fungible: amount,
},
},
],
});
We are again in the context of the relay chain, so the KSM asset is the Concrete
asset with parents: 0
and Here
. KSM is a fungible token, so we can specify how much of it we want to reserve transfer using Fungible
and a number. In this case, we are doing 10^11 which is 0.1 KSM due to the fixed point math.
Selecting the Fee Asset Item
We need to pay fees for our XCM and we can choose which asset we want to pay with. Since we only have one asset in this example, the fee_asset_item
is indexed to 0
. You can imagine if you were transferring multiple assets at a time, you could select a different asset index based on the MultiAssets
you constructed.
const fee_asset_item = '0';
Constructing a Weight Limit
While this is not best practice, the easiest thing to do when making a reserve transfer is to set the weight_limit
to Unlimited
. You do risk burning a lot of assets to weight fees in the case that the calculated weight is very high, but generally speaking this shouldn't be the case, and really is up to the parachain to ensure that things are correct.
const weight_limit = 'Unlimited';
Constructing our XCM Pallet Call
These different variables were exactly what we needed to create our limitedReserveTransferAssets
call. We just plug it all in:
const call = api.tx.xcmPallet.limitedReserveTransferAssets(
destination,
beneficiary,
assets,
fee_asset_item,
weight_limit
);
Signing and Submitting our Call with Polkadot JS Extension
Earlier, we selected an address to be the beneficiary of this XCM Reserve Transfer. We are assuming that this same address will also be the person initiating the transfer on Kusama itself.
We also assume that whoever is running this JS has already imported this account into their extension. Of course you can add fancy UI to query the available accounts in the extension and let the user select from them, but for this minimal example, we will just keep things hardcoded.
await web3Enable('xcm js example');
const injector = await web3FromAddress(address);
const unsubscribe = await call.signAndSend(
address,
{ signer: injector.signer },
({ status }) => {
console.log(`Current status is ${status}`);
if (status.isInBlock) {
console.log(`Transaction included at blockHash ${status.asInBlock}`);
} else if (status.isFinalized) {
console.log(`Transaction finalized at blockHash ${status.asFinalized}`);
unsubscribe();
}
}
);
Tricks here:
- Before we can access the
injector
for our address, we must first call web3Enable
which prompts the user to allow the website to access your Polkadot Extension. The name you choose there will be what the user sees when they are deciding to accept access.
- Then we obtain an
injector
for the address
we want to sign with. Of course this will only work if the account is imported into the extension.
- Then we sign and submit our call, using our injector, and keeping track of the status until it is finalized, at which point you should see your reserved back asset on the parachain.
For more documentation about interacting with the extension check out: https://polkadot.js.org/docs/extension/cookbook#get-all-extensions-accounts
Final View