title: 31. ERC721 tags: cairo starknet wtfacademy ERC721 WTF Cairo极简教程: 31. ERC721 我最近在学 ,巩固一下细节,也写一个 ,供小白们使用。教程基于 版本。 推特:@0xAAScience|@WTFAcademy WTF Academy 社群:Discord|微信群|官网 wtf.academy 所有代码和教程开源在 github: github.com/WTFAcademy/WTF-Cairo 在这节中,我们将会讲一下非同质化代币(NFT),介绍一下ERC721标准,并基于它发行一款NFT。 NFT 当我们谈论 、 或 等加密数字货币时,通常指的是同质化代币。
title: 31. ERC721 tags: - cairo - starknet - wtfacademy - ERC721
我最近在学cairo-lang,巩固一下细节,也写一个WTF Cairo极简教程,供小白们使用。教程基于cairo 2.2.0版本。
WTF Academy 社群:Discord|微信群|官网 wtf.academy
所有代码和教程开源在 github: github.com/WTFAcademy/WTF-Cairo
在这节中,我们将会讲一下非同质化代币(NFT),介绍一下ERC721标准,并基于它发行一款NFT。
当我们谈论BTC、ETH或STRK等加密数字货币时,通常指的是同质化代币。这意味着无论是第一枚还是第一千枚代币,它们之间没有区别,都是可相互替换的,并且可以被分割成任意大小。
然而,与同质化代币截然不同的是非同质化代币(Non-Fungible Token,NFT)。NFT的关键特征在于每一个代币都拥有独一无二的标识,使其不可替换。此外,NFT是不可分割的,其最小单位是1。这意味着,每一个NFT都是唯一的存在,就像人类每个个体都有自己独特的特质和个性一样。
通过ERC165标准,智能合约可以声明它支持的接口,供其他合约检查。简单的说,ERC165就是检查一个智能合约是不是支持了ERC721,ERC1155的接口。
IERC165接口合约只声明了一个supportsInterface函数,输入要查询的interfaceId接口id,若合约实现了该接口id,则返回true:
#[starknet::interface] trait IERC165<TState> { fn supports_interface(self: @TState, interface_id: felt252) -> bool; }
IERC721是ERC721标准的接口合约,规定了ERC721要实现的基本函数。它利用tokenId来表示特定的非同质化代币,授权或转账都要明确tokenId;而ERC20只需要明确转账的数额即可。
#[starknet::interface] trait IERC721<TState> { fn balance_of(self: @TState, account: ContractAddress) -> u256; fn owner_of(self: @TState, token_id: u256) -> ContractAddress; fn safe_transfer_from( ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252> ); fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256); fn approve(ref self: TState, to: ContractAddress, token_id: u256); fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); fn get_approved(self: @TState, token_id: u256) -> ContractAddress; fn is_approved_for_all( self: @TState, owner: ContractAddress, operator: ContractAddress ) -> bool; }
balance_of:返回某地址的NFT持有量balance。owner_of:返回某tokenId的主人owner。safe_transfer_from:安全转账(如果接收方是合约地址,会要求实现IERC721Receiver接口)。参数为转出地址from,接收地址to和tokenId。transfer_from:普通转账,参数为转出地址from,接收地址to和tokenId。approve:授权另一个地址使用你的NFT。参数为被授权地址approve和tokenId。set_approval_for_all:将自己持有的该系列NFT批量授权给某个地址operator。get_approved:查询tokenId被批准给了哪个地址。is_approved_for_all:查询某地址的NFT是否批量授权给了另一个operator地址。如果一个合约没有实现ERC721的相关函数,转入的NFT就进了黑洞,永远转不出来了。为了防止误转账,ERC721实现了safe_transfer_from()安全转账函数,目标合约必须实现了IERC721Receiver接口才能接收ERC721代币,不然会revert。IERC721Receiver接口只包含一个on_erc721_received()函数。
#[starknet::interface] trait IERC721Receiver<TState> { fn on_erc721_received( self: @TState, operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span<felt252> ) -> felt252; }
IERC721Metadata是ERC721的拓展接口,实现了3个查询metadata元数据的常用函数:
#[starknet::interface] trait IERC721Metadata<TState> { fn name(self: @TState) -> felt252; fn symbol(self: @TState) -> felt252; fn token_uri(self: @TState, token_id: felt252) -> Array<felt252>; }
name():返回代币名称。symbol():返回代币代号。token_uri():通过tokenId查询metadata的链接url,ERC721特有的函数。ERC721主合约实现了IERC721,IERC165和IERC721Metadata定义的所有功能,包含4个状态变量和17个函数。实现都比较简单:
use starknet::ContractAddress; // 接口对应的hash const IERC721_ID: felt252 = 0x33eb2f84c309543403fd69f0d0f363781ef06ef6faeb0131ff16ea3175bd943; const IERC721_METADATA_ID: felt252 = 0xabbcd595a567dce909050a1038e055daccb3c42af06f0add544fa90ee91f25; const IERC721_RECEIVER_ID: felt252 = 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; #[starknet::interface] trait IERC165<TState> { fn supports_interface(self: @TState, interface_id: felt252) -> bool; } #[starknet::interface] trait IERC721<TState> { fn balance_of(self: @TState, account: ContractAddress) -> u256; fn owner_of(self: @TState, token_id: u256) -> ContractAddress; fn safe_transfer_from( ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252> ); fn transfer_from(ref self: TState, from: ContractAddress, to: ContractAddress, token_id: u256); fn approve(ref self: TState, to: ContractAddress, token_id: u256); fn set_approval_for_all(ref self: TState, operator: ContractAddress, approved: bool); fn get_approved(self: @TState, token_id: u256) -> ContractAddress; fn is_approved_for_all( self: @TState, owner: ContractAddress, operator: ContractAddress ) -> bool; } #[starknet::interface] trait IERC721Metadata<TState> { fn name(self: @TState) -> felt252; fn symbol(self: @TState) -> felt252; fn token_uri(self: @TState, token_id: felt252) -> Array<felt252>; } #[starknet::interface] trait IERC721Receiver<TState> { fn on_erc721_received( self: @TState, operator: ContractAddress, from: ContractAddress, token_id: u256, data: Span<felt252> ) -> felt252; } #[starknet::contract] mod ERC721 { use starknet::ContractAddress; use starknet::get_caller_address; use super::IERC721_RECEIVER_ID; use super::IERC721_ID; use super::IERC721_METADATA_ID; #[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>, total_supply: u256, } #[event] #[derive(Drop, PartialEq, starknet::Event)] enum Event { Transfer: Transfer, Approval: Approval, ApprovalForAll: ApprovalForAll, } #[derive(Drop, PartialEq, starknet::Event)] struct Transfer { #[key] from: ContractAddress, #[key] to: ContractAddress, #[key] token_id: u256 } #[derive(Drop, PartialEq, starknet::Event)] struct Approval { #[key] owner: ContractAddress, #[key] approved: ContractAddress, #[key] token_id: u256 } #[derive(Drop, PartialEq, starknet::Event)] struct ApprovalForAll { #[key] owner: ContractAddress, #[key] operator: ContractAddress, approved: bool } mod Errors { const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID'; const INVALID_ACCOUNT: felt252 = 'ERC721: invalid account'; const UNAUTHORIZED: felt252 = 'ERC721: unauthorized caller'; const APPROVAL_TO_OWNER: felt252 = 'ERC721: approval to owner'; const SELF_APPROVAL: felt252 = 'ERC721: self approval'; const INVALID_RECEIVER: felt252 = 'ERC721: invalid receiver'; const ALREADY_MINTED: felt252 = 'ERC721: token already minted'; const WRONG_SENDER: felt252 = 'ERC721: wrong sender'; const SAFE_MINT_FAILED: felt252 = 'ERC721: safe mint failed'; const SAFE_TRANSFER_FAILED: felt252 = 'ERC721: safe transfer failed'; } #[constructor] fn constructor( ref self: ContractState, name: felt252, symbol: felt252 ){ self.name.write(name); self.symbol.write(symbol); } #[abi(embed_v0)] impl ERC165Impl of super::IERC165<ContractState>{ fn supports_interface(self: @ContractState, interface_id: felt252) -> bool{ (interface_id == IERC721_ID) | (interface_id == IERC721_METADATA_ID) } } #[abi(embed_v0)] impl ERC721Impl of super::IERC721<ContractState> { fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { assert(!account.is_zero(), Errors::INVALID_ACCOUNT); self.balances.read(account) } fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { let owner = self.owners.read(token_id); assert(!owner.is_zero(),Errors::INVALID_TOKEN_ID); return owner; } fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { assert(!self.owners.read(token_id).is_zero(), Errors::INVALID_TOKEN_ID); self.token_approvals.read(token_id) } fn is_approved_for_all( self: @ContractState, owner: ContractAddress, operator: ContractAddress ) -> bool { self.operator_approvals.read((owner, operator)) } fn approve(ref self: ContractState, to: ContractAddress, token_id: u256) { let owner = self.owners.read(token_id); let caller = get_caller_address(); assert( owner == caller || ERC721Impl::is_approved_for_all(@self, owner, caller), Errors::UNAUTHORIZED ); self._approve(to, token_id); } fn set_approval_for_all( ref self: ContractState, operator: ContractAddress, approved: bool ) { let owner = get_caller_address(); assert(owner != operator, Errors::SELF_APPROVAL); self.operator_approvals.write((owner, operator), approved); self.emit(ApprovalForAll { owner, operator, approved }); } fn transfer_from( ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 ) { let owner = self.owners.read(token_id); let spender = get_caller_address(); assert( self._is_approved_or_owner(owner, spender, token_id), Errors::UNAUTHORIZED ); self._transfer(owner, from, to, token_id); } fn safe_transfer_from( ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252> ) { let owner = self.owners.read(token_id); assert( self._is_approved_or_owner(owner, get_caller_address(), token_id), Errors::UNAUTHORIZED ); self._safe_transfer(owner, from, to, token_id, data); } } #[abi(embed_v0)] impl ERC721MetadataImpl of super::IERC721Metadata<ContractState> { fn name(self: @ContractState) -> felt252 { self.name.read() } fn symbol(self: @ContractState) -> felt252 { self.symbol.read() } //无聊猿BAYC的baseURI为ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ fn token_uri(self: @ContractState, token_id: felt252) -> Array<felt252> { let mut uri = ArrayTrait::<felt252>::new(); uri.append('ipfs://QmeSjSinHpPnmXm'); uri.append('spMjwiXyN6zS4E9zcc'); uri.append('ariGR3jxcaWtq/'); uri.append(token_id); uri } } #[starknet::interface] trait IERC721Metadata<TState> { fn name(self: @TState) -> ByteArray; fn symbol(self: @TState) -> ByteArray; fn token_uri(self: @TState, token_id: u256) -> ByteArray; } #[generate_trait] impl InternalImpl of InternalTrait { fn _approve(ref self: ContractState, to: ContractAddress, token_id: u256) { let owner = self.owners.read(token_id); assert(owner != to, Errors::APPROVAL_TO_OWNER); self.token_approvals.write(token_id, to); self.emit(Approval { owner, approved: to, token_id }); } fn _transfer( ref self: ContractState, owner: ContractAddress, from: ContractAddress, to: ContractAddress, token_id: u256 ) { assert(!to.is_zero(), Errors::INVALID_RECEIVER); assert(from == owner, Errors::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); self.owners.write(token_id, to); self.emit(Transfer { from, to, token_id }); } fn _is_approved_or_owner( self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256 ) -> bool { owner == spender || self.token_approvals.read(token_id) == spender || self.operator_approvals.read((owner, spender)) } fn _safe_transfer( ref self: ContractState, owner: ContractAddress, from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252> ) { self._transfer(owner, from, to, token_id); assert( InternalImpl::_check_on_erc721_received(from, to, token_id, data), Errors::SAFE_TRANSFER_FAILED ); } //在实际开发中,请按照OpenZeppenlin的格式来 fn _check_on_erc721_received( from: ContractAddress, to: ContractAddress, token_id: u256, data: Span<felt252> ) -> bool { return true; } fn _burn(ref self: ContractState, token_id: u256) { let owner = self.owners.read(token_id); self.token_approvals.write(token_id, Zeroable::zero()); self.balances.write(owner, self.balances.read(owner) - 1); self.owners.write(token_id, Zeroable::zero()); self.emit(Transfer { from: owner, to: Zeroable::zero(), token_id }); } } #[external(v0)] fn mint(ref self: ContractState, to: ContractAddress){ let token_id = self.total_supply.read() + 1; assert(!to.is_zero(), Errors::INVALID_RECEIVER); assert(!self.owners.read(token_id).is_zero(), Errors::ALREADY_MINTED); self.balances.write(to, self.balances.read(to) + 1); self.owners.write(token_id, to); self.total_supply.write(self.total_supply.read() + 1); self.emit(Transfer { from: Zeroable::zero(), to, token_id }); } }
有了ERC721标准后,在ETH链上发行NFT变得非常简单。现在,我们发行属于我们的NFT。

这一讲,我介绍了ERC721标准、接口及其实现,你可以利用它在Starknet上发行NFT!