使用 React 开发 DApp 入门教程
在本教程中,我们将使用Hardhat、React和ethers.js[构建DAPP](https://learnblockchain.cn/article/1113),它可以与用户控制的钱包如MetaMask一起使用。
DAPP通常由三部分组成:
– 部署在链上的智能合约
– 用Node.js、React和Next.js构建的Webapp(用户界面)
– 钱包(用户在浏览器中控制的/移动钱包App)
我们使用`ethers.js`来连接各个:

在DApp(webapp)的用户界面中,MetaMask等钱包给开发者提供了一个以太坊的提供者,我们可以在`Ethers.js`中使用,与区块链进行交互。(具体来说,钱包提供的是一个 “连接器”,ethers.js创建 “提供者(provider)”和/或 “签名者(signer)”供我们使用)。
作为用户,我们可能已经知道了MetaMask的用法,作为开发者,我们将学习如何使用MetaMask和它注入浏览器的`window.ethereum`([MetaMask开发者文档](https://docs.metamask.io/guide/ethereum-provider.html#using-the-provider)。
本教程的代码库在:
Hardhat项目:https://github.com/fjun99/chain-tutorial-hardhat-starter
Webapp项目:https://github.com/fjun99/web3app-tutorial-using-ethers
> **特别感谢**在准备webapp代码库时,Wesley的[Proof-of-Competence, POC](https://github.com/wslyvh/proof-of-competence)项目学到了很多。我们也像他的项目一样使用Chakra UI。你可能也会发现网页与POC几乎一样。
—
## 前置知识和工具
在我们开始之前,你需要对一下内容有一些了解:
知识:
– 区块链
– 以太坊
– 钱包
– [Solidity](https://learnblockchain.cn/docs/solidity/)
– ERC20 & ERC721
– [Ethers.js](https://learnblockchain.cn/docs/ethers.js/)
工具:
– MetaMask (钱包浏览器插件)
– Node.js, yarn, TypeScript
– OpenZeppelin (Solidity库)
– Etherscan区块浏览器
让我们开始建立一个DApp
## 任务1:设置开发环境
为了建立一个DApp,我们要做两个工作:
– 使用Hardhat和Solidity构建智能合约
– 使用Node.js、React和Next.js构建Web 应用。
我们将把目录组织成两个子目录`chain`和`webapp`:
“`
– hhproject
– chain (working dir for hardhat)
– contracts
– test
– scripts
– webapp (working dir for NextJS app)
– src
– pages
– components
“`
### 任务1.1 安装Hardhat并启动Hardhat项目
安装Hardhat,这是一个以太坊开发环境。
要使用Hardhat,你需要在电脑上有`node.js`和`yarn`:
– 第1步:建立一个目录并在其中安装Hardhat
“`bash
mkdir hhproject && cd hhproject
mkdir chain && cd chain
yarn init -y
“`
安装Hardhat:
“`bash
yarn add hardhat
“`
– 第2步:创建一个Hardhat样本项目
“`bash
yarn hardhat
//choose: Create an advanced sample project that uses TypeScript
“`
用我们将在任务3中使用的样本智能合约`Greeter.sol`创建一个hardhat项目。
– 第3步:运行Harhat网络(本地testnet)
“`
yarn hardhat node
“`
将运行一个本地测试网(**chainId: 31337**)。
> 开始了HTTP和WebSocket JSON-RPC服务器,地址是http://127.0.0.1:8545/
它提供了20个账户,每个账户有`10000.0测试ETH`。
“`
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
…
“`
请注意,Hardhat Network本地testnet有两种模式:进程中模式和独立模式。我们用命令行`yarn hardhat node`运行一个独立的testnet。当运行像`yarn hardhat compile`这样的命令行时,如果没有网络参数(`–network localhost`),我们就运行一个进程内测试网。
### 任务1.2:在Hardhat的开发
我们将在Hardhat开发环境中体验智能合约的开发过程。
在Hardhat启动的项目中,默认包含有智能合约、测试脚本和部署脚本的样本。
“`
├── contracts
│ └── Greeter.sol
├── scripts
│ └── deploy.ts
├── test
│ └── index.ts
├── hardhat.config.ts
“`
我想改变测试和部署脚本的文件名:
“`
– contracts
– Greeter.sol
– test
– Greeter.test.ts (<-index.ts)
– scripts
– deploy_greeter.ts (<-deploy.ts)
“`
第1步:运行命令,显示账户:
“`
yarn hardhat accounts
“`
这是在`hardhat.config.ts`中添加的hardhat 样本任务。
第2步:编译智能合约
“`
yarn hardhat compile
“`
第3步:运行单元测试
“`
yarn hardhat test
“`
第4步:尝试部署到进程中的测试网
“`
yarn hardhat run ./scripts/deploy_greeter.ts
“`
在接下来的两个步骤中,将运行一个独立的Hardhat网络,并将智能合约部署上去。
第5步:运行一个独立的本地测试网
在另一个终端,运行:
“`bash
yarn hardhat node
//Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
“`
第6步: 部署到独立的本地测试网
“`bash
yarn hardhat run ./scripts/deploy.ts –network localhost
//Greeter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
“`
如果你多次运行部署,你会发现合约实例被部署到不同的地址。
### 任务1.3:将MetaMask切换到本地测试网
确保Hardhat Network 本地测试网仍在运行(你可以通过命令`yarn hardhat node`来运行它)。
– 第1步:在MataMask浏览器插件中,点击顶部栏的网络选择。将网络从`mainnet`切换到`localhost 8545`。
– 第2步:点击顶栏上的账户图标,进入 `设置/网络/`。选择 `localhost 8445`。
注意:确保链ID是**31337**。在MetaMask中,它可能默认为 `1337`。
### 任务 1.4: 用Next.js和Chakra UI创建webapp
我们将使用`Node.js`、`React`、`Next.js`和`Chakra UI`框架创建一个webapp。(你可以选择你喜欢的任何其他UI框架,如`Material UI`,`Ant Design`等。你也可能想选择前端框架`Vue`而不是`Next.js`)
– 第1步:创建Next.js项目`webapp`。
在`hhproject/`目录下,运行:
“`bash
yarn create next-app webapp –typescript
//will make a sub-dir webapp and create an empty Next.js project in it
cd webapp
“`
– 第2步:改变一些默认值并运行webapp
我们将使用`src`作为应用程序目录,而不是`pages`(关于`src`和`pages`的更多信息[在Next.js docs](https://nextjs.org/docs/advanced-features/src-directory))。
“`
mkdir src
mv pages src/pages
mv styles src/styles
vim tsconfig.json
//in “compilerOptions” add:
// “baseUrl”: “./src”
“`
运行Next.js应用程序并在浏览器中查看。
“`
yarn dev
//ready – started server on 0.0.0.0:3000, url: http://localhost:3000
“`
浏览`http://localhost:3000`。
– 第3步:安装Chakra UI
通过运行Chakra UI([文档](https://chakra-ui.com/docs/getting-started))来安装:
“`
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
“`
我们将在下一个子任务中编辑next.js应用程序,使其适合我们的项目。
### 任务1.5:编辑webapp
webapp 会包含头部、layout、_app.tsx、index.tsx 等
– 第1步:添加一个页眉组件
“`
mkdir src/components
touch src/components/header.tsx
“`
编辑`header.tsx`为:
“`typescript
//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>
)
}
“`
– 第2步:添加Next.js布局
添加布局([Next.js文档](https://nextjs.org/docs/basic-features/layouts))
“`
touch src/components/layout.tsx
“`
编辑`layout.tsx`为:
“`typescript
// 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>
)
}
“`
– 第3步:在`_app.tsx `和布局中添加Chakra UI Provider
编辑`_app.tsx`
“`typescript
// 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
“`
– 第4步:编辑 `index.tsx`
“`typescript
// 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
“`
你可能还想添加`_documents.tsx`([docs](https://nextjs.org/docs/advanced-features/custom-document))来定制你的Next.js应用程序中的页面。
你可能想删除这个项目中不需要的文件,如`src/styles`。
– 第5步:运行webapp
“`
yarn dev
“`
在http://localhost:3000/ 的页面将看起来像:

你可以从github [scaffold repo](https://github.com/fjun99/web3app-tutorial-starter-scaffold)下载代码:
在你的’hhproject/’目录下。
“`bash
git clone git@github.com:fjun99/web3app-tutorial-using-ethers.git webapp
cd webapp
yarn install
yarn dev
“`
## 任务2:通过MetaMask将DApp连接到区块链上
在这个任务中,我们将创建一个DAPP,它可以通过MetaMask连接到区块链(本地测试网)。
我们将使用Javascript API库`Ethers.js`与区块链交互。
### 任务2.1:安装`Ethers.js`
在`webapp/`目录下,添加`Ethers.js`:
“`bash
yarn add ethers
“`
### 任务2.2:连接到MetaMask钱包

我们将在`index.tsx`上添加一个按钮:
– 当未连接时,按钮文本为 `Connect Wallet(连接钱包)`。点击即可通过MetaMask链接区块链。
– 当连接时,按钮文本是连接的账户地址。用户可以点击断开连接。
我们将获得当前账户的ETH余额并显示在页面上,以及区块链网络信息。
有关于连接MetaMask的以太坊文档([文档链接](https://docs.ethers.io/v5/getting-started/#getting-started–connecting))。
我写了一张PPT来解释`connector`、`provider`、`signer`和`ethers.js`中的wallet之间的关系!

我们将使用[react hook](https://reactjs.org/docs/hooks-intro.html)功能`useState`和`useEffect`。
相关代码片段在 `src/pages/index.tsx`中:
“`typescript
// 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
“`
解释一下:
– 我们添加了两个UI组件:一个用于连接钱包,一个用于显示账户和链的信息。
– 当 `连接MetaMask `按钮被点击时,执行:
– 通过连接器(即`window.ethereum`, 他是MetaMask注入到页面的)获得`Web3Provider`,。
– 调用`eth_requestAccounts`,这将要求MetaMask确认分享账户信息。用户在MetaMask的弹出窗口确认或拒绝该请求。
– 将返回的账户设置为`currentAccount`。
– 当断开连接被调用时,我们重置currentAccount和余额。
– 每次改变currentAccount时,都会调用副作用函数(useEffect),在这里执行查询:
– 通过调用`getBalance`查询当前账户的ETH余额。
– 通过调用`getNetwork()`来查询网络信息。
请注意:
– 在页面中断开连接,不会改变MetaMask的连接和该页面的权限。打开MetaMask扩展,你会发现你的钱包仍然连接到这个页面。下次你再点击 `连接MetaMask `按钮时,MetaMask不会弹出确认窗口(因为你的确认仍然有效)。你智能通过MetaMask来断开钱包和页面的连接。
– 当用户在MetaMask中切换网络时,我们没有编写代码来显示变化。
– 我们没有存储这个页面的状态。因此,当页面被刷新时,连接被重置。
## 任务3:使用OpenZeppelin构建ERC20智能合约
在任务3中,我们将使用OpenZeppelin库构建ERC20智能合约([ERC20 docs](https://docs.openzeppelin.com/contracts/4.x/api/token/erc20))。
### 任务3.1: 编写一个ERC20智能合约 – ClassToken
添加`OpenZeppelin/contract`:
“`
yarn add @openzeppelin/contracts
“`
到`chain/`目录下,添加`ccontracts/ClassToken.sol`:
“`solidity
//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);
}
}
“`
### 任务 3.2 编译智能合约
“`
yarn hardhat compile
//Solidity compilation should succeed
“`
### 任务 3.3 添加单元测试脚本
添加单元测试脚本`test/ClassToken.test.ts`。
“`typescript
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);
});
});
“`
运行单元测试。
“`shell
yarn hardhat test
// ClassToken
// Should have the correct initial supply (392ms)
// 1 passing (401ms)
“`
### 任务 3.4 添加部署脚本
添加部署脚本 `scripts/deploy_classtoken.ts`。
“`typescript
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;
});
“`
我们部署ClassToken,并将initialSupply `10000.0 CLT`发送到部署者(`msg.sender`)。
尝试运行合约部署到进程中的Hardhat Network本地测试网(进程中模式)。
“`
yarn hardhat run scripts/deploy_classtoken.ts
“`
### 任务3.5 运行独立的测试网,向其部署智能合约
在另一个终端,在`chain/`目录下运行:
“`
yarn hardhat node
“`
在当前终端,运行连接到localhost `–network localhost`的hardhat任务。
“`
yarn hardhat run scripts/deploy_classtoken.ts –network localhost
//ClassToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
“`
### 任务3.6 在hardhat控制台与ClassToken交互
运行连接到独立的本地测试网的hardhat控制台。
“`
yarn hardhat console –network localhost
“`
在控制台中与 `ClassToken `交互:
“`
formatEther = ethers.utils.formatEther;
address = ‘0x5FbDB2315678afecb367f032d93F642f64180aa3′;
token = await ethers.getContractAt(“ClassToken”, address);
totalSupply = await token.totalSupply();
formatEther(totalSupply)
//’10000.0’
“`
`ethers.getContractAt()`是Hardhat插件`hardhat-ethers`提供的一个辅助函数,[文档链接](https://hardhat.org/plugins/nomiclabs-hardhat-ethers.html#helpers)。
### 任务3.7:将代币添加到MetaMask中
添加Token到MetaMask,地址为:`0x5FbDB2315678afecb367f032d93F642f64180aa3`。(**请使用你得到的部署合约地址。**)
我们可以看到0号账户(`0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`)有`10000.0 CLT`。
你也可以从[github repo](https://github.com/fjun99/chain-tutorial-hardhat-starter)下载hardhat示例项目。
“`
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
“`
## 任务4:读取合约数据–在webapp中与智能合约交互
在任务4和任务5中,我们将继续构建我们的webapp。
我们将允许用户与新部署的ERC20代币智能合约–`ClassToken(CLT)`进行交互。

### 任务4.1: 添加空的`ReadERC20`组件来读取ClassToken
在webapp目录中,添加一个空组件`components/ReadERC20.tsx`。
“`typescript
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>
)
}
“`
导入并添加这个组件到`index.tsx`:
“`typescript
<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>
“`
在下面的子任务中,我们将逐步添加`ReadERC20`组件的功能。
### 任务4.2:准备智能合约ABI
要在Javascript中与智能合约交互,我们需要它的[ABI](https://docs.soliditylang.org/en/v0.8.11/abi-spec.html)。
> 合约**应用二进制接口**(ABI)是与以太坊生态系统中的合约交互的标准方式。数据是根据其类型进行编码的。
ERC20智能合约是一个标准,我们将使用一个文件而不是Hardhat项目中输出的编译工件。我们添加的是[人类可读的ABI](https://docs.ethers.io/v5/api/utils/abi/formats/#abi-formats–human-readable-abi)。
添加一个目录`src/abi`并添加文件`src/abi/ERC20ABI.tsx`:
“`typescript
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)”
];
“`
### 任务4.3: 当组件加载时查询智能合约信息
我们使用React钩子`useEffect`来查询组件加载时的智能合约信息。
编辑`ReadERC20.tsx`:
“`typescript
// 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>
)
}
“`
解释一下:
– 钩子`useEffect(()=>{},[])`将只被调用一次。
– 用`window.ethereum`(通过MetaMask注入到页面)创建一个[Web3Provider](https://docs.ethers.io/v5/api/providers/other/#Web3Provider)。
– 在`ethers.js`中用`addressContract`、`abi`、`provider`创建一个合约实例。
– 调用只读函数`symbol()`, `totalSupply()`,并将结果设置为反应状态的变量,可以在页面上显示。
### 任务 4.3: 当账户变化时,查询当前账户的CLT余额
编辑`ReadERC20.tsx`:
“`typescript
// 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>
)
“`
解释一下:
– 当`currentAccount`改变时,副作用钩子函数`useEffect(()=>{},[currentAccount]`被调用,调用`balanceOf(address)`来获得余额。
– 当我们刷新页面时,没有当前账户,也没有显示余额。在我们连接钱包后,余额被查询到并显示在页面上。
还有更多的工作要做:
– 当MetaMask切换账户时,我们的Web 应用不知道,也不会改变页面的显示,因此需要监听MetaMask的账户变化事件。
– 当当前账户的余额发生变化时,由于当前账户没有被改变,我们的Web应用程序将不会更新。
你可以使用MetaMask将CLT发送给其他人,你会发现我们需要在页面上更新CLT的账户余额。我们将在任务6中完成这一工作。在任务5中,我们将首先为用户创建转账组件。
## 任务5:执行写操作(转账)
继续在Web App中与智能合约交互,现在执行一个写操作
### 任务5.1:添加空的`TransferERC20`组件
“`typescript
// 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>
)
}
“`
导入并添加这个组件到index.tsx:
“`typescript
<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>
“`
### 任务5.2: 实现transfer()函数
在`TransferERC20.tsx`中实现转移函数:
“`typescript
// 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>
)
}
“`
解释一下:
– 我们调用`transfer(address recipient, uint256 amount) → bool`,这是ERC20智能合约的状态变化函数。
正如你所看到的,转移后ClassToken的余额没有改变。我们将在任务6中解决这个问题:
## 任务6:监听事件:在Web 应用中与智能合约交互
我们可以通过智能合约事件的设计来更新CLT余额。对于ERC20代币智能合约,当转账在链上被确认时,会发出一个事件`Transfer(address from, address to, uint256 value)`([文档](https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#IERC20-Transfer-address-address-uint256-))。
我们可以在Node.js webapp中监听这个事件并更新页面显示。
### 任务6.1: 了解智能合约事件
简单解释事件:当我们调用会智能合约的状态变化函数时,有三个步骤:
– 第1步:链外调用。我们使用JavaScript API(ethers.js)在链外调用智能合约的状态变化函数。
– 第2步:链上确认。状态改变交易需要由矿工使用共识算法在链上的几个区块进行确认。所以我们不能立即得到结果。
– 第3步:触发事件。一旦交易被确认,就会发出一个事件。你可以通过监听事件来获得链外的结果。

### 任务6.2:当当前账户变化时,添加事件监听器
编辑`ReadERC20.tsx`:
“`typescript
//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])
“`
这个代码片段改编自[How to Fetch and Update Data From 以太坊 with React and SWR](https://consensys.net/blog/developers/how-to-fetch-and-update-data-from-ethereum-with-react-and-swr/)。
解释一下:
– 当`currentAccount`改变时(useEffect),我们添加两个监听器:一个用于从currentAccount转移的事件,另一个用于转移到currentAccount。
– 当监听到一个事件时,查询currentAccount的token余额并更新页面。
你可以在页面上或在MetaMask中从当前账户转账,你会看到页面在事件发生时正在更新。
当完成任务6时,你已经建立了一个简单而实用的DAPP,它有智能合约和Web页面。
综上所述,DAPP有三个部分:
– 智能合约和区块链
– Web 应用(用户界面),通过智能合约获取和设置数据
– 用户控制的钱包(这里是MetaMask),作为资产管理工具和用户的签名者,以及区块链的连接器。
通过这些任务,我们还了解到3种与智能合约交互的方式:
– 读取:从智能合约中获取数据
– 写:在智能合约中更新数据
– 监听,监听智能合约发出的事件
在本教程中,我们直接使用`ethers.js`来连接到区块链。作者还有一篇使用 Web3-react 库进行连接的文章,有机会在翻译。
英文原文:https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
scaffold-eth 因为引入内容太多了,对于我来说太复杂了, 不知道大家有没有同感,找到一篇使用 React 开发 DApp 的非常简单入门教程。翻译一下.
在本教程中,我们将使用Hardhat、React和ethers.js构建DAPP,它可以与用户控制的钱包如MetaMask一起使用。
DAPP通常由三部分组成:
- 部署在链上的智能合约
- 用Node.js、React和Next.js构建的Webapp(用户界面)
- 钱包(用户在浏览器中控制的/移动钱包App)
我们使用ethers.js
来连接各个:
在DApp(webapp)的用户界面中,MetaMask等钱包给开发者提供了一个以太坊的提供者,我们可以在Ethers.js
中使用,与区块链进行交互。(具体来说,钱包提供的是一个 “连接器”,ethers.js创建 “提供者(provider)”和/或 “签名者(signer)”供我们使用)。
作为用户,我们可能已经知道了MetaMask的用法,作为开发者,我们将学习如何使用MetaMask和它注入浏览器的window.ethereum
(MetaMask开发者文档。
本教程的代码库在: Hardhat项目:https://github.com/fjun99/chain-tutorial-hardhat-starter Webapp项目:https://github.com/fjun99/web3app-tutorial-using-ethers
特别感谢在准备webapp代码库时,Wesley的Proof-of-Competence, POC项目学到了很多。我们也像他的项目一样使用Chakra UI。你可能也会发现网页与POC几乎一样。
前置知识和工具
在我们开始之前,你需要对一下内容有一些了解:
知识:
- 区块链
- 以太坊
- 钱包
- Solidity
- ERC20 & ERC721
- Ethers.js
工具:
- MetaMask (钱包浏览器插件)
- Node.js, yarn, TypeScript
- OpenZeppelin (Solidity库)
- Etherscan区块浏览器
让我们开始建立一个DApp
任务1:设置开发环境
为了建立一个DApp,我们要做两个工作:
- 使用Hardhat和Solidity构建智能合约
- 使用Node.js、React和Next.js构建Web 应用。
我们将把目录组织成两个子目录chain
和webapp
:
- hhproject
- chain (working dir for hardhat)
- contracts
- test
- scripts
- webapp (working dir for NextJS app)
- src
- pages
- components
任务1.1 安装Hardhat并启动Hardhat项目
安装Hardhat,这是一个以太坊开发环境。
要使用Hardhat,你需要在电脑上有node.js
和yarn
:
- 第1步:建立一个目录并在其中安装Hardhat
mkdir hhproject && cd hhproject
mkdir chain && cd chain
yarn init -y
安装Hardhat:
yarn add hardhat
- 第2步:创建一个Hardhat样本项目
yarn hardhat
//choose: Create an advanced sample project that uses TypeScript
用我们将在任务3中使用的样本智能合约Greeter.sol
创建一个hardhat项目。
- 第3步:运行Harhat网络(本地testnet)
yarn hardhat node
将运行一个本地测试网(chainId: 31337)。
开始了HTTP和WebSocket JSON-RPC服务器,地址是http://127.0.0.1:8545/
它提供了20个账户,每个账户有10000.0测试ETH
。
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
...
请注意,Hardhat Network本地testnet有两种模式:进程中模式和独立模式。我们用命令行yarn hardhat node
运行一个独立的testnet。当运行像yarn hardhat compile
这样的命令行时,如果没有网络参数(--network localhost
),我们就运行一个进程内测试网。
任务1.2:在Hardhat的开发
我们将在Hardhat开发环境中体验智能合约的开发过程。
在Hardhat启动的项目中,默认包含有智能合约、测试脚本和部署脚本的样本。
├── contracts
│ └── Greeter.sol
├── scripts
│ └── deploy.ts
├── test
│ └── index.ts
├── hardhat.config.ts
我想改变测试和部署脚本的文件名:
- contracts
- Greeter.sol
- test
- Greeter.test.ts (<-index.ts)
- scripts
- deploy_greeter.ts (<-deploy.ts)
第1步:运行命令,显示账户:
yarn hardhat accounts
这是在hardhat.config.ts
中添加的hardhat 样本任务。
第2步:编译智能合约
yarn hardhat compile
第3步:运行单元测试
yarn hardhat test
第4步:尝试部署到进程中的测试网
yarn hardhat run ./scripts/deploy_greeter.ts
在接下来的两个步骤中,将运行一个独立的Hardhat网络,并将智能合约部署上去。
第5步:运行一个独立的本地测试网
在另一个终端,运行:
yarn hardhat node
//Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
第6步: 部署到独立的本地测试网
yarn hardhat run ./scripts/deploy.ts --network localhost
//Greeter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
如果你多次运行部署,你会发现合约实例被部署到不同的地址。
任务1.3:将MetaMask切换到本地测试网
确保Hardhat Network 本地测试网仍在运行(你可以通过命令yarn hardhat node
来运行它)。
- 第1步:在MataMask浏览器插件中,点击顶部栏的网络选择。将网络从
mainnet
切换到localhost 8545
。 - 第2步:点击顶栏上的账户图标,进入
设置/网络/
。选择localhost 8445
。
注意:确保链ID是31337。在MetaMask中,它可能默认为 1337
。
任务 1.4: 用Next.js和Chakra UI创建webapp
我们将使用Node.js
、React
、Next.js
和Chakra UI
框架创建一个webapp。(你可以选择你喜欢的任何其他UI框架,如Material UI
,Ant Design
等。你也可能想选择前端框架Vue
而不是Next.js
)
- 第1步:创建Next.js项目
webapp
。
在hhproject/
目录下,运行:
yarn create next-app webapp --typescript
//will make a sub-dir webapp and create an empty Next.js project in it
cd webapp
- 第2步:改变一些默认值并运行webapp
我们将使用src
作为应用程序目录,而不是pages
(关于src
和pages
的更多信息在Next.js docs)。
mkdir src
mv pages src/pages
mv styles src/styles
vim tsconfig.json
//in "compilerOptions" add:
// "baseUrl": "./src"
运行Next.js应用程序并在浏览器中查看。
yarn dev
//ready - started server on 0.0.0.0:3000, url: http://localhost:3000
浏览http://localhost:3000
。
- 第3步:安装Chakra UI
通过运行Chakra UI(文档)来安装:
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
我们将在下一个子任务中编辑next.js应用程序,使其适合我们的项目。
任务1.5:编辑webapp
webapp 会包含头部、layout、_app.tsx、index.tsx 等
- 第1步:添加一个页眉组件
mkdir src/components
touch src/components/header.tsx
编辑header.tsx
为:
//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>
)
}
- 第2步:添加Next.js布局
添加布局(Next.js文档)
touch src/components/layout.tsx
编辑layout.tsx
为:
// 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>
)
}
- 第3步:在
_app.tsx
和布局中添加Chakra UI Provider
编辑_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
- 第4步:编辑
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
你可能还想添加_documents.tsx
(docs)来定制你的Next.js应用程序中的页面。
你可能想删除这个项目中不需要的文件,如src/styles
。
- 第5步:运行webapp
yarn dev
在http://localhost:3000/ 的页面将看起来像:
你可以从github scaffold repo下载代码:
在你的’hhproject/’目录下。
git clone git@github.com:fjun99/web3app-tutorial-using-ethers.git webapp
cd webapp
yarn install
yarn dev
任务2:通过MetaMask将DApp连接到区块链上
在这个任务中,我们将创建一个DAPP,它可以通过MetaMask连接到区块链(本地测试网)。
我们将使用Javascript API库Ethers.js
与区块链交互。
任务2.1:安装Ethers.js
在webapp/
目录下,添加Ethers.js
:
yarn add ethers
任务2.2:连接到MetaMask钱包
我们将在index.tsx
上添加一个按钮:
- 当未连接时,按钮文本为
Connect Wallet(连接钱包)
。点击即可通过MetaMask链接区块链。 - 当连接时,按钮文本是连接的账户地址。用户可以点击断开连接。
我们将获得当前账户的ETH余额并显示在页面上,以及区块链网络信息。
有关于连接MetaMask的以太坊文档(文档链接)。
我写了一张PPT来解释connector
、provider
、signer
和ethers.js
中的wallet之间的关系!
我们将使用react hook功能useState
和useEffect
。
相关代码片段在 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
解释一下:
- 我们添加了两个UI组件:一个用于连接钱包,一个用于显示账户和链的信息。
-
当
连接MetaMask
按钮被点击时,执行:- 通过连接器(即
window.ethereum
, 他是MetaMask注入到页面的)获得Web3Provider
,。 - 调用
eth_requestAccounts
,这将要求MetaMask确认分享账户信息。用户在MetaMask的弹出窗口确认或拒绝该请求。 - 将返回的账户设置为
currentAccount
。
- 通过连接器(即
- 当断开连接被调用时,我们重置currentAccount和余额。
-
每次改变currentAccount时,都会调用副作用函数(useEffect),在这里执行查询:
- 通过调用
getBalance
查询当前账户的ETH余额。 - 通过调用
getNetwork()
来查询网络信息。
- 通过调用
请注意:
- 在页面中断开连接,不会改变MetaMask的连接和该页面的权限。打开MetaMask扩展,你会发现你的钱包仍然连接到这个页面。下次你再点击
连接MetaMask
按钮时,MetaMask不会弹出确认窗口(因为你的确认仍然有效)。你智能通过MetaMask来断开钱包和页面的连接。 - 当用户在MetaMask中切换网络时,我们没有编写代码来显示变化。
- 我们没有存储这个页面的状态。因此,当页面被刷新时,连接被重置。
任务3:使用OpenZeppelin构建ERC20智能合约
在任务3中,我们将使用OpenZeppelin库构建ERC20智能合约(ERC20 docs)。
任务3.1: 编写一个ERC20智能合约 – ClassToken
添加OpenZeppelin/contract
:
yarn add @openzeppelin/contracts
到chain/
目录下,添加ccontracts/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);
}
}
任务 3.2 编译智能合约
yarn hardhat compile
//Solidity compilation should succeed
任务 3.3 添加单元测试脚本
添加单元测试脚本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);
});
});
运行单元测试。
yarn hardhat test
// ClassToken
// Should have the correct initial supply (392ms)
// 1 passing (401ms)
任务 3.4 添加部署脚本
添加部署脚本 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;
});
我们部署ClassToken,并将initialSupply 10000.0 CLT
发送到部署者(msg.sender
)。
尝试运行合约部署到进程中的Hardhat Network本地测试网(进程中模式)。
yarn hardhat run scripts/deploy_classtoken.ts
任务3.5 运行独立的测试网,向其部署智能合约
在另一个终端,在chain/
目录下运行:
yarn hardhat node
在当前终端,运行连接到localhost --network localhost
的hardhat任务。
yarn hardhat run scripts/deploy_classtoken.ts --network localhost
//ClassToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
任务3.6 在hardhat控制台与ClassToken交互
运行连接到独立的本地测试网的hardhat控制台。
yarn hardhat console --network localhost
在控制台中与 ClassToken
交互:
formatEther = ethers.utils.formatEther;
address = '0x5FbDB2315678afecb367f032d93F642f64180aa3';
token = await ethers.getContractAt("ClassToken", address);
totalSupply = await token.totalSupply();
formatEther(totalSupply)
//'10000.0'
ethers.getContractAt()
是Hardhat插件hardhat-ethers
提供的一个辅助函数,文档链接。
任务3.7:将代币添加到MetaMask中
添加Token到MetaMask,地址为:0x5FbDB2315678afecb367f032d93F642f64180aa3
。(请使用你得到的部署合约地址。)
我们可以看到0号账户(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
)有10000.0 CLT
。
你也可以从github repo下载hardhat示例项目。
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
任务4:读取合约数据–在webapp中与智能合约交互
在任务4和任务5中,我们将继续构建我们的webapp。
我们将允许用户与新部署的ERC20代币智能合约–ClassToken(CLT)
进行交互。
任务4.1: 添加空的ReadERC20
组件来读取ClassToken
在webapp目录中,添加一个空组件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>
)
}
导入并添加这个组件到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>
在下面的子任务中,我们将逐步添加ReadERC20
组件的功能。
任务4.2:准备智能合约ABI
要在Javascript中与智能合约交互,我们需要它的ABI。
合约应用二进制接口(ABI)是与以太坊生态系统中的合约交互的标准方式。数据是根据其类型进行编码的。
ERC20智能合约是一个标准,我们将使用一个文件而不是Hardhat项目中输出的编译工件。我们添加的是人类可读的ABI。
添加一个目录src/abi
并添加文件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)"
];
任务4.3: 当组件加载时查询智能合约信息
我们使用React钩子useEffect
来查询组件加载时的智能合约信息。
编辑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>
)
}
解释一下:
- 钩子
useEffect(()=>{},[])
将只被调用一次。 - 用
window.ethereum
(通过MetaMask注入到页面)创建一个Web3Provider。 - 在
ethers.js
中用addressContract
、abi
、provider
创建一个合约实例。 - 调用只读函数
symbol()
,totalSupply()
,并将结果设置为反应状态的变量,可以在页面上显示。
任务 4.3: 当账户变化时,查询当前账户的CLT余额
编辑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>
)
解释一下:
- 当
currentAccount
改变时,副作用钩子函数useEffect(()=>{},[currentAccount]
被调用,调用balanceOf(address)
来获得余额。 - 当我们刷新页面时,没有当前账户,也没有显示余额。在我们连接钱包后,余额被查询到并显示在页面上。
还有更多的工作要做:
- 当MetaMask切换账户时,我们的Web 应用不知道,也不会改变页面的显示,因此需要监听MetaMask的账户变化事件。
- 当当前账户的余额发生变化时,由于当前账户没有被改变,我们的Web应用程序将不会更新。
你可以使用MetaMask将CLT发送给其他人,你会发现我们需要在页面上更新CLT的账户余额。我们将在任务6中完成这一工作。在任务5中,我们将首先为用户创建转账组件。
任务5:执行写操作(转账)
继续在Web App中与智能合约交互,现在执行一个写操作
任务5.1:添加空的TransferERC20
组件
// 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>
)
}
导入并添加这个组件到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>
任务5.2: 实现transfer()函数
在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>
)
}
解释一下:
- 我们调用
transfer(address recipient, uint256 amount) → bool
,这是ERC20智能合约的状态变化函数。
正如你所看到的,转移后ClassToken的余额没有改变。我们将在任务6中解决这个问题:
任务6:监听事件:在Web 应用中与智能合约交互
我们可以通过智能合约事件的设计来更新CLT余额。对于ERC20代币智能合约,当转账在链上被确认时,会发出一个事件Transfer(address from, address to, uint256 value)
(文档)。
我们可以在Node.js webapp中监听这个事件并更新页面显示。
任务6.1: 了解智能合约事件
简单解释事件:当我们调用会智能合约的状态变化函数时,有三个步骤:
- 第1步:链外调用。我们使用JavaScript API(ethers.js)在链外调用智能合约的状态变化函数。
- 第2步:链上确认。状态改变交易需要由矿工使用共识算法在链上的几个区块进行确认。所以我们不能立即得到结果。
- 第3步:触发事件。一旦交易被确认,就会发出一个事件。你可以通过监听事件来获得链外的结果。
任务6.2:当当前账户变化时,添加事件监听器
编辑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])
这个代码片段改编自How to Fetch and Update Data From 以太坊 with React and SWR。
解释一下:
- 当
currentAccount
改变时(useEffect),我们添加两个监听器:一个用于从currentAccount转移的事件,另一个用于转移到currentAccount。 - 当监听到一个事件时,查询currentAccount的token余额并更新页面。
你可以在页面上或在MetaMask中从当前账户转账,你会看到页面在事件发生时正在更新。
当完成任务6时,你已经建立了一个简单而实用的DAPP,它有智能合约和Web页面。
综上所述,DAPP有三个部分:
- 智能合约和区块链
- Web 应用(用户界面),通过智能合约获取和设置数据
- 用户控制的钱包(这里是MetaMask),作为资产管理工具和用户的签名者,以及区块链的连接器。
通过这些任务,我们还了解到3种与智能合约交互的方式:
- 读取:从智能合约中获取数据
- 写:在智能合约中更新数据
- 监听,监听智能合约发出的事件
在本教程中,我们直接使用ethers.js
来连接到区块链。作者还有一篇使用 Web3-react 库进行连接的文章,有机会在翻译。
英文原文:https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi
本文参与区块链开发网写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 2022-07-04 14:56
- 阅读 ( 1699 )
- 学分 ( 205 )
- 分类:DApp