注:本文不是教程,仅用于自己记录学习经验。
准备部分
安装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 tasks和Creating 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.js和Hardhat 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