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!