初级用例

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

说明:必须先安装 nodejs,接下来我们所有的编程都会依赖和使用到它。

准备阶段

在本地终端上使用命令创建一个空目录,并且快速生成 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 来初始化项目。

post image

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

  • 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/ 上创建一个 rinkeby 项目:

post image

点击 VIEW KEY 来获取 URL 粘贴到 .env 文件的 ALCHEMY_API_KEY_URL 字段

post image

私钥可在 metamask 上获取,粘贴到 .env 文件的 PRIVATE_KEY 字段

post image

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

$ npm run node

粘贴到 .env 文件的 LOCALHOST_PRIVATE_KEY 字段

本地网络 http://localhost:8545 粘贴到 .env 文件 的 LOCALHOST_URL 字段

post image

链 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
post image

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

$ npm run rinkeby

通过在终端上输出的合约地址,在 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);
  });
});
post image

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

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

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

// 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 来修改区块链的状态

post image

点击 Update Count 来更新 count 的值

post image