Tutorials, for Developer

How to build and mint a random NFT on Klaytn using Orakl

Introduction

Oracles are a cutting-edge technology for building real world applications powered by the blockchain. As it stands, many real-world applications require access to real-time data or events, such as stock prices, weather conditions, sports scores, random numbers and more. 

Image source

Oracles such as Orakl Network enable smart contracts to:

  • Integrate external data sources to access random numbers, price feeds, weather results etc.
  • Automate execution of smart contract logic. For example, a betting smart contract could automatically initiate a claim payout when an oracle confirms a sport result.
  • Ensure secure and accurate data inputs by leveraging cryptographic techniques.

The possibilities that come with integrating oracles into smart contracts on the blockchain are endless. Developers can now build a whole new set of applications for NFTs, blockchain gaming, lucky draws, auctions. 

In this tutorial, you will learn how to create and deploy a smart contract that uses Orakl VRF to randomly pick one of the four images below to mint as an NFT on the Klaytn Testnet Baobab. The image below represents our random NFT collection.

Primer to Orakl VRF

Orakl Network is a decentralized oracle network that allows smart contracts to access off-chain data such as verifiable random functions (VRF) and other resources. Its VRF solutions provide smart contracts to access verifiable random values. On a high level, Orakl VRF works as follows:

  • Consuming contracts requests for randomness.
  • Orakl VRF generates randomness based on some input data called the “seed”.
  • The VRF contract verifies the randomness. Note anyone who has the VRF output and the seed can verify that the output was generated correctly.
  • Consuming contracts receive verified randomness.

Account types for requesting randomness

Orakl Network VRF can be used with two different account types that support prepayment method:

  • Permanent Account: Permanent Account allows consumers to pay in advance for VRF services, and then use those funds when interacting with Orakl Network. This account is beneficial to users who want to perform VRF requests frequently and possibly from multiple smart contracts.
  • Temporary Account: Looking to pay directly for each request? This account allows users to pay directly for VRF requests and also get their response as soon as possible.

Getting started

Now that you have a background knowledge of Orakl VRF, let’s get into building our random NFT smart contract on Remix IDE. For the sake of this guide, we will be using the temporary account to request verifiable random numbers. With that said, let’s buidl!

Prerequisites

Creating a Random NFT Collection with Orakl VRF

In this section, you will learn how to create a random NFT collection using Orakl VRF on Klaytn Baobab Network. To get started, navigate to Remix IDE, create a new file in the `Contracts` folder, and then name it `SageBadgeNFT.sol`.

Step 1: Importing Contract Dependencies

To get all our functionalities up and running, we need to import the necessary contract dependencies.

For our random NFT contract, we will need to import:

  • KIP7URIStorage contract: This contract is an extension of the KIP-17 token contract obtained from Klaytn Contracts. It helps us manage our token URI based on token ids.
  • Ownable contract: We are importing this contract from Klaytn Contracts to provide us access control while using our functions. We will need this later in our `withdraw` function to allow only the owner to withdraw from the smart contract.

Also, for our smart contract to request random numbers, we need to import these Orakl VRF contract interfaces:

  • VRFConsumerBase: This must be imported and extended from the calling contract. This contract ensures that fulfillment comes from the VRFCoordinator and that the consumer contract implements fulfillRandomWords.
  • IVRFCoordinator: This contract is used both for requesting random words and for request fulfillment.

To import the above, open the SageBadgeNFT.sol file and add the following code to the top:

solidity

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.11;

import "@klaytn/contracts/KIP/token/KIP17/extensions/KIP17URIStorage.sol";

import "@klaytn/contracts/access/Ownable.sol";

import "@bisonai/orakl-contracts/src/v0.1/VRFConsumerBase.sol";

import "@bisonai/orakl-contracts/src/v0.1/interfaces/IVRFCoordinator.sol";

Step 2: Initializing The State Variables

Let’s get started by naming our contract SageBadgeNFT, and also making it inherit its dependent contracts (KIP17URIStorage, VRFConsumerBase, and Ownable). Paste the following code below the import statements in SageBadgeNFT.sol:

solidity

contract SageBadgeNFT is KIP17URIStorage, VRFConsumerBase, Ownable {

   VRFCoordinatorInterface COORDINATOR;

    // Events

    event NftRequested(uint256 indexed requestId, address requester);

    event NftMinted(SageBadge sageBage, address minter);

    enum SageBadge {

        sophos, 

        agathos,

        spoudaios,

        sphairos

    }

    mapping(uint256 => address) public s_requestIdToSender;

    uint256 public s_randomResult;

    address payable sOwner;

    uint256 private s_tokenCounter;

    uint256 public mintFee = 0.5 * 10**18;

    bytes32 keyHash = 0xd9af33106d664a53cb9946df5cd81a30695f5b72224ee64e798b278af812779c;

    uint32 callbackGasLimit = 500000;

    uint32 numWords = 1;

Let’s quickly go over what each line of code does:

The `COORDINATOR` interface instance will be used to call the `requestRandomWordsPayment()` function in order to request random numbers.

Next, we declared two events (i.e., NftRequested and NftMinted), which are logged when the user requests random NFT and when the random request is fulfilled.

We then declared the SageBadge Enum, which houses the different SageBadge types ( sophos, agathos, spoudaios, sphairos)

Next, we introduced the s_requestIdToSender mapping, which maps each requestId to the msg.sender. 

Subsequently, we declared the s_randomResult variable, which stores the random number after the request has been fulfilled. The sOwner variable is the owner of the contract, the s_tokenCounter variable takes a count of each token id, and the mintFee variable represents the amount of KLAY to be paid before minting our random NFTs. This mintFee tends to also cater for the gas fee to be paid when requesting for randomWords. 

Finally, we set the following variables:

KeyHash: It represents the maximum gas price you are willing to pay for a request in Peb.

callbackGasLimit: The limit for how much gas to use for the callback request to your contract’s `fulfillRandomWords` function.

numWords: This represents how many random values to request.

Step 3: Initializing in the Constructor

For our contract functionality to work accordingly, we have to initialize some of the variables in our dependent contracts. Let’s first pass in our constructor arguments: address coordinator, which represents the smart contract address of the VRFCoordinator on Klaytn Testnet Baobab.

> The Coordinator contract address = 0x6B4c0b11bd7fE1E9e9a69297347cFDccA416dF5F

We then initialize VRFConsumerBaseV2 with the coordinator address and KIP17 with the token name and symbol. Next, we set the COORDINATOR variable with IVRFCoordinator, and we then set the owner variable to the owner() function from Ownable.sol. Paste the following code below the previous code:

solidity

    constructor(address coordinator)

        VRFConsumerBase(coordinator)

        KIP17("Sage Badge NFT", "SBN")

    {

        COORDINATOR = IVRFCoordinator(coordinator);

        sOwner = payable(owner());

    }

Step 4: The requestRandomNFT function

This function below stands as a request to mint the random NFT. Hence, users will be paying a mint fee, which will also cater for the gas fees in requesting a random number. Here we are going to call the requestRandomWords() function defined in the COORDINATOR contract and pass keyHash, callbackGasLimit, numWords and refundRecipient as arguments. The payment for service is sent through msg.value to the requestRandomWords() in the COORDINATOR contract. If the payment is larger than expected, an exceeding payment is returned to the refundRecipient address; In our case it will be address(this) i.e the address of the contract making the request. Therefore it requires the user contract to define the receive() function as shown in the top of code listing. Eventually, it then generates a request for random words.

We then store the requestId in a s_requestIdToSender mapping to keep track of each user request. Finally, we emit the NftRequested event after the request has been executed. Paste the following code below the previous code:

solidity

    receive() external payable {}

    function requestRandomNFT()

        public

        payable

        returns (uint256 requestId)

    {

        require(msg.value == mintFee, "Mint Fee not enough");

        requestId = COORDINATOR.requestRandomWords{value: msg.value}(

            keyHash,

            callbackGasLimit,

            numWords,

address(this)

        );

        s_requestIdToSender[requestId] = _msgSender();

        emit NftRequested(requestId, _msgSender());

    }

Step 5: The fulfillRandomWords function

The VRFCoordinator calls this function when fulfilling the request. Also, it is in this function that a random NFT is minted to the users. Let’s get into the details of the function.

First, the VRFCoordinator passes the requestId and randomWords, which can then be used for our contract functionality. 

Do you remember our s_requestIdToSender mapping used in the requestRandomWordsDirect function? Now we can use it to get the owner of the request, which is stored in the nftOwner variable. Next, the tokenId counter was obtained, followed by the random value stored in s_randomresult. This value returns the modulo of 111, which equates to figure 0-110.

Then we get the SageBadge rarity value by calling the `getSageBadgeRarity()` function, which will be discussed in the helper functions section.

Next, to get our randomized token URI, we called getRandomizedTokenUri(). To be discussed in the next section.

Subsequently, we called the `safeMint()` and `_setTokenURI` functions, passing in their respective arguments. This mints a random NFT to the user’s address and sets its corresponding token URI, respectively.

Finally, we incremented the token counter and emitted the `NftMinted` event. Paste the following code below the previous code:

solidity

    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)

        internal

        override

    {

        address nftOwner = s_requestIdToSender[requestId];

        uint256 newItemId = s_tokenCounter;

        s_randomResult = randomWords[0] % 111;

        SageBadge nftSageType = getSageTypeFromRarity(s_randomResult);

        string memory tokenUri = getRandomizedTokenUri(uint256(nftSageType));

        _safeMint(nftOwner, newItemId);

        _setTokenURI(newItemId, tokenUri);

        s_tokenCounter = s_tokenCounter + 1;

        emit NftMinted(nftSageType, nftOwner);

    }

Step 6: Other helper functions

1. `getChancesArray()`: This function provide us an array of different chances each type of Sage holds

solidity

    function getChanceArray() public pure returns (uint8[4] memory) {

        // index 0 -> 15-0: 5% chance: sophos

        // index 1: 30-10: 20% chance: agathos

        // index 2: 65-30: 35% chance: spoudaios

        // index 3: 110-65: 45% chance: sphairos

        return [15, 30, 65, 110];

    }

2. `getSageTypeFromRarity()`: This function is used to get the sage type given the random number obtained from the fulfill randomness function, and their chances by calling the getChanceArray function.  In this function, given that chanceSum starts from 0, we loop through the chance array to return a particular Sage type. 

Hence if :

randomNumber is between 0 – 14: then it returns a Sophos Sage type 

randomNumber is between 15-29: then it returns a Agathos Sage type 

randomNumber is between 25-64: then it returns a Spoudaios Sage type

randomNumber is between 65-110: then it returns a Sphairos Sage type. 

solidity

function getSageTypeFromRarity(uint256 randomNumber)

        public

        pure

        returns (SageBadge sageBadge)

    {

        uint256 chanceSum = 0;

        uint8[4] memory chanceArray = getChanceArray();

        // loop through chanceArray: [15, 30, 65, 110]

        for (uint256 i = 0; i < chanceArray.length; i++) {

            if (

                randomNumber >= chanceSum && randomNumber < chanceArray[i]

            ) {

                // if randomNumber: 0-14 => sophos

                // 15-29 => agathos ,

                // 30-64 => spoudaios

                // 65-110 => sphairos

                return SageBadge(i);

            }

            chanceSum = chanceArray[i];

        }

    }

3. `getRandomizedTokenUri()`: This function returns the token uri based on the sageBadge type passed in. This token uri is passed as an argument in the _setTokenURI() function in the fulfillRandomWords() function.

To get our token URI, we need to store the NFT metadata and image of each item on a decentralized storage. To do that, we will be using Piñata to upload the data to IPFS.

Firstly, login to your Piñata account, then upload your NFT images to get their CID.

Next, create four JSON files with the metadata for your NFT. Add the four files(call them 1_agathos.json, 2_ sophos.json, 3_ sphairos, 4_ spoudaios) into a folder. 

Here is how your metadata should look like with each attribute and image values changed respectively. 

js

{

    "attributes" : [

       {

          "trait_type" : "level",

          "value" : 3

       },

       {

          "trait_type" : "stamina",

          "value" : 65

       },

       {

          "trait_type" : "personality",

          "value" : "Excellent"

       },

       {

          "display_type" : "boost_number",

          "trait_type" : "aqua_power",

          "value" : 55

       },

       {

          "display_type" : "boost_percentage",

          "trait_type" : "stamina_increase",

          "value" : 60

       },

       {

          "display_type" : "number",

          "trait_type" : "generation",

          "value" : 2

       }

    ],

    "description" : "A man of excellent character",

    "image" : "https://gateway.pinata.cloud/ipfs/QmReKwkUEdP8GHyZ7zCxgUSrycuRtmEdhn2uNUfWp2qX58",

    "name" : "Agathos"

 }

After a successful upload, you’ll see the CID for the folder. You’ll need this for the URI.

If you click on the folder you’ll see the details of the files separately on IPFS.

Now you can copy the link for each item and store in the variable `tokenUri` as seen in the function below:

solidity

    function getRandomizedTokenUri(uint256 randomNum)

        internal

        pure

        returns (string memory uri)

    {

        string[4] memory tokenUri = [

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/1_%20agathos.json",

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/2_%20sophos.json",

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/3_%20sphairos.json",

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/4_%20spoudaios.json"

        ];

        return tokenUri[randomNum];

    }

4. Withdraw, getMintFee, getTokenCounter function

The withdraw function allows the owner to withdraw KLAY available in this contract. Also the getMintFee function returns the mintFee. Finally the getTokenCounter function returns the current token id. 

solidity

    function withdraw() public onlyOwner {

        require(sOwner.send(address(this).balance));

    }

    // getters

    function getMintFee() public view returns (uint256) {

        return mintFee;

    }

    function getTokenCounter() public view returns (uint256) {

        return s_tokenCounter;

    }

Now we have been able to build our contract functionality. Your code will look like this: 

solidity

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.11;

import "@klaytn/contracts/KIP/token/KIP17/extensions/KIP17URIStorage.sol";

import "@klaytn/contracts/access/Ownable.sol";

import "@bisonai/orakl-contracts/src/v0.1/VRFConsumerBase.sol";

import "@bisonai/orakl-contracts/src/v0.1/interfaces/IVRFCoordinator.sol";

contract SageBadgeNFT is KIP17URIStorage, VRFConsumerBase, Ownable {

   IVRFCoordinator COORDINATOR;

    // Events

    event NftRequested(uint256 indexed requestId, address requester);

    event NftMinted(SageBadge sageBadge, address minter);

    enum SageBadge {

        sophos, 

        agathos, 

        spoudaios, 

        sphairos

    }

    mapping(uint256 => address) public s_requestIdToSender;

    uint256 public s_randomResult;

    address payable sOwner;

    uint256 private s_tokenCounter;

    uint256 public mintFee = 1 * 10**18;

    bytes32 keyHash = 0xd9af33106d664a53cb9946df5cd81a30695f5b72224ee64e798b278af812779c;

    uint32 callbackGasLimit = 500000;

    uint32 numWords = 1;

// VRF Coordinator contract address

// https://baobab.scope.klaytn.com/account/0x6B4c0b11bd7fE1E9e9a69297347cFDccA416dF5F

    constructor(address coordinator)

        VRFConsumerBase(coordinator)

        KIP17("Sage Badge NFT", "SBN")

    {

        COORDINATOR = IVRFCoordinator(coordinator);

        sOwner = payable(owner());

    }

    receive() external payable {}

    function requestRandomNFT()

        public

        payable

        returns (uint256 requestId)

    {

        require(msg.value == mintFee, "Mint Fee not enough");

        requestId = COORDINATOR.requestRandomWords{value: msg.value}(

            keyHash,

            callbackGasLimit,

            numWords,

            address(this)

        );

        s_requestIdToSender[requestId] = _msgSender();

        emit NftRequested(requestId, _msgSender());

    }

    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)

        internal

        override

    {

        address nftOwner = s_requestIdToSender[requestId];

        uint256 newItemId = s_tokenCounter;

        s_randomResult = randomWords[0] % 111;

        SageBadge nftSageType = getSageTypeFromRarity(s_randomResult);

        string memory tokenUri = getRandomizedTokenUri(uint256(nftSageType));

        _safeMint(nftOwner, newItemId);

        _setTokenURI(newItemId, tokenUri);

        s_tokenCounter = s_tokenCounter + 1;

        emit NftMinted(nftSageType, nftOwner);

    }

    function getChanceArray() public pure returns (uint8[4] memory) {

        // index 0 -> 15-0: 5% chance: sophos

        // index 1: 30-10: 20% chance: agathos

        // index 2: 65-30: 35% chance: spoudaios

        // index 3: 110-65: 45% chance: sphairos

        return [10, 25, 50, 110];

    }

    function getSageTypeFromRarity(uint256 randomNumber)

        public

        pure

        returns (SageBadge sageBadge)

    {

        uint256 chanceSum = 0;

        uint8[4] memory chanceArray = getChanceArray();

        // loop through chanceArray: [15,30, 65, 110]

        for (uint256 i = 0; i < chanceArray.length; i++) {

            if (

                randomNumber >= chanceSum && randomNumber < chanceArray[i]

            ) {

                // if randomNumber: 0-14 => sophos

                // 15-29 => agathos ,

                // 30-64 => spoudaios

                // 65-110 => sphairos

                return SageBadge(i);

            }

            chanceSum = chanceArray[i];

        }

    }

    function getRandomizedTokenUri(uint256 randomNum)

        internal

        pure

        returns (string memory uri)

    {

        string[4] memory tokenUri = [

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/1_%20agathos.json",

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/2_%20sophos.json",

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/3_%20sphairos.json",

            "https://gateway.pinata.cloud/ipfs/QmczSfe1Z8hkuRFhEQR6DY6KaMEo2jbGNMXqdLZhvTbVSP/4_%20spoudaios.json"

        ];

        return tokenUri[randomNum];

    }

    function withdraw() public onlyOwner {

        require(sOwner.send(address(this).balance));

    }

    // getter functions

    function getMintFee() public view returns (uint256) {

        return mintFee;

    }

    function getTokenCounter() public view returns (uint256) {

        return s_tokenCounter;

    }

}

Deploying the Random NFT Smart Contract on Remix IDE

  • In Remix, click Compile contract.
  • Click the Klaytn tab on your left having installed the plugin
  • Select Environment > Injected ProviderMetaMask.
  • In Contract, select your contract. For example, SageBadgeNFT.sol.
  • Pass in the VRFCoordinator contract address: 0xfa605ca6dc9414e0f7fa322d3fd76535b33f7a4f as argument 
  • Click Deploy.

Interacting with the Random NFT Smart Contract on Remix IDE

Having deployed our random SageBadge NFT successfully, we want to be able to request random numbers and mint the NFT once this random number request has been fulfilled. 

To accomplish this, we will have to go through the following steps: 

A. Execute the requestRandomNFT function, buy sending in the minting-fee (1 KLAY)

B. Wait for the VRFCoordinator to execute the fulfillRandom function, which will then execute the safemint function. 

C. Check OpenSea for your newly minted random NFTs.

At another `requestRandomNFT()` function call, the nft below was minted.

Conclusion

In this tutorial, you successfully requested a random number using Orakl VRF on Klaytn Baobab TestNet, which was instrumental in generating a random NFT. Currently, the Orakl VRF service is only available on Klaytn Testnet. Once it is live on Klaytn Cypress Mainnet, these same steps can be applied when trying to get the random numbers.

Orakl VRF solution can be used for other use cases other than generating random NFTs. It can further be used in your smart contract for fairly selecting participants, randomly selecting winners, etc.

If you want more information, visit Klaytn Docs and Orakl Network Docs. If you have any questions, please visit Klaytn Forum.