In this tutorial, you will work with a few different tools to create a working decentralized App front-end from start to finish, leveraging React to bootstrap our front end. Web3Modal and web3.js will be used to create a touchpoint between our front-end and the XDC Network. You'll use the smart contract development environment of your choice: Truffle, Hardhat or Remix!
What you will learn
In this tutorial, you will learn how to interact with XDC Network smart contracts and tokens through a simple web front-end on the XDC Network mainnet and XDC Apothem testnet.
What you will do
Create a simple web front end
Further instructions on how to deploy multiple smart contracts
Interact with smart contracts
Create Interfaces to use XRC20 tokens on a React App
Create Interfaces to use XRC721 tokens on a React App
What you will need
There are a few technical requirements before we start. Please install the following:
This tutorial is full of important concepts and we wanted to make something both interesting and fun, with that in mind, we are happy to introduce EGG GACHA!
EGG GACHA!
Gacha is a Japanese term for a small toy sold in a plastic capsule in a vending machine. And this is what we will make: A NFT vending machine where you can trade in an XRC20 token for a new XRC721 non-fungible token. We will create in the course of this tutorial:
An XRC20 Token called EGT (Egg Tokens);
An XRC721 Token called EGGS;
An EGT Faucet smart contract, so people can get more EGT and buy new EGGS;
An interface where users can interact with EGT, EGGS and the EGT Faucet Smart Contract;
Smart Contracts
It is not uncommon to see several smart contracts interacting in a real-world application to create a complete user experience - and that's what you'll be doing here. You will deploy three smart contracts that depend on one another to create our decentralized app experience.
If you have never deployed a Smart Contract on XDC Mainnet or Apothem Tesnet, please check the following tutorials before continuing (You can chose between using Truffle, Hardhat or Remix):
First, you will abstract some of the code for the EGT tokens using Open Zeppelin's Smart Contract Wizard. If you followed one of the tutorials listed in the Smart Contracts section, you might need to install @openzeppelin/contracts to your working directory first:
npm install @openzeppelin/contracts
Create your EGT tokens by creating an EggToken.sol file with the following content:
This URI is pointing to a pre-defined list of assets created for this tutorial and hosted on IPFS. The full list of URI metadata can be found here: Egg Metadata on IPFS.
You also need to provide users a way to claim a few EGT tokens! The best way to do so is creating a FAUCET smart contract. Our faucet will have a claimTokens() method that users can call to get 50 EGT tokens for free every 24-hours. Create a Faucet.sol contract with the following code:
// SPDX-License-Identifier: MITpragmasolidity ^0.8.4;import"@openzeppelin/contracts/token/ERC20/ERC20.sol";import"@openzeppelin/contracts/access/Ownable.sol";contractFaucetisOwnable { ERC20 public token;structAirdrop {address claimer;uint256 lastTimeClaimed; }eventtokenAirdropped(addressindexed claimer, uint256 claimTime);mapping ( address=> Airdrop ) private tokensDroped;constructor(ERC20_token) {require(address(_token) !=address(0),"Token address can't be address zero"); token = _token; }functiondepositToken(uint256 amount_) public {require(token.transferFrom(msg.sender,address(this), amount_),"Transaction Failed!"); }functionclaimTokens() public {require( currentTime() > tokensDroped[msg.sender].lastTimeClaimed +86400,'User claimed less than 24hrs ago'); Airdrop memory _airdrop =Airdrop({ claimer: msg.sender, lastTimeClaimed:currentTime() }); tokensDroped[msg.sender] = _airdrop;require(token.transfer(msg.sender,50ether),"Token Transfer Failed!");emittokenAirdropped(msg.sender, _airdrop.lastTimeClaimed); }functioncurrentTime() privateviewreturns (uint256) {return block.timestamp; }}
Migration script using Truffle
If you followed the Use Truffle to deploy a Smart Contract tutorial, you might need to adjust your migration script accordingly so all three smart contracts are correctly deployed to the blockchain. For Truffle, you'll need to create a 1_project_migration.js file with the following code:
Conversely, if you followed the Use Hardhat to deploy a Smart Contract tutorial, you need to adjust your deploy.js script to deploy all three contracts:
To Verify and Publish your smart contracts inherited from @openzeppelin/contracts, you'll need to flatten your solidity smart contract into one file. In this section, you will learn how to use the Solidity Visual Developer plugin on VSCode to flatten your smart contracts.
Step 01
On the left-sided panel, click on Extensions (or press CTRL+SHIFT+X);
Seach for Solidity;
Find Solidity Visual Developer and click in install.
Step 02
Go to Explorer on the left-side panel (or press CTRL+SHIFT+E);
Select the .sol file you want to flatten;
On the top of your visual code Editor window, you will see a list of new commands. Find flatten and click on it:
Step 03
Once you click on flatten, a new editor window will open to the right;
Press CTRL+S to Save As and name it {name}-flat.sol or whatever you feel is a good option to keep your folder organized;
Repeat the process to any .sol file that uses import @openzeppelin/contracts;
By the end of this process, your folder should look like this:
🚨Remember to use the FLATTENED .sol files instead of the original file when verifying these contracts on the XDC Block Explorer.
🏗 Building a Front-End Application
We prepared a project scaffold for the Egg Gacha. You can find the project folder Here. Get started by cloning this dApp to your working directory:
git clone https://github.com/menezesphill/egg-gacha-scaffold.git
cd egg-gacha-scaffold
Once you have cloned your dApp scaffold, you can install all the necessary dependencies. You can either use yarn or npm, but in this example we use npm:
npm install
When npm finishes installing your dependencies, you can run the start script to see if everything is working:
npm run start
You should see the following React App served at http://localhost:3000/:
In this project folder, you will find a contexts folder with all methods necessary to connect to your XDCPay wallet. If you are not sure where this code is coming from or how to use it, please check the XDCPay Integration Tutorial before continuing.
We are ready to move to the next steps if you see the page above!
Creating Smart Contract Instances in React
You will start by creating your smart contract instances in React. Create a blockchain folder locally, and a sub-folder called contracts:
mkdir -p ./src/blockchain/contracts
The only files you need to import from Truffle and Hardhat (whichever you decided to use for development), are the compiled artifacts of our Smart Contracts. These artifacts can be found at:
// On your Truffle project folder:'./build/contracts/EggToken.json''./build/contracts/EggNFT.json''./build/contracts/Faucet.json'// On your Hardhat project folder:'./artifacts/contracts/Eggs.sol/EggNFT.json''./artifacts/contracts/EggToken.sol/EggToken.json''./artifacts/contracts/TokenFaucet.sol/Faucet.json'
Move or copy these three .json files to our recently created ./src/blockchain/contracts folder. Our working directory should now look like this:
You'll need to install two new dependencies to your project, web3-utils and web3-eth-contract:
npm install web3-utils web3-eth-contract
Create a generic contract handler in the ./src/blockchain/contracts folder:
This way, you can your EggToken, EggNFT and Faucet contracts inherit from Contract.ts. You'll create three new files in the ./src/blockchain/contracts folder:
The next step is to create Wrappers, where you'll define what kind of methods you want to access on the blockchain. You will create one for each contract:
You won't need to use all methods nor access all variables available in EggToken.sol, so you will only create the balanceOf(), approve(), and allowance() methods in your EggTokenWrapper.ts file:
In EggNFTWrapper.ts, you will declare your buyEgg() method, which is probably the most important method, and a few other methods to help you display your collection in the front-end application like:balanceOf(), tokenOfOwnerByIndex(), and tokenURI():
You are almost there! Next, you'll want to create a Blockchain Context so that you can access our blockchain methods throughout our React app. Right now, it might sound a bit exhausting to go through all these files, but trust us, in a real-world application, you will be glad you have created such a nice-looking and well-sectioned React project.
If you feel like grabbing a coffee, go ahead, I'll be here waiting for you...
☕
...and continuing, it is now time to create a BlockchainProvider.tsx into our contexts folder:
touch ./src/contexts/BlockchainProvider.tsx
There, we will create our BlockchainContext by writting:
This way, whenever your account address, chainId, or web3 provider changes, all contracts are updated accordingly throughout your React App, and you are also capable of tracking your EGGS NFTs images to show on the front-end.
Integrating Blockchain Methods to DOM elements
Now you'll need to update our index.tsx file in the src with your newly created provider. Your index.tsx should look like this:
import ReactDOM from"react-dom/client";import Web3ModalProvider from"./contexts/Web3ModalProvider";import BlockchainProvider from"./contexts/BlockchainProvider"; // Importing new Providerimport App from"./app";constroot=ReactDOM.createRoot(document.getElementById("root") asHTMLElement);root.render( <Web3ModalProvider> <BlockchainProvider> // Add it to our provider list <App /> </BlockchainProvider> </Web3ModalProvider>);
Updating app.tsx elements
Next, move to your main App file at ./src/app.tsx and make some changes:
import React, { useState, useCallback, useEffect } from"react";import"./app.css";import { Web3ModalContext } from"./contexts/Web3ModalProvider";// You will start by importing your newly created BlockchainContext to app.tsx:import { BlockchainContext } from"./contexts/BlockchainProvider";constApp:React.FC= () => {// You are not using this state anymore, so you can remove it.// Instead we will use the EGGS state from our BlockchainContext./* const EGTS = [ { image: "blueEgg" }, { image: "brownEgg" }, { image: "cyanRedEgg" }, { image: "darkEgg" }, { image: "fullGreenEgg" }, { image: "orangeEgg" }, { image: "whiteBlackEgg" }, { image: "whiteGreenEgg" }, { image: "yellowStripedEgg" }, ]; */const [slide,setSlide] =useState(0);// Here, you'll want to import from Web3ModalContext 'web3' and 'chainId'// besides 'account', 'connect' and 'disconnect'const { web3,account,connect,disconnect,chainId } =React.useContext(Web3ModalContext);// And you will import from BlockchainContext the 'EGGS' state and your // contract wrappers:const { eggNFT: eggNFTWrapper, eggToken: eggTokenWrapper, faucet: faucetWrapper,EGGS, } =React.useContext(BlockchainContext);// You'll need to create your balance states and gachaAllowance state:const [egtTokenBalance,setEgtTokenBalance] =useState("");const [egtNftBalance,setEgtNftBalance] =useState("");const [gachaAllowance,setGachaAllowance] =useState("");// A getBalance function that will get our EGT and EGG token balancesconstgetBalances=async () => {if (web3 && account && chainId) {const_egtBalance=awaiteggTokenWrapper?.balanceOf();const_eggBalance=awaiteggNFTWrapper?.balanceOf();setEgtTokenBalance(String(Number(_egtBalance) /10**18) ||"0");setEgtNftBalance(String(_eggBalance) ||"0"); } };// And a getGachaAllowance function to check whether// the EggNFT contract is allowed to spend your EGT tokensconstgetGachaAllowance=async () => {if (web3 && account && chainId) {const_gachaAllowance=awaiteggTokenWrapper?.allowance();setGachaAllowance(String(Number(_gachaAllowance) /10**18) ||"0"); } };// This useEffect will update your balances and allowance// so you can update our UIuseEffect(() => {getBalances();getGachaAllowance(); });// This function handles the DROP ME MORE EGT! button clicksconsthandleDrop= () => {if (web3 && account && chainId) { faucetWrapper?.claimTokens().then(() => {alert("Claimed 50 EGTS!"); }).then(() => {window.location.reload(); }).catch((err) => {alert(`Error: ${err.message}`); }); } };// This function handles the MINT NEW EGG! button clicksconsthandleBuyEgg= () => {if (web3 && account && chainId) { eggNFTWrapper?.buyEgg().then(() => {alert("Minted Egg!"); }).then(() => {window.location.reload(); }).catch((err) => {alert(`Error: ${err.message}`); }); } };// This function handles the APPROVE GACHA! button clicksconsthandleApprove= () => {if (web3 && account && chainId) { eggTokenWrapper?.approve().then(() => {alert("Approved!"); }).then(() => {window.location.reload(); }).catch((err) => {alert(`Error: ${err.message}`); }); } };// <=== This section of the code is pretty much left unchanged ===>consthandleConnectWallet=useCallback(() => {connect(); }, [connect]);consthandleDisconnectWallet=useCallback(() => {disconnect(); }, [disconnect]);functionellipseAddress(address:string="", width:number=4):string {return`xdc${address.slice(2, width +2)}...${address.slice(-width)}`; }// <===============================================================>return ( <main> <divclassName="background"> <divclassName="decoration"> <imgsrc="/images/decorator.svg"alt="Decoration" /> </div> <divclassName="dragon"> <imgsrc="/images/dragon.webp"alt="Dragon" /> </div> </div>