Hardhat开发框架
Hardhat是一个专业的以太坊开发环境,提供了完整的开发、测试和部署工具链。本章将介绍Hardhat的核心功能和最佳实践。
环境设置
1. 安装Hardhat
1
2
3
4
5
6
7
8
9
10
11
12 | # 创建新项目目录
mkdir my-dapp
cd my-dapp
# 初始化npm项目
npm init -y
# 安装Hardhat
npm install --save-dev hardhat
# 初始化Hardhat项目
npx hardhat
|
2. 项目结构
| my-dapp/
├── contracts/ # 智能合约目录
├── scripts/ # 部署和任务脚本
├── test/ # 测试文件
├── hardhat.config.js # Hardhat配置文件
└── package.json # 项目配置文件
|
3. 基础配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 | // hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-ethers");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();
module.exports = {
solidity: {
version: "0.8.17",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 1337
},
localhost: {
url: "http://127.0.0.1:8545"
},
goerli: {
url: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`,
accounts: [process.env.PRIVATE_KEY]
},
mainnet: {
url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
|
合约开发
1. 创建合约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | // contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract Token is ERC20, Ownable {
constructor(string memory name, string memory symbol)
ERC20(name, symbol)
{
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
|
2. 编译合约
| # 编译合约
npx hardhat compile
# 清理编译文件
npx hardhat clean
|
3. 部署脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 | // scripts/deploy.js
async function main() {
// 获取合约工厂
const Token = await ethers.getContractFactory("Token");
// 部署合约
const token = await Token.deploy("MyToken", "MTK");
await token.deployed();
console.log("Token deployed to:", token.address);
// 验证合约
if (network.name !== "hardhat") {
await hre.run("verify:verify", {
address: token.address,
constructorArguments: ["MyToken", "MTK"],
});
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
|
测试框架
1. 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 | // test/Token.test.js
const { expect } = require("chai");
describe("Token", function() {
let Token;
let token;
let owner;
let addr1;
let addr2;
beforeEach(async function() {
// 获取合约工厂
Token = await ethers.getContractFactory("Token");
[owner, addr1, addr2] = await ethers.getSigners();
// 部署合约
token = await Token.deploy("MyToken", "MTK");
await token.deployed();
});
describe("Deployment", function() {
it("Should set the right owner", async function() {
expect(await token.owner()).to.equal(owner.address);
});
it("Should assign the total supply of tokens to the owner", async function() {
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
});
describe("Transactions", function() {
it("Should transfer tokens between accounts", async function() {
// 转账
await token.transfer(addr1.address, 50);
expect(await token.balanceOf(addr1.address)).to.equal(50);
// 从addr1转到addr2
await token.connect(addr1).transfer(addr2.address, 50);
expect(await token.balanceOf(addr2.address)).to.equal(50);
});
it("Should fail if sender doesn't have enough tokens", async function() {
const initialOwnerBalance = await token.balanceOf(owner.address);
await expect(
token.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
expect(await token.balanceOf(owner.address)).to.equal(
initialOwnerBalance
);
});
});
});
|
2. 集成测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | // test/integration/Token.test.js
describe("Token Integration", function() {
it("Should interact with other contracts", async function() {
// 部署代币合约
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy("MyToken", "MTK");
// 部署交易所合约
const Exchange = await ethers.getContractFactory("Exchange");
const exchange = await Exchange.deploy();
// 测试交互
await token.approve(exchange.address, 1000);
await exchange.deposit(token.address, 1000);
expect(await token.balanceOf(exchange.address)).to.equal(1000);
});
});
|
3. 测试工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | // test/helpers.js
const { ethers } = require("hardhat");
// 时间操作
async function increaseTime(seconds) {
await ethers.provider.send("evm_increaseTime", [seconds]);
await ethers.provider.send("evm_mine");
}
// 区块操作
async function mineBlocks(count) {
for (let i = 0; i < count; i++) {
await ethers.provider.send("evm_mine");
}
}
// 快照
async function takeSnapshot() {
return await ethers.provider.send("evm_snapshot", []);
}
async function revertToSnapshot(id) {
await ethers.provider.send("evm_revert", [id]);
}
module.exports = {
increaseTime,
mineBlocks,
takeSnapshot,
revertToSnapshot
};
|
任务和脚本
1. 自定义任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | // tasks/token.js
task("balance", "Prints account balance")
.addParam("account", "The account's address")
.addParam("token", "The token's address")
.setAction(async (taskArgs, hre) => {
const token = await hre.ethers.getContractAt("Token", taskArgs.token);
const balance = await token.balanceOf(taskArgs.account);
console.log(`Balance: ${hre.ethers.utils.formatEther(balance)} MTK`);
});
task("mint", "Mints tokens to an address")
.addParam("to", "The recipient's address")
.addParam("amount", "The amount to mint")
.setAction(async (taskArgs, hre) => {
const Token = await hre.ethers.getContractFactory("Token");
const token = Token.attach(process.env.TOKEN_ADDRESS);
const tx = await token.mint(
taskArgs.to,
hre.ethers.utils.parseEther(taskArgs.amount)
);
await tx.wait();
console.log(`Minted ${taskArgs.amount} tokens to ${taskArgs.to}`);
});
|
2. 部署脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | // scripts/deploy-all.js
async function main() {
// 部署代币
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy("MyToken", "MTK");
await token.deployed();
console.log("Token deployed to:", token.address);
// 部署交易所
const Exchange = await ethers.getContractFactory("Exchange");
const exchange = await Exchange.deploy(token.address);
await exchange.deployed();
console.log("Exchange deployed to:", exchange.address);
// 部署后配置
const MINTER_ROLE = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes("MINTER_ROLE")
);
await token.grantRole(MINTER_ROLE, exchange.address);
// 保存部署信息
const deployments = {
token: token.address,
exchange: exchange.address,
network: network.name,
timestamp: new Date().toISOString()
};
fs.writeFileSync(
'deployments.json',
JSON.stringify(deployments, null, 2)
);
}
main();
|
3. 维护脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | // scripts/verify-all.js
async function main() {
const deployments = require('../deployments.json');
// 验证代币合约
await hre.run("verify:verify", {
address: deployments.token,
constructorArguments: ["MyToken", "MTK"]
});
// 验证交易所合约
await hre.run("verify:verify", {
address: deployments.exchange,
constructorArguments: [deployments.token]
});
}
main();
|
高级特性
1. Gas优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | // hardhat.config.js
module.exports = {
solidity: {
version: "0.8.17",
settings: {
optimizer: {
enabled: true,
runs: 200,
details: {
yul: true,
yulDetails: {
stackAllocation: true,
optimizerSteps: "dhfoDgvulfnTUtnIf"
}
}
}
}
}
};
|
2. 合约大小优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | // 使用库减少合约大小
library MathLib {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "Math: addition overflow");
return c;
}
}
contract Token {
using MathLib for uint256;
function transfer(address to, uint256 amount) public {
balances[msg.sender] = balances[msg.sender].sub(amount);
balances[to] = balances[to].add(amount);
}
}
|
3. 自动化测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | // package.json
{
"scripts": {
"test": "hardhat test",
"test:coverage": "hardhat coverage",
"test:gas": "REPORT_GAS=true hardhat test"
}
}
// hardhat.config.js
require("solidity-coverage");
require("hardhat-gas-reporter");
module.exports = {
gasReporter: {
enabled: process.env.REPORT_GAS ? true : false,
currency: "USD",
gasPrice: 100,
coinmarketcap: process.env.COINMARKETCAP_API_KEY
}
};
|
最佳实践
1. 环境配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | // .env
INFURA_KEY=your_infura_key
PRIVATE_KEY=your_private_key
ETHERSCAN_API_KEY=your_etherscan_key
COINMARKETCAP_API_KEY=your_coinmarketcap_key
// .env.example
INFURA_KEY=
PRIVATE_KEY=
ETHERSCAN_API_KEY=
COINMARKETCAP_API_KEY=
// .gitignore
.env
coverage
coverage.json
typechain
deployments.json
|
2. 开发工作流
| # 开发流程
npm install # 安装依赖
npx hardhat compile # 编译合约
npx hardhat test # 运行测试
npx hardhat node # 启动本地节点
npx hardhat run scripts/deploy.js --network localhost # 本地部署
npx hardhat run scripts/deploy.js --network goerli # 测试网部署
npx hardhat verify --network goerli DEPLOYED_ADDRESS # 验证合约
|
3. CI/CD配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | # .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
- run: npm ci
- run: npm test
- run: npm run test:coverage
|
练习题
- 创建一个完整的代币合约项目:
| # 练习:创建ERC20代币项目
npx hardhat init token-project
cd token-project
# 完成合约、测试和部署脚本
|
- 实现自动化部署脚本:
| // 练习:实现部署脚本
async function deployAll() {
// 你的代码
}
|
- 编写完整的测试套件:
| // 练习:实现测试套件
describe("Token", function() {
// 你的代码
});
|
- 创建自定义Hardhat任务:
| // 练习:实现自定义任务
task("custom-task", "Description")
.setAction(async () => {
// 你的代码
});
|
- 实现Gas优化策略:
| // 练习:实现Gas优化
contract OptimizedToken {
// 你的代码
}
|
参考资源
- Hardhat文档
- Hardhat教程
- Solidity优化技巧
- OpenZeppelin合约
- Hardhat最佳实践