Hardhat开发框架基础教程:从入门到实战
2024/12/6大约 9 分钟
Hardhat开发框架基础教程:从入门到实战
引言
Hardhat是目前最受欢迎的以太坊开发框架之一,为智能合约开发者提供了完整的开发环境。本文将从基础概念开始,详细介绍Hardhat的安装、配置、使用方法和最佳实践。
什么是Hardhat
Hardhat概述
Hardhat是一个专业的以太坊开发环境,帮助开发者编译、部署、测试和调试以太坊软件。它由Nomic Foundation开发和维护。
核心特性
- 本地以太坊网络:内置Hardhat Network
- 智能合约编译:支持Solidity编译器
- 自动化测试:强大的测试框架
- 脚本执行:自动化部署和任务
- 插件生态:丰富的扩展插件
- 调试工具:console.log和stack traces
与其他工具的对比
| 特性 | Hardhat | Truffle | Foundry |
|---|---|---|---|
| 语言 | JavaScript/TypeScript | JavaScript | Solidity |
| 测试 | Mocha + Chai | Mocha | Forge |
| 网络 | 内置Hardhat Network | Ganache | Anvil |
| 调试 | 优秀 | 良好 | 优秀 |
| 学习曲线 | 中等 | 较易 | 较难 |
环境准备与安装
系统要求
# Node.js版本要求
Node.js >= 16.0.0
# npm或yarn包管理器
npm >= 7.0.0
# 或
yarn >= 1.22.0安装Hardhat
方法一:创建新项目
# 创建项目目录
mkdir my-hardhat-project
cd my-hardhat-project
# 初始化npm项目
npm init -y
# 安装Hardhat
npm install --save-dev hardhat
# 初始化Hardhat项目
npx hardhat方法二:使用模板
# 使用官方模板
npx hardhat init
# 选择项目类型
? What do you want to do? …
❯ Create a JavaScript project
Create a TypeScript project
Create an empty hardhat.config.js
Quit方法三:全局安装
# 全局安装(不推荐)
npm install -g hardhat
# 创建项目
hardhat init依赖包安装
# 核心依赖
npm install --save-dev @nomicfoundation/hardhat-toolbox
# 测试相关
npm install --save-dev chai
npm install --save-dev @nomicfoundation/hardhat-chai-matchers
# 以太坊工具
npm install --save-dev ethers
npm install --save-dev @nomiclabs/hardhat-ethers项目结构详解
标准项目结构
my-hardhat-project/
├── contracts/ # 智能合约源码
│ └── Lock.sol
├── scripts/ # 部署和任务脚本
│ └── deploy.js
├── test/ # 测试文件
│ └── Lock.js
├── artifacts/ # 编译产物(自动生成)
├── cache/ # 编译缓存(自动生成)
├── node_modules/ # 依赖包
├── hardhat.config.js # Hardhat配置文件
├── package.json # npm配置文件
└── README.md # 项目说明配置文件详解
hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
// Solidity编译器版本
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
// 网络配置
networks: {
// 本地开发网络
localhost: {
url: "http://127.0.0.1:8545"
},
// 测试网络
goerli: {
url: "https://goerli.infura.io/v3/YOUR-PROJECT-ID",
accounts: ["0xYOUR-PRIVATE-KEY"]
},
// 主网络
mainnet: {
url: "https://mainnet.infura.io/v3/YOUR-PROJECT-ID",
accounts: ["0xYOUR-PRIVATE-KEY"]
}
},
// 路径配置
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts"
},
// Gas报告
gasReporter: {
enabled: true,
currency: "USD",
gasPrice: 20
}
};TypeScript配置
// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.19",
networks: {
hardhat: {
chainId: 1337
}
}
};
export default config;基本命令与操作
编译合约
# 编译所有合约
npx hardhat compile
# 强制重新编译
npx hardhat compile --force
# 查看编译信息
npx hardhat compile --verbose运行本地网络
# 启动Hardhat Network
npx hardhat node
# 自定义端口
npx hardhat node --port 8545
# Fork主网
npx hardhat node --fork https://mainnet.infura.io/v3/YOUR-PROJECT-ID控制台交互
# 启动Hardhat控制台
npx hardhat console
# 连接到指定网络
npx hardhat console --network localhost运行测试
# 运行所有测试
npx hardhat test
# 运行特定测试文件
npx hardhat test test/Lock.js
# 显示Gas使用情况
npx hardhat test --gas-report部署脚本
# 运行部署脚本
npx hardhat run scripts/deploy.js
# 部署到指定网络
npx hardhat run scripts/deploy.js --network goerli智能合约开发
示例合约
// contracts/SimpleStorage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleStorage {
uint256 private storedData;
address public owner;
event DataStored(uint256 indexed newValue, address indexed setter);
modifier onlyOwner() {
require(msg.sender == owner, "Not the contract owner");
_;
}
constructor(uint256 _initialValue) {
storedData = _initialValue;
owner = msg.sender;
}
function set(uint256 _value) public {
storedData = _value;
emit DataStored(_value, msg.sender);
}
function get() public view returns (uint256) {
return storedData;
}
function increment() public onlyOwner {
storedData += 1;
emit DataStored(storedData, msg.sender);
}
}编译和ABI
# 编译后查看ABI
npx hardhat compile
# ABI位于artifacts目录
cat artifacts/contracts/SimpleStorage.sol/SimpleStorage.json测试框架使用
基础测试结构
// test/SimpleStorage.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SimpleStorage", function () {
let SimpleStorage;
let simpleStorage;
let owner;
let addr1;
let addr2;
beforeEach(async function () {
// 获取合约工厂
SimpleStorage = await ethers.getContractFactory("SimpleStorage");
// 获取签名者
[owner, addr1, addr2] = await ethers.getSigners();
// 部署合约
simpleStorage = await SimpleStorage.deploy(42);
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await simpleStorage.owner()).to.equal(owner.address);
});
it("Should set the right initial value", async function () {
expect(await simpleStorage.get()).to.equal(42);
});
});
describe("Storage", function () {
it("Should store the value", async function () {
await simpleStorage.set(100);
expect(await simpleStorage.get()).to.equal(100);
});
it("Should emit DataStored event", async function () {
await expect(simpleStorage.set(100))
.to.emit(simpleStorage, "DataStored")
.withArgs(100, owner.address);
});
});
describe("Access Control", function () {
it("Should only allow owner to increment", async function () {
await expect(simpleStorage.connect(addr1).increment())
.to.be.revertedWith("Not the contract owner");
});
it("Should allow owner to increment", async function () {
await simpleStorage.increment();
expect(await simpleStorage.get()).to.equal(43);
});
});
});高级测试技巧
时间操作
const { time } = require("@nomicfoundation/hardhat-network-helpers");
// 增加时间
await time.increase(3600); // 增加1小时
// 设置下一个区块时间
await time.increaseTo(timestamp);
// 获取最新区块时间
const latestTime = await time.latest();快照和恢复
const { takeSnapshot, restoreSnapshot } = require("@nomicfoundation/hardhat-network-helpers");
let snapshot;
beforeEach(async function () {
snapshot = await takeSnapshot();
});
afterEach(async function () {
await restoreSnapshot(snapshot);
});Gas估算
it("Should estimate gas correctly", async function () {
const estimatedGas = await simpleStorage.estimateGas.set(100);
console.log("Estimated gas:", estimatedGas.toString());
const tx = await simpleStorage.set(100);
const receipt = await tx.wait();
console.log("Actual gas used:", receipt.gasUsed.toString());
});部署脚本开发
基础部署脚本
// scripts/deploy.js
const { ethers } = require("hardhat");
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 SimpleStorage = await ethers.getContractFactory("SimpleStorage");
// 部署合约
const simpleStorage = await SimpleStorage.deploy(42);
console.log("SimpleStorage deployed to:", simpleStorage.address);
console.log("Transaction hash:", simpleStorage.deployTransaction.hash);
// 等待确认
await simpleStorage.deployed();
console.log("Contract deployed successfully!");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});高级部署脚本
// scripts/deploy-advanced.js
const { ethers, network } = require("hardhat");
const fs = require("fs");
async function main() {
console.log(`Deploying to network: ${network.name}`);
const [deployer] = await ethers.getSigners();
console.log(`Deployer: ${deployer.address}`);
// 部署参数
const initialValue = 42;
// 部署合约
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
const simpleStorage = await SimpleStorage.deploy(initialValue);
await simpleStorage.deployed();
console.log(`SimpleStorage deployed to: ${simpleStorage.address}`);
// 保存部署信息
const deploymentInfo = {
network: network.name,
contractAddress: simpleStorage.address,
deployerAddress: deployer.address,
initialValue: initialValue,
deploymentTime: new Date().toISOString(),
transactionHash: simpleStorage.deployTransaction.hash
};
fs.writeFileSync(
`deployments/${network.name}.json`,
JSON.stringify(deploymentInfo, null, 2)
);
// 验证合约(如果在测试网或主网)
if (network.name !== "hardhat" && network.name !== "localhost") {
console.log("Waiting for block confirmations...");
await simpleStorage.deployTransaction.wait(6);
await verify(simpleStorage.address, [initialValue]);
}
}
async function verify(contractAddress, args) {
console.log("Verifying contract...");
try {
await run("verify:verify", {
address: contractAddress,
constructorArguments: args,
});
} catch (e) {
if (e.message.toLowerCase().includes("already verified")) {
console.log("Already verified!");
} else {
console.log(e);
}
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});网络配置与管理
本地网络
// hardhat.config.js
module.exports = {
networks: {
hardhat: {
chainId: 1337,
accounts: {
count: 20,
accountsBalance: "10000000000000000000000" // 10000 ETH
}
}
}
};测试网络配置
// 使用环境变量管理私钥
require("dotenv").config();
module.exports = {
networks: {
goerli: {
url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY],
chainId: 5,
gasPrice: 20000000000, // 20 gwei
gas: 6000000
},
sepolia: {
url: `https://sepolia.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY],
chainId: 11155111
}
}
};环境变量配置
# .env文件
INFURA_PROJECT_ID=your_infura_project_id
PRIVATE_KEY=0x_your_private_key_here
ETHERSCAN_API_KEY=your_etherscan_api_key插件生态系统
常用插件
合约验证插件
# 安装
npm install --save-dev @nomiclabs/hardhat-etherscan
# 配置
module.exports = {
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
# 使用
npx hardhat verify --network goerli CONTRACT_ADDRESS "Constructor arg 1"Gas报告插件
# 安装
npm install --save-dev hardhat-gas-reporter
# 配置
module.exports = {
gasReporter: {
enabled: true,
currency: "USD",
gasPrice: 20,
coinmarketcap: process.env.COINMARKETCAP_API_KEY
}
};合约大小插件
# 安装
npm install --save-dev hardhat-contract-sizer
# 配置
require("hardhat-contract-sizer");
module.exports = {
contractSizer: {
alphaSort: true,
runOnCompile: true,
disambiguatePaths: false,
}
};自定义任务
// hardhat.config.js
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
task("balance", "Prints an account's balance")
.addParam("account", "The account's address")
.setAction(async (taskArgs, hre) => {
const balance = await hre.ethers.provider.getBalance(taskArgs.account);
console.log(hre.ethers.utils.formatEther(balance), "ETH");
});调试与开发技巧
合约调试
使用console.log
// contracts/Debug.sol
import "hardhat/console.sol";
contract Debug {
function debugFunction(uint256 value) public {
console.log("Input value:", value);
uint256 result = value * 2;
console.log("Calculated result:", result);
console.log("Sender address:", msg.sender);
}
}错误追踪
// 详细错误信息
try {
await contract.functionThatFails();
} catch (error) {
console.log("Error message:", error.message);
console.log("Error reason:", error.reason);
console.log("Error code:", error.code);
}性能优化
编译优化
module.exports = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200 // 优化运行次数
}
}
}
};测试优化
// 使用beforeEach进行setup
describe("Contract Tests", function () {
let contract;
beforeEach(async function () {
const Contract = await ethers.getContractFactory("MyContract");
contract = await Contract.deploy();
await contract.deployed();
});
// 测试用例...
});实际项目案例
ERC-20代币项目
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) {
_mint(msg.sender, initialSupply * 10**decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}完整测试套件
// test/MyToken.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function () {
let MyToken;
let myToken;
let owner;
let addr1;
let addr2;
const INITIAL_SUPPLY = 1000000;
beforeEach(async function () {
MyToken = await ethers.getContractFactory("MyToken");
[owner, addr1, addr2] = await ethers.getSigners();
myToken = await MyToken.deploy(
"MyToken",
"MTK",
INITIAL_SUPPLY
);
});
describe("Deployment", function () {
it("Should assign the total supply to the owner", async function () {
const ownerBalance = await myToken.balanceOf(owner.address);
expect(await myToken.totalSupply()).to.equal(ownerBalance);
});
it("Should set the right name and symbol", async function () {
expect(await myToken.name()).to.equal("MyToken");
expect(await myToken.symbol()).to.equal("MTK");
});
});
describe("Transfers", function () {
it("Should transfer tokens between accounts", async function () {
await myToken.transfer(addr1.address, 100);
expect(await myToken.balanceOf(addr1.address)).to.equal(100);
await myToken.connect(addr1).transfer(addr2.address, 50);
expect(await myToken.balanceOf(addr2.address)).to.equal(50);
expect(await myToken.balanceOf(addr1.address)).to.equal(50);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const initialOwnerBalance = await myToken.balanceOf(owner.address);
await expect(
myToken.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("ERC20: transfer amount exceeds balance");
expect(await myToken.balanceOf(owner.address)).to.equal(
initialOwnerBalance
);
});
});
describe("Minting", function () {
it("Should mint tokens to specified address", async function () {
await myToken.mint(addr1.address, 1000);
expect(await myToken.balanceOf(addr1.address)).to.equal(1000);
});
it("Should only allow owner to mint", async function () {
await expect(
myToken.connect(addr1).mint(addr2.address, 1000)
).to.be.revertedWith("Ownable: caller is not the owner");
});
});
});最佳实践与建议
项目组织
目录结构规范
project/
├── contracts/
│ ├── interfaces/
│ ├── libraries/
│ ├── mocks/
│ └── tokens/
├── scripts/
│ ├── deploy/
│ └── tasks/
├── test/
│ ├── unit/
│ └── integration/
├── deployments/
└── docs/命名规范
// 合约命名:PascalCase
contract MyAwesomeContract {}
// 函数命名:camelCase
function getUserBalance() {}
// 变量命名:camelCase
uint256 public totalSupply;
// 常量命名:UPPER_SNAKE_CASE
uint256 public constant MAX_SUPPLY = 1000000;安全考虑
私钥管理
# 使用环境变量
export PRIVATE_KEY="0x..."
# 使用.env文件(记得添加到.gitignore)
echo "PRIVATE_KEY=0x..." > .env
echo ".env" >> .gitignore网络安全
// 网络配置分离
const networks = {
development: {
url: "http://localhost:8545",
accounts: ["0x..."] // 测试私钥
},
production: {
url: process.env.MAINNET_URL,
accounts: [process.env.MAINNET_PRIVATE_KEY]
}
};持续集成
GitHub Actions配置
# .github/workflows/test.yml
name: Test Smart Contracts
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Compile contracts
run: npx hardhat compile
- name: Run tests
run: npx hardhat test
- name: Generate gas report
run: npx hardhat test --gas-reporter故障排除
常见问题
编译错误
# 清除缓存
npx hardhat clean
# 强制重新编译
npx hardhat compile --force
# 检查Solidity版本
npx hardhat compile --show-stack-traces网络连接问题
# 检查网络配置
npx hardhat console --network localhost
# 验证RPC连接
curl -X POST \
-H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://localhost:8545Gas估算问题
// 手动设置gas限制
const tx = await contract.myFunction({
gasLimit: 500000,
gasPrice: ethers.utils.parseUnits('20', 'gwei')
});调试技巧
详细日志
# 启用详细日志
DEBUG=hardhat:* npx hardhat test
# 显示堆栈跟踪
npx hardhat test --show-stack-traces交易分析
// 分析交易详情
const tx = await contract.myFunction();
const receipt = await tx.wait();
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Transaction hash:", receipt.transactionHash);
console.log("Block number:", receipt.blockNumber);总结
核心优势
- 开发效率:完整的开发工具链
- 测试框架:强大的自动化测试
- 调试功能:优秀的调试和错误追踪
- 插件生态:丰富的扩展功能
- 社区支持:活跃的开发者社区
学习路径
- 基础入门:安装配置和基本命令
- 合约开发:编写和编译智能合约
- 测试编写:全面的测试覆盖
- 部署实践:多网络部署经验
- 高级特性:插件使用和自定义任务
发展建议
- 关注更新:定期更新Hardhat版本
- 最佳实践:遵循官方推荐的开发模式
- 安全意识:重视私钥和网络安全
- 测试覆盖:确保充分的测试覆盖率
- 文档维护:保持项目文档的及时更新
Hardhat作为现代以太坊开发的标准工具,为智能合约开发者提供了完整、高效的开发体验。掌握Hardhat的使用,是成为专业区块链开发者的重要一步。
