Note! This tutorial is outdated due to changes in the Cairo language. Please refer to this document for version 1.10 syntax: https://starknetastro.xlog.app/Starknet_Shanghai_Workshop_DAY1#
Introduction#
This course serves as a quick-start BootCamp and will not delve too deeply into the features of the Cairo language (but our last class will introduce some other useful parts of the Cairo language in more depth).
If you want to study Cairo in depth in advance, you can check out our recently translated Cairo-Book Chinese version.
Environment Configuration#
Minimum installation options:
This course requires you to have installed Cairo 1.0 as required in the first class, as well as all necessary dependencies for the Starknet Cairo 0.x CLI.
Note! Due to the upgrade of DevNet, only starknet-compile 1.1.0
is now supported, so you need to upgrade Cairo 1.0 to v1.1.0.
Execute the following commands in the directory where you installed Cairo:
git checkout 4020f7b3
git pull
cargo build --all --release
Note: The following steps are similar to the last class. Students who have completed the configuration can directly jump to ERC721 code template
Install CLI#
To facilitate operations, let's first enter the root directory of Cairo 1.0 (refer to the directory where you installed 1.0 in the last class, for example, mine is as shown below)
cd ~/cairo/
Create a camp1_lesson3 folder:
mkdir -p starknetastro/camp1_lesson3/
cd starknetastro/camp1_lesson3/
Next, create a Python virtual environment:
python3.9 -m venv venv
Activate the virtual environment:
source venv/bin/activate
At this point, you should see that the terminal has a (venv) prefix. Let's install the CLI
(venv) camp1 $ pip install cairo-lang
Check if the installation was successful
(venv) camp1 $ starknet --version
It should output:
starknet 0.11.2
Configure Starknet Testnet Account#
Next, let's configure the testnet account. First, define a few environment variables as follows:
# Specify the testnet
export STARKNET_NETWORK=alpha-goerli
# Set the default wallet implementation
export STARKNET_WALLET=starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount
# Specify the compiler path as the path for Cairo 1 (please modify according to your own path)
export CAIRO_COMPILER_DIR=~/cairo/target/release/
You can save this in your .zshrc
or .bashrc
so that you can still use the environment variables after closing the terminal. The method will not be elaborated here.
We will use the following two commands to test
(venv) $ $CAIRO_COMPILER_DIR/cairo-compile --version
cairo-compile 1.1.0
(venv) $ $CAIRO_COMPILER_DIR/starknet-compile --version
starknet-compile 1.1.0
Both should output 1.1.0, indicating that the environment configuration is correct.
Create a Testnet Wallet#
Next, let's create a wallet on the testnet:
starknet new_account --account StarknetAstro
It should output
Account address: 0xYourAddress
Public key: 0xYourPublicKey
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.
Next, we need to deploy it on the testnet. Deploying in Starknet is also a transaction, so gas is required. We can send some test ETH to 0xYourAddress
via a cross-chain bridge or the official Faucet.
Then use this command to deploy
starknet deploy_account --account StarknetAstro
Output:
Sending the transaction with max_fee: 0.000114 ETH (113796902644445 WEI).
Sent deploy account contract transaction.
Contract address: 0xYourAddress
Transaction hash: 0xTransactionAddress
Deployment Test#
Let's first try to deploy a test contract to see if it can be deployed normally.
Execute the following commands:
mkdir src sierra
touch src/example.cairo
Then, fill in the test content in src/example.cairo
:
#[starknet::contract]
mod SimpleStorage {
#[storage]
struct Storage {
}
}
Next, we will compile the Cairo file into a Sierra file:
$CAIRO_COMPILER_DIR/starknet-compile src/example.cairo sierra/example.json
After successful compilation, we need to declare the contract's ClassHash:
starknet declare --contract sierra/example.json --account StarknetAstro
Of course, since the same ClassHash cannot be declared multiple times, this test contract has already been declared, so you will get an error output indicating that it has already been declared, which is not a problem:
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”}.
Next, let's actually deploy the contract instance:
starknet deploy --class_hash 0x695874cd8feed014ebe379df39aa0dcef861ff495cc5465e84927377fa8e7e6 --account StarknetAstro
Output:
Sending the transaction with max_fee: 0.000132 ETH (132082306595047 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x060e17c12d4e3fee8af2e28e6a310a3192a1b1190d060cb4324e234213d72b64
Transaction hash: 0x73f995d1fd9a05161e271f5ffb879bd70b79185a2b3de5536a289780387dd30
This indicates that the actual deployment was successful! You can see our deployed contract here.
ERC721 Code Template#
Save the following erc721.cairo in the src
created above for easy terminal operation.
File name: 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);
}
}
## Deploy ERC721
First, we still need to compile the Cairo file to Sierra
$CAIRO_COMPILER_DIR/starknet-compile src/erc721.cairo sierra/erc721.json
Next is the declaration
starknet declare --contract sierra/erc721.json --account StarknetAstro
This time the output is different because the erc20 file we wrote has not been deployed by anyone yet, so there will be no error (of course, students following the course will still encounter the already declared error).
Sending the transaction with max_fee: 0.000001 ETH (1378300016540 WEI).
Declare transaction was sent.
Contract class hash: 0x232f6df73a376998769e0571aa14281dc2e8a6f2dce0578e4c45741392a2b37
Transaction hash: 0x24b19a2b7b9663699ce134ce29bd03cf8f17e7cb17a30ab19509fcdc568f27f
Next, we will deploy this erc721 contract instance. Note that the constructor in the erc721 code has parameters, so we also need to provide the corresponding parameters:
starknet deploy --inputs 0x417374726F546F6B656E 0x417374726F --class_hash 0x232f6df73a376998769e0571aa14281dc2e8a6f2dce0578e4c45741392a2b37 --account StarknetAstro
The output is:
Sending the transaction with max_fee: 0.000006 ETH (6121500073459 WEI).
Invoke transaction for contract deployment was sent.
Contract address: 0x051566565f452d8dc645f7aaa4f232c935ce0f89d98f7663c094261c04748dfb
Transaction hash: 0x36d8fc6b9dfece81d2804c563fd000b5b5cb8f0f704d978b7a2c1623e390d08
Great job! You can see our [deployed contract here](https://testnet.starkscan.co/contract/0x051566565f452d8dc645f7aaa4f232c935ce0f89d98f7663c094261c04748dfb)