Search…
βŒƒK

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.

🧭 Table of contents

πŸ“° Overview

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:
As you will be using XDCPay to interact with our first dApp on XDC Network, you can download XDCPay at:

πŸš€ Project Introduction

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. 1.
    An XRC20 Token called EGT (Egg Tokens);
  2. 2.
    An XRC721 Token called EGGS;
  3. 3.
    An EGT Faucet smart contract, so people can get more EGT and buy new EGGS;
  4. 4.
    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):

Using Truffle

Using Hardhat

Using Remix

XRC20 Egg Token

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
β€Ό
​

XRC721 Egg NFT

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
β€Ό
​

About XRC721 Egg NFT Metadata

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.

EGT Faucet

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;
}
}

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:
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

Migration script using Hardhat

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

πŸ“€ Flattening Solidity files

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.
Solidity Flattening 01

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:
Solidity Flattening 02

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;
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.

πŸ— 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/:
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!

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:
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

// 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

// 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

// 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

Creating Smart Contract Constants File

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",
},
};

Creating Smart Contract Wrappers in React

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

EggTokenWrapper.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;
}
}
}

EggNFTWrapper.ts

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;
}
}
}

FaucetWrapper.ts

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 =