注:本文不是教程,仅用于自己记录学习经验。

准备部分

安装hardhat

use npm7+

npm install --save-dev hardhat

开始构建

创建一个简单的项目

使用npx hardhat init命令初始化项目文件夹。

$ npx hardhat init
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.22.2 👷‍

? What do you want to do? …
❯ Create a JavaScript project
  Create a TypeScript project
  Create a TypeScript project (with Viem)
  Create an empty hardhat.config.js
  Quit

在运行命令后,系统会让选择不同项目语言,默认的是js,本系列也是基于js的基本语言。

运行tasks

hardhat允许你通过在项目文件夹下运行npx hardhat来快速了解可用内容和查看正在发生的情况。

$ npx hardhat
Hardhat version 2.9.9

Usage: hardhat [GLOBAL OPTIONS] <TASK> [TASK OPTIONS]

GLOBAL OPTIONS:

  --config              A Hardhat config file.
  --emoji               Use emoji in messages.
  --help                Shows this message, or a task's help if its name is provided
  --max-memory          The maximum amount of memory that Hardhat can use.
  --network             The network to connect to.
  --show-stack-traces   Show stack traces.
  --tsconfig            A TypeScript config file.
  --verbose             Enables Hardhat verbose logging
  --version             Shows hardhat's version.


AVAILABLE TASKS:

  check                 Check whatever you need
  clean                 Clears the cache and deletes all artifacts
  compile               Compiles the entire project, building all artifacts
  console               Opens a hardhat console
  coverage              Generates a code coverage report for tests
  flatten               Flattens and prints contracts and their dependencies
  help                  Prints this message
  node                  Starts a JSON-RPC server on top of Hardhat Network
  run                   Runs a user-defined script after compiling the project
  test                  Runs mocha tests
  typechain             Generate Typechain typings for compiled contracts
  verify                Verifies contract on Etherscan

To get help for a specific task run: npx hardhat help [task]

同时,hardhat也允许你自己编写tasks,具体可以查看官方文档Writing tasksCreating a task

编译合约

以下是contracts/文件夹下的Lock.sol,也是hardhat在初始化项目文件夹时自动构建的示例合约。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

contract Lock {
    uint public unlockTime;
    address payable public owner;

    event Withdrawal(uint amount, uint when);

    constructor(uint _unlockTime) payable {
        require(
            block.timestamp < _unlockTime,
            "Unlock time should be in the future"
        );

        unlockTime = _unlockTime;
        owner = payable(msg.sender);
    }

    function withdraw() public {
        // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
        // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);

        require(block.timestamp >= unlockTime, "You can't withdraw yet");
        require(msg.sender == owner, "You aren't the owner");

        emit Withdrawal(address(this).balance, block.timestamp);

        owner.transfer(address(this).balance);
    }
}

运行npx hardhat compile,hardhat会自动编译contracts/文件夹下所有合约。
当然,也可以指定合约进行编译。
运行npx hardhat compile ./contracts/Lock.sol将达到同样的效果。
除此之外,还可以运行npx hardhat compile --help来查看npx hardhat compile的具体情况。

$ npx hardhat compile --help
Hardhat version 2.22.2

Usage: hardhat [GLOBAL OPTIONS] compile [--concurrency <INT>] [--force] [--no-typechain] [--quiet]

OPTIONS:

  --concurrency         Number of compilation jobs executed in parallel. Defaults to the number of CPU cores - 1 (default: 15)
  --force               Force compilation ignoring cache
  --no-typechain        Skip Typechain compilation
  --quiet               Makes the compilation process less verbose

compile: Compiles the entire project, building all artifacts

For global options help run: hardhat help

测试合约

hardhat允许使用并集成了Mocha, Chai, Ethers.jsHardhat Ignition等测试工具。
test/文件夹下,同样有一个hardhat初始化时为项目自动生成的测试文件:

const {
  time,
  loadFixture,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const { expect } = require("chai");

describe("Lock", function () {
  // We define a fixture to reuse the same setup in every test.
  // We use loadFixture to run this setup once, snapshot that state,
  // and reset Hardhat Network to that snapshot in every test.
  async function deployOneYearLockFixture() {
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const ONE_GWEI = 1_000_000_000;

    const lockedAmount = ONE_GWEI;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

    // Contracts are deployed using the first signer/account by default
    const [owner, otherAccount] = await ethers.getSigners();

    const Lock = await ethers.getContractFactory("Lock");
    const lock = await Lock.deploy(unlockTime, { value: lockedAmount });

    return { lock, unlockTime, lockedAmount, owner, otherAccount };
  }

  describe("Deployment", function () {
    it("Should set the right unlockTime", async function () {
      const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

      expect(await lock.unlockTime()).to.equal(unlockTime);
    });

    it("Should set the right owner", async function () {
      const { lock, owner } = await loadFixture(deployOneYearLockFixture);

      expect(await lock.owner()).to.equal(owner.address);
    });

    it("Should receive and store the funds to lock", async function () {
      const { lock, lockedAmount } = await loadFixture(
        deployOneYearLockFixture
      );

      expect(await ethers.provider.getBalance(lock.target)).to.equal(
        lockedAmount
      );
    });

    it("Should fail if the unlockTime is not in the future", async function () {
      // We don't use the fixture here because we want a different deployment
      const latestTime = await time.latest();
      const Lock = await ethers.getContractFactory("Lock");
      await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith(
        "Unlock time should be in the future"
      );
    });
  });

  describe("Withdrawals", function () {
    describe("Validations", function () {
      it("Should revert with the right error if called too soon", async function () {
        const { lock } = await loadFixture(deployOneYearLockFixture);

        await expect(lock.withdraw()).to.be.revertedWith(
          "You can't withdraw yet"
        );
      });

      it("Should revert with the right error if called from another account", async function () {
        const { lock, unlockTime, otherAccount } = await loadFixture(
          deployOneYearLockFixture
        );

        // We can increase the time in Hardhat Network
        await time.increaseTo(unlockTime);

        // We use lock.connect() to send a transaction from another account
        await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
          "You aren't the owner"
        );
      });

      it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
        const { lock, unlockTime } = await loadFixture(
          deployOneYearLockFixture
        );

        // Transactions are sent using the first signer by default
        await time.increaseTo(unlockTime);

        await expect(lock.withdraw()).not.to.be.reverted;
      });
    });

    describe("Events", function () {
      it("Should emit an event on withdrawals", async function () {
        const { lock, unlockTime, lockedAmount } = await loadFixture(
          deployOneYearLockFixture
        );

        await time.increaseTo(unlockTime);

        await expect(lock.withdraw())
          .to.emit(lock, "Withdrawal")
          .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg
      });
    });

    describe("Transfers", function () {
      it("Should transfer the funds to the owner", async function () {
        const { lock, unlockTime, lockedAmount, owner } = await loadFixture(
          deployOneYearLockFixture
        );

        await time.increaseTo(unlockTime);

        await expect(lock.withdraw()).to.changeEtherBalances(
          [owner, lock],
          [lockedAmount, -lockedAmount]
        );
      });
    });
  });
});

运行npx hardhat test,hardhat将自动测试test/文件夹下所有测试文件。

$ npx hardhat test
Compiled 2 Solidity files successfully


  Lock
    Deployment
      ✔ Should set the right unlockTime (610ms)
      ✔ Should set the right owner
      ✔ Should receive and store the funds to lock
      ✔ Should fail if the unlockTime is not in the future
    Withdrawals
      Validations
        ✔ Should revert with the right error if called too soon
        ✔ Should revert with the right error if called from another account
        ✔ Shouldn't fail if the unlockTime has arrived and the owner calls it
      Events
        ✔ Should emit an event on withdrawals
      Transfers
        ✔ Should transfer the funds to the owner


  9 passing (790ms)

同样的,也可以通过npx hardhat test ./test/Lock.js来指定测试文件。
运行npx hardhat test --help,hardhat会显示test命令相关的详细信息。

$ npx hardhat test --help
Hardhat version 2.22.2

Usage: hardhat [GLOBAL OPTIONS] test [--bail] [--grep <STRING>] [--no-compile] [--parallel] [...testFiles]

OPTIONS:

  --bail        Stop running tests after the first test failure 
  --grep        Only run tests matching the given string or regexp
  --no-compile  Don't compile before running this task
  --parallel    Run tests in parallel

POSITIONAL ARGUMENTS:

  testFiles     An optional list of files to test (default: [])

test: Runs mocha tests

For global options help run: hardhat help

部署合约

hardhat允许使用Ignition模块部署合约,合约部署文件放置在ignition/modules文件夹下。
以下是hardhat初始化时为Lock合约自动生成的部署文件Lock.js

const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

const JAN_1ST_2030 = 1893456000;
const ONE_GWEI = 1_000_000_000n;

module.exports = buildModule("LockModule", (m) => {
  const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030);
  const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI);

  const lock = m.contract("Lock", [unlockTime], {
    value: lockedAmount,
  });

  return { lock };
});

运行npx hardhat ignition deploy ./ignition/modules/Lock.js,正常情况控制台将打印以下类似内容:

$ npx hardhat ignition deploy ./ignition/modules/Lock.js
Compiled 1 Solidity file successfully (evm target: paris).
You are running Hardhat Ignition against an in-process instance of Hardhat Network.
This will execute the deployment, but the results will be lost.
You can use --network <network-name> to deploy to a different network.

Hardhat Ignition 🚀

Deploying [ LockModule ]

Batch #1
  Executed LockModule#Lock

[ LockModule ] successfully deployed 🚀

Deployed Addresses

LockModule#Lock - 0x5FbDB2315678afecb367f032d93F642f64180aa3

运行npx hardhat ignition --help,可以发现ignition命令详细信息。

$ npx hardhat ignition --help
Hardhat version 2.22.2

Usage: hardhat [GLOBAL OPTIONS] ignition <TASK> [TASK OPTIONS]

AVAILABLE TASKS:

  deploy        Deploy a module to the specified network
  status        Show the current status of a deployment
  verify        Verify contracts from a deployment against the configured block explorers
  visualize     Visualize a module as an HTML report
  wipe          Reset a deployment's future to allow rerunning

ignition: Deploy your smart contracts using Hardhat Ignition

For global options help run: hardhat help

当然,除了使用Ignition,在test中也可以部署合约。

连接hardhat网络

默认情况下,Hardhat 将在启动时启动一个新的内存中 Hardhat 网络实例。还可以以独立方式运行 Hardhat Network,以便外部客户端可以连接到它。这可以是钱包、Dapp 前端或 Hardhat Ignition deployment。
运行npx hardhat node,hardhat会自动生成模拟账户,每个用户具有10000ETH,同时控制台会打印用户公私钥信息。
注意:建议重新打开一个控制台运行,因为该网络实例在开启时需要一直占用当前控制台

$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

运行npx hardhat node --help,控制台将打印该命令详细信息:

$ npx hardhat node --help    
Hardhat version 2.22.2

Usage: hardhat [GLOBAL OPTIONS] node [--fork <STRING>] [--fork-block-number <INT>] [--hostname <STRING>] [--port <INT>]

OPTIONS:

  --fork                The URL of the JSON-RPC server to fork from
  --fork-block-number   The block number to fork from
  --hostname            The host to which to bind to for new connections (Defaults to 127.0.0.1 running locally, and 0.0.0.0 in Docker)
  --port                The port on which to listen for new connections (default: 8545)

node: Starts a JSON-RPC server on top of Hardhat Network

For global options help run: hardhat help