StarknetAstro

StarknetAstro

(BootCamp Lesson3)在 Starknet 測試網上部署 ERC-721 代幣

2023-06-11 20.15.07

注意!由於 Cairo 語言變化,此教程已經過期。1.10 語法請參考此文 https://starknetastro.xlog.app/Starknet_Shanghai_Workshop_DAY1#

簡介#

本課程作為一個快速上手的 BootCamp 將不會過於深入的講解 Cairo 語言的特性(但我們最後一節課將稍微深入的介紹一下 Cairo 語言其他有用的部分)。

如果你想提前深入學習 Cairo,可以查看我們最近翻譯的Cairo-Book 中文版

環境配置#

最小安裝選項:
這次的課程要求你已經安裝了我們在第一節課 所要求安裝的 Cairo 1.0,以及 Starknet 的 Cairo 0.x CLI 的所有所需依賴。
注意!由於 DevNet 的升級,導致現在只支持starknet-compile 1.1.0,因此你需要將 Cairo 1.0 升級到 v1.1.0。
在你安裝 Cairo 的目錄下執行如下命令:

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

注意:以下步驟和上節課類似。已經完成配置的同學可以直接跳轉到ERC721 代碼模版

安裝 CLI#

為了方便操作讓我們先進入 Cairo 1.0 的根目錄(此處參照你上節課裝 1.0 時的目錄,比如我的就是如下所示)

cd ~/cairo/

創建一個 camp1_lesson3 文件夾:

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

接著創建一個 python 虛擬環境:

python3.9 -m venv  venv

啟動虛擬環境:

source venv/bin/activate

此時你應該可以看到終端的前面帶上了一个(venv)。讓我們安裝 CLI

(venv) camp1 $ pip install cairo-lang

檢驗是否安裝成功

(venv) camp1 $ starknet --version

這裡應該輸出:

starknet 0.11.2

配置 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,方便關閉 terminal 之後依然可以使用環境變量。方法此處不再贅述。

我們用以下兩個命令來測試

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

兩者都輸出 1.1.0 表示環境配置正確。

創建測試網的錢包#

接著我們來創建一個測試網上的錢包:

starknet new_account --account StarknetAstro

這裡應該會輸出

Account address: 0x你的地址
Public key: 0x你的公鑰

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.

接著我們需要在測試網上部署它。在 Starknet 中部署也是一次交易,所以需要 gas。我們通過跨鏈橋或者官方 Faucet0x你的地址 发送一些測試用 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 中填入測試內容:

#[starknet::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”}.

接著我們來實際部署合約的 instance:

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

這表明實際部署成功了!你可以在這裡看到我們部署的合約

ERC721 代碼模版#

將下面的 erc721.cairo 保存在上面創建的 src中 ,方便終端操作。

文件名: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);
    }

}

## 部署ERC721

首先我們依然需要編譯cairo文件到sierra

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

接下來是聲明

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

這次的輸出不一樣了,因為我們編寫的這個erc20文件還沒有被人部署過,因此不會出現錯誤(當然,照著課程做的同學們還是會遇上已經被聲明的錯誤)。

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


接著則是Deploy這個erc721合約的instance。注意erc721代碼中的構造函數是有參數的,因此我們也需要給出相應的參數:

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


輸出為:

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

大功告成!你可以在這裡看到我們 [部署的合約](https://testnet.starkscan.co/contract/0x051566565f452d8dc645f7aaa4f232c935ce0f89d98f7663c094261c04748dfb)
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。