Creating an NFT with on-chain metadata

Third week of Road to Web3

Hi again! As you might know I recently published a blog post about my second week of Road to Web3 with Alchemy. The thing is I finished that project almost 2 months ago but I couldn't find time to complete the article at that time. Then I had to take a break but now I am back on track again. This time I am writing the blog post immediately after finishing the third week.

Before moving on I want to share the documentation of this week and I want to thank to Rahat who was the teacher of this week's tutorial. In this challenge we are going to write an ERC721 NFT smart contract, we will deploy it and last but not least we are going to change the on-chain NFT metadata using a function on our smart contract. And again, I'm not going to tell you how to do it, I will only write what I learned during the process.

Creating Smart Contract

Before writing the contract, we have to import some libraries first and we will use OpenZeppelin for this purpose. The first thing that needs to be imported is the token standard which is going to be ERC721 because this is a NFT project. You can check the OpenZeppelin docs and see that there are different kinds of contracts and also some extensions. If you were working on RoadtoWeb3 like me I'm sure you realised that we needed to use an extension for this project and this question might have come to your mind: What is the difference between ERC721 and ERC721URIStorage? Why did we use that? Actually the answer is simple. I won't go into details but if you use ERC721 you need to store the metadata off-chain and if you want to create an on-chain NFT you need to use the URIStorage extension.

The other things that we needed to import were utilities like strings, counters and they were used like this: using Strings for uint256; and using Counters for Counters.Counter; What do those mean? "using Strings for uint256". It sounds weird, isn't it? Let me explain. Strings in here is the library that we will use and uint256 is our variable. This means we can use any function in the Strings Library for integers with dot(.) notation like this: myInteger.toString(). Same as we can use Counters Library for Counters.Counter which is a struct already defined by the library. I want to give an example for clearer explanation.

Counters.Counter private _tokenIds;
_tokenIds.increment();

As I said above Counters.Counter is a struct. In the first line I declare that "_tokenIds" is a struct. In the second line I use the "increment()" function from the Counters library for the struct that I already declared. I am not sure that I can be more clearer but you can check this too if you want.

Now, the next stop. Mapping. In this project we have this code: mapping(uint256 => uint256) public tokenIdToLevels;. Mapping is like connecting two things to each other as key-value pairs. The code above connects two integers, its name is tokenIdToLevels, and to use this mapping you have to use it with square brackets like this: mappingName[_keyValue] or tokenIdToLevels[1]. This is the basic part but at the end of this article I will go into some details when I try to explain how I manage to handle this week's challenges.

Do you see? There have already been a lot of words but I still couldn't start to write about the functions yet. The part until here was only the preparatory section which I believe is extremely important for learning with solid foundations. Now let's move on to the contract functions which are generating character, getting token URI, minting and training.

function generateCharacter(uint256 tokenId) public returns(string memory){

    bytes memory svg = abi.encodePacked(
        '<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">',
        '<style>.base { fill: white; font-family: serif; font-size: 14px; }</style>',
        '<rect width="100%" height="100%" fill="black" />',
        '<text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">',"Warrior",'</text>',
        '<text x="50%" y="50%" class="base" dominant-baseline="middle" text-anchor="middle">', "Levels: ",getLevels(tokenId),'</text>',
        '</svg>'
    );

    return string(
        abi.encodePacked(
            "data:image/svg+xml;base64,",
            Base64.encode(svg)
        )    
    );
}

To create an on-chain NFT, we are going to use a SVG code which represens the image of the NFT. We need to do a few conversions to be able to see the image and I will explain the logic behind it now.

  1. SVG is a huge text
  2. We need base64 data to see the image.
  3. If we type "data:image/svg+xml;base64," and then add the "base64 data" we created in the URL, we can see the image.

In the code above first we convert the SVG text into bytes and declare it as "svg" variable. The abi.encodePacked function helps us to do that. At the end with Base64.encode(svg) line we take that variable and convert it to base64 data. Now we have our base64 data and we need to do the third step above but at this point I learned that unfortunately we can not concatenate two strings directly like string0 = string1 + string2 or string("string1", "string2") in Solidity. That's why we have to use abi.encodePacked in this function.

Then we have the getTokenURI function which is almost the same as the previous function. Only difference is we use JSON objects rather than SVG text.

It is time to write the minting function. It was pretty straightforward actually. I only want to mention that thanks to OpenZeppelin contracts everything is already prepared. We will use _safeMint and _setTokenURI functions. safemint settokenURI In these screenshots you can see _safeMint function takes two arguments which are address and token ID. _setTokenURI also takes two arguments which are token ID and token URI. You can check the requirements in OpenZeppelin documents with the links.

After that we have the training function to change the metadata on the blockchain, which is amazing. There were new functions for me to learn which were ownerOf and _exists. You can also check them with this link. That was the tutorial and it was fun to follow. Everything was working and after that I had to complete two challenges of the week.

Challenges of the week

Do you remember that in the beginning of the article I mentioned mapping. It was integer to integer: token ID to level. My first challenge was to implement a new mapping and it should be integer to struct. This way I could add not only levels but also attack points or defence etc. Of course I had to read a lot to manage it. I read the documentation to understand the structs and how to use them as a value type in mappings.

  1. Declare the struct. struct
  2. Declare the mapping (I didn't change the mapping name but it is not important). mapping At this point tokenIdtoLevels[tokenId] will give me a struct because the value type of mapping is struct. In the previous one this same code was giving me an integer.

  3. Access the struct elements. So I learned that I can access the elements of the struct directly like tokenIdtoLevels[tokenId].levels with dot(.) notation or I can assign a local variable in storage like this: pokemon storage p = tokenIdtoLevels[tokenId]. Then use that local variable to access the struct elements. levels After figuring this out I only needed to make small changes in the mint function and other functions.

The second challenge of the week was to learn how to create random numbers in solidity and use those numbers in the train function. I asked it to dear google of course and read this and that, and started copying and trying. At first I decided to write a random number generator function and call that function inside the train function like this: random What was the result? Yes, you are right. Disappointment! It was creating random numbers but there was something off. Both attack and defence were getting the same number, which I definitely don't want that. Screenshot 2022-07-16 at 15.41.03.png

Where was the problem? I was using block.difficulty and block.timestamp as variables. So when the train function is called it is executed in one block. It didn't matter to call the random() function two times because variables were the same. Then I changed the variables. Screenshot 2022-07-16 at 15.49.25.png In this one there was also a "nonce" variable which was going to change when the function was called. So in this way when it is called a second time, the random number should be different, right? No, it was not right. Another disappointment. Both attack and defence points were the same. Screenshot 2022-07-16 at 15.58.09.png

I am still not sure why it didn't work but then I decided to not create a random number generator function and create the random numbers inside the train function instead. Screenshot 2022-07-16 at 16.00.13.png

Screenshot 2022-07-16 at 16.03.15.png

That was the last code I wrote for this challenge and voila! Numbers were different, finally.

This was my journey on the third week of RoadtoWeb3 and this is the github repo of the project. I learned a lot of stuff this week too. The other thing I realised is writing this blog post takes much longer time than I anticipated. I could have definitely finished week 4 in the same amount of time I spent writing this article. On the other hand, creating this series makes me read a lot more and learn more. I hope it helps you too and I also hope you enjoy reading it. See you next time.