使用 React 和 ethers.js 构建DApp

  • 张小风
  • 更新于 2022-07-04 14:56
  • 阅读 6133

使用 React 开发 DApp 入门教程

scaffold-eth 因为引入内容太多了,对于我来说太复杂了, 不知道大家有没有同感,找到一篇使用 React 开发 DApp 的非常简单入门教程。翻译一下.

在本教程中,我们将使用Hardhat、React和ethers.js构建DAPP,它可以与用户控制的钱包如MetaMask一起使用。

DAPP通常由三部分组成:

  • 部署在链上的智能合约
  • 用Node.js、React和Next.js构建的Webapp(用户界面)
  • 钱包(用户在浏览器中控制的/移动钱包App)

我们使用ethers.js来连接各个:

DApp解释

在DApp(webapp)的用户界面中,MetaMask等钱包给开发者提供了一个以太坊的提供者,我们可以在Ethers.js中使用,与区块链进行交互。(具体来说,钱包提供的是一个 "连接器",ethers.js创建 "提供者(provider)"和/或 "签名者(signer)"供我们使用)。

作为用户,我们可能已经知道了MetaMask的用法,作为开发者,我们将学习如何使用MetaMask和它注入浏览器的window.ethereumMetaMask开发者文档

本教程的代码库在: 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几乎一样。


前置知识和工具

在我们开始之前,你需要对一下内容有一些了解:

知识:

工具:

  • 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

任务1.1 安装Hardhat并启动Hardhat项目

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

要使用Hardhat,你需要在电脑上有node.jsyarn

  • 第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.jsReactNext.jsChakra UI框架创建一个webapp。(你可以选择你喜欢的任何其他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
  • 第2步:改变一些默认值并运行webapp

我们将使用src作为应用程序目录,而不是pages(关于srcpages的更多信息在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/ 的页面将看起来像:

DAPP webapp

你可以从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钱包

显示ETH余额

我们将在index.tsx上添加一个按钮:

  • 当未连接时,按钮文本为 Connect Wallet(连接钱包)。点击即可通过MetaMask链接区块链。
  • 当连接时,按钮文本是连接的账户地址。用户可以点击断开连接。

我们将获得当前账户的ETH余额并显示在页面上,以及区块链网络信息。

有关于连接MetaMask的以太坊文档(文档链接)。

我写了一张PPT来解释connectorprovidersignerethers.js中的wallet之间的关系!

connector, provider, signer

我们将使用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

解释一下:

  • 我们添加了两个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)进行交互。

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

导入并添加这个组件到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中用addressContractabiprovider创建一个合约实例。
  • 调用只读函数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

点赞 6
收藏 11
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
张小风
张小风
0xD305...609D
ETH