StarknetAstro

StarknetAstro

(BootCamp Lesson3) Deploying ERC-721 Tokens on the Starknet Testnet

2023-06-11 20.15.07

Note! This tutorial is outdated due to changes in the Cairo language. Please refer to this document for version 1.10 syntax: https://starknetastro.xlog.app/Starknet_Shanghai_Workshop_DAY1#

Introduction#

This course serves as a quick-start BootCamp and will not delve too deeply into the features of the Cairo language (but our last class will introduce some other useful parts of the Cairo language in more depth).

If you want to study Cairo in depth in advance, you can check out our recently translated Cairo-Book Chinese version.

Environment Configuration#

Minimum installation options:
This course requires you to have installed Cairo 1.0 as required in the first class, as well as all necessary dependencies for the Starknet Cairo 0.x CLI.
Note! Due to the upgrade of DevNet, only starknet-compile 1.1.0 is now supported, so you need to upgrade Cairo 1.0 to v1.1.0.
Execute the following commands in the directory where you installed Cairo:

git checkout 4020f7b3
git pull
cargo build --all --release

Note: The following steps are similar to the last class. Students who have completed the configuration can directly jump to ERC721 code template

Install CLI#

To facilitate operations, let's first enter the root directory of Cairo 1.0 (refer to the directory where you installed 1.0 in the last class, for example, mine is as shown below)

cd ~/cairo/

Create a camp1_lesson3 folder:

mkdir -p starknetastro/camp1_lesson3/
cd starknetastro/camp1_lesson3/

Next, create a Python virtual environment:

python3.9 -m venv venv

Activate the virtual environment:

source venv/bin/activate

At this point, you should see that the terminal has a (venv) prefix. Let's install the CLI

(venv) camp1 $ pip install cairo-lang

Check if the installation was successful

(venv) camp1 $ starknet --version

It should output:

starknet 0.11.2

Configure Starknet Testnet Account#

Next, let's configure the testnet account. First, define a few environment variables as follows:

# Specify the testnet
export STARKNET_NETWORK=alpha-goerli

# Set the default wallet implementation
export STARKNET_WALLET=starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount

# Specify the compiler path as the path for Cairo 1 (please modify according to your own path)
export CAIRO_COMPILER_DIR=~/cairo/target/release/

You can save this in your .zshrc or .bashrc so that you can still use the environment variables after closing the terminal. The method will not be elaborated here.

We will use the following two commands to test

(venv) $ $CAIRO_COMPILER_DIR/cairo-compile --version
cairo-compile 1.1.0
(venv) $ $CAIRO_COMPILER_DIR/starknet-compile --version
starknet-compile 1.1.0

Both should output 1.1.0, indicating that the environment configuration is correct.

Create a Testnet Wallet#

Next, let's create a wallet on the testnet:

starknet new_account --account StarknetAstro

It should output

Account address: 0xYourAddress
Public key: 0xYourPublicKey

Move the appropriate amount of funds to the account, and then deploy the account by invoking the 'starknet deploy_account' command.

NOTE: This is a modified version of the OpenZeppelin account contract. The signature is computed differently.

Next, we need to deploy it on the testnet. Deploying in Starknet is also a transaction, so gas is required. We can send some test ETH to 0xYourAddress via a cross-chain bridge or the official Faucet.

スクリーンショット 2023-06-02 21.12.07

Then use this command to deploy

starknet deploy_account --account StarknetAstro

Output:

Sending the transaction with max_fee: 0.000114 ETH (113796902644445 WEI).
Sent deploy account contract transaction.

Contract address: 0xYourAddress
Transaction hash: 0xTransactionAddress

Deployment Test#

Let's first try to deploy a test contract to see if it can be deployed normally.
Execute the following commands:

mkdir src sierra
touch src/example.cairo

Then, fill in the test content in src/example.cairo:

#[starknet::contract]

mod SimpleStorage {
    #[storage]
    struct Storage {
    }
}

Next, we will compile the Cairo file into a Sierra file:

$CAIRO_COMPILER_DIR/starknet-compile src/example.cairo sierra/example.json

After successful compilation, we need to declare the contract's ClassHash:

starknet declare --contract sierra/example.json --account StarknetAstro

Of course, since the same ClassHash cannot be declared multiple times, this test contract has already been declared, so you will get an error output indicating that it has already been declared, which is not a problem:

Got BadRequest while trying to access https://alpha4.starknet.io/feeder_gateway/simulate_transaction?blockNumber=pending&skipValidate=false. Status code: 500;
text: {"code": "StarknetErrorCode.CLASS_ALREADY_DECLARED",
"message": "Class with hash 0x695874cd8feed014ebe379df39aa0dcef861ff495cc5465e84927377fa8e7e6 is already declared. 0x317d3ac2cf840e487b6d0014a75f0cf507dff0bc143c710388e323487089bfa != 0”}.

Next, let's actually deploy the contract instance:

starknet deploy --class_hash 0x695874cd8feed014ebe379df39aa0dcef861ff495cc5465e84927377fa8e7e6 --account StarknetAstro

Output:

Sending the transaction with max_fee: 0.000132 ETH (132082306595047 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x060e17c12d4e3fee8af2e28e6a310a3192a1b1190d060cb4324e234213d72b64
Transaction hash: 0x73f995d1fd9a05161e271f5ffb879bd70b79185a2b3de5536a289780387dd30

This indicates that the actual deployment was successful! You can see our deployed contract here.

ERC721 Code Template#

Save the following erc721.cairo in the src created above for easy terminal operation.

File name: erc721.cairo

use starknet::ContractAddress;

#[starknet::interface]
trait IERC721<TStorage> {
    fn name(self: @TStorage) -> felt252;
    fn symbol(self: @TStorage) -> felt252;
    fn approve(ref self: TStorage, to: ContractAddress, tokenId: u256);
    // camelCase
    fn balanceOf(self: @TStorage, account: ContractAddress) -> u256;
    fn ownerOf(self: @TStorage, tokenId: u256) -> ContractAddress;
    fn transferFrom(
        ref self: TStorage, from: ContractAddress, to: ContractAddress, tokenId: u256
    );
    fn safeTransferFrom(
        ref self: TStorage, from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span<felt252>
    );
    fn setApprovalForAll(ref self: TStorage, operator: ContractAddress, approved: bool);
    fn getApproved(self: @TStorage, tokenId: u256) -> ContractAddress;
    fn isApprovedForAll(self: @TStorage, owner: ContractAddress, operator: ContractAddress) -> bool;
    fn tokenUri(self: @TStorage, tokenId: u256) -> felt252;
}

#[starknet::contract]
mod ERC721 {

    // Other
    use zeroable::Zeroable;
    use starknet::get_caller_address;
    use starknet::contract_address_const;
    use starknet::ContractAddress;
    use option::OptionTrait;
    use array::SpanTrait;
    use traits::Into;

    #[storage]
    struct Storage {
        name: felt252,
        symbol: felt252,
        owners: LegacyMap<u256, ContractAddress>,
        balances: LegacyMap<ContractAddress, u256>,
        token_approvals: LegacyMap<u256, ContractAddress>,
        operator_approvals: LegacyMap<(ContractAddress, ContractAddress), bool>,
        token_uri: LegacyMap<u256, felt252>,
    }

    #[event]
    fn Transfer(from: ContractAddress, to: ContractAddress, token_id: u256) {}

    #[event]
    fn Approval(owner: ContractAddress, approved: ContractAddress, token_id: u256) {}

    #[event]
    fn ApprovalForAll(owner: ContractAddress, operator: ContractAddress, approved: bool) {}

    #[constructor]
    fn constructor(ref self: Storage, name_: felt252, symbol_: felt252) {
        self.name.write(name_);
        self.symbol.write(symbol_);
    }

    #[external(v0)]
    impl IERC721Impl of super::IERC721<Storage> {
        fn name(self: @Storage) -> felt252 {
            self.name.read()
        }

        fn symbol(self: @Storage) -> felt252 {
            self.symbol.read()
        }

        fn tokenUri(self: @Storage, tokenId: u256) -> felt252 {
            self.token_uri.read(tokenId)
        }

        fn balanceOf(self: @Storage, account: ContractAddress) -> u256 {
            self.balances.read(account)
        }

        fn ownerOf(self: @Storage, tokenId: u256) -> ContractAddress {
            let owner = self.owners.read(tokenId);
            match owner.is_zero() {
                bool::False(()) => owner,
                bool::True(()) => panic_with_felt252('ERC721: invalid token ID')
            }
        }

        fn approve(ref self: Storage, to: ContractAddress, tokenId: u256) {
            self.approve_helper(to, tokenId)
        }

        fn getApproved(self: @Storage, tokenId: u256) -> ContractAddress {
            self.token_approvals.read(tokenId)
        }

        fn isApprovedForAll(self: @Storage, owner: ContractAddress, operator: ContractAddress) -> bool {
            self.operator_approvals.read((owner, operator))
        }

        fn setApprovalForAll(ref self: Storage, operator: ContractAddress, approved: bool) {
            self.approval_for_all_helper(get_caller_address(), operator, approved)
        }

        fn transferFrom(ref self: Storage, from: ContractAddress, to: ContractAddress, tokenId: u256) {
            self.transfer_helper(from, to, tokenId);
        }

        fn safeTransferFrom(
            ref self: Storage, from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span<felt252>
        ) {
            self.safe_transfer_helper(from, to, tokenId, data);
        }
    }

    #[generate_trait]
    impl StorageImpl of StorageTrait {
        fn transfer_helper(
            ref self: Storage, from: ContractAddress, to: ContractAddress, token_id: u256
        ) {
            assert(!to.is_zero(), 'ERC721: invalid receiver');
            let owner = self.owners.read(token_id);
            assert(from == owner, 'ERC721: wrong sender');

            // Implicit clear approvals, no need to emit an event
            self.token_approvals.write(token_id, Zeroable::zero());

            // Update balances
            self.balances.write(from, self.balances.read(from) - 1);
            self.balances.write(to, self.balances.read(to) + 1);

            // Update token_id owner
            self.owners.write(token_id, to);

            // Emit event
            Transfer(from, to, token_id);
        }

        fn safe_transfer_helper(
            ref self: Storage, from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252>
        ) {
            self.transfer_helper(from, to, token_id);
        
        }

        fn approve_helper(
            ref self: Storage, to: ContractAddress, token_id: u256
        ) {
            let owner = self.owners.read(token_id);
            assert(owner != to, 'ERC721: approval to owner');
            assert(!owner.is_zero(), 'ERC721: invalid token ID');
            self.token_approvals.write(token_id, to);
            Approval(owner, to, token_id);
        }

        fn approval_for_all_helper(ref self: Storage, owner: ContractAddress, operator: ContractAddress, approved: bool) {
            assert(owner != operator, 'ERC721: self approval');
            self.operator_approvals.write((owner, operator), approved);
            ApprovalForAll(owner, operator, approved);
        }

        fn exists_helper(self: @Storage, token_id: u256) -> bool {
            !self.owners.read(token_id).is_zero()
        }
    }

    #[external]
    fn mint(ref self: Storage, to: ContractAddress, token_id: u256) {
        assert(!to.is_zero(), 'ERC721: invalid receiver');
        assert(!self.exists_helper(token_id), 'ERC721: token already minted');

        // Update balances
        self.balances.write(to, self.balances.read(to) + 1);

        // Update token_id owner
        self.owners.write(token_id, to);

        // Emit event
        Transfer(Zeroable::zero(), to, token_id);
    }

}

## Deploy ERC721

First, we still need to compile the Cairo file to Sierra

$CAIRO_COMPILER_DIR/starknet-compile src/erc721.cairo sierra/erc721.json

Next is the declaration

starknet declare --contract sierra/erc721.json --account StarknetAstro

This time the output is different because the erc20 file we wrote has not been deployed by anyone yet, so there will be no error (of course, students following the course will still encounter the already declared error).

Sending the transaction with max_fee: 0.000001 ETH (1378300016540 WEI).
Declare transaction was sent.
Contract class hash: 0x232f6df73a376998769e0571aa14281dc2e8a6f2dce0578e4c45741392a2b37
Transaction hash: 0x24b19a2b7b9663699ce134ce29bd03cf8f17e7cb17a30ab19509fcdc568f27f


Next, we will deploy this erc721 contract instance. Note that the constructor in the erc721 code has parameters, so we also need to provide the corresponding parameters:

starknet deploy --inputs 0x417374726F546F6B656E 0x417374726F --class_hash 0x232f6df73a376998769e0571aa14281dc2e8a6f2dce0578e4c45741392a2b37 --account StarknetAstro


The output is:

Sending the transaction with max_fee: 0.000006 ETH (6121500073459 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x051566565f452d8dc645f7aaa4f232c935ce0f89d98f7663c094261c04748dfb
Transaction hash: 0x36d8fc6b9dfece81d2804c563fd000b5b5cb8f0f704d978b7a2c1623e390d08

Great job! You can see our [deployed contract here](https://testnet.starkscan.co/contract/0x051566565f452d8dc645f7aaa4f232c935ce0f89d98f7663c094261c04748dfb)
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.