hardhat-ignition-viem 实战指南:用 viem 类型安全地部署 Hardhat Ignition 模块
【免费下载链接】hardhatHardhat is a development environment to compile, deploy, test, and debug your Ethereum software.项目地址: https://gitcode.com/GitHub_Trending/ha/hardhat
Hardhat Ignition 是 Hardhat 的声明式智能合约部署系统,而hardhat-ignition-viem插件将它与 viem 无缝桥接:部署完成后,模块中每个合约都会被自动包装为类型安全的 viem 合约实例,开发者可以立即通过read/write调用其方法并读取状态。本文将基于本仓库中该插件的完整源码与测试,从安装配置、基本用法到deploy的执行流程与高级选项,给出可直接上手的实战方案与底层原理。
插件定位:连接 Ignition 与 viem 的桥梁
hardhat-ignition-viem是 packages/hardhat-ignition-viem 包提供的插件,其核心职责只有一个:为 Hardhat 的网络连接(network connection)注入一个ignition属性,该属性带有一个deploy方法,用于部署 Ignition 模块,并把模块返回的每个合约(m.contract(...)的返回值)转换为对应的 viem 合约实例。
这一点在源码中体现得非常直接:插件通过definePlugin注册,并声明了对@nomicfoundation/hardhat-ignition与@nomicfoundation/hardhat-viem两个插件的依赖(见 src/index.ts):
const hardhatIgnitionViemPlugin: HardhatPlugin = definePlugin({ id: "hardhat-ignition-viem", dependencies: () => [ import("@nomicfoundation/hardhat-ignition"), import("@nomicfoundation/hardhat-viem"), ], hookHandlers: { network: () => import("./internal/hook-handlers/network.js"), }, npmPackage: "@nomicfoundation/hardhat-ignition-viem", });也就是说,使用本插件前需要保证hardhat-ignition(提供 Ignition 部署引擎)与hardhat-viem(提供 viem 网络客户端)已经可用。从 package.json 的peerDependencies可以看到运行时依赖范围:hardhat、@nomicfoundation/hardhat-ignition、@nomicfoundation/hardhat-viem、@nomicfoundation/ignition-core以及viem(^2.47.6)。
注意:该插件与
hardhat-ignition-ethers属于同一类"网络扩展"插件,二者互斥。源码在挂载ignition属性前会检查connection.ignition !== undefined,一旦发现已有同类扩展就会抛出ONLY_ONE_IGNITION_EXTENSION_PLUGIN_ALLOWED错误(见 internal/hook-handlers/network.ts),因此一个项目中只能启用一个 Ignition 网络扩展。
安装
安装命令:
npm install --save-dev @nomicfoundation/hardhat-ignition-viem如果已经使用 Viem Hardhat Toolbox(@nomicfoundation/hardhat-toolbox-viem),则无需单独安装——本插件是该 Toolbox 的组成部分,随 Toolbox 一起启用。
在 hardhat.config.ts 中注册插件
在hardhat.config.ts中导入插件并加入plugins数组:
import { defineConfig } from "hardhat/config"; import hardhatIgnitionViem from "@nomicfoundation/hardhat-ignition-viem"; export default defineConfig({ plugins: [hardhatIgnitionViem], });defineConfig中的plugins数组由 Hardhat 3 的插件系统负责加载,加载完成后插件的networkhook 即被注册,后续创建的每个网络连接都会自动带上ignition属性。仓库中对应的 fixture 项目配置可参考 test/fixture-projects/minimal/hardhat.config.js,其还演示了在networks.default.mining.auto: false下手动挖矿的部署环境。
基本用法:部署模块并立即操作合约
插件将ignition属性挂载到每个网络连接(network connection)上,典型用法如下(取自原文档核心示例):
import { network } from "hardhat"; import Counter from "../ignition/modules/Counter.js"; const { ignition } = await network.create(); const { counter } = await ignition.deploy(Counter); await counter.write.inc(); console.log(await counter.read.x());这段代码完成三件事:
await network.create()创建一个新的网络连接,ignition属性即来自该连接;ignition.deploy(Counter)部署模块,返回按模块结果键名组织的对象;- 解构出的
counter是类型安全的 viem 合约实例,因此可以直接用counter.write.inc()发起写交易、用counter.read.x()读取链上状态——编译期即可获得完整的方法签名与参数类型提示。
类型安全:模块结果如何映射为 viem 合约
deploy的返回值类型由IgnitionModuleResultsToViemContracts类型(见 src/types.ts)决定。该类型逐键遍历模块的results:
- 若结果为
ContractDeploymentFuture(含 ABI 的部署 future)或ContractAtFuture(既有合约地址绑定),直接利用其内联 ABI 生成GetContractReturnType<Abi>; - 若是
NamedArtifactContractDeploymentFuture等按名字引用 artifact 的 future,则通过ContractNameOfContractFuture提取合约名,再从@nomicfoundation/hardhat-viem提供的ContractAbis类型映射中查找对应 ABI,推导出合约实例类型。
这意味着部署结果中每个键的类型都与模块定义严格对应:Foo合约实例上不存在Bar的方法,调用错误方法会在编译期报错。仓库测试 test/viem-results.ts 专门验证了这一点——对result.foo调用isBar()会同时产生类型错误与运行时 reject,同时测试也确认返回对象只包含模块results中声明的键(result.nonexistant为undefined)。
底层原理:ignition 如何挂载到网络连接
插件通过newConnectionhook 为每个网络连接注入ignition(见 internal/hook-handlers/network.ts):
async newConnection(context, next) { const connection = await next(context); if (connection.ignition !== undefined) { throw new HardhatError( HardhatError.ERRORS.IGNITION.INTERNAL.ONLY_ONE_IGNITION_EXTENSION_PLUGIN_ALLOWED, ); } connection.ignition = new LazyViemIgnitionHelper( context.config, context.artifacts, connection, context.interruptions, context.hooks, context.config.ignition, ); return connection; }这里有一个值得注意的实现细节:真正承担部署逻辑的是ViemIgnitionHelperImpl(见 internal/viem-ignition-helper.ts),而LazyViemIgnitionHelper是一个惰性包装器,deploy首次被调用时才通过await import加载实现类。该包装器将await import放在实例缓存判断之前,确保并发调用共享同一个微任务去重点,不会各自构造出状态不一致的实例(源码注释对此有明确说明,见 network.ts)。此外,getResolvedConfig是同步方法,无法异步加载实现类,因此在包装器中以注释说明的方式复刻了同样的合并逻辑。
deploy 的执行流程(源码级)
deploy的完整执行流程可以在 ViemIgnitionHelperImpl.deploy 中梳理出来:
- 互斥检查:使用
#mutex布尔锁保证同一连接上同时只有一个部署在进行,重复调用会抛出IGNITION.DEPLOY.ALREADY_IN_PROGRESS;finally中无论成败都会释放锁,因此首次部署失败后仍可再次部署(对应测试见 viem-results.ts)。 - 查询账户:通过
eth_accounts获取可用账户列表。 - 构建 artifact 解析器:
new HardhatArtifactResolver(this.#artifactsManager)负责按合约名解析编译产物。 - 合并配置:
getResolvedConfig(perDeployConfig)将插件级配置与本次部署配置合并。 - 解析策略配置:若传入了
strategy且未给strategyConfig,则从hardhatConfig.ignition.strategyConfig[strategyName]读取(见#resolveStrategyConfig,viem-ignition-helper.ts)。 - 解析链 ID 与部署 ID:通过
eth_chainId获取链 ID,再经resolveDeploymentId得到deploymentId。 - 计算部署目录:常规网络下为
<项目>/ignition/deployments/<deploymentId>;若当前网络类型是edr-simulated(模拟链),则不写部署产物目录。 - 注册 UI 事件监听:
displayUi为true时创建PrettyEventHandler并临时注册用户中断 hook。 - 读取部署参数:
parameters若为字符串,则视为部署参数文件路径,通过readDeploymentParameters读取 JSON 文件。 - 继承网络级 gas 配置:当本次部署未显式给出
maxRetries/retryInterval时,会回落到networkConfig.ignition中的同名配置;maxFeePerGasLimit、maxPriorityFeePerGas同样来自网络配置。 - 调用 ignition-core 的
deploy:把解析好的配置、provider、部署目录、事件监听器、模块、参数、账户、策略等一并交给 ignition-core 的deploy执行真实部署。 - 结果转换:部署成功后遍历
ignitionModule.results,按 future 类型把每个合约包装为 viem 实例:- 按名字引用 artifact 的 future(
NAMED_ARTIFACT_CONTRACT_DEPLOYMENT等)走connection.viem.getContractAt(contractName, address); - 携带内联 ABI 的 future(
CONTRACT_DEPLOYMENT等)则用getContract({ address, abi, client: { public, wallet } })手工构造,其中 wallet client 缺失时会抛出NO_DEFAULT_VIEM_WALLET_CLIENT错误; - 地址统一通过
#ensureAddressFormat规范为0x前缀的 checksum 格式。
- 按名字引用 artifact 的 future(
deploy 选项详解
deploy的签名与选项在 src/types.ts 中定义,默认值可从 viem-ignition-helper.ts 的实现确认:
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
parameters | DeploymentParameters \| string | {} | 部署参数对象,或指向部署参数 JSON 文件的路径(字符串时按文件读取) |
config | Partial<DeployConfig> | {} | 本次部署级配置,与插件级配置合并,优先级最高 |
defaultSender | string | 未指定 | 覆盖默认发送账户;未指定时使用eth_accounts的第一个账户 |
strategy | keyof StrategyConfig | "basic" | 部署策略名,如create2 |
strategyConfig | StrategyConfig[StrategyT] | 未指定 | 策略参数,缺省时回落到hardhat.config中的ignition.strategyConfig |
deploymentId | string | 自动生成 | 部署产物目录名,缺省时由链 ID 推导 |
displayUi | boolean | false | 是否展示交互式部署 UI(PrettyEventHandler) |
典型的高级用法示例——使用create2策略并指定 salt:
const { ignition } = await network.create(); const result = await ignition.deploy(MyModule, { strategy: "create2", strategyConfig: { salt: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", }, });该用法与仓库测试 test/strategies/helper-invocation.ts 完全一致:策略与配置可以直接在deploy调用中传入(此时strategyConfig直接生效);也可以只在deploy中传strategy,让插件从 Hardhat 配置里读取strategyConfig。若提供了策略却缺少必需参数(如create2缺少salt),会抛出IGNITION.STRATEGIES.MISSING_CONFIG_PARAM错误。
配置合并与优先级
插件支持两级配置:Hardhat 配置(context.config.ignition,见 network.ts)与每次deploy调用时的config选项。合并逻辑非常直观(getResolvedConfig):
return { ...this.#config, // 插件级(来自 hardhat.config 的 ignition 配置) ...perDeployConfig, // 本次 deploy 传入的 config,优先级更高 };测试 test/config.ts 验证了这一点:Hardhat 配置中设置的requiredConfirmations: 42在结果中保留,而 per-deploy 传入的maxFeeBumps: 7覆盖同名配置。
控制部署发起账户
默认情况下,部署由eth_accounts返回的第一个账户发起。通过defaultSender可以覆盖:
const result = await ignition.deploy(MyModule, { defaultSender: "0x...", });测试 test/default-sender.ts 用一个构造时记录msg.sender的OwnerSender合约验证:指定defaultSender后,owner读取结果等于该指定地址;不指定时则等于第一个 wallet client 的地址。
部署产物与多网络部署
常规网络下,每次部署的产物会写入ignition/deployments/<deploymentId>目录(deploymentId由链 ID 推导,也可用deploymentId选项显式指定);当网络类型为edr-simulated(如 EDR 模拟链)时则跳过产物落盘。这意味着同一份模块可以在不同网络分别部署、互不干扰,产物目录可用于后续的审计与复用。
小结
hardhat-ignition-viem用极小的 API 表面(一个ignition属性、一个deploy方法)把声明式部署与 viem 的类型安全体验完整地衔接起来。理解其底层的挂载机制、配置合并优先级、策略解析与互斥保护,能帮助你在真实项目中更准确地控制部署行为。若想进一步研究其行为边界,仓库中的 test/ 目录提供了从类型推导、并发保护、默认发送账户到 create2 策略的完整测试矩阵,是很好的学习与回归参考。
【免费下载链接】hardhatHardhat is a development environment to compile, deploy, test, and debug your Ethereum software.项目地址: https://gitcode.com/GitHub_Trending/ha/hardhat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考