迁移到Hardhat 3 - 第3部分:任务迁移
本文是Hardhat 2迁移到Hardhat 3系列的最后一部分,专门介绍自定义任务的迁移。
1. 引言
在第一部分中,我们介绍了迁移到 Hardhat 3 的基础内容:基本配置更改和测试迁移。在第二部分中,我们使用 Hardhat Ignition 替换了 hardhat-deploy。
在最后一部分中,我们将介绍自定义 Hardhat 任务迁移——这是迁移到 Hardhat 3 的最后一块拼图。我们的项目中有几个自定义任务,用于管理完整的空投生命周期:生成领取链接和部署合约、收集链上统计数据以及回收无人认领的代币。
我们将涵盖以下内容:
- 任务定义语法:迁移前后对比
- 两种操作模式:内联与延迟加载模块
- Hardhat 2 和 Hardhat 3 之间的参数定义差异
- 从 Hardhat 2 到 Hardhat 3 的 CLI 参数映射
- 任务代码内部的变化:网络访问、返回值以及读取 Ignition 工件
2. 任务定义:迁移前后对比
Hardhat 3 中任务的定义方式发生了重大变化。任务通过构建器(builder)创建,并且必须在配置中显式注册。参数是带有类型定义的结构化对象。操作可以从单独的模块中延迟加载。
以下是我们的 drop 任务在迁移前后的样子。
迁移前 (Hardhat 2):
import { task } from 'hardhat/config';
import { dropTask } from './src/tasks/hardhat-drop-task';
task('drop', '生成 Merkle 空投链接、部署合约并验证所有生成的领取链接')
.addParam('v', '部署版本')
.addParam('a', '要生成的金额')
.addParam('n', '要生成的编码')
.addFlag('debug', '调试模式')
.setAction(async (taskArgs, hre) => {
await dropTask(hre, taskArgs);
});
export default { solidity: {...}, networks: {...} };
迁移后 (Hardhat 3):
import { defineConfig, task } from 'hardhat/config';
import { ArgumentType } from 'hardhat/types/arguments';
const drop = task('drop', '生成 Merkle 空投链接、部署合约并验证所有生成的领取链接')
.addOption({
name: 'ver',
shortName: 'v',
description: '部署版本(默认值:.latest + 1)',
defaultValue: 0,
type: ArgumentType.INT,
})
.addOption({
name: 'amounts',
shortName: 'a',
description: '要生成的空投金额',
defaultValue: 'not set',
type: ArgumentType.STRING,
})
.addOption({
name: 'numbers',
shortName: 'n',
description: '要生成的编码数量',
defaultValue: 'not set',
type: ArgumentType.STRING,
})
.addFlag({
name: 'debug',
description: '调试模式',
})
.setAction(() => import('./src/tasks/drop'))
.build();
export default defineConfig({
// ...
tasks: [drop],
// ...
});
关键区别:
- 构建器模式与
.build()——task()返回一个构建器。你可以链式调用方法,并通过.build()完成构建,返回一个任务对象。如果忘记调用.build(),任务将不会被注册。 - 显式注册 —— 构建好的任务存储在一个变量中,并传递给
defineConfig({ tasks: [...] })。不再有副作用注册。 - 结构化的参数定义 ——
.addParam('v', 'desc')变为.addOption({ name, shortName, description, defaultValue, type })。参数现在通过ArgumentType枚举进行类型化,并原生支持短名称(详见第 4 节)。 - 延迟加载的操作 ——
.setAction()接受一个模块导入,而不是内联函数(下一节将详细介绍)。
3. 操作与内联操作
Hardhat 3 支持两种定义任务操作的模式(参见细节和对比)。
内联操作 —— 直接定义函数:
const myTask = task('my-task', '执行某些操作')
.setInlineAction(async (args, hre) => {
const conn = await hre.network.connect();
console.log('已连接到', conn.networkName);
return successfulResult(true);
})
.build();
这种方式适用于简单任务,但会将逻辑放入 hardhat.config.ts 中。
模块操作(延迟加载) —— 指向一个模块:
const drop = task('drop', '生成 Merkle 空投链接...')
.setAction(() => import('./src/tasks/drop'))
.build();
该模块必须导出一个默认函数,其签名为 (args, hre) => Promise<TaskResult>:
// src/tasks/drop.ts
import { HardhatRuntimeEnvironment } from 'hardhat/types/hre';
import { successfulResult, errorResult } from 'hardhat/utils/result';
interface DropTaskArguments {
ver: number;
amounts: string;
numbers: string;
debug: boolean;
}
export default async function (
args: DropTaskArguments,
hre: HardhatRuntimeEnvironment,
) {
if (args.amounts === 'not set' || args.numbers === 'not set') {
return errorResult(new Error('缺少必需参数'));
}
const conn = await hre.network.connect();
const chainId = conn.networkConfig.chainId ?? 31337;
// ... 任务逻辑 ...
return successfulResult<boolean>(true);
}
我们为所有任务使用了模块模式,因为在这种情况下:
hardhat.config.ts只包含任务定义和参数模式,而不是实现细节。- 任务代码只在任务实际运行时才被加载。
- 每个任务都可以独立维护。我们从一个庞大的文件变成了五个专注的文件,加上
src/tasks/lib/中的共享工具。
注意参数接口(
DropTaskArguments):属性名称必须与任务定义中.addOption()和.addFlag()的name值匹配。Hardhat 3 不会自动为你生成这些类型——你需要自己定义并确保匹配。
4. CLI 参数:Hardhat 2 到 Hardhat 3 的映射
参数从 Hardhat 2 到 Hardhat 3 的映射是我们迁移过程中最令人困惑的部分之一。相关概念被重新组织,而且关于此主题的官方文档很少——我们最终阅读了 Hardhat 3 的源代码来理解旧参数类型如何映射到新参数。为了节省你的精力,以下是我们整理的参考表:
| Hardhat 2 | Hardhat 3 | 变化内容 |
|---|---|---|
addParam(name, ...) |
无直接 1:1 等效项 | HH3 中没有 HH2 必需参数的直接等效项。始终需要设置默认值,验证由用户自行完成。 |
addOptionalParam(name, ...) |
addOption({ name, type, defaultValue, ... }) |
替代 HH2 的可选命名参数。 |
addFlag(name, ...) |
addFlag({ name, ... }) |
HH2 和 HH3 行为等效。 |
| - | addLevel({ name, ... }) |
这是 HH3 新增的;addLevel 定义了一个接受非负整数且默认值为 0 的选项。当该选项具有短名称(如 -v)时,重复使用它会增加级别,例如 -vvvv 表示级别 4(相当于 --verbosity 4)。 |
addPositionalParam(name, ...) |
addPositionalArgument({ name, type }) |
与命名选项不同,位置参数可以是必需的——只需省略 defaultValue。 |
addOptionalPositionalParam(name, ...) |
addPositionalArgument({ ..., defaultValue }) |
替代 HH2 的可选位置参数。提供 defaultValue 使其变为可选。 |
addVariadicPositionalParam(name, ...) |
addVariadicArgument({ name, type }) |
与位置参数类似,可变参数可以是必需的——只需省略 defaultValue。 |
addOptionalVariadicPositionalParam(name, ...) |
addVariadicArgument({ ..., defaultValue }) |
替代 HH2 的可选可变位置参数。提供 defaultValue 使其变为可选。 |
请注意,addOption() 始终需要 defaultValue —— 在 Hardhat 3 中无法定义必需的命名选项。如果你在 Hardhat 2 中有必需参数,需要在任务操作中自行验证。例如,我们的 --ver 选项默认值为 0,我们在每个需要它的任务开始时进行检查:
// 任务定义:ver 默认值为 0
const verifyDeployment = task('verify-deployment', '...')
.addOption({ name: 'ver', shortName: 'v', defaultValue: 0, type: ArgumentType.INT })
.setAction(() => import('./src/tasks/verify-deployment'))
.build();
// 任务操作:验证 ver 是否实际提供
export default async function (args: VerifyDeploymentTaskArguments, hre: HardhatRuntimeEnvironment) {
const version = args.ver;
if (version < 1) {
console.error('错误:必须通过 --v 参数指定版本');
return errorResult(new Error('缺少必需的版本参数'));
}
// ... 其余任务逻辑
}
我们喜欢的几点:
ArgumentType枚举取代了旧的无类型字符串参数。可用类型包括:STRING、INT、BOOLEAN、BIGINT、FILE。在 Hardhat 2 中,参数本质上就是无类型的字符串——你需要自己解析和验证。在 Hardhat 3 中,类型由框架强制执行,因此ArgumentType.INT会在任务运行之前拒绝非数字输入。shortName是一个新属性,让你原生地拥有短 CLI 标志。在 Hardhat 2 中,如果你想要--v,你就把参数命名为v。在 Hardhat 3 中,你可以给它一个描述性的name(如ver)和一个shortName(如v),这样--ver 53和-v 53都能工作。
5. 任务内部:哪些内容发生了变化
除了定义语法外,实际的任务代码也需要更新。
5.1 网络访问
在 Hardhat 2 中,你可以直接从 hre 访问 ethers 和网络信息:
// Hardhat 2
const chainId = await hre.getChainId();
const networkName = hre.network.name;
const contract = new hre.ethers.Contract(address, abi, hre.ethers.provider);
在 Hardhat 3 中,你首先需要创建一个连接,然后通过它访问所有内容:
// Hardhat 3
const conn = await hre.network.connect();
const chainId = conn.networkConfig.chainId;
const networkName = conn.networkName;
const contract = new conn.ethers.Contract(address, abi, conn.ethers.provider);
这种 hre.network.connect() 模式与测试中使用的模式相同(在第一部分中介绍过)。连接对象提供了 ethers、networkConfig、networkName,并且如果已经注册了 Ignition,还提供了 ignition 用于部署。
5.2 返回值
Hardhat 2 任务返回 void。Hardhat 3 任务返回结构化的结果:
- export async function dropTask(hre, args): Promise<void> {
- // ... 执行工作 ...
- }
+ export default async function(args, hre): Promise<TaskResult> {
+ if (somethingFailed) {
+ return errorResult(new Error('描述性错误'));
+ }
+ return successfulResult<boolean>(true);
+ }
从 hardhat/utils/result 导入 successfulResult 和 errorResult。这取代了抛出错误或调用 process.exit(1) 的模式——任务运行器会根据你返回的结果类型来处理错误显示和退出码。
5.3 读取部署工件
在使用了 hardhat-deploy 的 Hardhat 2 中,读取部署数据非常简单——hre.deployments.getOrNull('MerkleDrop128-42') 一次调用就能获取合约地址、构造函数参数和交易收据。
Hardhat Ignition 没有等效的 API 用于以编程方式读取过去的部署工件。它将部署数据存储在 ignition/deployments/<deploymentId>/ 目录下的文件中,但没有提供内置的方法来从任务代码中查询它们。以下是每个部署文件夹的样子,以我们的项目为例:
ignition/deployments/sepolia-MerkleDrop-78/
├── artifacts/ - 编译后的合约工件(ABI、字节码、源代码信息)
│ └── SignatureDrop#SignatureMerkleDrop128.json
├── build-info/ - 完整的 Solidity 编译器输入/输出,用于可重现构建
│ └── solc-0_8_23-....json
├── deployed_addresses.json - 按 future ID 关联的合约地址
└── journal.jsonl - 每次部署步骤的换行分隔 JSON 日志
deployed_addresses.json 将 Ignition future ID 映射到已部署的地址:
{
"SignatureDrop#SignatureMerkleDrop128": "0xb56c499b57F720D59028f74D36Fb1571E031Cd83"
}
journal.jsonl 是部署过程的逐行日志。每行都是一个 JSON 对象,包含 type 字段。我们使用了以下条目类型:
DEPLOYMENT_EXECUTION_STATE_INITIALIZE- 包含constructorArgs、contractName和部署者地址(from)TRANSACTION_CONFIRM- 包含交易hash和包含blockNumber、blockHash、contractAddress的receiptDEPLOYMENT_EXECUTION_STATE_COMPLETE- 包含最终部署的address
我们不得不创建一个辅助类来直接解析这些文件,以提取合约地址、构造函数参数和区块号等部署参数。如果你的任务需要读取之前 Ignition 运行的部署数据,请准备好编写自己的解析层。
6. 结论
至此,我们从 Hardhat 2 迁移到 Hardhat 3 的三部分系列文章全部完成:
- 第一部分 涵盖了 ES 模块配置、依赖更新、测试迁移以及模块化测试中的
loadFixture陷阱。 - 第二部分 涵盖了用 Hardhat Ignition 替换
hardhat-deploy、构建配置文件以及evmVersion验证陷阱。 - 第三部分 涵盖了新的任务 API、内联操作与模块操作、CLI 参数映射,以及构建辅助工具来弥合 Ignition 部署工件与任务代码之间的差距。
一旦我们理解了新的模式,任务迁移就变得简单了。与 Hardhat 2 的 .addParam() 相比,使用 .addOption() / .build() 的构建器 API 更加冗长,但显式的类型、短名称和延迟加载是真正的改进。最大的痛点在于缺少用于读取 Ignition 部署工件的内置 API——而 hardhat-deploy 能够无缝处理这个问题。
总的来说,迁移到 Hardhat 3 需要付出一定的努力,但结果是代码库更加清晰、更易于维护。新的任务系统,加上原生 Solidity 测试和 Hardhat Ignition,使 Hardhat 3 成为以太坊开发工具的一个坚实进步。
- 原文链接: github.com/1inch/merkle-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~