React is a JavaScript library for building user interfaces. In this writing, I will walk you through how to build a simple front-end decentralized application. That will help you understand the actual implementation of Web3 in React and know how Front-end developers manage states and interact with data from smart contracts.
Set up and Install Libraries
To set up and install the libraries, follow the steps below:
Step 1. First, we need to create a repository on our GitHub account.
Step 2. Create a new react application. In this tutorial, we will use TypeScript.
npx create-react-app decentralized-app --template typescript
Step 3. Now we move to our working folder.
Now we will push our local directory to the GitHub repository above by using the following git command.
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/HoangMinhBK/decentralized-app.git
git push -u origin main
Now our repository is ready.
Let's add some more essential libraries we need for our project.
npm i @reduxjs/toolkit
npm i react-redux
npm i big-number
npm i web3
Creating a Decentralized Application
Front-end developers using ReactJS are familiar with the concept of fetching some data from a server through APIs and displaying it on the screen. In the decentralized application, the idea is pretty similar. Specifically, We communicate with smart contracts via an ABI file which lets you know what functions the contract contains and how data is read and returned.
The decentralized application needs to get information on the address and wallet balance. Moreover, it should be able to keep track of the connection status to update the display result corresponding to the user's actions, such as switching accounts, disconnecting, or reconnecting their wallet from our website.
Before we start, you need to download an ABI contract file and put it in your previously created "src" folder. The link to the ABI file: https://github.com/HoangMinhBK/decentralized-app/blob/main/src/BEP20_ABI.json
Connecting the wallet with EthereumJs
src/MyWallet.tsx
const address = useSelector((state: any) => state.wallet.address);
const tokenSymbol = useSelector((state: any) => state.wallet.symbol);
const tokenBalance = useSelector((state: any) => state.wallet.balance);
const connectionStatus = useSelector(
(state: any) => state.wallet.connectionStatus
);
const handleConnectWallet = async () => {
try {
const res = await (window as any).ethereum.request({
method: "eth_requestAccounts",
});
dispatch(connectWallet(res));
} catch (error) {
console.error("Some errors occurred!");
}
};
handleConnectWallet():
In this function, first, we prompt a request to connect to the Metamask by using window.ethereum.request({method: "eth_requestAccounts"}). This function triggers Metamask pop-up windows and requests us to sign in and grant access to our website. Then returns an array in which the first element is the currently active account in our wallet.
const handleDisconnectWallet = () => {
dispatch(disconnectWallet());
};
handleDisconnectWallet():
In this function, we reset the state inside the redux store to the initial state. However, this only works when we disconnect the wallet from the web app (Click the "Disconnect Wallet" button on the web UI). If we want to disconnect the wallet directly on Metamask and our app can recognize that action to update the UI, we have to use another function.
const handleAccountsList = async (addressList: Array<string>) => {
if (addressList.length === 0) {
handleDisconnectWallet();
} else handleConnectWallet();
};
useEffect(() => {
(window as any).ethereum.on("accountsChanged", handleAccountsList);
}, []);
useEffect(() => {
(window as any).ethereum.on("accountsChanged", handleAccountsList);
}, []);
handleAccountsList():
As mentioned above, we need a function to handle the disconnection from the wallet and switch accounts.
ethereum.on('accountsChanged', (addressList) => {
] // some functions
"addressList" is an array of 1 account address in your wallet connected to the site and is currently active. The array will automatically update when you switch to another account in the connected ones and only be empty when there are no accounts connected. In my code, this array is passed to handleAccountsList() function. If the addressList is empty, then we call handleDisconnectWallet(), otherwise, we take the first element in the array and pass it to handleConnectWallet() to switch to that account.
Getting the information of the contract using Web3.Js
src/constants.ts
export const CONNECTION_STATUS = {
disconnected: "Not connected",
connected: "Connected",
};
export const CONTRACT_ADDRESS = "0x4abef176f22b9a71b45ddc6c4a115095d8761b37";
export const PROVIDER_URL = "https://data-seed-prebsc-1-s1.binance.org:8545/";
src/contract.ts
import abi from "./BEP20_ABI.json";
import { CONTRACT_ADDRESS, PROVIDER_URL} from "./constants";
const Web3 = require("web3");
const web3 = new Web3(PROVIDER_URL);
const contract = new web3.eth.Contract(abi, CONTRACT_ADDRESS);
Creates a new contract instance with all its methods and events defined in abi file.
Parameters:
- Abi: The json interface for the contract to instantiate.
- Address: The address of the smart contract to call.
Returns:
- The contract instance with all its methods and events.
For detailed information, please visit the documentation of Web3js contract.
How can we know what methods the contract provides? We can use the mentioned ABI file to extract them. To understand more about ABI, click here.
In the ABI file, we have a function called "balanceOf" to get the amount of the contract token of an account address. That requires an account address as input and returns the corresponding balance.
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
Knowing the contract method, we can construct the callback function getTokenBalance as follows. Similarly, according to the ABI, we can create any functions we want(like getTokenSymbol).
export const getTokenBalance = (accountAddress: string) =>
contract.methods
.balanceOf(accountAddress)
.call((err: any, result: any) => result);
export const getTokenSymbol = () =>
contract.methods.symbol().call((err: any, result: any) => result);
export default contract;
You can have two or more token addresses and corresponding contracts, depending on how many types of tokens you want to show on your website. For example, I have 9990 TRAVA and 188 DAI in my Metamask wallet, and I want to show their figure on my web app. I need to create TRAVA and DAI contracts. In this tutorial, for simplicity, I only show the data of TRAVA tokens in my wallet.
src/redux/walletSlice.ts
interface Wallet {
connectionStatus: string;
address: string | undefined;
balance: number | undefined;
symbol: string | undefined;
}
const initialState = {
address: undefined,
balance: undefined,
connectionStatus: CONNECTION_STATUS.disconnected,
symbol: undefined,
} as Wallet;
Let's define the initial state when we first launch our app. Address, balance, and symbol are undefined, and the connectionStatus is "Not connected".
const walletSlice = createSlice({
name: "wallet",
initialState: initialState,
reducers: {
disconnectWallet(state) {
state.address = initialState.address;
state.symbol = initialState.symbol;
state.connectionStatus = initialState.connectionStatus;
state.balance = initialState.balance;
},
connectWallet(state, action) {
state.address = action.payload[0];
state.connectionStatus = CONNECTION_STATUS.connected;
},
getTokenSymbolAndBalance(state, action) {
state.balance = action.payload.balance;
state.symbol = action.payload.symbol;
},
},
});
export const { disconnectWallet, connectWallet, getTokenSymbolAndBalance } = walletSlice.actions;
export default walletSlice.reducer;
Here we have three actions, namely disconnectWallet, connectWallet, and getTokenSymbolAndBalance. These actions are used to dispatch to store the data we get from the smart contract.
const handleGetTokenBalanceAndSymbol = async (address: string) => {
try {
const symbol = await getTokenSymbol();
const balance = await getTokenBalance(address);
const formattedBalance = new BigNumber(balance).div(10**18).toNumber();
dispatch(
getTokenSymbolAndBalance({
symbol: symbol,
balance: formattedBalance,
})
);
} catch (err) {
console.error("Failed to get token symbol and balance");
}
};
Finally, we want to get the wallet's symbol and balance. For the symbol, simply call the getTokenSymbol() in src/contract.ts and dispatch the returned result in the Redux store.
A similar procedure is seen to get the balance, but the returned number is abnormally significant. That is because smart contracts represent a fixed number by multiplying it with 10^n with n, the number of digits after the comma. For example, if I want to represent the float number 987.45 in a smart contract with decimal n = 5, that number will be 98745000. With BEP20 contracts, as provided earlier in this post, their decimal is 18.
That means when we get the balance from the contract, we need to divide it by the contract's decimal using Bignumber.js react library. In fact, it's quite unnormal for somebody to see that they have 99999000000000000000000 ETH in their pocket due to the mistake of the developer.
Our app is working for now!
Extended features: Search for information on the BEP20 contract
In this section, we create a form that allows users to prompt a BEP20 smart contract to see some of its information. With the understanding of smart contracts above, you can easily create one. You can see the code on my GitHub repository for more details.
src/Explorer.tsx
You can visit the app's source code at: https://github.com/HoangMinhBK/decentralized-app
Conclusion
With the rapid expansion and development of decentralized architecture and blockchain, data is now distributed and can be collected by conventional APIs from centralized servers and various network nodes. From this tutorial, I hope you can have an overview of how to build a simple decentralized application with ReactJS and how front-end clients interact with smart contracts.
Reference
[1] Web3.eth.Contract, web3js.readthedocs.io, accessed 19th October 2022.
[2] What is an ABI of a Smart Contract? Examples and Usage, alchemy.com, accessed 19th October 2022.
[3] Ethereum Provider API, docs.metamask.io, accessed 19th October 2022.
[4] Nguyen Luu Hoang Minh GitHub, github.com/HoangMinhBK, accessed 19th October 2022.