注意!Cairo 言語の変更により、このチュートリアルは古くなっています。1.10 の構文については、こちらの文を参照してください https://starknetastro.xlog.app/Starknet_Shanghai_Workshop_DAY1#
概要#
本コースは、迅速に Cairo 言語を習得するための 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
に保存して、ターミナルを閉じた後でも環境変数を使用できるようにします。方法についてはここでは詳しく説明しません。
以下の 2 つのコマンドでテストします。
(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あなたの公開鍵
適切な金額の資金をアカウントに移動し、'starknet deploy_account'コマンドを実行してアカウントをデプロイしてください。
注意:これはOpenZeppelinアカウントコントラクトの修正版です。署名の計算方法が異なります。
次に、テストネットにデプロイする必要があります。Starknet でのデプロイもトランザクションの一部であるため、ガスが必要です。クロスチェーンブリッジまたは公式 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
にテスト内容を入力します:
#[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”}.
次に、コントラクトのインスタンスを実際にデプロイします:
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 {
// その他
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');
// 暗黙的に承認をクリアし、イベントを発火する必要はありません
self.token_approvals.write(token_id, Zeroable::zero());
// 残高を更新
self.balances.write(from, self.balances.read(from) - 1);
self.balances.write(to, self.balances.read(to) + 1);
// token_idの所有者を更新
self.owners.write(token_id, to);
// イベントを発火
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');
// 残高を更新
self.balances.write(to, self.balances.read(to) + 1);
// token_idの所有者を更新
self.owners.write(token_id, to);
// イベントを発火
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
次に、この erc721 コントラクトのインスタンスをデプロイします。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
大成功!ここで私たちのデプロイしたコントラクトを見ることができます。