Hardhat是一個便於在以太坊上進行構建的開發環境。它幫助開發人員管理和自動化構建智慧合約和dApp的過程中 固有的重複任務,以及輕鬆地圍繞此工作流程引入更多功能,並且內建了開發專用以太坊網路,這意味著從根本上進行編譯和測試。 本教程涵蓋從Hardhat開發環境配置到以太坊智慧合約部署的完整流程,適合正在準備從頭開始快速構建以太坊專案的開發者。
用自己熟悉的語言學習 以太坊DApp開發 : Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart
在本教程中,我們將指導你完成以下操作:
為以太坊開發設定Node.js環境建立和配置Hardhat專案實現令牌的Solidity智慧合約的基礎使用Ethers.js和Waffle為合同編寫自動化測試使用Hardhat Network除錯Solidity將你的合約部署到Hardhat網路和以太坊測試網1、Hardhat開發環境搭建大多數以太坊庫和工具都是用JavaScript編寫的,Hardhat也是如此。Node.js是基於Chrome的V8 JavaScript引擎構建 的JavaScript執行時。它是在Web瀏覽器之外執行JavaScript的最受歡迎的解決方案,而Hardhat則建立在此之上。
如果已經安裝了Node.js>=12.0,則可以跳過本節。如果沒有,請按照以下步驟在Ubuntu,MacOS和Windows上安裝它。
Linux :
將以下命令複製並貼上到終端中:
sudo apt updatesudo apt install curl gitcurl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -sudo apt install nodejs
如果你的Node.js版本早於12.0,請按照以下說明進行升級:
在終端中執行以刪除Node.js:sudo apt remove nodejs在此處找到要安裝的Node.js版本,然後按照說明進行操作。在終端中執行sudo apt update && sudo apt install nodejs,以再次安裝Node.js。MacOS :
確保已安裝git。否則,請按照以下說明進行操作。
在MacOS上有多種安裝Node.js的方法。我們將使用Node Version Manager(nvm)。將以下命令複製並貼上到終端中:
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.2/install.sh | bashnvm install 12nvm use 12nvm alias default 12npm install npm --global # Upgrade npm to the latest version
如果你的Node.js版本早於12.0,可以使用nvm更改Node.js版本。要升級到Node.js 12.x,請在終端中執行以下命令:
nvm install 12nvm use 12nvm alias default 12npm install npm --global # Upgrade npm to the latest version
Windows :
在Windows上安裝Node.js需要一些手動步驟。我們將安裝git,Node.js 12.x和npm。下載並執行以下命令:
Git的Windows安裝程式,下載地址node-v12.XX.XX-x64.msi,下載地址如果你的Node.js版本早於12.0,只能按上述步驟重新安裝,你可以在此處 檢視所有可用版本的列表。
2、建立一個新的Hardhat專案我們將使用npm CLI安裝安全帽。npm是node.js的包管理器和JavaScript程式碼線上倉庫。
開啟一個新的終端並執行以下命令:
安裝完畢後,在安裝Hardhat的目錄中執行:
npx hardhat
用鍵盤選擇Create an empty hardhat.config.js,然後按Enter:
$ npx hardhat888 888 888 888 888888 888 888 888 888888 888 888 888 8888888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888888 888 "88b 888P" d88" 888 888 "88b "88b 888888 888 .d888888 888 888 888 888 888 .d888888 888888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888Welcome to Hardhat v2.0.0? What do you want to do? … Create a sample project> Create an empty hardhat.config.js Quit
當Hardhat執行時,它將從當前工作目錄開始搜尋最近的hardhat.config.js檔案。該檔案通常位於專案的根目錄中, 並且一個空白hardhat.config.js足以使Hardhat正常工作。整個安裝過程都包含在此檔案中。
Hardhat是圍繞任務和外掛的概念設計的。Hardhat的功能大部分來自外掛,作為開發人員,你可以自由選擇要使用的外掛。
任務 : 每次從CLI執行Hardhat時,你都在執行任務。例如npx hardhat compile正在執行compile任務。要檢視專案中當前 可用的任務,請執行npx hardhat。執行npx hardhat help [task]可以檢視任務的幫助資訊。
外掛 : 在最終使用哪種工具方面,Hardhat並未受到質疑,但它確實具有一些內建預設值。所有這些都可以覆蓋。大多數時候, 使用給定工具的方法是使用將其整合到Hardhat中的外掛。
在本教程中,我們將使用Ethers.js和Waffle外掛。他們將允許你與以太坊進行互動並測試你的合約。稍後我們將解釋 它們的用法。要安裝它們,請在你的專案目錄中執行:
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",};
4、開發、編譯智慧合約
我們將建立一個簡單的智慧合約,該合約實現可以轉讓的代幣。代幣合約最常用於交換或儲存價值。在本教程中, 我們不會深入探討合約的Solidity程式碼,但是我們實現了一些你應該知道的邏輯:
有固定的代幣總供應量,無法更改。整個供應都分配給部署合約的地址。任何人都可以接收代幣。擁有至少一個代幣的任何人都可以轉讓代幣。你可能聽說過ERC20,這是以太坊中的代幣標準。DAI,USDC,MKR和ZRX之類的代幣都遵循ERC20標準,使它們可以 與任何可以處理ERC20代幣的軟體相容。為了簡單起見,我們要構建的代幣不是ERC20。
TIPS:可以使用TokenDIY在以太坊、幣安智慧鏈)或火幣生態鏈)上一鍵發幣。
首先建立一個名為contracts的新目錄,然後該目錄內建立一個檔案Token.sol。
將下面的程式碼貼上到檔案中,花一點時間閱讀程式碼。它很簡單,並且包含了解釋Solidity基礎知識的註釋。
// 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]; }}
要編譯合約,只需要在你的終端執行npx hardhat compile。該compile任務是內建任務之一。
$ npx hardhat compileCompiling 1 file with 0.7.3Compilation finished successfully
合約已成功編譯,可以使用了。
5、測試合約開發智慧合約時編寫自動化測試至關重要,因為使用者的資金安全是重要的問題。為此,我們將使用Hardhat Network, 這是一個內建的以太坊網路,專門為開發而設計,是Hardhat中的預設網路,無需進行任何設定即可使用它。
在我們的測試中,我們將使用ethers.js與上一節中構建的以太坊合約進行互動,並使用Mocha作為我們的測試執行器。
在我們的專案的根目錄中建立一個名為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); });});
在終端執行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是代表以太坊賬戶的物件,它用於將交易傳送到合同和其他帳戶。在這裡,我們獲得了 所連線節點中的帳戶列表,在本例中為Hardhat Network,僅保留第一個帳戶。
該ethers變數在全域性範圍內可用。如果您希望程式碼始終是明確的,則可以在頂部新增以下行:
const Token = await ethers.getContractFactory("Token");
ethers.js中ContractFactory用於部署新智慧合約的抽象,因此這是代幣合約例項的工廠。
const hardhatToken = await Token.deploy();
呼叫ContractFactory的deploy()將開始部署合約,並返回一個解析為Contract物件的Promise。該物件具有用於你 的智慧合約功能的全部方法。
const ownerBalance = await hardhatToken.balanceOf(owner.address);
部署合約後,我們可以呼叫合約方法balanceOf()來獲取所有者帳戶的餘額。
請記住,獲得全部供應的代幣所有者是進行部署的帳戶,並且在使用hardhat-ethers外掛 ContractFactory 和Contract例項時,預設情況下將其連線到第一個Signer。這意味著owner變數中的帳戶執行了部署,並且 balanceOf()應返回全部供應量。
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
在這裡,我們再次使用Contract例項在Solidity程式碼中呼叫智慧合約功能。totalSupply()返回代幣供應量, 我們檢查它是否等於ownerBalance。
為此,我們使用的是Chai,這是一個斷言庫。這些斷言函式稱為“匹配器”,而實際上我們在這裡使用的是Waffle。 這就是我們使用hardhat-waffle外掛的原因,它使從以太坊宣告值變得更加容易。請檢視Waffle文件中的說明, 以獲取以太坊特定匹配器的完整列表。
如果你需要使用預設帳戶以外的其他帳戶傳送交易來測試程式碼,則可以使用ethers.js中Contract的connect()方法 將其連線到其他帳戶。像這樣:
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); });});
我們已經介紹了測試合約所需的基礎知識,下面是代幣的完整測試用力,其中包含有關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時,如果合約自上次執行測試以來已發生更改,則將對其進行編譯。
6、使用Hardhat網路進行除錯Hardhat內建有Hardhat Network,Hardhat Network是為開發而設計的本地以太坊網路。它允許你部署合約、 執行測試和除錯程式碼。這是Hardhat連線的預設網路,因此無需進行任何設定即可正常工作。只需執行你的測試。
在Hardhat Network上執行和測試合約時,你可以使用console.log()列印記錄訊息y以及Solidity程式碼呼叫的合約變數。 要使用它,你必須在你的合約程式碼輸入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;}
執行測試時將顯示日誌記錄輸出:
$ npx hardhat test Token contract Deployment ✓ Should set the right owner ✓ Should assign the total supply of tokens to the owner TransactionsSender balance is 1000 tokensTrying to send 50 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4Sender balance is 50 tokensTrying to send 50 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3 ✓ Should transfer tokens between accounts (373ms) ✓ Should fail if sender doesn’t have enough tokensSender balance is 1000 tokensTrying to send 100 tokens to 0xead9c93b79ae7c1591b1fb5323bd777e86e150d4Sender balance is 900 tokensTrying to send 100 tokens to 0xe5904695748fe4a84b40b3fc79de2277660bd1d3 ✓ Should update balances after transfers (187ms) 5 passing (2s)
可以檢視文件以瞭解有關此功能的更多資訊
7、部署合約準備好與其他人共享dApp後,你可能要做的就是將其部署到實時網路中。這樣,其他人可以訪問不在系統 上本地執行的例項。
有一個處理真實貨幣的以太坊網路稱為“主網”,然後還有一些不處理真實貨幣但是很好地模仿現實世界場景的實時網路, 並且可以被其他人用作共享階段環境。這些被稱為“測試網”,以太坊有多個:Ropsten,Kovan,Rinkeby和Goerli。 我們建議你將合約部署到Ropsten測試網。
在軟體層面,部署到測試網與部署到主網是一樣的。唯一的區別是你連線到哪個網路。讓我們看一下使用ethers.js 部署合約的程式碼是什麼樣的。
使用的主要概念Signer,ContractFactory而Contract這是我們在後面解釋的測試部分。與測試相比,沒有什麼新的需要 做,因為當你測試合約時,實際上是在向開發網路進行部署。這使程式碼非常相似或相同。
讓我們在專案根目錄下建立一個新目錄scripts,並將以下內容貼上到deploy.js檔案中:
async function main() { const [deployer] = await ethers.getSigners(); console.log( "Deploying contracts with the account:", deployer.address ); console.log("Account balance:", (await deployer.getBalance()).toString()); const Token = await ethers.getContractFactory("Token"); const token = await Token.deploy(); console.log("Token address:", token.address);}main() .then(() => process.exit(0)) .catch(error => { console.error(error); process.exit(1); });
要指示Hardhat在執行任務時連線到特定的以太坊網路,可以使用--network引數。像這樣:
npx hardhat run scripts/deploy.js --network <network-name>
在這種情況下,不帶--network引數執行時,程式碼將針對Hardhat Network的嵌入式例項執行,因此 當Hardhat完成執行時,部署實際上會丟失,但是測試我們的部署程式碼是否仍然有用:
$ npx hardhat run scripts/deploy.jsDeploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266Account balance: 10000000000000000000000Token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
要部署到諸如主網或任何測試網之類的遠端網路,你需要在hardhat.config.js檔案中新增一個network條目。 在此示例中,我們將使用Ropsten,但你可以類似地新增任何網路:
require("@nomiclabs/hardhat-waffle");// Go to https://www.alchemyapi.io, sign up, create// a new App in its dashboard, and replace "KEY" with its keyconst ALCHEMY_API_KEY = "KEY";// Replace this private key with your Ropsten account private key// To export your private key from Metamask, open Metamask and// go to Account Details > Export Private Key// Be aware of NEVER putting real Ether into testing accountsconst ROPSTEN_PRIVATE_KEY = "YOUR ROPSTEN PRIVATE KEY";module.exports = { solidity: "0.7.3", networks: { ropsten: { url: `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_API_KEY}`, accounts: [`0x${ROPSTEN_PRIVATE_KEY}`] } }};
我們正在使用Alchemy,但是指向url任何以太坊節點或閘道器都可以。
要在Ropsten上進行部署,需要將ropsten-ETH傳送到將要進行部署的地址中。你可以從Faucet獲得 一些用於測試網的ETH,該服務是免費分發測試ETH的服務。這是Ropsten的一個faucet, 你必須在進行交易之前將Metamask的網路更改為Ropsten。
你可以透過以下連結為其他測試網獲取一些ETH:
Kovan FaucetRinkeby FaucetGoerli Faucet最後,執行:
npx hardhat run scripts/deploy.js --network ropsten
如果一切順利,你應該看到已部署的合約地址。
8、Hardhat示例DApp專案如果你想快速開始使用dApp或使用前端檢視整個專案的外觀,可以使用我們的hackathon樣板庫。
https://github.com/nomiclabs/hardhat-hackathon-boilerplat
該專案中包含以下內容:
我們在本教程中使用的Solidity合約使用ethers.js和Waffle的測試套件使用ethers.js與合約進行互動的最小前端在倉庫的根目錄中,你將找到我們將本教程與Token合約放在一起的Hardhat專案。要重新整理其實現的記憶,請執行以下操作:
有固定的代幣總供應量,無法更改。整個供應都分配給部署合約的地址。任何人都可以接收代幣。擁有至少一個代幣的任何人都可以轉讓代幣。代幣不可分割。你可以轉讓1、2、3或37個代幣,但不能轉讓2.5個代幣。在frontend/目錄中,你將找到一個簡單的應用程式,該應用程式允許使用者執行以下兩項操作:
檢視已連線錢包的餘額將代幣傳送到地址這是一個單獨的npm專案,它是使用create-react-app建立的,因此這意味著它使用了webpack和babel。
其他目錄說明如下:
src/ 包含所有程式碼src/components 包含react元件Dapp.js是唯一具有業務邏輯的檔案。如果要將其用作樣板,請在此處用自己的程式碼替換程式碼每個其他元件僅呈現HTML,沒有邏輯。src/contracts 包含合約的ABI和地址,這些由部署指令碼自動生成如何使用首先克隆儲存庫,然後部署合約:
cd hardhat-hackathon-boilerplate/npm installnpx hardhat node
在這裡,我們僅安裝npm專案的依賴項,然後透過執行npx hardhat node我們啟動一個Hardhat Network例項, 你可以使用MetaMask連線到該例項。在同一目錄中的另一個終端中,執行:
npx hardhat --network localhost run scripts/deploy.js
這會將合同部署到Hardhat Network。完成此執行後:
cd hardhat-hackathon-boilerplate/frontend/npm installnpm run start
啟動React Web應用程式。在瀏覽器中開啟http://localhost:3000/,你應該看到以下內容:
這裡用於顯示當前錢包餘額的前端程式碼正在檢測到餘額為0,因此你將無法嘗試轉賬功能。透過執行:
npx hardhat --network localhost faucet <your address>
你將執行一個包含的自定義hardhat任務,該任務使用部署帳戶的餘額將100 MBT和1 ETH傳送到你的地址。 這將允許你將代幣傳送到另一個地址。
$ npx hardhat --network localhost faucet 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90Transferred 1 ETH and 100 tokens to 0x0987a41e73e69f60c5071ce3c8f7e730f9a60f90
在終端中執行npx hardhat node還應該看到:
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
在合約中顯示該函式的console.log輸出,這是執行faucet任務後Web應用程式的外觀:
9、結束語恭喜你完成了本教程!
原文連結:http://blog.hubwiz.com/2021/02/26/hardhat-beginner-tutorial/