Skip to main content

Build a Microservice for your dApp

Let's build a microservice for your decentralized App

info

This guide extends the Build a dApp in 15 minutes guide, please follow it before following this one. We'll work on the Devnet, so you should create and manage a web wallet here.

This guide has been made available in video format as well:

Ping Pong Microservice

This guide extends the decentralized app we have built in our previous guide Build a dApp in 15 minutes. If you haven't followed it so far, please do it now.

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 "Build a dApp in 15 minutes" 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-service

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 for 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 we followed the previous guide "Build a dApp in 15 minutes"). If you don't have a smart contract deployed on devnet, then we suggest to follow the previous guide first and then get 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 like 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 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.

Conclusion

So that's all, we created a microservice in order to make our dApp faster and scalable. This is a generic decentralized application architecture and most of the examples from this guide were the starting point for some of our highly available and massively used products. Now we provide you a starting point in order to build your ideas and projects.

Where to go next?

Break down this guide and learn more about how to extend the microservice, the dapp and the smart contract. Learn more on the MultiversX official documentation site here: https://docs.multiversx.com.

If you have any questions, feel free to ask them using stackoverflow here: https://stackoverflow.com/questions/tagged/multiversx.

Share this guide if you found it useful.