进阶 docs.web3js.org 2026-10-19 02:32:30 · 11 阅读
第12章 在 Hardhat 中使用 Web3.js v4 插件开发智能合约
使用 Web3 和 Hardhat
简介
随着 Hardhat 插件 hardhat-web3-v4 最近完成兼容性更新,现在可以在 Hardhat 中将 Web3.Js 作为插件使用。
本教程将指导你如何使用 Web3js 作为插件来与区块链进行交互。
info 本教程说明如何从零开始创建一个 Web3.js + Hardhat 项目。create-hardhat-web3 命令行工具旨在自动化本教程中的许多步骤,从而简化快速构建新的 Web3.js + Hardhat 项目。
前置条件
本教程假设你具备编写智能合约的基础知识,并且熟悉 JavaScript/TypeScript 开发。你应已安装 NodeJS 版本 v16 或更高。
note 安装依赖项时,我们将使用 NPM。
步骤
初始化 Hardhat 项目
安装必需依赖项(包括 hardhat-web3-v4)
编写智能合约
编译和部署合约
测试并与合约交互
初始化 Hardhat 项目
创建一个新的项目文件夹并进入其中。
mkdir myproject
cd myproject
在当前项目中安装并初始化 Hardhat。 npm install hardhat
npx hardhat init
安装必需依赖项(包括 hardhat-web3-v4) 选择 TypeScript,其余选项均选择 Yes。 系统将提示你安装所需的依赖项,回复 yes 以完成安装。 为了包含 Hardhat-web3-v4 插件,我们需要通过 npm 安装它。 npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
这将把 Web3.Js 添加到你的项目中,使其包含在 'node_modules' 文件夹中。为了使用该插件扩展 Hardhat 功能,我们需要在 Hardhat 配置文件 hardhat.config.ts 中导入 web3-v4 插件。请在该配置文件顶部添加以下导入。 import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-web3-v4'; // <================
const config: HardhatUserConfig = {
solidity: '0.8.19',
};
export default config;
默认情况下,hardhat-toolbox 会被添加到项目中,但你需要显式调用该插件。这样会修改 Hardhat 运行时环境(HRE),使其同时包含 Web3 类和已实例化的 web3 对象。后者的优势在于,你获得的是一个经过修改、开箱即用的 web3 对象,其中已配置了初始化的本地/Hardhat 提供程序。该对象可在项目中的任意位置使用,例如测试文件和部署文件。 编写智能合约 当你创建一个新项目时,Hardhat 会提供一个示例 Lock 合约。请参阅 myproject/contracts/Lock.sol 文件中的合约。 // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// 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);
}
}
Lock.sol 是一个简单的时间锁合约,可以在部署时接收任意数量的 Ether,并预期一个参数,该参数在部署时会被赋值给一个状态变量。withdraw 函数仅允许被标记为 owner 的地址提取合约的总余额,但前提是 'unlockTime' 不在未来。 编译、测试和部署合约 编译和部署合约 npx hardhat compile
运行上面的命令会生成一个名为 artifacts 的文件夹,里面包含构建信息和编译后的合约。之后在测试和部署时,我们会从这个目录中用到 jsonInterface,也就是应用二进制接口(ABI)。 要部署合约,需要按如下方式修改 script/deploy.ts 文件。 首先,从 hardhat 导入初始化好的 web3 对象,然后获取 artifacts。 import { web3 } from 'hardhat';
import artifacts from '../artifacts/contracts/Lock.sol/Lock.json';
async function main() {}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
在 main 函数中,我们借助 web3.js 的部分功能(比如 .utils 和 .eth 模块)来准备部署。 async function main() {
const currentTimestampInSeconds = Math.round(Date.now() / 1000);
const unlockTime = currentTimestampInSeconds + 60;
const lockedAmount = web3.utils.toWei('0.001', 'ether');
const [deployer] = await web3.eth.getAccounts();
const lockContract = new web3.eth.Contract(artifacts.abi);
const rawContract = lockContract.deploy({
data: artifacts.bytecode,
arguments: [unlockTime],
});
const lock = await rawContract.send({
from: deployer,
gasPrice: '10000000000',
value: lockedAmount.toString(),
});
console.log(
`Lock with ${web3.utils.toWei(
lockedAmount,
'ether',
)}ETH and unlock timestamp ${unlockTime} deployed to ${lock.options.address}`,
);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
运行下面的命令会把 Lock 合约部署到 Hardhat 内置的本地区块链上。我们通过 Web3.js 与区块链通信,把智能合约数据广播到网络中。 npx hardhat run scripts/deploy.ts
测试与合约交互 在前面的步骤中,我们已经将合约编译并部署到本地区块链网络。现在该测试合约,确认其表现符合预期。由于我们此前使用 Web3.Js 与区块链交互以广播和保存数据,这里也将沿用同一协议来查看和修改数据。 在 myproject/test/Lock.ts 文件中,用以下代码替换原有内容。 import { time, loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { expect } from 'chai';
import { web3 } from 'hardhat';
import artifacts from '../artifacts/contracts/Lock.sol/Lock.json';
describe('Lock', function () {
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;
const lockContract = new web3.eth.Contract(artifacts.abi);
lockContract.handleRevert = true;
const [deployer, otherAccount] = await web3.eth.getAccounts();
const rawContract = lockContract.deploy({
data: artifacts.bytecode,
arguments: [unlockTime],
});
// 通过预估来了解 gas 消耗量。
const estimateGas = await rawContract.estimateGas({
from: deployer,
value: lockedAmount.toString(),
});
const lock = await rawContract.send({
from: deployer,
gas: estimateGas.toString(),
gasPrice: '10000000000',
value: lockedAmount.toString(),
});
console.log('Lock contract deployed to: ', lock.options.address);
return { lock, unlockTime, lockedAmount, deployer, otherAccount, rawContract };
}
describe('Deployment', function () {
it('Should set the right unlockTime', async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);
const setTime = await lock.methods.unlockTime().call();
console.log('SetTime', setTime);
expect(setTime).to.equal(unlockTime);
});
it('Should set the right deployer', async function () {
const { lock, deployer } = await loadFixture(deployOneYearLockFixture);
expect(await lock.methods.owner().call()).to.equal(deployer);
});
it('Should receive and store the funds to lock', async function () {
const { lock, lockedAmount } = await loadFixture(deployOneYearLockFixture);
const balance = await web3.eth.getBalance(String(lock.options.address));
expect(balance).to.equal(lockedAmount);
});
it("Shouldn't fail if the unlockTime has arrived and the deployer calls it", async function () {
const { lock, unlockTime, deployer } = await loadFixture(deployOneYearLockFixture);
await time.increaseTo(unlockTime);
await expect(lock.methods.withdraw().send({ from: deployer })).not.to.be.reverted;
});
});
});
在这个文件中,我们执行了与部署脚本相似的步骤,使用 deployOneYearLockFixture() 中的 ABI 和字节码来准备并部署合约。要从区块链读取所有者数据,我们使用了已部署合约的实例,即 lock.methods.owner().call()。调用 .call() 不会改变区块链状态,因此不需要钱包签名。 若要更改之前保存的数据状态,我们需要访问目标函数的方法容器,并调用 .send 将意图广播到网络 lock.methods.withdraw().send({from: deployer})。 注意:使用 .send() 时,必须在 from 字段中明确提供交易发送者(在上述示例中是 deployer 账户的地址)。 要运行测试,可以使用以下命令 npx hardhat test test/Lock.ts
你将获得类似以下的结果: /*
Lock
Deployment
Lock contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
SetTime 1739193193n
✔ Should set the right unlockTime (884ms)
✔ Should set the right deployer (54ms)
✔ Should receive and store the funds to lock
✔ Shouldn't fail if the unlockTime has arrived and the deployer calls it
*/
cd myproject
在当前项目中安装并初始化 Hardhat。 npm install hardhat
npx hardhat init
安装必需依赖项(包括 hardhat-web3-v4) 选择 TypeScript,其余选项均选择 Yes。 系统将提示你安装所需的依赖项,回复 yes 以完成安装。 为了包含 Hardhat-web3-v4 插件,我们需要通过 npm 安装它。 npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
这将把 Web3.Js 添加到你的项目中,使其包含在 'node_modules' 文件夹中。为了使用该插件扩展 Hardhat 功能,我们需要在 Hardhat 配置文件 hardhat.config.ts 中导入 web3-v4 插件。请在该配置文件顶部添加以下导入。 import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-web3-v4'; // <================
const config: HardhatUserConfig = {
solidity: '0.8.19',
};
export default config;
默认情况下,hardhat-toolbox 会被添加到项目中,但你需要显式调用该插件。这样会修改 Hardhat 运行时环境(HRE),使其同时包含 Web3 类和已实例化的 web3 对象。后者的优势在于,你获得的是一个经过修改、开箱即用的 web3 对象,其中已配置了初始化的本地/Hardhat 提供程序。该对象可在项目中的任意位置使用,例如测试文件和部署文件。 编写智能合约 当你创建一个新项目时,Hardhat 会提供一个示例 Lock 合约。请参阅 myproject/contracts/Lock.sol 文件中的合约。 // SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// 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);
}
}
Lock.sol 是一个简单的时间锁合约,可以在部署时接收任意数量的 Ether,并预期一个参数,该参数在部署时会被赋值给一个状态变量。withdraw 函数仅允许被标记为 owner 的地址提取合约的总余额,但前提是 'unlockTime' 不在未来。 编译、测试和部署合约 编译和部署合约 npx hardhat compile
运行上面的命令会生成一个名为 artifacts 的文件夹,里面包含构建信息和编译后的合约。之后在测试和部署时,我们会从这个目录中用到 jsonInterface,也就是应用二进制接口(ABI)。 要部署合约,需要按如下方式修改 script/deploy.ts 文件。 首先,从 hardhat 导入初始化好的 web3 对象,然后获取 artifacts。 import { web3 } from 'hardhat';
import artifacts from '../artifacts/contracts/Lock.sol/Lock.json';
async function main() {}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
在 main 函数中,我们借助 web3.js 的部分功能(比如 .utils 和 .eth 模块)来准备部署。 async function main() {
const currentTimestampInSeconds = Math.round(Date.now() / 1000);
const unlockTime = currentTimestampInSeconds + 60;
const lockedAmount = web3.utils.toWei('0.001', 'ether');
const [deployer] = await web3.eth.getAccounts();
const lockContract = new web3.eth.Contract(artifacts.abi);
const rawContract = lockContract.deploy({
data: artifacts.bytecode,
arguments: [unlockTime],
});
const lock = await rawContract.send({
from: deployer,
gasPrice: '10000000000',
value: lockedAmount.toString(),
});
console.log(
`Lock with ${web3.utils.toWei(
lockedAmount,
'ether',
)}ETH and unlock timestamp ${unlockTime} deployed to ${lock.options.address}`,
);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
运行下面的命令会把 Lock 合约部署到 Hardhat 内置的本地区块链上。我们通过 Web3.js 与区块链通信,把智能合约数据广播到网络中。 npx hardhat run scripts/deploy.ts
测试与合约交互 在前面的步骤中,我们已经将合约编译并部署到本地区块链网络。现在该测试合约,确认其表现符合预期。由于我们此前使用 Web3.Js 与区块链交互以广播和保存数据,这里也将沿用同一协议来查看和修改数据。 在 myproject/test/Lock.ts 文件中,用以下代码替换原有内容。 import { time, loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers';
import { expect } from 'chai';
import { web3 } from 'hardhat';
import artifacts from '../artifacts/contracts/Lock.sol/Lock.json';
describe('Lock', function () {
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;
const lockContract = new web3.eth.Contract(artifacts.abi);
lockContract.handleRevert = true;
const [deployer, otherAccount] = await web3.eth.getAccounts();
const rawContract = lockContract.deploy({
data: artifacts.bytecode,
arguments: [unlockTime],
});
// 通过预估来了解 gas 消耗量。
const estimateGas = await rawContract.estimateGas({
from: deployer,
value: lockedAmount.toString(),
});
const lock = await rawContract.send({
from: deployer,
gas: estimateGas.toString(),
gasPrice: '10000000000',
value: lockedAmount.toString(),
});
console.log('Lock contract deployed to: ', lock.options.address);
return { lock, unlockTime, lockedAmount, deployer, otherAccount, rawContract };
}
describe('Deployment', function () {
it('Should set the right unlockTime', async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);
const setTime = await lock.methods.unlockTime().call();
console.log('SetTime', setTime);
expect(setTime).to.equal(unlockTime);
});
it('Should set the right deployer', async function () {
const { lock, deployer } = await loadFixture(deployOneYearLockFixture);
expect(await lock.methods.owner().call()).to.equal(deployer);
});
it('Should receive and store the funds to lock', async function () {
const { lock, lockedAmount } = await loadFixture(deployOneYearLockFixture);
const balance = await web3.eth.getBalance(String(lock.options.address));
expect(balance).to.equal(lockedAmount);
});
it("Shouldn't fail if the unlockTime has arrived and the deployer calls it", async function () {
const { lock, unlockTime, deployer } = await loadFixture(deployOneYearLockFixture);
await time.increaseTo(unlockTime);
await expect(lock.methods.withdraw().send({ from: deployer })).not.to.be.reverted;
});
});
});
在这个文件中,我们执行了与部署脚本相似的步骤,使用 deployOneYearLockFixture() 中的 ABI 和字节码来准备并部署合约。要从区块链读取所有者数据,我们使用了已部署合约的实例,即 lock.methods.owner().call()。调用 .call() 不会改变区块链状态,因此不需要钱包签名。 若要更改之前保存的数据状态,我们需要访问目标函数的方法容器,并调用 .send 将意图广播到网络 lock.methods.withdraw().send({from: deployer})。 注意:使用 .send() 时,必须在 from 字段中明确提供交易发送者(在上述示例中是 deployer 账户的地址)。 要运行测试,可以使用以下命令 npx hardhat test test/Lock.ts
你将获得类似以下的结果: /*
Lock
Deployment
Lock contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
SetTime 1739193193n
✔ Should set the right unlockTime (884ms)
✔ Should set the right deployer (54ms)
✔ Should receive and store the funds to lock
✔ Shouldn't fail if the unlockTime has arrived and the deployer calls it
*/