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.
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: 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
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
Install Hardhat:
yarn add hardhat
- STEP 2: create a sample Hardhat project
yarn hardhat
//choose: Create an advanced sample project that uses TypeScript
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
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
...
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
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)
STEP 1: Run command to show accounts:
yarn hardhat accounts
This is the sample hardhat task added in the hardhat.config.ts
.
STEP 2: Compile smart contract
yarn hardhat compile
STEP 3: Run unit test
yarn hardhat test
STEP 4: Try to deploy to in-process testnet
yarn hardhat run ./scripts/deploy_greeter.ts
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/
STEP 6: Deploy to stand-alone local testnet
yarn hardhat run ./scripts/deploy.ts --network localhost
//Greeter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
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
tolocalhost 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
- 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"
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
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
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
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>
)
}
- STEP 2: add Next.js layout
Add layout (docs)
touch src/components/layout.tsx
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>
)
}
- 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
- 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
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
The page at http://localhost:3000/ will look like:
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
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
Task 2.2: connect to MetaMask wallet
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
!
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
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
.
- get
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()
.
- ETH balance of the current account by calling
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: 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
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);
}
}
Task 3.2 Compile smart contract
yarn hardhat compile
//Solidity compilation should succeed
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);
});
});
Run unit test:
yarn hardhat test
// ClassToken
// ✓ Should have the correct initial supply (392ms)
// 1 passing (401ms)
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;
});
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
Task 3.5 Run stand-alone testnet to deploy smart contract to it
In another terminal, run in chain/
directory:
yarn hardhat node
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
Task 3.6 Interact with ClassToken in hardhat console
Run hardhat console connecting to stand-alone local testnet:
yarn hardhat console --network localhost
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'
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
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)
.
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>
)
}
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>
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)"
];
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>
)
}
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
inEthers.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>
)
Explanation:
When
currentAccount
changes, the side effect hookuseEffect(()=>{},[currentAccount]
is called. We callbalanceOf(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: 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>
)
}
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>
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>
)
}
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: 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.
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])
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
Top comments (8)
These articles are of high value in content. Keep up the wonderful work you !!!!
This tutorial is really awesome
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?
import tokens link under the assets list
I find a question in this course.
code:
provider.on(toMe, (from, to, amount, event)
can't console right message in console.log.I change code as:
erc20.on(toMe, (from, to, amount, event)
, console message is right.But another problem bothered me, when I refresh page, the Chrome console always trigger the last transaction event message, hardhat console no message was received.
Please advise, thank you.
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
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.
awesome