Skip to content

Polkadot.js API Library

Introduction

Polkadot.js is a collection of tools that allow you to interact with Substrate-based blockchains, such as your appchain! The Polkadot.js API is one component of Polkadot.js and is a library that allows application developers to query an appchain and interact with the node's Substrate interfaces using JavaScript, enabling you to read and write data to the network.

You can use the Polkadot.js API to query on-chain data and send extrinsics from the Substrate side of your Tanssi appchain. You can query runtime constants, chain state, events, transaction (extrinsic) data, and more.

Here you will find an overview of the available functionalities and some commonly used TypeScript code examples to get you started on interacting with your Tanssi appchain using the Polkadot.js API library.

Note

The examples in this guide are based on a MacOS or Ubuntu 20.04 environment. If you're using Windows, you'll need to adapt them accordingly.

Furthermore, please ensure that you have Node.js and a package manager (such as npm or yarn) installed. To learn how to install Node.js, please check their official documentation.

Also, make sure you've initialized a package.json file for ES6 modules. You can initialize a default package.json file using npm by running the following command npm init --yes.

Install Polkadot.js API

First, you need to install the Polkadot.js API library and the RLP library through a package manager such as yarn. Both dependencies are required to run the examples in this guide successfully.

Install them in your project directory with the following command:

npm i @polkadot/api
npm i @polkadot/util-rlp
yarn add @polkadot/api
yarn add @polkadot/util-rlp

The library also includes other core components, like Keyring for account management or some utilities that are used throughout this guide.

Create an API Provider Instance

To start interacting with your Tanssi appchain using the Polkadot.js API, you first need to create an instance of the Polkadot.js API. Create the WsProvider using the WebSocket endpoint of your Tanssi appchain.

// Import
import { ApiPromise, WsProvider } from '@polkadot/api';

const main = async () => {
  // Construct API provider
  const wsProvider = new WsProvider('INSERT_APPCHAIN_WSS_ENDPOINT');
  const api = await ApiPromise.create({ provider: wsProvider });

  // Code goes here

  await api.disconnect();
};

main();

Metadata and Dynamic API Decoration

Before diving into the details of performing different tasks via the Polkadot.js API library, it's useful to understand some of the basic workings of the library.

When the Polkadot.js API connects to a node, one of the first things it does is retrieve the metadata and decorate the API based on the metadata information. The metadata effectively provides data in the form of:

api.<type>.<module>.<section>

Where <type> can be either:

  • query - for endpoints to read all the state queries
  • tx - for endpoints related to transactions
  • rpc - for endpoints specific to RPC calls
  • consts - for endpoints specific to runtime constants

And therefore, none of the information contained in the api.{query, tx, rpc, consts}.<module>.<method> endpoints are hard-coded in the API. This allows the Polkadot.js API library to be modular and adapt to any Substrate-based chains with different modules, like your Tanssi appchain!

Query On-Chain Data

In this section, you will learn how to query for on-chain information using the Polkadot.js API library.

Chain State Queries

This category of queries retrieves information related to the current state of the chain. These endpoints are generally of the form api.query.<module>.<method>, where the module and method decorations are generated through metadata. You can see a list of all available endpoints by examining the api.query object, for example via:

console.log(api.query);

For example, assuming you've initialized the API, you can retrieve basic account information with the following snippet:

// Define wallet address
const addr = 'INSERT_ADDRESS';

// Retrieve the last timestamp
const now = await api.query.timestamp.now();

// Retrieve the account balance & current nonce via the system module
const { nonce, data: balance } = await api.query.system.account(addr);

console.log(
  `${now}: balance of ${balance.free} and a current nonce of ${nonce}`
);
View the complete script
import '@polkadot/api-augment';
import { ApiPromise, WsProvider } from '@polkadot/api';

const main = async () => {
  // Construct API provider
  const wsProvider = new WsProvider('INSERT_APPCHAIN_WSS_ENDPOINT');
  const api = await ApiPromise.create({ provider: wsProvider });

  // Define wallet address
  const addr = 'INSERT_ADDRESS';

  // Retrieve the last timestamp via the timestamp module
  const now = await api.query.timestamp.now();

  // Retrieve the account balance & current nonce via the system module
  const { nonce, data: balance } = await api.query.system.account(addr);

  console.log(
    `${now}: balance of ${balance.free} and a current nonce of ${nonce}`
  );

  // Disconnect the API
  await api.disconnect();
};

main();

RPC Queries

The RPC calls provide the backbone for the transmission of data to and from the node. This means that all API endpoints such as api.query, api.tx or api.derive just wrap RPC calls, providing information in the encoded format as expected by the node. You can see a list of all available endpoints by examining the api.rpc object, for example, via:

console.log(api.rpc);

The api.rpc interface follows the a similar format to api.query. For instance, assuming you've initialized the API, you can get chain data and latest header with the following snippet:

// Retrieve the chain name
const chain = await api.rpc.system.chain();

// Retrieve the latest header
const lastHeader = await api.rpc.chain.getHeader();

// Log the information
console.log(
  `${chain}: last block #${lastHeader.number} has hash ${lastHeader.hash}`
);
View the complete script
import { ApiPromise, WsProvider } from '@polkadot/api';

const main = async () => {
  // Construct API provider
  const wsProvider = new WsProvider('INSERT_APPCHAIN_WSS_ENDPOINT');
  const api = await ApiPromise.create({ provider: wsProvider });

  // Retrieve the chain name
  const chain = await api.rpc.system.chain();

  // Retrieve the latest header
  const lastHeader = await api.rpc.chain.getHeader();

  // Log the information
  console.log(
    `${chain}: last block #${lastHeader.number} has hash ${lastHeader.hash}`
  );

  // Disconnect the API
  await api.disconnect();
};

main();

Query Subscriptions

The rpc API also provides endpoints for subscriptions. Assuming you've initialized the API, you can adapt the previous example to start using subscriptions to listen to new blocks.

// Retrieve the chain name
const chain = await api.rpc.system.chain();

// Subscribe to the new headers
await api.rpc.chain.subscribeNewHeads((lastHeader) => {
  console.log(
    `${chain}: last block #${lastHeader.number} has hash ${lastHeader.hash}`
  );
});

The general pattern for api.rpc.subscribe* functions is to pass a callback into the subscription function, and this will be triggered on each new entry as they are imported.

Other calls under api.query.* can be modified in a similar fashion to use subscription by providing a callback function, including calls that have parameters. Here is an example of how to subscribe to balance changes in an account:

// Define wallet address
const addr = 'INSERT_ADDRESS';

// Subscribe to balance changes for a specified account
await api.query.system.account(addr, ({ nonce, data: balance }) => {
  console.log(
    `Free balance is ${balance.free} with ${balance.reserved} reserved and a nonce of ${nonce}`
  );
});
View the complete script
import '@polkadot/api-augment';
import { ApiPromise, WsProvider } from '@polkadot/api';

const main = async () => {
  // Construct API provider
  const wsProvider = new WsProvider('INSERT_APPCHAIN_WSS_ENDPOINT');
  const api = await ApiPromise.create({ provider: wsProvider });

  // Retrieve the chain name
  const chain = await api.rpc.system.chain();

  // Subscribe to the new headers
  await api.rpc.chain.subscribeNewHeads((lastHeader) => {
    console.log(
      `${chain}: last block #${lastHeader.number} has hash ${lastHeader.hash}`
    );
  });

  // Define wallet address
  const addr = 'INSERT_ADDRESS';

  // Subscribe to balance changes for a specified account
  await api.query.system.account(addr, ({ nonce, data: balance }) => {
    console.log(
      `Free balance is ${balance.free} with ${balance.reserved} reserved and a nonce of ${nonce}`
    );

    // Handle API disconnect here if needed
  });
};

main();

Create a Keyring for an Account

The Keyring object is used for maintaining key pairs, and the signing of any data, whether it's a transfer, a message, or a contract interaction.

Create a Keyring Instance

You can create an instance by just creating an instance of the Keyring class and specifying the default type of wallet address used. The default wallet type is SR25519, but for Tanssi EVM-compatible appchains, the wallet type should be ethereum.

// Import the keyring as required
import Keyring from '@polkadot/keyring';

// Create a keyring instance (ECDSA)
const keyringECDSA = new Keyring({ type: 'ethereum' });

// Create a keyring instance (SR25519)
const keyring = new Keyring({ type: 'sr25519' });

Add an Account to a Keyring

There are a number of ways to add an account to the keyring instance, including from the mnemonic phrase and the short-form private key. The following sample code will provide some examples:

// Import the required packages
import Keyring from '@polkadot/keyring';
import { u8aToHex } from '@polkadot/util';
import { mnemonicToLegacySeed, hdEthereum } from '@polkadot/util-crypto';

// Import Ethereum account from mnemonic
const keyringECDSA = new Keyring({ type: 'ethereum' });
const mnemonic = 'INSERT_MNEMONIC';

// Define index of the derivation path and the derivation path
const index = 0;
const ethDerPath = "m/44'/60'/0'/0/" + index;
console.log(`Mnemonic: ${mnemonic}`);
console.log(`--------------------------\n`);

// Extract Ethereum address from mnemonic
const newPairEth = keyringECDSA.addFromUri(`${mnemonic}/${ethDerPath}`);
console.log(`Ethereum Derivation Path: ${ethDerPath}`);
console.log(`Derived Ethereum Address from Mnemonic: ${newPairEth.address}`);

// Extract private key from mnemonic
const privateKey = u8aToHex(
  hdEthereum(mnemonicToLegacySeed(mnemonic, '', false, 64), ethDerPath)
    .secretKey
);
console.log(`Derived Private Key from Mnemonic: ${privateKey}`);
// Import the required packages
import Keyring from '@polkadot/keyring';

// Import Ethereum account from private key
const keyringECDSA = new Keyring({ type: 'ethereum' });
const privateKeyInput = 'INSERT_PK';

// Extract address from private key
const otherPair = keyringECDSA.addFromUri(privateKeyInput);
console.log(`Derived Address from provided Private Key: ${otherPair.address}`);
// Import the required packages
import Keyring from '@polkadot/keyring';
import { cryptoWaitReady } from '@polkadot/util-crypto';

const main = async () => {
  await cryptoWaitReady();

  // Import SR25519 account from mnemonic
  const keyring = new Keyring({ type: 'sr25519' });
  const mnemonic = 'INSERT_MNEMONIC';

  // Extract SR25519 address from mnemonic
  const newPair = keyring.addFromUri(`${mnemonic}`);
  console.log(`Derived SR25519 Address from Mnemonic: ${newPair.address}`);
};

main();

Sending Transactions

Transaction endpoints are exposed on endpoints generally of the form api.tx.<module>.<method>, where the module and method decorations are generated through metadata. These allow you to submit transactions for inclusion in blocks, be they transfers, interactions with pallets, or anything else Moonbeam supports. You can see a list of all available endpoints by examining the api.tx object, for example, via:

console.log(api.tx);

Send a Basic Transaction

The Polkadot.js API library can be used to send transactions to the network. For example, assuming you've initialized the API and a keyring instance, you can use the following snippet to send a basic transaction (this code sample will also retrieve the encoded calldata of the transaction as well as the transaction hash after submitting):

// Initialize wallet key pairs
const alice = keyring.addFromUri('INSERT_ALICES_PRIVATE_KEY');

// Form the transaction
const tx = await api.tx.balances.transferAllowDeath(
  'INSERT_BOBS_ADDRESS',
  BigInt(INSERT_VALUE)
);

// Retrieve the encoded calldata of the transaction
const encodedCallData = tx.method.toHex()
console.log(`Encoded calldata: ${encodedCallData}`);

// Sign and send the transaction
const txHash = await tx.signAndSend(alice);

// Show the transaction hash
console.log(`Submitted with hash ${txHash}`);
View the complete script
import { ApiPromise, WsProvider } from '@polkadot/api';
import Keyring from '@polkadot/keyring';

const main = async () => {
  // Construct API provider
  const wsProvider = new WsProvider('INSERT_APPCHAIN_WSS_ENDPOINT');
  const api = await ApiPromise.create({ provider: wsProvider });

  // Create a keyring instance (ECDSA)
  const keyring = new Keyring({ type: 'ethereum' });

  // Initialize wallet key pairs
  const alice = keyring.addFromUri('INSERT_ALICES_PRIVATE_KEY');

  // Form the transaction
  const tx = await api.tx.balances.transferAllowDeath(
    'INSERT_BOBS_ADDRESS',
    BigInt(INSERT_VALUE)
  );

  // Retrieve the encoded calldata of the transaction
  const encodedCalldata = tx.method.toHex();
  console.log(`Encoded calldata: ${encodedCalldata}`);

  // Sign and send the transaction
  const txHash = await tx.signAndSend(alice);

  // Show the transaction hash
  console.log(`Submitted with hash ${txHash}`);

  // Disconnect the API
  await api.disconnect();
};

main();

Note that the signAndSend function can also accept optional parameters, such as the nonce. For example, signAndSend(alice, { nonce: aliceNonce }). You can use the sample code from the State Queries section to retrieve the correct nonce, including transactions in the mempool.

Fee Information

The transaction endpoint also offers a method to obtain weight information for a given api.tx.<module>.<method>. To do so, you'll need to use the paymentInfo function after having built the entire transaction with the specific module and method.

The paymentInfo function returns weight information in terms of refTime and proofSize, which can be used to determine the transaction fee. This is extremely helpful when crafting remote execution calls via XCM.

For example, assuming you've initialized the API, the following snippet shows how you can get the weight info for a simple balance transfer between two accounts:

// Transaction to get weight information
const tx = api.tx.balances.transferAllowDeath('INSERT_BOBS_ADDRESS', BigInt(INSERT_VALUE));

// Get weight info
const { partialFee, weight } = await tx.paymentInfo('INSERT_SENDERS_ADDRESS');

console.log(`Transaction weight: ${weight}`);
console.log(`Transaction fee: ${partialFee.toHuman()}`);
View the complete script
import { ApiPromise, WsProvider } from '@polkadot/api';

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

  // Transaction to get weight information
  const tx = api.tx.balances.transferAllowDeath('INSERT_BOBS_ADDRESS', BigInt(INSERT_VALUE));

  // Get weight info
  const { partialFee, weight } = await tx.paymentInfo('INSERT_SENDERS_ADDRESS');

  console.log(`Transaction weight: ${weight}`);
  console.log(`Transaction fee: ${partialFee.toHuman()}`);

  // Disconnect the API
  await api.disconnect();
};

main();

Transaction Events

Any transaction will emit events; at a bare minimum, this will always be a system.ExtrinsicSuccess or system.ExtrinsicFailed event for the specific transaction. These provide the overall execution result for the transaction, that is, whether the execution has succeeded or failed.

Depending on the transaction sent, some other events may be emitted; for instance, for a balance transfer event, this could include one or more balance.Transfer events.

Batch Transactions

The Polkadot.js API allows transactions to be batch processed via the api.tx.utility.batch method. The batched transactions are processed sequentially from a single sender. The transaction fee can be estimated using the paymentInfo helper method.

For example, assuming you've initialized the API, a keyring instance and added an account, the following example makes a couple of transfers in one transaction:

// Construct a list of transactions to batch
const txs = [
  api.tx.balances.transferAllowDeath('INSERT_BOBS_ADDRESS', BigInt(INSERT_VALUE)),
  api.tx.balances.transferAllowDeath('INSERT_CHARLEYS_ADDRESS', BigInt(INSERT_VALUE)),
];

// Estimate the fees as RuntimeDispatchInfo, using the signer (either
// address or locked/unlocked keypair) 
const info = await api.tx.utility
  .batch(txs)
  .paymentInfo(alice);

console.log(`Estimated fees: ${info}`);

// Construct the batch and send the transactions
await api.tx.utility
  .batch(txs)
  .signAndSend(alice, ({ status }) => {
    if (status.isInBlock) {
      console.log(`included in ${status.asInBlock}`);

      // Disconnect API here!
    }
  });
View the complete script
import { ApiPromise, WsProvider } from '@polkadot/api';
import Keyring from '@polkadot/keyring';

const main = async () => {
  // Construct API provider
  const wsProvider = new WsProvider('INSERT_APPCHAIN_WSS_ENDPOINT');
  const api = await ApiPromise.create({ provider: wsProvider });

  // Create a keyring instance (ECDSA)
  const keyring = new Keyring({ type: 'ethereum' });

  // Initialize wallet key pairs
  const alice = keyring.addFromUri('INSERT_ALICES_PRIVATE_KEY');

  // Construct a list of transactions to batch
  const txs = [
    api.tx.balances.transferAllowDeath('INSERT_BOBS_ADDRESS', BigInt(INSERT_VALUE)),
    api.tx.balances.transferAllowDeath('INSERT_CHARLEYS_ADDRESS', BigInt(INSERT_VALUE)),
  ];

  // Estimate the fees as RuntimeDispatchInfo, using the signer (either
  // address or locked/unlocked keypair)
  const info = await api.tx.utility.batch(txs).paymentInfo(alice);

  console.log(`Estimated fees: ${info}`);

  // Construct the batch and send the transactions
  await api.tx.utility.batch(txs).signAndSend(alice, async ({ status }) => {
    if (status.isInBlock) {
      console.log(`Included in ${status.asInBlock}`);

      // Disconnect the API
      await api.disconnect();
    }
  });
};

main();

Sample Code for Monitoring Native Token Transfers

The following code samples will demonstrate how to listen to both types of native token transfers, sent via Substrate or Ethereum API, using either the Polkadot.js API library or Substrate API Sidecar. The following code snippets are for demo purposes only and should not be used without modification and further testing in a production environment.

The following code snippet uses subscribeFinalizedHeads to subscribe to new finalized block headers, and loops through extrinsics fetched from the block, and retrieves the events of each extrinsic.

Then, it checks if any event corresponds to a balances.Transfer event. If so, it will extract the from, to, amount, and the tx hash of the transfer and display it on the console. Note that the amount is shown in the smallest unit (Wei). You can find all the available information about Polkadot.js and the Substrate JSON RPC in their official documentation site.

import '@polkadot/api-augment';
import { ApiPromise, WsProvider } from '@polkadot/api';

// This script will listen to all Native token transfers (Substrate & Ethereum) and extract the tx hash
// It can be adapted for any Tanssi appchain

const main = async () => {
  // Define the provider
  const wsProvider = new WsProvider('INSERT_WSS_ENDPOINT');
  // Create the provider
  const polkadotApi = await ApiPromise.create({
    provider: wsProvider,
  });

  // Subscribe to finalized blocks
  await polkadotApi.rpc.chain.subscribeFinalizedHeads(
    async (lastFinalizedHeader) => {
      const [{ block }, records] = await Promise.all([
        polkadotApi.rpc.chain.getBlock(lastFinalizedHeader.hash),
        (await polkadotApi.at(lastFinalizedHeader.hash)).query.system.events(),
      ]);

      block.extrinsics.forEach((extrinsic, index) => {
        const {
          method: { args, method, section },
        } = extrinsic;

        const isEthereum = section == 'ethereum' && method == 'transact';

        // Gets the transaction object
        const tx = args[0] as any;

        // Convert to the correct Ethereum Transaction format
        const ethereumTx =
          isEthereum &&
          ((tx.isLegacy && tx.asLegacy) ||
            (tx.isEip1559 && tx.asEip1559) ||
            (tx.isEip2930 && tx.asEip2930));

        // Check if the transaction is a transfer
        const isEthereumTransfer =
          ethereumTx &&
          ethereumTx.input.length === 0 &&
          ethereumTx.action.isCall;

        // Retrieve all events for this extrinsic
        const events = records.filter(
          ({ phase }) =>
            phase.isApplyExtrinsic && phase.asApplyExtrinsic.eq(index)
        );

        // This hash will only exist if the transaction was executed through Ethereum.
        let ethereumHash = '';

        if (isEthereum) {
          // Search for Ethereum execution
          events.forEach(({ event }) => {
            if (event.section == 'ethereum' && event.method == 'Executed') {
              ethereumHash = event.data[2].toString();
            }
          });
        }

        // Search if it is a transfer
        events.forEach(({ event }) => {
          if (event.section == 'balances' && event.method == 'Transfer') {
            const from = event.data[0].toString();
            const to = event.data[1].toString();
            const balance = (event.data[2] as any).toBigInt();

            const substrateHash = extrinsic.hash.toString();

            console.log(
              `Transfer from ${from} to ${to} of ${balance} (block #${lastFinalizedHeader.number})`
            );
            console.log(`  - Triggered by extrinsic: ${substrateHash}`);
            if (isEthereum) {
              console.log(
                `  - Ethereum (isTransfer: ${isEthereumTransfer}) hash: ${ethereumHash}`
              );
            }
          }
        });
      });
    }
  );
};

main();

In addition, you can find more sample code snippets related to more specific cases around balance transfers at this GitHub page.

Utility Functions

The Polkadot.js API also includes a number of utility libraries for computing commonly used cryptographic primitives and hash functions.

The following example computes the deterministic transaction hash of a raw Ethereum legacy transaction by first computing its RLP (Recursive Length Prefix) encoding and then hashing the result with keccak256.

import { encode } from '@polkadot/util-rlp';
import { keccakAsHex } from '@polkadot/util-crypto';
import { numberToHex } from '@polkadot/util';

// Set the key type to string
type txType = {
  [key: string]: any;
};

// Define the raw signed transaction
const txData: txType = {
  nonce: numberToHex(1),
  gasPrice: numberToHex(21000000000),
  gasLimit: numberToHex(21000),
  to: '0xc390cC49a32736a58733Cf46bE42f734dD4f53cb',
  value: numberToHex(1000000000000000000),
  data: '',
  v: '0507',
  r: '0x5ab2f48bdc6752191440ce62088b9e42f20215ee4305403579aa2e1eba615ce8',
  s: '0x3b172e53874422756d48b449438407e5478c985680d4aaa39d762fe0d1a11683',
};

// Extract the values to an array
var txDataArray = Object.keys(txData).map(function (key) {
  return txData[key];
});

// Calculate the RLP encoded transaction
var encoded_tx = encode(txDataArray);

// Hash the encoded transaction using keccak256
console.log(keccakAsHex(encoded_tx));

You can check the respective NPM repository page for a list of available methods in the @polkadot/util-crypto library and their descriptions.

The information presented herein has been provided by third parties and is made available solely for general information purposes. Tanssi does not endorse any project listed and described on the Tanssi Doc Website (https://docs.tanssi.network/). Tanssi Foundation does not warrant the accuracy, completeness or usefulness of this information. Any reliance you place on such information is strictly at your own risk. Tanssi Foundation disclaims all liability and responsibility arising from any reliance placed on this information by you or by anyone who may be informed of any of its contents. All statements and/or opinions expressed in these materials are solely the responsibility of the person or entity providing those materials and do not necessarily represent the opinion of Tanssi Foundation. The information should not be construed as professional or financial advice of any kind. Advice from a suitably qualified professional should always be sought in relation to any particular matter or circumstance. The information herein may link to or integrate with other websites operated or content provided by third parties, and such other websites may link to this website. Tanssi Foundation has no control over any such other websites or their content and will have no liability arising out of or related to such websites or their content. The existence of any such link does not constitute an endorsement of such websites, the content of the websites, or the operators of the websites. These links are being provided to you only as a convenience and you release and hold Tanssi Foundation harmless from any and all liability arising from your use of this information or the information provided by any third-party website or service.
Last update: May 10, 2024
| Created: August 15, 2023