# 你的第一个Web3 DApp

By [IrStar](https://paragraph.com/@irstar) · 2022-05-19

---

现在网络上已经有很多Web3应用教程的资源，可是作为新手往往还是很容易一头雾水。因为现有的技术栈实在太多了，同时大部分教程会默认读者对教程中所使用的技术很熟悉。于是即使是获取余额这样的简单功能，不同教程会给出完全不同的实现方法。实在是很不友好。

我作为一个只会一点c++/python的初学者，之前基本没有接触过前端代码，妥妥的初学者。整理本文一方面希望能给别的初学者些微帮助，同时也算是记录一下，免得自己过段时间忘记了如何操作。所以本文尽量不涉及DApp技术栈的底层细节，而是着眼于先写出一个可用的东西。想要在这个领域继续深入的读者，自然会再去自行寻找更多的技术知识来提高。

本文部分例子来自于[《Web3 Tutorial: build DApp with Web3-React and SWR》](https://dev.to/yakult/tutorial-build-dapp-with-web3-react-and-swr-1fb0)。

特别注意：因为本人完全不掌握前端技能，所以代码中的页面展示部分存在诸多不合理与需优化的地方。但这些都不是重点，本文的目标就是先让程序跑起来。

本文全部代码（不包含各种依赖库）已上传至[Github](https://github.com/IrStar/hello-web3)

[https://github.com/IrStar/hello-web3](https://github.com/IrStar/hello-web3)

*   包管理：npm
    
*   IDE：VS Code
    
*   前端页面：Next.js + TypeScript
    
*   UI库：NextUI
    
*   Provider: web3-react
    
*   wallet: MetaMask
    
*   合约环境: hardhat
    

1\. 搭建前端页面
----------

建立文件夹`/hello-web3`作为我们的项目目录。通常来说，我们需要开发两部分内容：前端页面、链上合约。有些情况下可能还需要服务端，在本文先不涉及。为了方便，我们在本文会使用两个不同目录来保存相应内容。

我们使用[Next.js](https://www.nextjs.cn/docs) + [NextUI](https://nextui.org/docs/guide/getting-started) + TypeScript来进行前端页面开发。由于typescript是javascript的超集，web3-react也是基于typescript来编写，所以为了便于 照抄 学习其代码，我们也选择使用typescript。

    cd ./hello-web3
    npx create-next-app webapp --typescript
    ...
    
    cd webapp
    

这时会自动创建一个名为`/webapp`的文件夹，我们会在其中存放前端部分的代码。为了看起来比较整洁，我们使用`src`作为root路径。（[Next.js关于src文件夹的说明](https://nextjs.org/docs/advanced-features/src-directory)）

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

这里还需要一步操作，打开`tsconfig.json`文件，

    // 在"compilerOptions"中添加：
        "baseUrl": "./src",
    

然后运行 `npm run dev`，浏览器会自动打开一个窗口，也可以自己打开访问`http://localhost:3000` 显示Next.js的欢迎页面。

![Next.js的欢迎页面](https://storage.googleapis.com/papyrus_images/31a25ee033e399ce2a29987fdc8c6be9a3d61660daa4cac9f5d270fedff1b062.png)

Next.js的欢迎页面

我们使用[NextUI](https://nextui.org/)作为我们的UI库。

    npm i @nextui-org/react
    

在`/src`目录下新建`/components`目录，并在其中新建文件`layout.tsx`

    import Head from 'next/head'
    import React, { ReactNode } from 'react'
    import styles from 'styles/layout.module.css'
    
    type Props = {
      children: ReactNode
    }
    
    export default function Layout(props: Props) {
      return (
        <>
          <Head>
            <title>Hello Web3 World.</title>
          </Head>
          <main className={styles.main}>{props.children}</main>
        </>
      )
    }
    

相应地，在`/src/styles`目录下新建文件`layout.module.css`，内容非常简单

    .main {
      height: calc(100vh - 64px);
      background-color: white;
      padding: 0rem 8rem;
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    

接下来修改`/src/pages/_app.tsx`

    import type { AppProps } from 'next/app'
    import Layout from 'components/layout'
    
    export default function MyApp({ Component, pageProps }: AppProps) {
      return (
        <Layout>
          <Component {...pageProps} />
        </Layout>
      )
    }
    

最后回来修改`/src/pages/index.tsx`

    import type { NextPage } from 'next'
    import { Grid, Card, Text, Button} from "@nextui-org/react";
    
    interface Props {
      title: string,
      content: string
    }
    
    const HomeCard = (props:Props) => {
      return (
        <>
          <Card hoverable bordered shadow={false} css={{my: "10px"}}>
            <Text h4> {props.title} </Text>
            <Card.Footer>
              <Text> {props.content} </Text>
            </Card.Footer>
          </Card>
        </>
      )
    }
    
    const Home: NextPage = () => {
    
      return (
        <>
          <Grid.Container justify="flex-start">
            <Text h2 css={{my: "1rem", px: "5px"}}>
              第一个 Web3 DApp
            </Text>
            <HomeCard title='本文目标' content='让DApp跑起来' />
          </Grid.Container>
        </>
      )
    }
    
    export default Home
    

刷新浏览器窗口，可以看到新的页面。

![简单的前端页面，我们将在此基础上添加更多内容](https://storage.googleapis.com/papyrus_images/8205c56314d6c8a74fd3ad02245f26af6741e9b6e3fcf0ec55f80c2bf2a40dfd.png)

简单的前端页面，我们将在此基础上添加更多内容

2\. 前端页面与MetaMask连接
-------------------

目前web3-react有[stable V6](https://github.com/NoahZinsmeister/web3-react/tree/v6)和[beta V8](https://github.com/NoahZinsmeister/web3-react/tree/main)两个版本，我们在这里使用V6版本。回到`/webapp`文件夹，使用npm安装web3-react

    npm i @web3-react/core @web3-react/injected-connector ethers
    

接下来修改`/src/pages/_app.tsx`，首先增加两个`import`

    import { Web3ReactProvider } from '@web3-react/core'
    import { Web3Provider } from '@ethersproject/providers'
    

增加`getLibrary`函数

    function getLibrary(provider: any): Web3Provider {
      return new Web3Provider(provider)
    }
    

然后对`MyApp`函数进行修改

    export default function MyApp({ Component, pageProps }: AppProps) {
      return (
        <Web3ReactProvider getLibrary={getLibrary}>
        <Layout>
          <Component {...pageProps} />
        </Layout>
        </Web3ReactProvider>
      )
    }
    

接下来我们以连接MetaMask为例，在`/src/components`目录中新建一个`connectMetaMask.tsx`文件。如果要连接其它的钱包，使用web3-react提供的useWeb3React也可以很方便地实现，具体方法可以参考[这篇文章](https://hackmd.io/Ykpp1MWLTjixIZG2ZJEShA)。

    import { useEffect } from 'react'
    import { Grid, Card, Button, Text} from '@nextui-org/react'
    import { useWeb3React } from '@web3-react/core'
    import { Web3Provider } from '@ethersproject/providers'
    import { UserRejectedRequestError } from '@web3-react/injected-connector'
    import { injectedConnector } from 'utils/connectors'
    import { formatAddress } from 'utils/helpers'
    
    const ConnectMetaMask = () => {
    
        const { account, chainId, active, library, connector, 
          activate, deactivate, setError } = useWeb3React<Web3Provider>()
    
        const onClickConnect = () => {
          activate(injectedConnector,(error) => {
            if (error instanceof UserRejectedRequestError) {
              // ignore user rejected error
              console.log("user refused")
            } else {
              setError(error)
            }
          }, false)
        }
    
        const onClickDisconnect = () => {
            deactivate()
          }
    
        useEffect(() => {
          console.log(chainId, account, active, library, connector)
        })
    
        return (
          <>
            <Card bordered shadow={false} css={{my: "10px"}}>
              {active && typeof account === 'string' 
                ? <Grid><Button onPress={onClickDisconnect} >
                    Account: {formatAddress(account,4)}
                    </Button>
                    <Text>ChainID: {chainId} connected</Text>
                  </Grid>
                : <Grid><Button onPress={onClickConnect}>
                    Connect MetaMask
                    </Button>
                  <Text> not connected </Text>
                  </Grid>
              }
            </Card>
          </>
        )
    }
    
    export default ConnectMetaMask
    

> 注意：本文使用的是web-react的**V6**版本。当使用**V8**版本时，const { library } = useWeb3React()语句中的**library**需要修改为**provider**
> 
> 这里用到了两个功能函数。在`/src`目录下新建`utils`文件夹，并新建`connectors.tsx`和`helpers.tsx`两个文件。其中`connector.tsx`列出了当前支持的链ID，也可以再手动添加[新的链ID](https://chainlist.org/)。
> 
> > 这里要注意的是，我们稍后会使用hardhat来进行合约开发和本地链的部署，所使用的链ID是**31337**，而在MetaMask中默认的本地链ID是**1337**，所以一定要记得修改，不然会导致连接失败。
> 
>     import { InjectedConnector } from "@web3-react/injected-connector";
>     
>     export const injectedConnector = new InjectedConnector({
>         supportedChainIds: [
>             1,      // mainnet
>             3,      // ropsten
>             4,      // rinkeby
>             5,      // goerli
>             42,     // kovan
>             31337,  // localhost
>         ]
>     })
>     
> 
> `helpers`中提供了将40位地址只显示前后各4位的方法。
> 
>     export function formatAddress(value: string, length: number = 4) {
>         return `${value.substring(0, length + 2)}...${value.substring(value.length - length)}`
>     }
>     
> 
> 记得将我们刚刚新建的`connectMetaMask`引入到`index.tsx`中，然后刷新页面。
> 
>     import ConnectMetaMask from 'components/connectMetaMask'
>     ...
>             <ConnectMetaMask />
>     
> 
> 可以看到我们的页面已经具备了与MetaMask连接的能力，并显示当前的链。
> 
> ![未连接状态](https://storage.googleapis.com/papyrus_images/fda5eb24937d062d2fe1cc3e7e20255ef1dc66d6902cbf05aac869bff6ad5356.png)
> 
> 未连接状态
> 
> ![连接了以太坊主链](https://storage.googleapis.com/papyrus_images/9b0d817493cc6072087ebe75eccaa20c223c5bf283e80c72c0b0fed29e6544fc.png)
> 
> 连接了以太坊主链
> 
> 作为一个入门教程，我们只在本地链上操作，先不去访问真实链上的数据，所以就不需要Relay Network了。未来真实上链的时候，可以考虑[Infura](https://infura.io/)或者[Alchemy](https://www.alchemy.com/)。我个人使用Alchemy，主要是因为个人感觉它的免费方案更实惠。还有[Moralis](https://moralis.io/)提供了直接访问区块链状态的功能，避免反复调用第三方网络的API，不过我对这个功能的需求不是很强，所以暂时还没有尝试。
> 
> 3\. 合约编写
> --------
> 
> 我们首先回到`/hello-web3`，创建`/chain`文件夹，并进行初始化。我们将把合约相关的代码放在这个目录里。
> 
>     mkdir chain
>     cd chain
>     npm init -y
>     
> 
> 接下来我们需要安装[hardhat](https://hardhat.org/)作为本地的Solidity合约开发环境，并创建起始项目。
> 
>     npm i hardhat
>     
> 
> 创建起始项目
> 
> > 这里会出现几个选项。使用箭头上下移动，选择**Create an advanced sample project that uses TypeScript** 其实选择另外几个选项也是可以的，不过我懒得手动安装工具包，就让hardhat自己看着来了
> 
> 一路回车。hardhat会自动安装一些工具包，等待安装完成后即可。hardhat会自动生成一个样例合约，一个部署脚本和一个测试脚本。有兴趣的同学可以试试。我们先把它们删除，再利用[OpenZeppelin](https://docs.openzeppelin.com/contracts/4.x/)编写一个简单的ERC20合约，那么先安装OpenZeppelin
> 
>     npm i @openzeppelin/contracts
>     
> 
> 接下来，在`/chain/contracts`目录中新建`HelloToken.sol`
> 
>     //SPDX-License-Identifier: MIT
>     pragma solidity ^0.8.0;
>     
>     import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
>     
>     contract HelloToken is ERC20 {
>         constructor(uint supply) ERC20("HelloToken", "HLT") {
>             _mint(msg.sender, supply);
>         }
>     }
>     
> 
> 该合约定义了一种叫做HelloToken的ERC20代币，简写符号为HLT，当有人在链上部署该合约时，会给部署者发放若干数量的代币。
> 
> 我们快速写一个单元测试脚本`/chain/test/HelloToken.test.ts`
> 
>     import { expect } from "chai";
>     import { ethers } from "hardhat";
>     
>     describe("HelloToken Initial Supply", function () {
>       it("Should have the correct initial supply", async function () {
>         const initialSupply = ethers.utils.parseEther('10000.0')
>         const HelloToken = await ethers.getContractFactory("HelloToken");
>         const token = await HelloToken.deploy(initialSupply);
>         await token.deployed();
>     
>         expect(await token.totalSupply()).to.equal(initialSupply);
>       });
>     });
>     
> 
> 再写一个简单的部署脚本`/chain/scripts/HelloToken_deploy.ts`
> 
>     import { ethers } from "hardhat";
>     
>     async function main() {
>       const initialSupply = ethers.utils.parseEther('10000.0')
>       const HelloToken = await ethers.getContractFactory("HelloToken");
>       const token = await HelloToken.deploy(initialSupply);
>       await token.deployed();
>       console.log("HelloToken deployed to:", token.address);
>     }
>     
>     main().catch((error) => {
>       console.error(error);
>       process.exitCode = 1;
>     });
>     
> 
> 这里需要注意一点，hardhat的本地测试链有in-process和stand-alone两种模式。当没有指定网络时，默认使用in-process模式。但是由于in-process模式并不具有独立的链ID，所以大部分情况下我们需要使用的是stand-alone模式。为了启动stand-alone模式的本地测试链，首先打开一个新的终端窗口并导航到当前的`/chain`目录中，然后运行
> 
> 稍等一下，就能看到本地测试链启动成功，并创建了20个账号地址与它们的私钥，每个地址上有10000个ETH。
> 
> ![共创建20个账号地址与相应私钥。注意由于这些地址的私钥是公开的，所以任何人都可以操作主链上的这些地址。千万不要在主链上给这些地址转账！有兴趣的同学可以去Etherscan上面看看主链上的这几个地址里面的存款和交易记录，被坑过的人不少啊！](https://storage.googleapis.com/papyrus_images/1b8e49428555f61fce3370726ea3ff5cbceda5b31c3f1c161a0bdec19a042f60.png)
> 
> 共创建20个账号地址与相应私钥。注意由于这些地址的私钥是公开的，所以任何人都可以操作主链上的这些地址。千万不要在主链上给这些地址转账！有兴趣的同学可以去Etherscan上面看看主链上的这几个地址里面的存款和交易记录，被坑过的人不少啊！
> 
> 回到之前的终端窗口中，来运行单元测试。
> 
>     npx hardhat test --network localhost
>     
> 
> 注意，如果只输入`npx hardhat test`而忽略掉后面的`--network localhost`，则使用的就是in-process模式。
> 
> 那么继续，在本地测试链上部署我们的ERC20合约。
> 
>     npx hardhat run scripts/HelloToken_deploy.ts --network localhost
>     
> 
> 部署成功后，通过部署脚本打印出了合约的部署地址。
> 
> ![这里显示的就是合约在本地测试链上的部署地址](https://storage.googleapis.com/papyrus_images/d2f375c27a016b20b24f96b799456889272290c81a6db97f541797d6a6b4e783.png)
> 
> 这里显示的就是合约在本地测试链上的部署地址
> 
> > 一定要注意的是，每次重新部署都可能让合约地址发生变化，所以在与合约交互时一定要注意传递的合约地址是否正确。有兴趣的同学也可以尝试通过hardhat提供的控制台来与合约进行交互，这里就省略了。
> 
> 当我们部署合约时，可以看到前面执行了`npx hardhat node`的控制台窗口中一直有信息输出，包括在链上执行的只读指令和出块信息。持续一段时间之后，可能会导致我们无法查看之前创建的账号地址与私钥。这时可以在另一个终端窗口中执行`npx hardhat accounts`，来查看当前测试链上的账号地址信息。
> 
> 4\. 通过页面读取合约
> ------------
> 
> 为了读取合约，我们需要让前端知道合约都提供了哪些可用方法。回到`/hello-web3/webapp/src`目录，新建`/abi`文件夹，并在其中新建`HelloTokenABI.tsx`文件。
> 
>     export const HelloTokenABI = [
>         // Read-Only Functions
>         "function balanceOf(address owner) view returns (uint)",
>         "function totalSupply() view returns (uint)",
>         "function symbol() view returns (string)",
>     ];
>     
> 
> 我们使用了合约中提供的三个只读方法。其中`balanceOf`返回指定地址中的代币数量，`totalSupply`返回代币的总发行数量，`symbol`返回代币的简写符号。
> 
> 在`/src/components`目录中新建`HelloToken.tsx`
> 
>     import React, { useEffect,useState } from 'react'
>     import { Card, Text } from "@nextui-org/react"
>     import {HelloTokenABI as abi} from 'abi/HelloTokenABI'
>     import { useWeb3React } from '@web3-react/core'
>     import { Web3Provider } from '@ethersproject/providers'
>     import {Contract} from "@ethersproject/contracts";
>     import { formatEther } from "@ethersproject/units"
>     import { parseEther }from "@ethersproject/units"
>     
>     interface Props {
>         addressContract: string
>     }
>     
>     export default function HelloToken(props:Props){
>       const addressContract = props.addressContract
>       const [symbol,setSymbol]= useState<string>("")
>       const [totalSupply,setTotalSupply]=useState<string>()
>       const [balance,setBalance]=useState<string|undefined>(undefined)
>     
>       const { account, active, library } = useWeb3React<Web3Provider>()
>     
>       useEffect( () => {
>         if(!(active && account && library)) return
>     
>         const contract = new Contract(addressContract, abi, library);
>         library.getCode(addressContract).then((result:string)=>{
>           //check whether it is a contract
>           if(result === '0x') return
>           
>           contract.symbol().then((result:string) => {
>             setSymbol(result)
>           }).catch('error', console.error)
>       
>           contract.totalSupply().then((result:string) => {
>             setTotalSupply(formatEther(result))
>           }).catch('error', console.error)
>         })
>       //called only when changed to active
>       },[active])
>     
>       useEffect(()=>{
>         if(!(active && account && library)) {
>             setBalance(undefined)
>             return
>         }
>     
>         const contract = new Contract(addressContract, abi, library);
>         library.getCode(addressContract).then((result:string)=>{
>           //check whether it is a contract
>           if(result === '0x') return
>     
>           contract.balanceOf(account).then((result:string) => {
>             setBalance(parseFloat(formatEther(result)).toFixed(2))
>           }).catch('error', console.error)
>         })
>       },[active,account])
>     
>     　return (
>         <>
>           <Card hoverable bordered shadow={false} css={{my: "10px"}}>
>             <Text><b>HelloToken合约地址</b>: {addressContract}</Text>
>             <Text><b>Token总发行量</b>: {totalSupply} {symbol}</Text>
>             <Text><b>当前钱包中的HelloToken数量</b>: {balance} {symbol}</Text>
>           </Card>
>         </>
>       )
>     }
>     
> 
> > 这里同样注意，当将web-react从**V6**版本更新到**V8**版本后，原有的const { library } = useWeb3React()需要将**library**修改为**provider**
> > 
> > 相应还要修改`/src/pages/index.tsx`
> > 
> >     import HelloToken from 'components/HelloToken';
> >     ...
> >         const addressContract = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
> >     ...
> >             <HelloToken addressContract={addressContract} />
> >     
> > 
> > > 注意，这里我们赋给addressContract的值，就是前面所得到的合约地址。如果得到的合约地址变化了，这里也一定要修改成实际的地址。另外，需要将MetaMask的网络切换到 **Localhost 8545**也就是本地测试链。
> > 
> > ![连接钱包后，即可显示当前钱包地址中拥有的HelloToken代币数量](https://storage.googleapis.com/papyrus_images/c977bf6d53b69132bb165b825c9b7d93391d39af6119ee01b61bcaad676f461f.png)
> > 
> > 连接钱包后，即可显示当前钱包地址中拥有的HelloToken代币数量
> > 
> > 5\. 从页面向合约发送数据
> > --------------
> > 
> > 刚才我们只使用了合约当中的只读方法，也就是只能读取链上数据，并不能对数据做出修改。那么现在我们来尝试对链上数据进行修改操作。
> > 
> > 首先还是在`/src/abi/HelloTokenABI.tsx`文件中，增加合约所提供的写入链上状态数据的方法。这里准备使用合约中的`transfer`方法，该方法实现了转账功能，会将调用者地址中的若干数量的代币，发送至目标地址中。
> > 
> >     "function transfer(address to, uint amount) returns (bool)",
> >     
> > 
> > 然后我们修改`/src/components/HelloToken.tsx`文件。首先增加两个hooks，用于设置目标地址和待发送数量
> > 
> >     const [toAddress, setToAddress]=useState<string>("")
> >     const [amount,setAmount]=useState<string>('100')
> >     
> > 
> > 然后增加一个异步函数用于完成实际的转账功能
> > 
> >     async function onTransfer(event:React.FormEvent) {
> >         event.preventDefault()
> >         if(!(active && account && library)) return
> >     
> >         // new contract instance with **signer**
> >         const contract = new Contract(addressContract, abi, library.getSigner());
> >         contract.transfer(toAddress,parseEther(amount)).catch('error', console.error)
> >       }
> >     
> > 
> > > 注意在实例化contract时，与上一节中的方法相比，最后一个参数从**library**变成了**library.getSigner()**，这是因为该操作会改变链上状态数据，所以需要调用者进行签名。当然，还需要支付gas
> > 
> > 然后增加需要在页面上显示的内容，相应也要记得引入NextUI库的Input和Button组件
> > 
> >           <Card hoverable bordered shadow={false} css={{my: "10px"}}>
> >             <form onSubmit={onTransfer}>
> >               <Input label="Amount" type="number" css={{mr: "10px"}}
> >                 required onChange={(e) => setAmount(e.target.value)}/>
> >               <Input label="To Address" type="text" css={{ml: "10px"}}
> >                 required onChange={(e) => setToAddress(e.target.value)} />
> >               <Button type="submit" disabled={!account} css={{mt: "20px"}}> Transfer </Button>
> >             </form>
> >           </Card>
> >     
> > 
> > 刷新浏览器页面。首先连接钱包，然后填入准备转账的代币数量，这里以1000个为例，再从测试链的20个钱包地址中随便选一个地址作为目标地址。点击Transfer发送，MetaMask会弹出来要你签名确认，几秒钟后转账成功！
> > 
> > > 两点需要注意。第一点是发送代币数量不要多于当前钱包的拥有数量，否则合约执行会发生错误，而我们目前还没有处理链上发出的错误。第二点是不要使用当前钱包地址作为转账目标地址，否则合约执行同样会发生错误。
> > 
> > ![MetaMask提示的交易信息，确认无误后点击确认](https://storage.googleapis.com/papyrus_images/915b2f1a52c0fe20be00a9a93532c8ba072abd31be461dd6fae7f8a273f66876.png)
> > 
> > MetaMask提示的交易信息，确认无误后点击确认
> > 
> > ![耐心等待几秒钟，就能看到交易已经成功了](https://storage.googleapis.com/papyrus_images/2025f047778f3bbae388d1e953ff1cff0efeeda5a8a290352f52b5b7c27fa926.png)
> > 
> > 耐心等待几秒钟，就能看到交易已经成功了
> > 
> > 不过虽然转账成功了，但是页面上显示的当前账户金额却没有变化。这是因为我们的useEffect hook并没有被触发。我们接下来就解决这个问题。
> > 
> > 6\. 监听合约事件
> > ----------
> > 
> > 由于链上的合约执行结束后并不会向链外返回数据，所以即使链上状态数据发生了变化，在链外也无法得知。为了解决这个问题，合约会在链上状态数据发生变化时发送事件广播。在链外对合约的事件广播进行监听，就可以确定链上发生了什么事情。例如对于ERC20合约，在transfer执行成功后会发送Transfer事件广播。我们首先修改`/src/abi/HelloTokenABI.tsx`文件，告知前端页面可能会接收到的事件类型。
> > 
> >     "event Transfer(address indexed from, address indexed to, uint amount)",
> >     
> > 
> > 然后修改`/src/components/HelloToken.tsx`文件，增加对Transfer事件的监听。我们之前有一个useEffect hook用于更新账户持币数量，现在需要在其中增加两个监听器，一个用于监听当前地址向外发送代币，另一个用于监听当前地址收到代币。
> > 
> >       useEffect(()=>{
> >         if(!(active && account && library)) {
> >             setBalance(undefined)
> >             return
> >         }
> >     
> >         const contract = new Contract(addressContract, abi, library);
> >         library.getCode(addressContract).then((result:string)=>{
> >           //check whether it is a contract
> >           if(result === '0x') return
> >     
> >           balanceOf(contract, account)
> >     
> >           // listen for changes on an Ethereum address
> >           console.log(`listening for Transfer...`)
> >     
> >           const fromMe = contract.filters.Transfer(account, null)
> >           contract.on(fromMe, (from, to, amount, event) => {
> >             console.log('Transfer|sent', { from, to, amount, event })
> >             balanceOf(contract, account)
> >           })
> >     
> >           const toMe = contract.filters.Transfer(null, account)
> >           contract.on(toMe, (from, to, amount, event) => {
> >             console.log('Transfer|received', { from, to, amount, event })
> >             balanceOf(contract, account)
> >           })
> >     
> >           // remove listener when the component is unmounted
> >           return () => {
> >             contract.removeAllListeners(fromMe)
> >             contract.removeAllListeners(toMe)
> >           }
> >         })
> >       },[active,account])
> >     
> > 
> > 我们将查询当前地址代币余额的方法封装为了`balanceOf`方法。
> > 
> >       async function balanceOf(contract:Contract, account:any) {
> >         contract.balanceOf(account).then((result:string) => {
> >             setBalance(parseFloat(formatEther(result)).toFixed(2))
> >           }).catch('error', console.error)
> >       }
> >     
> > 
> > 刷新浏览器页面，连接钱包，发送代币。耐心等待几秒钟，链上交易完成后，页面自动刷新成功。
> > 
> > ![我们再发送1000个代币，成功后页面自动刷新，显示当前钱包中只剩下了8000个HelloToken](https://storage.googleapis.com/papyrus_images/d7c3687030d563b3eaaba0cda2851d9c977d8754bd5a95c6630285202204921b.png)
> > 
> > 我们再发送1000个代币，成功后页面自动刷新，显示当前钱包中只剩下了8000个HelloToken
> > 
> > 大功告成。
> > 
> > 当然，这个DApp还非常简单，缺少实际生产环境中必须具备的很多功能。比如当链上合约抛出revert错误时如何处理、不同钱包的切换等等。合约提供的功能也非常简单。现有代码还存在着大量的优化空间。不过这毕竟是一个开始。

---

*Originally published on [IrStar](https://paragraph.com/@irstar/web3-dapp)*
