StarknetAstro

StarknetAstro

(Starknet Shanghai Workshop)CairoにおけるERC標準の実装:環境設定および実践

52994791718314e29c662ab33aafc40c

概要#

本コースは、Cairo 言語の特性を深く掘り下げることなく、迅速に学べるワークショップとして提供されます(ただし、オンラインブートキャンプの最後のセッションでは、Cairo 言語の他の有用な部分について詳しく紹介します)。

Cairo を事前に深く学びたい場合は、最近翻訳したCairo-Book 日本語版を参照してください。

環境設定#

最小インストールオプション:

システム:curl、git
IDE:VSCode またはお好みのエディタ(Windows に付属の notepad は使用しないでください)
MacOS:homebrew
Cairo-Lang CLI。

Rust のインストール#

rust をインストールするには、rustup (docs) を使用することをお勧めします。rustup は rust のバージョンを切り替えたり、アップグレードしたりできます。

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

その後、コマンドラインターミナルを再起動して、インストールが成功したかどうかを確認するか、ターミナル内で以下のコマンドを実行します:

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

バージョンを確認します:

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

curl がインストールされていない場合は、ここから rust のさまざまなシステムのインストーラをダウンロードできます。

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

Cairo のインストール#

ターミナルで以下のコマンドを入力して、Github から最新の Cairo リポジトリをクローンします:

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

次に、安定版の 1.10 を使用します:

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

成功したかどうかをテストします:

cargo run --bin starknet-compile --help

または

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

.cairo ファイルの実行#

まず、テスト用の Cairo ファイルを作成しましょう。現在のディレクトリに新しいファイルを作成します。
ファイル名は:hellostarknetastro.cairo

内容は以下の通りです:

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

次のコマンドを実行します:

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

または、上記でコンパイルしたリリースを使用して実行します:

target/release/cairo-run hellostarknetastro.cairo

この時、ターミナルには以下のような内容が出力されます:

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

.cairo ファイルのコンパイル#

Cairo にはいくつかのサンプルが付属しており、次のコマンドでコンパイルできます:
まず、Cairo のルートディレクトリに出力用のフォルダを作成します:

mkdir output

その後、cargo を使用してコンパイルします:

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

または

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

ここで出力されるのは中間コードで、Cairo ではこれを Sierra と呼びます。Cairo-VM で直接実行できるファイルを出力したい場合は、Sierra を Cairo アセンブリ(casm)ファイルにさらにコンパイルする必要があります。

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

または

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

一般的には、starknet にデプロイする必要がある場合にのみ、Cairo コントラクトを casm にコンパイルする必要があります。特別な要件がない限り、単純な Cairo コードを casm にコンパイルする必要はありません。

Python のインストール#

古い Cairo-CLI には Python 3.9 が必要です。既にインストールされているものと衝突しないように、Rust と同様に、Python のバージョン管理ツール pyenv を使用して Python をインストールすることをお勧めします。

MacOS:

brew update
brew install pyenv

または

curl https://pyenv.run | bash

その後

pyenv install 3.9
pyenv global 3.9

Linux:

curl https://pyenv.run | bash

その後

pyenv install 3.9
pyenv global 3.9

インストールが成功したかどうかを確認します:

python3.9 --version

または、直接 Python 3.9 のバージョンを簡単にインストールします:
https://www.python.org/downloads/release/python-3915/

CLI のインストール#

この CLI は starknet コントラクトをデプロイするために使用されます。まず、GMP 環境サポートをインストールする必要があります。

Linux:

sudo apt install -y libgmp3-dev

MacOS:

brew install gmp

操作を簡単にするために、まず Cairo 1.0 のルートディレクトリに移動します(ここでは Cairo のインストールディレクトリを参照します。例えば、私のものは以下のようになります):

cd ~/cairo/

camp1_lesson3フォルダを作成します:

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

次に、Python の仮想環境を作成します:

python3.9 -m venv  venv

仮想環境を起動します:

source venv/bin/activate

この時、ターミナルの前に(venv)が表示されるはずです。CLI をインストールしましょう:

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

インストールが成功したかどうかを確認します:

(venv) camp1 $ starknet --version

ここで出力されるべきは:

starknet 0.11.1.1

よくある問題#

多くの学生が Python をインストールした後も正常にコンパイルできないことがあります。ここではいくつかの一般的な問題の解決方法を示します:

  1. linker 'cc' not found、cc のリンクが見つからない場合、以下の方法で解決できます。
sudo apt install build-essential

もちろん、万が一に備えて、以下のように一式をインストールすることもできます:

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'
    解決策は少し面倒で、Python を再インストールする必要があります。
pyenv uninstall 3.9.16
sudo yum install libffi-devel
pyenv install 3.9.16
  1. use_2to3 is invalid.時々、理由もなく発生することがあります。
pip install -U setuptools

Starknet テストネットアカウントの設定#

次に、テストネットアカウントを設定します。まず、以下の環境変数を定義します:

# テストネットを指定
export STARKNET_NETWORK=alpha-goerli

# デフォルトのウォレット実装を設定
export STARKNET_WALLET=starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount

# コンパイラのパスをCairo 1のパスに指定(自分のパスに応じて修正してください)
export CAIRO_COMPILER_DIR=~/cairo/target/release/

その後、これを.zshrcまたは.bashrcに保存して、ターミナルを閉じた後でも環境変数を使用できるようにします。方法についてはここでは詳しく説明しません。

以下の 2 つのコマンドでテストします:

(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

両方とも 1.1.0 が出力されれば、環境設定は正しいです。

テストネットのウォレットを作成#

次に、テストネット上にウォレットを作成します:

starknet new_account --account StarknetAstro

ここでは以下のような出力が表示されるはずです:

Account address: 0xあなたのアドレス
Public key: 0xあなたの公開鍵

適切な金額の資金をアカウントに移動し、'starknet deploy_account'コマンドを実行してアカウントをデプロイしてください。

注意:これはOpenZeppelinアカウントコントラクトの修正版です。署名は異なる方法で計算されます。

次に、テストネットにデプロイする必要があります。Starknet でのデプロイもトランザクションの一部であるため、ガスが必要です。クロスチェーンブリッジまたは公式 Faucetを通じて0xあなたのアドレスにいくつかのテスト用 eth を送信します。

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

次のコマンドを使用してデプロイします:

starknet deploy_account --account StarknetAstro

出力:

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

Contract address: 0xあなたのアドレス
Transaction hash: 0xトランザクションアドレス

デプロイのテスト#

まず、テストコントラクトを使用して正常にデプロイできるかどうかを確認します。次のコマンドを実行します:

mkdir src sierra
touch src/example.cairo

その後、src/example.cairoにテスト内容を記入します:

#[contract]

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

次に、Cairo ファイルを Sierra ファイルにコンパイルします:

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

コンパイルが成功した後、コントラクトの ClassHash を宣言する必要があります:

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

もちろん、同じ ClassHash は複数回宣言できないため、このテスト用コントラクトはすでに宣言されているため、すでに宣言されたというエラー出力が表示されることがありますが、これは問題ありません:

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”}.

次に、コントラクトのインスタンスを実際にデプロイします:

starknet deploy --class_hash 0x695874cd8feed014ebe379df39aa0dcef861ff495cc5465e84927377fa8e7e6 --account StarknetAstro

出力は以下の通りです:

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

これにより、実際にデプロイが成功したことが示されます!ここで私たちがデプロイしたコントラクトを見ることができます。

ERC20 コードテンプレート#

以下の erc20.cairo を上記で作成したsrcに保存し、ターミナル操作を便利にします。

ファイル名: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);
        }
    }
}

ERC20 のデプロイ#

まず、Cairo ファイルを Sierra にコンパイルする必要があります:

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

次に、宣言します:

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

今回の出力は異なります。なぜなら、私たちが作成したこの erc20 ファイルはまだ誰もデプロイしていないため、エラーは発生しません(もちろん、コースに従っている学生はすでに宣言されたエラーに遭遇するかもしれません)。

Contract class hash: 0x580e15048e1a124d1ce0579c299391b8583bc167dc64768bb99f95af0d9e2b6
Transaction hash: 0x74680a930e3999195bb534c26b7a7c8ce00b357ddccd3681378a0d9b1e01d5c

次に、この erc20 コントラクトのインスタンスをデプロイします。erc20 コードのコンストラクタにはパラメータがあるため、対応するパラメータを指定する必要があります:

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

出力は:

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

大成功です!ここで私たちがデプロイしたコントラクトを見ることができます。

ERC721 コードテンプレート#

以下の erc721.cairo を上記で作成したsrcに保存し、ターミナル操作を便利にします。

ファイル名: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;

    // その他
    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]
    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]
    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]
    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');

        // バランスを更新
        _balances::write(to, _balances::read(to) + 1.into());

        // token_idの所有者を更新
        _owners::write(token_id, to);

        // イベントを発火
        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');

        // 暗黙的に承認をクリアし、イベントを発火する必要はありません
        _token_approvals::write(token_id, Zeroable::zero());

        // バランスを更新
        _balances::write(from, _balances::read(from) - 1.into());
        _balances::write(to, _balances::read(to) + 1.into());

        // token_idの所有者を更新
        _owners::write(token_id, to);

        // イベントを発火
        Transfer(from, to, token_id);
    }

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

        // 暗黙的に承認をクリアし、イベントを発火する必要はありません
        _token_approvals::write(token_id, Zeroable::zero());

        // バランスを更新
        _balances::write(owner, _balances::read(owner) - 1.into());

        // 所有者を削除
        _owners::write(token_id, Zeroable::zero());

        // イベントを発火
        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())
    }
}

ERC721 のデプロイ#

まず、Cairo ファイルを Sierra にコンパイルする必要があります:

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

次に、宣言します:

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

今回の出力は異なります。なぜなら、私たちが作成したこの erc721 ファイルはまだ誰もデプロイしていないため、エラーは発生しません(もちろん、コースに従っている学生はすでに宣言されたエラーに遭遇するかもしれません)。

Sending the transaction with max_fee: 0.000001 ETH (1378300012405 WEI).
Declare transaction was sent.
Contract class hash: 0x49f6ecc90497560cb72901c514407cb91a42cc5d8868b2d348ba0264067c94c
Transaction hash: 0x24999e08268d91439d75c251bf0e86ad4abc633d9f03ac2ca1eb00d421d3c54

次に、この erc721 コントラクトのインスタンスをデプロイします。erc721 コードのコンストラクタにはパラメータがあるため、対応するパラメータを指定する必要があります:

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

出力は:

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

大成功です!ここで私たちがデプロイしたコントラクトを見ることができます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。