这是一个关于如何配置 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 来初始化项目。

现在我们得到了一个有如下目录结构的项目:
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 项目:

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

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

接下来我们需要配置本地网络,在终端上使用 npm run node 启动节点,hardhat 会自动创建 20 个地址,每一个地址上都有 10000 个 ETH,随便获取其中一个的私钥:
$ npm run node
粘贴到 .env 文件的 LOCALHOST_PRIVATE_KEY 字段
本地网络 http://localhost:8545 粘贴到 .env 文件 的 LOCALHOST_URL 字段

链 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

如果我们想将合约部署到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);
});
});

说明:后续的开发与测试都在本地网络上进行
我们需要将合约进行一些修改,将状态存储到区块链上,并且通过钱包的交互来改变状态。
// 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 来修改区块链的状态

点击 Update Count 来更新 count 的值

