How to Subscribe to Polkadot Events

This tutorial will show you how to build a basic Polkadash component, how to subscribe to and display Polkadot blockchain events, and how to unsubscribe from listening to those events.

If you haven't already, boot up a copy of Polkadash first as per this guide.

Skill level prerequisites:

  • JavaScript: intermediate
  • VueJS: none / beginner
  • Polkadot: essentials

Creating a Polkadash Component

The fastest way to create a new component is to copy an existing one. Make a copy of the src/pages/BlockNumber.vue component. Replace its contents with this boilerplate code:

<template>
  <div class="content">
    <card>
      <template slot="header">
        <h5 class="title">Events</h5>
        <p class="category">Outputs events from new blocks</p>
      </template>
      <div class="row">
        <div class="col-lg-2 col-md-3 col-sm-4 col-xs-6 col-xs-6">
          Placeholder
        </div>
      </div>
    </card>
  </div>
</template>
<script>
import { Card } from "@/components/index";

import api from "../connection.js";

export default {
  components: {
    Card,
  },
  data() {
    return {
      events: {},
    };
  },
  methods: {
    subscribe() {},
  },
};
</script>
<style></style>

A VueJS component is completely contained in a single file: its styles, JavaScript logic, and HTML template are all in this one file. This makes building and sharing components incredibly easy.

The data of a component is defined with the data function which returns an object of data. In the above code, we defined events as an empty object, and it becomes immediately available to us in our template and our JavaScript code below.

The functionality of a VueJS component is placed into methods, and that's where we placed our subscribe method stub, ready to be filled up with logic.

Before we do that, let's add this component to our UI. We need to modify two files. In src\pages\Layout\DashboardLayout.vue, add a new <sidebar-link> component instance under the existing ones, like so:

      <sidebar-link to="/events">
        <i class="tim-icons icon-bell-55"></i>
        <p>Events</p>
      </sidebar-link>

Note: to find out which icons this dashboard supports out of the box, see this link.

Next, we need to import our new Events.vue component and attach it to a route (we defined the route as /events in the code above). Let's edit src\router.js and add a new route:

// ...

      {
        path: "dashboard",
        name: "Dashboard",
        component: Dashboard,
      },
      { // THIS SECTION IS NEW
        path: "events",
        name: "Events",
        component: () => import("@/pages/Events.vue"),
      },
			
			// ....

If you still have the dashboard running, it should refresh and show you the new option automatically. If not, run it with yarn serve --port=8081 while in the polkadash folder.

Subscribing to Polkadot Events

Polkadash comes with PolkadotJS API built-in, so the infrastructure for interacting with the Substrate chain of our choice (in this case Polkadot, but can be anything Substrate-based) is already there.

Notice how our boilerplate component already imports api from the connection.js file. This api variable is a promise which needs to resolve before becoming usable.

Promises can be scary when you first encounter them, but what it boils down to is that JavaScript became so asynchronous (different commands running at the same time and completing in unpredictable order), people had to come up with ways to force it to become a more standard language again and follow instructions sequentially. Promises help us do that: they make it possible to execute one instruction after another without falling into callback hell.

Let's get to it.

First, we need to tell the component to call our subscribe method as soon as the component is loaded. In VueJS parlance, that's when a component is created, so we'll add that.

  methods: {
    subscribe: async function() {
      let myapi = await api;
      console.log("Done");
    },
  },
  created() {
    this.subscribe();
  },

Tip: created is a lifecycle hook. VueJS has several lifecycle hooks you can use to trigger functions. Read more here.

this in the code above refers to "this component". The async part lets us use the await keyword inside this function, which forces a promise to resolve before continuing. You see this in action with let myapi = await api. It's a conveinent way to make sure our api is instantly usable.

Querying Events

Because the Polkadot ecosystem is still so very young and undocumented, it's not easy or intuitive to find out which RPC and query endpoints we can query with this connection. You could read the JS API docs, but those are incomplete, and you could try wading through the code, but that's not easy. The simplest approach would be to open Polkadot JS Apps UI, and go to either Toolbox, Chain State, or Extrinsics - depending on what you're interested in reading or writing.

Things to remember:

  • Toolbox is for RPC calls, which let you query past data. Block numbers, past balances, past events will all be here. Everything in RPC is read through api.rpc.MODULE.FUNCTION, e.g. api.rpc.system.chain()
  • Chain State lets you read the current state, so only current data. Everything here is read with api.query.MODULE.FUNCTION, so api.query.system.events().
  • Extrinsics let you write to the blockchain - these help you compose "transactions" with different parameters. These are called with api.tx.MODULE.FUNCTION.signAndSend(CALLER), e.g. api.tx.balances.transfer(BOB, 12345).signAndSend(alice);

We're interested in new events as they show up, and that's at Chain State -> system.events. Let's modify our subscribe function.

subscribe: async function() {
      let myapi = await api;

      myapi.query.system.events((events) => {
        console.log(`\nReceived ${events.length} events:`);

        // Loop through the Vec<EventRecord>
        events.forEach((record) => {
          // Extract the phase, event and the event types
          const { event, phase } = record;
          const types = event.typeDef;

          // Show what we are busy with
          console.log(
            `\t${event.section}:${event.method}:: (phase=${phase.toString()})`
          );
          console.log(`\t\t${event.meta.documentation.toString()}`);

          // Loop through each of the parameters, displaying the type and data
          event.data.forEach((data, index) => {
            console.log(`\t\t\t${types[index].type}: ${data.toString()}`);
          });
        });
      });
    },

As soon as we save this, events will start appearing in the JavaScript console of our dashboard.

Read through the code comments to understand what each part does - the Event objects contain plenty of information about the context they occured in.

Tip: to learn about the data structure of the Event object and everything you can access on it, see the Event Type and look into its dependencies.

Displaying Polkadot Events

We'll use the already imported Card component to render each incoming block and all its events. First, let's log which block the events are coming from. We'll edit the subscribe method to be:

    subscribe: async function() {
      let myapi = await api;

      myapi.query.system.events(async (events) => {
        let header = await myapi.rpc.chain.getHeader();
        let blockNumber = header.toJSON().number;

// ...

Notice that we had to add async in front of (events) to enable use of await inside this closure (a.k.a. anonymous function). The async keyword in the function one level above no longer applies - our closure is a separate context and needs its own async.

The structure we want looks something like this:

Our events object we initially defined in our component will have keys that correspond to block numbers, and values which will contain an array of events as strings.

Let's finish our subscribe function.

subscribe: async function() {
      let myapi = await api;

      myapi.query.system.events(async (events) => {
        let header = await myapi.rpc.chain.getHeader();
        let blockNumber = header.toJSON().number;

        let eventArray = [];
        events.forEach((record) => {
          const { event, phase } = record;
          const types = event.typeDef;
          eventArray.push(
            `\t${event.section}:${
              event.method
            }:: (phase=${phase.toString()}): \t\t${event.meta.documentation.toString()}`
          );
        });

        this.$set(this.events, blockNumber, eventArray);
      });
    }

After fetching the block number, we define eventArray as the array to hold our event messages. We then go through each event just like in the console-only version above, but this time we push the messages into this array. Finally, we $set the key of our this.events object to the block number, and the value to eventArray.

Tip: You may be wondering why we're using this.$set rather than just setting a value like this.events[blockNumber] = eventArray. This is because of VueJS' reactivity - VueJS cannot detect when an object updates to change what's shown on screen, so the update is called through a VueJS wrapper function which does this detecting for us.

Finally, let's change the contents of <template> in our component to:

<template>
  <div class="content">
    <card>
      <template slot="header">
        <h5 class="title">Events</h5>
        <p class="category">Outputs events from new blocks</p>
      </template>
      <div class="row">
        <div class="col-lg-12 col-md-3 col-sm-4 col-xs-6 col-xs-6 text-left">
          <card v-for="(event, block) in events">
            {{ block }}
            <p v-for="e in event">
              {{ e }}
            </p>
          </card>
        </div>
      </div>
    </card>
  </div>
</template>

VueJS lets us iterate over objects and their properties with v-for .. in, which is what we're using here. First we loop through the blocks in the list, then we loop through the events in each block and show them.

Looks like someone bonded their tokens to get ready to validate on Polkadot! Exciting!

Unsubscribing from Polkadot Events

It's easy to forget that in single-page-apps, if one doesn't tell the app to unsubscribe from listening to something - be that UI events or websocket changes like we're using above - the app won't stop on its own.

If we navigate to another component, our subscription stays alive, but doesn't do anything other than use bandwidth and CPU cycles. We can check that by adding console.log("Still here"); to our subscribe method and navigating to another tab.

For that reason, cleanup is very important when developing components. This is usually done before the component is destroyed - in beforeDestroy:

// ...
  created() {
    this.subscribe();
  },
  beforeDestroy() {
    this.unsubscribe();
  },

Any API call with Polkadot JS API that results in a subscription is also a Promise which resolves to a function you can call to unsubscribe. In other words, ...query.system.events() will feed the events into any callback we provide, but the result of this Promise will be a callable function which, when called, lets us unsubscribe.

Let's add a new component-level property unsub which will store this callable function for us:

  data() {
    return {
      events: {},
      unsub: null,
    };
  },

Then, let's populate this value once our subscription is established. We'll change the very end of our subscribe function to:

          //.....
          let eventArray = [];
          events.forEach((record) => {
            const { event, phase } = record;
            const types = event.typeDef;
            eventArray.push(
              `\t${event.section}:${
                event.method
              }:: (phase=${phase.toString()}): \t\t${event.meta.documentation.toString()}`
            );
          });
          console.log("Still here");
          this.$set(this.events, blockNumber, eventArray);
        })
        .then((_unsub) => (this.unsub = _unsub)); // <-- THIS IS NEW

As soon as we subscribe to events, our component's unsub property will contain the function to call if we want to unsubscribe. Finally, let's build the unsubscribe function by adding a new method.

    unsubscribe: function() {
      if (this.unsub) {
        this.unsub();
        console.log("Unsubbed");
      }
    },

The function first checks if anything is set in this.unsub. If it is, it means we're subscribed and can call unsubscribe.

We can now fully refresh the page and test this implementation out. Navigate to events, wait for some to load, then navigate away. You should see "Still here" and "Unsubbed" messages in the console.

You can find the full code of this tutorial in the tutorial-events branch of the Polkadash repo.


Remember to subscribe to our newsletter to be kept up to date on new posts and to be kept in the loop about Web 3.0 developments.

Related posts