发布于 2026-01-06 7 阅读
0

Web3教程:使用Hardhat、React和Ethers.js构建DApp

Web3教程:使用Hardhat、React和Ethers.js构建DApp

在本教程中,我们将使用 Hardhat、React 和 Ethers.js 构建 DAPP,该 DAPP 可与 MetaMask 等用户控制的钱包配合使用。

DAPP通常由三部分组成:

  • 部署在链上的智能合约
  • 使用 Node.js、React 和 Next.js 构建的 Web 应用程序(用户界面)
  • 钱包(用户可通过浏览器/移动钱包应用程序控制)

我们用它Ethers.js来连接这些部件。

DApp详解

在去中心化应用(DApp,即Web应用)的用户界面中,像MetaMask这样的钱包会为开发者提供一个以太坊提供程序,以便我们Ethers.js与区块链进行交互。(具体来说,钱包提供的是“连接器”,而Ethers.js会创建“提供程序”和/或“签名器”供我们使用。)

我们可能已经作为用户了解了 MetaMask 的用法,我们将window.ethereum作为开发者学习如何使用 MetaMask 以及它如何注入到浏览器中(MetaMask 开发者文档)。

您可以在这里找到本教程的代码仓库:
Hardhat 项目:https://github.com/fjun99/chain-tutorial-hardhat-starter
Webapp 项目:https://github.com/fjun99/web3app-tutorial-using-ethers

您可能会发现 DApp 开发人员的第一个操作指南对您的 web3 之旅很有帮助:教程:使用 Remix 和 Etherscan 构建您的第一个 DAPP(作者:Fangjun)

特别鸣谢:在编写 Web 应用代码库的过程中,我从 Wesley 的能力证明 (POC)项目中获益良多。我们也像他的项目一样使用了 Chakra UI。您可能会发现,我们的网页与 POC 项目几乎完全相同。


必备知识和工具

开始之前,你需要一些基础知识和工具。

知识:

  • 区块链
  • 以太坊
  • 钱包
  • 固体
  • ERC20 和 ERC721
  • 以太体.js

工具:

  • MetaMask(钱包浏览器扩展程序)
  • Node.js、yarn、TypeScript
  • OpenZeppelin(Solidity 库)
  • Etherscan区块浏览器

让我们开始构建一个去中心化应用(DApp)。


任务一

任务 1:搭建开发环境

要构建一个去中心化应用(DApp),我们需要完成两种类型的工作:

  • 使用 Hardhat 和 Solidity 构建智能合约
  • 使用 Node.js、React 和 Next.js 构建 Web 应用程序

我们将把目录组织成两个子目录chainwebapp

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

任务 1.1 安装 Hardhat 并初始化 Hardhat 项目

安装 Hardhat,这是一个以太坊开发环境。

要使用安全帽,您的计算机需要安装node.js并运行相关软件。yarn

  • 步骤 1:创建一个目录并将 Hardhat 安装到该目录中。
mkdir hhproject && cd hhproject
mkdir chain && cd chain
yarn init -y
Enter fullscreen mode Exit fullscreen mode

安装 Hardhat:

yarn add hardhat
Enter fullscreen mode Exit fullscreen mode
  • 步骤 2:创建 Hardhat 项目示例
yarn hardhat
//choose: Create an advanced sample project that uses TypeScript
Enter fullscreen mode Exit fullscreen mode

我们创建了一个安全帽项目,其中包含一个示例智能合约,Greeter.sol我们将在任务 3 中使用。

  • 第 3 步:运行 Hardhat Network(本地测试网)
yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

本地测试网正在运行(chainId: 31337):

已在http://127.0.0.1:8545/启动 HTTP 和 WebSocket JSON-RPC 服务器

它提供 20 个账户,每个账户有10000.0 test ETH

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

请注意,Hardhat Network 本地测试网有两种模式:进程内测试网和独立测试网。我们使用命令行运行独立测试网yarn hardhat node。如果在命令行中yarn hardhat compile不带网络参数(例如 `--network-parameters` --network localhost),则运行进程内测试网。

任务 1.2:安全帽中的发展圈

我们将以 Hardhat 开发环境为例,讲解智能合约的开发周期。

Hardhat 发起的项目中包含了示例智能合约、测试脚本和部署脚本。

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

我想更改测试脚本和部署脚本的文件名。

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

步骤 1:运行命令显示账户:

yarn hardhat accounts
Enter fullscreen mode Exit fullscreen mode

这是在 中添加的示例安全帽任务hardhat.config.ts

步骤二:编译智能合约

yarn hardhat compile
Enter fullscreen mode Exit fullscreen mode

步骤 3:运行单元测试

yarn hardhat test
Enter fullscreen mode Exit fullscreen mode

步骤 4:尝试部署到进程内测试网。

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

接下来两步,我们将运行一个独立的 Hardhat 网络,并在其上部署智能合约。

步骤 5:运行独立的本地测试网

在另一个终端中运行:

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

步骤 6:部署到独立的本地测试网

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

如果多次运行部署,你会发现合约实例被部署到不同的地址。

任务 1.3:将 MetaMask 切换到本地测试网

请确保 Hardhat Network 本地测试网仍在运行。(您可以通过命令运行它yarn hardhat node。)

  • 步骤 1:在 MataMask 浏览器扩展程序中,单击顶部栏上的网络选择器。将网络从 切换mainnetlocalhost 8545

  • 步骤 2:点击顶部栏的帐户图标,然后进入“设置/网络/”。选择“localhost 8445”。

注意:请确保链 ID 为31337。在 MetaMask 中,默认值可能为“1337”。

任务 1.4:使用 Next.js 和 Chakra UI 创建 Web 应用程序

我们将使用Node.jsReact和框架创建一个 Web 应用程序。(您可以选择任何其他您喜欢的 UI 框架Next.jsChakra UI例如Material UIAnt 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
Enter fullscreen mode Exit fullscreen mode
  • 步骤 2:更改一些默认设置并运行 Web 应用程序

我们将使用src作为我们的应用程序目录,而不是pages(更多信息srcpages 参阅 Next.js 文档):

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

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

运行 Next.js 应用并在浏览器中查看:

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

浏览http://localhost:3000

  • 步骤 3:安装 Chakra UI

运行以下命令安装 Chakra UI(文档):

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

我们将在下一个子任务中修改 next.js 应用程序,使其适合我们的项目。

任务 1.5:编辑 Web 应用程序 - 头部、布局、_app.tsx、index.tsx

  • 步骤 1:添加头部组件
mkdir src/components
touch src/components/header.tsx
Enter fullscreen mode Exit fullscreen mode

修改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>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • 步骤 2:添加 Next.js 布局

添加布局(文档

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

修改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>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • 步骤 3:将 Chakra UI Provider 添加到“_app.tsx”以及布局文件中

编辑_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
  • 步骤 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
Enter fullscreen mode Exit fullscreen mode

您可能还想添加“_documents.tsx”(文档)来自定义 Next.js 应用程序中的页面。

您可能需要删除此项目中不需要的文件,例如src/styles

  • 步骤 5:运行 Web 应用程序
yarn dev
Enter fullscreen mode Exit fullscreen mode

http://localhost:3000/的页面将显示如下:

DAPP 网络应用程序


您可以从 github scaffold 仓库下载代码

在您的“hhproject/”目录中:

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

任务二

任务 2:通过 MetaMask 将 DApp 连接到区块链

在这个任务中,我们将创建一个可以通过 MetaMask 连接到区块链(本地测试网)的 DAPP。

我们将使用 Javascript API 库与区块链进行交互Ethers.js

任务 2.1:安装Ethers.js

webapp/目录中添加Ethers.js

yarn add ethers
Enter fullscreen mode Exit fullscreen mode

任务 2.2:连接到 MetaMask 钱包

显示以太坊余额

我们将在以下位置添加一个按钮index.tsx

  • 未连接时,按钮文字为“连接钱包”。点击即可通过 MetaMask 连接区块链。

  • 连接成功后,按钮文字会显示已连接的账户地址。用户可以点击按钮断开连接。

我们将获取当前账户的 ETH 余额,并在页面上显示,同时还会显示区块链网络信息。

Ethers.js 文档中有关于连接 MetaMask 的内容(链接)。

connector我写了一张幻灯片来解释,providersigner钱包之间的关系Ethers.js

连接器、提供者、签名者

我们将使用React Hook功能useStateuseEffect

相关代码片段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

一些解释:

  • 我们添加了两个 UI 组件:一个用于连接按钮,一个用于显示帐户和链信息。

  • 点击“连接 MetaMask”按钮后,我们将执行以下操作:

    • 通过MetaMask 注入到页面的Web3Providerconnector( )获取。window.ethereum
    • 调用 MetaMasketh_requestAccounts后,MetaMask 将请求确认共享帐户信息。用户可以在 MetaMask 弹出窗口中确认或拒绝此请求。
    • 将返回的帐户设置为currentAccount
  • 调用断开连接时,我们会重置 currentAccount 和余额。

  • 每次 currentAccount 发生变化时,都会调用副作用函数 (useEffect)。我们将查询:

    • 通过调用以下代码查看当前账户的 ETH 余额getBalance
    • 通过拨打电话获取网络信息getNetwork()

请注意:

  • 断开页面连接不会改变 MetaMask 与此页面的连接和权限。打开 MetaMask 扩展程序,您会发现您的钱包仍然连接到此页面。下次您Connect MetaMask再次点击按钮时,MetaMask 将不会弹出确认窗口(因为您的确认仍然有效)。您需要断开钱包和页面与 MetaMask 的连接。

  • 我们没有编写代码来在 MetaMask 中用户切换网络时显示更改。

  • 我们不会存储此页面的状态。因此,当页面刷新时,连接会被重置。


任务#3

任务 3:使用 OpenZeppelin 构建 ERC20 智能合约

在任务 3 中,我们将使用 OpenZeppelin 库(ERC20 文档)构建 ERC20 智能合约。

任务 3.1:编写一个 ERC20 智能合约——ClassToken

添加OpenZeppelin/contract

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

进入chain/目录并添加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

任务 3.2 编译智能合约

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

任务 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);
  });
});
Enter fullscreen mode Exit fullscreen mode

运行单元测试:

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

任务 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;
});
Enter fullscreen mode Exit fullscreen mode

我们将 ClassToken 部署到 deployer() 中,并将 initialSupply10000.0 CLT发送给 deployer( msg.sender)。

尝试在 Hardhat Network 本地测试网(进程内模式)上运行合约部署:

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

任务 3.5 运行独立测试网,将智能合约部署到该测试网上。

在另一个终端中,运行以下chain/目录:

yarn hardhat node
Enter fullscreen mode Exit fullscreen mode

在当前终端中,运行连接到本地主机的 hardhat 任务--network localhost

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

任务 3.6 在安全帽控制台中与 ClassToken 交互

运行连接到独立本地测试网的 Hardhat 控制台:

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

ClassToken在控制台中交互:

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()是 Hardhat 插件提供的辅助函数hardhat-ethers文档链接

任务 3.7:向 MetaMask 添加令牌

使用地址:将代币添加到 MetaMask 0x5FbDB2315678afecb367f032d93F642f64180aa3。(请使用您获得的已部署合约地址。

我们可以看到 Account#0( 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266) 有10000.0 CLT

您还可以从GitHub 仓库下载安全帽示例项目

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

任务#4

任务 4:读取合约数据 - 在 Web 应用程序中与智能合约交互

在任务 4 和任务 5 中,我们将继续构建我们的 Web 应用程序。

我们将允许用户与新部署的 ERC20 代币智能合约进行交互ClassToken(CLT)

DApp ERC20

任务 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>
  )
}

Enter fullscreen mode Exit fullscreen mode

导入此组件并将其添加到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

在接下来的子任务中,我们将ReadERC20逐步添加组件的功能。

任务 4.2:准备智能合约 ABI

要在 Javascript 中与智能合约交互,我们需要它的ABI

合约应用程序二进制接口(ABI)是以太坊生态系统中与合约交互的标准方式。数据根据其类型进行编码。

ERC20智能合约是一个标准,在我们的Hardhat项目中,我们将使用包含人类可读ABI的文件,而不是编译后的输出文件。我们添加的是人类可读的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)"
];
Enter fullscreen mode Exit fullscreen mode

任务 4.3:组件加载时查询智能合约信息

我们使用 React hookuseEffect在组件加载(挂载)时查询智能合约信息。

编辑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

说明:

  • HookuseEffect(()=>{},[])只会调用一次。

  • 创建一个Web3Provider ,并window.ethereum通过 MetaMask 将以太坊连接器注入到页面中。

  • addressContract使用, abi,provider在 中创建合约实例Ethers.js

  • 调用只读函数symbol()totalSupply()并将结果设置到 React 状态变量中,以便在页面上显示。

任务 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>
  )
Enter fullscreen mode Exit fullscreen mode

解释:

  • 当发生变化时,会调用currentAccount副作用钩子函数。我们调用该函数来获取平衡值。useEffect(()=>{},[currentAccount]balanceOf(address)

  • 刷新页面后,当前账户未显示,余额也未显示。连接钱包后,余额才会查询并显示在页面上。

还有更多工作要做:

  • 当 MetaMask 切换账户时,我们的 Web 应用无法感知,也不会更改页面上的显示内容。我们需要监听 MetaMask 账户切换事件。

  • 当活期账户余额发生变化时,我们的网页应用不会更新,因为您的活期账户余额没有发生变化。

您可以使用 MetaMask 将 CLT 发送给其他人,并且您会发现我们需要在页面上更新 CLT 的账户余额。我们将在任务 6 中完成此操作。在任务 5 中,我们将首先构建用户转账组件。


任务五

任务 5:写入/传输:在 Web 应用中与智能合约交互

任务 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>
  )
}

Enter fullscreen mode Exit fullscreen mode

将此组件导入并添加到 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

任务 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>
  )
}


Enter fullscreen mode Exit fullscreen mode

解释:

  • 我们称之为transfer(address recipient, uint256 amount) → boolERC20 智能合约的状态变更函数。

如您所见,转移后 ClassToken 的余额没有改变。我们将在任务 6 中修复此问题。


任务#6

任务 6:监听事件:与 Web 应用程序中的智能合约进行交互

我们可以通过智能合约事件的设计来更新CLT余额。对于ERC20代币智能合约,当链上确认转账时,会发出一个事件Transfer(address from, address to, uint256 value)文档)。

我们可以在Node.js web应用程序中监听此事件并更新页面显示。

任务 6.1:理解智能合约事件

用通俗易懂的语言解释事件。当我们调用智能合约的状态变更函数时,会经历三个步骤:

  • 第一步:链下调用。我们使用 JavaScript API (ethers.js) 在链下调用智能合约的状态更改函数。

  • 步骤二:链上确认。状态变更交易需要矿工使用共识算法在链上多个区块中进行确认。因此,我们无法立即获得结果。

  • 步骤 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])
Enter fullscreen mode Exit fullscreen mode

此代码片段改编自《如何使用 React 和 SWR 从以太坊获取和更新数据》

解释:

  • currentAccount更改(useEffect)时,我们添加两个监听器:一个用于从 currentAccount 转移事件,另一个用于向 currentAccount 转移事件。

  • 监听事件时,查询 currentAccount 的令牌余额并更新页面。

您可以在页面上或 MetaMask 中从当前帐户转移令牌,您会看到页面会随着事件的发生而更新。

完成任务 6 后,您将构建一个简单但功能齐全的 DAPP,其中包含智能合约和 Web 应用程序。


总而言之,DAPP 由三个部分组成:

  • 智能合约和区块链
  • 用于通过智能合约获取和设置数据的 Web 应用程序(用户界面)
  • 用户控制的钱包(这里是 MetaMask),它既是用户的资产管理工具和签名器,也是区块链的连接器。

在这些任务中,您可能还会注意到我们通过以下 3 种方式与智能合约进行交互:

  • 读取智能合约中的数据
  • 在智能合约中写入和设置数据
  • 倾听,倾听智能合约发出的事件。

本教程中我们Ethers.js直接连接到区块链。但 Web3-react 和其他库可以在 React 中处理与以太坊节点的连接,即使Ethers.js底层代码正在运行。这将是我们下一期教程的主题——使用 Web3-React 构建 DApp。


提示:您可能会发现 DApp 开发人员的第一个操作指南对您的 web3 之旅很有帮助:教程:使用 Remix 和 Etherscan 构建您的第一个 DAPP(作者:Fangjun)


如果你觉得这篇教程有用,请在推特上关注我@fjun99

文章来源:https://dev.to/yakult/a-tutorial-build-dapp-with-hardhat-react-and-ethersjs-1gmi