Developing dApps on MultiversX with React

Developing dApps on MultiversX with React

If you’ve ever been curious about developing dApps and felt a bit lost in the technical sea, you’re in the right place. Today, we’re diving into the world of dApps on MultiversX using React.

I’ll guide you through the process step-by-step. So, grab your favorite beverage and let’s get started!

Why MultiversX? A Quick Overview

High Scalability

MultiversX is the first truly sharded blockchain, capable of processing up to 100,000 transactions per second (TPS). This means your dApps can handle heavy traffic without breaking a sweat. 🚀

Security

With a secure Proof of Stake consensus mechanism, MultiversX ensures your dApps are robust against attacks. Safety first, right? 🔒

Developer-Friendly Tools

MultiversX offers a suite of SDKs, APIs, and tools that make the development process smoother than ever. Think of it as having a Swiss Army knife for all your blockchain needs. 🛠️

In the words of Vitalik Buterin, “The more scalable and secure a blockchain is, the better the user experience for all.” MultiversX ticks both boxes.

Getting Started with React and MultiversX

Setting Up Your Environment

First things first, let’s set up your environment. The easiest way to get started is by using the MultiversX dApp template. This template provides a pre-configured environment so you can jump straight into building your application without the usual setup headaches.

  1. Clone the Template: Head over to the MultiversX GitHub page and clone the dApp template repository.
  2. Install Dependencies. Run ‘npm install’ to install all necessary packages.
  3. Start Developing

Connecting Your MultiversX Wallet

Integrating a wallet with your dApp is crucial. MultiversX supports various wallet providers like xPortal, Defi Extension, Web Wallet, Ledger, and xAlias.

Soon, Metamask will join the party too. Let’s see how you can integrate these wallets with a few lines of code.

import { ExtensionLoginButton } from '@multiversx/sdk-dapp/UI/extension/ExtensionLoginButton/ExtensionLoginButton';
import { LedgerLoginButton } from '@multiversx/sdk-dapp/UI/ledger/LedgerLoginButton/LedgerLoginButton';
import { WalletConnectLoginButton } from '@multiversx/sdk-dapp/UI/walletConnect/WalletConnectLoginButton/WalletConnectLoginButton';
import { WebWalletLoginButton } from '@multiversx/sdk-dapp/UI/webWallet/WebWalletLoginButton';
import { XaliasLoginButton } from '@multiversx/sdk-dapp/UI/webWallet/XaliasLoginButton/XaliasLoginButton';
import { nativeAuth } from 'config';
import { RouteNamesEnum } from 'localConstants';
import { useNavigate } from 'react-router-dom';
import { AuthRedirectWrapper } from 'wrappers';


export const Unlock = () => {
 const commonProps = {
   callbackRoute: RouteNamesEnum.dashboard,
   nativeAuth
 };


 <AuthRedirectWrapper requireAuth={false}>
    <div className='flex flex-col'>
      <WalletConnectLoginButton
        loginButtonText='xPortal App'
        {...commonProps}
      />
      <LedgerLoginButton loginButtonText='Ledger' {...commonProps} />
      <ExtensionLoginButton loginButtonText='DeFi Wallet' {...commonProps} />
      <XaliasLoginButton
        loginButtonText='xAlias'
        data-testid='xAliasLoginBtn'
        {...commonProps}
      />
      <WebWalletLoginButton {...commonProps} />
    </div>
  </AuthRedirectWrapper>
};        

Reading Data About Your Wallet

Once you’re logged in, the @multiversx/sdk-dapp library populates a Redux store with data about your wallet. You can inspect this data under Inspect -> Application -> Local storage. There are two ways to read the current user state: hooks (for use inside components and reacting to changes) and simple functions (for reading data outside React components or inside handlers).

Hooks

  • useGetLoginInfo
  • useGetAccountInfo
  • useGetNetworkConfig

Functions

  • getAccount
  • getAccountBalance
  • getAccountShard
  • getAddress
  • getIsLoggedIn

  1. Using Hooks

Hooks are perfect for use inside components and reacting to changes. Here’s an example:

import { useGetAccount } from '@multiversx/sdk-dapp/hooks/account/useGetAccount';
import { useGetAccountInfo } from '@multiversx/sdk-dapp/hooks/account/useGetAccountInfo';
import { useGetLoginInfo } from '@multiversx/sdk-dapp/hooks/account/useGetLoginInfo';


const WalletInfo = () => {
 const { isLoggedIn } = useGetLoginInfo();
 const { address, shard } = useGetAccountInfo();
 const { balance, nonce, txCount } = useGetAccount();


 if (!isLoggedIn) {
   return <p>Please connect your wallet</p>;
 }


 return (
   <div>
     <p>Address: {address}</p>
     <p>Balance: {balance}</p>
     <p>Shard: {shard}</p>
     <p>Nonce: {nonce}</p>
     <p>Tx Count: {txCount}</p>
   </div>
 );
};


export default WalletInfo;        

2. Using Functions Outside a Component

If you need to access wallet data outside a React component, functions are the way to go:

import {
 getIsLoggedIn,
 getAddress,
 getAccount,
 getAccountBalance
} from '@multiversx/sdk-dapp/utils';


const fetchWalletData = async () => {
 if (getIsLoggedIn()) {
   const address = await getAddress();
   const account = await getAccount(address);
   const balance = await getAccountBalance(address);
   console.log('Address:', address);
   console.log('Balance:', balance);
   console.log('Account:', account);
 } else {
   console.log('User is not logged in');
 }
};


// Call fetchWalletData when needed
fetchWalletData();        

Smart Contract Interactions

Interacting with MultiversX smart contracts involves understanding their inputs and outputs as defined by their ABI. 

The ABI, usually in JSON format, details the functions (endpoints) of the contract, including their names, inputs, outputs, and mutability (whether the function can modify the contract state or not).

Let’s dive into how to query and send transactions to a smart contract.

1. Querying the Smart Contract

To query a smart contract, you’ll use the ABI to craft the query. Here’s an example of a function from a Lottery contract: 

  {
           "name": "getLotteryInfo",
           "mutability": "readonly",
           "inputs": [
               {
                   "name": "lottery_name",
                   "type": "bytes"
               }
           ],
           "outputs": [
               {
                   "type": "LotteryInfo"
               }
           ]
       },        

2. Generic Utility Function for Queries

Below is a generic utility function that can handle any query. It makes a POST request to the MultiversX gateway using the sdk-core and parses the response using the endpoint definition from the ABI.

import {ContractFunction, ResultsParser, SmartContract, TypedValue
} from '@multiversx/sdk-core/out';
import { ProxyNetworkProvider } from '@multiversx/sdk-network-providers/out';


const resultsParser = new ResultsParser();


const mvxGatewayAddress = 'https://devnet-gateway.multiversx.com';


export const vmQuery = async (
 smartContract: SmartContract,
 endpoint: string,
 args: TypedValue[]
): Promise<TypedValue | undefined> => {
 try {
   const proxy = new ProxyNetworkProvider(mvxGatewayAddress, {
     timeout: 30000
   });


   const query = smartContract.createQuery({
     func: new ContractFunction(endpoint),
     args
   });
   const queryResponse = await proxy.queryContract(query);


   const endpointDefinition = smartContract.getEndpoint(endpoint);


   const { firstValue } = resultsParser.parseQueryResponse(
     queryResponse,
     endpointDefinition
   );
   return firstValue ?? undefined;
 } catch (err: any) {
   throw new Error(`${endpoint} vmQuery failed`, err);
 }
};        

Now we need to create an instance of the SmartContract object by providing it with the contract address on the network and the ABI.

const smartContract = new SmartContract({
 address: new Address(
   'erd1qqqqqqqqqqqqqpgqknhu7pf4l0yt5prkjp40fjc75q5pms8yj03quydsqx'
 ),
 abi: AbiRegistry.create(nftStakingJson)
});        

The last thing we need to do is to call the vmQuery function, my personal preference is to use react-query.

import {
 AbiRegistry,
 Address,
 BytesValue,
 SmartContract
} from '@multiversx/sdk-core/out';
import nftStakingJson from 'contracts/lottery-esdt.abi.json';
import { useQuery } from 'react-query';
import { vmQuery } from './vmQuery';
import { LotteryInfo } from './LotteryInfo';


const endpoint = 'getLotteryInfo';


export const useGetLotteryInfo = (lotteryName: string) => {
 const queryKey = ['lotteryInfo', lotteryName];


 const queryFn = async (): Promise<LotteryInfo> => {
   const value = await vmQuery(smartContract, endpoint, [
     new BytesValue(Buffer.from(lotteryName))
   ]);


   const data = value?.valueOf();
   const lotteryInfo = LotteryInfo.fromResponse(data);


   return lotteryInfo;
 };


 return useQuery<LotteryInfo>(queryKey, queryFn, {
   retry: false,
   refetchOnWindowFocus: false
 });
};        

Notice that we call vmQuery with our smartContract object, the endpoint we want to call - getLotteryInfo, and the args from the endpoint. Since there is only one argument, lottery_name of type bytes, we provide it using the BytesValue from sdk-core. All the types from the ABI have an equivalent in sdk-core (e.g., U64Value, AddressValue, BooleanValue, etc.).

Regarding the data we receive from a query, we can find it in the outputs of the endpoint:

"outputs": [
               {
                   "type": "LotteryInfo"
               }
           ]        

The types are also described in the ABI as follows:

"types": {
       "LotteryInfo": {
           "type": "struct",
           "fields": [
               {
                   "name": "token_identifier",
                   "type": "EgldOrEsdtTokenIdentifier"
               },
               {
                   "name": "ticket_price",
                   "type": "BigUint"
               },
               {
                   "name": "tickets_left",
                   "type": "u32"
               },
               {
                   "name": "deadline",
                   "type": "u64"
               },
               {
                   "name": "max_entries_per_user",
                   "type": "u32"
               },
               {
                   "name": "prize_distribution",
                   "type": "bytes"
               },
               {
                   "name": "prize_pool",
                   "type": "BigUint"
               }
           ]
       },        

The LotteryInfo class in the code helps in structuring this data:

import BigNumber from 'bignumber.js';


export class LotteryInfo {
 deadline!: number;
 maxEntriesPerUser!: number;
 prizeDistribution!: number;
 prizePool!: BigNumber;
 ticketPrice!: BigNumber;
 ticketsLeft!: number;
 tokenIdentifier!: string;


 constructor(init?: Partial<LotteryInfo>) {
   Object.assign(this, init);
 }


 static fromResponse(data: any): LotteryInfo {
   return new LotteryInfo({
     deadline: Number(data.deadline),
     maxEntriesPerUser: Number(data.max_entries_per_user),
     prizeDistribution: data.prize_distribution.toString(),
     prizePool: new BigNumber(data.prize_pool),
     ticketPrice: new BigNumber(data.ticket_price),
     ticketsLeft: Number(data.tickets_left),
     tokenIdentifier: data.token_identifier
   });
 }
}        

This class parses and converts the response from the smart contract query into a structured LotteryInfo object.

Sending Transactions

To change the state of a smart contract, we call mutable functions by sending transactions. Consider the buy_ticket function:

{
           "name": "buy_ticket",
           "mutability": "mutable",
           "payableInTokens": [
               "*"
           ],
           "inputs": [
               {
                   "name": "lottery_name",
                   "type": "bytes"
               }
           ],
           "outputs": []
       },        

This function takes the lottery name as input and is payable in tokens, meaning it accepts any tokens.

Example: Sending a Transaction

import { Address } from '@multiversx/sdk-core/out';
import { useGetAccount } from '@multiversx/sdk-dapp/hooks/account/useGetAccount';
import { sendTransactions } from '@multiversx/sdk-dapp/services/transactions/sendTransactions';
import { getChainID } from '@multiversx/sdk-dapp/utils';
import { smartContract } from './useGetLotteryInfo';
import BigNumber from 'bignumber.js';


export const useBuyTickets = () => {
 const { address } = useGetAccount();


 const displayInfo = {
   processingMessage: 'Processing buy tickets transaction',
   errorMessage:
     'An error has occurred while processing the buy tickets transaction',
   successMessage: 'Buy tickets transaction successful'
 };


 const sendBuyTicketsTransaction = async (ticketPrice: BigNumber) => {
   try {
     const interaction = smartContract.methods
       .buy_ticket(['FirstLottery'])
       .withValue(ticketPrice.toFixed())
       .withSender(new Address(address))
       .withGasLimit(10_000_000)
       .withChainID(getChainID());


     const transaction = interaction.buildTransaction();


     const { sessionId, error } = await sendTransactions({
       transactions: [transaction],
       displayInfo
     });
   } catch (err) {
     console.error(
       `Unable to send buy tickets transaction: ${(err as Error).message}`,
       err
     );
   }
 };


 return { sendBuyTicketsTransaction };
};        

Explanation

  • Build the Transaction: Use the smartContract object and call the method from the ABI with its inputs (lottery_name).
  • Payable in Tokens: Send EGLD using withValue.
  • Set Sender Address: Use withSender to set the user's address.
  • Set Gas Limit: Ensure it’s sufficient to avoid running out of gas.
  • Set Chain ID: Use getChainID to set the appropriate chain ID (‘D’ for devnet, ‘T’ for testnet, and ‘1’ for mainnet).
  • Send Transaction: Call sendTransactions from sdk-dapp with the built transaction.

Monitoring the Transaction Status

We either use the useGetActiveTransactionsStatus hook, but this will monitor all the transactions that we are sending (for example, ‘pending’ will be true if at least one transaction is pending, and ‘success’ is true if all transactions have succeeded).

 const { pending, success, fail } = useGetActiveTransactionsStatus();        

Or we can target a specific transaction by using the useTrackTransactionStatus with the sessionId returned from sendTransactions function.

 const transactionStatus = useTrackTransactionStatus({
   transactionId: sessionId,
 });        

Diving into the MultiversX ecosystem is like stepping into a vast, interconnected web of possibilities. 

As you build your dApp, you’ll find that the platform’s infrastructure is designed to support innovation at every turn. Whether you’re integrating advanced wallet functionalities or experimenting with smart contract capabilities, MultiversX provides a solid foundation that ensures scalability and security without compromising on performance. 

Imagine having the flexibility to create complex, decentralized applications that can handle thousands of transactions per second seamlessly. That’s the power of Adaptive State Sharding at your fingertips.

But it’s not just about the technical prowess; it’s about the community and the resources available to you. Engaging with this community can provide invaluable insights, collaborative opportunities, and support as you navigate the challenges of dApp development. 

If you’re eager to start building on MultiversX, you should definitely check out xAlliance. They are dedicated to supporting projects and builders through various initiatives. With community grants, earning opportunities, and impactful programs, xAlliance is here to ensure that developers have the resources and support they need to bring their innovative ideas to life. 

So, as you start on your project, remember that you’re building more than just an app; you’re contributing to a dynamic ecosystem that’s shaping the future of blockchain.

Plus, we’re here for the builders too! You can book a free 30-minute meeting with our team to discuss your project and get guidance. So, if you’re ready to #buidl. 

Conclusion

Building dApps with React is not just feasible but also enjoyable, thanks to the comprehensive tools and libraries at your disposal. Whether you’re a doxxed developer or just starting out, MultiversX offers a robust platform to bring your decentralized application ideas to life.

Remember, the only limit is your imagination!

To view or add a comment, sign in

More articles by buidly

Others also viewed

Explore content categories