First create a file called Runners.sol and paste the following code (make sure to adjust your Remix compiler to refer to the compiler version in the pragma statement below):
// SPDX-License-Identifier: MITpragmasolidity 0.8.19;// Deploy this contract on Fujiimport"@openzeppelin/contracts@4.6.0/token/ERC721/extensions/ERC721URIStorage.sol";import"@openzeppelin/contracts@4.6.0/utils/Counters.sol";import"@openzeppelin/contracts@4.6.0/utils/Base64.sol";import"@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";import"@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";contractRunnersisERC721, ERC721URIStorage, VRFConsumerBaseV2 {usingCountersforCounters.Counter;usingStringsforuint256;// VRFeventRequestSent(uint256 requestId, uint32 numWords);eventRequestFulfilled(uint256 requestId, uint256[] randomWords);structRequestStatus {bool fulfilled; // whether the request has been successfully fulfilledbool exists; // whether a requestId existsuint256[] randomWords; }mapping(uint256=> RequestStatus) public s_requests; /* requestId --> requestStatus */// Fuji coordinator// https://docs.chain.link/docs/vrf/v2/subscription/supported-networks/ VRFCoordinatorV2Interface COORDINATOR;address vrfCoordinator =0x2eD832Ba664535e5886b75D64C46EB9a228C2610;bytes32 keyHash =0x354d2f95da55398f44b7cff77da56283d9c6c829a4bdf1bbcaf2ad6a4d081f61;uint32 callbackGasLimit =2500000;uint16 requestConfirmations =3;uint32 numWords =1;// past requests Ids.uint256[] public requestIds;uint256public lastRequestId;uint256[] public lastRandomWords;// Your subscription ID.uint64public s_subscriptionId;//Runners NFT Counters.Counter public tokenIdCounter;string[] characters_image = ["https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png","https://ipfs.io/ipfs/QmZGQA92ri1jfzSu61JRaNQXYg1bLuM7p8YT83DzFA2KLH?filename=Chainlink_Knight.png","https://ipfs.io/ipfs/QmW1toapYs7M29rzLXTENn3pbvwe8ioikX1PwzACzjfdHP?filename=Chainlink_Orc.png","https://ipfs.io/ipfs/QmPMwQtFpEdKrUjpQJfoTeZS1aVSeuJT6Mof7uV29AcUpF?filename=Chainlink_Witch.png" ];string[] characters_name = ["Elf","Knight","Orc","Witch" ];structRunner {string name;string image;uint256 distance;uint256 round; } Runner[] public runners;mapping(uint256=>uint256) public request_runner; /* requestId --> tokenId*/constructor(uint64 subscriptionId) ERC721("Runners", "RUN")VRFConsumerBaseV2(vrfCoordinator) { COORDINATOR =VRFCoordinatorV2Interface(vrfCoordinator); s_subscriptionId = subscriptionId;safeMint(msg.sender,0); }functionsafeMint(address to,uint256 charId) public {uint8 aux =uint8 (charId);require( (aux >=0) && (aux <=3),"invalid charId");stringmemory yourCharacterImage = characters_image[charId]; runners.push(Runner(characters_name[charId], yourCharacterImage,0,0));uint256 tokenId = tokenIdCounter.current();stringmemory uri = Base64.encode(bytes(string( abi.encodePacked('{"name": "', runners[tokenId].name,'",''"description": "Chainlink runner",','"image": "', runners[tokenId].image,'",''"attributes": [','{"trait_type": "distance",','"value": ', runners[tokenId].distance.toString(),'}',',{"trait_type": "round",','"value": ', runners[tokenId].round.toString(),'}',']}' ) ) ) );// Create token URIstringmemory finalTokenURI =string( abi.encodePacked("data:application/json;base64,", uri) ); tokenIdCounter.increment();_safeMint(to, tokenId);_setTokenURI(tokenId, finalTokenURI); }functionrun(uint256 tokenId) externalreturns (uint256 requestId) {require (tokenId < tokenIdCounter.current(),"tokenId not exists");// Will revert if subscription is not set and funded. requestId = COORDINATOR.requestRandomWords( keyHash, s_subscriptionId, requestConfirmations, callbackGasLimit, numWords ); s_requests[requestId] =RequestStatus({ randomWords:newuint256[](0), exists:true, fulfilled:false }); requestIds.push(requestId); lastRequestId = requestId;emitRequestSent(requestId, numWords); request_runner[requestId] = tokenId;return requestId; }functionfulfillRandomWords(uint256_requestId,/* requestId */uint256[] memory_randomWords ) internaloverride {require (tokenIdCounter.current() >=0,"You must mint a NFT");require(s_requests[_requestId].exists,"request not found"); s_requests[_requestId].fulfilled =true; s_requests[_requestId].randomWords = _randomWords; lastRandomWords = _randomWords;uint aux = (lastRandomWords[0] % 10+1) *10;uint256 tokenId = request_runner[_requestId]; runners[tokenId].distance += aux; runners[tokenId].round ++;stringmemory uri = Base64.encode(bytes(string( abi.encodePacked('{"name": "', runners[tokenId].name,'",''"description": "Chainlink runner",','"image": "', runners[tokenId].image,'",''"attributes": [','{"trait_type": "distance",','"value": ', runners[tokenId].distance.toString(),'}',',{"trait_type": "round",','"value": ', runners[tokenId].round.toString(),'}',']}' ) ) ) );// Create token URIstringmemory finalTokenURI =string( abi.encodePacked("data:application/json;base64,", uri) );_setTokenURI(tokenId, finalTokenURI); }functiongetRequestStatus(uint256_requestId ) externalviewreturns (bool fulfilled,uint256[] memory randomWords) { }// The following functions are overrides required by Solidity.functiontokenURI(uint256 tokenId)publicviewoverride(ERC721,ERC721URIStorage) returns (stringmemory) {return super.tokenURI(tokenId); }function_burn(uint256 tokenId) internaloverride(ERC721,ERC721URIStorage) { super._burn(tokenId); }}
In our constructor, you can see we are passing in a subscriptionId which means we first need to set up our VRF subscription here:
A VRF Subscription is a way to ensure that the decentralized Chainlink VRF service that sends your smart contract verifiably random numbers is "funded" with LINK.
For this exercise, let's use the Avalanche Fuji testnet.
First ensure you are connected to Avalanche Fuji network in your wallet. Then select Create Subscription. This subscription will be used to manage your VRF service for your smart contract.
On the Create Subscription page, enter a name for your subscription (optional) and click create subscription again.
Then you will sign and confirm the following metamask transactions. After you will see the following pop-up on your subscription page. You must wait for the transaction to be confirmed for a few seconds, after which you will see add funds which you must select.
After you click add funds you will be prompted to enter an amount of LINK tokens (if you need more testnet LINK tokens and AVAX tokens, head to https://faucets.chain.link/fuji).
For this demo we will be adding 10 LINK, then select confirm and confirm the following transaction.
Now we will be directed to the subscription page. From this page we need to copy our ID which is our subscriptionId.
Now we want to head back to our remix to the Deploy and run transactions tab, select environment Injected Provider - Metamask and make sure we are still on Avalanche Fuji Network (43113).
Then with our copied subscriptionId we will deploy the Runners.sol contract, then select the orange Deploy button
In our constructor we minted an NFT, so now we will go to Opensea and have a look at this NFT. To do this, you must copy your contract address. Note. Opensea does not support all testets, but it does support Avalanche Fuji. Here is a list of testnets that Opensea supports.
Next, paste your contract address in the Openseas search bar and select your collection. You should now see an NFT minted which is the Elf.
We now want to take our copied contract address, and head back to our VRF Subscription page. On this page we need to select Add Consumer.
This will prompt you to enter you consumer address, where you will paste your contract address and click add consumer.
Once this transaction is confirmed, we will head back to our contract in remix, open it, and call the orange run function passing in '0' for the parameter tokenId. Then click transact to execute this function.
Once the transaction has been executed, scroll down until you see the blue lastRequestId function and select this. This will return a long number which is the requestId for your call to VRF so copy this.
After some time has passed we will then paste this requestId into the blue s_requests function and we should expect to see the following as true
Now we will call the blue runners function, passing '0' as a parameter
You will notice that your value for distance may be different, mine was '50'. This is because Chainlink VRF was used to generate this number randomly - we cannot predict it or know it in advance! Random!
Lastly we will head back to Opensea and view the previously minted NFT. If we scroll down and open the Levels tab you will see that the NFT metadata has been updated with this change!