DEV Community

fangjun
fangjun

Posted on • Updated on

Web3 Tutorial: build DApp with Hardhat, React and Ethers.js

In this tutorial, we will build DAPP using Hardhat, React and Ethers.js which works with user-controlled wallets like MetaMask.

DAPP is usually composed of three parts:

  • Smart contract deployed on chain
  • Webapp (user interface) built with Node.js, React and Next.js
  • Wallet (user-controlled in browser / mobile wallet App)

We use Ethers.js to connect these parts.

DApp explained

In the user interface of a DApp (webapp), wallets such as MetaMask gives developers a provider of Ethereum which we can use in Ethers.js to interact with blockchain. (To be specific, what wallet provide is a "connector", Ethers.js create "provider" and/or "signer" for us to use.)

We may have known the usage of MetaMask as a user, and we will learn how to use MetaMask and the window.ethereum it injects into the browser as a developer (MetaMask developer docs).

You can find the code repos for this tutorial:
Hardhat project: https://github.com/fjun99/chain-tutorial-hardhat-starter
Webapp project: https://github.com/fjun99/web3app-tutorial-using-ethers

You may find the very first how-to guide for DApp developers is useful for your web3 journey: Tutorial : build your first DAPP with Remix and Etherscan(by Fangjun).

Special thanks: When preparing the webapp codebase, I learned a lot for Wesley's Proof-of-Competence, POC project. We also use Chakra UI just as his project. You may also find the the web page is almost the same as POC.


Prerequisite Knowledge and tools

You need basic knowledge and tools before we start.

Knowledge:

  • Blockchain
  • Ethereum
  • Wallet
  • Solidity
  • ERC20 & ERC721
  • Ethers.js

Tools:

  • MetaMask (Wallet browser extension)
  • Node.js, yarn, TypeScript
  • OpenZeppelin (Solidity library)
  • Etherscan block explorer

Let's start to build a DApp.


TASK #1

Task 1: setup development environment

To build a DApp, we are doing two kinds of jobs:

  • build smart contract using Hardhat and Solidity
  • build web app using Node.js, React and Next.js

We will organize our directory in two sub-directory chain and webapp.

- hhproject
  - chain (working dir for hardhat)
    - contracts
    - test
    - scripts
  - webapp (working dir for NextJS app)
    - src
      - pages
      - components  
Enter fullscreen mode Exit fullscreen mode

Task 1.1 Install Hardhat and init a Hardhat project

Install Hardhat which is an Ethereum development environment.

To use hardhat, you need to have node.js and yarn in your computer.

  • STEP 1: make a directory and install Hardhat in it
mkdir hhproject && cd hhproject
mkdir chain && cd chain
yarn init -y
Enter fullscreen mode Exit fullscreen mode

Install Hardhat:

yarn add hardhat
Enter fullscreen mode Exit fullscreen mode
  • STEP 2: create a sample Hardhat project
yarn hardhat
//choose: Create an advanced sample project that uses TypeScript
Enter fullscreen mode Exit fullscreen mode

A hardhat project is created with a sample smart contract Greeter.sol we will use in Task 3.

  • STEP 3: run Hardhat Network (local testnet)
yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

A local testnet will is running (chainId: 31337):

Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

It provides 20 accounts each has 10000.0 test ETH:

Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
...
Enter fullscreen mode Exit fullscreen mode

Please note that there are two modes of Hardhat Network local testnet: in-process and stand-alone. We run a stand-alone testnet with command line yarn hardhat node. When running command line like yarn hardhat compile without network parameters(--network localhost), we run an in-process testnet.

Task 1.2: Development Circle in Hardhat

We will go through the development circle of smart contract in Hardhat development environment.

There are sample smart contract, test scripts and deploy scripts in the project initiated by Hardhat.

├── contracts
│   └── Greeter.sol
├── scripts
│   └── deploy.ts
├── test
│   └── index.ts
├── hardhat.config.ts
Enter fullscreen mode Exit fullscreen mode

I would like to change the file names of test and deployment script.

- contracts
  - Greeter.sol
- test
  - Greeter.test.ts (<-index.ts)
- scripts
  - deploy_greeter.ts (<-deploy.ts)
Enter fullscreen mode Exit fullscreen mode

STEP 1: Run command to show accounts:

yarn hardhat accounts
Enter fullscreen mode Exit fullscreen mode

This is the sample hardhat task added in the hardhat.config.ts.

STEP 2: Compile smart contract

yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode

STEP 3: Run unit test

yarn hardhat test
Enter fullscreen mode Exit fullscreen mode

STEP 4: Try to deploy to in-process testnet

yarn hardhat run ./scripts/deploy_greeter.ts
Enter fullscreen mode Exit fullscreen mode

In the next two steps, we will run a stand-alone Hardhat Network and deploy smart contracts to it.

STEP 5: Run a stand-alone local testnet

In another terminal, run:

yarn hardhat node
//Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
Enter fullscreen mode Exit fullscreen mode

STEP 6: Deploy to stand-alone local testnet

yarn hardhat run ./scripts/deploy.ts --network localhost
//Greeter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Enter fullscreen mode Exit fullscreen mode

If you run the deployment several times, you will find that the contract instances are deployed to different addresses.

Task 1.3: Switch MetaMask to Local testnet

Please make sure Hardhat Network local testnet is still running. (You can run it by command yarn hardhat node.)

  • STEP 1: In MataMask browser extension, click the network selector on the top bar. Switch the network from mainnet to localhost 8545.

  • STEP 2: Click the Account icon on the topbar and go to "Settings / Network / ". Choose "localhost 8445".

Note: make sure Chain ID is 31337. It may be "1337" by default in MetaMask.

Task 1.4: Create webapp with Next.js and Chakra UI

We will create a webapp using Node.js, React, Next.js and Chakra UI frameworks. (You may choose any other UI frameworks you like such as Material UI, Ant Design, etc. You may would like to choose front-end framework Vue instead of Next.js)

  • STEP 1: create Next.js project webapp

In hhproject/ directory, run:

yarn create next-app webapp --typescript
//will make a sub-dir webapp and create an empty Next.js project in it

cd webapp
Enter fullscreen mode Exit fullscreen mode
  • STEP 2: change some defaults and run webapp

We will use src as our app directory instead of pages (more about src and pages in Next.js docs):

mkdir src
mv pages src/pages
mv styles src/styles

vim tsconfig.json
//in "compilerOptions" add:
//        "baseUrl": "./src"
Enter fullscreen mode Exit fullscreen mode

Run Next.js app and view it in your browser:

yarn dev
//ready - started server on 0.0.0.0:3000, url: http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

Browse http://localhost:3000

  • STEP 3: install Chakra UI

Install Chakra UI (docs) by running:

yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
Enter fullscreen mode Exit fullscreen mode

We will edit the next.js app in the next sub-task to make it suitable for our project.

Task 1.5: Edit webapp - header, layout, _app.tsx, index.tsx

  • STEP 1: add a header component
mkdir src/components
touch src/components/header.tsx
Enter fullscreen mode Exit fullscreen mode

Edit header.tsx to be:

//src/components/header.tsx
import NextLink from "next/link"
import { Flex, Button, useColorModeValue, Spacer, Heading, LinkBox, LinkOverlay } from '@chakra-ui/react'

const siteTitle="FirstDAPP"
export default function Header() {

  return (
    <Flex as='header' bg={useColorModeValue('gray.100', 'gray.900')} p={4} alignItems='center'>
      <LinkBox>
        <NextLink href={'/'} passHref>
          <LinkOverlay>
            <Heading size="md">{siteTitle}</Heading>
          </LinkOverlay>
        </NextLink>
      </LinkBox>      
      <Spacer />
      <Button >Button for Account </Button>
    </Flex>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • STEP 2: add Next.js layout

Add layout (docs)

touch src/components/layout.tsx
Enter fullscreen mode Exit fullscreen mode

Edit layout.tsx to be:

// src/components/layout.tsx
import React, { ReactNode } from 'react'
import { Text, Center, Container, useColorModeValue } from '@chakra-ui/react'
import Header from './header'

type Props = {
  children: ReactNode
}

export function Layout(props: Props) {
  return (
    <div>
      <Header />
      <Container maxW="container.md" py='8'>
        {props.children}
      </Container>
      <Center as="footer" bg={useColorModeValue('gray.100', 'gray.700')} p={6}>
          <Text fontSize="md">first dapp by W3BCD - 2022</Text>
      </Center>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • STEP 3: Add Chakra UI Provider to "_app.tsx" as well as layout

Edit _app.tsx

// src/pages/_app.tsx
import { ChakraProvider } from '@chakra-ui/react'
import type { AppProps } from 'next/app'
import { Layout } from 'components/layout'

function MyApp({ Component, pageProps }: AppProps) {
  return (
      <ChakraProvider>
        <Layout>
        <Component {...pageProps} />
        </Layout>
      </ChakraProvider>
  )
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode
  • STEP 4: Edit "index.tsx"
// src/pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import NextLink from "next/link"
import { VStack, Heading, Box, LinkOverlay, LinkBox} from "@chakra-ui/layout"
import { Text, Button } from '@chakra-ui/react'

const Home: NextPage = () => {
  return (
    <>
      <Head>
        <title>My DAPP</title>
      </Head>

      <Heading as="h3"  my={4}>Explore Web3</Heading>          
      <VStack>
        <Box  my={4} p={4} w='100%' borderWidth="1px" borderRadius="lg">
          <Heading my={4}  fontSize='xl'>Task 1</Heading>
          <Text>local chain with hardhat</Text>
        </Box>

        <Box  my={4} p={4} w='100%' borderWidth="1px" borderRadius="lg">
          <Heading my={4}  fontSize='xl'>Task 2</Heading>
          <Text>DAPP with React/NextJS/Chakra</Text>
        </Box>

        <LinkBox  my={4} p={4} w='100%' borderWidth="1px" borderRadius="lg">
          <NextLink href="https://github.com/NoahZinsmeister/web3-react/tree/v6" passHref>
          <LinkOverlay>
            <Heading my={4} fontSize='xl'>Task 3 with link</Heading>
            <Text>Read docs of Web3-React V6</Text>
          </LinkOverlay>
          </NextLink>
        </LinkBox>
      </VStack>
    </>
  )
}

export default Home
Enter fullscreen mode Exit fullscreen mode

You may also want to add "_documents.tsx" (docs) to customize pages in your Next.js app.

You may want to delete unneeded files in this project such as src/styles.

  • STEP 5: Run webapp
yarn dev
Enter fullscreen mode Exit fullscreen mode

The page at http://localhost:3000/ will look like:

DAPP webapp


You can download the code from github scaffold repo.

In your 'hhproject/' directory:

git clone git@github.com:fjun99/web3app-tutorial-using-ethers.git webapp
cd webapp
yarn install
yarn dev
Enter fullscreen mode Exit fullscreen mode

TASK #2

Task 2: Connect DApp to blockchain through MetaMask

In this task, we will create a DAPP which can connect to blockchain(local testnet) through MetaMask.

We will interact with the blockchain using Javascript API library Ethers.js.

Task 2.1: install Ethers.js

In webapp/ directory, add Ethers.js

yarn add ethers
Enter fullscreen mode Exit fullscreen mode

Task 2.2: connect to MetaMask wallet

display ETH balance

We will add a button on index.tsx:

  • when not connected, the button text is "Connect Wallet". Click to link blockchain through MetaMask.

  • when connected, button text is connected account address. User can click to disconnect.

We will get ETH balance of the current account and display on the page as well as blockchain network info.

There are Ethers.js docs about connect MetaMask (link).

I wrote a slide to explain the relationship between connector, provider, signer and wallet in Ethers.js!

connector, provider, signer

We will use react hook feature useState and useEffect

Related code snippet in src/pages/index.tsx

// src/pages/index.tsx
...
import { useState, useEffect} from 'react'
import {ethers} from "ethers"

declare let window:any

const Home: NextPage = () => {
  const [balance, setBalance] = useState<string | undefined>()
  const [currentAccount, setCurrentAccount] = useState<string | undefined>()
  const [chainId, setChainId] = useState<number | undefined>()
  const [chainname, setChainName] = useState<string | undefined>()

  useEffect(() => {
    if(!currentAccount || !ethers.utils.isAddress(currentAccount)) return
    //client side code
    if(!window.ethereum) return
    const provider = new ethers.providers.Web3Provider(window.ethereum)
    provider.getBalance(currentAccount).then((result)=>{
      setBalance(ethers.utils.formatEther(result))
    })
    provider.getNetwork().then((result)=>{
      setChainId(result.chainId)
      setChainName(result.name)
    })

  },[currentAccount])

  const onClickConnect = () => {
    //client side code
    if(!window.ethereum) {
      console.log("please install MetaMask")
      return
    }
    /*
    //change from window.ethereum.enable() which is deprecated
    //see docs: https://docs.metamask.io/guide/ethereum-provider.html#legacy-methods
    window.ethereum.request({ method: 'eth_requestAccounts' })
    .then((accounts:any)=>{
      if(accounts.length>0) setCurrentAccount(accounts[0])
    })
    .catch('error',console.error)
    */

    //we can do it using ethers.js
    const provider = new ethers.providers.Web3Provider(window.ethereum)

    // MetaMask requires requesting permission to connect users accounts
    provider.send("eth_requestAccounts", [])
    .then((accounts)=>{
      if(accounts.length>0) setCurrentAccount(accounts[0])
    })
    .catch((e)=>console.log(e))
  }

  const onClickDisconnect = () => {
    console.log("onClickDisConnect")
    setBalance(undefined)
    setCurrentAccount(undefined)
  }

  return (
    <>
      <Head>
        <title>My DAPP</title>
      </Head>

      <Heading as="h3"  my={4}>Explore Web3</Heading>          
      <VStack>
        <Box w='100%' my={4}>
        {currentAccount  
          ? <Button type="button" w='100%' onClick={onClickDisconnect}>
                Account:{currentAccount}
            </Button>
          : <Button type="button" w='100%' onClick={onClickConnect}>
                  Connect MetaMask
              </Button>
        }
        </Box>
        {currentAccount  
          ?<Box  mb={0} p={4} w='100%' borderWidth="1px" borderRadius="lg">
          <Heading my={4}  fontSize='xl'>Account info</Heading>
          <Text>ETH Balance of current account: {balance}</Text>
          <Text>Chain Info: ChainId {chainId} name {chainname}</Text>
        </Box>
        :<></>
        }
...
      </VStack>
    </>
  )
}

export default Home
Enter fullscreen mode Exit fullscreen mode

Some explanations:

  • We add two UI components: one for the connect button, one for displaying account and chain information.

  • When "Connect MetaMask" button is clicked, we do:

    • get Web3Provider through connector(window.ethereum) which MetaMask injected to the page.
    • call eth_requestAccounts, which will ask MetaMask to confirm sharing account information. Users can confirm or reject the request in MetaMask popup.
    • set the returned account to currentAccount.
  • When disconnect is called, we reset currentAccount and balance.

  • Every time the currentAccount is changed, the side effect (useEffect) is called. We will query:

    • ETH balance of the current account by calling getBalance.
    • network information by calling getNetwork().

Please note that:

  • Disconnect in page will not change MetaMask connection and permissions to this page. Open MetaMask extension, you will see that your wallet is still connected to this page. Next time you click Connect MetaMask button again, the MetaMask will not popup for confirmation (as your confirmation is still active). You need to disconnect wallet and page from MetaMask.

  • We do not write codes to display changes when the user switches network in MetaMask.

  • We do not store the state of this page. So when the page is refreshed, the connection is reset.


TASK #3

Task 3: Build ERC20 smart contract using OpenZeppelin

In Task 3, we will build ERC20 smart contract using OpenZeppelin library (ERC20 docs).

Task 3.1: Write an ERC20 smart contract - ClassToken

Add OpenZeppelin/contract:

yarn add @openzeppelin/contracts
Enter fullscreen mode Exit fullscreen mode

Go to chain/ directory and add contracts/ClassToken.sol:

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ClassToken is ERC20 {
        constructor(uint256 initialSupply) 
          ERC20("ClassToken", "CLT") 
        {
                _mint(msg.sender, initialSupply);
        }
}

Enter fullscreen mode Exit fullscreen mode

Task 3.2 Compile smart contract

yarn hardhat compile
//Solidity compilation should succeed
Enter fullscreen mode Exit fullscreen mode

Task 3.3 Add unit test script

Add unit test script test/ClassToken.test.ts

import { expect } from "chai";
import { ethers } from "hardhat";

describe("ClassToken", function () {
  it("Should have the correct initial supply", async function () {
    const initialSupply = ethers.utils.parseEther('10000.0')
    const ClassToken = await ethers.getContractFactory("ClassToken");
    const token = await ClassToken.deploy(initialSupply);
    await token.deployed();

    expect(await token.totalSupply()).to.equal(initialSupply);
  });
});
Enter fullscreen mode Exit fullscreen mode

Run unit test:

yarn hardhat test
//  ClassToken
//    ✓ Should have the correct initial supply (392ms)
//  1 passing (401ms)
Enter fullscreen mode Exit fullscreen mode

Task 3.4 Add deploy script

Add deploy script scripts/deploy_classtoken.ts

import { ethers } from "hardhat";

async function main() {

  const initialSupply = ethers.utils.parseEther('10000.0')
  const ClassToken = await ethers.getContractFactory("ClassToken");
  const token = await ClassToken.deploy(initialSupply);
  await token.deployed();

  console.log("ClassToken deployed to:", token.address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

We deploy ClassToken with initialSupply 10000.0 CLT sent to deployer(msg.sender).

Try to run contract deployment to in-process Hardhat Network local testnet (in-process mode):

yarn hardhat run  scripts/deploy_classtoken.ts
Enter fullscreen mode Exit fullscreen mode

Task 3.5 Run stand-alone testnet to deploy smart contract to it

In another terminal, run in chain/ directory:

yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

In the current terminal, run hardhat tasks connecting to localhost --network localhost:

yarn hardhat run scripts/deploy_classtoken.ts --network localhost
//ClassToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Enter fullscreen mode Exit fullscreen mode

Task 3.6 Interact with ClassToken in hardhat console

Run hardhat console connecting to stand-alone local testnet:

yarn hardhat console  --network localhost
Enter fullscreen mode Exit fullscreen mode

Interact with ClassToken in console:

formatEther = ethers.utils.formatEther;
address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
token = await ethers.getContractAt("ClassToken", address);

totalSupply = await token.totalSupply();
formatEther(totalSupply)
//'10000.0'
Enter fullscreen mode Exit fullscreen mode

ethers.getContractAt() is a helper function provided by Hardhat plugin hardhat-ethers, docs link.

Task 3.7: Add token to MetaMask

Add token to MetaMask with Address:0x5FbDB2315678afecb367f032d93F642f64180aa3. (Please use the deployed contract address you get.)

We can see that Account#0(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) has 10000.0 CLT.

You can also download the hardhat sample project from github repo.

git clone git@github.com:fjun99/chain-tutorial-hardhat-starter.git chain
yarn install
// then, you can run stand-alone testnet and 
// go through the compile-test-deploy circle 
Enter fullscreen mode Exit fullscreen mode

TASK #4

Task 4: Read contract data - interact with smart contract in webapp

In task 4 and task 5, we will continue to build our webapp.

We will allow users to interact with the newly deployed ERC20 Token smart contract - ClassToken(CLT).

DApp ERC20

Task 4.1: Add empty ReadERC20 component to read ClassToken

In webapp directory, add an empty component components/ReadERC20.tsx

import React, { useEffect,useState } from 'react'
import { Text} from '@chakra-ui/react'
interface Props {
    addressContract: string,
    currentAccount: string | undefined
}

export default function ReadERC20(props:Props){
  const addressContract = props.addressContract
  const currentAccount = props.currentAccount


return (
    <div>
        <Text >ERC20 Contract: {addressContract}</Text>
        <Text>token totalSupply:</Text>
        <Text my={4}>ClassToken in current account:</Text>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

Import and add this component to index.tsx:

        <Box  mb={0} p={4} w='100%' borderWidth="1px" borderRadius="lg">
          <Heading my={4}  fontSize='xl'>Read ClassToken Info</Heading>
          <ReadERC20 
            addressContract='0x5FbDB2315678afecb367f032d93F642f64180aa3'
            currentAccount={currentAccount}
          />
        </Box>

Enter fullscreen mode Exit fullscreen mode

In the following sub-tasks, we will add functionality of ReadERC20 Component step by step.

Task 4.2: Prepare smart contract ABI

To interact with a smart contract in Javascript, we need its ABI:

Contract Application Binary Interface (ABI) is the standard way to interact with contracts in the Ethereum ecosystem. Data is encoded according to its type.

ERC20 smart contract is a standard and we will use a file with human-readable ABI instead of the compiled artifacts output in our Hardhat project. What we add is human-readable ABI.

Add a directory src/abi and create a file src/abi/ERC20ABI.tsx

export const ERC20ABI = [
    // Read-Only Functions
    "function balanceOf(address owner) view returns (uint256)",
    "function totalSupply() view returns (uint256)",
    "function decimals() view returns (uint8)",
    "function symbol() view returns (string)",
    // Authenticated Functions
    "function transfer(address to, uint amount) returns (bool)",
    // Events
    "event Transfer(address indexed from, address indexed to, uint amount)"
];
Enter fullscreen mode Exit fullscreen mode

Task 4.3: Query smart contract info when component loaded

We use React hook useEffect to query smart contract info when the component is loaded (mounted).

Edit ReadERC20.tsx

// src/components/ReadERC20.tsx
import React, {useEffect, useState } from 'react';
import {Text} from '@chakra-ui/react'
import {ERC20ABI as abi} from 'abi/ERC20ABI'
import {ethers} from 'ethers'

interface Props {
    addressContract: string,
    currentAccount: string | undefined
}

declare let window: any;

export default function ReadERC20(props:Props){
  const addressContract = props.addressContract
  const currentAccount = props.currentAccount
  const [totalSupply,setTotalSupply]=useState<string>()
  const [symbol,setSymbol]= useState<string>("")

  useEffect( () => {
    if(!window.ethereum) return

    const provider = new ethers.providers.Web3Provider(window.ethereum)
    const erc20 = new ethers.Contract(addressContract, abi, provider);
    erc20.symbol().then((result:string)=>{
        setSymbol(result)
    }).catch('error', console.error)

    erc20.totalSupply().then((result:string)=>{
        setTotalSupply(ethers.utils.formatEther(result))
    }).catch('error', console.error);
    //called only once
  },[])  

  return (
    <div>
        <Text><b>ERC20 Contract</b>: {addressContract}</Text>
        <Text><b>ClassToken totalSupply</b>:{totalSupply} {symbol}</Text>
        <Text my={4}><b>ClassToken in current account</b>:</Text>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

Explanations:

  • Hook useEffect(()=>{},[]) will only be called once.

  • Create a Web3Provider with window.ethereum, ethereum connector injected into page by MetaMask.

  • Create a contract instance with addressContract, abi, provider in Ethers.js.

  • Call read-only functions symbol(), totalSupply() and set results to react state variable which can be displayed on page.

Task 4.3: Query CLT balance of current account when account changes

Edit ReadERC20.tsx

// src/components/ReadERC20.tsx
  const [balance, SetBalance] =useState<number|undefined>(undefined)
...
  //call when currentAccount change
  useEffect(()=>{
    if(!window.ethereum) return
    if(!currentAccount) return

    queryTokenBalance(window)
  },[currentAccount])

  async function queryTokenBalance(window:any){
    const provider = new ethers.providers.Web3Provider(window.ethereum)
    const erc20 = new ethers.Contract(addressContract, abi, provider);

    erc20.balanceOf(currentAccount)
    .then((result:string)=>{
        SetBalance(Number(ethers.utils.formatEther(result)))
    })
    .catch('error', console.error)
  }  
...
  return (
    <div>
        <Text><b>ERC20 Contract</b>: {addressContract}</Text>
        <Text><b>ClassToken totalSupply</b>:{totalSupply} {symbol}</Text>
        <Text my={4}><b>ClassToken in current account</b>: {balance} {symbol}</Text>
    </div>
  )
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • When currentAccount changes, the side effect hook useEffect(()=>{},[currentAccount] is called. We call balanceOf(address) to get the balance.

  • When we refresh the page, there is no current account and no balance is displayed. After we connect wallet, the balance is queried and displayed on the page.

There are still more jobs to be done:

  • When MetaMask switch account, our web app doesn't know and will not change the display in the page. We need to listen event of MetaMask account change.

  • When the balance of the current account changes, our web app will not update since your current account has not been changed.

You can send CLT to others using MetaMask and you will find that we need to update the account balance of CLT on the page. We will do it in task 6. In task 5, we will build transfer components for users first.


TASK #5

Task 5: Write/Transfer: Interact with smart contract in web App

Task 5.1: Add empty TransferERC20 component

// src/component/TransferERC20.tsx
import React, { useEffect,useState } from 'react';
import { Text, Button, Input , NumberInput,  NumberInputField,  FormControl,  FormLabel } from '@chakra-ui/react'

interface Props {
    addressContract: string,
    currentAccount: string | undefined
}

export default function ReadERC20(props:Props){
  const addressContract = props.addressContract
  const currentAccount = props.currentAccount
  const [amount,setAmount]=useState<string>('100')
  const [toAddress, setToAddress]=useState<string>("")

  async function transfer(event:React.FormEvent) {
    event.preventDefault()
    console.log("transfer clicked")
 }

  const handleChange = (value:string) => setAmount(value)

  return (
    <form onSubmit={transfer}>
    <FormControl>
    <FormLabel htmlFor='amount'>Amount: </FormLabel>
      <NumberInput defaultValue={amount} min={10} max={1000} onChange={handleChange}>
        <NumberInputField />
      </NumberInput>
      <FormLabel htmlFor='toaddress'>To address: </FormLabel>
      <Input id="toaddress" type="text" required  onChange={(e) => setToAddress(e.target.value)} my={3}/>
      <Button type="submit" isDisabled={!currentAccount}>Transfer</Button>
    </FormControl>
    </form>
  )
}

Enter fullscreen mode Exit fullscreen mode

Import and add this component to index.tsx:

        <Box  mb={0} p={4} w='100%' borderWidth="1px" borderRadius="lg">
          <Heading my={4}  fontSize='xl'>Transfer Classtoken</Heading>
          <TransferERC20 
            addressContract='0x5FbDB2315678afecb367f032d93F642f64180aa3'
            currentAccount={currentAccount}
          />
        </Box>
Enter fullscreen mode Exit fullscreen mode

Task 5.2: Implement transfer() function

Implement the transfer function in TransferERC20.tsx:

// src/component/TransferERC20.tsx
import React, { useState } from 'react'
import {Button, Input , NumberInput,  NumberInputField,  FormControl,  FormLabel } from '@chakra-ui/react'
import {ethers} from 'ethers'
import {parseEther } from 'ethers/lib/utils'
import {ERC20ABI as abi} from 'abi/ERC20ABI'
import { Contract } from "ethers"
import { TransactionResponse,TransactionReceipt } from "@ethersproject/abstract-provider"

interface Props {
    addressContract: string,
    currentAccount: string | undefined
}

declare let window: any;

export default function TransferERC20(props:Props){
  const addressContract = props.addressContract
  const currentAccount = props.currentAccount
  const [amount,setAmount]=useState<string>('100')
  const [toAddress, setToAddress]=useState<string>("")

  async function transfer(event:React.FormEvent) {
    event.preventDefault()
    if(!window.ethereum) return    
    const provider = new ethers.providers.Web3Provider(window.ethereum)
    const signer = provider.getSigner()
    const erc20:Contract = new ethers.Contract(addressContract, abi, signer)

    erc20.transfer(toAddress,parseEther(amount))
      .then((tr: TransactionResponse) => {
        console.log(`TransactionResponse TX hash: ${tr.hash}`)
        tr.wait().then((receipt:TransactionReceipt)=>{console.log("transfer receipt",receipt)})
      })
      .catch((e:Error)=>console.log(e))

  }

  const handleChange = (value:string) => setAmount(value)

  return (
    <form onSubmit={transfer}>
    <FormControl>
    <FormLabel htmlFor='amount'>Amount: </FormLabel>
      <NumberInput defaultValue={amount} min={10} max={1000} onChange={handleChange}>
        <NumberInputField />
      </NumberInput>
      <FormLabel htmlFor='toaddress'>To address: </FormLabel>
      <Input id="toaddress" type="text" required  onChange={(e) => setToAddress(e.target.value)} my={3}/>
      <Button type="submit" isDisabled={!currentAccount}>Transfer</Button>
    </FormControl>
    </form>
  )
}


Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We call transfer(address recipient, uint256 amount) → bool, which is state-change function of ERC20 smart contract.

As you can see, the balance of ClassToken is not changed after transferring. We will fix this in Task 6.


TASK #6

Task 6: Listen to Event: Interact with smart contract in web App

We can update CLT balance with the design of smart contract events. For ERC20 token smart contract, when a transfer is confirmed on-chain, an Event Transfer(address from, address to, uint256 value) (docs) is emitted.

We can listen to this event in Node.js webapp and update the page display.

Task 6.1: Understanding Smart contract events

Events explained in plain English. When we call a state-change function of a smart contract, there are three steps:

  • STEP 1: Off-chain call. We call a state-change function of a smart contract using JavaScript API (ethers.js) off-chain.

  • STEP 2: On-chain confirmation. State-change transactions need to be confirmed by miners using consensus algorithms in several blocks on-chain. So we can't get the result immediately.

  • STEP 3: Emit events. Once the transaction is confirmed, an event is emitted. You can listen to events to get the results off-chain.

events explained

Task 6.2: Add event listener when current account change

Edit ReadERC20.tsx:

  //call when currentAccount change
  useEffect(()=>{
    if(!window.ethereum) return
    if(!currentAccount) return

    queryTokenBalance(window)

    const provider = new ethers.providers.Web3Provider(window.ethereum)
    const erc20 = new ethers.Contract(addressContract, abi, provider)

    // listen for changes on an Ethereum address
    console.log(`listening for Transfer...`)

    const fromMe = erc20.filters.Transfer(currentAccount, null)
    provider.on(fromMe, (from, to, amount, event) => {
        console.log('Transfer|sent', { from, to, amount, event })
        queryTokenBalance(window)
    })

    const toMe = erc20.filters.Transfer(null, currentAccount)
    provider.on(toMe, (from, to, amount, event) => {
        console.log('Transfer|received', { from, to, amount, event })
        queryTokenBalance(window)
    })

    // remove listener when the component is unmounted
    return () => {
        provider.removeAllListeners(toMe)
        provider.removeAllListeners(fromMe)
    }    
  }, [currentAccount])
Enter fullscreen mode Exit fullscreen mode

This code snippet is adapted from How to Fetch and Update Data From Ethereum with React and SWR.

Explanation:

  • When currentAccount change (useEffect), we add two listeners: one for events transfer from currentAccount, another for transfer to currentAccount.

  • When listening to an event, query the token balance of currentAccount and update the page.

You can transfer token from the current account on the page or in MetaMask, and you will see that the page is updating upon events.

When completing Task 6, you have built a simple but functional DAPP which has smart contract and webapp.


In summary, there are three parts of a DAPP:

  • smart contract and blockchain
  • web app (user interface) to get and set data with smart contract
  • user-controlled wallet (MetaMask here) which works as an asset management tool and signer for users, as well as connector to blockchain.

In these tasks, you may also notice that we interact with smart contracts in 3 ways:

  • Read, get data from smart contract
  • Write, set data in smart contract
  • Listen, listen to events emitted by smart contract.

We use Ethers.js directly to connect to blockchain in this tutorial. But Web3-react and other libraries can handle connections to Ethereum node in React while the Ethers.js is running in the underling. That's the topic of our next tutorial - build DApp with Web3-React.


A note: You may find the very first how-to guide for DApp developers is useful for your web3 journey: Tutorial : build your first DAPP with Remix and Etherscan(by Fangjun).


If you find this tutorial helpful, follow me at Twitter @fjun99

Discussion (6)

Collapse
gautham495 profile image
Gautham Vijayan

These articles are of high value in content. Keep up the wonderful work you !!!!

Collapse
pillarboy profile image
pillarBoy

This tutorial is really awesome

Collapse
satinder2000 profile image
satinder-2000

Dear fangjun,

Many thanks for giving us wonderful tutorials and I am learning a great deal from it. However, since I am just a beginner in Blockchain/Ethereum, I am stuck at Task 3.7 as MetaMask does not have any facility where I can manually add Tokens. What can I do?

Collapse
yakult profile image
fangjun Author

import tokens link under the assets list

Collapse
danielesposito profile image
Daniel Esposito

Great tutorial! Just one quick question. How can I persist my connection to Metamask so I don't have to connect to wallet every time I refresh the page and instead simply show the address and balance if there's a connection already established? Thank you in advance

Collapse
yakult profile image
fangjun Author

If you run connect in ethers.js, you will find that you are still connected.

The connection only can be canceled by user in metamask. So the connection is still there.