0

Hi I want to get a random number using Chainlink VRFConsumerBaseV2 but the fulfillRandomWords() isn't being called after calling VRFCoordinatorV2Interface.requestRandomWords(). I can confirm that as "fulfill" is being logged on the console.

Imp: I'm testing the contracts (including the Chainlink contracts) locally on hardhat. I have added a file test/VRFCoordinatorV2Mock.sol which simply imports the VRFV2 Mock contract:

import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol";

Below is my NFT.sol file:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import '@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol';
import '@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol';

import "hardhat/console.sol";

contract NFT is ERC721, Ownable, VRFConsumerBaseV2 {
    using SafeMath for uint256;

    event RequestSent(uint256 requestId, uint32 numWords);
    event RequestFulfilled(uint256 requestId, uint256[] randomWords);

    struct NFTData {
        uint256 mintedOn;
        uint256 initialPower;
    }

    struct NFTInfo {
        uint256 cardId;
        address userAddress;
        uint256 initialPower;
    }

    VRFCoordinatorV2Interface public coordinator;
    uint64 private _subscriptionId;
    bytes32 public keyHash;
    uint32 public callbackGasLimit = 100000;
    uint16 requestConfirmations = 3;
    uint32 numWords = 1;

    mapping(uint256 => NFTInfo) private _requestIdToNFTInfo;
    mapping(uint256 => NFTData) private _nfts;

    constructor(string memory _name, string memory _symbol, address _coordinator, uint64 subscriptionId, bytes32 _keyHash)
        ERC721(_name, _symbol)
        VRFConsumerBaseV2(_coordinator)
    {
        coordinator = VRFCoordinatorV2Interface(_coordinator);
        _subscriptionId = subscriptionId;
        keyHash = _keyHash;
    }

    function mintNFT(uint256 cardId, address userAddress, uint256 _initialPower) external onlyOwner returns(uint256) {
        console.log("requestRandom: ");
        uint256 requestId = coordinator.requestRandomWords(
            keyHash,
            _subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
        _requestIdToNFTInfo[requestId] = NFTInfo(cardId, userAddress, _initialPower);
        emit RequestSent(requestId, numWords);
        console.log("Emit");
        return requestId;
    }

    function fulfillRandomWords(uint256 _requestId, uint256[] memory _randomWords) internal override {
        console.log("fulfill");
        NFTInfo memory requestNFTInfo = _requestIdToNFTInfo[_requestId];
        require(requestNFTInfo.userAddress != address(0), "Request not found");

        _safeMint(requestNFTInfo.userAddress, requestNFTInfo.cardId);
        console.log("_safeMint");

        _nfts[requestNFTInfo.cardId] = NFTData(block.timestamp, requestNFTInfo.initialPower);
        emit RequestFulfilled(_requestId, _randomWords);
    }
}

Below is my nft.test.js file:

const { expect } = require("chai");

describe("Contract deployment", () => {
  let nft, vrfCoordinatorV2Mock, hardhatVrfCoordinatorV2Mock, owner;

  before(async () => {
    nftFactory = await ethers.getContractFactory("NFT");
    vrfCoordinatorV2Mock = await ethers.getContractFactory("VRFCoordinatorV2Mock");
  });

  beforeEach(async () => {
    [owner] = await ethers.getSigners();

    hardhatVrfCoordinatorV2Mock = await vrfCoordinatorV2Mock.deploy(0, 0);
    await hardhatVrfCoordinatorV2Mock.deployed();

    await hardhatVrfCoordinatorV2Mock.createSubscription();
    await hardhatVrfCoordinatorV2Mock.fundSubscription(1, 100000);

    nft = await nftFactory.deploy("NFT", "NFT", hardhatVrfCoordinatorV2Mock.address, 1, "0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15");
    await nft.deployed();
  });
  
  describe("NFT", () => {
    it("Mint NFT", async () => {
      await hardhatVrfCoordinatorV2Mock.addConsumer(1, nft.address);
      await nft.mintNFT(1, owner.address, 100);
      // here "fulfill" isn't being logged
    });
  });
});

Is it even possible to test the VRF locally on hardhat or am I missing something? Any help is highly appreciated. Also any suggestions on correctly testing my NFT contract locally with the VRF are also welcome.

1 Answer 1

0

If you are testing on your local (unit test) then you will have to simulate the fulfillRandomWords yourself because you use a mock. However on a tesnet or fork (integration test) the Chainlink VRF will answer. I think the best way is to test with a listener like so:

describe("NFT", () => {
    it("Mint NFT", async () => {

      await new Promise(async (resolve, reject) => {
        nft.once("RequestFulfilled", async () => {
          try {
            //Do your assertions here
          } catch (e) {
            reject(e);
          }
          resolve();
        });

      await hardhatVrfCoordinatorV2Mock.addConsumer(1, nft.address);
      const tx = await nft.mintNFT(1, owner.address, 100);
      const txReceipt = await tx.wait(1);
      await hardhatVrfCoordinatorV2Mock.fulfillRandomWords(
          txReceipt.events[0].args.requestId,
          nft.address
        );
    });
  });

Your test will only be completed once your listener is called. Here it will be call when the event RequestFulfilled is emit.

On a tesnet you will just do:

    describe("NFT", () => {
        it("Mint NFT", async () => {

          await new Promise(async (resolve, reject) => {
            nft.once("RequestFulfilled", async () => {
              try {
                //Do your assertions here
              } catch (e) {
                reject(e);
              }
              resolve();
            });

          const tx = await nft.mintNFT(1, owner.address, 100);
          await tx.wait(1);
        });
      });

And after waiting the answer from Chainlink VRF your event will be emit and your test completed.

Hope my answer can help you.

7
  • Hi since the test hung! I think it's because nft.mintNFT() is after the Promise?
    – BlockDev
    Commented Oct 23, 2022 at 17:51
  • No it should be inside the promise, it's the nft.once which will resolve your promise and complete your test. And this one should be call by the fulfillRandomWords when the event is emit. What happen exactly ? The test is never complete ? Or you get a revert ?
    – jrmunchkin
    Commented Oct 24, 2022 at 4:10
  • It never completed. It hung
    – BlockDev
    Commented Oct 24, 2022 at 18:46
  • Hi I found the issue: it is that txReceipt.events[0].args. is undefined so in fulfillRandomWords() requestId isn't defined which stalls the execution. Note, I'm using your 1st code snippet & testing on hardhat locally, not on a testnet
    – BlockDev
    Commented Oct 24, 2022 at 19:29
  • So maybe try txReceipt.events[1] because an event is also emit inside requestRandomWords function. If you are still not getting anything maybe you can try to log the inside of txReceipt, normally you should be able to access the events datas.
    – jrmunchkin
    Commented Oct 25, 2022 at 3:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.