1

I am following this tutorial and I am doing the exact same in terms of deployment, subscription funding, and unit testing.

But the problem is in the last unit test, when I call fulfillRandomWords on the vrfCoordinatorV2Mock contract, I expect it to call the fulfillRandomWords callback function on my contract. For some reason it does not and the unit test times out.

Here is my smart contract:

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

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

error RandomApeYachtClub__NotOwner();
error RandomApeYachtClub__BreedNotFound();
error RandomApeYachtClub__NotEnoughFeeToMint();
error RandomApeYachtClub__NotAbleToWithdraw();

contract RandomApeYachtClub is ERC721URIStorage, VRFConsumerBaseV2 {
  enum ApeBreed {
    CHIMP,
    GORILLA,
    BABOON,
    BLACK_HOWLER
  }

  // VRF helpers
  VRFCoordinatorV2Interface immutable private COORDINATOR;
  address immutable private i_vrfCoordinator;
  bytes32 immutable private i_keyHash;
  uint32 immutable private i_callbackGasLimit;
  uint64 immutable private i_subscriptionId;
  mapping (uint256 => address) s_requestIdToMinter;

  // NFT helper
  uint256 constant private MAX_RARITY = 500;
  uint256 private s_tokenId;
  string[4] private s_tokenURIs;
  mapping (uint256 => string) tokenIdToTokenURI;

  uint256 private i_mintFee;
  address immutable private i_owner;

  // Events
  event NFTRequested(uint256 requestId, address sender);
  event NFTMinted(uint256 requestId, uint256 tokenId, ApeBreed breed, address minter);

  constructor (
    address vrfCoordinator,
    bytes32 keyHash,
    uint32 callbackGasLimit,
    uint64 subscriptionId,
    string[4] memory tokenURIs,
    uint256 mintFee
  )
  ERC721("RandomApeYachtClub", "RAYC")
  VRFConsumerBaseV2(vrfCoordinator)
  {
    i_owner = msg.sender;
    COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
    i_vrfCoordinator = vrfCoordinator;
    i_keyHash = keyHash;
    i_callbackGasLimit = callbackGasLimit;
    i_subscriptionId = subscriptionId;
    s_tokenURIs = tokenURIs;
    i_mintFee = mintFee;
  }
  
  // ETH fee to mint, random nft minted
  function mintNft() payable external returns (uint256 requestId) {
    if(msg.value < i_mintFee)
      revert RandomApeYachtClub__NotEnoughFeeToMint();

    requestId = COORDINATOR.requestRandomWords(
      i_keyHash,
      i_subscriptionId,
      3,
      i_callbackGasLimit,
      1
    );

    s_requestIdToMinter[requestId] = msg.sender;
    emit NFTRequested(requestId, msg.sender);
  }

  // randomness
  function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
    address minter = s_requestIdToMinter[requestId];
    uint256 tokenId = s_tokenId;

    uint256 randomNumber = randomWords[0] % MAX_RARITY;
    ApeBreed apeBreed = getApeFromNumber(randomNumber);
    string memory _tokenURI = s_tokenURIs[uint256(apeBreed)];
    _safeMint(minter, tokenId);
    _setTokenURI(tokenId, _tokenURI);
    tokenIdToTokenURI[tokenId] = _tokenURI;
    s_tokenId++;
    emit NFTMinted(requestId, tokenId, apeBreed, minter);
  }

  function getApeFromNumber(uint256 randomNumber) public pure returns (ApeBreed) {
    uint256[4] memory rarityChance = getRarityChance();
    uint256 accu = 0;
    for (uint i = 0; i < rarityChance.length; i++) {
      if((randomNumber >= rarityChance[i]) && (randomNumber < (rarityChance[i] + accu))) {
        return ApeBreed(i);
      }
      accu += rarityChance[i];
    }
    revert RandomApeYachtClub__BreedNotFound();
  }

  function getRarityChance() internal pure returns (uint256[4] memory) {
    return [10, 30, 120, MAX_RARITY];
  }

  // owner of the contract can withdraw
  function withdraw () external ownerOnly {
    uint256 balance = address(this).balance;

    (bool success, ) = i_owner.call{value: balance}("");
    if(!success)
      revert RandomApeYachtClub__NotAbleToWithdraw();
  }

  // tokenURI to return the NFT's uri
  function tokenURI(uint256 tokenId) public view override returns (string memory) {
    return tokenIdToTokenURI[tokenId];
  }

  modifier ownerOnly () {
    if(i_owner != msg.sender) {
        revert RandomApeYachtClub__NotOwner();
    }
    _;
  }

  function getMaxRarity() public pure returns (uint256) {
    return MAX_RARITY;
  }

  function getTokenURIs() public view returns (string[4] memory) {
    return s_tokenURIs;
  }

  function getMintFee() public view returns (uint256) {
    return i_mintFee;
  }
}

this is the VRF v2 mock:

// SPDX-License-Identidier: MIT
pragma solidity ^0.8.0;

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

this is the deploy script for the mock:

const { network, ethers } = require('hardhat');
const { networkConfig, devChains } = require('../network.config');

const BASE_FEE = ethers.utils.parseEther("0.25");

module.exports = async ({ getNamedAccounts, deployments }) => {
  const currentNetworkConfig = networkConfig[network.config.chainId];
  const isLocal = devChains.includes(currentNetworkConfig.name);
  if(isLocal) {
    const { deploy, log } = deployments;
    const { deployer } = await getNamedAccounts();
    
    await deploy('VRFCoordinatorV2Mock', {
      from: deployer,
      contract: 'VRFCoordinatorV2Mock',
      args: [BASE_FEE, 10000000000000],
      log: true,
    });
    log('Mocks deployed')
    log('-------------------------------------------------');
  }
}

module.exports.tags = ['all', 'mocks'];

this is the deploy script for the randomApeYachtClub contract:

const { network, ethers } = require('hardhat');
const { networkConfig, devChains } = require('../network.config');
const { handleUploadToPinia } = require('../utils/handleUploadToPinata');
const { verify } = require('../utils/verify');

let tokenURIs = [
  'ipfs://QmR27pW2CVvZ8DRqC35vcvLjnfxZpesGJwyFW8ihyR2HQb',
  'ipfs://QmaqQxmqdodEeFigZ89TcNkT3hzsqqDcNaU9w14bNb4c4T',
  'ipfs://QmcEtwoygjJWkFapRLorJX6TJwZbXMLWtDSyFvBF2gzf1Q',
  'ipfs://QmWj9RjQhuSei9XVoMW1EGy9ueZAMD8wbCtWpsqjj5VM9d'
];

const FUND_AMOUNT = ethers.utils.parseEther('10');

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  
  const currentNetwork = networkConfig[network.config.chainId];
  const isLocal = devChains.includes(currentNetwork.name);

  let vrfCoordinator = '';
  let subscriptionId = 0;

  if(process.env.UPLOAD_TO_PINATA === 'true') {
    tokenURIs = await handleUploadToPinia();
  }

  console.log(isLocal);
  if(isLocal) {
    const VRFCoordinatorV2Mock = await ethers.getContract('VRFCoordinatorV2Mock');
    vrfCoordinator = VRFCoordinatorV2Mock.address;
    const tx = await VRFCoordinatorV2Mock.createSubscription();
    const receipt = await tx.wait(1);
    subscriptionId = receipt.events[0].args.subId;
    await VRFCoordinatorV2Mock.fundSubscription(subscriptionId, FUND_AMOUNT);
  } else {
    vrfCoordinator = currentNetwork.vrfCoordinator;
    subscriptionId = currentNetwork.subscriptionId;
  }

  const keyHash = currentNetwork.keyHash;
  const callbackGasLimit = currentNetwork.callbackGasLimit;
  const mintFee = currentNetwork.mintFee;

  const args = [
    vrfCoordinator,
    keyHash,
    callbackGasLimit,
    subscriptionId,
    tokenURIs,
    mintFee,
  ]

  const RandomApeYachtClub = await deploy('RandomApeYachtClub', {
    from: deployer,
    args,
    log: true,
    waitConfirmations: network.config.blockConfirmations || 1,
  });

  if(!isLocal) {
    await verify(RandomApeYachtClub.address, args);
  }
}

module.exports.tags = ['all', 'RAYC'];

this is the unit tests file (the last test is the only one that's not working):

const { expect, assert } = require('chai');
const { ethers, deployments, network, getNamedAccounts } = require('hardhat');
const { devChains, networkConfig } = require('../../network.config');

!devChains.includes(network.name) ?
describe.skip : 
describe('RAYC', function () {
  let randomApeYachtClub, vrfCoordinatorV2Mock, deployer;
  const mintFee = networkConfig[network.config.chainId].mintFee;

  beforeEach(async function() {
    accounts = await ethers.getSigners();
    deployer = accounts[0];
    await deployments.fixture(['mocks', 'RAYC']);

    vrfCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock");
    randomApeYachtClub = await ethers.getContract("RandomApeYachtClub");
  });

  describe('constructor', function () {
    it('tokenURIs is assigned', async function() {
      const tokenURIs = await randomApeYachtClub.getTokenURIs();
      expect(tokenURIs).to.have.lengthOf(4);
    });

    it('mint fee is assigned', async function() {
      const contractMintFee = await randomApeYachtClub.getMintFee();
      assert.equal(ethers.utils.formatEther(contractMintFee), ethers.utils.formatEther(mintFee));
    })
  });

  describe('minNft', function() {
    it('if paid less than mint fee, an error is thrown', async function() {
      const smallFee = ethers.utils.parseEther('0.09');
      await expect(randomApeYachtClub.mintNft({ value: smallFee })).to.be.revertedWith('RandomApeYachtClub__NotEnoughFeeToMint');
    });

    it('NFTRequested event is fired', async function () {
      await expect(randomApeYachtClub.mintNft({ value: mintFee })).to.emit(randomApeYachtClub, 'NFTRequested');
    });
  });

  describe('withdraw', function() {
    it('the amount of the contract is 0 after withdrawing', async function() {
      await randomApeYachtClub.withdraw();
      const provider = ethers.provider;
      const balance = await provider.getBalance(randomApeYachtClub.address);
      expect(balance).to.equal(0);
    });
  });

  describe('fulfillRandomWords', function() {
    it('reverts if no valid requestId is provided', async function () {
      await expect(vrfCoordinatorV2Mock.fulfillRandomWords(0, randomApeYachtClub.address)).to.be.revertedWith('nonexistent request');
    });

    it('emits RandomWordsFulfilled', async function () {
      // mint NFT
      const tx = await randomApeYachtClub.mintNft({ value: mintFee });
      const receipt = await tx.wait(1);
      const requestId = receipt.events.find(e => e.event === 'NFTRequested').args['requestId'];
      // call fulfillRandomWords
      // and watch for the event
      await expect(vrfCoordinatorV2Mock.fulfillRandomWords(requestId, randomApeYachtClub.address))
      .to.emit(vrfCoordinatorV2Mock, 'RandomWordsFulfilled');
    });

    it('the vrf fulfillRandomWords sends the randomWords and emits NFTMinted', async function () {
      await new Promise(async (resolve, reject) => {
        randomApeYachtClub.on('NFTMinted', () => {
          try {
            // This right here is never executed
            console.log('NFTMinted is emitted!');
          } catch (error) {
            console.log(erro);
          }
        });
        try {
          // mint NFT
          const tx = await randomApeYachtClub.mintNft({ value: mintFee });
          const receipt = await tx.wait(1);
          const requestId = receipt.events.find(e => e.event === 'NFTRequested').args['requestId'];
          // call fulfillRandomWords
          // and watch for the event
          await vrfCoordinatorV2Mock.fulfillRandomWords(requestId, randomApeYachtClub.address);
        } catch (error) {
          console.log(error);
        }
      })
    });
  });
});

I am not sure what I'm doing wrong. Please help!

Anas Latique
  • 357
  • 5
  • 13

0 Answers0