一、設置項目
Hardhat 項目是安裝了 hardhat
包并包含 hardhat.config.js
文件的 Node.js 項目。
操作步驟:
①初始化 npm
npm init -y
②安裝 Hardhat
npm install --save-dev hardhat
③創建 Hardhat 項目
npx hardhat init
-
如果選擇 Create an empty hardhat.config.js ,Hardhat 會生成如下配置文件:
/** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: "0.8.28", };
-
Hardhat 默認是支持
Ethers
,如果使用Viem
可選擇 Create a TypeScript project(with Viem) -
如果選擇 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 () {// 使用已部署的合約});
});
然而,這種方法有兩個問題:
- 如果需要部署多個合約,測試會變慢,因為每個測試作為設置的一部分都要發送多個交易。
- 在
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");
②使用配置變量
-
直接使用
const { vars } = require("hardhat/config");const PRIVATE_KEY = vars.get("環境變量名");
-
使用配置變量并設置默認值(變量不存在時使用)
const { vars } = require("hardhat/config");const PRIVATE_KEY = vars.get("環境變量名", "默認值");
-
先檢查再使用
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", // 默認網絡切換成 gethetherscan: {apiKey: ETHERSCAN_API_KEY,},
};
task
函數來定義新任務。
-
它的第一個參數是任務名稱,也就是在命令行中用來運行任務的名稱
-
第二個參數是任務描述,當你使用
npx hardhat help
時會顯示該描述 -
第三個參數是一個異步函數,在你運行任務時會執行這個函數。它接收兩個參數:
-
一個包含任務參數的對象。目前還沒有定義任何參數。
-
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 }, ...
]