创建 hardhat 项目
# 创建 npm 空项目
npm init
# 安装
npm install --save-dev hardhat@2.11.1
# 创建工程
npx hardhat -> 选择高级ts项目
运行测试
# 编译合约
npx hardhat compile
# 单元测试
npx hardhat test
添加合约
将 Worldcup.sol(上节编写的合约)添加到 contracts 目录,并进行编译
单元测试
创建 test/WorldCup.ts,用于编写测试文件:
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
import hre from "hardhat";
import { WorldCup } from "../typechain-types";
describe("WorldCup", function () {
enum Country {
GERMANY,
FRANCH,
CHINA,
BRAZIL,
KOREA
}
// const 声明常量
const TWO_WEEKS_IN_SECS = 14 * 24 * 60 * 60;
const ONE_GEWI = 1_000_000_000;
const ONE_ETHER = ethers.utils.parseEther("1");
// let 声明的变量只在 let 命令所在的代码块内有效
let worldcupIns: WorldCup
// 管理员地址
let ownerAddr:string
// 其他地址
let otherAccountAddr:string
let deadline1:number
// 定义一个 fixture,每次测试可重复使用相同的设置
// 利用 loadFixture 运行这个设置
async function deployWorldcupFixture() {
// 获取第一个钱包对象,用于发起交易
const [owner, otherAccount] = await ethers.getSigners();
// 获取合约对象
const WorldCup = await ethers.getContractFactory("WorldCup");
// 下注截止时间
const deadline = (await time.latest()) + TWO_WEEKS_IN_SECS;
// 部署合约
const worldcup = await WorldCup.deploy(deadline);
return {worldcup, deadline, owner, otherAccount};
}
// Mocha 库:beforeEach() 在测试前会调用该钩子
this.beforeEach(async () => {
// loadFixture -waffle 语法
// 从内存中获取合约状态快照(仅用于测试),执行每个单元测试的时候,状态都会回到最初
const {worldcup, owner, otherAccount, deadline} = await loadFixture(deployWorldcupFixture);
worldcupIns = worldcup
ownerAddr = owner.address
otherAccountAddr = otherAccount.address
deadline1 = deadline
})
// async ES7 异步关键字
// await 关键字仅在 async function 中有效
// await 返回值:1- Promise 对象:await 会暂停执行,等待 Promise 对象 resolve,然后恢复 async 函数的执行并返回解析值;2- 非 Promise 对象:直接返回对应的值;
let preparePlay = async () => {
const [A, B, C, D] = await ethers.getSigners();
await worldcupIns.connect(A).play(Country.GERMANY, {value: ONE_GEWI})
await worldcupIns.connect(B).play(Country.GERMANY, {value: ONE_GEWI})
await worldcupIns.connect(C).play(Country.GERMANY, {value: ONE_GEWI})
await worldcupIns.connect(D).play(Country.FRANCH, {value: ONE_GEWI})
}
/**
* 编写测试逻辑
*/
// 部署相关测试
describe("Deployment", function () {
// 检查部署时 “下注截止时间”是否正确 it() 属于 Mocha 库
it("Should set the right deadline", async function () {
console.log('deadline:', deadline1);
// chai.js 语法:expect,使用构造函数创建断言对象实例
expect(await worldcupIns.deadline()).to.equal(deadline1);
});
// 检查部署时 管理员是否正确
it("Should set the right owner", async function () {
expect(await worldcupIns.admin()).to.equal(ownerAddr);
});
// 检查部署时 如果时间不是在当前时间之后 是否会抛出异常
it("Should fail if the deadline is not in the future", async function () {
const latestTime = await time.latest();
const WorldCup = await ethers.getContractFactory("WorldCup");
await expect(WorldCup.deploy(latestTime)).to.be.revertedWith(
"WorldCupLottery: invalid deadline!"
);
});
});
// 玩家下注相关测试
describe("Play", function () {
// 测试奖金池是否正确
it("Should deposit 1 gwei", async function () {
// 调用合约
await worldcupIns.play(Country.CHINA, {
value: ONE_GEWI
})
// 校验
let bal = await worldcupIns.getVaultBalance()
console.log("bal:", bal);
console.log("bal.toString():", bal.toString());
expect(bal).to.equal(ONE_GEWI)
})
// 测试传入非法下注值
it("Should faild with invalid eth", async function () {
await expect(worldcupIns.play(Country.CHINA, {
value: ONE_GEWI * 2
})).to.revertedWith("invalid funds provided")
})
// 至少选择一个正确的球队
it("Should have 1 player for selected country", async function () {
await expect(worldcupIns.play(10, {
value: ONE_GEWI
})).to.revertedWithoutReason()
})
// 测试是否发出事件
it("Should emit Event Play", async function () {
await expect(worldcupIns.play(Country.BRAZIL, {
value:ONE_GEWI
})).to.emit(worldcupIns, "Play").withArgs(0, ownerAddr, Country.BRAZIL)
})
})
// 测试开奖过程
describe("Finalize", function () {
// 测试开奖人权限
it("Should failed when called by other account", async function () {
let otherAccount = await ethers.getSigner(otherAccountAddr)
await expect(worldcupIns.connect(otherAccount).finialize(Country.BRAZIL)).to.revertedWith("not authorized!")
})
// 测试奖金分配
it("Should distribute with correct reward", async function () {
const [A, B, C, D] = await ethers.getSigners();
// 玩家下注
await preparePlay()
// 调用 finalize
await worldcupIns.finialize(Country.GERMANY)
let rewardForA = await worldcupIns.winnerVaults(A.address)
let rewardForB = await worldcupIns.winnerVaults(B.address)
let rewardForC = await worldcupIns.winnerVaults(C.address)
let rewardForD = await worldcupIns.winnerVaults(D.address)
expect(rewardForA).to.equal(ethers.BigNumber.from(1333333334))
expect(rewardForB).to.equal(ethers.BigNumber.from(1333333333))
expect(rewardForC).to.equal(ethers.BigNumber.from(1333333333))
expect(rewardForD).to.equal(ethers.BigNumber.from(0))
})
// 测试是否发出事件
it("Should emit Finalize Event", async function () {
const [A, B, C, D] = await ethers.getSigners();
await preparePlay()
let winners = [A.address, B.address, C.address]
// 这里的事件入参故意设置成 4 个 应该是 2 个
await expect(worldcupIns.finialize(Country.GERMANY)).to.
emit(worldcupIns, "Finialize").withArgs(0, winners, 4 * ONE_GEWI, 1)
})
})
// 测试领奖相关
describe("ClaimReward", function () {
// 测试领奖者是否有兑换资格
it("Should fail if the claimer has no reward", async function () {
await expect(worldcupIns.claimReward()).to.revertedWith("nothing to claim!")
})
// 玩家领完奖金后 合约奖金池应对应减少
it("Should clear reward after claim", async function () {
const [A, B, C, D] = await ethers.getSigners();
await preparePlay()
// A B C 中奖了
await worldcupIns.finialize(Country.GERMANY)
// B 地址余额
let balBefore_B = await ethers.provider.getBalance(B.address)
// 奖金池
let balBefore_WC = await worldcupIns.getVaultBalance()
// 待兑现奖金
let balBefore_lockedAmts = await worldcupIns.lockedAmts()
console.log("balBefore_A: ", balBefore_B.toString());
console.log("balBefore_WC: ", balBefore_WC.toString())
console.log("balBefore_lockedAmts: ", balBefore_lockedAmts.toString())
// B 领奖
let rewardForB = await worldcupIns.winnerVaults(B.address)
await worldcupIns.connect(B).claimReward()
// 领完奖后
let balAfter_B = await ethers.provider.getBalance(B.address)
let balAfter_WC = await worldcupIns.getVaultBalance()
let balAfter_lockedAmts = await worldcupIns.lockedAmts()
console.log("balAfter_B : ", balAfter_B.toString());
console.log("balAfter_WC: ", balAfter_WC.toString())
console.log("balAfter_lockedAmts: ", balAfter_lockedAmts.toString())
// 合约奖金池中金额减少
expect(balBefore_WC.sub(balAfter_WC)).to.equal(rewardForB)
// 待兑现金额减少
expect(balBefore_lockedAmts.sub(balAfter_lockedAmts)).to.equal(rewardForB)
})
})
});
编写完,运行单元测试:npm hardhat test,效果如下
部署到本地网络
编写部署脚本 scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const TWO_WEEKS_IN_SECS = 14 * 24 * 60 * 60;
const timestamp = Math.floor(Date.now() / 1000)
const deadline = timestamp + TWO_WEEKS_IN_SECS;
console.log('deadline:', deadline)
// 获取合约对象
const WorldCup = await ethers.getContractFactory("WorldCup");
// 部署
const worldcup = await WorldCup.deploy(deadline);
// 等待部署完成
await worldcup.deployed();
console.log(`new worldcup deployed to ${worldcup.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;
});
hardhat 内部实现了一个本地 EVM,可以运行一个本地节点,开发过程,我们可以选择启动节点,并在上面部署,具体如下:
# 运行脚本,部署合约
npx hardhat run scripts/deploy.ts
# 启动节点 node
npx hardhat node
#部署合约到本地 node 节点
npx hardhat run scripts/deploy.ts --network localhost
部署成功后,效果如下:
部署到测试网络
首先修改配置文件 hardhat.config.ts,具体如下:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
// 需要先单独安装再引用:npm install dotenv
require('dotenv').config()
let ALCHEMY_KEY = process.env.ALCHEMY_KEY || ''
let INFURA_KEY = process.env.INFURA_KEY || ''
let PRIVATE_KEY = process.env.PRIVATE_KEY || ''
// 用于在 Etherscan 验证合约
let ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || ''
console.log(ALCHEMY_KEY);
console.log(INFURA_KEY);
console.log(PRIVATE_KEY);
console.log(ETHERSCAN_API_KEY);
const config: HardhatUserConfig = {
// solidity: "0.8.9",
// 配置网络 kovan, bsc, mainnet
networks: {
hardhat: {
},
// 配置 goerli 网络
goerli: {
// 注意 url 是 ``,而不是 ''
url : `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_KEY}`,
accounts: [PRIVATE_KEY]
},
kovan: {
url: `https://kovan.infura.io/v3/${INFURA_KEY}`,
accounts: [PRIVATE_KEY]
}
},
// 配置自动化 verify 相关
etherscan: {
apiKey: {
goerli: ETHERSCAN_API_KEY
}
},
// 配置编译器版本
solidity: {
version: "0.8.9",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
};
export default config;
然后在项目根目录下添加 .env 文件,以配置连接用到的 key,先获取 key
// 在 etherscan.io 官网获取
ETHERSCAN_API_KEY=
// 在 Alchemy 官网仪表板获取
ALCHEMY_KEY= "*****"(记住结尾不能加冒号)
INFURA_KEY=
// 测试网钱包私钥
PRIVATE_KEY=
接着部署到 goerli 测试网络(注意将 Worldcup.sol 中 console.sol 相关内容注释掉):
# npx hardhat run scripts/deploy.ts --network <netWorkName>
npx hardhat run scripts/deploy.ts --network goerli
# 执行后得到部署后的合约地址:******
再自动验证合约:
# npx hardhat verify <contractAddr> [para1] [para2] ... --network goerli
npx hardhat verify 0x06515F07F0B9c85Df8c5Cb745e9A24EA2f6e7882 1671691242 --network goerli
验证这一步,如果是国内使用梯子的朋友可能会报错,比如类似于:
根本之一可能是电脑设置的代理只针对浏览器,终端没有设置代理,这个问题我并没有真正解决,虽然我尝试在 hosts 文件中添加了地址映射,解决了连接超时的问题,但最后结果就像上面这样报另外一个错误,不知道如何解决了,如果有解决了的小伙伴可以留言。最后采取的方案是直接在 https://goerli.etherscan.io/ 页面上执行验证,具体验证过程可以参考另一篇文章:如何在 goerli.etherscan.io 上验证合约