ลองใช้ hardhat กัน


อยากลองเขียน smart contract แบบ run บนเครื่องตัวเอง หลังจากไล่ดูไล่อ่านจากหลายๆที่ก็ไปไปเจอ Hardhat นี้แหละที่เขาใช้กัน หลายๆที่จะแนะนำให้ใช้ Remix ในการเขียน ข้อดีของ Remix คือมันเป็น online ใช้บน browser ได้เลยไม่ต้องติดตั้งโปรแกรมอะไรทั้งนั้น แต่ผมต้องการ run บนเครื่องตัวเองจะได้หาข้อมูลเรื่องอื่นๆนอกจาก coding ด้วย
เนื้อหาในบทความนี้จะเอามาจาก Documentation ของเว็บ hardhat.org
เนื้อหา
- เนื้อหา
- Overview
- Installation
- Runing tasks
- Compiling your contracts
- Testing your contracts
- Deploying your contracts
- อ่านเพิ่ม
Overview
Hardhat เป็นเครื่องมือที่เราจะใช้เพื่อเขียนหรือพัฒนา smart contract สำหรับ Ethereum software ทำให้เราสามารถ compiling, debugging และ deploying ตัว smart contract ของเราได้
Hardhat Runner คือ main หรือเป็นส่วนประกอบหลักเมื่อเราใช้งาน Hadhat ออกแบบมาด้วย concepts ของ tasks และ plugins
ทุกครั้งที่เราสั่ง Hardhat จาก command-line หมายความว่าเรากำลังเรียกใช้ task เช่น npx hardhat compile
ใช้คำสั่ง built-in compile
task
Installation
ก่อนติดตั้งจะต้องสร้างโฟลเดอร์โปรเจคเปล่าๆขึ้นมาก่อน
mkdir my-projectcd my-project
เมื่อพร้อมแล้วสั่ง npx hardhat
เพื่อสร้าง project
npx hardhat
TIP
ถ้าใช้ Windows แนะนำให้ติดตั้งและใช้ command ผ่าน WSL2
$ 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 "Y888
👷 Welcome to Hardhat v2.11.2 👷
? What do you want to do? …❯ Create a JavaScript project Create a TypeScript project Create an empty hardhat.config.js Quit
เลือกสร้างโปรเจคเป็น JavaScript หรือ TypeScript
Runing tasks
เมื่อสร้างเสร็จแล้ว ลองใช้ npx hardhat
คำสั่งเดิมดูว่า มีคำสั่ง built-in อะไรให้ใช้บ้าง
$ npx hardhatHardhat version 2.11.2
Usage: hardhat [GLOBAL OPTIONS] <TASK> [TASK OPTIONS]
GLOBAL OPTIONS:
--config A Hardhat config file. --emoji Use emoji in messages. --flamegraph Generate a flamegraph of your Hardhat tasks --help Shows this message, or a task's help if its name is provided --max-memory The maximum amount of memory that Hardhat can use. --network The network to connect to. --show-stack-traces Show stack traces (always enabled on CI servers). --tsconfig A TypeScript config file. --typecheck Enable TypeScript type-checking of your scripts/tests --verbose Enables Hardhat verbose logging --version Shows hardhat's version.
AVAILABLE TASKS:
check Check whatever you need clean Clears the cache and deletes all artifacts compile Compiles the entire project, building all artifacts console Opens a hardhat console coverage Generates a code coverage report for tests flatten Flattens and prints contracts and their dependencies gas-reporter:merge help Prints this message node Starts a JSON-RPC server on top of Hardhat Network run Runs a user-defined script after compiling the project test Runs mocha tests typechain Generate Typechain typings for compiled contracts verify Verifies contract on Etherscan
To get help for a specific task run: npx hardhat help [task]
คำสั่งเหล่านี้คือ built-in จาก Hardhat แต่เราสามารถติดตั้ง plugins เพิ่มได้ หรือจะเพิ่มคำสั่งแบบ custom เองก็ได้แต่จะยังไม่เขียนไว้ตรงนี้ว่าเพิ่มยังไง
Compiling your contracts
ต่อมาลองดูที่โฟลเดอร์ contracts/
จะเห็นไฟล์ Lock.sol
เป็นตัวอย่าง smart contract ที่เขียนด้วยภาษา solidity เราจะลองสั่ง compile กัน
ยังไม่ต้องสนใจตัวโค้ด อยากให้ดูการใช้คำสั่งของ hardhat ก่อน
// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.9;
// Uncomment this line to use console.log// import "hardhat/console.sol";
contract Lock { uint public unlockTime; address payable public owner;
event Withdrawal(uint amount, uint when);
constructor(uint _unlockTime) payable { require( block.timestamp < _unlockTime, "Unlock time should be in the future" );
unlockTime = _unlockTime; owner = payable(msg.sender); }
function withdraw() public { // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal // console.log("Unlock time is %o and block timestamp is %o", 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); }}
ลอง compile ด้วยคำสั่ง
npx hardhat compile
หลังจาก compile จะเห็นว่ามีโฟลเดอร์ใหม่ชื่อ artifacts, cache ถูกสร้างขึ้นมา
npx hardhat compileCompiled 1 Solidity file successfully
Testing your contracts
ปกติแล้วหลังจากโปรแกรมเมอร์เขียนโค้ดเสร็จ ต่อมาก็ต้องลอง test กันหน่อย โดยตัวโปรเจคได้ใช้ lib ชื่อ Mocha, Chai, Ethers.js เพื่อใช้เขียน test
ลองดูที่โฟลเดอร์ test/
จะมีไฟล์ Lock.js
const { time, loadFixture,} = require("@nomicfoundation/hardhat-network-helpers");const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");const { expect } = require("chai");
describe("Lock", function () { // We define a fixture to reuse the same setup in every test. // We use loadFixture to run this setup once, snapshot that state, // and reset Hardhat Network to that snapshot in every test. async function deployOneYearLockFixture() { const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; const ONE_GWEI = 1_000_000_000;
const lockedAmount = ONE_GWEI; const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;
// Contracts are deployed using the first signer/account by default const [owner, otherAccount] = await ethers.getSigners();
const Lock = await ethers.getContractFactory("Lock"); const lock = await Lock.deploy(unlockTime, { value: lockedAmount });
return { lock, unlockTime, lockedAmount, owner, otherAccount }; }
describe("Deployment", function () { it("Should set the right unlockTime", async function () { const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);
expect(await lock.unlockTime()).to.equal(unlockTime); });
it("Should set the right owner", async function () { const { lock, owner } = await loadFixture(deployOneYearLockFixture);
expect(await lock.owner()).to.equal(owner.address); });
it("Should receive and store the funds to lock", async function () { const { lock, lockedAmount } = await loadFixture( deployOneYearLockFixture );
expect(await ethers.provider.getBalance(lock.address)).to.equal( lockedAmount ); });
it("Should fail if the unlockTime is not in the future", async function () { // We don't use the fixture here because we want a different deployment const latestTime = await time.latest(); const Lock = await ethers.getContractFactory("Lock"); await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( "Unlock time should be in the future" ); }); });
describe("Withdrawals", function () { describe("Validations", function () { it("Should revert with the right error if called too soon", async function () { const { lock } = await loadFixture(deployOneYearLockFixture);
await expect(lock.withdraw()).to.be.revertedWith( "You can't withdraw yet" ); });
it("Should revert with the right error if called from another account", async function () { const { lock, unlockTime, otherAccount } = await loadFixture( deployOneYearLockFixture );
// We can increase the time in Hardhat Network await time.increaseTo(unlockTime);
// We use lock.connect() to send a transaction from another account await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( "You aren't the owner" ); });
it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { const { lock, unlockTime } = await loadFixture( deployOneYearLockFixture );
// Transactions are sent using the first signer by default await time.increaseTo(unlockTime);
await expect(lock.withdraw()).not.to.be.reverted; }); });
describe("Events", function () { it("Should emit an event on withdrawals", async function () { const { lock, unlockTime, lockedAmount } = await loadFixture( deployOneYearLockFixture );
await time.increaseTo(unlockTime);
await expect(lock.withdraw()) .to.emit(lock, "Withdrawal") .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg }); });
describe("Transfers", function () { it("Should transfer the funds to the owner", async function () { const { lock, unlockTime, lockedAmount, owner } = await loadFixture( deployOneYearLockFixture );
await time.increaseTo(unlockTime);
await expect(lock.withdraw()).to.changeEtherBalances( [owner, lock], [lockedAmount, -lockedAmount] ); }); }); });});
เราสามารถสั่ง npx hardhat test
เพื่อทดสอบตาม script ได้เลย ตัว Hardhat จะไปหาไฟล์ที่อยู่ในโฟลเดอร์ test/
ให้เอง
npx hardhat test
Lock Deployment ✔ Should set the right unlockTime (1058ms) ✔ Should set the right owner ✔ Should receive and store the funds to lock ✔ Should fail if the unlockTime is not in the future (38ms) Withdrawals Validations ✔ Should revert with the right error if called too soon ✔ Should revert with the right error if called from another account ✔ Shouldn't fail if the unlockTime has arrived and the owner calls it Events ✔ Should emit an event on withdrawals Transfers ✔ Should transfer the funds to the owner
9 passing (1s)
Deploying your contracts
ต่อมา deploy ตัว contract โดยใช้ Hardhat script
ในโฟลเดอร์ scripts/
จะมีไฟล์ชื่อ deploy.js
// We require the Hardhat Runtime Environment explicitly here. This is optional// but useful for running the script in a standalone fashion through `node <script>`.//// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat// will compile your contracts, add the Hardhat Runtime Environment's members to the// global scope, and execute the script.const hre = require("hardhat");
async function main() { const currentTimestampInSeconds = Math.round(Date.now() / 1000); const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; const unlockTime = currentTimestampInSeconds + ONE_YEAR_IN_SECS;
const lockedAmount = hre.ethers.utils.parseEther("1");
const Lock = await hre.ethers.getContractFactory("Lock"); const lock = await Lock.deploy(unlockTime, { value: lockedAmount });
await lock.deployed();
console.log( `Lock with 1 ETH and unlock timestamp ${unlockTime} deployed to ${lock.address}` );}
// We recommend this pattern to be able to use async/await everywhere// and properly handle errors.main().catch((error) => { console.error(error); process.exitCode = 1;});
เราสามารถสั่ง npx hardhat run
เพื่อ deploy contract
$ npx hardhat run scripts/deploy.jsLock with 1 ETH and unlock timestamp 1695031063 deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
จากทั้งหมดที่ผ่านมา จะเห็นได้ว่าไม่มีการ run node เพื่อสร้าง blockchain network อะไรแบบนั้นเลยแทบจะไม่ต้องทำอะไรที่เกี่ยวกับ infra ของ blockchain ก็สามารถเขียนและทดสอบ smart contract ได้แล้วโดยใช้แค่ Hardhat