世界盃競猜專案Dapp-第二章(hardhat部署合約)
阿新 • • 發佈:2022-12-08
建立 hardhat 專案
# 建立 npm 空專案
npm init
# 安裝
npm install --save-dev [email protected]
# 建立工程
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 上驗證合約