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 Subscription
与 Chainlink Automation
.
Chainlink VRF Subscription
Create VRF Subscription,在与Chainlink
交互时,需要先创建订阅.

-
Fund Subscription
,请求Chainlink
需要消耗代币.交易完成后:
subscriptionId 很重要,后续合约中会用到.
-
网络参数
chainlink-vrf supported-networks
Chainlink Automation
Chainlink Automation,过程个创建VRF Subscription
一样.

- 网络参数
chainlink-automation supported-networks
编写合约
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.js
或Hardhat
的默认功能,而是由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!
})
})
})
})
-
准备
Chainlink VRF
- SubId for chainlink vrf
- 获取网络代币(
Token
),水龙头 - chainlink token for Sepolia
- create vrf subscription
- 查询调用vrf服务的费用
- 获取网络代币(
- Deploy our contract using the SubId
- Register the contract address for Chainlink VRF
- Register the contract address for Chainlink Keepers
- Running staging tests
- SubId for chainlink vrf
-
部署合约到
Sepolia
网络
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
即可,不需要每次解决组件版本依赖问题。