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.
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
Functions
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"
}
]
},
Recommended by LinkedIn
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
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!