1. 程式人生 > 其它 >世界盃競猜專案Dapp-第二章(hardhat部署合約)

世界盃競猜專案Dapp-第二章(hardhat部署合約)

建立 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 上驗證合約