Hardhat Tutorial
Overview
Hardhat 是一個有助於在 Ethereum 上進行構建的開發環境。它幫助開發者在構建 smart contracts 和 dApps 時管理和自動化重複的工作,以及在工作流程中引入更多功能。這意味著從根本上進行 compilie 和 test。
Hardhat 還內建了 Hardhat Network (一個為開發而設計的 local Ethereum network),它允許你 deploy contracts, run tests, debug codes.
在本教程將會學到:
- 為以太坊開發設置 Node.js 環境
- 創建和配置 a Hardhat project
- 實做 a token 所需 Solidity 智能合約的基礎
- 使用 Ethers.js 和 Waffle 為合約編寫自動化測試
- 使用 Hardhat Network 的
console.log()
Debug Solidity - 將合約部署到 Hardhat Network and Ethereum testnets
先備知識:
- JavaScript
- 操作 terminal
- git
- 了解 smart contracts 的基本運作原理
- 創件一個 Metamask 錢包
Setting up the environment
因為很簡單,所以請參考 Setting up the environment。
Creating a new Hardhat project
我們將使用 npm 下載 Hardhat。
The Node.js package manager is a package manager and an online repository for JavaScript code. ( npm 的官方敘述)
打開 terminal 接著輸入以下 commands :
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat
當下載好 Hardhat 後在同一目錄下執行: npx hardhat
用鍵盤選擇 Create an empty hardhat.config.js
後按下 enter。 $ npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
Welcome to Hardhat v2.0.0
? What do you want to do? …
Create a sample project
❯ Create an empty hardhat.config.js
Quit
運行 Hardhat 時,它將從 current working directory 開始搜索最近的 hardhat.config.js
文件。 該文件通常位於 project 的 root 中,而一個空白的 hardhat.config.js
文件足以使 Hardhat 正常工作。整個設定都包含在此文件中。
Hardhat's architecture
Hardhat 是圍繞 tasks 和 plugins 的概念而設計的。 Hardhat 大部分的功能來自於 plugins,做為一個開發者你可以自由選擇你所需要的 plugin 。
Tasks
其實每次從 CLI 運行 Hardhat 時都是在執行一個 task 。例如 npx hardhat compile
是在執行 compile
task 。執行 npx hardhat
可以查看 project 中當前可用的 tasks 。 執行 npx hardhat help [task]
可以查看該 task 詳細信息。
你可以創造自己的 tasks 。詳情請看 Creating a task guide。
Plugins
Hardhat 有一些預設值,但全部都可以被 override 。大多數時候使用給定的工具的方式是利用 Plugin 集成到 Hardhat 中。
在此教程我們將會使用 Ethers.js
和 Waffle
plugins 。它允許我們和 Ethereum 溝通並且 test 自己的 contracts 。後面會再介紹怎麼使用它們,在此同時我們先在 project 目錄 install :
npm install --save-dev @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
將第一行添加到 hardhat.config.js
中,使其如下所示: require("@nomiclabs/hardhat-waffle");
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.7.3",
};
在這裡只需要使用 hardhat-waffle
,因為它 depends on hardhat-ethers
,因此不需要同時添加兩者。
Writing and compiling smart contracts
我們將創建一個簡單的 smart contract,該 contract 實做能夠 transfer 的 token。 Token contracts 最常用於 exchange or store value. 這邊將不深入討論 contract 的 Solidity code,但是我們實做了一些應該知道的邏輯:
- token 的總供應量是固定的而且無法被更改
- 整個供應量都分配給部署 contract 的 address
- 任何人都可以收到 tokens
- 任何人只要擁有至少一個 token 則可以 transfer
- The token 無法分割。只可以 transfer 1, 2, 3 or 37 tokens but not 2.5
可能聽說過 ERC20,這是 Ethereum 中的 token 標準。DAI,USDC,MKR 和 ZRX 之類的 tokens 遵循 ERC20 標準,使它們都可以與任何可以處理 ERC20 tokens 的軟件兼容。 為簡單起見,我們要構建的 token 不是 ERC20 。
Writing smart contracts
首先新增一個名為 contracts
的新目錄,然後在該目錄內新增檔案 Token.sol
。 將下面的代碼複製到檔案中,花一點時間閱讀代碼。它很簡單,並且裡頭充滿了解釋 Solidity 代碼基礎的註釋。
如果要 syntax highlighting 應該在 text editor 中添加 Solidity support。 只需尋找 Solidity 或 Ethereum plugins。 建議使用 Visual Studio Code 或 Sublime Text 3。
// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.7.0;
// This is the main building block for smart contracts.
contract Token {
// Some string type variables to identify the token.
// The `public` modifier makes a variable readable from outside the contract.
string public name = "My Hardhat Token";
string public symbol = "MBT";
// The fixed amount of tokens stored in an unsigned integer type variable.
uint256 public totalSupply = 1000000;
// An address type variable is used to store ethereum accounts.
address public owner;
// A mapping is a key/value map. Here we store each account balance.
mapping(address => uint256) balances;
/**
* Contract initialization.
*
* The `constructor` is executed only once when the contract is created.
*/
constructor() {
// The totalSupply is assigned to transaction sender, which is the account
// that is deploying the contract.
balances[msg.sender] = totalSupply;
owner = msg.sender;
}
/**
* A function to transfer tokens.
*
* The `external` modifier makes a function *only* callable from outside
* the contract.
*/
function transfer(address to, uint256 amount) external {
// Check if the transaction sender has enough tokens.
// If `require`'s first argument evaluates to `false` then the
// transaction will revert.
require(balances[msg.sender] >= amount, "Not enough tokens");
// Transfer the amount.
balances[msg.sender] -= amount;
balances[to] += amount;
}
/**
* Read only function to retrieve the token balance of a given account.
*
* The `view` modifier indicates that it doesn't modify the contract's
* state, which allows us to call it without executing a transaction.
*/
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}
*.sol
用於 Solidity 文件。建議將文件名與其包含的 contract 進行匹配,這是一種常見的做法。
Compiling contracts
要編譯 contract,在 terminal 執行 npx hardhat compile
。 The compile
task 是 hardhat 內建的 tasks 之一。
$ npx hardhat compile
Compiling 1 file with 0.7.3
Compilation finished successfully
合約已經成功編譯且可以使用了!
Testing contracts
在建構 smart contracts 時撰寫自動化 tests 至關重要。為此,我們將使用 Hardhat Network ,這是一個內置的以太坊網絡,專門為開發而設計,是 Hardhat 中的默認網絡。無需進行任何設置即可使用它。在我們的 tests 中,我們將使用 ethers.js 與上一節中構建的 Ethereum contract 進行交互,並使用 Mocha 做為我們的 test runner。
Writing tests
在 project root directory 創建一個名為test
的新目錄,並在目錄裡創建一個名為 Token.js
的新文件。
讓我們從下面的代碼開始。接下來,我們將對其進行解釋,但現在將其複製到 Token.js
中:
const { expect } = require("chai");
describe("Token contract", function() {
it("Deployment should assign the total supply of tokens to the owner", async function() {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const hardhatToken = await Token.deploy();
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});
接著在 terminal 執行 npx hardhat test
將會看到下面的輸出: $ npx hardhat test
Token contract
✓ Deployment should assign the total supply of tokens to the owner (654ms)
1 passing (663ms)
這意味著測試通過了。現在讓我們解釋每一行: const [owner] = await ethers.getSigners();
ethers.js
中的 Signer
是一個代表 Ethereum account 的 object。它用於將 transactions 發送到 contracts 和其他 accounts 。在這裡,我們獲得了所連接 node 中的 accounts list,在本例中為 Hardhat Network,僅保留第一個 account 。
The ethers
variable is available in the global scope. 如果想要 code always being explicit, 可以在頂部加上這行:
const { ethers } = require("hardhat");
要了解有關 Signer 的更多信息,可以查看 Signers documentation。
const Token = await ethers.getContractFactory("Token");
ethers.js 中的 ContractFactory
是用於 deploy new smart contracts 的 abstraction,因此 Token
這裡是 a factory for instances of our token contract. const hardhatToken = await Token.deploy();
在 ContractFactory
上調用 deploy()
將啟動 deployment,並返回解析為 Contract
的 Promise
。該 object 具有用於每個 smart contract functions 的 method 。 const ownerBalance = await hardhatToken.balanceOf(owner.address);
deploye contract 後,我們可以在 hardhatToken
上調用我們的 contract methods ,並通過調用 balanceOf()
來獲取 owner account 的 balance 。
請記住,獲得 entire supply token 的 owner 是進行 deploy 的 account ,並且在使用 hardhat-ethers plugin
時,預設情況下,ContractFactory
和 Contract
instances 連接到 first signer 。 這意味著 owner
variable 中的 account executed the deployment ,而 balanceOf()
應該 return the entire supply amount.
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
在這裡,我們再次使用 Contract
instance 在 Solidity 代碼中調用 smart contract function 。totalSupply()
返回 token's supply amount ,我們正在檢查它是否等於 ownerBalance
。
為此,我們使用 Chai ,這是一個 assertions library。這些 asserting functions 稱為 "matchers" ,而我們在此處使用的實際上來自 Waffle 。這就是為什麼我們使用 hardhat-waffle
plugin 的原因,這使得從 assert values from Ethereum 更加容易。查看 Waffle documentation 中的 此部份,以了解 Ethereum-specific matchers 的完整列表.
Using a different account
如果需要從 default account 以外的其他 account ( or Signer
in ethers.js speak ) 發送 transaction 以 test code ,則可以在 ethers.js Contract
中使用 connect()
method 將其連接到其他 account 。像這樣:
const { expect } = require("chai");
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
const hardhatToken = await Token.deploy();
// Transfer 50 tokens from owner to addr1
await hardhatToken.transfer(addr1.address, 50);
expect(await hardhatToken.balanceOf(addr1.address)).to.equal(50);
// Transfer 50 tokens from addr1 to addr2
await hardhatToken.connect(addr1).transfer(addr2.address, 50);
expect(await hardhatToken.balanceOf(addr2.address)).to.equal(50);
});
});
Full coverage
既然已經介紹了 testing contracts 所需的基礎知識,這是 token 的完整測試,其中包含有關 Mocha 以及如何構建測試的許多其他信息。建議完整閱讀。
// We import Chai to use its asserting functions here.
const { expect } = require("chai");
// `describe` is a Mocha function that allows you to organize your tests. It's
// not actually needed, but having your tests organized makes debugging them
// easier. All Mocha functions are available in the global scope.
// `describe` receives the name of a section of your test suite, and a callback.
// The callback must define the tests of that section. This callback can't be
// an async function.
describe("Token contract", function () {
// Mocha has four functions that let you hook into the the test runner's
// lifecyle. These are: `before`, `beforeEach`, `after`, `afterEach`.
// They're very useful to setup the environment for tests, and to clean it
// up after they run.
// A common pattern is to declare some variables, and assign them in the
// `before` and `beforeEach` callbacks.
let Token;
let hardhatToken;
let owner;
let addr1;
let addr2;
let addrs;
// `beforeEach` will run before each test, re-deploying the contract every
// time. It receives a callback, which can be async.
beforeEach(async function () {
// Get the ContractFactory and Signers here.
Token = await ethers.getContractFactory("Token");
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();
// To deploy our contract, we just have to call Token.deploy() and await
// for it to be deployed(), which happens onces its transaction has been
// mined.
hardhatToken = await Token.deploy();
});
// You can nest describe calls to create subsections.
describe("Deployment", function () {
// `it` is another Mocha function. This is the one you use to define your
// tests. It receives the test name, and a callback function.
// If the callback function is async, Mocha will `await` it.
it("Should set the right owner", async function () {
// Expect receives a value, and wraps it in an Assertion object. These
// objects have a lot of utility methods to assert values.
// This test expects the owner variable stored in the contract to be equal
// to our Signer's owner.
expect(await hardhatToken.owner()).to.equal(owner.address);
});
it("Should assign the total supply of tokens to the owner", async function () {
const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
// Transfer 50 tokens from owner to addr1
await hardhatToken.transfer(addr1.address, 50);
const addr1Balance = await hardhatToken.balanceOf(addr1.address);
expect(addr1Balance).to.equal(50);
// Transfer 50 tokens from addr1 to addr2
// We use .connect(signer) to send a transaction from another account
await hardhatToken.connect(addr1).transfer(addr2.address, 50);
const addr2Balance = await hardhatToken.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50);
});
it("Should fail if sender doesn’t have enough tokens", async function () {
const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
// Try to send 1 token from addr1 (0 tokens) to owner (1000 tokens).
// `require` will evaluate false and revert the transaction.
await expect(
hardhatToken.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Not enough tokens");
// Owner balance shouldn't have changed.
expect(await hardhatToken.balanceOf(owner.address)).to.equal(
initialOwnerBalance
);
});
it("Should update balances after transfers", async function () {
const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
// Transfer 100 tokens from owner to addr1.
await hardhatToken.transfer(addr1.address, 100);
// Transfer another 50 tokens from owner to addr2.
await hardhatToken.transfer(addr2.address, 50);
// Check balances.
const finalOwnerBalance = await hardhatToken.balanceOf(owner.address);
expect(finalOwnerBalance).to.equal(initialOwnerBalance - 150);
const addr1Balance = await hardhatToken.balanceOf(addr1.address);
expect(addr1Balance).to.equal(100);
const addr2Balance = await hardhatToken.balanceOf(addr2.address);
expect(addr2Balance).to.equal(50);
});
});
});
對於完整的測試,這是執行 npx hardhat test
後的輸出: $ npx hardhat test
Token contract
Deployment
✓ Should set the right owner
✓ Should assign the total supply of tokens to the owner
Transactions
✓ Should transfer tokens between accounts (199ms)
✓ Should fail if sender doesn’t have enough tokens
✓ Should update balances after transfers (111ms)
5 passing (1s)
請記住,當運行 npx hardhat test
時,如果自上次運行測試以來 contract 已更改,則先對它進行 compile 。
Debugging with Hardhat Network
Hardhat 內建 Hardhat Network, 該網路是為開發而設計的 local Ethereum network 。它允許 deploy your contracts, run your tests and debug your code. 這是 Hardhat 連接的 default network, 因此您無需進行任何設置即可正常工作。 Just run your tests!
Solidity console.log
在 Hardhat Network 上運行 contracts 和 tests 時,可以從 Solidity 代碼中調用 console.log()
來 print logging messages 和 contract variables 。要使用它,必須從 contract 代碼中導入 Hardhat's console.log
。(第三行) 像是:
pragma solidity ^0.6.0;
import "hardhat/console.sol";
contract Token {
//...
}
將一些 console.log
添加到 transfer()
函數中,就像在 JavaScript 中使用它一樣: function transfer(address to, uint256 amount) external {
console.log("Sender balance is %s tokens", balances[msg.sender]);
console.log("Trying to send %s tokens to %s", amount, to);
require(balances[msg.sender] >= amount, "Not enough tokens");
balances[msg.sender] -= amount;
balances[to] += amount;
}
運行 tests 時將顯示 log 輸出: $ npx hardhat test
Token contract
Deployment
✓ Should set the right owner
✓ Should assign the total supply of tokens to the owner
Transactions
Sender balance is 1000 tokens
Trying to send 50 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4
Sender balance is 50 tokens
Trying to send 50 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3
✓ Should transfer tokens between accounts (373ms)
✓ Should fail if sender doesn’t have enough tokens
Sender balance is 1000 tokens
Trying to send 100 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4
Sender balance is 900 tokens
Trying to send 100 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3
✓ Should update balances after transfers (187ms)
5 passing (2s)
Deploying to a live network
準備好與其他人共享 dApp 後,接下來要做的就是將其 deploy to a live network 中。這樣,其他人就可以訪問不在本地系統上運行的 instance 。
有一個處理真實貨幣的 Ethereum network 被稱為 “mainnet” ,然後還有一些不處理真實貨幣但能夠很好地模仿 mainnet 的 live networks ,並且可以被其他人用作共享階段環境。這些被稱為 “testnets” ,以太坊有多個: Ropsten , Kovan , Rinkeby 和 Goerli 。我們建議你將 contracts 部署到 Ropsten 測試網。
對於軟體來說,部署到測試網與部署到主網相同。唯一的區別是你連接到哪個網絡。讓我們看一下使用 ethers.js 部署合約的代碼是什麼樣的。
主要概念是 Signer
, ContractFactory
和 Contract
,我們在 testing 部分對此進行了解釋。與 testing 部份相比,沒有什麼新的需要做,因為當測試 contracts 時,實際上是在向開發網絡進行部署。這使代碼非常相似或相同。
讓我們在 project root's directory 下創建一個新的 scripts
directory,並將以下內容複製到 deploy.js
文件中: 要指示 Hardhat 在運行任何 asks 時連接到特定的 Ethereum network ,可以使用 --network 參數。像這樣:
npx hardhat run scripts/deploy.js --network <network-name>
在這種情況下,如果不使用 --network 參數運行它,則代碼將針對 Hardhat Network 的 embedded instance 運行,因此當 Hardhat 完成運行時,部署實際上會丟失,但是它仍然能測試我們的部署的代碼是否有用: $ npx hardhat run scripts/deploy.js
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Account balance: 10000000000000000000000
Token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Deploying to remote networks
要部署到諸如主網或任何測試網之類的 remote networks ,您需要在您的 hardhat.config.js
文件中添加一個 network entry 。在此示例中,我們將添加 Ropsten : 我們正在使用 Infura ,但是將 url
指向任何 Ethereum node 或 gateway 都可以。快去獲取你的 INFURA_PROJECT_ID
,然後回來。
要在 Ropsten 上進行部署,需要將 ropsten-ETH 發送到將要進行部署的地址中。可以從水龍頭( faucet )獲得一些用於測試網的 ETH ,該服務免費分發測試 ETH 。 Here's the one for Ropsten ,必須在交易前將 Metamask 的網絡更改為 Ropsten 。
TIP 可以通過以下 links 為其他測試網獲取一些 ETH :
最後,執行:
npx hardhat run scripts/deploy.js --network ropsten
如果一切順利,應該看到已部署的 contract address 。
Hardhat Hackathon Boilerplate Project
如果想快速開始使用 dApp 或使用前端查看整個項目的外觀,可以使用 Hardhat 的 hackathon 樣板庫。
What's included
- 我們在本教程中使用的 Solidity 合約
- 使用 ethers.js 和 Waffle 的測試套件
- 使用 ethers.js 與合約進行溝通的 minimal front-end
Solidity contract & tests
在 repo 的根目錄中,將找到在本教程中使用 Token 合同將其整合在一起的 Hardhat 項目。
- token 的總供應量是固定的而且無法被更改
- 整個供應量都分配給部署 contract 的 address
- 任何人都可以收到 tokens
- 任何人只要擁有至少一個 token 則可以 transfer
- The token 無法分割。只可以 transfer 1, 2, 3 or 37 tokens but not 2.5
Frontend app
在 frontend/
中,將找到一個簡單的應用程序,該應用程序允許用戶執行以下兩項操作:
- 查看已連接 wallet's balance
- 發送 tokens to an address
這是一個單獨的 npm 項目,是使用 create-react-app
創建的,因此這意味著它使用了 webpack 和 babel 。
Frontend file architecture
src/
包含所有代碼src/components
包含 react componentsDapp.js
is the only file with business logic.如果要將其用作樣板,請在此處用自己的代碼替換代碼。- 所有其他 component just renders HTML, no logic。
src/contracts
具有合約的 ABI 和 address ,這些由部署腳本自動生成。
How to use it
首先 clone the repository ,然後部署 contracts :
cd hardhat-hackathon-boilerplate/
npm install
npx hardhat node
在這裡,我們僅安裝 npm project's dependencies ,並通過運行 npx hardhat node
啟動一個 Hardhat Network instance ,可以使用 MetaMask 連接到該 instance 。在同一 directory 中的另一個 terminal 運行: npx hardhat --network localhost run scripts/deploy.js
這會將 contract 部署到 Hardhat Network 。完成此操作後執行: cd hardhat-hackathon-boilerplate/frontend/
npm install
npm run start
啟動 react Web app 。在瀏覽器打開 http://localhost:3000/ 將會看到: 在 MetaMask 中將網絡設置為 localhost:8545 ,然後點擊按鈕。然後,將會看到以下內容: 這裡發生的是,用於顯示當前 wallet's balance 的前端代碼檢測到 balance 為 0 ,因此將無法嘗試 transfer 功能。通過運行: npx hardhat --network localhost faucet <your address>
將運行一個包含的c ustom Hardhat task ,該任務使用 deploying account 的 balance 向你的地址發送 100 MBT 和 1 ETH 。這將允許您將 tokens 發送到另一個 address 。
可以在 /tasks/faucet.js 中檢查出該 task 的代碼,這是 hardhat.config.js
所必需的。
$ npx hardhat --network localhost faucet 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90
Transferred 1 ETH and 100 tokens to 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90
在運行 npx hardhat node 的 terminal 中,還應該看到: eth_sendTransaction
Contract call: Token#transfer
Transaction: 0x460526d98b86f7886cd0f218d6618c96d27de7c745462ff8141973253e89b7d4
From: 0xc783df8a850f42e7f7e57013759c285caa701eb6
To: 0x7c2c195cd6d34b8f845992d380aadb2730bb9c6f
Value: 0 ETH
Gas used: 37098 of 185490
Block #8: 0x6b6cd29029b31f30158bfbd12faf2c4ac4263068fd12b6130f5655e70d1bc257
console.log:
Transferring from 0xc783df8a850f42e7f7e57013759c285caa701eb6 to 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90 100 tokens
在我們的合約中顯示 transfer()
函數的 console.log
輸出,這是運行水龍頭 task 後 Web 應用程序的外觀: 嘗試使用它並閱讀代碼。它充滿了註釋,解釋了正在發生的事情,並清楚地指示了什麼代碼是 Ethereum boilerplate ,以及實際的 dApp 邏輯。這將使該 repository 易於為自己的 project 重複使用。
Final thoughts
以下是在整個學習中可能會有用的一些 links :
- Hardhat's Hackathon Boilerplate
- Hardhat's documentation site
- Hardhat Support Discord server
- Ethers.js Documentation
- Waffle Documentation
- Mocha Documentation
- Chai Documentation