React前端调用Chainlink VRF获取随机数返回undefined问题
问题根因
- Chainlink VRF采用异步两阶段交互模型,不存在同步返回随机数的可能:调用
getRandomNumber()仅完成第一阶段——向Chainlink预言机网络提交随机数申请,交易上链后预言机节点需要1~2个区块的时间生成随机数并提交零知识证明,之后才会调用合约的fulfillRandomness回调函数把最终随机数写入合约存储。原有代码在发起请求后立刻读取结果,此时回调尚未执行,randomResult还未被赋值,自然返回undefined。 - 前端读取合约状态的写法错误:Solidity中
public修饰的状态变量会自动生成同名的view类型getter函数,在ethers.js等前端库中必须以函数调用形式(加括号)才能拉取链上值,原有代码写await gameContract.randomResult仅访问了合约实例的静态属性,不会触发链上查询,本身就拿不到正确值。 - 合约手续费配置存在逻辑漏洞:VRF服务手续费以LINK代币支付,不从合约原生币余额扣除,原有合约仅实现了原生币充值函数
contractFund,若部署后未向合约地址转入至少0.1个对应网络的LINK代币,随机数请求会直接失败,永远不会触发结果回调。
修复方案
1. 合约端调整
新增事件方便前端监听回调结果,避免低效轮询,补充请求ID和结果的映射,避免多请求并发时结果互相覆盖:
pragma solidity ^0.8.0; import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; contract game is VRFConsumerBase{ // 存储每个请求ID对应的随机数结果,避免并发请求覆盖值 mapping(bytes32 => uint256) public requestIdToResult; bytes32 internal keyHash; uint internal fee; // 新增随机数就绪事件,供前端监听 event RandomNumberReady(bytes32 indexed requestId, uint256 diceResult); constructor() VRFConsumerBase( 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // Goerli测试网VRF协调器地址 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // Goerli测试网LINK代币地址 ) { keyHash=0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311; fee=0.1 *10**18; } function getRandomNumber() public returns(bytes32 requestId){ requestId = requestRandomness(keyHash,fee); return requestId; } function fulfillRandomness(bytes32 requestId,uint randomness) internal override{ uint256 diceResult = randomness%7; requestIdToResult[requestId] = diceResult; emit RandomNumberReady(requestId, diceResult); } function contractBalance() public view returns(uint){ return address(this).balance; } // 内置receive函数即可接收原生币,无需额外写空的充值函数 receive() external payable {} }
合约部署完成后,必须向合约地址转入至少0.1个对应网络的LINK代币,否则VRF请求会被节点拒绝。
2. 前端逻辑修正
移除发起请求后立刻读值的逻辑,改为等待交易上链后监听事件获取结果,修正合约状态调用写法:
async BelowSeven(amount){ const gameContract = this.state.gameContract; // 发起随机数请求,等待交易上链 const tx = await gameContract.getRandomNumber(); window.alert("Rolling Dice, waiting for VRF response..."); const txReceipt = await tx.wait(); // 从交易日志里拿到本次请求的requestId const requestId = txReceipt.events[0].args.requestId; // 监听对应请求ID的随机数就绪事件 const firstDiceNumber = await new Promise((resolve, reject) => { // 设置60秒超时,避免异常情况无限等待 const timer = setTimeout(() => reject(new Error("VRF request timeout")), 60000); gameContract.on("RandomNumberReady", (emitRequestId, result) => { // 只处理本次发起的请求对应的结果 if(emitRequestId === requestId) { clearTimeout(timer); gameContract.removeAllListeners("RandomNumberReady"); // 将BigNumber类型转为普通JS数字 resolve(result.toNumber()); } }); }); console.log("First dice result:", firstDiceNumber); window.alert(`Roll completed! Dice number: ${firstDiceNumber}`); // 若需要掷第二个骰子,等第一个结果返回后再发起第二次请求即可 }
注意:编译合约时请选择0.8.x版本的Solidity编译器,和导入的Chainlink合约版本匹配,避免编译报错。
内容的提问来源于stack exchange,提问作者Azzor
相关产品推荐
相关产品推荐

