In the last lesson we developed a dynamic NFT with all metadata including the image stored on chain.

We were given the following challenge at the end of our lesson.

Week 3 challenge

At the moment we’re only storing the level of our NFTs, why not store more?

Substitute the current tokenIdToLevels[] mapping with a struct that stores:

  • Level
  • Speed
  • Strength
  • Life

Here is what we are seeking to build and how I was able to accomplish it.

To accomplish this challenge we will need to change and deploy a new contract.

In general, we will need to change;

  • the mapping to a struct, and
  • anywhere the mapping is referenced will now need to be compatible with the struct.

As we learned in previous lessons, a struct is basically an array of multiple variables. Instead of a one to one tokenID to level lookup we now must be able to look up other variables like speed, strength, and life.

To change our mapping to a struct, we must define the struct, Character, and all the varaibes and types that it will contain. Then we must create a new mapping which will allow us to pass our tokenID and return all of the the corresponding Character.


//The mapping will link an uint256, the NFTId, to another uint256, the level of the NFT
//mapping(uint256 => uint256) public tokenIdToLevels;

//Challenge 1
struct Character {
uint256 tokenID;
uint256 level;
uint256 speed;
uint256 strength;
uint256 life;
}

//The mapping will look up the Struct for that ID
mapping(uint256 => Character) public tokenIdToLevels;

Now that we have defined our struct and the new lookup, now we need to change all of the areas that reference the old mapping since they expect a single uint256 value.

Here are the functions that we need to change;

  • mint: We need to set the initial value of the struct.
  • generateCharacter: We need to make sure all of our new attributes appear in our SVG image.
  • getLevels: We need to get all of the attributes not just the level
  • train: need to update the new mapping.

In the mint function, to define the values of our new NFT we must add not only the level(0) but the entire struct definition.

We still want our initial Level to be 0, but the other values we want randomly generated. For this I created a new function named random.

   // create a new item in the tokenIdToLevels mapping and assign its value to 0
//tokenIdToLevels[newItemId] = 0;

//Challenge 1
//set the initial value of the struct
//random function used to generate initial attributes is defined in a separate function
tokenIdToLevels[newItemId] = Character(newItemId,0,random(100),random(100),random(100));

The random function takes one parameter(number) which is the upper parameter for the return value.

To calculate the random number the function converts an encoded, hashed, numeric version of block time and difficulty concatenated together, divides by our upper boundary parameter, and returns the remainder (% = modulus). For this example, I only used one of the random functions provided, so it returns the same number for all my new attributes.


// new function used to randomly generate a value for our new attributes.
function random(uint number) public view returns(uint){
return uint(keccak256(abi.encodePacked(block.timestamp,block.difficulty,
msg.sender))) % number;
}

For the generateCharacter function, we need to include all of those new attributes in the image of the NFT.

First, I added a new line of code which sets the values of 4 new variables (level, speed, strength, and life) by calling the getLevels function and passing the tokenID.

Finally, I adjust my SVG definition adding lines for new attributes, changing y = % to space them out, and finally pass the variable values (level, speed, strength, and life) from the result of the getLevels above.

//Challenge 1

//Return Struct values from mapping table
(string memory level, string memory speed, string memory strength,string memory life) = getLevels(tokenId);

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>',
//Challenge 1
//Adjust SVG definition y=% and variable attribute values from above
'<text x="50%" y="30%" class="base" dominant-baseline="middle" text-anchor="middle">',"Warrior",'</text>',
'<text x="50%" y="40%" class="base" dominant-baseline="middle" text-anchor="middle">', "Levels: ",level,'</text>',
'<text x="50%" y="50%" class="base" dominant-baseline="middle" text-anchor="middle">', "Speed: ",speed,'</text>',
'<text x="50%" y="60%" class="base" dominant-baseline="middle" text-anchor="middle">', "Strength: ",strength,'</text>',
'<text x="50%" y="70%" class="base" dominant-baseline="middle" text-anchor="middle">', "Life: ",life,'</text>',
'</svg>'
);

The getLevels function previously was just returning a single string. Since our generateCharacter function now needs multiple variables returned, we need to modify the getLevels function to accommodate.

First, I changed the levels variable to the type Character instead of uint256.

Next, I return four values by referencing the levels variable with each respective attribute(level, speed, strength, and life)

function getLevels(uint256 tokenId) public view returns (string memory,string memory,string memory,string memory) {
// uint256 levels = tokenIdToLevels[tokenId];

//Challenge 1
// levels variable now type Character
Character memory levels = tokenIdToLevels[tokenId];

// the toString() function, that's coming from the OpenZeppelin Strings library, and transforms our level, that is an uint256, into a string - that will be then be used by generateCharacter function as we've seen before.
//return levels.toString();
//Challenge 1
//return 4 different values by referencing Character struct definition
return (levels.level.toString(),levels.speed.toString(),levels.strength.toString(),levels.life.toString());
}

The final step is to modify the train function to update the struct definition from our new mapping. This looks very similar to the methods used in getLevels function above. We set a new variable character, equal to the struct returned from the mapping. We then collect the currentLevel by referencing character.level. Then we pass the incremented value (+1) back into our mapping.

// uint256 currentLevel = tokenIdToLevels[tokenId];
// Challenge 1
// new variable character
Character memory character = tokenIdToLevels[tokenId];
uint currentLevel = character.level;
tokenIdToLevels[tokenId].level = currentLevel + 1;

To complete the challenge we deploy the contract again to the Polygon Mumbai testnet, and verify our contract so we can interact with it using interact using polygonscan.

After minting a new NFT, we can validate it on testnet.opensea.io. 🎉

Then we call our train function to validate the change in level for the character.

Success 🎉

That completes Week 3 of the Road to Web3.

I really enjoyed this project and think it has made me a little more comfortable with the Solidity language.

--

--