参考官网教程,以下为macOS
环境。
1 环境
先安装(升级类似):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
nvm install 22
nvm use 22
nvm alias default 22 # 终端打开后默认22
npm install npm --global # 全局升级npm
2 新建项目
mkdir hardhat-tutorial
cd hardhat-tutorial
接下来可以npm init
,会问几个问题,最后生成一个package.json
。感觉没必要,还是直接安装hardhat(只是针对当前目录):
npm install --save-dev hardhat # 只是dev环境的依赖,去掉--save-dev应该也行
然后初始化:
npx hardhat init
四下回车,就初始化成功了,最后一步的@nomicfoundation/hardhat-toolbox
也是推荐使用的。然后npx hardhat
命令可以查看当前可用任务(命令)。
3 写合约
还是很容易看懂的,code ./
打开VSCode,在contracts
目录下新建Token.sol
。
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Token {
string public name = "My Hardhat Token";
string public symbol = "MHT";
uint256 public totalSupply = 1000000;
address public owner;
mapping(address => uint256) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor() {
balances[msg.sender] = totalSupply;
owner = msg.sender;
}
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Not enough tokens");
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}
保存、编译npx hardhat compile
。
4 测试合约
建一个测试代码文件,如图:
更专业内容如下:
const { expect } = require("chai");
const {
loadFixture,
} = require("@nomicfoundation/hardhat-toolbox/network-helpers");
describe("Token contract", function () { // not async
// 所有测试的共同部分, 相当于一个快照存盘点
async function deployTokenFixture() {
const [owner, addr1, addr2] = await ethers.getSigners(); // 框架默认会用第1个账户部署合约, 成为owner
const hardhatToken = await ethers.deployContract("Token");
await hardhatToken.waitForDeployment();
return { hardhatToken, owner, addr1, addr2 };
}
// describe嵌套分组
describe("Deployment", function () {
it("Should set the right owner", async function () { // async, Mocha will `await` it.
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
expect(await hardhatToken.owner()).to.equal(owner.address); // solidity自动为public状态变量owner生成getter函数, 而不能像访问属性那样
});
it("Should assign the total supply of tokens to the owner", async function () {
const { hardhatToken, owner } = await loadFixture(deployTokenFixture);
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 () {
const { hardhatToken, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await expect(
hardhatToken.transfer(addr1.address, 50)
).to.changeTokenBalances(hardhatToken, [owner, addr1], [-50, 50]);
await expect(
hardhatToken.connect(addr1).transfer(addr2.address, 50) // connect切换账号
).to.changeTokenBalances(hardhatToken, [addr1, addr2], [-50, 50]);
});
it("Should emit Transfer events", async function () {
const { hardhatToken, owner, addr1, addr2 } = await loadFixture(deployTokenFixture);
await expect(hardhatToken.transfer(addr1.address, 50))
.to.emit(hardhatToken, "Transfer")
.withArgs(owner.address, addr1.address, 50);
await expect(hardhatToken.connect(addr1).transfer(addr2.address, 50))
.to.emit(hardhatToken, "Transfer")
.withArgs(addr1.address, addr2.address, 50);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const { hardhatToken, owner, addr1 } = await loadFixture(deployTokenFixture);
const initialOwnerBalance = await hardhatToken.balanceOf(owner.address);
await expect(
hardhatToken.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Not enough tokens");
expect(await hardhatToken.balanceOf(owner.address)).to.equal(initialOwnerBalance);
});
});
});
接下来跑测试npx hardhat test test/Token.js
,注意:如果不写最后一个路径参数,就会跑test目录下所有测试;如果sol文件变动了,会自动编译。
推荐详读:
https://hardhat.org/tutorial/testing-contracts
5 调试
Hardhat
最方便的就是console.log()
,使用打印调试法,加在Token.sol
文件里。
import "hardhat/console.sol";
// ......
console.log("Transferring from %s to %s %s tokens", msg.sender, to, amount);
用法类似printf
。最终结果:
Token contract
Deployment
✔ Should set the right owner (1077ms)
✔ Should assign the total supply of tokens to the owner
Transactions
Transferring from 0xf39fd6e51aa8f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7db50e0d17dc79c8 50 tokens
Transferring from 0x70997970c812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd2e03d12fa4293bc 50 tokens
✔ Should transfer tokens between accounts
Transferring from 0xf39fd6e51aad88f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010d01b50e0d17dc79c8 50 tokens
Transferring from 0x709979c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b58d299e03d12fa4293bc 50 tokens
✔ Should emit Transfer events
✔ Should fail if sender doesn't have enough tokens (61ms)
5 passing (1s)
6 部署实时网络
先在ignition/modules
目录下编写点火脚本Token.js
:
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
const TokenModule = buildModule("TokenModule", (m) => {
const token = m.contract("Token");
return { token };
});
module.exports = TokenModule;
然后开始配置网络,点火如果不写network,会调用一个嵌入网,最终失联。
6.1 申请节点API
首先去alchemy
节点网站(也可以去infura
、quicknode
等)申请API Key
,比如Sepolia
的节点如下:
https://eth-sepolia.g.alchemy.com/v2/aqwowVrbkaqwowVrbGbZhtY3vH-X
可以把API Key
设置到Hardhat
的环境变量里,同时钱包私钥也可以设置下,注意这一步千万小心,不要把主网测试网弄混淆:
npx hardhat vars set ALCHEMY_API_KEY
# 输入申请到的aqwowVrbkaqwowVrbGbZhtY3vH-X
npx hardhat vars set SEPOLIA_PRIVATE_KEY
# 输入测试网的钱包私钥
当然也可以后续直接明文写在hardhat.config.js
里,但是显得不安全不专业。几个常用命令:
npx hardhat vars get TEST_API_KEY
npx hardhat vars list
npx hardhat vars delete TEST_API_KEY
npx hardhat vars path # 查看存储路径,明文有点风险,更好的方法我暂时没想到
HARDHAT_VAR_MY_KEY=123 npx hardhat some-task # 临时改变量
6.2 配置网络
编辑hardhat.config.js
,
require("@nomicfoundation/hardhat-toolbox");
const { vars } = require("hardhat/config");
const ALCHEMY_API_KEY = vars.get("ALCHEMY_API_KEY");
const SEPOLIA_PRIVATE_KEY = vars.get("SEPOLIA_PRIVATE_KEY");
module.exports = {
solidity: "0.8.28",
networks: {
sepolia: {
url: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
accounts: [SEPOLIA_PRIVATE_KEY]
}
}
};
有的配置还会写chainId
,不知道有没有用。
钱包、水龙头领免费币什么的这里不赘述。
最后部署到Sepolia
,
npx hardhat ignition deploy ./ignition/modules/Token.js --network sepolia
成功后:
✔ Confirm deploy to network sepolia (11155111)? … yes
Hardhat Ignition 🚀
Deploying [ TokenModule ]
Batch #1
Executed TokenModule#Token
[ TokenModule ] successfully deployed 🚀
Deployed Addresses
TokenModule#Token - 0x222732222222222222222222
ignition
目录下也有json文件可以看到地址,Sepolia
上可以查到。
7 融合remix
先安装remixd
:
npm install -g @remix-project/remixd
开个终端,运行:
remixd # 默认使用当前目录
# 或者
remixd -s ./myproject -u https://remix.ethereum.org
然后去remix
里连接就行了。
一般习惯是
VS code
开发,remix
调试,记住,在vscode
编辑的时候,一定要先把remix
的代码tab给关掉,否则切回remix
的时候,代码会被网页里的旧代码秒覆盖掉!。还可以使用
Hardhat
自带的节点:
cd myproject
npx hardhat node # 会给出20个账号
然后再remix部署环境里就可以选择dev-hardhat provider
了。其实用remix
自带的VM
调试就挺方便的,建议选择shanghai
节点,在编译里也选择一下默认的evm
为shanghai
。而cancun
有时候真的卡,不用编译和部署时选择不同的VM
,会有bug,比如什么opcode
错误。
当然也可以使用命令行部署,需要编辑hardhat.config.js
,添加localhost
:
module.exports = {
solidity: "0.8.28",
networks: {
localhost: {
url: "https://localhost:8545"
}
}
};
感觉还是remix
里调试方便。
8 其它
ABI导出
安装插件npm install --save-dev hardhat-abi-exporter
,然后配置hardhat.config.js
:
require("@nomicfoundation/hardhat-toolbox");
require('hardhat-abi-exporter');
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.28",
abiExporter: [
{
path: './abi/pretty',
pretty: true,
},
{
path: './abi/ugly',
pretty: false,
}
]
};
运行:
npx hardhat export-abi --no-compile # 参考https://www.npmjs.com/package/hardhat-abi-exporter
扁平
把合约整理成一个独立文件:
npx hardhat flatten
Code关系
部署测试的结论:
// 以下input和output只针对部署时
input = bytecode + 构造实参
bytecode = initCode + runtimeCode
output = runtimeCode
address(this).code = runtimeCode
type(Test).creationCode = bytecode // 需从另一个合约函数读取
另有一个interfaceId
是接口的函数签名keccak256
后再做异或。
签名
地址 + tokenId => 消息 => 以太坊签名消息
以太坊签名消息 + 签名(通过私钥) => 恢复出公钥 与 公钥进行比对
相同,则之前的消息是由签名者所签发
够用了!