从零搭建以太坊私链:手把手教你配置Devnet开发环境(Geth+Hardhat版)
当你在开发DeFi协议或NFT项目时,是否遇到过这样的困境:每次测试智能合约都要消耗真实的测试网代币,调试过程还要忍受漫长的区块确认时间?搭建本地Devnet开发环境就是解决这些痛点的最佳方案。本文将带你用最流行的以太坊客户端Geth和开发框架Hardhat,从创世区块开始构建一个完全自主控制的以太坊私有链。
1. 环境准备与工具安装
在开始之前,我们需要准备以下开发工具和环境。不同于公共测试网,私有Devnet给了你完全的控制权——你可以自定义Gas价格、区块时间、甚至是共识机制。这对于需要频繁测试合约交互的开发者来说,能节省大量等待时间。
1.1 基础软件安装
首先确保你的系统已经安装:
- Node.js (v16.x或更高版本)
- npm/yarn
- Git
然后安装核心工具:
npm install -g ganache-cli hardhat对于Geth客户端的安装,各平台略有不同:
Mac用户:
brew tap ethereum/ethereum brew install ethereumLinux用户:
sudo add-apt-repository -y ppa:ethereum/ethereum sudo apt-get update sudo apt-get install ethereumWindows用户: 建议使用Chocolatey包管理器:
choco install geth1.2 初始化Hardhat项目
创建一个新的项目目录并初始化Hardhat:
mkdir eth-devnet && cd eth-devnet npx hardhat init选择"Create a JavaScript project"选项,这将为你生成基本的项目结构。Hardhat的优势在于它内置了本地网络节点,但我们将配置它连接到我们自己的Geth私有链,以获得更真实的开发体验。
2. 配置创世区块
创世区块是区块链的起点,它定义了网络的基本参数。我们将创建一个名为genesis.json的文件来定制我们的私有链。
2.1 创世文件配置
{ "config": { "chainId": 1337, "homesteadBlock": 0, "eip150Block": 0, "eip155Block": 0, "eip158Block": 0, "byzantiumBlock": 0, "constantinopleBlock": 0, "petersburgBlock": 0, "istanbulBlock": 0, "berlinBlock": 0, "londonBlock": 0 }, "alloc": { "0xYourAccountAddress": { "balance": "1000000000000000000000" } }, "coinbase": "0x0000000000000000000000000000000000000000", "difficulty": "0x20000", "extraData": "", "gasLimit": "0x2fefd8", "nonce": "0x0000000000000042", "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "timestamp": "0x00" }关键参数说明:
chainId: 设置为1337,这是本地开发常用的链IDdifficulty: 调低挖矿难度,加速区块生成gasLimit: 设置较高的gas上限,方便测试复杂合约alloc: 预分配测试ETH到你的开发账户
2.2 初始化Geth节点
使用创世文件初始化你的私有链:
geth init --datadir=./chaindata genesis.json这将在chaindata目录下创建区块链的初始状态。--datadir参数指定了区块链数据的存储位置,保持项目结构整洁很重要。
3. 启动私有链节点
现在我们可以启动我们的私有链节点了。Geth提供了多种配置选项来优化开发体验。
3.1 基本启动命令
geth --datadir ./chaindata \ --networkid 1337 \ --http \ --http.addr 0.0.0.0 \ --http.port 8545 \ --http.api "eth,net,web3,personal,miner" \ --allow-insecure-unlock \ --mine \ --miner.threads 1 \ --miner.etherbase "0xYourAccountAddress"参数解析:
--networkid: 必须与创世文件中的chainId一致--http: 启用HTTP-RPC服务器--mine: 启用挖矿,自动处理交易--miner.threads: 控制挖矿CPU使用率--allow-insecure-unlock: 允许账户解锁(仅限开发环境)
3.2 优化开发体验
为了更方便的调试,建议添加以下参数:
--verbosity 3 \ --metrics \ --metrics.expensive \ --pprof \ --pprof.addr 0.0.0.0 \ --pprof.port 6060这些选项启用了性能监控和pprof分析工具,当你的DApp变得复杂时,这些工具对性能调优非常有帮助。
4. 配置Hardhat连接私有链
现在我们需要配置Hardhat来使用我们的Geth私有链,而不是它内置的本地网络。
4.1 修改hardhat.config.js
require("@nomicfoundation/hardhat-toolbox"); module.exports = { solidity: "0.8.19", networks: { devnet: { url: "http://127.0.0.1:8545", accounts: [ "0x你的私钥" // 仅用于开发环境! ] } } };安全提示:永远不要在版本控制中提交私钥。考虑使用环境变量或
.env文件来管理敏感信息。
4.2 账户管理最佳实践
在开发环境中,我们可以使用Geth的控制台创建测试账户:
geth attach http://localhost:8545在Geth控制台中执行:
personal.newAccount("你的密码") personal.unlockAccount(eth.accounts[0], "你的密码", 0)更安全的方式是使用Hardhat的账户插件:
npm install @nomicfoundation/hardhat-network-helpers然后在测试脚本中:
const helpers = require("@nomicfoundation/hardhat-network-helpers"); const account = await helpers.createRandomAccount();5. 智能合约开发与部署流程
现在环境已经搭建完成,让我们走一遍完整的智能合约开发部署流程。
5.1 创建示例合约
在contracts目录下创建SimpleStorage.sol:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorage { uint256 private storedData; event ValueChanged(uint256 newValue); function set(uint256 x) public { storedData = x; emit ValueChanged(x); } function get() public view returns (uint256) { return storedData; } }5.2 编写部署脚本
在scripts目录下创建deploy.js:
const hre = require("hardhat"); async function main() { const SimpleStorage = await hre.ethers.getContractFactory("SimpleStorage"); const simpleStorage = await SimpleStorage.deploy(); await simpleStorage.deployed(); console.log(`SimpleStorage deployed to: ${simpleStorage.address}`); } main().catch((error) => { console.error(error); process.exitCode = 1; });5.3 部署合约
运行部署命令:
npx hardhat run scripts/deploy.js --network devnet你应该会看到类似输出:
SimpleStorage deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa35.4 与合约交互
创建一个测试脚本scripts/interact.js:
const hre = require("hardhat"); async function main() { const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"; const SimpleStorage = await hre.ethers.getContractAt("SimpleStorage", contractAddress); // 设置值 const setTx = await SimpleStorage.set(42); await setTx.wait(); // 获取值 const value = await SimpleStorage.get(); console.log("Stored value:", value.toString()); } main().catch((error) => { console.error(error); process.exitCode = 1; });6. 高级配置与优化技巧
基本的Devnet已经搭建完成,但要让开发环境更高效,还需要一些进阶配置。
6.1 自定义Gas价格
在hardhat.config.js中为devnet网络添加:
gasPrice: 20000000000, // 20 Gwei gas: 8000000或者在Geth启动时设置:
--miner.gasprice 20000000000 \ --miner.gaslimit 80000006.2 自动挖矿配置
默认情况下,Geth需要手动启动挖矿。我们可以配置自动挖矿:
--mine \ --miner.threads 1 \ --miner.etherbase "0xYourAccountAddress" \ --miner.gastarget 8000000 \ --miner.gasprice 20000000000或者使用JavaScript API在Geth控制台中:
miner.start(1)6.3 快照与状态恢复
开发过程中经常需要重置链状态。Geth支持快照功能:
创建快照:
geth snapshot dump --datadir ./chaindata snapshot.json恢复快照:
geth snapshot restore --datadir ./chaindata snapshot.json6.4 跨项目共享配置
如果你有多个项目使用相同的Devnet配置,可以创建一个共享的Hardhat配置:
// shared-hardhat-config.js module.exports = { networks: { devnet: { url: "http://localhost:8545", chainId: 1337, gasPrice: 20000000000 } } }; // 项目中的hardhat.config.js const sharedConfig = require("../shared-hardhat-config"); module.exports = { ...sharedConfig, solidity: "0.8.19" };7. 常见问题排查
即使按照指南操作,开发过程中仍可能遇到各种问题。以下是几个常见问题及其解决方案。
7.1 连接问题排查
症状:Hardhat无法连接到Geth节点
检查步骤:
- 确认Geth正在运行并监听正确端口
lsof -i :8545 - 检查Geth日志是否有错误
- 尝试直接通过curl测试连接:
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545
7.2 交易卡住不处理
解决方案:
- 检查是否启用了挖矿:
如果返回false,启动挖矿:eth.miningminer.start(1) - 检查账户是否解锁:
eth.accounts personal.listAccounts - 增加Gas价格:
miner.setGasPrice(20000000000)
7.3 合约部署失败
常见原因:
- Solidity版本不匹配
- Gas不足
- 构造函数参数错误
排查方法:
- 检查Hardhat编译日志
- 增加部署脚本中的Gas限制:
const contract = await Contract.deploy({ gasLimit: 8000000 }); - 使用
--verbose标志运行部署脚本
7.4 性能优化技巧
当你的Devnet开始变慢时:
- 定期清理旧的链数据:
geth removedb --datadir ./chaindata geth init --datadir=./chaindata genesis.json - 增加缓存大小:
--cache 4096 - 禁用不需要的API:
--http.api "eth,net,web3"
8. 集成测试与持续开发
一个完善的Devnet环境应该支持自动化测试和持续集成流程。
8.1 编写自动化测试
在test目录下创建SimpleStorage.test.js:
const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("SimpleStorage", function () { it("Should store and retrieve a value", async function () { const SimpleStorage = await ethers.getContractFactory("SimpleStorage"); const simpleStorage = await SimpleStorage.deploy(); await simpleStorage.deployed(); // 设置值 await simpleStorage.set(42); // 验证值 expect(await simpleStorage.get()).to.equal(42); }); });运行测试:
npx hardhat test --network devnet8.2 配置CI/CD流程
创建一个基本的GitHub Actions工作流.github/workflows/test.yml:
name: Smart Contract Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: '16' - name: Install dependencies run: npm install - name: Start Geth Devnet run: | geth init --datadir=./chaindata genesis.json geth --datadir ./chaindata \ --networkid 1337 \ --http \ --http.addr 0.0.0.0 \ --http.port 8545 \ --http.api "eth,net,web3,personal" \ --allow-insecure-unlock \ --mine \ --miner.threads 1 & - name: Run tests run: npx hardhat test --network devnet8.3 模拟主网环境
为了更接近主网环境,可以调整以下参数:
- 修改
genesis.json:- 增加
difficulty - 调整
gasLimit到接近主网的值
- 增加
- 使用主网区块浏览器API:
// hardhat.config.js networks: { devnet: { // ... forking: { url: "https://mainnet.infura.io/v3/YOUR_PROJECT_ID", blockNumber: 15815693 // 特定区块高度 } } } - 设置合理的Gas价格:
await hre.network.provider.send("hardhat_setNextBlockBaseFeePerGas", ["0x" + (15e9).toString(16)])
9. 扩展Devnet功能
基础Devnet搭建完成后,你可以根据需要扩展更多功能来模拟真实的生产环境。
9.1 添加多个节点
创建真正的P2P网络而不是单节点:
- 为每个节点创建独立的数据目录:
mkdir -p chaindata/node{1..3} - 分别初始化每个节点:
geth init --datadir=./chaindata/node1 genesis.json - 获取节点enode信息:
geth --datadir ./chaindata/node1 console --exec "admin.nodeInfo.enode" - 启动节点时指定静态节点:
geth --datadir ./chaindata/node2 \ --port 30304 \ --bootnodes "enode://..."
9.2 监控与数据分析
配置Geth的监控接口:
--metrics \ --metrics.expensive \ --pprof \ --pprof.addr 0.0.0.0 \ --pprof.port 6060然后可以使用Prometheus和Grafana来可视化监控数据:
- 配置Prometheus收集指标:
# prometheus.yml scrape_configs: - job_name: 'geth' static_configs: - targets: ['localhost:6060'] - 导入Geth的Grafana仪表板模板
9.3 集成IPFS
对于NFT项目,通常需要IPFS支持:
- 安装IPFS:
curl -O https://dist.ipfs.io/go-ipfs/v0.12.0/go-ipfs_v0.12.0_linux-amd64.tar.gz tar -xvzf go-ipfs_v0.12.0_linux-amd64.tar.gz cd go-ipfs ./install.sh - 初始化并启动IPFS节点:
ipfs init ipfs daemon - 在Hardhat项目中使用IPFS:
然后创建上传脚本:npm install @pinata/sdkconst pinataSDK = require('@pinata/sdk'); const pinata = new pinataSDK('yourPinataApiKey', 'yourPinataSecretApiKey'); const fs = require('fs'); const readableStreamForFile = fs.createReadStream('./path/to/file.png'); pinata.pinFileToIPFS(readableStreamForFile).then((result) => { console.log(result.IpfsHash); });
9.4 实现跨链测试环境
虽然真正的跨链需要更复杂的设置,但你可以模拟多链环境:
- 创建不同chainId的多个Devnet:
// genesis1.json "config": { "chainId": 1337 } // genesis2.json "config": { "chainId": 31337 } - 使用Hardhat的forking功能模拟跨链调用:
// hardhat.config.js networks: { devnet1: { url: "http://localhost:8545", chainId: 1337 }, devnet2: { url: "http://localhost:8546", chainId: 31337, forking: { url: "http://localhost:8545", blockNumber: 12345 } } } - 编写跨链测试用例:
describe("Cross-chain", function() { it("should bridge assets", async function() { const [owner] = await ethers.getSigners(); // 在链1上锁定资产 const chain1 = await ethers.getSigner("devnet1"); await chain1.sendTransaction({ to: bridgeAddress, value: ethers.utils.parseEther("1.0") }); // 在链2上验证并铸造 const chain2 = await ethers.getSigner("devnet2"); const balance = await chain2.getBalance(); expect(balance).to.equal(ethers.utils.parseEther("1.0")); }); });
10. 安全最佳实践
虽然Devnet是开发环境,但养成良好的安全习惯很重要,特别是当你的代码最终要部署到主网时。
10.1 账户安全管理
- 永远不要在代码中硬编码私钥
- 使用环境变量管理敏感信息:
创建npm install dotenv.env文件:
然后在PRIVATE_KEY=your_private_keyhardhat.config.js中:require('dotenv').config(); module.exports = { networks: { devnet: { accounts: [process.env.PRIVATE_KEY] } } }; - 将
.env添加到.gitignore
10.2 合约安全测试
- 安装安全分析工具:
npm install --save-dev @nomicfoundation/hardhat-verify npm install --save-dev solidity-coverage - 配置Hardhat:
require("@nomicfoundation/hardhat-verify"); require("solidity-coverage"); module.exports = { // ... etherscan: { apiKey: process.env.ETHERSCAN_API_KEY } }; - 运行安全分析:
npx hardhat coverage
10.3 权限控制模式
即使在开发环境,也应该实现合理的权限控制:
// contracts/OwnableDemo.sol pragma solidity ^0.8.0; contract OwnableDemo { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function restrictedFunction() public onlyOwner { // 只有所有者能调用 } function transferOwnership(address newOwner) public onlyOwner { require(newOwner != address(0), "Invalid address"); owner = newOwner; } }10.4 升级模式实践
使用OpenZeppelin的升级插件实现可升级合约:
- 安装依赖:
npm install @openzeppelin/hardhat-upgrades - 配置Hardhat:
require('@openzeppelin/hardhat-upgrades'); - 编写可升级合约:
// contracts/UpgradeableDemo.sol pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract UpgradeableDemo is Initializable { uint256 public value; function initialize(uint256 _value) public initializer { value = _value; } function setValue(uint256 _value) public { value = _value; } } - 编写部署脚本:
const { ethers, upgrades } = require("hardhat"); async function main() { const UpgradeableDemo = await ethers.getContractFactory("UpgradeableDemo"); const instance = await upgrades.deployProxy(UpgradeableDemo, [42]); await instance.deployed(); console.log("Proxy deployed to:", instance.address); }
11. 性能调优与压力测试
当你的DApp复杂度增加时,Devnet的性能调优就变得尤为重要。
11.1 基准测试方法
- 安装基准测试工具:
npm install --save-dev @ethereumjs/vm @ethereumjs/block - 创建测试脚本:
const { VM } = require('@ethereumjs/vm'); const { Block } = require('@ethereumjs/block'); const Common = require('@ethereumjs/common').default; const common = new Common({ chain: 'mainnet' }); const vm = new VM({ common }); async function runBenchmark() { const block = Block.fromBlockData({}, { common }); const start = Date.now(); for (let i = 0; i < 1000; i++) { await vm.runBlock({ block }); } console.log(`Time taken: ${Date.now() - start}ms`); } runBenchmark();
11.2 Geth性能参数
优化Geth启动参数:
--cache 4096 \ --gcmode archive \ --txlookuplimit 0 \ --syncmode full \ --maxpeers 50 \ --metrics \ --pprof关键参数说明:
--cache: 增加内存缓存大小,提高性能--gcmode: 设置归档模式保留所有历史状态--txlookuplimit: 禁用交易索引限制--syncmode: 完整同步模式
11.3 负载测试工具
使用Hardhat的负载测试能力:
const { ethers } = require("hardhat"); async function loadTest() { const [deployer] = await ethers.getSigners(); const Contract = await ethers.getContractFactory("YourContract"); const contract = await Contract.deploy(); // 模拟100次连续调用 const start = Date.now(); for (let i = 0; i < 100; i++) { await contract.someFunction(); } const duration = Date.now() - start; console.log(`Average tx time: ${duration / 100}ms`); } loadTest();11.4 状态快照管理
当Devnet运行时间较长后,状态数据会变得庞大。我们可以使用快照来管理:
- 创建定期快照:
geth snapshot dump --datadir ./chaindata snapshot-$(date +%Y%m%d).json - 恢复到特定快照:
geth snapshot restore --datadir ./chaindata snapshot-20230801.json - 自动化快照管理脚本:
#!/bin/bash DATE=$(date +%Y%m%d) geth snapshot dump --datadir ./chaindata snapshot-$DATE.json find ./chaindata -name "snapshot-*.json" -mtime +7 -exec rm {} \;
12. 团队协作开发配置
当多个开发者共同使用一个Devnet环境时,需要一些额外的配置。
12.1 共享节点配置
- 将Geth节点作为服务运行:
[Unit] Description=Ethereum Devnet Node After=network.target [Service] User=ubuntu ExecStart=/usr/bin/geth --datadir /path/to/chaindata \ --networkid 1337 \ --http \ --http.addr 0.0.0.0 \ --http.port 8545 \ --http.api "eth,net,web3,personal,miner" \ --allow-insecure-unlock \ --mine \ --miner.threads 1 Restart=always RestartSec=3 LimitNOFILE=4096 [Install] WantedBy=multi-user.target - 启用服务:
sudo systemctl enable geth-devnet sudo systemctl start geth-devnet
12.2 多开发者账户管理
- 在创世文件中预分配多个账户:
"alloc": { "0xAccount1": { "balance": "1000000000000000000000" }, "0xAccount2": { "balance": "1000000000000000000000" }, "0xAccount3": { "balance": "1000000000000000000000" } } - 为每个开发者创建独立的Hardhat配置:
// hardhat.config.user1.js module.exports = { networks: { devnet: { url: "http://devnet.example.com:8545", accounts: ["0xuser1PrivateKey"] } } }; - 使用特定配置运行:
npx hardhat run scripts/deploy.js --config hardhat.config.user1.js
12.3 文档与知识共享
- 创建项目README.md:
# Ethereum Devnet 环境指南 ## 快速开始 1. 克隆仓库 2. 安装依赖:`npm install` 3. 启动Geth节点:`./scripts/start-devnet.sh` 4. 部署合约:`npx hardhat run scripts/deploy.js --network devnet` ## 账户管理 - 预分配账户: - 地址1: 0x... (私钥存储在1password) - 地址2: 0x... ## 常用命令 - 重启节点:`sudo systemctl restart geth-devnet` - 查看日志:`journalctl -u geth-devnet -f` - 使用Swagger记录API:
# swagger.yaml openapi: 3.0.0 info: title: Devnet API version: 1.0.0 paths: /: post: summary: JSON-RPC endpoint requestBody: content: application/json: schema: type: object properties: jsonrpc: type: string method: type: string params: type: array id: type: integer responses: '200': description: JSON-RPC response
12.4 持续集成流程
扩展之前的CI配置以支持团队开发:
name: Team Devnet CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x] network: [devnet1, devnet2] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm install - name: Start Geth ${{ matrix.network }} run: | geth init --datadir ./chaindata genesis-${{ matrix.network }}.json geth --datadir ./chaindata \ --networkid ${{ matrix.network == 'devnet1' && '1337' || '31337' }} \ --http --http.addr 0.0.0.0 --http.port 8545 \ --http.api "eth,net,web3,personal" \ --allow-insecure-unlock \ --mine --miner.threads 1 & - name: Run tests run: npx hardhat test --network ${{ matrix.network }} - name: Run linter run: npx eslint '**/*.js'13. 真实项目案例实践
让我们通过一个真实的DeFi项目案例来演示如何在Devnet环境中开发和测试。
13.1 创建ERC20代币
// contracts/MyToken.sol pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyToken is ERC20 { constructor(uint256 initialSupply) ERC20("MyToken", "MTK") { _mint(msg.sender, initialSupply); } }部署脚本:
// scripts/deploy-token.js async function main() { const [deployer] = await ethers.getSigners(); console.log("Deploying contracts with account:", deployer.address); const MyToken = await ethers.getContractFactory("MyToken"); const token = await MyToken.deploy(ethers.utils.parseEther("1000000")); console.log("Token deployed to:", token.address); }13.2 开发质押合约
// contracts/Staking.sol pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract Staking is ReentrancyGuard { IERC20 public token; mapping(address => uint256) public stakedBalances; mapping(address => uint256) public rewardBalances; mapping(address => uint256) public lastStakedTime; uint256 public constant REWARD_RATE = 10; // 10% per year constructor(address _token) { token = IERC20(_token); } function stake(uint256 amount) external nonReentrant { require(amount > 0, "Amount must be positive"); // 先发放之前的奖励 if (stakedBalances[msg.sender] > 0) { uint256 reward = calculateReward(msg.sender); rewardBalances[msg.sender] += reward; } token.transferFrom(msg.sender, address(this), amount); stakedBalances[msg.sender] += amount; lastStakedTime[msg.sender] = block.timestamp; } function calculateReward(address user) public view returns (uint256) { if (stakedBalances[user] == 0) return 0; uint256 stakedTime = block.timestamp - lastStakedTime[user]; return stakedBalances[user] * REWARD_RATE * stakedTime / (365 days * 100); } function claimReward() external nonReentrant { uint256 reward = rewardBalances[msg.sender] + calculateReward(msg.sender); require(reward > 0, "No reward to claim"); rewardBalances[msg.sender] = 0; lastStakedTime[msg.sender] = block.timestamp; token.transfer(msg.sender, reward); } function unstake(uint256 amount) external nonReentrant { require(amount > 0 && amount <= stakedBalances[msg.sender], "Invalid amount"); // 先发放奖励 uint256 reward = calculateReward(msg.sender); rewardBalances[msg.sender] += reward; stakedBalances[msg.sender] -= amount; lastStakedTime[msg.sender] = block.timestamp; token.transfer(msg.sender, amount); } }13.3 编写完整测试套件
// test/Staking.test.js const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("Staking System", function () { let token, staking, owner, user1, user2; beforeEach(async function () { [owner, user1, user2] = await ethers.getSigners(); const MyToken = await ethers.getContractFactory("MyToken"); token = await MyToken.deploy(ethers.utils.parseEther("1000000")); const Staking = await ethers.getContractFactory("Staking"); staking = await Staking.deploy(token.address); // 给用户分配代币 await token.transfer(user1.address, ethers.utils.parseEther("1000")); await token.transfer(user2.address, ethers.utils.parseEther("1000")); }); it("should allow users to stake tokens", async function