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.