solidity+chainlink 项目实例

本文档演示 solidity 中使用 chainlink,开发框架为:hardhat

创建项目

  • 创建项目目录
shell 复制代码
$ mkdir hardhat-smartcontract-lottery-fcc
$ cd hardhat-smartcontract-lottery-fcc/
$ code .
  • 初始化项目
shell 复制代码
> yarn init
➤ YN0088: A new stable version of Yarn is available: 4.8.1!
➤ YN0088: Upgrade now by running yarn set version 4.8.1

➤ YN0000: · Yarn 4.6.0
➤ YN0000: ┌ Resolution step
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0000: └ Completed
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed
➤ YN0000: · Done in 0s 126ms
> yarn add --dev hardhat
➤ YN0000: · Yarn 4.6.0
➤ YN0000: ┌ Resolution step
➤ YN0085: │ + hardhat@npm:2.22.19, @ethersproject/abi@npm:5.8.0, @ethersproject/abstract-provider@npm:5.8.0, @ethersproject/abstract-signer@npm:5.8.0, and 314 more.
➤ YN0000: └ Completed in 5s 395ms
➤ YN0000: ┌ Fetch step
➤ YN0013: │ 21 packages were added to the project (+ 5.38 MiB).
➤ YN0000: └ Completed in 1s 721ms
➤ YN0000: ┌ Link step
➤ YN0000: │ ESM support for PnP uses the experimental loader API and is therefore experimental
➤ YN0007: │ keccak@npm:3.0.4 must be built because it never has been before or the last one failed
➤ YN0007: │ secp256k1@npm:4.0.4 must be built because it never has been before or the last one failed
➤ YN0000: └ Completed in 1s 553ms
➤ YN0000: · Done with warnings in 8s 739ms

生成 package.json

json 复制代码
{
  "name": "hardhat-smartcontract-lottery-fcc",
  "packageManager": "yarn@4.6.0",
  "devDependencies": {
    "hardhat": "^2.22.19"
  }
}
  • hardhat 初始化`
shell 复制代码
> yarn hardhat
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

Welcome to Hardhat v2.22.19

√ What do you want to do? · Create an empty hardhat.config.js
Config file created

Give Hardhat a star on Github if you're enjoying it!

     https://github.com/NomicFoundation/hardhat


DEPRECATION WARNING

 Initializing a project with npx hardhat is deprecated and will be removed in the future.
 Please use npx hardhat init instead.

生成 hardhat.config.js

js 复制代码
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.28",
};
  • 安装依赖
shell 复制代码
> yarn add --dev ethers chai hardhat hardhat-contract-sizer hardhat-deploy hardhat-gas-reporter prettier prettier-plugin-solidity solhint dotenv solidity-coverage

安装完成后 package.json 变化:

json 复制代码
{
  "name": "hardhat-smartcontract-lottery-fcc",
  "packageManager": "yarn@4.6.0",
  "devDependencies": {
    "chai": "^5.2.0",
    "dotenv": "^16.4.7",
    "ethers": "6.13.5",
    "hardhat": "^2.22.19",
    "hardhat-contract-sizer": "^2.10.0",
    "hardhat-deploy": "^1.0.1",
    "hardhat-gas-reporter": "^2.2.2",
    "prettier": "^3.5.3",
    "prettier-plugin-solidity": "^1.4.2",
    "solhint": "^5.0.5",
    "solidity-coverage": "^0.8.14"
  },
  "dependencies": {
    "@chainlink/contracts": "^1.3.0"
  }
}

将依赖项添加到 hardhat.config.js

js 复制代码
require('dotenv').config();
require('solidity-coverage');

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.28",
};

@nomiclabs/hardhat-waffle 是基于 Chai + Mocha 构建的 Solidity 测试框架,可以方便继承于 Hardhat,主要用于:

  • 在 Hardhat 环境中编写和运行智能合约测试;
  • 使用 Chai 风格的断言语法编写测试;
  • 支持 Solidity 合约的部署和交互;
  • 提供区块链模拟环境,便于执行测试用例.

合约开发

本合约与 Chainlink 网络交互,获得随机数,自动化完成抽奖.故需要注册 Chainlink VRF SubscriptionChainlink Automation.

Create VRF Subscription,在与Chainlink交互时,需要先创建订阅.

  • Fund Subscription,请求Chainlink需要消耗代币.

    交易完成后:

    subscriptionId 很重要,后续合约中会用到.

  • 网络参数

chainlink-vrf supported-networks

Chainlink Automation,过程个创建VRF Subscription一样.

chainlink-automation-doc

  • 网络参数

chainlink-automation supported-networks

编写合约

chainlink-vrf-docs

github-repo

solidity 复制代码
// Raffle contract that uses Chainlink VRF to get a random winner

// Enter the lottery by sending ETH to the contract
// Pick a random winner(verifiably randomness)
// Winner to be selected every X minutes -> completely automated

// Chainlink Oracle -> Randomness, Automated execution(Chainlink eepers)

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import {VRFConsumerBaseV2Plus} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
// AutomationCompatible.sol imports the functions from both ./AutomationBase.sol and
// ./interfaces/AutomationCompatibleInterface.sol
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol";
// import "hardhat/console.sol";

/**
 * Request testnet LINK and ETH here: https://faucets.chain.link/
 * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/
 */

/* Errors */
    error Raffle_NotEnoughETHEntered();
    error Raffle__TransferFailed();
    error Raffle__SendMoreToEnterRaffle();
    error Raffle__RaffleNotOpen();
    error Raffle__UpkeepNotNeeded(
        uint256 currentBalance,
        uint256 numPlayers,
        uint256 raffleState
    );

/**
 * @title A sample Raffle Contract
 * @author yiyesushu
 * @notice This contract is for creating a sample raffle contract
 * @dev This implements the Chainlink VRF Version 2.5
 */
contract Raffle is VRFConsumerBaseV2Plus, AutomationCompatibleInterface {
    /* Type declarations */
    enum RaffleState {
        OPEN,
        CALCULATING
    } // uint256 0 = OPEN, 1 = CALCULATING

    struct RequestStatus {
        bool fulfilled; // whether the request has been successfully fulfilled
        bool exists; // whether a requestId exists
        uint256[] randomWords;
    }

    /* Events */
    event RaffleEnter(address indexed player); // indexed -> allows us to filter events in the logs
    event RequestedRaffleWinner(uint256 indexed requestId, uint32 numWords);
    event RequestFulfilled(uint256 indexed requestId, uint256[] randomWords);
    event WinnerPicked(address indexed player);

    /* State Variables */
    // Lottery Variables
    uint256 private immutable i_entranceFee;
    address payable[] private s_players; // s -> storage variable
    uint256 private immutable i_interval;
    uint256 private s_lastTimeStamp;
    address private s_recentWinner;
    RaffleState private s_raffleState;
    bool private immutable i_enableNativePayment; // Set to `true` to enable payment in native tokens, or `false` to pay in LINK

    mapping(uint256 => RequestStatus) private s_requests; // requestId --> requestStatus
    // Past request IDs.
    uint256[] private s_requestIds;
    uint256 private s_lastRequestId;

    // Chainlink VRF Variables
    uint32 private constant NUM_WORDS = 1; // Number of random words to request, cannot exceed VRFCoordinatorV2_5.MAX_NUM_WORDS(2^32-1)
    uint16 private constant REQUEST_CONFIRMATIONS = 3; // Number of confirmations to wait before responding to a request
    uint32 private immutable i_callbackGasLimit; // Gas limit for the callback function, cannot exceed VRFCoordinatorV2_5.MAX_GAS_LIMIT(2^32-1), recommended to be 1000000
    bytes32 private immutable i_keyHash; // The gas lane to use, which specifies the maximum gas price to bump to, see https://docs.chain.link/vrf/v2-5/supported-networks
    uint256 private immutable i_subscriptionId; // Your subscription ID. This is the ID of the subscription that you created in the Chainlink VRF UI. You can find it at https://vrf.chain.link/.

    constructor(
        uint256 entranceFee,
        bytes32 keyHash,
        uint32 callbackGasLimit,
        uint256 subscriptionId,
        address vrfCoordinatorV2Plus, // sepolia testnet coordinator: 0x9DdfaCa8183c41ad55329BdeeD9F6A8d53168B1B
        uint256 updateInterval,
        bool enableNativePayment
    ) VRFConsumerBaseV2Plus(vrfCoordinatorV2Plus) {
        i_entranceFee = entranceFee;
        i_keyHash = keyHash; // sepolia testnet keyHash: 0x787d74caea10b2b357790d5b5247c2f63d1d91572a9846f780606e4d953677ae
        i_callbackGasLimit = callbackGasLimit;
        i_subscriptionId = subscriptionId;
        s_raffleState = RaffleState(0); // Equivalent to RaffleState.OPEN
        s_lastTimeStamp = block.timestamp;
        i_interval = updateInterval;
        i_enableNativePayment = enableNativePayment;
    }

    function enterRaffle() public payable {
        // require(msg.value >= i_entranceFee, "Not enough ETH sent!");
        if (msg.value < i_entranceFee) {
            revert Raffle_NotEnoughETHEntered();
        }

        if (s_raffleState != RaffleState.OPEN) {
            revert Raffle__RaffleNotOpen();
        }

        s_players.push(payable(msg.sender));
        // Emit an event when we update a dynamic array or mapping
        // Name events with the function name reversed
        emit RaffleEnter(msg.sender);
    }

    /**
     * @dev This is the function that the Chainlink Keeper nodes call
     * they look for `upkeepNeeded` to return True.
     * the following should be true for this to return true:
     * 1. The time interval has passed between raffle runs.
     * 2. The lottery is open.
     * 3. The contract has ETH.
     * 4. Implicity, your subscription is funded with LINK.
     */
    function checkUpkeep(
        bytes memory /* checkData */
    )
    public
    view
    override
    returns (bool upkeepNeeded, bytes memory performData)
    {
        bool isOpen = RaffleState.OPEN == s_raffleState;
        bool timePassed = ((block.timestamp - s_lastTimeStamp) > i_interval);
        bool hasPlayers = s_players.length > 0;
        bool hasBalance = address(this).balance > 0;
        upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers);

        // We don't use the checkData in this example. The checkData is defined when the Upkeep was registered.
        // performData = checkData;
        performData = "";
    }

    /**
     * @dev Once `checkUpkeep` is returning `true`, this function is called
     * and it kicks off a Chainlink VRF call to get a random winner.
     */
    function performUpkeep(bytes calldata /* performData */) external override {
        (bool upkeepNeeded, ) = checkUpkeep("");
        // require(upkeepNeeded, "Upkeep not needed");
        if (!upkeepNeeded) {
            revert Raffle__UpkeepNotNeeded(
                address(this).balance,
                s_players.length,
                uint256(s_raffleState)
            );
        }
        // We don't use the performData in this example. The performData is generated by the Automation Node's call to your checkUpkeep function
        s_raffleState = RaffleState.CALCULATING;
        // Will revert if subscription is not set and funded.
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
        keyHash: i_keyHash, // gas Lane
        subId: i_subscriptionId,
        requestConfirmations: REQUEST_CONFIRMATIONS,
        callbackGasLimit: i_callbackGasLimit,
        numWords: NUM_WORDS,
        extraArgs: VRFV2PlusClient._argsToBytes(
                VRFV2PlusClient.ExtraArgsV1({
            nativePayment: i_enableNativePayment
            })
            )
        })
        );

        s_requests[requestId] = RequestStatus({
        randomWords: new uint256[](0),
        fulfilled: false,
        exists: true
        });
        s_requestIds.push(requestId);
        s_lastRequestId = requestId;

        // Quiz... is this redundant?
        emit RequestedRaffleWinner(requestId, NUM_WORDS);
    }

    // Assumes the subscription is funded sufficiently.
    // @param enableNativePayment: Set to `true` to enable payment in native tokens, or
    // `false` to pay in LINK
    // function requestRandomWinner(
    //     bool enableNativePayment
    // ) external onlyOwner returns (uint256 requestId) {
    //     // Will revert if subscription is not set and funded.
    //     requestId = s_vrfCoordinator.requestRandomWords(
    //         VRFV2PlusClient.RandomWordsRequest({
    //             keyHash: i_keyHash, // gas Lane
    //             subId: i_subscriptionId,
    //             requestConfirmations: REQUEST_CONFIRMATIONS,
    //             callbackGasLimit: i_callbackGasLimit,
    //             numWords: NUM_WORDS,
    //             extraArgs: VRFV2PlusClient._argsToBytes(
    //                 VRFV2PlusClient.ExtraArgsV1({
    //                     nativePayment: enableNativePayment
    //                 })
    //             )
    //         })
    //     );

    //     s_requests[requestId] = RequestStatus({
    //         randomWords: new uint256[](0),
    //         fulfilled: false,
    //         exists: true
    //     });
    //     s_requestIds.push(requestId);
    //     s_lastRequestId = requestId;

    //     emit RequestedRaffleWinner(requestId, NUM_WORDS);

    //     return requestId;
    // }

    function fulfillRandomWords(
        uint256 _requestId,
        uint256[] calldata _randomWords
    ) internal override {
        // Once we get it, do something with it
        require(s_requests[_requestId].exists, "request not found");
        // RequestStatus memory requestStatus = s_requests[_requestId]; // is a copy
        s_requests[_requestId].fulfilled = true;
        s_requests[_requestId].randomWords = _randomWords;
        emit RequestFulfilled(_requestId, _randomWords);

        // s_players size 10
        // randomNumber 202
        // 202 % 10 ? what's doesn't divide evenly into 202?
        // 20 * 10 = 200
        // 2
        // 202 % 10 = 2
        uint256 indexOfWinner = _randomWords[0] % s_players.length;
        address payable recentWinner = s_players[indexOfWinner];
        s_recentWinner = recentWinner;
        s_players = new address payable[](0);
        s_raffleState = RaffleState.OPEN;
        s_lastTimeStamp = block.timestamp; // current block timestamp as seconds since unix epoch
        (bool success, ) = recentWinner.call{value: address(this).balance}("");
        // require(success, "Transfer failed");
        if (!success) {
            revert Raffle__TransferFailed();
        }
        emit WinnerPicked(recentWinner);
    }

    /* pure / view function */
    function getRaffleState() public view returns (RaffleState) {
        return s_raffleState;
    }

    function getNumWords() public pure returns (uint256) {
        return NUM_WORDS;
    }

    function getRequestConfirmations() public pure returns (uint256) {
        return REQUEST_CONFIRMATIONS;
    }

    function getRecentWinner() public view returns (address) {
        return s_recentWinner;
    }

    function getPlayer(uint256 index) public view returns (address) {
        return s_players[index];
    }

    function getLastTimeStamp() public view returns (uint256) {
        return s_lastTimeStamp;
    }

    function getInterval() public view returns (uint256) {
        return i_interval;
    }

    function getEntranceFee() public view returns (uint256) {
        return i_entranceFee;
    }

    function getEnableNativePayment() public view returns (bool) {
        return i_enableNativePayment;
    }

    function getNumberOfPlayers() public view returns (uint256) {
        return s_players.length;
    }

    function getLastRequestId() public view returns (uint256) {
        return s_lastRequestId;
    }

    function getRequestIds() public view returns (uint256[] memory) {
        return s_requestIds;
    }

    function getRequestStatus(
        uint256 _requestId
    ) external view returns (bool fulfilled, uint256[] memory randomWords) {
        require(s_requests[_requestId].exists, "request not found");
        RequestStatus memory requestStatus = s_requests[_requestId];
        return (requestStatus.fulfilled, requestStatus.randomWords);
    }
}

编译合约

  • 补全缺少的组件
shell 复制代码
yarn add --dev @typechain/hardhat @nomicfoundation/hardhat-ignition @nomicfoundation/hardhat-ignition-ethers @nomicfoundation/hardhat-chai-matchers @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-ethers @nomicfoundation/hardhat-verify

如果出现link错误:

shell 复制代码
➤ YN0007: │ @arbitrum/nitro-contracts@npm:3.0.0 must be built because it never has been before or the last one failed
➤ YN0007: │ @arbitrum/nitro-contracts@npm:1.1.1 must be built because it never has been before or the last one failed
➤ YN0007: │ @arbitrum/nitro-contracts@npm:1.3.0 must be built because it never has been before or the last one failed
➤ YN0009: │ @arbitrum/nitro-contracts@npm:3.0.0 couldn't be built successfully
➤ YN0000: └ Completed in 0s 404ms
➤ YN0000: · Failed with errors in 1s 110ms

请先安装:

shell 复制代码
yarn add @arbitrum/nitro-contracts

这里随着环境变化,可能会有别的错误,策略就是缺啥补啥.

出现以下错误:

shell 复制代码
 > yarn hardhat compile
Error HH19: Your project is an ESM project (you have "type": "module" set in your package.json) but your Hardhat config file uses the .js extension.

Rename the file to use the .cjs to fix this problem.

For more info go to https://hardhat.org/HH19 or run Hardhat with --show-stack-traces

因为 chai@5.x.x 默认是 ESM, 需要将chai降级:

shell 复制代码
yarn remove chai
yarn add --dev chai@4.2.0
  • 编译
shell 复制代码
> yarn hardhat compile
Compiled 3 Solidity files successfully (evm target: paris).

部署合约

安装依赖: Yarn PnP(Plug'n'Play) 模式不允许"幽灵依赖",每个依赖只能使用自己package.json声明的包.

shell 复制代码
 yarn add --dev @ethersproject/hash @ethersproject/abstract-signer @ethersproject/web @ethersproject/bytes

缺啥补啥

配置部署账号 deployer:

js 复制代码
module.exports = {
    namedAccounts: {
        deployer: {
            default: 0, // here this will by default take the first account as deployer
            1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another
        },
        player: {
            default: 1,
        },
    },
}

部署代码

js 复制代码
const { network, ethers } = require("hardhat");
const { parseEther } = require("ethers"); // ethers v6
const { networkConfig, developmentChains } = require("../helper-hardhat-config");
const { verify } = require("../utils/verify");
const { extractEventArg } = require("../utils/extractEventArg");
 
const VRF_SUB_FUND_AMOUNT = parseEther("30"); // 30 LINK

module.exports = async ({ deployments, getNamedAccounts }) => {
    const { deploy, log } = deployments;
    const { deployer } = await getNamedAccounts();
    const chainId = network.config.chainId;

    let vrfCoordinatorV2PlusAddress, subscriptionId;
    if (chainId == 31337) {
        // get the mock contract address
        const deployment = await deployments.get("VRFCoordinatorV2_5Mock");
        vrfCoordinatorV2PlusAddress = deployment.address;
        console.log(`Deployed contract to: ${vrfCoordinatorV2PlusAddress}`);

        // use ethers v6 to get the contract instance
        const vrfCoordinatorV2PlusMockContract = await ethers.getContractAt(
            "VRFCoordinatorV2_5Mock",   
            vrfCoordinatorV2PlusAddress
        );

        // create a subscription
        const transactionResponse = await vrfCoordinatorV2PlusMockContract.createSubscription();
        const transactionReceipt = await transactionResponse.wait();

        // analyze the transaction receipt to get the subscription ID for ethers v6
       subscriptionId = extractEventArg(transactionReceipt, vrfCoordinatorV2PlusMockContract, "SubscriptionCreated", "subId");
        // subscriptionId = transactionReceipt.events[0].args.subId; // ethers v5
        log(`Subscription ID: ${subscriptionId}`);

        // Fund the subscription
        // Usually, you'd need the LINK token on a real network, but on a local network, you can use the mock LINK token
        await vrfCoordinatorV2PlusMockContract.fundSubscription(subscriptionId, VRF_SUB_FUND_AMOUNT);
    } else {
        subscriptionId = networkConfig[chainId]["subscriptionId"];
        vrfCoordinatorV2PlusAddress = networkConfig[chainId]["vrfCoordinatorV2Plus"];
    }
    const entranceFee = networkConfig[chainId]["raffleEntranceFee"];
    const gasLane = networkConfig[chainId]["gasLane"];
    const callbackGasLimit = networkConfig[chainId]["callbackGasLimit"];
    const updateInterval = networkConfig[chainId]["keepersUpdateInterval"];
    const enableNativePayment = false;        
    const args = [
        vrfCoordinatorV2PlusAddress,
        entranceFee,
        gasLane,
        callbackGasLimit,
        subscriptionId,
        updateInterval,
        enableNativePayment,
    ];

    log("----------------------------------------------------")
    log("Deploying Raffle and waiting for confirmations...")
    const raffle = await deploy("Raffle", {
        from: deployer,
        // gasLimit: 4000000,
        args: args,
        log: true,
        waitConformations: network.config.blockConfirmations || 1,
    })
    log(`Raffle deployed at ${raffle.address}`)
    
    // if we are on a live network, we want to verify the contract
    if (
        !developmentChains.includes(network.name) &&
        process.env.ETHERSCAN_API_KEY
    ) {
        await verify(raffle.address, args)
    }

    log("Enter lottery with command:")
    const networkName = network.name == "hardhat" ? "localhost" : network.name
    log(`yarn hardhat run scripts/enterRaffle.js --network ${networkName}`)
    log("----------------------------------------------------")
}

module.exports.tags = ["all", "reffle"] 

封装函数,从事件中获取指定参数的值(for ethers v6):

js 复制代码
// event SubscriptionCreated(uint256 indexed subId, address owner);

const extractEventArg = (receipt, contract, eventName, argName) => {
    const iface = contract.interface;

    for (const log of receipt.logs) {
        try {
            const parsedLog = iface.parseLog(log);
            if (parsedLog.name === eventName) {
                return parsedLog.args[argName];
            }
        } catch (err) {
            // Ignore errors for logs that are not related to the event we are looking for
            // console.log("Error parsing log:", err);
            continue;
        }
    }
    throw new Error(`Event "${eventName}" with argument "${argName}" not found in logs.`);
}

module.exports = { extractEventArg }

部署合约:

shell 复制代码
> yarn hardhat deploy
Nothing to compile
 ·------------------------|--------------------------------|--------------------------------·
 |  Solc version: 0.8.28  ·  Optimizer enabled: false      ·  Runs: 200                     │
 ·························|································|·································
 |  Contract Name         ·  Deployed size (KiB) (change)  ·  Initcode size (KiB) (change)  │
 ·························|································|·································
 |  Raffle                ·                 9.563 (0.000)  ·                11.301 (0.000)  │
 ·------------------------|--------------------------------|--------------------------------·
chainId: 31337
deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
BASE_FEE: 2000000000000000
GAS_PRICE: 40000000000
WEi_PER_UNIT_LINK: 4000000000000000
----------------------------------------------------------
Local network detected! Deploying mocks...
deploying "VRFCoordinatorV2_5Mock" (tx: 0xb0024517029cb28ab94518742ba2cba4b07c068e44810725523ebf919a3a7bca)...: deployed at 0x5FbDB2315678afecb367f032d93F642f64180aa3 with 5364172 gas
Mocks Deployed!
----------------------------------------------------------
You are deploying to a local network, you'll need a local network running to interact
Please run `yarn hardhat console --network localhost` to interact with the deployed smart contracts!
----------------------------------------------------------
Deployed contract to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Subscription ID: 10070964775317200573588976226600768198989740443180866896722457179464021397134
----------------------------------------------------
Deploying Raffle and waiting for confirmations...
deploying "Raffle" (tx: 0xedde824752ef66b0fac25f11a310203d8bc8f498ae740986a9fb0d42b72a141e)...: deployed at 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 with 2256300 gas
Raffle deployed at 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
Enter lottery with command:
yarn hardhat run scripts/enterRaffle.js --network localhost
----------------------------------------------------

测试合约

添加插件

shell 复制代码
yarn add --dev hardhat-deploy-ethers

hardhat.config.js 导入插件:

js 复制代码
require("hardhat-deploy-ethers"); // 这个插件是为了使用 ethers.js 的 deploy 功能,如 ethers.getContract()

getContract方法并非 Ethers.jsHardhat 的默认功能,而是由 hardhat-deploy-ethers 插件提供的扩展功能

Uint Test
js 复制代码
const {assert, expect} = require("chai")
const { network, ethers, deployments, getNamedAccounts } = require("hardhat")
const { developmentChains, networkConfig } = require("../../helper-hardhat-config")
const { simulateCheckUpkeep } = require("../../utils/simulateStaticCall")
const { extractEventArg } = require("../../utils/extractEventArg")

!developmentChains.includes(network.name)
    ? describe.skip
    : describe("Raffle Unit Tests", async function () {
        let raffle, raffleContract, vrfCoordinatorV2PlusMock, raffleEntranceFee, player1, accounts, deployer, interval
        const chainId = network.config.chainId  // Get the chain ID of the current network

        beforeEach(async function () {
            deployer = (await getNamedAccounts()).deployer // Get the deployer account from named accounts
            // Get the signers (accounts) from the Hardhat network
            accounts = await ethers.getSigners()
            // deployer = accounts[0]
            player1 = accounts[1]

            // /* could also use getNamedAccounts()
            //  * getNamedAccounts() 函数(由 hardhat-deploy 插件提供)返回的是命名账户的地址字符串,
            //  * 而不是具有签名权限的签名者对象(Signer),若要使用这些地址执行需要签名的操作,
            //  * 需要将这些地址转换为签名者对象
            //  */
            // const { deployer: deployerAddress, player1: player1Address, player2: player2Address, player3: player3Address } = 
            //     await getNamedAccounts() // 对象解构赋值(Destructuring Assignment)
            // console.log("Deployer Address:", deployerAddress);
            // console.log("Player Address:", player1Address, player2Address, player3Address);
            // deployer = await ethers.getSigner(deployerAddress)
            // player1 = await ethers.getSigner(player1Address)
            // player2 = await ethers.getSigner(player2Address)    
            // player3 = await ethers.getSigner(player3Address)

            await deployments.fixture(["mocks", "raffle"]) // Deploys modules with the tags "mocks" and "raffle"
            raffleContract = await ethers.getContract("Raffle", deployer) // Returns a new connection to the Raffle contract
            raffle = raffleContract.connect(player1) // Connects the player1 account to the Raffle contract
            raffleEntranceFee = await raffle.getEntranceFee() // Gets the entrance fee for the raffle
            interval = await raffle.getInterval()
            // console.log("Raffle Entrance Fee:", raffleEntranceFee.toString())
            vrfCoordinatorV2PlusMock = await ethers.getContract("VRFCoordinatorV2_5Mock", deployer) // Return a new connection to the VRFCoordinatorV2_5Mock contract
        })

        describe("constructor", function () {
            it("initializes the raffle correctly", async function () {
                // Ideally, we'd separate these out so that only 1 assert per "it" block
                // And ideally, we'd make this check everything

                const raffleState = (await raffle.getRaffleState()).toString()

                // Comparisons for Raffle initialization:
                assert.equal(raffleState, "0") // 0 means OPEN
                assert.equal(interval.toString(), networkConfig[chainId]["keepersUpdateInterval"])
            })
        })
    })

对于 getNamedAccounts 方法,需要在 hardhat.config.js 进行配置:

yaml 复制代码
 namedAccounts: {
    deployer: {
        default: 0, // here this will by default take the first account as deployer
        1: 0, // 指定了在链 ID 为 1(即以太坊主网)时使用的账户索引
    },
    player1: {
        default: 1,
    },
    player2: {
        default: 2,
    },
    player3: {
        default: 3,
    },
  }
  • 测试
shell 复制代码
> yarn hardhat test -- --grep "picks a winner"
 ·------------------------|--------------------------------|--------------------------------·
 |  Solc version: 0.8.28  ·  Optimizer enabled: false      ·  Runs: 200                     │
 ·························|································|·································
 |  Contract Name         ·  Deployed size (KiB) (change)  ·  Initcode size (KiB) (change)  │
 ·························|································|·································
 |  Raffle                ·                 9.563 (0.000)  ·                11.301 (0.000)  │
 ·------------------------|--------------------------------|--------------------------------·


  Raffle Unit Tests
    fulfillRandomWords
Deployed mock contract to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Event emitted: SubscriptionCreated Result(2) [
  38147654579573413202907816624892754858907827445622294453690321546784750558234n,
  '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
]
Subscription created with ID: 38147654579573413202907816624892754858907827445622294453690321546784750558234
Setting up the listener...
Starting Balance: 9999989928718348860900
Event emitted: RequestedRaffleWinner Result(2) [ 1n, 1n ]
WinnerPicked event fired!
      ✔ picks a winner, resets the raffle, and sends money (88ms)


  1 passing (902ms)
Staging Test
js 复制代码
const { assert, expect } = require("chai")
const { getNamedAccounts, ethers, network } = require("hardhat")
const { developmentChains } = require("../../helper-hardhat-config")

developmentChains.includes(network.name)
    ? describe.skip
    : describe("Raffle Staging Tests", function () {
        let raffle, raffleEntranceFee, deployer

        beforeEach(async function () {
            deployer = (await getNamedAccounts()).deployer
            raffle = await ethers.getContract("Raffle", deployer)
            raffleEntranceFee = await raffle.getEntranceFee()
        })

        describe("fulfillRandomWords", function () {
            it("works with live Chainlink Keepers and Chainlink VRF, we get a random winner", async function () {
                // enter the raffle
                console.log("Setting up test...")
                const startingTimeStamp = await raffle.getLastTimeStamp()
                const accounts = await ethers.getSigners()
                const provider = ethers.provider

                console.log("Setting up Listener...")
                let winnerStartingBalance
                await new Promise(async (resolve, reject) => {
                    // setup listener before we enter the raffle
                    // Just in case the blockchain moves REALLY fast
                    raffle.once("WinnerPicked", async () => {
                        console.log("WinnerPicked event fired!")
                        try {
                            // add our asserts here
                            const recentWinner = await raffle.getRecentWinner()
                            const raffleState = await raffle.getRaffleState()
                            const winnerEndingBalance = await provider.getBalance(recentWinner)
                            const endingTimeStamp = await raffle.getLastTimeStamp()

                            await expect(raffle.getPlayer(0)).to.be.reverted
                            assert.equal(recentWinner.toString(), accounts[0].address)
                            assert.equal(raffleState, 0)
                            assert.equal(
                                winnerEndingBalance.toString(),
                                (winnerStartingBalance + raffleEntranceFee).toString()
                            )
                            assert(endingTimeStamp > startingTimeStamp)
                            resolve()
                        } catch (error) {
                            console.log(error)
                            reject(error)
                        }
                    })

                    // Then entering the raffle
                    console.log("Entering Raffle...")
                    const tx = await raffle.enterRaffle({ value: raffleEntranceFee })
                    await tx.wait(1)
                    console.log("Ok, time to wait...")

                    // get the starting balance of the winner for ethers v6
                    winnerStartingBalance = await provider.getBalance(accounts[0].address)
                    console.log("Starting Balance:", winnerStartingBalance.toString())

                    // and this code WONT complete until our listener has finished listening!
                })

            })

        })
    })
shell 复制代码
> yarn hardhat deploy --network sepolia
chainId: 11155111
deployer: 0x5e1A6e10aEfDe4E0626876CCE841b9AE5517F4Cb
BASE_FEE: 2000000000000000
GAS_PRICE: 40000000000
WEi_PER_UNIT_LINK: 4000000000000000
----------------------------------------------------------
----------------------------------------------------
Deploying Raffle and waiting for confirmations...
reusing "Raffle" at 0x910F371E32D265B0B3731f86d7C35e8487756EF4
Raffle deployed at 0x910F371E32D265B0B3731f86d7C35e8487756EF4

浏览器查看合约

  • Add Consumer for Chainlink VRF
  • Create Upkeep
  • 与合约交互
js 复制代码
const { ethers } = require("hardhat")

async function enterRaffle() {
    const raffle = await ethers.getContract("Raffle")
    const entranceFee = await raffle.getEntranceFee()
    await raffle.enterRaffle({ value: entranceFee })
    console.log("Entered!")
}

enterRaffle()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error)
        process.exit(1)
    })
  • 执行
shell 复制代码
> yarn hardhat run ./scripts/enter.js --network sepolia
 ·------------------------|--------------------------------|--------------------------------·
 |  Solc version: 0.8.28  ·  Optimizer enabled: false      ·  Runs: 200                     │
 ·························|································|·································
 |  Contract Name         ·  Deployed size (KiB) (change)  ·  Initcode size (KiB) (change)  │
 ·························|································|·································
 |  Raffle                ·                 9.563 (0.000)  ·                11.301 (0.000)  │
 ·------------------------|--------------------------------|--------------------------------·
Entered!

这个脚本用 deployer 链接合约, 调用 enterRaffle 接口, 对于集成测试不是必需的, 可以修改用别的钱包账号链接合约, 这样合约的参与者就不会只是 deployer 了.

  • 执行集成测试
shell 复制代码
> yarn hardhat test --network sepolia
 ·------------------------|--------------------------------|--------------------------------·
 |  Solc version: 0.8.28  ·  Optimizer enabled: false      ·  Runs: 200                     │
 ·························|································|·································
 |  Contract Name         ·  Deployed size (KiB) (change)  ·  Initcode size (KiB) (change)  │
 ·························|································|·································
 |  Raffle                ·                 9.563 (0.000)  ·                11.301 (0.000)  │
 ·------------------------|--------------------------------|--------------------------------·


  Raffle Staging Tests
    fulfillRandomWords
Setting up test...
Setting up Listener...
Entering Raffle...
Ok, time to wait...
Starting Balance: 1947863775687439947
WinnerPicked event fired!
      ✔ works with live Chainlink Keepers and Chainlink VRF, we get a random winner (106056ms)


  1 passing (2m)

可复用项目模板

在创建hardhat项目时, yarn add --dev <> 默认会拉取最新版本(或符合 ^ 规则的版本),每次新建项目都会有潜在的不兼容风险。 为了避免版本不兼容带来的风险,建议 yarn init 后直接在 package.json 中锁死各个组件的版本(可从已有项目中拷贝),一个建议模板:

json 复制代码
{
  "name": "hardhat-template",
  "version": "1.0.0",
  "private": true,
  "type": "commonjs",
  "scripts": {
    "compile": "hardhat compile",
    "deploy": "hardhat deploy",
    "node": "hardhat node",
    "test": "hardhat test"
  },
  "devDependencies": {
    "hardhat": "2.26.3",
    "hardhat-deploy": "1.0.4",
    "@nomicfoundation/hardhat-toolbox": "3.0.0",
    "@nomiclabs/hardhat-ethers": "3.0.0",
    "ethers": "6.9.0",
    "dotenv": "17.2.2"
  }
}

type: "commonjs",这样部署脚本可以直接用 CommonJS 写法

这样,只需执行 yarn install 即可,不需要每次解决组件版本依赖问题。

相关推荐
木西2 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(兑换-合约部分)
web3·智能合约·solidity
.刻舟求剑.3 天前
solidity得高级语法3
区块链·solidity·语法笔记
许强0xq3 天前
Ethernaut Level 1: Fallback - 回退函数权限提升攻击
区块链·solidity·foundry·ethernaut
.刻舟求剑.5 天前
solidity的高阶语法4
区块链·solidity·语法笔记
大白猴5 天前
【GMX v1实战】时序风险结算与资本成本:深度解析 GMX 永续合约的资金费率机制
区块链·智能合约·solidity·永续合约·gmx·资金费率·去中心化交易所
天涯学馆7 天前
在Solidity中实现DAO:从概念到代码的全面剖析
智能合约·solidity·以太坊
大白猴12 天前
【大白话解析】OpenZeppelin 的 ReentrancyGuard 库:以太坊防重入攻击安全工具箱(附源代码)
区块链·智能合约·solidity·以太坊·evm·重入攻击·恶意合约
木西13 天前
React Native DApp 开发全栈实战·从 0 到 1 系列(流动性挖矿-合约部分)
web3·智能合约·solidity
空中湖14 天前
solidity从入门到精通 第七章:高级特性与实战项目
区块链·solidity