StarkNet ERC721 Workshop: Exercise 7

Note: This article was updated on Sep 20th, 2022 to use Cairo 0.10

Adding Metadata

  • Create a new ERC721 contract that supports metadata. You can use this contract as a base
  • The base token URI is the chosen IPFS gateway
  • You can upload your NFTs directly on this website
  • Your tokens should be visible on Oasis once minted!
  • Deploy your new contract
  • Call submit_exercise() in the Evaluator to configure the contract you want evaluated
  • Claim points on ex7_add_metadata (2 pts)

Looking for exercise 6? Click here.

Solution

To truly understand what’s expected from us on this exercise we have to start by exploring what ex7_add_metadata does.

Evaluator.cairo

@external
func ex7_add_metadata{...}() {
    alloc_locals;
    // Reading caller address
    let (sender_address) = get_caller_address();
    // Retrieve exercise address
    let (submited_exercise_address) = player_exercise_solution_storage.read(sender_address);
    // Get evaluator address
    let (evaluator_address) = get_contract_address();
    // Retrieve dummy token address
    let (dummy_metadata_erc721_address) = dummy_metadata_erc721_storage.read();
    // Reading metadata URI for token 1 on both contracts. For these to show up in Oasis, they should be equal
    let token_id = Uint256(1, 0);
    with_attr error_message("Couldn't retrieve the metadata URI") {
        let (metadata_player_len, metadata_player) = IERC721_metadata.tokenURI(
            contract_address=submited_exercise_address, token_id=token_id
        );
    }

    let (metadata_dummy_len, metadata_dummy) = IERC721_metadata.tokenURI(
        contract_address=dummy_metadata_erc721_address, token_id=token_id
    );
    with_attr error_message("Your token uri is not the same length as the dummy metadata") {
        // Verifying they are equal
        assert metadata_dummy_len = metadata_player_len;
    }

    with_attr error_message("Your token uri is not the same as the dummy metadata") {
        ex7_check_arrays_are_equal(metadata_dummy_len, metadata_dummy, metadata_player);
    }
    // Checking if player has validated this exercise before
    let (has_validated) = has_validated_exercise(sender_address, 7);

    if (has_validated == 0) {
        // player has validated
        validate_exercise(sender_address, 7);
        // Sending points
        distribute_points(sender_address, 2);
        return ();
    else {
        return ();
    }
}

In a nutshell, ex7_add_metadata is calling the function tokenURI for the NFT with token_id 1 on our smart contract and comparing if the returned value is the same as the value returned by the Dummy ERC721 Token provided at the beginning of the workshop.

We can check what that value returned by the dummy token is by going to Voyager (0x4fc2…) and calling the function tokenURI passing the number 1 as the token_id.

Token URI for the NFT with token id 1 in the Dummy ERC721
Token URI for the NFT with token id 1 in the Dummy ERC721

Concatenating the returned array we can see that the URI is https://gateway.pinata.cloud/ipfs/QmWUB2TAYFrj1Uhvrgo69NDsycXfbfznNURj1zVbzNTVZv/1.json. If we follow the link we get the metadata of the NFT.

{
    "name": "Gan generated image 1",
    "image": "https://gateway.pinata.cloud/ipfs/Qmd9PegtrP3c7r6uJMWTC3CMCQUTVTzqg8jtmZsxnuUAeD/1.jpeg"
}

If we follow the URI for the image we can see the NFT itself.

NFT image
NFT image

Now that we know what our smart contract is supposed to return as tokenURI we can move on to the implementation. We are going to follow the recommendation from the exercise and copy over the base files we need to have an ERC721 smart contract with the Metadata extension.

In particular, this is what we need to do:

Once we finish copying over the relevant files, we should end up with the following folder structure:

$ tree .
>>>
.
├── README.md
├── abis
│   ├── ERC721_ex3.json
│   └── ERC721_ex4.json
├── comp
│   ├── ERC721_ex1.json
│   ├── ERC721_ex2.json
│   ├── ERC721_ex3.json
│   ├── ERC721_ex4.json
│   └── ERC721_ex5.json
├── src
│   ├── ERC721_Metadata.cairo
│   ├── ERC721_ex1.cairo
│   ├── ERC721_ex2.cairo
│   ├── ERC721_ex3.cairo
│   ├── ERC721_ex4.cairo
│   ├── ERC721_ex5.cairo
│   └── ERC721_ex7.cairo
└── libs
    ├── Array.cairo
    └── ShortString.cairo
└── utils.py

Because we have changed some names and have a different folder structure than the base files, we need to make some changes on how some files are imported. The changes are shown below.

Changes to src/ERC721_ex7.cairo

// before
from contracts.token.ERC721.ERC721_Metadata_base import (
    ERC721_Metadata_initializer,
    ERC721_Metadata_tokenURI,
    ERC721_Metadata_setBaseTokenURI,
)

// after
from contracts.ERC721_Metadata import (
    ERC721_Metadata_initializer,
    ERC721_Metadata_tokenURI,
    ERC721_Metadata_setBaseTokenURI,
)

Changes to src/ERC721_Metadata.cairo

// before
from contracts.utils.ShortString import uint256_to_ss
from contracts.utils.Array import concat_arr

// after
from shared.ShortString import uint256_to_ss
from shared.Array import concat_arr

With these changes in place we should be ready to compile our smart contract.

$ starknet-compile src/ERC721_ex7.cairo \
  --output comp/ERC721_ex7.json --abi abis/ERC721_ex7.json

In order to deploy our smart contract we need to know what arguments we need to pass to the constructor.

src/ERC721_ex7.cairo

@constructor
func constructor{...}(
    name : felt,
    symbol : felt,
    owner : felt,
    base_token_uri_len : felt,
    base_token_uri : felt*,
    token_uri_suffix : felt,
) {
    ...
}

I want to name my NFT collection “Ready Doggo One” with the symbol “RD1”. The owner will be my Argent X test account (0x0113…), the base URI https://gateway.pinata.cloud/ipfs/QmWUB2TAYFrj1Uhvrgo69NDsycXfbfznNURj1zVbzNTVZv/ and the prefix “.json”.

Note: You can use Braavos as an alternative to Argent X.

Let’s get the felt version of the short strings (less than 31 characters) first by using utils.py.

$ python -i utils.py
>>> str_to_felt("Ready Doggo One")
427824581996521952334490376445324901
>>> str_to_felt("RD1")
5391409
>>> str_to_felt(".json")
199354445678

Next we can get the felt version of the Argent X test account as we have done in previous exercises.

>>> hex_to_felt("0x0113349F3B0Cf24A953BBD1Bb3B9ea20cedaf49a00e918F56A9B3327164A39D5")
486246126474359946192348700142268263967120013078464126154508728538516568533

Finally, let’s get the felt version of the base URI.

>>> str_to_felt_array("https://gateway.pinata.cloud/ipfs/QmWUB2TAYFrj1Uhvrgo69NDsycXfbfznNURj1zVbzNTVZv/")
[184555836509371486644298270517380613565396767415278678887948391494588524912, 181013377130045435659890581909640190867353010602592517226438742938315085926, 2194400143691614193218323824727442803459257903]

Notice that when dealing with a long string (more than 31 characters) the returned value is not a single felt number but an array of them. When we want to pass an array as an input for a function like in the case of our constructor, we have to pass first the length of the array and then the elements of the array in order.

The deploy command for our smart contract will be a long one. Notice that the number 3 provided as part of the inputs is the size of the array that describes the baseURI.

$ starknet deploy --contract comp/ERC721_ex7.json \
  --network alpha-goerli --no_wallet \
  --inputs 427824581996521952334490376445324901 5391409 486246126474359946192348700142268263967120013078464126154508728538516568533 3 184555836509371486644298270517380613565396767415278678887948391494588524912 181013377130045435659890581909640190867353010602592517226438742938315085926 2194400143691614193218323824727442803459257903 199354445678

>>>
Deploy transaction was sent.
Contract address: 0x051ea806002f77025c64e53fe425fa53e5d73c497ead8db27f6300e4791a2bdb
...

Once the contract is deployed we will need to head over to Voyager (0x051e…) to mint an NFT with token_id 1.

Minting an NFT with Token ID 1
Minting an NFT with Token ID 1

Once the NFT is minted we can verify that our smart contract is returning the same token URI for token id 1 as the Dummy ERC721 did.

Our smart contract tokenURI for token_id 1
Our smart contract tokenURI for token_id 1

Concatenating the returned array we get the exact same link as before: https://gateway.pinata.cloud/ipfs/QmWUB2TAYFrj1Uhvrgo69NDsycXfbfznNURj1zVbzNTVZv/1.json.

This should be all we need to submit our exercise for evaluation. We start by invoking the function submit_exercise on the Evaluator (0x06d2…) passing the address of our smart contract.

Submitting our smart contract for evaluation
Submitting our smart contract for evaluation

We then evaluate our solution by invoking the function ex7_add_metadata on the Evaluator (0x06d2…).

Validating our solution for exercise 7
Validating our solution for exercise 7

If everything works as expected we should have been granted two more points on the Points Counter smart contract (​​0x00a0…).

Checking the score after completing exercise 7
Checking the score after completing exercise 7

And sure enough, we have been awarded two more points. Challenge completed successfully.

To see the final implementation of the ERC721 token for exercise 7 go to my GitHub repo.

2 thoughts on “StarkNet ERC721 Workshop: Exercise 7”

    1. Hi, I’m glad the article was of help. I’m considering what to do next. I was inclined to do a deep dive on unit testing StarkNet’s smart contracts first but I’ll keep your suggestion in mind as well.

So, what do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.