StarknetAstro

StarknetAstro

(Starknet Shanghai Workshop) Implementation of ERC Standards in Cairo: Environment Configuration and Practical Operations

52994791718314e29c662ab33aafc40c

Introduction#

This course serves as a quick-start workshop and will not delve too deeply into the features of the Cairo language (but we will cover other useful parts of the Cairo language in detail during the last session of our online Bootcamp).

If you want to learn Cairo in depth ahead of time, you can check out our recently translated Cairo-Book English version.

Environment Setup#

Minimum Installation Options:

System: curl, git
IDE: VSCode or any editor you prefer (just don't use the default Notepad on Windows)
MacOS: homebrew
Cairo-Lang CLI.

Install Rust#

It is recommended to install Rust via rustup (docs). rustup allows you to switch Rust versions and upgrade.

~ $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Then restart your command line terminal to verify if the installation was successful, or execute the following command in the terminal:

~ $ source "$HOME/.cargo/env"

Verify the version:

~ $ rustup --version
rustup 1.25.2 (17db695f1 2023-02-01)

If you haven't installed curl, you can also download various installation packages for Rust here:

https://forge.rust-lang.org/infra/other-installation-methods.html#rustup

Install Cairo#

Enter the following command in the terminal to clone the latest Cairo repo from GitHub:

git clone https://github.com/starkware-libs/cairo/ ./cairo

Next, we will use the stable version 1.10:

cd ./cairo
git checkout 1003d5d14c09191bbfb64ee318d1975584b3c819
git pull
cargo build --all --release

Test if it was successful:

cargo run --bin starknet-compile --help

or

./target/release/starknet-compile --version

Execute .cairo Files#

Let's first write a test Cairo file. Create a new file in the current directory.
The file name is: hellostarknetastro.cairo

The content is:

use debug::PrintTrait;
fn main() {
    'Hello, StarknetAstro!'.print();
}

Execute the following command:

cargo run --bin cairo-run -- hellostarknetastro.cairo

Or use the compiled release to execute:

target/release/cairo-run hellostarknetastro.cairo

At this point, the terminal will output something like the following:

[DEBUG]  Hello, StarknetAstro!  (raw: 105807143882116536446217580363080108601441594273569)

Compile .cairo Files#

Cairo comes with some examples that we can compile using the following command:
First, create a folder in the Cairo root directory to save the output:

mkdir output

Then use cargo to compile:

cargo run --bin cairo-compile examples/fib.cairo output/fib.json

Or use:

target/release/cairo-compile examples/fib.cairo output/fib.json

What we output here is actually intermediate code, which Cairo calls Sierra.
If you want to output a file that can be directly executed on the Cairo-VM, we need to further compile Sierra into Cairo assembly (casm) files.

cargo run --bin sierra-compile -- output/fib.json output/fib.casm

Or:

target/release/sierra-compile -- output/fib.json output/fib.casm

Of course, generally speaking, you only need to compile Cairo contracts to casm when you need to deploy them to Starknet. We usually do not need to compile pure Cairo code to casm unless there is a special requirement.

Install Python#

The old Cairo-CLI requires Python version 3.9. To avoid conflicts with already installed versions, similar to Rust, we recommend using the Python version management tool pyenv to install Python.

MacOS:

brew update
brew install pyenv

Or:

curl https://pyenv.run | bash

Then:

pyenv install 3.9
pyenv global 3.9

Linux:

curl https://pyenv.run | bash

Then:

pyenv install 3.9
pyenv global 3.9

Verify if the installation was successful:

python3.9 --version

Or simply install a Python 3.9 version directly from:

https://www.python.org/downloads/release/python-3915/

Install CLI#

This CLI is used to deploy Starknet contracts. We need to install GMP environment support first.

Linux:

sudo apt install -y libgmp3-dev

MacOS:

brew install gmp

To facilitate operations, let's first enter the root directory of Cairo 1.0 (refer to your Cairo installation directory, for example, mine is as follows):

cd ~/cairo/

Create a camp1_lesson3 folder:

mkdir -p starknetastro/workshop/
cd starknetastro/workshop/

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 the terminal prefix with (venv). Let's install the CLI:

(venv) camp1 $ pip install cairo-lang==0.11.1.1

Check if the installation was successful:

(venv) camp1 $ starknet --version

It should output:

starknet 0.11.1.1

Common Issues#

Many students still cannot compile normally after installing Python. Here are some common solutions:

  1. linker 'cc' not found, the cc linker cannot be found, which can be solved with the following solution.
sudo apt install build-essential

Of course, to be safe, you can directly install a complete set:

sudo apt-get install build-essential libncursesw5-dev libgdbm-dev libc6-dev zlib1g-dev libsqlite3-dev tk-dev libssl-dev openssl libbz2-dev libreadline-dev
  1. ModuleNotFoundError: No module named '_ctypes'
    The solution is more complicated; you need to reinstall Python.
pyenv uninstall 3.9.16
sudo yum install libffi-devel
pyenv install 3.9.16
  1. use_2to3 is invalid. Sometimes this error occurs inexplicably.
pip install -U setuptools

Configure Starknet Testnet Account#

Next, we will 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 test with the following two commands:

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

Both output 1.1.0, indicating that the environment is configured correctly.

Create a Testnet Wallet#

Next, we will create a wallet on the testnet:

starknet new_account --account StarknetAstro

This 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 it requires gas. We can send some test ETH to 0xYourAddress through a cross-chain bridge or the official Faucet.

Screenshot 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 works normally. Execute the following commands:

mkdir src sierra
touch src/example.cairo

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

#[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 first:

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, we will 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 the deployed contract here.

ERC20 Code Template#

Save the following erc20.cairo in the src folder created above for easy terminal operations.

File name: erc20.cairo

use starknet::ContractAddress;

#[abi]
trait IERC20 {
    #[view]
    fn name() -> felt252;
    #[view]
    fn symbol() -> felt252;
    #[view]
    fn decimals() -> u8;
    #[view]
    fn totalSupply() -> u256;
    #[view]
    fn balanceOf(account: ContractAddress) -> u256;
    #[view]
    fn allowance(owner: ContractAddress, spender: ContractAddress) -> u256;
    #[external]
    fn transfer(recipient: ContractAddress, amount: u256) -> bool;
    #[external]
    fn transferFrom(sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool;
    #[external]
    fn approve(spender: ContractAddress, amount: u256) -> bool;
    #[external]
    fn increaseAllowance(spender: ContractAddress, added_value: u256) -> bool;
    #[external]
    fn decreaseAllowance(spender: ContractAddress, subtracted_value: u256) -> bool;
}

#[contract]
mod ERC20 {
    use super::IERC20;
    use integer::BoundedInt;
    use starknet::ContractAddress;
    use starknet::get_caller_address;
    use zeroable::Zeroable;

    struct Storage {
        _name: felt252,
        _symbol: felt252,
        _total_supply: u256,
        _balances: LegacyMap<ContractAddress, u256>,
        _allowances: LegacyMap<(ContractAddress, ContractAddress), u256>,
    }

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

    #[event]
    fn Approval(owner: ContractAddress, spender: ContractAddress, value: u256) {}

    impl ERC20 of IERC20 {
        fn name() -> felt252 {
            _name::read()
        }

        fn symbol() -> felt252 {
            _symbol::read()
        }

        fn decimals() -> u8 {
            18_u8
        }

        fn totalSupply() -> u256 {
            _total_supply::read()
        }

        fn balanceOf(account: ContractAddress) -> u256 {
            _balances::read(account)
        }

        fn allowance(owner: ContractAddress, spender: ContractAddress) -> u256 {
            _allowances::read((owner, spender))
        }

        fn transfer(recipient: ContractAddress, amount: u256) -> bool {
            let sender = get_caller_address();
            _transfer(sender, recipient, amount);
            true
        }

        fn transferFrom(
            sender: ContractAddress, recipient: ContractAddress, amount: u256
        ) -> bool {
            let caller = get_caller_address();
            _spend_allowance(sender, caller, amount);
            _transfer(sender, recipient, amount);
            true
        }

        fn approve(spender: ContractAddress, amount: u256) -> bool {
            let caller = get_caller_address();
            _approve(caller, spender, amount);
            true
        }

        fn increaseAllowance(spender: ContractAddress, added_value: u256) -> bool {
            _increase_allowance(spender, added_value)
        }

        fn decreaseAllowance(spender: ContractAddress, subtracted_value: u256) -> bool {
            _decrease_allowance(spender, subtracted_value)
        }
    }

    #[constructor]
    fn constructor(
        name: felt252, symbol: felt252, initial_supply: u256, recipient: ContractAddress
    ) {
        initializer(name, symbol);
        _mint(recipient, initial_supply);
    }

    #[view]
    fn name() -> felt252 {
        ERC20::name()
    }

    #[view]
    fn symbol() -> felt252 {
        ERC20::symbol()
    }

    #[view]
    fn decimals() -> u8 {
        ERC20::decimals()
    }

    #[view]
    fn totalSupply() -> u256 {
        ERC20::totalSupply()
    }

    #[view]
    fn balanceOf(account: ContractAddress) -> u256 {
        ERC20::balanceOf(account)
    }

    #[view]
    fn allowance(owner: ContractAddress, spender: ContractAddress) -> u256 {
        ERC20::allowance(owner, spender)
    }

    #[external]
    fn transfer(recipient: ContractAddress, amount: u256) -> bool {
        ERC20::transfer(recipient, amount)
    }

    #[external]
    fn transferFrom(sender: ContractAddress, recipient: ContractAddress, amount: u256) -> bool {
        ERC20::transferFrom(sender, recipient, amount)
    }

    #[external]
    fn approve(spender: ContractAddress, amount: u256) -> bool {
        ERC20::approve(spender, amount)
    }

    #[external]
    fn increaseAllowance(spender: ContractAddress, added_value: u256) -> bool {
        ERC20::increaseAllowance(spender, added_value)
    }

    #[external]
    fn decreaseAllowance(spender: ContractAddress, subtracted_value: u256) -> bool {
        ERC20::decreaseAllowance(spender, subtracted_value)
    }

    ///
    /// Internals
    ///

    #[internal]
    fn initializer(name_: felt252, symbol_: felt252) {
        _name::write(name_);
        _symbol::write(symbol_);
    }

    #[internal]
    fn _increase_allowance(spender: ContractAddress, added_value: u256) -> bool {
        let caller = get_caller_address();
        _approve(caller, spender, _allowances::read((caller, spender)) + added_value);
        true
    }

    #[internal]
    fn _decrease_allowance(spender: ContractAddress, subtracted_value: u256) -> bool {
        let caller = get_caller_address();
        _approve(caller, spender, _allowances::read((caller, spender)) - subtracted_value);
        true
    }

    #[internal]
    fn _mint(recipient: ContractAddress, amount: u256) {
        assert(!recipient.is_zero(), 'ERC20: mint to 0');
        _total_supply::write(_total_supply::read() + amount);
        _balances::write(recipient, _balances::read(recipient) + amount);
        Transfer(Zeroable::zero(), recipient, amount);
    }

    #[internal]
    fn _burn(account: ContractAddress, amount: u256) {
        assert(!account.is_zero(), 'ERC20: burn from 0');
        _total_supply::write(_total_supply::read() - amount);
        _balances::write(account, _balances::read(account) - amount);
        Transfer(account, Zeroable::zero(), amount);
    }

    #[internal]
    fn _approve(owner: ContractAddress, spender: ContractAddress, amount: u256) {
        assert(!owner.is_zero(), 'ERC20: approve from 0');
        assert(!spender.is_zero(), 'ERC20: approve to 0');
        _allowances::write((owner, spender), amount);
        Approval(owner, spender, amount);
    }

    #[internal]
    fn _transfer(sender: ContractAddress, recipient: ContractAddress, amount: u256) {
        assert(!sender.is_zero(), 'ERC20: transfer from 0');
        assert(!recipient.is_zero(), 'ERC20: transfer to 0');
        _balances::write(sender, _balances::read(sender) - amount);
        _balances::write(recipient, _balances::read(recipient) + amount);
        Transfer(sender, recipient, amount);
    }

    #[internal]
    fn _spend_allowance(owner: ContractAddress, spender: ContractAddress, amount: u256) {
        let current_allowance = _allowances::read((owner, spender));
        if current_allowance != BoundedInt::max() {
            _approve(owner, spender, current_allowance - amount);
        }
    }
}

Deploy ERC20#

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

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

Next is the declaration:

starknet declare --contract sierra/erc20.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).

Contract class hash: 0x580e15048e1a124d1ce0579c299391b8583bc167dc64768bb99f95af0d9e2b6
Transaction hash: 0x74680a930e3999195bb534c26b7a7c8ce00b357ddccd3681378a0d9b1e01d5c

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

starknet deploy --inputs 0x417374726F546F6B656E 0x417374726F 0x3e8 0x0 0x01d4557fb129128c4db7a1d8049cbba4779e5aa64798cec7aea1a477a489d695 --class_hash 0x0580e15048e1a124d1ce0579c299391b8583bc167dc64768bb99f95af0d9e2b6 --account StarknetAstro

Output:

Sending the transaction with max_fee: 0.000009 ETH (8827510645965 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x010d93ff677abe100d6577d5e96416a01aa5c212f8012b105444c3efde20a95f
Transaction hash: 0x6e87fd6b23af80f59e21127ad0e54e5526062970d4a420e8e79d7e6911f992

Great! You can see the deployed contract here.

ERC721 Code Template#

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

File name: erc721.cairo

use array::ArrayTrait;
use array::SpanTrait;
use option::OptionTrait;
use serde::Serde;
use serde::deserialize_array_helper;
use serde::serialize_array_helper;
use starknet::ContractAddress;

const IERC165_ID: u32 = 0x01ffc9a7_u32;
const IERC721_ID: u32 = 0x80ac58cd_u32;
const IERC721_METADATA_ID: u32 = 0x5b5e139f_u32;
const IERC721_RECEIVER_ID: u32 = 0x150b7a02_u32;

#[abi]
trait IERC721 {
    fn balance_of(account: ContractAddress) -> u256;
    fn owner_of(token_id: u256) -> ContractAddress;
    fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256);
    fn safe_transfer_from(
        from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252>
    );
    fn approve(to: ContractAddress, token_id: u256);
    fn set_approval_for_all(operator: ContractAddress, approved: bool);
    fn get_approved(token_id: u256) -> ContractAddress;
    fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool;
    // IERC721Metadata
    fn name() -> felt252;
    fn symbol() -> felt252;
    fn token_uri(token_id: u256) -> felt252;
}

#[abi]
trait IERC721Camel {
    fn balanceOf(account: ContractAddress) -> u256;
    fn ownerOf(tokenId: u256) -> ContractAddress;
    fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256);
    fn safeTransferFrom(
        from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span<felt252>
    );
    fn approve(to: ContractAddress, tokenId: u256);
    fn setApprovalForAll(operator: ContractAddress, approved: bool);
    fn getApproved(tokenId: u256) -> ContractAddress;
    fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool;
    // IERC721Metadata
    fn name() -> felt252;
    fn symbol() -> felt252;
    fn tokenUri(tokenId: u256) -> felt252;
}

#[abi]
trait IASTRONFT  {
    fn mint(to: ContractAddress, token_id: u256);
}

//
// ERC721Receiver
//

#[abi]
trait IERC721ReceiverABI {
    fn on_erc721_received(
        operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span<felt252>
    ) -> u32;
    fn onERC721Received(
        operator: ContractAddress, from: ContractAddress, tokenId: u256, data: Span<felt252>
    ) -> u32;
}

#[abi]
trait IERC721Receiver {
    fn on_erc721_received(
        operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span<felt252>
    ) -> u32;
}

#[abi]
trait IERC721ReceiverCamel {
    fn onERC721Received(
        operator: ContractAddress, from: ContractAddress, tokenId: u256, data: Span<felt252>
    ) -> u32;
}

#[abi]
trait ERC721ABI {
    // case agnostic
    #[view]
    fn name() -> felt252;
    #[view]
    fn symbol() -> felt252;
    #[external]
    fn approve(to: ContractAddress, token_id: u256);
    // snake_case
    #[view]
    fn balance_of(account: ContractAddress) -> u256;
    #[view]
    fn owner_of(token_id: u256) -> ContractAddress;
    #[external]
    fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256);
    #[external]
    fn safe_transfer_from(
        from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252>
    );
    #[external]
    fn set_approval_for_all(operator: ContractAddress, approved: bool);
    #[view]
    fn get_approved(token_id: u256) -> ContractAddress;
    #[view]
    fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool;
    #[view]
    fn token_uri(token_id: u256) -> felt252;
    // camelCase
    #[view]
    fn balanceOf(account: ContractAddress) -> u256;
    #[view]
    fn ownerOf(tokenId: u256) -> ContractAddress;
    #[external]
    fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256);
    #[external]
    fn safeTransferFrom(
        from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span<felt252>
    );
    #[external]
    fn setApprovalForAll(operator: ContractAddress, approved: bool);
    #[view]
    fn getApproved(tokenId: u256) -> ContractAddress;
    #[view]
    fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool;
    #[view]
    fn tokenUri(tokenId: u256) -> felt252;
}

#[abi]
trait ASTRONFTABI {
    #[external]
    fn mint(to: ContractAddress, token_id: u256);
}

#[contract]
mod ERC721 {
    use super::IERC721;
    use super::IERC721Camel;
    use super::IASTRONFT;

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

    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(name: felt252, symbol: felt252) {
        initializer(name, symbol);
    }

    impl IASTRONFTImpl of IASTRONFT {
        fn mint(to: ContractAddress, token_id: u256) {
            _mint(to, token_id);
        }
    }

    impl ERC721Impl of IERC721 {
        fn name() -> felt252 {
            _name::read()
        }

        fn symbol() -> felt252 {
            _symbol::read()
        }

        fn token_uri(token_id: u256) -> felt252 {
            assert(_exists(token_id), 'ERC721: invalid token ID');
            _token_uri::read(token_id)
        }

        fn balance_of(account: ContractAddress) -> u256 {
            assert(!account.is_zero(), 'ERC721: invalid account');
            _balances::read(account)
        }

        fn owner_of(token_id: u256) -> ContractAddress {
            _owner_of(token_id)
        }

        fn get_approved(token_id: u256) -> ContractAddress {
            assert(_exists(token_id), 'ERC721: invalid token ID');
            _token_approvals::read(token_id)
        }

        fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool {
            _operator_approvals::read((owner, operator))
        }

        fn approve(to: ContractAddress, token_id: u256) {
            let owner = _owner_of(token_id);

            let caller = get_caller_address();
            assert(
                owner == caller | is_approved_for_all(owner, caller), 'ERC721: unauthorized caller'
            );
            _approve(to, token_id);
        }

        fn set_approval_for_all(operator: ContractAddress, approved: bool) {
            _set_approval_for_all(get_caller_address(), operator, approved)
        }

        fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256) {
            assert(
                _is_approved_or_owner(get_caller_address(), token_id), 'ERC721: unauthorized caller'
            );
            _transfer(from, to, token_id);
        }

        fn safe_transfer_from(
            from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252>
        ) {
            assert(
                _is_approved_or_owner(get_caller_address(), token_id), 'ERC721: unauthorized caller'
            );
            _safe_transfer(from, to, token_id, data);
        }
    }

    impl ERC721CamelImpl of IERC721Camel {
        fn name() -> felt252 {
            ERC721Impl::name()
        }

        fn symbol() -> felt252 {
            ERC721Impl::symbol()
        }

        fn tokenUri(tokenId: u256) -> felt252 {
            ERC721Impl::token_uri(tokenId)
        }

        fn balanceOf(account: ContractAddress) -> u256 {
            ERC721Impl::balance_of(account)
        }

        fn ownerOf(tokenId: u256) -> ContractAddress {
            ERC721Impl::owner_of(tokenId)
        }

        fn approve(to: ContractAddress, tokenId: u256) {
            ERC721Impl::approve(to, tokenId)
        }

        fn getApproved(tokenId: u256) -> ContractAddress {
            ERC721Impl::get_approved(tokenId)
        }

        fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool {
            ERC721Impl::is_approved_for_all(owner, operator)
        }

        fn setApprovalForAll(operator: ContractAddress, approved: bool) {
            ERC721Impl::set_approval_for_all(operator, approved)
        }

        fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256) {
            ERC721Impl::transfer_from(from, to, tokenId)
        }

        fn safeTransferFrom(
            from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span<felt252>
        ) {
            ERC721Impl::safe_transfer_from(from, to, tokenId, data);
        }
    }

    // View

    #[view]
    fn supports_interface(interface_id: u32) -> bool {
        if super::IERC165_ID == interface_id {
            return true;
        } else if super::IERC721_METADATA_ID == interface_id {
            return true;
        } else {
            return super::IERC721_ID == interface_id;
        }
    }

    #[view]
    fn supportsInterface(interfaceId: u32) -> bool {
        if super::IERC165_ID == interfaceId {
            return true;
        } else if super::IERC721_METADATA_ID == interfaceId {
            return true;
        } else {
            return super::IERC721_ID == interfaceId;
        }
    }

    #[view]
    fn name() -> felt252 {
        ERC721Impl::name()
    }

    #[view]
    fn symbol() -> felt252 {
        ERC721Impl::symbol()
    }

    #[view]
    fn token_uri(token_id: u256) -> felt252 {
        ERC721Impl::token_uri(token_id)
    }

    #[view]
    fn tokenUri(tokenId: u256) -> felt252 {
        ERC721CamelImpl::tokenUri(tokenId)
    }

    #[view]
    fn balance_of(account: ContractAddress) -> u256 {
        ERC721Impl::balance_of(account)
    }

    #[view]
    fn balanceOf(account: ContractAddress) -> u256 {
        ERC721CamelImpl::balanceOf(account)
    }

    #[view]
    fn owner_of(token_id: u256) -> ContractAddress {
        ERC721Impl::owner_of(token_id)
    }

    #[view]
    fn ownerOf(tokenId: u256) -> ContractAddress {
        ERC721CamelImpl::ownerOf(tokenId)
    }

    #[view]
    fn get_approved(token_id: u256) -> ContractAddress {
        ERC721Impl::get_approved(token_id)
    }

    #[view]
    fn getApproved(tokenId: u256) -> ContractAddress {
        ERC721CamelImpl::getApproved(tokenId)
    }

    #[view]
    fn is_approved_for_all(owner: ContractAddress, operator: ContractAddress) -> bool {
        ERC721Impl::is_approved_for_all(owner, operator)
    }

    #[view]
    fn isApprovedForAll(owner: ContractAddress, operator: ContractAddress) -> bool {
        ERC721CamelImpl::isApprovedForAll(owner, operator)
    }

    // External

    #[external]
    fn approve(to: ContractAddress, token_id: u256) {
        ERC721Impl::approve(to, token_id)
    }

    #[external]
    fn set_approval_for_all(operator: ContractAddress, approved: bool) {
        ERC721Impl::set_approval_for_all(operator, approved)
    }

    #[external]
    fn setApprovalForAll(operator: ContractAddress, approved: bool) {
        ERC721CamelImpl::setApprovalForAll(operator, approved)
    }

    #[external]
    fn transfer_from(from: ContractAddress, to: ContractAddress, token_id: u256) {
        ERC721Impl::transfer_from(from, to, token_id)
    }

    #[external]
    fn transferFrom(from: ContractAddress, to: ContractAddress, tokenId: u256) {
        ERC721CamelImpl::transferFrom(from, to, tokenId)
    }

    #[external]
    fn safe_transfer_from(
        from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252>
    ) {
        ERC721Impl::safe_transfer_from(from, to, token_id, data);
    }

    #[external]
    fn safeTransferFrom(
        from: ContractAddress, to: ContractAddress, tokenId: u256, data: Span<felt252>
    ) {
        ERC721CamelImpl::safeTransferFrom(from, to, tokenId, data);
    }

    #[external]
    fn mint(to: ContractAddress, token_id: u256) {
            IASTRONFTImpl::mint(to, token_id);
    }
    
    // Internal

    #[internal]
    fn initializer(name_: felt252, symbol_: felt252) {
        _name::write(name_);
        _symbol::write(symbol_);
    }

    #[internal]
    fn _owner_of(token_id: u256) -> ContractAddress {
        let owner = _owners::read(token_id);
        match owner.is_zero() {
            bool::False(()) => owner,
            bool::True(()) => panic_with_felt252('ERC721: invalid token ID')
        }
    }

    #[internal]
    fn _exists(token_id: u256) -> bool {
        !_owners::read(token_id).is_zero()
    }

    #[internal]
    fn _is_approved_or_owner(spender: ContractAddress, token_id: u256) -> bool {
        let owner = _owner_of(token_id);
        owner == spender | is_approved_for_all(owner, spender) | spender == get_approved(token_id)
    }

    #[internal]
    fn _approve(to: ContractAddress, token_id: u256) {
        let owner = _owner_of(token_id);
        assert(owner != to, 'ERC721: approval to owner');
        _token_approvals::write(token_id, to);
        Approval(owner, to, token_id);
    }

    #[internal]
    fn _set_approval_for_all(owner: ContractAddress, operator: ContractAddress, approved: bool) {
        assert(owner != operator, 'ERC721: self approval');
        _operator_approvals::write((owner, operator), approved);
        ApprovalForAll(owner, operator, approved);
    }

    #[internal]
    fn _mint(to: ContractAddress, token_id: u256) {
        assert(!to.is_zero(), 'ERC721: invalid receiver');
        assert(!_exists(token_id), 'ERC721: token already minted');

        // Update balances
        _balances::write(to, _balances::read(to) + 1.into());

        // Update token_id owner
        _owners::write(token_id, to);

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

    #[internal]
    fn _transfer(from: ContractAddress, to: ContractAddress, token_id: u256) {
        assert(!to.is_zero(), 'ERC721: invalid receiver');
        let owner = _owner_of(token_id);
        assert(from == owner, 'ERC721: wrong sender');

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

        // Update balances
        _balances::write(from, _balances::read(from) - 1.into());
        _balances::write(to, _balances::read(to) + 1.into());

        // Update token_id owner
        _owners::write(token_id, to);

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

    #[internal]
    fn _burn(token_id: u256) {
        let owner = _owner_of(token_id);

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

        // Update balances
        _balances::write(owner, _balances::read(owner) - 1.into());

        // Delete owner
        _owners::write(token_id, Zeroable::zero());

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

    #[internal]
    fn _safe_mint(to: ContractAddress, token_id: u256, data: Span<felt252>) {
        _mint(to, token_id);
    }

    #[internal]
    fn _safe_transfer(
        from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252>
    ) {
        _transfer(from, to, token_id);
    }

    #[internal]
    fn _set_token_uri(token_id: u256, token_uri: felt252) {
        assert(_exists(token_id), 'ERC721: invalid token ID');
        _token_uri::write(token_id, token_uri)
    }
}

impl SpanSerde<
    T, impl TSerde: Serde<T>, impl TCopy: Copy<T>, impl TDrop: Drop<T>
> of Serde<Span<T>> {
    fn serialize(self: @Span<T>, ref output: Array<felt252>) {
        (*self).len().serialize(ref output);
        serialize_array_helper(*self, ref output);
    }
    fn deserialize(ref serialized: Span<felt252>) -> Option<Span<T>> {
        let length = *serialized.pop_front()?;
        let mut arr = ArrayTrait::new();
        Option::Some(deserialize_array_helper(ref serialized, arr, length)?.span())
    }
}

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 ERC721 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 (1378300012405 WEI).
Declare transaction was sent.
Contract class hash: 0x49f6ecc90497560cb72901c514407cb91a42cc5d8868b2d348ba0264067c94c
Transaction hash: 0x24999e08268d91439d75c251bf0e86ad4abc633d9f03ac2ca1eb00d421d3c54

Next, we will deploy the instance of this ERC721 contract. 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 0x49f6ecc90497560cb72901c514407cb91a42cc5d8868b2d348ba0264067c94c --account StarknetAstro

Output:

Sending the transaction with max_fee: 0.000006 ETH (6121500055094 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x06722e748113c467542ad7f9985fdf2dd81c2b92088fc832dc7845c13d2eff41
Transaction hash: 0x40bd533e6e2503f05c20f1abfc584aaf03075dee9659278ec6674195f4d2795

Great! You can see the deployed contract here.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.