区块链全栈以太坊(十)项目实战之彩票

[TOC]

一、简介

彩票项目,用到了随机数 :chainlink VRF,定时器: Chainlink Automation。

(一)源码lottery

hardhat-smartcontract-lottery-fcc

(二)快速开始

安装依赖

#注意node版本,管理员身份打开cmd
nvm install 18.16.0
nvm use 18.16.0
nvm list
# yarn
yarn
# npm
#npm install

安装hardhat-shorthand

hardhat命令缩写, 如:

hh compile 代表 yarn hardhat compile

npm i -g hardhat-shorthand
# 或者
yarn global add hardhat-shorthand

二、源码分析Raffle.sol

(一)购买彩票逻辑

付钱入场
选一个随机数(chainlink VRF)
每几分钟选出一个获奖者(chainlink Automation)

(二)付钱入场


function enterRaffle() public payable {
    // require(msg.value >= i_entranceFee, "Not enough value sent");
    // require(s_raffleState == RaffleState.OPEN, "Raffle is not open");
    if (msg.value < i_entranceFee) {
        revert Raffle__SendMoreToEnterRaffle();
    }
    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
    // Named events with the function name reversed
    emit RaffleEnter(msg.sender);
    }

1) Logging

Evm中的功能。

1.当区块链上发生某些事情Events的时候,Evm 会把这些事情写入到一种名为Logs 的特殊数据结构中。

2.我们可以从我们运行的区块链节点中阅读这些Logs。
(运行、连接到一个节点,可以调用eth_getLogs来获取Logs)

3.Logging中有一个很重要的部分,就是Events
Events可以让你将信息保存到Logging结构中。
(这种方式比保存到storage 变量 更节省gas

4.events和 logs存在于一种 智能合约无法访问的特殊数据结构中。
(这就是节省gas的原因)

2) Events

当我们更新动态对象,例如 数组或者映射时,总是希望能触发Event。
https://docs.soliditylang.org/en/v0.8.24/contracts.html#events
https://learnblockchain.cn/docs/solidity/structure-of-a-contract.html#event

每一个 Event 都与在交易中发出该 Event 的智能合约或账户地址相关联。The Graph会监听并存储这些Event,以便以后可以查询。

js中可以对event进行监听。

 /* Events */
event RaffleEnter(address indexed player);

1. Topics

即:indexed参数,是一种更容易搜索的参数。

eth_getlogs函数,有一个参数可以让我们直接搜索特定的Topic
相对于非 indexed参数而言,搜索性更强。

  1. indexed 参数,
    最多可以有三个 。
  2. 非index 参数搜索困难。
    因为被ABI编码过(需要ABI才能解码)。

2. emit

触发事件,和调用函数非常类似。

emit RaffleEnter(msg.sender);

3.查看日志

etherscan搜索当前合约的地址,在Event tab中能看到事件日志列表。
日志数据结构如下:

event_1.png

因为这个合约,已经验证过代码、验证过了合约,所以etherscan知道它的ABI 是什么
所以,我们可以使用 Dec(解码模式)来查看log。

(三)随机数决定中奖

使用 Chainlink VSF 来进行随机数的获取。

(三) 随机开奖-chainlinkVSF

1)为什么要使用VSF

如果用区块链上的随机数,可能会被矿工攻击,因为矿工可以放弃区块,来多次重试以获取对自己最有利的随机数。

VSF Version2有些不同:
https://docs.chain.link/vrf

与Version1 最大的区别
不再需要通过Link向你的合约注入资金,而是新建一个订阅,往里存钱(link token), 每次调用VSF都会扣掉费用。
它基本上就是一个允许你为多个用户合约提供资金和维持余额的账户。

2)创建Subscriptions

我们需要…个用于请求随机数并支付“Oracle gas(“预言机 gas”)所需 Link的订阅ID。

中文说明:
https://zhuanlan.zhihu.com/p/522356922

官网参考
https://docs.chain.link/vrf/v2/subscription/examples/get-a-random-number

  1. 确保MetaMask连上测试网Sepolia,并且有eth 和 link的余额。

  2. https://vrf.chain.link/ 点击 connect ,连接钱包(account1)。

    基本上,这就是你放置资金的地方,以便你能够进行跨链使用。

  3. Create Subscription

    Admin adress: account1

    现在我们有了一个订阅,也就是用来注入资金的账户。

    接着就可以i用这个账户来进行我们所有的随机数请求。

  4. fund a Subscriptions

vsf_1.png

vsf_2.png
vsf_3.png
vsf_4.png
  1. add Consumer address( 合约地址)。

    创建一个合约,用来请求随机数。

    https://docs.chain.link/vrf/v2/subscription/examples/get-a-random-number#create-and-deploy-a-vrf-v2-compatible-contract

    根据上面的文档 用 remix 部署 样本合约(Subscription ID 要改)。

    合约地址填写到 该Subscription里。

    到这里,已经授权用户合约可以发起随机数请求了。

3)导入

import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
#添加依赖
yarn add --dev @chainlink/contracts

4)构造函数说明

 /* Functions */
constructor(
    //这个"vrfCoordinator"是…个负责随机数验证的合约的地址
    address vrfCoordinatorV2,
    //
    uint64 subscriptionId,
    //gas通道 ,能接受的以 wel 为单位支付的 “最高 gas 价格”。
    bytes32 gasLane, // keyHash
    uint256 interval,
    uint256 entranceFee,
    uint32 callbackGasLimit
)

1.vrfCoordinator

一个负责随机数验证的合约的地址

2.gasLane (keyHash)

https://docs.chain.link/vrf/v2/subscription/supported-networks#ethereum-mainnet
gas通道,即 keyHash
它告诉 Chainlink 节点你所能接受的以 wel 为单位支付的最高 gas 价格

如果gas价格飙升,获取随机数将花费你大量的资金,通过设定上限,我们
确保随机数不会返回

主网有多个通道可以选,测试网一般只有一个通道。

3.subscriptionId

合约用于支付随机数请求费用的订阅id。

实际上,他是一个链上合约,
我们可以使用它来为外部数据和外部计算的订阅提供资金

4.callbackGasLimit

'callbackGasLimit”是指回调函数""fulfillRandomWords" 能使用的 gas上限。

如果gas非常昂贵,他就会锁定随机数的响应,所以需要设置上限。

5)请求随机数。

requestId = vrfCoordinator.requestRandomWords()

6)获得随机数(回调).

//固定
fulfillRandomWords(uint256, /* requestId */ uint256[] memory randomWords )internal override

分两个交易比一个交易好得多。
因为:如果只有一个交易,就可以通过暴力尝试模拟调用这个交易。

(四) 定时开奖 -chainlinkAutomation

1)counter 定时任务Demo学习

计时器demo官方文档https://docs.chain.link/chainlink-automation/guides/compatible-contracts#vyper-example)

a.源码分析

checkUpkeep这实际上是一个不在链上运行的方法,

他是由一个 chainlink keepers/automations 网络节点 离链运行的。
并且很好的是,这里用到的 gas 实际上并不是链上的 gas。

如果你的"checkUpkeep"方法返回了"upkeepNeeded",它就会在链上运行
performUpkeep()

所以你可以在链外生成数据,然后传进来,称为"checkData。

然后这些数据将成为"performData"并传递给"performUpkeep。

performUpkeep"方法是验证正确性的地方,
你需要确认这些东西是否真的应该在链上进行修改和运行。

b.注册合约

  1. 打开 chainlink automation网页: https://automation.chain.link/

  2. register new upkeep(连接钱包),这个表单新版改动较大。

    1. 填写基本信息。 gas limit 200000, 预先存入 link
  3. upkeep address :粘贴counter合约地址。

    1. 从remix 或者 vscode复制 abi信息填入, 选择abi里的方法填入data。
  4. 等待钱包MetaMask确认。

c.查看任务详情

timer_1.png

timer_2.png
timer_3.png

2) 定时开奖实现

// 定时任务的业务执行条件是否具备,reture true则执行 定时任务performUpkeep。 
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);
        return (upkeepNeeded, "0x0"); // can we comment this out?
    }

    /**
     * @dev Once `checkUpkeep` is returning `true`, this function is called
     * and it kicks off a Chainlink VRF call to get a random winner.
     */
//performUpkeep 里再 手工调用一下 checkUpkeep。
    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));
        }
        s_raffleState = RaffleState.CALCULATING;
        uint256 requestId = i_vrfCoordinator.requestRandomWords(
            i_gasLane, i_subscriptionId, REQUEST_CONFIRMATIONS, i_callbackGasLimit, NUM_WORDS
        );
        // Quiz... is this redundant?
        emit RequestedRaffleWinner(requestId);
    }

三、部署

(一)编译

hh compile
yarn hardhat compile

(二)本地部署

mock

vrfCoordinator 合约mock,以及创建 mock 订阅,mock不需要link币。

// create VRFV2 Subscription
vrfCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock")
vrfCoordinatorV2Address = vrfCoordinatorV2Mock.address
const transactionResponse = await vrfCoordinatorV2Mock.createSubscription()
 const transactionReceipt = await transactionResponse.wait()
subscriptionId = transactionReceipt.events[0].args.subId
// Fund the subscription
// Our mock makes it so we don't actually have to worry about sending fund
await vrfCoordinatorV2Mock.fundSubscription(subscriptionId, FUND_AMOUNT)

yarn hardhat deploy

四、chainlink

注意,pricefeed的调用不需要费用,因为有很多公司自主。

随机数、定时任务的调用时需要支付 link币的。
随机数:创建Subscriptions
定时任务:register new upkeep

五、hardhat单测

evm_increaseTime

可以让我们增加区块链的时间。

evm_mine

可以让我们挖矿,或者说创建一个新的区块。

callStatic

hardhat中,调用合约public函数时,会认为是要发送一笔交易 了(public view不会)。
如下例子中的情况,实际并不用发送交易,而是要 看下函数返回值 是什么。

就可以用callStatic来模拟发送交易,但是又能直接拿到函数返回值。

it("returns false if people haven't sent any ETH", async () => {
await network.provider.send("evm_increaseTime", [interval.toNumber() + 1])
await network.provider.request({ method: "evm_mine", params: [] })
const { upkeepNeeded } = await raffle.callStatic.checkUpkeep("0x") // upkeepNeeded = (timePassed && isOpen && hasBalance && hasPlayers)
assert(!upkeepNeeded)
})

txReceipt.events[1].

https://stackoverflow.com/questions/73230175/does-etherjs-transactionreceipt-have-an-events-object

六、hardhat集成测试

监听chainlink VSF

单元测试 时,对 vsf进行mock模拟调用,
集成测试可不行 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容