Intermediate

 

Build a Microservice for your dApp

In the world of decentralized applications (dApps), microservices play a crucial role in ensuring the smooth functioning of dApp. A microservice is a small, independent service that performs a specific task and communicates with other services to fulfill a larger objective.

In this tutorial, we will explore the steps required to build a microservice for your dApp on MultiversX, a leading decentralized platform that allows developers to build and deploy dApps with ease. By the end of this tutorial, you will have a better understanding of how to create a microservice, integrate it with your dApp, and deploy it on MultiversX.

So, let’s get started and learn how to build a microservice for your dApp on MultiversX.

Ping Pong Microservice

In this guide we’re going to build a microservice (an API), which is an intermediary layer between the blockchain layer and the app layer. Our app will consume this microservice instead of making requests directly on the blockchain.

Caching

In our guide, the purpose of this microservice is to cache the values that come from the blockchain (e.g. get_time_to_pong), so every subsequent request will get fast results from our microservice.

Transaction processor

We will also invalidate the cache when a pong transaction will be done. This means that the microservice will listen to all the pong transactions on the blockchain that have our smart contract address as the receiver and as soon as one transaction is confirmed, we will invalidate the cache record corresponding to the sender wallet address.

The Microservice

We’re going to use a microservice template based on nestjs, the caching will be done using redis, so the prerequisites for this guide are nodejs, npm, and redis.

We will extend the “Build a dApp in on MultiversX” guide, so let’s build on the existing folder structure and create the microservice into a subfolder of the parent project folder:

Prerequisites

Before we begin, we’ll make sure redis-server is installed and is running on our development server.

sudo apt install redis-server

Optionally, we can daemonize redis-server, so it’ll run in the background.

redis-server --daemonize yes

We want to make sure redis is running, so if we run:

ps aux | grep redis

then, we will have to see a log line like this one:

/usr/bin/redis-server 127.0.0.1:6379

The microservice

Ok, let’s get started with the microservice. First, we’ll clone the template provided by the MultiversX team.

git clone https://github.com/multiversx/mx-ping-pong-sc microservice
cd microservice
ls -l

Let’s take a look at the app structure: config – here we’ll set up the ping pong smart contract address src/crons – transactions processors are defined here src/endpoints – here we will find the code for /ping-pong/time-to-pong/<address> endpoint

Configure the microservice

We’ll find a configuration file specific to every network we want to deploy the microservice on. In our guide we will use the devnet configuration, which will be found here:

~ping-pong/microservice/config/config.devnet.yaml

First, we’re going to configure the redis server url. If we run a redis-server on the same machine (or on our development machine) then we can leave the default value.

Now we’ll move on to the smart contract address. We can find it in our dapp repository. If you don’t have a smart contract deployed on devnet, then we suggest following the previous guide first and then getting back to this step.

Set the contracts.pingPong key with the value for the smart contract address and we’re done with configuring the microservice.

Start the microservice

We’ll install the dependencies using npm

npm install

and then we will start the microservice for the devnet:

npm run start:devnet

Now we have our microservice started on port 3001. Let’s identify its URL. The default url is http://localhost:3001, but if you run the decentralized application on a different machine, then we should use http://<ip>:3001.

Revisit “Your First dApp”

Now it’s time to tell the dApp to use the microservice instead of directly reading the values from the blockchain. First, we will set up the microservice URL in the dApp configuration file src/config.devnet.tsx: We will add:

export const microserviceAddress = "http://<ip>:3001/ping-pong/time-to-pong/";

Next, we want to switch from using the vm query to using our newly created microservice. The request to get the time to pong is done in src/pages/Dashboard/Actions/index.tsx.

We will change vm query code:

React.useEffect(() => {
  const query = new Query({
    address: new Address(contractAddress),
    func: new ContractFunction("getTimeToPong"),
    args: [new AddressValue(new Address(address))],
  });
  dapp.proxy
    .queryContract(query)
    .then(({ returnData }) => {
      const [encoded] = returnData;
      switch (encoded) {
        case undefined:
          setHasPing(true);
          break;
        case "":
          setSecondsLeft(0);
          setHasPing(false);
          break;
        default: {
          const decoded = Buffer.from(encoded, "base64").toString("hex");
          setSecondsLeft(parseInt(decoded, 16));
          setHasPing(false);
          break;
        }
      }
    })
    .catch((err) => {
      console.error("Unable to call VM query", err);
    });
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

into a generic HTTP request (in our example we use axios):

React.useEffect(() => {
  axios
    .get(`${microserviceAddress}${address}`)
    .then(({ data }) => {
      const { status, timeToPong } = data;
      switch (status) {
        case "not_yet_pinged":
          setHasPing(true);
          break;
        case "awaiting_pong":
          setSecondsLeft(timeToPong);
          setHasPing(false);
          break;
      }
    })
    .catch((err) => {
      console.error("Unable to call microservice", err);
    });
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Of course, don’t forget to manage the required imports (axios and the microservice address that we defined previously in the configuration file config.devnet.tsx).

import axios from "axios";
import { contractAddress, microserviceAddress } from "config";

We can now save index.tsx and let’s run the decentralized app one more time.

npm run start

We can now verify that on the dashboard we still have the countdown and the Pong button is shown as it should be. We can refresh the app multiple times and at first, the app will take the value (time to pong in seconds) from the blockchain. This value is then cached and all subsequent queries will read the value from the cache.

You can also find the complete code on our public repository for the dApp in the branch microservice:

https://github.com/multiversx/mx-template-dapp/blob/microservice/src/pages/Dashboard/Actions/index.tsx

Let’s deep dive into the microservice code and explain the 2 basic features we implemented.

We want to minimize the number of requests done directly on the blockchain because of the overhead they incur, so we’ll first read the time to pong from the blockchain, we’ll cache that value and all the subsequent reads will be done from the cache. That value won’t change over time. It will only reset AFTER we pong.

The Cache

So the caching part is done in

ping-pong/microservice/src/endpoints/ping.pong/ping.pong.controller.ts

which uses

ping-pong/microservice/src/endpoints/ping.pong/ping.pong.service.ts

The number of seconds until the user can pong is returned by the function getTimeToPong at line 16 in ping.pong.service.ts.

async getTimeToPong(address: Address): Promise<{ status: string, timeToPong?: number }> {

 

On line 17 we call this.getPongDeadline which, on line 33 will set the returned value in the cache

return await this.cachingService.getOrSetCache(
  `pong:${address}`,
  async () => await this.getPongDeadlineRaw(address),
  Constants.oneMinute() * 10
);

The function this.getPongDeadlineRaw will invoke the only read action on the blockchain, then this.cachingService.getOrSetCache will set it in cache.

The Transaction Processor

After the user clicks the Pong button and performs the pong transaction, we have to invalidate the cache and we will use the transaction processor to identify all the pong transactions on the blockchain that have the receiver set to our smart contract address.

Let’s look at the transaction processor source file here:

~/ping-pong/microservice/src/crons/transaction.processor.cron.ts

On line 23 we’ll implement the async handleNewTransactions() function that has an interesting event: onTransactionsReceived. Whenever new transactions are confirmed on the blockchain, this event will be executed and an array of transactions will be provided as a parameter. We’ll look in that array for a transaction that has the receiver equal to our smart contract address and the data field should be pong (as defined in the smart contract).

if (
  transaction.receiver === this.apiConfigService.getPingPongContract() &&
  transaction.data
) {
  let dataDecoded = Buffer.from(transaction.data, "base64").toString();
  if (["ping", "pong"].includes(dataDecoded)) {
    await this.cachingService.deleteInCache(`pong:${transaction.sender}`);
  }
}

If we find one, we will invalidate the cache data for the key pong:<wallet address>, where we previously stored the time-to-pong value. We will use this.cachingService.deleteInCache function for this.

With this, you complete this workshop successfully!!