簡介#
本課程作為一個快速上手的 Workshop 將不會過於深入的講解 Cairo 語言的特性(但我們在線 Bootcamp 最後一節課將深入的介紹一下 Cairo 語言其他有用的部分)。
如果你想提前深入學習 Cairo,可以查看我們最近翻譯的Cairo-Book 中文版 。
環境配置#
最小安裝選項:
系統:curl,git
IDE:VSCode 或任何你喜歡的編輯器(唯獨不要使用 windows 自帶的 notepad)
MacOS:homebrew
Cairo-Lang CLI。
安裝 Rust#
建議通過 rustup (docs) 來安裝 rust。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 上 Clone 最新的 Cairo repo
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
或者使用上面編譯好的 relese 中的來進行執行
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
或者直接簡單的安裝一個 py3.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 後依然無法正常編譯,在這裡給出一些常見問題的解決方法:
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
ModuleNotFoundError: No module named '_ctypes'
解決方案比較麻煩,要重新安裝 python。
pyenv uninstall 3.9.16
sudo yum install libffi-devel
pyenv install 3.9.16
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
,方便關閉 terminal 之後依然可以使用環境變量。方法此處不再贅述。
我們用以下兩個命令來測試
(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你的公鑰
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。我們通過跨鏈橋或者官方 Faucet給0x你的地址
发送一些測試用 eth。
然後使用此命令部署
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”}.
接著我們來實際部署合約的 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
這表明實際部署成功了!你可以在這裡看到我們部署的合約。
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
接著則是 Deploy 這個 erc20 合約的 instance。注意 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;
// 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())
}
}
部署 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
接著則是 Deploy 這個 erc721 合約的 instance。注意 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
大功告成!你可以在這裡看到我們 部署的合約