The VRFCoordinatorV2Mock has no logic inside so you have to implement it yourself. You also need to create the subscription, fund it with enough LINK and register your contract as a consumer.
NOTE: In the following code, you should replace MyContract by your contract, the method generate by your method that requests the random numbers and Generated by the events raised once the random numbers are handled.
NOTE: The following code uses features from TypeChain and hardhat-deploy.
First you need to deploy both the LinkToken and the VRFCoordinatorV2Mock contracts but only if deploying to localhost. I use the following 000_Mocks.ts script:
import {DeployFunction} from 'hardhat-deploy/types'; const POINT_ONE_LINK = '100000000000000000'; const func: DeployFunction = async ({ deployments, getNamedAccounts, getChainId, }) => { const {deploy, log} = deployments; const {deployer} = await getNamedAccounts(); const chainId = await getChainId(); if (chainId == '1337') { log('Local network detected! Deploying mocks...'); log('Deploying LinkToken...'); await deploy('LinkToken', { from: deployer, log: true, autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks }); log('Deploying VRFCoordinatorV2Mock...'); await deploy('VRFCoordinatorV2Mock', { from: deployer, log: true, args: [ POINT_ONE_LINK, 1e9, // 0.000000001 LINK per gas ], autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks }); } }; export default func; func.tags = ['all', 'mocks'];
I pass all the required data as constructor parameters to my contract. One of the parameters is the subscriptionId. The script 010_MyContract.ts, creates the subscription and deposits 10 LINK before deploying the contract. Once the contract is deployed, the contract is added as a consumer to the subscription.
import {ethers, network} from 'hardhat'; import {DeployFunction} from 'hardhat-deploy/types'; import { ChainlinkVrfPropsStruct, } from '../build/typechain/src/MyContract'; import { networkConfig, developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS, } from '../helper-hardhat-config'; import {VRFCoordinatorV2Mock__factory} from '../build/typechain/factories/@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock__factory'; const TEN_LINK = '10000000000000000000'; const func: DeployFunction = async ({ deployments, getNamedAccounts, getChainId, }) => { const {deploy, get, log} = deployments; const {deployer} = await getNamedAccounts(); const chainId = await getChainId(); const config = networkConfig[chainId]; let chainlink: ChainlinkVrfPropsStruct = config.chainlink; let waitBlockConfirmations = developmentChains.includes(network.name) ? 1 : VERIFICATION_BLOCK_CONFIRMATIONS; if (chainId == '1337') { const linkTokenDeployment = await get('LinkToken'); const VRFCoordinatorV2MockDeployment = await get( 'VRFCoordinatorV2Mock', ); const [signer] = await ethers.getSigners(); const VRFCoordinatorV2Mock = VRFCoordinatorV2Mock__factory.connect( VRFCoordinatorV2MockDeployment.address, signer, ); // create VRF subscription const subscriptionCreationReceipt = await ( await VRFCoordinatorV2Mock.createSubscription() ).wait(); const subscriptionId = subscriptionCreationReceipt.events![0].topics[1]; chainlink = { ...chainlink, keyHash: ethers.constants.HashZero, subscriptionId: subscriptionId, vrfCoordinatorContract: VRFCoordinatorV2MockDeployment.address ?? ethers.constants.AddressZero, linkTokenContract: linkTokenDeployment.address ?? ethers.constants.AddressZero, }; waitBlockConfirmations = 0; } const args: any[] = [ chainlink, ]; /** * Validate Fields */ if (!chainlink) throw new Error('Missing chainlink'); if (chainlink.subscriptionId === 0) throw new Error('Missing chainlink subscriptionId'); if (!chainlink.linkTokenContract) throw new Error('Missing chainlink linkTokenContract'); if (!chainlink.keyHash) throw new Error('Missing chainlink keyHash'); if (!chainlink.vrfCoordinatorContract) throw new Error('Missing chainlink vrfCoordinatorContract'); if (!chainlink.minimumRequestConfirmations) throw new Error('Missing chainlink minimumRequestConfirmations'); log('Deploying MyContract...'); const result = await deploy('MyContract', { from: deployer, args: args, log: true, waitConfirmations: waitBlockConfirmations, autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks //deterministicDeployment: true, gasLimit: 8_000_000, }); if (chainId == '1337') { const VRFCoordinatorV2MockDeployment = await get( 'VRFCoordinatorV2Mock', ); const [signer] = await ethers.getSigners(); const VRFCoordinatorV2Mock = VRFCoordinatorV2Mock__factory.connect( VRFCoordinatorV2MockDeployment.address, signer, ); // add consumer await ( await VRFCoordinatorV2Mock.addConsumer( chainlink.subscriptionId, result.address, ) ).wait(); // fund the VRF subscription await ( await VRFCoordinatorV2Mock.fundSubscription( chainlink.subscriptionId, TEN_LINK, ) ).wait(); } }; export default func; func.tags = ['all', 'MyContract'];
This deployment script gets the configurations from a helper-hardhat-config.ts file:
import {ethers} from 'hardhat'; const networkConfig: any = { default: { name: 'hardhat', }, 1337: { name: 'localhost', chainlink: { keyHash: '0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc', minimumRequestConfirmations: 3, }, }, 5: { name: 'goerli', // https://docs.chain.link/docs/vrf-contracts/#configurations chainlink: { subscriptionId: 6918, vrfCoordinatorContract: '0x2ca8e0c643bde4c2e08ab1fa0da3401adad7734d', linkTokenContract: '0x326c977e6efc84e512bb9c30f76e30c160ed06fb', keyHash: '0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15', minimumRequestConfirmations: 3, }, }, 1: { // https://docs.chain.link/docs/vrf-contracts/#configurations chainlink: { subscriptionId: 1696, vrfCoordinatorContract: '0x271682DEB8C4E0901D1a1550aD2e64D568E69909', linkTokenContract: '0x514910771af9ca656af840dff83e8264ecf986ca', keyHash: '0x8af398995b04c28e9951adb9721ef74c74f93e6a478f39e7e0777be13527e7ef', minimumRequestConfirmations: 5, }, }, }; const developmentChains = ['hardhat', 'localhost']; const VERIFICATION_BLOCK_CONFIRMATIONS = 6; export {networkConfig, developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS};
Now that all the required contracts have been deployed, lets setup the units tests.
import {expect} from 'chai'; import {MyAccessControl, MyContract} from '../build/typechain'; import {VRFCoordinatorV2Mock__factory} from '../build/typechain/factories/@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock__factory'; import {MyAccessControl__factory} from '../build/typechain/factories/src/MyAccessControl__factory'; import {MyPaymentSplitter__factory} from '../build/typechain/factories/src/MyPaymentSplitter__factory'; import {MyContract__factory} from '../build/typechain/factories/src/MyContract__factory'; import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers'; const {deployments} = require('hardhat'); ///////////////////////////////////////////////////////////////////////////////////// // Fixtures const userData = ( signer: SignerWithAddress, myContract: MyContract, ) => { return { signer: signer, myContract: myContract.connect(signer), }; }; // this fixture deploys the VRFCoordinatorV2Mock and MyContract contracts const fixtureDeployed = deployments.createFixture( async ({deployments, ethers}) => { const {get} = deployments; await deployments.fixture(['all']); const vrfCoordinatorV2MockDeployment = await get( 'VRFCoordinatorV2Mock', ); const myContractDeployment = await get('MyContract'); const [deployer, user] = await ethers.getSigners(); const vrfCoordinatorV2Mock = VRFCoordinatorV2Mock__factory.connect( vrfCoordinatorV2MockDeployment.address, deployer, ); const myContract = MyContract__factory.connect( myContractDeployment.address, deployer, ); return { vrfCoordinatorV2Mock, myContract, deployer: userData(deployer, myContract), user: userData(user, myContract), }; }, ); // this fixture calls the generate() method and waits for the Generated event to be emitted const fixtureGenerated = deployments.createFixture(async () => { // wait for the contract to be deployed const {myContract, vrfCoordinatorV2Mock, ...fixture} = await fixtureDeployed(); // the random number is generated asynchronouly so // this promise registers to the events, calls generate() and // resolves once flow ends const waitGenerated = () => new Promise<void>(resolve => { // declare event listeners const onRandomWordsRequested: TypedListener< RandomWordsRequestedEvent > = ( _keyHash: string, requestId: BigNumber, _preSeed: BigNumber, _subId: BigNumber, _minimumRequestConfirmations: number, _callbackGasLimit: number, _numWords: number, sender: string, ) => { // unregister this event listener vrfCoordinatorV2Mock.removeListener( vrfCoordinatorV2Mock.filters.RandomWordsRequested(), onRandomWordsRequested, ); // simulate callback from the oracle network vrfCoordinatorV2Mock.fulfillRandomWords(requestId, sender); }; const onGenerated: TypedListener<GeneratedEvent> = ( _sender: string, _value: BigNumber, ) => { // unregister this event listener myContract.removeListener( myContract.filters.Generated(), onGenerated, ); // Generated event has been emitted resolve(); }; // register event listeners vrfCoordinatorV2Mock.on( vrfCoordinatorV2Mock.filters.RandomWordsRequested(), onRandomWordsRequested, ); myContract.on( myContract.filters.Generated(), onGenerated); // call generate() fixture.deployer.myContract.generate(); }); // call generate() and wait for Generated event to be emitted await waitGenerated(); return { ...fixture, vrfCoordinatorV2Mock, myContract, }; }); ///////////////////////////////////////////////////////////////////////////////////// // Tests describe('MyContract tests', () => { describe('Reveal transactions', () => { it('Should successfully generate a random number', async () => { const {deployer} = await fixtureGenerated(); // TODO: assert that the value generated has been correctly handled }).timeout(60_000); }); });
The test uses a fixture defined in the method fixtureGenerated. This fixture simply awaits for the method waitGenerated that emulates the controller behaviour missing in the VRFCoordinatorV2Mock implementation.
The method uses a Promise with a lambda expression which registers handlers for the the event RandomWordsRequested emitted by the VRFCoordinatorV2Mock instance and the event Generated by the MyContract instance. It then calls the generate method from the MyContract instance which calls the requestRandomWords. This raises the RandomWordsRequested from the VRFCoordinatorV2Mock instance.
The onRandomWordsRequested handler calls the fulfillRandomWords method from the VRFCoordinatorV2Mock instance which then calls the method with the same name in all the registered consumers.
The method fulfillRandomWords in the MyContract instance emits a Generated event which is then handled by the onGenerated method. This handler then resolves the Promise and the unit test can continue its flow.
NOTE: To make TypeChain generate the typescript wrappers for VRFCoordinatorV2Mock and LinkToken just add the following files to your contracts folder:
LinkToken.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.4.24; import "@chainlink/token/contracts/v0.4/LinkToken.sol";
VRFCoordinatorV2Mock.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@chainlink/contracts/src/v0.8/mocks/VRFCoordinatorV2Mock.sol";
It's intricate but I hope this helps.