Skip to content

Remote Staking via XCM

by Kevin Neilson

Introduction

In this tutorial, we’ll stake DEV tokens remotely by sending XCM instructions from an account on the Moonbase relay chain (equivalent to the Polkadot relay chain). This tutorial assumes a basic familiarity with XCM and Remote Execution via XCM. You don’t have to be an expert on these topics but you may find it helpful to have some XCM knowledge as background.

There are actually two possible approaches for staking on Moonbeam remotely via XCM. We could send a remote EVM call that calls the staking precompile, or we could use XCM to call the parachain staking pallet directly without interacting with the EVM. For this tutorial, we’ll be taking the latter approach and interacting with the parachain staking pallet directly.

Note that there are still limitations in what you can remotely execute through XCM messages. In addition, 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.

Checking Prerequisites

For development purposes this tutorial is written for Moonbase Alpha and Moonbase relay using TestNet funds. For prerequisites:

Calculating your Computed Origin Account

Copy the account of your existing or newly created account on the Moonbase relay chain. You're going to need it to calculate the corresponding Computed Origin account, which is a special type of account that’s keyless (the private key is unknown). Transactions from a Computed Origin account can be initiated only via valid XCM instructions from the corresponding account on the relay chain. In other words, you are the only one who can initiate transactions on your Computed Origin account, and if you lose access to your Moonbase relay account, you’ll also lose access to your Computed Origin account.

To generate the Computed Origin account, first clone the xcm-tools repo. Run yarn to install the necessary packages, and then run:

yarn calculate-multilocation-derivative-account \
--ws-provider wss://wss.api.moonbase.moonbeam.network \
--address INSERT_MOONBASE_RELAY_ACCOUNT \
--para-id INSERT_ORIGIN_PARACHAIN_ID_IF_APPLIES \
--parents INSERT_PARENTS_VALUE_IF_APPLIES

Let's review the parameters passed along with this command:

  • The --ws-provider or -w flag corresponds to the endpoint we’re using to fetch this information
  • The --address or -a flag corresponds to your Moonbase relay chain address
  • The --para-id or -p flag corresponds to the parachain ID of the origin chain (if applicable). If you are sending the XCM from the relay chain, you don't need to provide this parameter
  • The -parents flag corresponds to the parents value of the origin chain in relation to the destination chain. If you're deriving a multi-location derivative account on a parachain destination from a relay chain origin, this value would be 1. If left out, the parents value defaults to 0

Here, we have specified a parents value of 1 because the relay chain is the origin of the request (and the relay chain is considered a parent to the Moonbase alpha parachain). The relay chain does not have a parachain id so that field is omitted.

yarn calculate-multilocation-derivative-account \ --ws-provider wss://wss.api.moonbase.moonbeam.network \ --address 5DCvkTpkqo5AuvUFSrT76ABnm48iSBHpgsoDFNxFZAtesvWD \ --parents 1
yarn run v1.22.10 warning ../../../package.json: No license field $ ts-node 'scripts/calculate-multilocation-derivative-account.ts' --ws-provider wss://wss.api.moonbase.moonbeam.network --address 5DCvkTpkqo5AuvUFSrT76ABnm48iSBHpgsoDFNxFZAtesvWD --parents 1
Remote Origin calculated as ParentChain Parents 1 AccountId32: 5DCvkTpkqo5AuvUFSrT76ABnm48iSBHpgsoDFNxFZAtesvWD 32 byte address is 0x55738eb7227f27c9d55775f65ad261c5ac2894dcde73d913f77f69bf51e26279 20 byte address is 0x55738eb7227f27c9d55775f65ad261c5ac2894dc ✨ Done in 1.02s.

The script will return 32-byte and 20-byte addresses. We’re interested in the Ethereum-style account - the 20-byte one. Feel free to look up your Computed Origin account on Moonscan. You’ll note that this account is empty. You’ll now need to fund this account with at least 1.1 DEV which you can get from the faucet. And if you need more, you can always reach out to us on Discord for additional DEV tokens.

Preparing to Stake on Moonbase Alpha

First and foremost, you’ll need the address of the collator you want to delegate to. To locate it, head to the Moonbase Alpha Staking dApp in a second window. Ensure you’re on the correct network, then press Select a Collator. Press the icon next to your desired collator to copy its address. You’ll also need to make a note of the number of delegations your collator has. The Moonbeam Foundation 01 collator shown below has 7 delegations at the time of writing.

Moonbeam Network Apps Dashboard

Remote Staking via XCM with the Polkadot.js API

This tutorial will cover the two-step process to perform remote staking operations. The first step we'll take is to generate the encoded call data for delegating a collator. Secondly, we'll send the encoded call data via XCM from the relay chain to Moonbase Alpha, which will result in the execution of the delegation.

Generate the Encoded Call Data

We'll be using the delegateWithAutoCompound function of the Parachain Staking Pallet, which accepts six parameters: candidate, amount, autoCompound, candidateDelegationCount, candidateAutoCompoundingDelegationCount, and delegationCount.

In order to generate the encoded call data, we'll need to assemble the arguments for each of the delegateWithAutoCompound parameters and use them to build a transaction which will call the delegateWithAutoCompound function. We are not submitting a transaction, but simply preparing one to get the encoded call data. We'll take the following steps to build our script:

  1. Create a Polkadot.js API provider
  2. Assemble the arguments for each of the parameters of the delegateWithAutoCompound function:

    • candidate- for this example we'll use the Moonbeam Foundation 01 collator: 0x12E7BCCA9b1B15f33585b5fc898B967149BDb9a5. To retrieve the entire list of candidates, you can refer back to the Preparing to Stake section
    • amount - we'll stake the minimum amount, which is 1 DEV or 1000000000000000000 Wei. You can find a unit converter on Moonscan
    • autoCompound - we'll set this to 100 to auto-compound all rewards
    • candidateDelegationCount - we'll retrieve using the candidateInfo function of the Parachain Staking Pallet to get the exact count. Alternatively, you can enter the upper bound of 300 because this estimation is only used to determine the weight of the call
    • candidateAutoCompoundingDelegationCount - we'll retrieve using the autoCompoundingDelegations function of the Parachain Staking Pallet to get the exact count. Alternatively, you can enter the upper bound of 300 because this estimation is only used to determine the weight of the call
    • delegationCount - we'll retrieve using the delegatorState function of the Parachain Staking Pallet to get the exact count. Alternatively, you can specify an upper bound here of 100
  3. Craft the parachainStaking.delegateWithAutoCompound extrinsic with each of the required arguments

  4. Use the transaction to get the encoded call data for the delegation
import { ApiPromise, WsProvider } from '@polkadot/api';
const provider = new WsProvider('wss://wss.api.moonbase.moonbeam.network');

const candidate = '0x12E7BCCA9b1B15f33585b5fc898B967149BDb9a5';
const amount = '1000000000000000000';
const autoCompound = 100;

const main = async () => {
  const api = await ApiPromise.create({ provider: provider });

  // Fetch your existing number of delegations
  let delegatorDelegationCount;
  const delegatorInfo = await api.query.parachainStaking.delegatorState(
    'INSERT_ACCOUNT' // Use the account you're delegating with
  );

  if (delegatorInfo.toHuman()) {
    delegatorDelegationCount = delegatorInfo.toHuman()['delegations'].length;
  } else {
    delegatorDelegationCount = 0;
  }

  // Fetch the collators existing delegations
  const collatorInfo = await api.query.parachainStaking.candidateInfo(
    candidate
  );
  const candidateDelegationCount = collatorInfo.toHuman()['delegationCount'];

  // Fetch the collators number of existing auto-compounding delegations
  const autoCompoundingDelegationsInfo =
    await api.query.parachainStaking.autoCompoundingDelegations(candidate);
  const candidateAutoCompoundingDelegationCount =
    autoCompoundingDelegationsInfo.length;

  // Craft extrinsic
  const tx = api.tx.parachainStaking.delegateWithAutoCompound(
    candidate,
    amount,
    autoCompound,
    candidateDelegationCount,
    candidateAutoCompoundingDelegationCount,
    delegatorDelegationCount
  );

  // Get SCALE encoded call data
  const encodedCall = tx.method.toHex();
  console.log(`Encoded Call Data: ${encodedCall}`);

  api.disconnect();
};
main();

Note

If running this as a TypeScript project, be sure to set the strict flag under compilerOptions to false in your tsconfig.json.

If you'd prefer not to set up a local environment, you can run a code snippet in the JavaScript console of Polkadot.js Apps.

Code to run in the Polkadot.js Apps JavaScript console
const candidate = '0x12E7BCCA9b1B15f33585b5fc898B967149BDb9a5';
const amount = '1000000000000000000';
const autoCompound = 100;

// Fetch your existing number of delegations
let delegatorDelegationCount;
// Use the account you're delegating with
const delegatorInfo = await api.query.parachainStaking.delegatorState(
  'INSERT_ACCOUNT'
);

if (delegatorInfo.toHuman()) {
  delegatorDelegationCount = delegatorInfo.toHuman()['delegations'].length;
} else {
  delegatorDelegationCount = 0;
}

// Fetch the collators existing delegations
const collatorInfo = await api.query.parachainStaking.candidateInfo(candidate);
const candidateDelegationCount = collatorInfo.toHuman()['delegationCount'];

// Fetch the collators number of existing auto-compounding delegations
const autoCompoundingDelegationsInfo =
  await api.query.parachainStaking.autoCompoundingDelegations(candidate);
const candidateAutoCompoundingDelegationCount =
  autoCompoundingDelegationsInfo.length;

// Craft extrinsic
const tx = api.tx.parachainStaking.delegateWithAutoCompound(
  candidate,
  amount,
  autoCompound,
  candidateDelegationCount,
  candidateAutoCompoundingDelegationCount,
  delegatorDelegationCount
);

// Get SCALE Encoded Call Data
const encodedCall = tx.method.toHex();
console.log(`Encoded Call Data: ${encodedCall}`);

Assemble and Send XCM Instructions via the Polkadot.js API

In this section, we'll be using the Polkadot.js API to construct and send XCM instructions via the send extrinsic of the XCM Pallet on the Alphanet relay chain. The XCM message will transport our remote execution instructions to the Moonbase Alpha parachain to ultimately stake our desired amount of DEV tokens to a chosen collator.

The send function of the XCM Pallet accepts two parameters: dest and message. You can start assembling these parameters by taking the following steps:

  1. Build the multilocation of the DEV token on Moonbase Alpha for the dest:

    const dest = { V4: { parents: 0, interior: { X1: [{ Parachain: 1000 }] } } };
    
  2. Build the WithdrawAsset instruction, which will require you to define:

    • The multilocation of the DEV token on Moonbase Alpha
    • The amount of DEV tokens to withdraw
    const instr1 = {
      WithdrawAsset: [
        {
          id: {
            parents: 0,
            interior: { X1: [{ PalletInstance: 3 }] },
          },
          fun: { Fungible: 100000000000000000n },
        },
      ],
    };
    
  3. Build the BuyExecution instruction, which will require you to define:

    • The multilocation of the DEV token on Moonbase Alpha
    • The amount of DEV tokens to buy for execution
    • The weight limit
    const instr2 = {
      BuyExecution: [
        {
          id: {
            parents: 0,
            interior: { X1: [{ PalletInstance: 3 }] },
          },
          fun: { Fungible: 100000000000000000n },
        },
        { Unlimited: null },
      ],
    };
    
  4. Build the Transact instruction, which will require you to define:

    • The origin type, which will be SovereignAccount
    • The required weight for the transaction. You'll need to define a value for refTime, which is the amount of computational time that can be used for execution, and the proofSize, which is the amount of storage in bytes that can be used. It is recommended that the weight given to this instruction needs to be around 10% more of 25000 times the gas limit for the call you want to execute via XCM
    • The encoded call data for delegating a collator, which we generated in the previous section
    const instr3 = {
      Transact: {
        originKind: 'SovereignAccount',
        requireWeightAtMost: { refTime: 40000000000n, proofSize: 900000n },
        call: {
          encoded:
            '0x0c1212e7bcca9b1b15f33585b5fc898b967149bdb9a5000064a7b3b6e00d000000000000000064070000000700000000000000',
        },
      },
    };
    
  5. Combine the XCM instructions into a versioned XCM message:

    const message = { V4: [instr1, instr2, instr3] };
    

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

  1. Provide the values for each of the parameters of the send function
  2. Create the Polkadot.js API provider using the WSS endpoint of the Alphanet relay chain
  3. Create a Keyring instance using the mnemonic of your relay chain account, which will be used to send the transaction
  4. Craft the xcmPallet.send extrinsic with the dest and message
  5. Send the transaction using the signAndSend extrinsic and the Keyring instance you created in the third step

Remember

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

import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { cryptoWaitReady } from '@polkadot/util-crypto';

const privateKey = 'INSERT_PRIVATE_KEY_OR_MNEMONIC';

// 1. Define the dest and message arguments
const dest = { V4: { parents: 0, interior: { X1: [{ Parachain: 1000 }] } } };
const instr1 = {
  WithdrawAsset: [
    {
      id: {
        parents: 0,
        interior: { X1: [{ PalletInstance: 3 }] },
      },
      fun: { Fungible: 100000000000000000n },
    },
  ],
};
const instr2 = {
  BuyExecution: [
    {
      id: {
        parents: 0,
        interior: { X1: [{ PalletInstance: 3 }] },
      },
      fun: { Fungible: 100000000000000000n },
    },
    { Unlimited: null },
  ],
};
const instr3 = {
  Transact: {
    originKind: 'SovereignAccount',
    requireWeightAtMost: { refTime: 40000000000n, proofSize: 900000n },
    call: {
      encoded:
        '0x0c1212e7bcca9b1b15f33585b5fc898b967149bdb9a5000064a7b3b6e00d000000000000000064070000000700000000000000',
    },
  },
};
const message = { V4: [instr1, instr2, instr3] };

const performRemoteDelegation = async () => {
  // 2. Construct API provider
  const wsProvider = new WsProvider(
    'wss://relay.api.moonbase.moonbeam.network'
  );
  const api = await ApiPromise.create({ provider: wsProvider });

  // 3. Initialize wallet key pairs
  await cryptoWaitReady();
  const keyring = new Keyring({ type: 'sr25519' });
  // For demo purposes only. Never store your private key or mnemonic in a JavaScript file
  const otherPair = keyring.addFromUri(privateKey);
  console.log(`Derived Address from Private Key: ${otherPair.address}`);

  // 4. Define the transaction using the send method of the xcm pallet
  const tx = api.tx.xcmPallet.send(dest, message);

  // 5. Sign and send the transaction
  const txHash = await tx.signAndSend(otherPair);
  console.log(`Submitted with hash ${txHash}`);

  api.disconnect();
};

performRemoteDelegation();

Note

Remember that your Computed Origin account must be funded with at least 1.1 DEV or more to ensure you have enough to cover the stake amount and transaction fees.

In the above snippet, besides submitting the remote staking via XCM transaction, we also print out the transaction hash to assist with any debugging.

And that’s it! To verify that your delegation was successful, you can visit Subscan to check your staking balance. Be advised that it may take a few minutes before your staking balance is visible on Subscan. Additionally, be aware that you will not be able to see this staking operation on Moonscan, because we initiated the delegation action directly via the Parachain Staking Pallet (on the Substrate side) rather than through the Staking Precompile (on the EVM).

This tutorial is for educational purposes only. As such, any contracts or code created in this tutorial should not be used in production.
Last update: November 28, 2024
| Created: December 10, 2022