StarkNet ERC721 Workshop: Exercise 1

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

Deploying an ERC721

  • Create an ERC721 token contract. You can use this implementation as a base
  • Deploy it to the testnet (check the constructor for the needed arguments. Also note that the arguments should be decimals.)
$ starknet-compile contracts/ERC721/ERC721.cairo --output artifacts/ERC721.json
$ starknet deploy --contract artifacts/ERC721.json --inputs arg1 arg2 arg3 --network alpha-goerli
  • Give token #1 to Evaluator contract
  • Call submit_exercise() in the Evaluator to configure the contract you want evaluated (4 pts)
  • Call ex1_test_erc721() in the evaluator to receive your points (2 pts)

Looking for the project setup? Click here.

Solution

The goal of this exercise is to learn how to deploy a smart contract to StarkNet’s Goerli test network and to learn how to interact with them using StarkNet’s block explorer Voyager and StarkNet’s wallet Argent X. You can also use Braavos as an alternative for a wallet on Starknet.

Note: If you haven’t done so already make sure to install the Argent X browser extension and create an account on Goerli to follow along with the workshop.

To start I’m going to copy over to my project the OpenZeppelin implementation of the ERC721MintableBurnable.cairo referenced by the exercise under the file ERC721_ex1.cairo inside of the contracts folder. 

src/ERC721_ex1.cairo (excerpt)

@constructor
func constructor{...}(name: felt, symbol: felt, owner: felt)

//
// Getters
//

@view
func supportsInterface{...}(interfaceId: felt) -> (success: felt)

@view
func name{...}() -> (name: felt)

@view
func symbol{...}() -> (symbol: felt)

@view
func balanceOf{...}(owner: felt) -> (balance: Uint256)

@view
func ownerOf{...}(tokenId: Uint256) -> (owner: felt)

@view
func getApproved{...}(tokenId: Uint256) -> (approved: felt)

@view
func isApprovedForAll{...}(owner: felt, operator: felt) -> (isApproved: felt)

@view
func tokenURI{...}(tokenId: Uint256) -> (tokenURI: felt)

@view
func owner{...}() -> (owner: felt)

//
// Externals
//

@external
func approve{...}(to: felt, tokenId: Uint256)

@external
func setApprovalForAll{...}(operator: felt, approved: felt)

@external
func transferFrom{...}(from_: felt, to: felt, tokenId: Uint256)

@external
func safeTransferFrom{...}(from_: felt, to: felt, tokenId: Uint256, data_len: felt, data: felt*)

@external
func mint{...}(to: felt, tokenId: Uint256)

@external
func burn{...}(tokenId: Uint256)

@external
func setTokenURI{...}(tokenId: Uint256, tokenURI: felt)

@external
func transferOwnership{...}(newOwner: felt)

@external
func renounceOwnership{...}()

We are ready now to compile our ERC721 smart contract so it’s ready to be deployed to StarkNet.

$ mkdir comp
$ starknet-compile src/ERC721_ex1.cairo --output comp/ERC721_ex1.json

The starknet-compile command is unable to create new folders so that’s why I had to create the output folder first with mkdir before attempting to compile the smart contract.

After compilation is completed, our project folder structure should look like this:

$ tree .
>>>
.
├── comp
│   └── ERC721_ex1.json
└── src
    └── ERC721_ex1.cairo

When deploying a smart contract we need to provide the arguments of the constructor. Let’s check what the constructor is expecting as arguments.

src/ERC721_ex1.cairo

@constructor
func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(
    name: felt, symbol: felt, owner: felt
) {
    ERC721.initializer(name, symbol);
    Ownable.initializer(owner);
    return ();
}

In our case we will need to provide the arguments name, symbol and owner in the same order they are declared in the constructor making our deploy command to look like this:

$ starknet deploy --contract comp/ERC721_ex1.json \
  --inputs <name> <symbol> <owner> --network alpha-goerli --no_wallet

Even though the type of the three arguments is felt we know intuitively that name and symbol are strings while owner is an address. To transform string and hex to felt we will need to use the utility library provided by the StarkWare team that I’m going to copy over to our project.

utils.py

MAX_LEN_FELT = 31

def str_to_felt(text):
    if len(text) > MAX_LEN_FELT:
        raise Exception("Text length too long to convert to felt.")
    return int.from_bytes(text.encode(), "big")

def felt_to_str(felt):
    length = (felt.bit_length() + 7) // 8
    return felt.to_bytes(length, byteorder="big").decode("utf-8")

def str_to_felt_array(text):
    return [str_to_felt(text[i:i+MAX_LEN_FELT]) for i in range(0, len(text), MAX_LEN_FELT)]

def uint256_to_int(uint256):
    return uint256[0] + uint256[1]*2**128

def uint256(val):
    return (val & 2**128-1, (val & (2**256-2**128)) >> 128)

def hex_to_felt(val):
    return int(val, 16)

We are going to name our collection “Animals” while giving it the symbol “ANI”. Because I want to be able to mint a new token using Voyager’s UI, I’m going to set up one of my Argent X’s test accounts (0x0113…) as the owner of the deployed smart contract.

We can now use the utils.py library to derive the felt representation of our strings and address.

$ python -i utils.py
>>> str_to_felt("Animal")
71942470984044
>>> str_to_felt("ANI")
4279881
>>> hex_to_felt("0x0113349F3B0Cf24A953BBD1Bb3B9ea20cedaf49a00e918F56A9B3327164A39D5")
486246126474359946192348700142268263967120013078464126154508728538516568533

Now that I have the felt representation of the three values required by the constructor, I can deploy the smart contract to StarkNet’s Goerli test network.

$ starknet deploy --contract comp/ERC721_ex1.json \
  --inputs 71942470984044 4279881 4862… --network alpha-goerli --no_wallet
>>>
Deploy transaction was sent.
Contract address: 0x035b…
Transaction hash: 0x19f5…

Once the Sequencer picks up the transaction and adds it to a block, we can explore our newly deployed ERC721 smart contract using Voyager (0x035b…).

Reading the NFT collection name on Voyager
Fig 1. Reading the NFT collection name on Voyager

We can now mint our first NFT and define the Evaluator smart contract (0x06d2…) as its owner as the exercise requests. Remember that to execute this transaction you’ll need to have test tokens on your wallet to pay for gas fees. You can get test tokens for free from the Goerli faucet.

Minting the first NFT and assigning it to the Evaluator
Fig 2. Minting the first NFT and assigning it to the Evaluator

Clicking on the “Transact” button will open Argent X to sign the transaction. After waiting for a while for the transaction to be confirmed we can use Voyager again to check if the NFT with tokenId 1 is in effect assigned to the Evaluator smart contract.

Verifying NFT ownership on Voyager
Fig 3. Verifying NFT ownership on Voyager

The function ownerOf returns the expected address. We are now ready to submit our ERC721 smart contract (0x035b…) to the Evaluator (0x06d2…) using the submit_exercise function.

Submitting our ERC721 smart contract to the Evaluator
Fig 4. Submitting our ERC721 smart contract to the Evaluator

Once the transaction is processed we can verify if we passed the first exercise by calling the function ex1_test_erc721 of the Evaluator.

Validating the completion of the first exercise
Fig 5. Validating the completion of the first exercise

If everything has gone according to plan we should be able to verify that our Argent X wallet (0x0113…) has gotten 6 points on the Point Counter smart contract (0x00a0…).

Checking the score on Voyager
Fig 6. Checking the score on Voyager

Indeed we got awarded 6 points for completing exercise 1.

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

To continue the tutorial go to Exercise 2: Creating Token Attributes.

17 thoughts on “StarkNet ERC721 Workshop: Exercise 1”

  1. When I want to deploy the contract, it shows an error :
    “Error: JSONDecodeError: Expecting value: line 1 column 1 (char 0)”, is the owner address too long?

      1. starknet deploy –contract artifacts/ERC721.json \
        –input 71942470984044 4279881 486246126474359946192348700142268263967120013078464126154508728538516568533 \
        –network alpha-goerli \
        –no_wallet

        1. The problem could be the backslashes, try removing them and have the whole command on a single line. Also, you are making my wallet address the owner of your smart contract which I don’t think that’s what you want. You should create your own test account using Argent X and then derive the felt value of the hex address using utils.py

          1. I probably found my problem, I hadn’t compiled the source file before. There is a bit of a problem in the source code, the import path for ERC165 and ownable is not correct. the base template on Starknet has the same problem.

          2. Thanks to your comment I realized that OpenZeppelin released a new version of their Cairo smart contracts (0.3.1) that changed how imports should be handled. I have updated all my articles and my Github repo with the changes. If you spot any other issue please let me know so we can make life a little bit easier for the next person reading along.

    1. Reading your comment and checking the article I realized that the image I had for minting an NFT assigned to the Evaluator smart contract was wrong, I put the tokenId in the “to” field and the owner address in the “tokenId” field. It has been corrected now but maybe you made the same mistake?

    2. When repeat the step to mint again, Argent X will prompt an error “error message: erc721: token already minted”.

      1. Sounds to me that you did the minting correctly the first time and an NFT with tokenId 1 already exist. What’s the address of your smart contract?

  2. The last step to verify the balance of account.The picture shows that need to input “account” on the function balanceOf, and the contract on the voyager display “owner”, is it the same property?

    1. Couldn’t get the coin. and fill with some error:

      Transaction log
      Error at pc=0:12: Got an exception while executing a hint. Cairo traceback (most recent call last): Unknown location (pc=0:161) Unknown location (pc=0:147) Error in the called contract : Error message: nonce invalid Error at pc=0:1638: An ASSERT_EQ instruction failed: 18 != 20. Cairo traceback (most recent call last): Unknown location (pc=0:802) Unknown location (pc=0:655)

        1. While I run the function “balanceOf”, it show the error as I memtion before. After that I recreation the contract. Seems it works, but only get 1 point on the balance.

          1. I would like to help you but I think chatting in the comment section is not very effective. Are you on the StarkNet Discord server? If not here’s the link to join: https://discord.gg/BmHKEquN

            I’m usually in the “tutorials-support” channel and my Discord handle is “DavidB | StarkWare”. I think it would be easier to continue our discussion there.

So, what do you think?

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