以太坊智能合约开发框架:Hardhat v2 核心功能从入门到基础教程

发布于:2025-06-02 ⋅ 阅读:(812) ⋅ 点赞:(0)

一、设置项目

Hardhat 项目是安装了 hardhat 包并包含 hardhat.config.js 文件的 Node.js 项目。

操作步骤:

①初始化 npm

npm init -y

②安装 Hardhat

npm install --save-dev hardhat

③创建 Hardhat 项目

npx hardhat init

在这里插入图片描述

  1. 如果选择 Create an empty hardhat.config.js ,Hardhat 会生成如下配置文件:

    /** @type import('hardhat/config').HardhatUserConfig */  
    module.exports = {  
      solidity: "0.8.28",  
    };  
    
  2. Hardhat 默认是支持 Ethers,如果使用 Viem 可选择 Create a TypeScript project(with Viem)

  3. 如果选择 Create a JavaScript project 、 Create a TypeScript project 、 Create a TypeScript project(with Viem),向导会询问几个问题,随后创建目录、文件并安装依赖。其中最重要的依赖是 Hardhat Toolbox ,它集成了Hardhat 所需的所有核心插件。

这里选择 Create a JavaScript project

在这里插入图片描述

初始化后的项目结构如下:

contracts/  
ignition/modules/  
test/  
hardhat.config.js  

这是 Hardhat 项目的默认路径:

  • contracts/:存放合约源码
  • ignition/modules/:存放处理合约部署的 Ignition 模块
  • test/:存放测试文件

如需修改路径,可查看路径配置文档

二、VS Code 插件

Hardhat for Visual Studio Code 是官方 VS Code扩展,为 VS Code 提供 Solidity 高级支持。

在这里插入图片描述

三、Hardhat 架构

Hardhat 是围绕 任务 和 插件 的概念设计的,Hardhat 的大部分功能来自插件。

1.任务

每次从命令行运行 Hardhat 命令时,都在运行一项任务。

查看项目中当前可用的任务

npx hardhat

在这里插入图片描述

Hardhat 内置的常用任务:

  • npx hardhat compile:编译Solidity合约代码
  • npx hardhat test:运行测试脚本
  • npx hardhat run [path/to/script.js]:运行一个脚本
  • npx hardhat clean:清除构建输出和缓存文件
  • npx hardhat console:交互式控制台
  • npx hardhat node:启动本地开发节点

2.插件

Hardhat 的大部分功能来自插件,可在 Plugins 列表中查看官方推荐。

使用插件的步骤如下:

①安装插件

npm install --save-dev @nomicfoundation/hardhat-toolbox

②在 hardhat.config.js 文件中引入插件

require("@nomicfoundation/hardhat-toolbox");  

module.exports = {  
  solidity: "0.8.28",  
}; 

四、网络

1.Hardhat 内置网络

内置的 Hardhat Network 作为开发测试网络,可搭配 Hardhat Network Helpers 库控制网络状态,这样更灵活。

启动 HardHat 网络节点

npx hardhat node  

在这里插入图片描述

2.其他网络

Hardhat 默认网络是 Hardhat Network,如需使用其他网络(如以太坊测试网、主网或其他节点软件),可在 hardhat.config.js 导出对象的 networks 配置中进行设置,这是 Hardhat 项目管理网络配置的方式。

①配置外部网络(例如本地Geth)

module.exports = {
  networks: {
    geth: {
      url: "http://127.0.0.1:8545",
      accounts: ['你的私钥1', '你的私钥2', ...]  
    },
  },
}

②使用外部网络

通过 --network 命令行参数可快速切换网络

npx hardhat [任务] --network [网络名]  

如果不加 --network [网络名] 则将默认网络作为任务网络

3.更改默认网络

如果要切换默认网络,可在 hardhat.config.js 导出对象的 defaultNetwork 配置中进行设置(前提得先定义好 networks )。

module.exports = {
  networks: {
    geth: {
      url: "http://127.0.0.1:8545",
      accounts: ['私钥1', '私钥2', ...]  
    },
  },
  // defaultNetwork: "geth",  // 默认网络切换成 geth,但开发测试还是使用 hardhat 网络比较好
}

五、编写合约

在contracts目录下编写 Lock 合约

contracts/Lock.sol

// SPDX许可标识符: 未经许可
pragma solidity ^0.8.28;

contract Lock {
    // 公开状态变量:解锁时间戳 & 合约所有者地址
    uint public unlockTime;
    address payable public owner;

    // 定义提款事件(提款金额、操作时间)
    event Withdrawal(uint amount, uint when);

    // 构造函数:接收解锁时间并验证有效性
    // 必须附带 ETH 存款(payable 修饰)
    constructor(uint _unlockTime) payable {
        // 检查解锁时间是否在未来
        require(
            block.timestamp < _unlockTime,
            "Unlock time should be in the future"
        );

        unlockTime = _unlockTime;
        owner = payable(msg.sender); // 设置部署者为所有者
    }

    // 提款函数(仅限所有者调用)
    function withdraw() public {

        // 验证当前时间是否已到解锁时间
        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);
    }
}

六、编译合约

1.执行编译任务

使用 Hardhat 内置的 compile 任务来编译合约

npx hardhat compile

在这里插入图片描述

这将会把 contracts/ 目录下的所有合约进行编译,编译后会自动生成 artifacts 目录,并自动将编译相关的信息放在 artifacts/ 目录下。

在这里插入图片描述

后期如果仅修改了一个文件,那么只会重新编译该文件以及受其影响的其他文件。

这是因为 Hardhat 有缓存机制。Hardhat会将每个智能合约的编译结果缓存起来,以便在后续的编译过程中重复使用。

这意味着如果您没有对合约进行任何更改,Hardhat将直接从缓存中读取编译结果,而不需要重新编译整个合约。如果对合约部分修改,那么会差量编译。这样可以极大地减少编译时间,特别是在项目中存在多个合约的情况下。

但如果想强制进行编译,可以使用 --force 参数,或者运行 npx hardhat clean 来清除缓存并删除编译产物。

# 清除编译缓存
npx hardhat clean
# 强制编译
npx hardhat compile --force

2.配置编译器

如果需要自定义 Solidity 编译器选项,可以通过 hardhat.config.js 中的 solidity 字段来实现。使用该字段最简单的方式是通过简写形式来设置编译器版本。Hardhat 会自动下载所设置的 solc(solidity 编译器) 版本。

module.exports = {
  solidity: "0.8.28",
};

如果不指定版本则以 Hardhat 指定的默认版本

建议自定义版本,因为如果后期随着 Solidity 新版本发布, Hardhat 官方可能会修改默认的编译器版本 ,从而导致项目出现意外行为或编译错误

注意:合约的版本与配置的编译器版本不兼容,Hardhat 将会抛出错误。

3.更多编译器相关配置

module.exports = {
  solidity: {
    version: "0.8.28",
    settings: {
      optimizer: {
        enabled: true,
        runs: 1000,
      },
      evmVersion: 'london'
    },
  },
};

settings 的结构与可以传递给编译器的输入 JSON 中的 settings 条目相同。一些常用的设置如下:

  • optimizer:一个包含enabled和runs键的对象。默认值为{ enabled: false, runs: 200 }。
  • evmVersion:一个字符串,用于控制目标 EVM 版本。例如:istanbul、berlin或london。默认值由solc 管理。

七、测试合约

本指南将介绍在 Hardhat 中测试合约的推荐方法。该方法借助 ethers 库连接到 Hardhat 网络,使用 Mocha 和 Chai 进行测试。同时,还会用到自定义的 Chai 匹配器以及 Hardhat 网络辅助工具,从而更轻松地编写简洁的测试代码。这些工具包均属于 Hardhat Toolbox 插件的一部分。

虽然这是推荐的测试设置方式,但 Hardhat 具有很高的灵活性:可以对该方法进行自定义,也可以采用其他工具开辟全新的测试路径。

1.测试工具

1.1. mocha

Mocha 是一个能够运行在 Node.js 和 浏览器 中的多功能 JavaScript 测试框架,它让异步测试变得 简单 和 有趣。Mocha 顺序运行测试,并给出灵活而精确的报告,同时能够将未捕获的异常映射到准确的测试用例上。

1️⃣describe 是一个 Mocha 函数,可组织测试

  • 参数: 接收测试组织名称和回调函数。回调必须定义该部分的测试。这个回调不能是异步函数。
  • 全Mocha函数在全局范围内可用、组织好测试可以让调试变得更容易。
describe("学习mocha测试", function () {
    // 里面装测试函数it
})

2️⃣it 是另一个 Mocha 函数。可定义每个测试单元

  • 参数:接收单元测试名称和回调函数。
  • 如果回调函数是异步的,Mocha 将自动 “await” 它
describe("Mocha测试", function () {
	it("测试单元1", async function () {
    	// 具体的测试
	})
    
    it("测试单元2", async function () {
    	// 具体的测试
	})
})

3️⃣beforeEach 是 Mocha 中 describe 函数的一个钩子。可在执行 describe 函数中 it 函数 之前执行

describe("Mocha测试", function () {
    beforeEach(async function () {
        // 在执行 it 函数前做些什么
  	});
    
	it("测试单元1", async function () {
    	// 具体的测试
	})
    
    it("测试单元2", async function () {
    	// 具体的测试
	})
})  

1.2 chai

chai 是一个可以在 node 和 浏览器 环境运行的BDD/TDD断言库,可以和任何 JavaScript 测试框架结合。在 Hardhat 中对其进行了加强,使得 chain 更符合合约的测试。学习Chai断言库

2.测试变量

测试内容:

  • 部署 Lock 合约
  • 断言unlockTime() 返回的解锁时间与在构造函数中传入的时间一致

测试准备:

  • 查看 contracts/Lock.sol 合约代码,了解逻辑

  • 在 test 目录下新建 myLock.js 测试文件,编写测试代码

测试步骤:

①导入所需的测试工具:

  • 从 chai 中导入 expect 函数用于编写断言
  • 导入 Hardhat 运行时环境(hre)
  • 与 Hardhat 网络交互的网络辅助工具
const { expect } = require("chai");
const hre = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

②使用 describe 和 it 函数,它们是Mocha的全局函数,用于描述和分组测试

const { expect } = require("chai");
const hre = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

describe("Lock", function () {
  it("应设置正确的解锁时间", async function () {
      // 测试代码位于 it 函数的回调参数内。
  });
});

③编写部署合约的逻辑

首先,设置要锁定的金额(以 wei 为单位)和解锁时间。对于解锁时间,使用 time.latest 这个网络辅助工具,它会返回最后一个已挖出区块的时间戳。然后,部署合约:调用ethers.deployContract,传入要部署的合约名称和包含解锁时间的构造函数参数数组。再传入一个包含交易参数的对象,这是可选的,但通过设置其 value 字段来发送一些 ETH。

const { expect } = require("chai");
const hre = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

describe("Lock", function () {
  it("应设置正确的解锁时间", async function () {
    const lockedAmount = 1_000_000_000;
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

	// 部署一个可以提取资金的锁定合约
	// 未来一年
    const lock = await hre.ethers.deployContract("Lock", [unlockTime], {
      value: lockedAmount,
    });
  });
});

④测试合约变量

检查合约中 unlockTime() getter 方法返回的值是否与部署时使用的值相匹配。
由于调用合约上的所有函数都是异步的,必须使用 await 关键字来获取其值;否则,将比较一个 Promise 和一个数字,这肯定会失败。

const { expect } = require("chai");
const hre = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

describe("Lock", function () {
  it("应设置正确的解锁时间", async function () {
    const lockedAmount = 1_000_000_000;
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

	// 部署一个可以提取资金的锁定合约
	// 未来一年
    const lock = await hre.ethers.deployContract("Lock", [unlockTime], {
      value: lockedAmount,
    });

    // 断言该值是正确的
    expect(await lock.unlockTime()).to.equal(unlockTime);
  });
});

3.测试回滚函数

在之前的测试中,检查了一个 getter 函数是否返回了正确的值。这是一个只读函数,可以免费调用且没有任何风险。

然而,其他函数可能会修改合约的状态,且再修改状态之前会有一些前置检查,比如 Lock 合约中的 withdraw 函数。这意味着希望在调用这个函数之前满足一些前置条件。如果查看该函数的前几行,会看到有几个 require检查用于此目的:

contracts/Lock.sol

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

第一条语句检查是否已达到解锁时间,第二条语句检查调用合约的地址是否为合约所有者。

为第一个前置条件编写测试:

it("如果调用过快,应返回正确的错误", async function () {
    const lockedAmount = 1_000_000_000;
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

    // 部署一个可以提取资金的锁定合约
    // 未来一年
    const lock = await hre.ethers.deployContract("Lock", [unlockTime], {
      value: lockedAmount,
    })

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

在之前的测试中,使用了 .to.equal,这是 Chai 的一部分,用于比较两个值。在这里,使用 .to.be.revertedWith,它用于断言交易将回滚,并且回滚的原因字符串等于给定的字符串。 .to.be.revertedWith 匹配器并非 Chai 本身的一部分,而是由 Hardhat Chai Matchers 插件添加的

注意,在之前的测试中写的是 expect(await ...),但现在是 await expect(...)。在第一种情况下,是以同步方式比较两个值;内部的 await 只是为了等待获取值。在第二种情况下,整个断言是异步的,因为它必须等待交易被挖出。这意味着 expect 调用返回一个 Promise,必须对其使用 await。

4.操纵网络时间

部署的 Lock 合约的解锁时间为一年。如果想编写一个测试来检查解锁时间过后会发生什么,显然不能真的等上一年。可以使用更短的解锁时间,比如 5 秒,但这不是一个很现实的值,而且在测试中等待 5 秒仍然很长。

解决办法是模拟时间的流逝。这可以通过 time.increaseTo 网络辅助工具来实现,它会挖出一个带有给定时间戳的新区块:

it("应将资金转移给所有者", async function () {
    const lockedAmount = 1_000_000_000;
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

    // 部署一个可以提取资金的锁定合约
    // 未来一年
    const lock = await hre.ethers.deployContract("Lock", [unlockTime], {
      value: lockedAmount,
    })

    // 生成指定时间戳的最新区块
  	await time.increaseTo(unlockTime);

  	// 如果交易回滚,这里会抛出错误
  	await lock.withdraw();
});

如前所述,调用 lock.withdraw() 会返回一个 Promise。如果交易失败,该 Promise 将被拒绝。使用 await 在这种情况下会抛出错误,所以如果交易回滚,测试将失败。

5.使用不同的账户

withdraw 函数进行的第二个检查是调用该函数的地址是否为合约所有者。默认情况下,部署和函数调用是使用第一个配置的账户进行的。如果想检查只有所有者才能调用某个函数,就需要使用不同的账户,并验证调用会失败。

ethers.getSigners() 会返回一个包含所有配置账户的数组。可以使用合约的 .connect 方法,用不同的账户调用函数,并检查交易是否回滚:

  it("如果从其他帐户调用,应返回正确的错误", async function () {
    const lockedAmount = 1_000_000_000;
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

    // 部署一个可以提取资金的锁定合约
    // 未来一年
    const lock = await hre.ethers.deployContract("Lock", [unlockTime], {
      value: lockedAmount,
    })

    // 获取账户列表,并解构
    const [owner, otherAccount] = await hre.ethers.getSigners();

    // 增加链上的时间以通过第一个检查
    await time.increaseTo(unlockTime);

    // 使用lock.connect()从另一个账户发送交易
    await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
      "You aren't the owner"
    );
  });

这里再次调用一个函数,并断言它会以正确的原因字符串回滚。不同之处在于用 .connect(anotherAccount) 从不同的地址调用该方法。

6.使用固定装置(Fixtures)

到目前为止,在每个测试中都部署了 Lock 合约。这意味着在每个测试开始时,都必须获取合约工厂,然后部署合约。对于单个合约来说,这可能没问题,但如果设置更复杂,每个测试开始时都会有几行代码只是为了设置所需的状态,而且大多数时候这些代码都是相同的。

在典型的 Mocha 测试中,这种代码重复问题可以通过beforeEach钩子来处理。

const hre = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

describe("Lock", function () {
  let lock: any;
  let unlockTime: number;
  let lockedAmount = 1_000_000_000;

  beforeEach(async function () {
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

    lock = await hre.ethers.deployContract("Lock", [unlockTime], {
      value: lockedAmount,
    });
  });

  it("some test", async function () {
    // 使用已部署的合约
  });
});

然而,这种方法有两个问题:

  1. 如果需要部署多个合约,测试会变慢,因为每个测试作为设置的一部分都要发送多个交易。
  2. 在 beforeEach 钩子和测试之间像这样共享变量既不美观又容易出错。

Hardhat 网络辅助工具中的 loadFixture 助手解决了这两个问题。这个助手接收一个固定装置(fixture),即一个将链设置到所需状态的函数。第一次调用 loadFixture 时,会执行该固定装置函数。但第二次调用时, loadFixture 不会再次执行固定装置函数,而是将网络状态重置到固定装置函数执行后的状态。这样更快,并且会撤销前一个测试所做的任何状态更改。

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

describe("Lock", function () {

  // 定义固定装置函数
  async function deployOneYearLockFixture() {
    const lockedAmount = 1_000_000_000;
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

    const lock = await hre.ethers.deployContract("Lock", [unlockTime], {
      value: lockedAmount,
    });

    return { lock, unlockTime, lockedAmount };
  }

  it("应设置正确的解锁时间", async function () {
    
    // 使用固定装置(第一次使用,初始化执行一次deployOneYearLockFixture函数,并记录当前区块), 并获取合约实例和解锁时间
    const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

    // 断言值是正确的
    expect(await lock.unlockTime()).to.equal(unlockTime);
  });

  it("如果调用过快,应返回正确的错误", async function () {

    // 使用固定装置(第二次使用,回滚区块到初始化deployOneYearLockFixture函数所记录的区块), 并获取合约实例和解锁时间
    const { lock } = await loadFixture(deployOneYearLockFixture);

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

  it("应将资金转移给所有者", async function () {

    // 使用固定装置(第二次使用,回滚区块到初始化deployOneYearLockFixture函数所记录的区块), 并获取合约实例和解锁时间
    const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

    // 生成指定时间戳的最新区块
    await time.increaseTo(unlockTime);

    // 如果交易回滚,这里会抛出错误
    await lock.withdraw();
  });

  it("如果从其他帐户调用,应返回正确的错误", async function () {

    // 使用固定装置(第二次使用,回滚区块到初始化deployOneYearLockFixture函数所记录的区块), 并获取合约实例和解锁时间
    const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

    // 获取账户列表,并解构
    const [owner, otherAccount] = await hre.ethers.getSigners();

    // 增加链上的时间以通过第一个检查
    await time.increaseTo(unlockTime);

    // 使用lock.connect()从另一个账户发送交易
    await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
      "You aren't the owner"
    );
  });
});

固定装置函数可以返回任何想要的值,loadFixture 助手会返回该值。建议像这里一样返回一个对象,这样就可以提取出该测试中关心的值。

7.使用调试

Hardhat Network 允许通过从 Solidity 代码调用来打印日志记录消息和合约变量

使用步骤:

①在合约中导入 hardhat 的 console.sol

import "hardhat/console.sol";

②在合约中使用console.log()

// SPDX许可标识符: 未经许可
pragma solidity ^0.8.28;

// 导入 console.sol 库
import "hardhat/console.sol";

contract Lock {
    // 公开状态变量:解锁时间戳 & 合约所有者地址
    uint public unlockTime;
    address payable public owner;

    // 定义提款事件(提款金额、操作时间)
    event Withdrawal(uint amount, uint when);

    // 构造函数:接收解锁时间并验证有效性
    // 必须附带 ETH 存款(payable 修饰)
    constructor(uint _unlockTime) payable {
        // 检查解锁时间是否在未来
        require(
            block.timestamp < _unlockTime,
            "Unlock time should be in the future"
        );

        unlockTime = _unlockTime;
        owner = payable(msg.sender); // 设置部署者为所有者
    }

    // 提款函数(仅限所有者调用)
    function withdraw() public {
        // 调试
        console.log("unlockTime %d,currentTimestamp %d", 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);
    }
}

8.执行测试任务

使用 Hardhat 内置的 test 任务来执行测试脚本

npx hardhat test

这将会执行 test/ 目录下所有测试脚本

在这里插入图片描述

很明显第一个单元测试时间花费最长 522ms ,这是因为初始化执行 fixture函数。

9.其他测试

9.1 测量测试覆盖率

Hardhat Toolbox 包含 solidity-coverage 插件,用于测量项目的测试覆盖率。只需运行 coverage 任务,就会得到一份报告:

npx hardhat coverage

在这里插入图片描述

9.2 使用 gas 报告器

Hardhat Toolbox 还包含 hardhat-gas-reporter 插件,根据测试执行情况获取使用了多少 gas 的指标。当执行test任务且设置了 REPORT_GAS 环境变量时,会运行 gas 报告器:

REPORT_GAS=true npx hardhat test

对于 Windows 用户,在 PowerShell 会话中设置环境变量的命令是$env:REPORT_GAS="true":

$env:REPORT_GAS="true"; npx hardhat test

八、部署合约

1.编写部署模块

若要部署合约,可使用声明式部署系统 Hardhat Ignition。

新建负责部署 Lock 合约的 Ignition 模块 LockModule 位于 ignition/modules 目录下

ignition/modules/Lock.js

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

const JAN_1ST_2030 = 1893456000;  // 默认值为 2030 年 1 月 1 日,时间戳`1893456000`
const ONE_GWEI = 1_000_000_000n;  // 默认值为 1 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 };  
});  

2.执行部署任务

npx hardhat ignition deploy ./ignition/modules/你的部署模块文件名.js --network <网络名>  

若未指定网络,Hardhat Ignition 将部署到 hardhat.config.js 配置的默认网络。

1️⃣部署到 Hardhat 网络

npx hardhat ignition deploy ./ignition/modules/Lock.js

在这里插入图片描述

2️⃣指定网络部署

npx hardhat ignition deploy ./ignition/modules/Lock.js --network <网络名>  

九、配置变量

1.为什么需要配置变量?

Hardhat 项目中使用配置变量,是为存储用户特定值、保护敏感数据(如 API 密钥、私钥等),便于项目共享协作,提高代码可维护性与灵活性,以及适应不同环境。

注意:配置变量以明文形式存储在磁盘上,请勿用于存储不希望以未加密文件形式保存的数据。可通过 npx hardhat vars path 查看存储文件位置。

2.配置变量和环境变量的区别

dotenv 环境变量:若使用 dotenv ,可能导致意外上传 .env 文件导致敏感数据泄露。

vars 配置变量:Hardhat 项目可以将配置变量用于用户特定的值或不应包含在代码存储库中的数据。这些变量是通过作用域中的任务设置的,可以使用对象在配置中检索。

3.在命令行中操作配置变量

1️⃣设置配置变量(为配置变量赋值,不存在则创建)

npx hardhat vars set 配置变量名

2️⃣获取配置变量

npx hardhat vars get 配置变量名

3️⃣查询计算机上存储的所有的配置变量

npx hardhat vars list

4️⃣删除配置变量语法

npx hardhat vars delete 配置变量名

4.在文件中使用配置变量

①导入 vars 实例

const { vars } = require("hardhat/config");

②使用配置变量

  1. 直接使用

    const { vars } = require("hardhat/config");
    
    const PRIVATE_KEY = vars.get("环境变量名");
    
  2. 使用配置变量并设置默认值(变量不存在时使用)

    const { vars } = require("hardhat/config");
    
    const PRIVATE_KEY = vars.get("环境变量名", "默认值");
    
  3. 先检查再使用

    const { vars } = require("hardhat/config");
    
    const PRIVATE_KEY = vars.has("环境变量名") ? [vars.get("环境变量名")] : '备选值'; 
    

5.使用配置变量存储私钥

①先设置私钥的配置变量

npx hardhat vars set PRIVATE_KEY

在这里插入图片描述

可以在 hardhat.config.js 文件中检索存储的配置变量。

require("@nomicfoundation/hardhat-toolbox");
const { vars } = require("hardhat/config");

const PRIVATE_KEY = vars.get("PRIVATE_KEY");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    version: "0.8.28",
    settings: {
      optimizer: {
        enabled: true,
        runs: 1000,
      },
      evmVersion: 'london'
    },
  },
  networks: {
    geth: {
      url: "http://127.0.0.1:8545",
      accounts: [PRIVATE_KEY]  
    },
  },
  // defaultNetwork: "geth",  // 默认网络切换成 geth
};

十、验证合约

验证合约是指公开合约的源代码以及所用的编译器设置,这样任何人都能编译该代码,并将生成的字节码与链上部署的字节码进行对比。在像以太坊这样的开放平台上,这一操作极其重要。

本指南将介绍如何在 Etherscan 浏览器中完成此操作。

提示:如果想验证非 Hardhat Ignition 部署的合约,或者想在 Sourcify 而非 Etherscan 上验证合约,可以使用 hardhat-verify 插件。

1.从 Etherscan 获取 API 密钥

操作步骤:

①首先,你需要从 Etherscan 获取一个 API 密钥。

具体操作如下:访问 Etherscan 网站,登录账号(若没有则需注册),打开 “API Keys” 标签页,点击 “Add” 按钮,为你创建的 API 密钥命名(例如 “Hardhat”),之后你会在列表中看到新创建的密钥。

②将 Etherscan 的 API 密钥存在配置变量中

npx hardhat vars set ETHERSCAN_API_KEY 

③在 Hardhat 的 hardhat.config.js 中 添加 etherscan 配置,并将 API 密钥添加到这里

const ETHERSCAN_API_KEY = vars.get("ETHERSCAN_API_KEY");

module.exports = {
  // ...其他配置...
  etherscan: {
    apiKey: ETHERSCAN_API_KEY,
  },
};

2.在 Sepolia 测试网部署并验证合约

将使用 Sepolia 测试网来部署和验证合约,因此需要在 Hardhat 配置文件中添加该网络。这里使用 Infura 连接到网络,如果愿意,也可以使用其他 JSON - RPC URL,如 Alchemy。

操作步骤:

①访问 https://infura.io 注册账号,在其控制台创建一个新的 API 密钥

②将 INFURA 的 API 密钥存储到配置变量中

npx hardhat vars set INFURA_API_KEY

③在 Hardhat 的 hardhat.config.js 的 networks 中添加 sepolia 网络配置,并将 API 密钥添加到这里

const INFURA_API_KEY = vars.get("INFURA_API_KEY");

export default {
  // ...其他配置...
  networks: {
    sepolia: {
      url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [PRIVATE_KEY],
    },
  },
};

要在 Sepolia 上部署合约,需要向进行部署的地址发送一些 Sepolia 以太币。可以从水龙头获取测试网以太币,水龙头是一种免费分发测试以太币的服务。以下是一些 Sepolia 水龙头:

  • Alchemy Sepolia Faucet
  • QuickNode Sepolia Faucet
  • Ethereum Ecosystem Sepolia Faucet

现在可以部署合约了,但在此之前,要让合约的源代码具有唯一性。

打开的合约文件,添加一条包含独特信息的注释,比如你的 GitHub 用户名。请记住,在这里添加的任何内容都会和代码的其他部分一样,在 Etherscan 上公开可见:

contracts/Lock.sol

// Author: @你的名称
contract Lock {

将利用在 “部署合约” 指南中创建的 Ignition 模块 Lock 来进行部署。使用 Hardhat Ignition 和新添加的 Sepolia 网络运行部署命令:

npx hardhat ignition deploy ignition/modules/Lock.js --network sepolia --deployment-id sepolia-deployment

提示:--deployment-id 标志是可选的,但它允许你为部署指定一个自定义名称。这样在后续操作中,比如验证合约时,引用起来会更方便。

最后,要验证已部署的合约,你可以运行 ignition verify 任务并传入部署 ID:

npx hardhat ignition verify sepolia-deployment

或者,你可以使用 --verify 标志调用 deploy 任务,将部署和验证合并为一步:

npx hardhat ignition deploy ignition/modules/Lock.js --network sepolia --verify

提示:如果你收到错误信息,提示地址没有字节码,这可能意味着 Etherscan 尚未对合约进行索引。这种情况下,等待一分钟后再试。

当 ignition verify 任务成功执行后,将看到一个指向你合约公开验证代码的链接。

十一、自定义任务

Hardhat 本质上是一个任务运行器,借助它能够让开发工作流程实现自动化。它自带了像 compile 和 test 这类内置任务,同时也可以自行添加自定义任务。

1️⃣编写自定义任务

编写不带参数的基本任务,该任务会打印出可用账户的列表,同时探究其工作原理。

hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
const { vars } = require("hardhat/config");

const PRIVATE_KEY = vars.get("PRIVATE_KEY");
const ETHERSCAN_API_KEY = vars.get("ETHERSCAN_API_KEY");
const INFURA_API_KEY = vars.get("INFURA_API_KEY");

// 自定义任务
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    version: "0.8.28",
    settings: {
      optimizer: {
        enabled: true,
        runs: 1000,
      },
      evmVersion: 'london'
    },
  },
  networks: {
    geth: {
      url: "http://127.0.0.1:8545",
      accounts: [PRIVATE_KEY]  
    },
    sepolia: {
      url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [PRIVATE_KEY],
    },
  },
  // defaultNetwork: "geth",  // 默认网络切换成 geth
  etherscan: {
    apiKey: ETHERSCAN_API_KEY,
  },
};

task 函数来定义新任务。

  • 它的第一个参数是任务名称,也就是在命令行中用来运行任务的名称

  • 第二个参数是任务描述,当你使用 npx hardhat help 时会显示该描述

  • 第三个参数是一个异步函数,在你运行任务时会执行这个函数。它接收两个参数:

    1. 一个包含任务参数的对象。目前还没有定义任何参数。

    2. Hardhat 运行时环境(HRE),它包含了 Hardhat 及其插件的所有功能。在任务执行期间,能发现它的所有属性被注入到全局命名空间中。

在这个函数里,可以自由地实现任何功能。在这个例子中,使用 ethers.getSigners() 来获取所有已配置的账户,并打印出每个账户的地址。

可以为任务添加参数,Hardhat 会帮你处理参数的解析和验证。还可以覆盖现有的任务,这样就能改变 Hardhat 不同部分的工作方式。

2️⃣执行自定义任务

npx hardhat accounts

在这里插入图片描述

十二、Hardhat 控制台

Hardhat 内置了一个交互式 JavaScript 控制台。通过运行以下命令即可使用:

$ npx hardhat console  
Welcome to Node.js v12.10.0.  
Type ".help" for more information.  
>  

打开控制台前会先调用 compile 任务,若需跳过可使用 --no-compile 参数

npx hardhat console --no-compile  

1.执行环境

控制台的执行环境与任务、脚本和测试完全一致:配置已处理完毕,Hardhat 运行时环境(HRE)已初始化并注入全局作用域。

  • config:查看 Hardhat 配置对象

    > config  
    {  
      solidity: { compilers: [ [Object] ], overrides: {} },  
      defaultNetwork: 'hardhat',  
      ...  
    }  
    >  
    
  • ethers:若按入门指南操作或安装了@nomicfoundation/hardhat-ethers

    > ethers  
    {  
      Signer: [Function: Signer] { isSigner: [Function] },  
      ...  
      provider: EthersProviderWrapper { ... },  
      getSigners: [Function: getSigners],  
      getContractAt: [Function: bound getContractAt] AsyncFunction  
    }  
    >  
    

所有注入到 HRE 中的内容都会自动在全局作用域中可用。如需显式引用 HRE,也可通过require导入:

> const hre = require("hardhat")  
> hre.ethers  
{ /* 与上述ethers对象一致 */ }  

2.历史记录功能

控制台支持大多数交互式终端的历史记录功能(包括跨会话记录),可通过向上箭头键查看历史命令。本质上,Hardhat 控制台是 Node.js 控制台的实例,因此 Node.js 的所有功能均可在此使用。

3.异步操作与顶级 await

与以太坊网络(及智能合约)的交互均为异步操作,因此大多数 API 和库通过 JavaScript 的 Promise 返回值。
为简化操作,Hardhat 控制台支持顶级 await语句(例如直接使用await调用异步函数):

> console.log(await ethers.getSigners())  
[  
  Signer { address: '0xf39F...', provider: Provider },  
  Signer { address: '0x7099...', provider: Provider },  
  ...  
] 
  • config:查看 Hardhat 配置对象

    > config  
    {  
      solidity: { compilers: [ [Object] ], overrides: {} },  
      defaultNetwork: 'hardhat',  
      ...  
    }  
    >  
    
  • ethers:若按入门指南操作或安装了@nomicfoundation/hardhat-ethers

    > ethers  
    {  
      Signer: [Function: Signer] { isSigner: [Function] },  
      ...  
      provider: EthersProviderWrapper { ... },  
      getSigners: [Function: getSigners],  
      getContractAt: [Function: bound getContractAt] AsyncFunction  
    }  
    >  
    

所有注入到 HRE 中的内容都会自动在全局作用域中可用。如需显式引用 HRE,也可通过require导入:

> const hre = require("hardhat")  
> hre.ethers  
{ /* 与上述ethers对象一致 */ }  

2.历史记录功能

控制台支持大多数交互式终端的历史记录功能(包括跨会话记录),可通过向上箭头键查看历史命令。本质上,Hardhat 控制台是 Node.js 控制台的实例,因此 Node.js 的所有功能均可在此使用。

3.异步操作与顶级 await

与以太坊网络(及智能合约)的交互均为异步操作,因此大多数 API 和库通过 JavaScript 的 Promise 返回值。
为简化操作,Hardhat 控制台支持顶级 await语句(例如直接使用await调用异步函数):

> console.log(await ethers.getSigners())  
[  
  Signer { address: '0xf39F...', provider: Provider },  
  Signer { address: '0x7099...', provider: Provider },  
  ...  
] 

微信公众号

今日签到

点亮在社区的每一天
去签到