注意!由於 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 main
git pull
cargo build --all --release
此外,根據上節課的反饋,很多同學在安裝 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
安裝 CLI#
為了方便操作讓我們先進入 Cairo 1.0 的根目錄(此處參照你上節課裝 1.0 時的目錄,比如我的就是如下所示)
cd ~/cairo/
創建一個 test 文件夾 ():
mkdir -p starknetastro/camp1_lesson2/
cd starknetastro/camp1_lesson2/
接著創建一個 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。我們通過跨鏈橋或者官方 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 {
#[starknet::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;
#[starknet::interface]
trait IERC20<TStorage> {
fn name(self: @TStorage) -> felt252;
fn symbol(self: @TStorage) -> felt252;
fn decimals(self: @TStorage) -> u8;
fn totalSupply(self: @TStorage) -> u256;
fn balanceOf(self: @TStorage, account: ContractAddress) -> u256;
fn allowance(self: @TStorage, owner: ContractAddress, spender: ContractAddress) -> u256;
fn transfer(ref self: TStorage, recipient: ContractAddress, amount: u256);
fn transferFrom(
ref self: TStorage, sender: ContractAddress, recipient: ContractAddress, amount: u256
);
fn approve(ref self: TStorage, spender: ContractAddress, amount: u256);
fn increase_allowance(ref self: TStorage, spender: ContractAddress, added_value: u256);
fn decrease_allowance(ref self: TStorage, spender: ContractAddress, subtracted_value: u256);
}
#[contract]
mod ERC20 {
use zeroable::Zeroable;
use starknet::get_caller_address;
use starknet::contract_address_const;
use starknet::ContractAddress;
#[starknet::storage]
struct Storage {
name: felt252,
symbol: felt252,
decimals: u8,
totalSupply: 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) {}
#[constructor]
fn constructor(
ref self: Storage,
name: felt252,
symbol: felt252,
decimals: u8,
initialSupply: u256,
recipient: ContractAddress
) {
self.name.write(name);
self.symbol.write(symbol);
self.decimals.write(decimals);
assert(!recipient.is_zero(), 'ERC20: mint to the 0 address');
self.totalSupply.write(initialSupply);
self.balances.write(recipient, initialSupply);
}
#[external]
impl IERC20Impl of super::IERC20<Storage> {
fn name(self: @Storage) -> felt252 {
self.name.read()
}
fn symbol(self: @Storage) -> felt252 {
self.symbol.read()
}
fn decimals(self: @Storage) -> u8 {
self.decimals.read()
}
fn totalSupply(self: @Storage) -> u256 {
self.totalSupply.read()
}
fn balanceOf(self: @Storage, account: ContractAddress) -> u256 {
self.balances.read(account)
}
fn allowance(self: @Storage, owner: ContractAddress, spender: ContractAddress) -> u256 {
self.allowances.read((owner, spender))
}
fn transfer(ref self: Storage, recipient: ContractAddress, amount: u256) {
let sender = get_caller_address();
self.transfer_helper(sender, recipient, amount);
}
fn transferFrom(
ref self: Storage, sender: ContractAddress, recipient: ContractAddress, amount: u256
) {
let caller = get_caller_address();
self.transfer_helper(sender, recipient, amount);
}
fn approve(ref self: Storage, spender: ContractAddress, amount: u256) {
let caller = get_caller_address();
self.approve_helper(caller, spender, amount);
}
fn increase_allowance(ref self: Storage, spender: ContractAddress, added_value: u256) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) + added_value
);
}
fn decrease_allowance(ref self: Storage, spender: ContractAddress, subtracted_value: u256) {
let caller = get_caller_address();
self
.approve_helper(
caller, spender, self.allowances.read((caller, spender)) - subtracted_value
);
}
}
#[generate_trait]
impl StorageImpl of StorageTrait {
fn transfer_helper(
ref self: Storage, sender: ContractAddress, recipient: ContractAddress, amount: u256
) {
assert(!sender.is_zero(), 'ERC20: transfer from 0');
assert(!recipient.is_zero(), 'ERC20: transfer to 0');
self.balances.write(sender, self.balances.read(sender) - amount);
self.balances.write(recipient, self.balances.read(recipient) + amount);
Transfer(sender, recipient, amount);
}
fn approve_helper(
ref self: Storage, owner: ContractAddress, spender: ContractAddress, amount: u256
) {
assert(!spender.is_zero(), 'ERC20: approve from 0');
self.allowances.write((owner, spender), amount);
Approval(owner, spender, amount);
}
}
}
部署 ERC20#
首先我們依然需要編譯 cairo 文件到 sierra
$CAIRO_COMPILER_DIR/starknet-compile src/erc20.cairo sierra/erc20.json
接下來是聲明
starknet declare --contract sierra/erc20.json --account StarknetAstro
這次的輸出不一樣了,因為我們編寫的這個 erc20 文件還沒有被人部署過,因此不會出現錯誤(當然,照著課程做的同學們還是會遇上已經被聲明的錯誤)。
Sending the transaction with max_fee: 0.000101 ETH (101461098713976 WEI).
Declare transaction was sent.
Contract class hash: 0x5d144ad5b9af2752da0ef741b565d334a1cb18d02224462d978ebf0a6e8e919
Transaction hash: 0x21da071ce36db1544f482150a8b99902ca1b5db090b66618de724362408ea00
接著則是 Deploy 這個 erc20 合約的 instance。注意 erc20 代碼中的構造函數是有參數的,因此我們也需要給出相應的參數:
starknet deploy --inputs 0x417374726F546F6B656E 0x417374726F 0x12 0x3e8 0x0 0x04202409943437c6ebe83697e1ba183976912b667066db7b8796064e59426b0e --class_hash 0x5d144ad5b9af2752da0ef741b565d334a1cb18d02224462d978ebf0a6e8e919 --account StarknetAstro
輸出為:
Sending the transaction with max_fee: 0.000791 ETH (790973560660141 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x059128b7b3001106211376c12ee86feedd9247033cd1a771e8957efac3b61ca8
Transaction hash: 0x4bcf2d5f9a2e2090755d7a07d2849d4e5cf13fbdd36befd4f73d2030cea392c
大功告成!你可以在這裡看到我們 部署的合約
)。