# 初级用例

By [icepy](https://paragraph.com/@icepy) · 2022-03-17

---

这是一个关于如何配置 hardhat，创建前端工程，编写智能合约，部署智能合约，使用 ethers.js 与之交互的用例。

> 说明：必须先安装 [nodejs](https://nodejs.org/en/)，接下来我们所有的编程都会依赖和使用到它。

准备阶段
----

在本地终端上使用命令创建一个空目录，并且快速生成 package.json 文件，package.json 是 nodejs 用于管理项目和模块的文件：

    $ mkdir useweb3
    $ cd useweb3
    $ npm init -y
    

安装 Hardhat

    $ npm install hardhat --save-dev
    

让我们对 `package.json` 做一些适当的修改，`scripts` 字段上：

    "scripts": {
      "init": "hardhat",
      "node": "hardhat node",
      "test": "hardhat test",
      "compile": "hardhat compile"
    }
    

在终端处运行 npm run init，且选择 create a basic sample project 来初始化项目。

![](https://storage.googleapis.com/papyrus_images/ce72ab4a529ae79eb20c839ce9ae3d29376b04ff61588e13776c96cfca2cbea1.png)

现在我们得到了一个有如下目录结构的项目：

*   contracts 编写合同的目录
    
*   scripts 编写部署脚本的目录
    
*   test 编写测试脚本的目录
    
*   hardhat.config.js hardhat 的配置文件
    

接下来，我们可以利用 `create-react-app` 来创建前端工程

> 说明：你也可以选择 Next.js

    $ npx create-react-app client
    

最后我们需要完善 hardhat 的配置和部署脚本，在根目录中创建一个.env文件，并安装 `dotenv`：

    $ npm install dotenv --save-dev
    

**⚠️⚠️⚠️ .env 文件由于存放了私钥，千万不要提交！！！**

我们主要将合约部署在本地和 rinkeby 测试网络，所以 .env 文件中，只配置这两项。

    ALCHEMY_API_KEY_URL=""
    LOCALHOST_URL=""
    PRIVATE_KEY=""
    LOCALHOST_PRIVATE_KEY=""
    

在 [https://www.alchemy.com/](https://www.alchemy.com/) 上创建一个 rinkeby 项目：

![](https://storage.googleapis.com/papyrus_images/e75295306edcd85dcde5d0fa6aaddc0d1f5abb09fa86b6f145fe1303fc377dba.png)

点击 VIEW KEY 来获取 URL 粘贴到 .env 文件的 ALCHEMY\_API\_KEY\_URL 字段

![](https://storage.googleapis.com/papyrus_images/823bf346c4cafcabfed9ef8d15775552e8ebb0883bb5fec50a93eda112cf9ed4.png)

私钥可在 metamask 上获取，粘贴到 .env 文件的 PRIVATE\_KEY 字段

![](https://storage.googleapis.com/papyrus_images/6c2bc8c642114f31f4a5373ec2c90b35b6a8ff0c0e21b265730c1c6f11885ce3.png)

接下来我们需要配置本地网络，在终端上使用 npm run node 启动节点，hardhat 会自动创建 20 个地址，每一个地址上都有 10000 个 ETH，随便获取其中一个的私钥：

    $ npm run node
    

粘贴到 .env 文件的 LOCALHOST\_PRIVATE\_KEY 字段

本地网络 [http://localhost:8545](http://localhost:8545) 粘贴到 .env 文件 的 LOCALHOST\_URL 字段

![](https://storage.googleapis.com/papyrus_images/18fd6f9fa4094e538b891036c7cc96a260931670ce7af6cb9c56f47526f62d53.png)

链 ID 修改为 31337

现在我们需要对 `hardhat.config.js` 做一些修改：

    require("@nomiclabs/hardhat-waffle");
    require("dotenv").config({path: '.env'});
    
    // This is a sample Hardhat task. To learn how to create your own go to
    // https://hardhat.org/guides/create-task.html
    task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
      const accounts = await hre.ethers.getSigners();
    
      for (const account of accounts) {
        console.log(account.address);
      }
    });
    
    // You need to export an object to set up your config
    // Go to https://hardhat.org/config/ to learn more
    
    const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL;
    const LOCALHOST_URL = process.env.LOCALHOST_URL;
    const PRIVATE_KEY = process.env.PRIVATE_KEY;
    const LOCALHOST_PRIVATE_KEY = process.env.LOCALHOST_PRIVATE_KEY;
    
    /**
     * @type import('hardhat/config').HardhatUserConfig
     */
    module.exports = {
      solidity: "0.8.4",
      networks: {
        rinkeby: {
          url: ALCHEMY_API_KEY_URL,
          accounts: [PRIVATE_KEY],
        },
        localhost: {
          url: LOCALHOST_URL,
          accounts: [LOCALHOST_PRIVATE_KEY]
        }
      }
    };
    

最后在 scripts 目录中创建 `deploy.js` 文件，并修改 package.json 在 scripts 中增加两个配置：

    {
      "rinkeby": "hardhat run scripts/deploy.js --network rinkeby",
      "localhost": "hardhat run scripts/deploy.js --network localhost"
    }
    

`deploy.js` 因为还是一个空文件，所以并不急于测试。

编写一个简单的合约
---------

在 contracts 目录中创建一个 Hello 合约，在初始化时将 Hello useweb3 传递给它，并且实现一个函数可返回这个字符串 ！

`hardhat/console.sol` 是 hardhat 提供可以打印日志的模块

    // SPDX-License-Identifier: SEE LICENSE IN LICENSE
    pragma solidity ^0.8.4;
    
    import "hardhat/console.sol";
    
    contract Hello {
    
      string text;
    
      constructor(string memory _text) {
        console.log("%s", _text);
        text = _text;
      }
    
      function say() public view returns (string memory) {
        return text;
      }
    }
    

现在我们需要在`deploy.js` 文件中编写我们的部署脚本：

    const hre = require("hardhat");
    
    async function main(){
      const Hello = await hre.ethers.getContractFactory("Hello");
      const hello = await Hello.deploy("hello useweb3");
      hello.deployed();
    
      console.log("Hello Contract Address: ", hello.address);
    }
    
    main()
      .then(() => process.exit(0))
      .catch((error) => {
        console.error(error);
        process.exit(1);
      });
    

现在，我们如果想将合约部署到本地网络可运行：

    $ npm run localhost
    

![](https://storage.googleapis.com/papyrus_images/0b76f8e1238f551039b8fe44347a99c9e0aa5a80c59a2f43d1a2f917a4040f00.png)

如果我们想将合约部署到rinkeby测试网络可运行：

    $ npm run rinkeby
    

通过在终端上输出的合约地址，在 [https://rinkeby.etherscan.io/](https://rinkeby.etherscan.io/) 处查看。

最后，我们需要在 test 目录创建一个 Hello.js 用于测试

    const { expect } = require("chai");
    const { ethers } = require("hardhat");
    
    describe("Hello", function () {
      it("测试 say 函数的返回值", async function () {
        const value = "hello useweb3";
        const Hello = await ethers.getContractFactory("Hello");
        const hello = await Hello.deploy(value);
        await hello.deployed();
    
        expect(await hello.say()).to.equal(value);
      });
    });
    

![](https://storage.googleapis.com/papyrus_images/db4b3406f920d28b7f8c6d0cee1c8ac9dd13d286c2d8ebff33a556db0423552a.png)

编写一个简单的可交互界面
------------

> 说明：后续的开发与测试都在本地网络上进行

我们需要将合约进行一些修改，将状态存储到区块链上，并且通过钱包的交互来改变状态。

    // SPDX-License-Identifier: SEE LICENSE IN LICENSE
    pragma solidity ^0.8.4;
    
    import "hardhat/console.sol";
    
    contract Hello {
    
      string text;
      uint256 count;
    
      constructor(string memory _text) {
        console.log("%s", _text);
        text = _text;
        count = 0;
      }
    
      function addition() public{
        count += 1;
      }
    
      function getCount() public view returns (uint256) {
        return count;
      }
    
      function say() public view returns (string memory) {
        return text;
      }
    }
    

重新运行 npm run localhost 来部署合约。

在进入之前又需要普及一个知识点：ABI（Application Binary Interface）和 ethers.js。

*   ABI是合约接口的说明，内容包括合约的接口列表、接口名称、参数名称、参数类型、返回类型等，这些信息以 JSON 格式保存，可以在 solidity 文件编译时由合约编译器生成。
    
*   ethers.js 是目前前端用来与合约进行交互的基础库，提供只读（Provider）和可读可写（Signer）的能力
    

在 client/src 目录中创建一个 abi 目录并创建一个 Hello.json 文件，现在我们需要将之前部署好的合约生成的对应 JSON 复制过来。

> 说明：由于只支持 metamask 钱包进行交互，因此直接依赖 Web3Provider

    import React, { useEffect, useState } from 'react';
    import { ethers } from 'ethers';
    import HelloJSON from './abi/Hello.json';
    import './Hello.css'
    
    const HELLO_CONTRACT_ADDRESS = "0xB581C9264f59BF0289fA76D61B2D0746dCE3C30D";
    const abi = HelloJSON.abi;
    
    
    const getWeb3Provider = () => {
      let web3Provider = window.web3Provider;
      if (!web3Provider){
        web3Provider = new ethers.providers.Web3Provider(window.ethereum);
        window.web3Provider = web3Provider;
      }
      return web3Provider;
    }
    
    export default function Hello(){
      const [address, setAddress] = useState('');
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        if (!address){
          const web3Provider = getWeb3Provider();
          const selectedAddress = web3Provider.provider.selectedAddress;
          if (!selectedAddress){
            web3Provider.provider.request({ method: 'eth_requestAccounts' }).then((accounts) => {
              const account = accounts[0];
              setAddress(account);
            });
          } else {
            setAddress(selectedAddress);
          }
        }
      }, [address]);
    
      const onGetCount = async () => {
        const web3Provider = getWeb3Provider();
        const signer = web3Provider.getSigner();
        const helloContract = new ethers.Contract(HELLO_CONTRACT_ADDRESS, abi, signer);
        const result = await helloContract.getCount();
        setCount(result.toNumber());
      }
    
      const onAddition = async () => {
        const web3Provider = getWeb3Provider();
        const signer = web3Provider.getSigner();
        const helloContract = new ethers.Contract(HELLO_CONTRACT_ADDRESS, abi, signer);
        await helloContract.addition();
      }
    
      return (
        <div className='helloContainer'>
          <div>address: {address}</div>
          <div className='helloButtons'>
            <button onClick={onGetCount}>Update Count</button>
            <button onClick={onAddition}>Addition</button>
          </div>
          <div>count: {count}</div>
        </div>
      );
    }
    

点击 Addition 来修改区块链的状态

![](https://storage.googleapis.com/papyrus_images/aeb5b440d706ab9e4c7a78cfd9d5f267682059576f81f7ab3ac26aeb3d5d901c.png)

点击 Update Count 来更新 count 的值

![](https://storage.googleapis.com/papyrus_images/467951b5e8d84e2f236142051d6bcb399c32b198ab213d24d9c9585ab73e276b.png)

---

*Originally published on [icepy](https://paragraph.com/@icepy/fS76qrBgwhiPziIip7oJ)*
