How-To Create a React App to Interact with XRC20 and XRC721 Tokens
Creating a front-end for an XDC decentralized app and connecting it to XDC smart contracts.
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!
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.
- 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
There are a few technical requirements before we start. Please install the following:
As you will be using XDCPay to interact with our first dApp on XDC Network, you can download XDCPay at:
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!
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:
- 1.An XRC20 Token called
EGT
(Egg Tokens); - 2.An XRC721 Token called
EGGS
; - 3.An
EGT Faucet
smart contract, so people can get moreEGT
and buy newEGGS
; - 4.An interface where users can interact with
EGT
,EGGS
and theEGT Faucet
Smart Contract;
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:// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract EggToken is ERC20, Ownable {
constructor() ERC20("Egg Token", "EGT") {
_mint(msg.sender, 50000 * 10 ** decimals());
}
}
IMPORTANT: the contract above needs to be flattened to get verified on the block explorer
‼
‼
Following the same logic as above, you will ceate an
Eggs.sol
file using a Open Zeppelin's smart contracts with a few small changes:// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract EggNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
ERC20 public paymentCurrency;
constructor(ERC20 _paymentCurrency) ERC721("Egg NFT", "EGGS") {
require(address(_paymentCurrency) != address(0), "Token address can't be address zero");
paymentCurrency = _paymentCurrency;
}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
function buyEgg() public {
require(paymentCurrency.transferFrom(msg.sender, address(this), 10 ether), "Failed to process payment!");
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId,
string(
abi.encodePacked(
"https://gateway.pinata.cloud/ipfs/QmYDm8Bzye4RMS7h9HUv1KoupajqXcsfKUWwMeGvsC3ZkA/eggo00",
Strings.toString(tokenId),
".json"
)
)
);
}
// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(address from, address to, uint256 tokenId)
internal
override(ERC721, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId);
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
require(
_exists(tokenId),
"ERC721Metadata: URI query for nonexistent token"
);
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
IMPORTANT: the contract above needs to be flattened to get verified on the block explorer
‼
‼
In the section above, we define the NFT metadata inside the
buyEgg()
method: _setTokenURI(tokenId,
string(
abi.encodePacked(
"https://gateway.pinata.cloud/ipfs/QmYDm8Bzye4RMS7h9HUv1KoupajqXcsfKUWwMeGvsC3ZkA/eggo00",
Strings.toString(tokenId),
".json"
)
)
);
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.
Publishing files to IPFS is not within the scope of this tutorial, but if you want to know more, check out This tutorial on how to create NFTs and publish metadata to IPFS using Pinata.
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: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract Faucet is Ownable {
ERC20 public token;
struct Airdrop {
address claimer;
uint256 lastTimeClaimed;
}
event tokenAirdropped(address indexed 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;
}
function depositToken(uint256 amount_) public {
require(token.transferFrom(msg.sender, address(this), amount_), "Transaction Failed!");
}
function claimTokens() 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, 50 ether), "Token Transfer Failed!");
emit tokenAirdropped(msg.sender, _airdrop.lastTimeClaimed);
}
function currentTime() private view returns (uint256) {
return block.timestamp;
}
}
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:const EGT = artifacts.require("EggToken");
const EGGS = artifacts.require("EggNFT");
const Faucet = artifacts.require("Faucet");
module.exports = function (deployer) {
deployer.deploy(EGT)
.then(() => deployer.deploy(EGGS, EGT.address))
.then(() => deployer.deploy(Faucet, EGT.address));
}
And your folder should look like this:

Truffle Workspace
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:async function main() {
// Deploy EggToken
const EggToken = await ethers.getContractFactory("EggToken");
const eggToken = await EggToken.deploy();
await eggToken.deployed();
console.log("EggToken deployed to:", eggToken.address);
// Deploy EggNFT
const EggNFT = await ethers.getContractFactory("EggNFT");
const eggNFT = await EggNFT.deploy(eggToken.address);
await eggNFT.deployed();
console.log("EggNFT deployed to:", eggNFT.address);
// Deploy Faucet
const Faucet = await ethers.getContractFactory("Faucet");
const faucet = await Faucet.deploy(eggToken.address);
await faucet.deployed();
console.log("Faucet deployed to:", faucet.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
And your folder should look like this:

Hardhat Workspace
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.- On the left-sided panel, click on
Extensions
(or pressCTRL
+SHIFT
+X
); - Seach for
Solidity
; - Find
Solidity Visual Developer
and click in install.

Solidity Flattening 01
- Go to
Explorer
on the left-side panel (or pressCTRL
+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:

Solidity Flattening 02
- 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 usesimport @openzeppelin/contracts
;

Solidity Flattening 03
By the end of this process, your folder should look like this:

Solidity Flattening 04
Remember to use the FLATTENED
🚨
.sol
files instead of the original file when verifying these contracts on the XDC Block Explorer.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/
:
Egg Gacha Front-end
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!
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:
Egg Gacha Folder 01
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:touch ./src/blockchain/contracts/Contract.ts
With the following code:
import Web3 from "web3";
import { AbiItem } from 'web3-utils';
import { Contract as Web3Contract } from "web3-eth-contract";
class Contract {
web3: Web3;
chainId: number;
account: string | null;
tag: string | null;
events: object;
contract: Web3Contract;
constructor(options, tag: string, abi, address: string) {
this.web3 = options.web3;
this.chainId = options.chainId;
this.account = options.account;
this.contract = new this.web3.eth.Contract(abi as AbiItem[], address);
if (tag) this.tag = tag;
else this.tag = "contract-" + Date.now();
this.events = {};
}
call(method, ...params) {
return new Promise((resolve, reject) => {
this.contract.methods[method](...params).call({from: this.account})
.then(resolve)
.catch(reject)
});
}
send(method, options, ...params) {
return new Promise((resolve, reject) => {
this.contract.methods[method](...params).send({...options, from: this.account})
.then(resolve)
.catch(reject)
});
}
on(event, callback, onerr) {
if (this.events[event])
return;
this.contract.events[event]((err, res) => {
if (err === null) {
callback(res.returnValues, this.tag);
} else {
if (onerr) onerr(err);
else console.log(err);
}
});
this.events[event] = true;
}
}
export default Contract;
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:touch ./src/blockchain/contracts/EggToken.ts
touch ./src/blockchain/contracts/EggNFT.ts
touch ./src/blockchain/contracts/Faucet.ts
And each one of these files extends
Contract
:// EggToken.ts
import Contract from "./Contract";
import Artifacts from "./EggToken.json";
class EggToken extends Contract {
constructor(options, address) {
super(options, "EggToken", Artifacts["abi"], address);
}
}
export default EggToken;
// EggNFT.ts
import Contract from "./Contract";
import Artifacts from "./EggNFT.json";
class EggNFT extends Contract {
constructor(options, address) {
super(options, "EggNFT", Artifacts["abi"], address);
}
}
export default EggNFT;
// Faucet.ts
import Contract from "./Contract";
import Artifacts from "./Faucet.json";
class Faucet extends Contract {
constructor(options, address) {
super(options, "Faucet", Artifacts["abi"], address);
}
}
export default Faucet;
At this point, your project folder should look like this:

Egg Gacha Folder 02
At this point, if you still haven't deployed the contracts, remember to check the scripts provided in Migration script using Truffle or Migration script using Hardhat. In this instance, we will show you how to deploy them using
Truffle
:truffle migrate --network apothem
If migrations complete sucessfully, you can run
truffle networks
to get your contract addresses:Network: apothem (id: 51)
EggNFT: 0xDfe0F690Bb0F03b62D0350cc34B8195EdDa85134
EggToken: 0x8544C3568Fd88BC256eef824C5232fB12fAd2F69
Faucet: 0x71e9774B1c70202f072326759B55c9c2a9C46E0b
Network: xinfin (id: 50)
No contracts deployed.
To keep your React dApp folder organized, you will create a
constants.ts
file with your deployment information:touch ./src/blockchain/constants.ts
Our
constants.ts
file should have the following exports:export const EggTokenAddress = {
Contract: {
51: "0x8544C3568Fd88BC256eef824C5232fB12fAd2F69",
},
};
export const EggNFTAddress = {
Contract: {
51: "0xDfe0F690Bb0F03b62D0350cc34B8195EdDa85134",
},
};
export const FaucetAddress = {
Contract: {
51: "0x71e9774B1c70202f072326759B55c9c2a9C46E0b",
},
};
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:
touch ./src/blockchain/EggTokenWrapper.ts
touch ./src/blockchain/EggNFTWrapper.ts
touch ./src/blockchain/FaucetWrapper.ts
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:// EggTokenWrapper.ts
import Web3 from 'web3';
import EggToken from './contracts/EggToken';
import { EggTokenAddress, EggNFTAddress } from './constants';
export default class EggTokenWrapper {
web3: Web3;
chainId: number;
account: string;
wrapperOptions: any;
Contract: EggToken;
constructor(web3, chainId, account, options = {}) {
this.web3 = web3;
this.chainId = chainId;
this.account = account;
this.wrapperOptions = {
web3, chainId, account, ...options
}
this.Contract = new EggToken(this.wrapperOptions, EggTokenAddress.Contract[this.chainId]);
}
async balanceOf() : Promise<unknown> {
try {
const balance = await this.Contract.call("balanceOf", this.account);
return balance;
} catch (error) {
throw error;
}
}
async approve() {
const value = '115792089237316195423570985008687907853269984665640564039457584007913129639935'; //(2^256 - 1 )
try {
const tx = await this.Contract.send("approve", {from: this.account}, EggNFTAddress.Contract[this.chainId], value);
console.log(tx);
} catch (error) {
throw error;
}
}
async allowance() : Promise<unknown> {
try {
const allowance = await this.Contract.call("allowance", this.account, EggNFTAddress.Contract[this.chainId]);
return allowance;
} catch (error) {
throw error;
}
}
}
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()
:import Web3 from 'web3';
import EggNFT from './contracts/EggNFT';
import { EggNFTAddress } from './constants';
export default class EggNFTWrapper {
web3: Web3;
chainId: number;
account: string;
wrapperOptions: any;
Contract: EggNFT;
constructor(web3, chainId, account, options = {}) {
this.web3 = web3;
this.chainId = chainId;
this.account = account;
this.wrapperOptions = {
web3, chainId, account, ...options
}
this.Contract = new EggNFT(this.wrapperOptions, EggNFTAddress.Contract[this.chainId]);
}
async balanceOf() : Promise<unknown> {
try {
const balance = await this.Contract.call("balanceOf", this.account);
return balance;
} catch (error) {
throw error;
}
}
async buyEgg() : Promise<unknown> {
try {
const tx = await this.Contract.send("buyEgg", { from: this.account });
return tx;
} catch (error) {
throw error;
}
}
async tokenOfOwnerByIndex(index: number) : Promise<unknown> {
try {
const tokenId = await this.Contract.call("tokenOfOwnerByIndex", this.account, index);
return tokenId;
} catch (error) {
throw error;
}
}
async tokenURI(tokenId: number) : Promise<unknown> {
try {
const tokenURI = await this.Contract.call("tokenURI", tokenId);
return tokenURI;
} catch (error) {
throw error;
}
}
}
Our
FaucetWrapper.ts
is the simpliest of the three contracts. You should only care about the claimTokens()
method in your dApp:import Web3 from 'web3';
import Faucet from './contracts/Faucet';
import { FaucetAddress } from './constants';
export default class Faucetrapper {
web3: Web3;
chainId: number;
account: string;
wrapperOptions: any;
Contract: Faucet;
constructor(web3, chainId, account, options = {}) {
this.web3 = web3;
this.chainId = chainId;
this.account = account;
this.wrapperOptions = {
web3, chainId, account, ...options
}
this.Contract = new Faucet(this.wrapperOptions, FaucetAddress.Contract[this.chainId]);
}
async claimTokens() {
try {
const tx = await this.Contract.send("claimTokens", {from: this.account});
console.log(tx);
} catch (error) {
throw error;
}
}
}
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.