From 9703d23b81a7908ff67c4e353c3f9ac546fd0ff7 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 16 Oct 2024 17:57:31 +0200 Subject: [PATCH 01/50] Add ERC721 initial impl --- Scarb.lock | 4 + listings/applications/erc721/.gitignore | 1 + listings/applications/erc721/Scarb.toml | 17 ++ listings/applications/erc721/src/lib.cairo | 1 + listings/applications/erc721/src/token.cairo | 262 +++++++++++++++++++ src/SUMMARY.md | 1 + src/applications/erc721.md | 19 ++ 7 files changed, 305 insertions(+) create mode 100644 listings/applications/erc721/.gitignore create mode 100644 listings/applications/erc721/Scarb.toml create mode 100644 listings/applications/erc721/src/lib.cairo create mode 100644 listings/applications/erc721/src/token.cairo create mode 100644 src/applications/erc721.md diff --git a/Scarb.lock b/Scarb.lock index b73c3911..c007268d 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -78,6 +78,10 @@ version = "0.1.0" name = "erc20" version = "0.1.0" +[[package]] +name = "erc721" +version = "0.1.0" + [[package]] name = "errors" version = "0.1.0" diff --git a/listings/applications/erc721/.gitignore b/listings/applications/erc721/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/listings/applications/erc721/.gitignore @@ -0,0 +1 @@ +target diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml new file mode 100644 index 00000000..410c0006 --- /dev/null +++ b/listings/applications/erc721/Scarb.toml @@ -0,0 +1,17 @@ +[package] +name = "erc721" +version.workspace = true +edition = "2024_07" + +[lib] + +[dependencies] +starknet.workspace = true + +[dev-dependencies] +cairo_test.workspace = true + +[scripts] +test.workspace = true + +[[target.starknet-contract]] diff --git a/listings/applications/erc721/src/lib.cairo b/listings/applications/erc721/src/lib.cairo new file mode 100644 index 00000000..79c66ba6 --- /dev/null +++ b/listings/applications/erc721/src/lib.cairo @@ -0,0 +1 @@ +pub mod token; diff --git a/listings/applications/erc721/src/token.cairo b/listings/applications/erc721/src/token.cairo new file mode 100644 index 00000000..62341ab9 --- /dev/null +++ b/listings/applications/erc721/src/token.cairo @@ -0,0 +1,262 @@ +use starknet::ContractAddress; + +// ANCHOR: interface +#[starknet::interface] +pub trait IERC721 { + fn balance_of(self: @TContractState, owner: ContractAddress) -> u256; + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; + fn safe_transfer_from( + ref self: TContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ); + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ); + fn approve(ref self: TContractState, approved: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool); + fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; + fn is_approved_for_all( + self: @TContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool; +} + +pub const IERC721_RECEIVER_ID: felt252 = + 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; + +#[starknet::interface] +pub trait IERC721Receiver { + fn on_erc721_received( + self: @TState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span + ) -> felt252; +} +// ANCHOR_END: interface + +// ANCHOR: erc721 +#[starknet::contract] +pub mod erc721 { + use core::num::traits::Zero; + use starknet::get_caller_address; + use starknet::ContractAddress; + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use openzeppelin_introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; + use super::{IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID}; + + #[storage] + struct Storage { + owners: Map, + balances: Map, + approvals: Map, + operator_approvals: Map<(ContractAddress, ContractAddress), bool>, + } + + #[event] + #[derive(Drop, PartialEq, starknet::Event)] + pub enum Event { + Transfer: Transfer, + Approval: Approval, + ApprovalForAll: ApprovalForAll, + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Transfer { + pub from: ContractAddress, + pub to: ContractAddress, + pub token_id: u256 + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct Approval { + pub owner: ContractAddress, + pub approved: ContractAddress, + pub token_id: u256 + } + + #[derive(Drop, PartialEq, starknet::Event)] + pub struct ApprovalForAll { + pub owner: ContractAddress, + pub operator: ContractAddress, + pub approved: bool + } + + pub mod Errors { + pub const INVALID_TOKEN_ID: felt252 = 'ERC721: invalid token ID'; + pub const INVALID_ACCOUNT: felt252 = 'ERC721: invalid account'; + pub const INVALID_OPERATOR: felt252 = 'ERC721: invalid operator'; + pub const UNAUTHORIZED: felt252 = 'ERC721: unauthorized caller'; + pub const INVALID_RECEIVER: felt252 = 'ERC721: invalid receiver'; + pub const INVALID_SENDER: felt252 = 'ERC721: invalid sender'; + pub const SAFE_TRANSFER_FAILED: felt252 = 'ERC721: safe transfer failed'; + } + + #[abi(embed_v0)] + impl IERC721Impl of super::IERC721 { + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + self._require_owned(token_id) + } + + fn balance_of(self: @ContractState, owner: ContractAddress) -> u256 { + assert(!owner.is_zero(), Errors::INVALID_ACCOUNT); + self.balances.read(owner) + } + + fn set_approval_for_all( + ref self: ContractState, operator: ContractAddress, approved: bool + ) { + self._set_approval_for_all(get_caller_address(), operator, approved) + } + + fn approve(ref self: ContractState, approved: ContractAddress, token_id: u256) { + self._approve(approved, token_id, get_caller_address()); + } + + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + self._require_owned(token_id); + self.approvals.read(token_id) + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ) { + assert(!to.is_zero(), Errors::INVALID_RECEIVER); + + let caller = get_caller_address(); + let previous_owner = self._owner_of(token_id); + + // Perform (optional) operator check + if !caller.is_zero() { + self._check_authorized(from, caller, token_id); + } + if !from.is_zero() { + let zero_address = Zero::zero(); + self._approve_with_optional_event(zero_address, token_id, zero_address, false); + + self.balances.write(from, self.balances.read(from) - 1); + } + if !to.is_zero() { + self.balances.write(to, self.balances.read(to) + 1); + } + + self.owners.write(token_id, to); + self.emit(Transfer { from, to, token_id }); + + assert(from == previous_owner, Errors::INVALID_SENDER); + } + + fn safe_transfer_from( + ref self: ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) { + Self::transfer_from(ref self, from, to, token_id); + assert( + _check_on_erc721_received(from, to, token_id, data), Errors::SAFE_TRANSFER_FAILED + ); + } + + fn is_approved_for_all( + self: @ContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool { + self.operator_approvals.read((owner, operator)) + } + } + + #[generate_trait] + pub impl InternalImpl of InternalTrait { + fn _require_owned(self: @ContractState, token_id: u256) -> ContractAddress { + let owner = self._owner_of(token_id); + assert(!owner.is_zero(), Errors::INVALID_TOKEN_ID); + owner + } + + fn _owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + self.owners.read(token_id) + } + + fn _approve( + ref self: ContractState, to: ContractAddress, token_id: u256, auth: ContractAddress + ) { + self._approve_with_optional_event(to, token_id, auth, true); + } + + fn _approve_with_optional_event( + ref self: ContractState, + to: ContractAddress, + token_id: u256, + auth: ContractAddress, + emit_event: bool + ) { + if emit_event || !auth.is_zero() { + let owner = self._require_owned(token_id); + + if !auth.is_zero() && owner != auth { + let is_approved_for_all = IERC721Impl::is_approved_for_all(@self, owner, auth); + assert(is_approved_for_all, Errors::UNAUTHORIZED); + } + + if emit_event { + self.emit(Approval { owner, approved: to, token_id }); + } + } + + self.approvals.write(token_id, to); + } + + fn _set_approval_for_all( + ref self: ContractState, + owner: ContractAddress, + operator: ContractAddress, + approved: bool + ) { + assert(!operator.is_zero(), Errors::INVALID_OPERATOR); + self.operator_approvals.write((owner, operator), approved); + self.emit(ApprovalForAll { owner, operator, approved }); + } + + fn _is_authorized( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256 + ) -> bool { + let is_approved_for_all = IERC721Impl::is_approved_for_all(self, owner, spender); + + !spender.is_zero() + && (owner == spender + || is_approved_for_all + || spender == IERC721Impl::get_approved(self, token_id)) + } + + fn _check_authorized( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256 + ) { + // Non-existent token + assert(!owner.is_zero(), Errors::INVALID_TOKEN_ID); + assert(self._is_authorized(owner, spender, token_id), Errors::UNAUTHORIZED); + } + } + + fn _check_on_erc721_received( + from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ) -> bool { + let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; + + if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { + IERC721ReceiverDispatcher { contract_address: to } + .on_erc721_received( + get_caller_address(), from, token_id, data + ) == IERC721_RECEIVER_ID + } else { + src5_dispatcher.supports_interface(openzeppelin_account::interface::ISRC6_ID) + } + } +} +// ANCHOR_END: erc721 + +#[cfg(test)] +mod tests {} diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 60b89e4a..2c5dd379 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -57,6 +57,7 @@ Summary - [Upgradeable Contract](./applications/upgradeable_contract.md) - [Defi Vault](./applications/simple_vault.md) - [ERC20 Token](./applications/erc20.md) +- [ERC721 NFT](./applications/erc721.md) - [NFT Dutch Auction](./applications/nft_dutch_auction.md) - [Constant Product AMM](./applications/constant-product-amm.md) - [TimeLock](./applications/timelock.md) diff --git a/src/applications/erc721.md b/src/applications/erc721.md new file mode 100644 index 00000000..4dc4cab3 --- /dev/null +++ b/src/applications/erc721.md @@ -0,0 +1,19 @@ +# ERC721 Token + +Contracts that follow the [ERC721 Standard](https://eips.ethereum.org/EIPS/eip-721) are called ERC721 tokens. They are used to represent non-fungible assets. + +To create an ERC721 contract, it must implement the following interface: + +```rust +{{#include ../../listings/applications/721/src/token.cairo:interface}} +``` + +Becase function names in Starknet should be written in _snake_case_, the Starknet ERC721 interface is slightly different from the Solidity ERC721 interface which uses _camelCase_. + +Here's an implementation of the ERC721 interface in Cairo: + +```rust +{{#include ../../listings/applications/721/src/token.cairo:erc721}} +``` + +There are other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc721) one. From 41bd83276593f520f146383de0e64555229feafd Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 16 Oct 2024 17:59:07 +0200 Subject: [PATCH 02/50] Rename token.cairo->erc721.cairo --- listings/applications/erc721/src/{token.cairo => erc721.cairo} | 0 listings/applications/erc721/src/lib.cairo | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename listings/applications/erc721/src/{token.cairo => erc721.cairo} (100%) diff --git a/listings/applications/erc721/src/token.cairo b/listings/applications/erc721/src/erc721.cairo similarity index 100% rename from listings/applications/erc721/src/token.cairo rename to listings/applications/erc721/src/erc721.cairo diff --git a/listings/applications/erc721/src/lib.cairo b/listings/applications/erc721/src/lib.cairo index 79c66ba6..02397419 100644 --- a/listings/applications/erc721/src/lib.cairo +++ b/listings/applications/erc721/src/lib.cairo @@ -1 +1 @@ -pub mod token; +pub mod erc721; From cdc5a9a1665af589d4b106205ed6eed3b8173793 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 16 Oct 2024 18:23:02 +0200 Subject: [PATCH 03/50] Simplify contract --- listings/applications/erc721/src/erc721.cairo | 130 ++++++------------ 1 file changed, 39 insertions(+), 91 deletions(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index 62341ab9..a9775e30 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -109,11 +109,21 @@ pub mod erc721 { fn set_approval_for_all( ref self: ContractState, operator: ContractAddress, approved: bool ) { - self._set_approval_for_all(get_caller_address(), operator, approved) + assert(!operator.is_zero(), Errors::INVALID_OPERATOR); + let owner = get_caller_address(); + self.operator_approvals.write((owner, operator), approved); + self.emit(ApprovalForAll { owner, operator, approved }); } fn approve(ref self: ContractState, approved: ContractAddress, token_id: u256) { - self._approve(approved, token_id, get_caller_address()); + let owner = self._require_owned(token_id); + let caller = get_caller_address(); + assert( + caller == owner || self.is_approved_for_all(owner, caller), Errors::UNAUTHORIZED + ); + + self.approvals.write(token_id, approved); + self.emit(Approval { owner, approved, token_id }); } fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { @@ -124,29 +134,19 @@ pub mod erc721 { fn transfer_from( ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 ) { + let previous_owner = self._require_owned(token_id); + assert(from == previous_owner, Errors::INVALID_SENDER); assert(!to.is_zero(), Errors::INVALID_RECEIVER); + assert( + self._is_approved_or_owner(from, get_caller_address(), token_id), + Errors::UNAUTHORIZED + ); - let caller = get_caller_address(); - let previous_owner = self._owner_of(token_id); - - // Perform (optional) operator check - if !caller.is_zero() { - self._check_authorized(from, caller, token_id); - } - if !from.is_zero() { - let zero_address = Zero::zero(); - self._approve_with_optional_event(zero_address, token_id, zero_address, false); - - self.balances.write(from, self.balances.read(from) - 1); - } - if !to.is_zero() { - self.balances.write(to, self.balances.read(to) + 1); - } - + 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 }); - assert(from == previous_owner, Errors::INVALID_SENDER); + self.emit(Transfer { from, to, token_id }); } fn safe_transfer_from( @@ -158,7 +158,7 @@ pub mod erc721 { ) { Self::transfer_from(ref self, from, to, token_id); assert( - _check_on_erc721_received(from, to, token_id, data), Errors::SAFE_TRANSFER_FAILED + self._check_on_erc721_received(from, to, token_id, data), Errors::SAFE_TRANSFER_FAILED ); } @@ -172,56 +172,12 @@ pub mod erc721 { #[generate_trait] pub impl InternalImpl of InternalTrait { fn _require_owned(self: @ContractState, token_id: u256) -> ContractAddress { - let owner = self._owner_of(token_id); + let owner = self.owners.read(token_id); assert(!owner.is_zero(), Errors::INVALID_TOKEN_ID); owner } - fn _owner_of(self: @ContractState, token_id: u256) -> ContractAddress { - self.owners.read(token_id) - } - - fn _approve( - ref self: ContractState, to: ContractAddress, token_id: u256, auth: ContractAddress - ) { - self._approve_with_optional_event(to, token_id, auth, true); - } - - fn _approve_with_optional_event( - ref self: ContractState, - to: ContractAddress, - token_id: u256, - auth: ContractAddress, - emit_event: bool - ) { - if emit_event || !auth.is_zero() { - let owner = self._require_owned(token_id); - - if !auth.is_zero() && owner != auth { - let is_approved_for_all = IERC721Impl::is_approved_for_all(@self, owner, auth); - assert(is_approved_for_all, Errors::UNAUTHORIZED); - } - - if emit_event { - self.emit(Approval { owner, approved: to, token_id }); - } - } - - self.approvals.write(token_id, to); - } - - fn _set_approval_for_all( - ref self: ContractState, - owner: ContractAddress, - operator: ContractAddress, - approved: bool - ) { - assert(!operator.is_zero(), Errors::INVALID_OPERATOR); - self.operator_approvals.write((owner, operator), approved); - self.emit(ApprovalForAll { owner, operator, approved }); - } - - fn _is_authorized( + fn _is_approved_or_owner( self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256 ) -> bool { let is_approved_for_all = IERC721Impl::is_approved_for_all(self, owner, spender); @@ -231,29 +187,21 @@ pub mod erc721 { || is_approved_for_all || spender == IERC721Impl::get_approved(self, token_id)) } - - fn _check_authorized( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256 - ) { - // Non-existent token - assert(!owner.is_zero(), Errors::INVALID_TOKEN_ID); - assert(self._is_authorized(owner, spender, token_id), Errors::UNAUTHORIZED); - } - } - - fn _check_on_erc721_received( - from: ContractAddress, to: ContractAddress, token_id: u256, data: Span - ) -> bool { - let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; - - if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { - IERC721ReceiverDispatcher { contract_address: to } - .on_erc721_received( - get_caller_address(), from, token_id, data - ) == IERC721_RECEIVER_ID - } else { - src5_dispatcher.supports_interface(openzeppelin_account::interface::ISRC6_ID) - } + + fn _check_on_erc721_received( + self: @ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, data: Span + ) -> bool { + let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; + + if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { + IERC721ReceiverDispatcher { contract_address: to } + .on_erc721_received( + get_caller_address(), from, token_id, data + ) == IERC721_RECEIVER_ID + } else { + src5_dispatcher.supports_interface(openzeppelin_account::interface::ISRC6_ID) + } + } } } // ANCHOR_END: erc721 From 286e8c4e50018653cd44dcde7e43069284ff55c2 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 16 Oct 2024 19:04:15 +0200 Subject: [PATCH 04/50] Implement mint and burn --- listings/applications/erc721/src/erc721.cairo | 65 +++++++++++++------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index a9775e30..49ab7350 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -21,6 +21,8 @@ pub trait IERC721 { fn is_approved_for_all( self: @TContractState, owner: ContractAddress, operator: ContractAddress ) -> bool; + fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); + fn burn(ref self: TContractState, token_id: u256); } pub const IERC721_RECEIVER_ID: felt252 = @@ -93,6 +95,7 @@ pub mod erc721 { pub const INVALID_RECEIVER: felt252 = 'ERC721: invalid receiver'; pub const INVALID_SENDER: felt252 = 'ERC721: invalid sender'; pub const SAFE_TRANSFER_FAILED: felt252 = 'ERC721: safe transfer failed'; + pub const ALREADY_MINTED: felt252 = 'ERC721: token already minted'; } #[abi(embed_v0)] @@ -158,7 +161,8 @@ pub mod erc721 { ) { Self::transfer_from(ref self, from, to, token_id); assert( - self._check_on_erc721_received(from, to, token_id, data), Errors::SAFE_TRANSFER_FAILED + self._check_on_erc721_received(from, to, token_id, data), + Errors::SAFE_TRANSFER_FAILED ); } @@ -167,6 +171,27 @@ pub mod erc721 { ) -> bool { self.operator_approvals.read((owner, operator)) } + + fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + 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.emit(Transfer { from: Zero::zero(), to, token_id }); + } + + fn burn(ref self: ContractState, token_id: u256) { + let owner = self._require_owned(token_id); + + self.balances.write(owner, self.balances.read(owner) - 1); + + self.owners.write(token_id, Zero::zero()); + self.approvals.write(token_id, Zero::zero()); + + self.emit(Transfer { from: owner, to: Zero::zero(), token_id }); + } } #[generate_trait] @@ -180,28 +205,30 @@ pub mod erc721 { fn _is_approved_or_owner( self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256 ) -> bool { - let is_approved_for_all = IERC721Impl::is_approved_for_all(self, owner, spender); - !spender.is_zero() && (owner == spender - || is_approved_for_all - || spender == IERC721Impl::get_approved(self, token_id)) + || self.is_approved_for_all(owner, spender) + || spender == self.get_approved(token_id)) } - - fn _check_on_erc721_received( - self: @ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, data: Span - ) -> bool { - let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; - - if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { - IERC721ReceiverDispatcher { contract_address: to } - .on_erc721_received( - get_caller_address(), from, token_id, data - ) == IERC721_RECEIVER_ID - } else { - src5_dispatcher.supports_interface(openzeppelin_account::interface::ISRC6_ID) - } + + fn _check_on_erc721_received( + self: @ContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ) -> bool { + let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; + + if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { + IERC721ReceiverDispatcher { contract_address: to } + .on_erc721_received( + get_caller_address(), from, token_id, data + ) == IERC721_RECEIVER_ID + } else { + src5_dispatcher.supports_interface(openzeppelin_account::interface::ISRC6_ID) } + } } } // ANCHOR_END: erc721 From 0b1bb65464143b622b655a26840ced762f2a24ef Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 08:49:49 +0200 Subject: [PATCH 05/50] refactor erc721 --- .../src/{erc721.cairo => contract.cairo} | 19 +++++++++---------- listings/applications/erc721/src/lib.cairo | 5 ++++- 2 files changed, 13 insertions(+), 11 deletions(-) rename listings/applications/erc721/src/{erc721.cairo => contract.cairo} (95%) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/contract.cairo similarity index 95% rename from listings/applications/erc721/src/erc721.cairo rename to listings/applications/erc721/src/contract.cairo index 49ab7350..97c6853e 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/contract.cairo @@ -29,9 +29,9 @@ pub const IERC721_RECEIVER_ID: felt252 = 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; #[starknet::interface] -pub trait IERC721Receiver { +pub trait IERC721Receiver { fn on_erc721_received( - self: @TState, + self: @TContractState, operator: ContractAddress, from: ContractAddress, token_id: u256, @@ -42,7 +42,7 @@ pub trait IERC721Receiver { // ANCHOR: erc721 #[starknet::contract] -pub mod erc721 { +pub mod ERC721 { use core::num::traits::Zero; use starknet::get_caller_address; use starknet::ContractAddress; @@ -59,28 +59,28 @@ pub mod erc721 { } #[event] - #[derive(Drop, PartialEq, starknet::Event)] + #[derive(Drop, starknet::Event)] pub enum Event { Transfer: Transfer, Approval: Approval, ApprovalForAll: ApprovalForAll, } - #[derive(Drop, PartialEq, starknet::Event)] + #[derive(Drop, starknet::Event)] pub struct Transfer { pub from: ContractAddress, pub to: ContractAddress, pub token_id: u256 } - #[derive(Drop, PartialEq, starknet::Event)] + #[derive(Drop, starknet::Event)] pub struct Approval { pub owner: ContractAddress, pub approved: ContractAddress, pub token_id: u256 } - #[derive(Drop, PartialEq, starknet::Event)] + #[derive(Drop, starknet::Event)] pub struct ApprovalForAll { pub owner: ContractAddress, pub operator: ContractAddress, @@ -99,7 +99,7 @@ pub mod erc721 { } #[abi(embed_v0)] - impl IERC721Impl of super::IERC721 { + impl ERC721 of super::IERC721 { fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { self._require_owned(token_id) } @@ -233,5 +233,4 @@ pub mod erc721 { } // ANCHOR_END: erc721 -#[cfg(test)] -mod tests {} + diff --git a/listings/applications/erc721/src/lib.cairo b/listings/applications/erc721/src/lib.cairo index 02397419..fca38f41 100644 --- a/listings/applications/erc721/src/lib.cairo +++ b/listings/applications/erc721/src/lib.cairo @@ -1 +1,4 @@ -pub mod erc721; +pub mod contract; + +#[cfg(test)] +mod tests; \ No newline at end of file From eda9fc95e8614cd0450b03d75e3fd56f5083c94a Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 08:51:00 +0200 Subject: [PATCH 06/50] rename back contract->erc721 --- .../applications/erc721/src/{contract.cairo => erc721.cairo} | 0 listings/applications/erc721/src/lib.cairo | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename listings/applications/erc721/src/{contract.cairo => erc721.cairo} (100%) diff --git a/listings/applications/erc721/src/contract.cairo b/listings/applications/erc721/src/erc721.cairo similarity index 100% rename from listings/applications/erc721/src/contract.cairo rename to listings/applications/erc721/src/erc721.cairo diff --git a/listings/applications/erc721/src/lib.cairo b/listings/applications/erc721/src/lib.cairo index fca38f41..38628066 100644 --- a/listings/applications/erc721/src/lib.cairo +++ b/listings/applications/erc721/src/lib.cairo @@ -1,4 +1,4 @@ -pub mod contract; +pub mod erc721; #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; From 446f735a9e6c5b0a70d96f950e544caf774eb8d0 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 09:09:19 +0200 Subject: [PATCH 07/50] Set up tests --- Scarb.lock | 4 +++ listings/applications/erc721/Scarb.toml | 5 +++- listings/applications/erc721/src/tests.cairo | 29 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 listings/applications/erc721/src/tests.cairo diff --git a/Scarb.lock b/Scarb.lock index c007268d..c9246bef 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -81,6 +81,10 @@ version = "0.1.0" [[package]] name = "erc721" version = "0.1.0" +dependencies = [ + "openzeppelin", + "snforge_std", +] [[package]] name = "errors" diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml index 410c0006..6ee4ef9d 100644 --- a/listings/applications/erc721/Scarb.toml +++ b/listings/applications/erc721/Scarb.toml @@ -7,11 +7,14 @@ edition = "2024_07" [dependencies] starknet.workspace = true +openzeppelin.workspace = true +snforge_std.workspace = true [dev-dependencies] -cairo_test.workspace = true +assert_macros.workspace = true [scripts] test.workspace = true [[target.starknet-contract]] +build-external-contracts = ["openzeppelin_introspection::interface::ISRC5"] diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo new file mode 100644 index 00000000..62760d44 --- /dev/null +++ b/listings/applications/erc721/src/tests.cairo @@ -0,0 +1,29 @@ +use erc721::erc721::{IERC721Dispatcher, IERC721DispatcherTrait// , ERC721::{Event, Transfer, Approval} +}; + +use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; +use starknet::{ContractAddress, contract_address_const}; + +pub const TOKEN_ID: u256 = 21; + +pub fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() +} + +fn deploy() -> IERC721Dispatcher { + let contract = declare("ERC721").unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@array![]).unwrap(); + IERC721Dispatcher { contract_address } +} + +fn setup() -> IERC721Dispatcher { + let contract = deploy(); + contract.mint(OWNER(), TOKEN_ID); + contract +} + +#[test] +fn test_balance_of() { + let contract = setup(); + assert_eq!(contract.balance_of(OWNER()), 1); +} From 7a3afc71ef8de60d79758272b84bad3e050a05e4 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 09:26:13 +0200 Subject: [PATCH 08/50] Add all getter tests --- listings/applications/erc721/src/tests.cairo | 57 +++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 62760d44..2a3b75a0 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -1,7 +1,8 @@ -use erc721::erc721::{IERC721Dispatcher, IERC721DispatcherTrait// , ERC721::{Event, Transfer, Approval} +use erc721::erc721::{ + IERC721Dispatcher, IERC721DispatcherTrait // , ERC721::{Event, Transfer, Approval} }; -use snforge_std::{declare, DeclareResultTrait, ContractClassTrait}; +use snforge_std::{declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address,}; use starknet::{ContractAddress, contract_address_const}; pub const TOKEN_ID: u256 = 21; @@ -10,6 +11,14 @@ pub fn OWNER() -> ContractAddress { contract_address_const::<'OWNER'>() } +pub fn ZERO() -> ContractAddress { + contract_address_const::<0>() +} + +pub fn SPENDER() -> ContractAddress { + contract_address_const::<'SPENDER'>() +} + fn deploy() -> IERC721Dispatcher { let contract = declare("ERC721").unwrap().contract_class(); let (contract_address, _) = contract.deploy(@array![]).unwrap(); @@ -22,8 +31,52 @@ fn setup() -> IERC721Dispatcher { contract } +// +// Getters +// + #[test] fn test_balance_of() { let contract = setup(); assert_eq!(contract.balance_of(OWNER()), 1); } + +#[test] +#[should_panic(expected: ('ERC721: invalid account',))] +fn test_balance_of_zero() { + let contract = setup(); + contract.balance_of(ZERO()); +} + +#[test] +fn test_owner_of() { + let contract = setup(); + assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_owner_of_non_minted() { + let contract = setup(); + contract.owner_of(7); +} + +#[test] +fn test_get_approved() { + let mut contract = setup(); + let spender = SPENDER(); + let token_id = TOKEN_ID; + + start_cheat_caller_address(contract.contract_address, OWNER()); + + assert_eq!(contract.get_approved(token_id), ZERO()); + contract.approve(spender, token_id); + assert_eq!(contract.get_approved(token_id), spender); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_get_approved_nonexistent() { + let contract = setup(); + contract.get_approved(7); +} From fbb07afd3faa52d8a87c53c8c7b388205b92252d Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 09:35:57 +0200 Subject: [PATCH 09/50] Return contract_address from deploy --- listings/applications/erc721/src/tests.cairo | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 2a3b75a0..08fef614 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -19,16 +19,16 @@ pub fn SPENDER() -> ContractAddress { contract_address_const::<'SPENDER'>() } -fn deploy() -> IERC721Dispatcher { +fn deploy() -> (IERC721Dispatcher, ContractAddress) { let contract = declare("ERC721").unwrap().contract_class(); let (contract_address, _) = contract.deploy(@array![]).unwrap(); - IERC721Dispatcher { contract_address } + (IERC721Dispatcher { contract_address }, contract_address) } -fn setup() -> IERC721Dispatcher { - let contract = deploy(); +fn setup() -> (IERC721Dispatcher, ContractAddress) { + let (contract, contract_address) = deploy(); contract.mint(OWNER(), TOKEN_ID); - contract + (contract, contract_address) } // @@ -37,37 +37,37 @@ fn setup() -> IERC721Dispatcher { #[test] fn test_balance_of() { - let contract = setup(); + let (contract, _) = setup(); assert_eq!(contract.balance_of(OWNER()), 1); } #[test] #[should_panic(expected: ('ERC721: invalid account',))] fn test_balance_of_zero() { - let contract = setup(); + let (contract, _) = setup(); contract.balance_of(ZERO()); } #[test] fn test_owner_of() { - let contract = setup(); + let (contract, _) = setup(); assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); } #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_owner_of_non_minted() { - let contract = setup(); + let (contract, _) = setup(); contract.owner_of(7); } #[test] fn test_get_approved() { - let mut contract = setup(); + let (mut contract, contract_address) = setup(); let spender = SPENDER(); let token_id = TOKEN_ID; - start_cheat_caller_address(contract.contract_address, OWNER()); + start_cheat_caller_address(contract_address, OWNER()); assert_eq!(contract.get_approved(token_id), ZERO()); contract.approve(spender, token_id); @@ -77,6 +77,6 @@ fn test_get_approved() { #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_get_approved_nonexistent() { - let contract = setup(); + let (contract, _) = setup(); contract.get_approved(7); } From 15fa3aeedd84cc30a1232dbb2c5063068ee26aee Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 09:54:18 +0200 Subject: [PATCH 10/50] Add approve tests --- listings/applications/erc721/error.log | 5 + listings/applications/erc721/src/tests.cairo | 126 ++++++++++++++++++- 2 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 listings/applications/erc721/error.log diff --git a/listings/applications/erc721/error.log b/listings/applications/erc721/error.log new file mode 100644 index 00000000..2e85421c --- /dev/null +++ b/listings/applications/erc721/error.log @@ -0,0 +1,5 @@ + Compiling snforge_scarb_plugin v0.2.0 + Finished `release` profile [optimized] target(s) in 0.05s + Compiling lib(erc721) erc721 v0.1.0 (/home/neshi/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) + Compiling starknet-contract(erc721) erc721 v0.1.0 (/home/neshi/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) +warn: external contracts not found for selectors: `openzeppelin_introspection::interface::ISRC5` diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 08fef614..62a81619 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -1,24 +1,39 @@ use erc721::erc721::{ - IERC721Dispatcher, IERC721DispatcherTrait // , ERC721::{Event, Transfer, Approval} + IERC721Dispatcher, IERC721DispatcherTrait, ERC721::{Event, Transfer, Approval} }; -use snforge_std::{declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address,}; +use snforge_std::{ + declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, spy_events, + EventSpyAssertionsTrait, +}; use starknet::{ContractAddress, contract_address_const}; pub const TOKEN_ID: u256 = 21; -pub fn OWNER() -> ContractAddress { - contract_address_const::<'OWNER'>() +pub fn CALLER() -> ContractAddress { + contract_address_const::<'CALLER'>() } -pub fn ZERO() -> ContractAddress { - contract_address_const::<0>() +pub fn OPERATOR() -> ContractAddress { + contract_address_const::<'OPERATOR'>() +} + +pub fn OTHER() -> ContractAddress { + contract_address_const::<'OTHER'>() +} + +pub fn OWNER() -> ContractAddress { + contract_address_const::<'OWNER'>() } pub fn SPENDER() -> ContractAddress { contract_address_const::<'SPENDER'>() } +pub fn ZERO() -> ContractAddress { + contract_address_const::<0>() +} + fn deploy() -> (IERC721Dispatcher, ContractAddress) { let contract = declare("ERC721").unwrap().contract_class(); let (contract_address, _) = contract.deploy(@array![]).unwrap(); @@ -80,3 +95,102 @@ fn test_get_approved_nonexistent() { let (contract, _) = setup(); contract.get_approved(7); } + +// +// approve +// + +#[test] +fn test_approve_from_owner() { + let (mut contract, contract_address) = setup(); + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OWNER()); + contract.approve(SPENDER(), TOKEN_ID); + + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Approval( + Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID } + ) + ) + ] + ); + + let approved = contract.get_approved(TOKEN_ID); + assert_eq!(approved, SPENDER()); +} + +#[test] +fn test_approve_from_operator() { + let (mut contract, contract_address) = setup(); + + start_cheat_caller_address(contract_address, OWNER()); + contract.set_approval_for_all(OPERATOR(), true); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OPERATOR()); + contract.approve(SPENDER(), TOKEN_ID); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Approval( + Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID } + ) + ) + ] + ); + + let approved = contract.get_approved(TOKEN_ID); + assert_eq!(approved, SPENDER()); +} + +#[test] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_approve_from_unauthorized() { + let (mut contract, contract_address) = setup(); + + start_cheat_caller_address(contract_address, OTHER()); + contract.approve(SPENDER(), TOKEN_ID); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_approve_nonexistent() { + let (mut contract, _) = setup(); + contract.approve(SPENDER(), TOKEN_ID); +} + +#[test] +fn test_approve_auth_is_approved_for_all() { + let (mut contract, contract_address) = setup(); + let auth = CALLER(); + + start_cheat_caller_address(contract_address, OWNER()); + contract.set_approval_for_all(auth, true); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, auth); + contract.approve(SPENDER(), TOKEN_ID); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Approval( + Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID } + ) + ) + ] + ); + + let approved = contract.get_approved(TOKEN_ID); + assert_eq!(approved, SPENDER()); +} From dceff792c66a61f6068eae376118deb502fad208 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 10:13:59 +0200 Subject: [PATCH 11/50] Add transfer_from tests --- listings/applications/erc721/src/tests.cairo | 224 ++++++++++++++++++- 1 file changed, 222 insertions(+), 2 deletions(-) diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 62a81619..ca10091e 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -1,7 +1,7 @@ +use core::num::traits::Zero; use erc721::erc721::{ - IERC721Dispatcher, IERC721DispatcherTrait, ERC721::{Event, Transfer, Approval} + IERC721Dispatcher, IERC721DispatcherTrait, ERC721::{Event, Transfer, Approval, ApprovalForAll} }; - use snforge_std::{ declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, spy_events, EventSpyAssertionsTrait, @@ -26,6 +26,10 @@ pub fn OWNER() -> ContractAddress { contract_address_const::<'OWNER'>() } +pub fn RECIPIENT() -> ContractAddress { + contract_address_const::<'RECIPIENT'>() +} + pub fn SPENDER() -> ContractAddress { contract_address_const::<'SPENDER'>() } @@ -194,3 +198,219 @@ fn test_approve_auth_is_approved_for_all() { let approved = contract.get_approved(TOKEN_ID); assert_eq!(approved, SPENDER()); } + +// +// set_approval_for_all +// + +#[test] +fn test_set_approval_for_all() { + let (mut contract, contract_address) = setup(); + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OWNER()); + let not_approved_for_all = !contract.is_approved_for_all(OWNER(), OPERATOR()); + assert!(not_approved_for_all); + + contract.set_approval_for_all(OPERATOR(), true); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::ApprovalForAll( + ApprovalForAll { owner: OWNER(), operator: OPERATOR(), approved: true } + ) + ) + ] + ); + + let is_approved_for_all = contract.is_approved_for_all(OWNER(), OPERATOR()); + assert!(is_approved_for_all); + + contract.set_approval_for_all(OPERATOR(), false); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::ApprovalForAll( + ApprovalForAll { owner: OWNER(), operator: OPERATOR(), approved: false } + ) + ) + ] + ); + + let not_approved_for_all = !contract.is_approved_for_all(OWNER(), OPERATOR()); + assert!(not_approved_for_all); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid operator',))] +fn test_set_approval_for_all_invalid_operator() { + let (mut contract, _) = setup(); + contract.set_approval_for_all(ZERO(), true); +} + +// +// transfer_from +// + +#[test] +fn test_transfer_from_owner() { + let (mut contract, contract_address) = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + // set approval to check reset + start_cheat_caller_address(contract_address, owner); + contract.approve(OTHER(), token_id); + + assert_state_before_transfer(contract, owner, recipient, token_id); + + let approved = contract.get_approved(token_id); + assert_eq!(approved, OTHER()); + + let mut spy = spy_events(); + + contract.transfer_from(owner, recipient, token_id); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: recipient, token_id }) + ) + ] + ); + + assert_state_after_transfer(contract, owner, recipient, token_id); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_transfer_from_nonexistent() { + let (mut contract, contract_address) = setup(); + start_cheat_caller_address(contract_address, OWNER()); + contract.transfer_from(ZERO(), RECIPIENT(), TOKEN_ID); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test_transfer_from_to_zero() { + let (mut contract, contract_address) = setup(); + start_cheat_caller_address(contract_address, OWNER()); + contract.transfer_from(OWNER(), ZERO(), TOKEN_ID); +} + +#[test] +fn test_transfer_from_to_owner() { + let (mut contract, contract_address) = setup(); + let mut spy = spy_events(); + + assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); + assert_eq!(contract.balance_of(OWNER()), 1); + + start_cheat_caller_address(contract_address, OWNER()); + contract.transfer_from(OWNER(), OWNER(), TOKEN_ID); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: OWNER(), to: OWNER(), token_id: TOKEN_ID }) + ) + ] + ); + + assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); + assert_eq!(contract.balance_of(OWNER()), 1); +} + +#[test] +fn test_transfer_from_approved() { + let (mut contract, contract_address) = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(contract, owner, recipient, token_id); + + start_cheat_caller_address(contract_address, owner); + contract.approve(OPERATOR(), token_id); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OPERATOR()); + contract.transfer_from(owner, recipient, token_id); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: recipient, token_id }) + ) + ] + ); + + assert_state_after_transfer(contract, owner, recipient, token_id); +} + +#[test] +fn test_transfer_from_approved_for_all() { + let (mut contract, contract_address) = setup(); + let token_id = TOKEN_ID; + let owner = OWNER(); + let recipient = RECIPIENT(); + + assert_state_before_transfer(contract, owner, recipient, token_id); + + start_cheat_caller_address(contract_address, owner); + contract.set_approval_for_all(OPERATOR(), true); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OPERATOR()); + contract.transfer_from(owner, recipient, token_id); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: recipient, token_id }) + ) + ] + ); + + assert_state_after_transfer(contract, owner, recipient, token_id); +} + +#[test] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_transfer_from_unauthorized() { + let (mut contract, contract_address) = setup(); + start_cheat_caller_address(contract_address, OTHER()); + contract.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); +} + +// +// Helpers +// + +fn assert_state_before_transfer( + contract: IERC721Dispatcher, owner: ContractAddress, recipient: ContractAddress, token_id: u256 +) { + assert_eq!(contract.owner_of(token_id), owner); + assert_eq!(contract.balance_of(owner), 1); + assert!(contract.balance_of(recipient).is_zero()); +} + +fn assert_state_after_transfer( + contract: IERC721Dispatcher, owner: ContractAddress, recipient: ContractAddress, token_id: u256 +) { + assert_eq!(contract.owner_of(token_id), recipient); + assert_eq!(contract.balance_of(owner), 0); + assert_eq!(contract.balance_of(recipient), 1); + assert!(contract.get_approved(token_id).is_zero()); +} From f5ed044b27784bfc81449659336c750316774d50 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 11:08:26 +0200 Subject: [PATCH 12/50] Add safe_transfer_from tests --- listings/applications/erc721/src/lib.cairo | 1 + listings/applications/erc721/src/mocks.cairo | 7 + .../erc721/src/mocks/account.cairo | 46 ++++ .../erc721/src/mocks/non_receiver.cairo | 10 + .../erc721/src/mocks/receiver.cairo | 50 ++++ listings/applications/erc721/src/tests.cairo | 216 +++++++++++++++++- 6 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 listings/applications/erc721/src/mocks.cairo create mode 100644 listings/applications/erc721/src/mocks/account.cairo create mode 100644 listings/applications/erc721/src/mocks/non_receiver.cairo create mode 100644 listings/applications/erc721/src/mocks/receiver.cairo diff --git a/listings/applications/erc721/src/lib.cairo b/listings/applications/erc721/src/lib.cairo index 38628066..8aef62c8 100644 --- a/listings/applications/erc721/src/lib.cairo +++ b/listings/applications/erc721/src/lib.cairo @@ -1,4 +1,5 @@ pub mod erc721; +mod mocks; #[cfg(test)] mod tests; diff --git a/listings/applications/erc721/src/mocks.cairo b/listings/applications/erc721/src/mocks.cairo new file mode 100644 index 00000000..3058ce37 --- /dev/null +++ b/listings/applications/erc721/src/mocks.cairo @@ -0,0 +1,7 @@ +pub mod account; +pub mod receiver; +pub mod non_receiver; + +pub use account::AccountMock; +pub use non_receiver::NonReceiverMock; +pub use receiver::ERC721ReceiverMock; \ No newline at end of file diff --git a/listings/applications/erc721/src/mocks/account.cairo b/listings/applications/erc721/src/mocks/account.cairo new file mode 100644 index 00000000..d26d2585 --- /dev/null +++ b/listings/applications/erc721/src/mocks/account.cairo @@ -0,0 +1,46 @@ +//! Copied with modifications from OpenZeppelin's repo +//! https://github.com/OpenZeppelin/cairo-contracts/blob/6e60ba9310fa7953f045d0c30b343b0ffc168c14/packages/test_common/src/mocks/account.cairo + +#[starknet::contract(account)] +pub mod AccountMock { + use openzeppelin_account::AccountComponent; + use openzeppelin_introspection::src5::SRC5Component; + + component!(path: AccountComponent, storage: account, event: AccountEvent); + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // Account + #[abi(embed_v0)] + impl SRC6Impl = AccountComponent::SRC6Impl; + #[abi(embed_v0)] + impl DeclarerImpl = AccountComponent::DeclarerImpl; + #[abi(embed_v0)] + impl DeployableImpl = AccountComponent::DeployableImpl; + impl AccountInternalImpl = AccountComponent::InternalImpl; + + // SCR5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub account: AccountComponent::Storage, + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + AccountEvent: AccountComponent::Event, + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState, public_key: felt252) { + self.account.initializer(public_key); + } +} \ No newline at end of file diff --git a/listings/applications/erc721/src/mocks/non_receiver.cairo b/listings/applications/erc721/src/mocks/non_receiver.cairo new file mode 100644 index 00000000..953521be --- /dev/null +++ b/listings/applications/erc721/src/mocks/non_receiver.cairo @@ -0,0 +1,10 @@ +#[starknet::contract] +pub mod NonReceiverMock { + #[storage] + pub struct Storage {} + + #[external(v0)] + fn nope(self: @ContractState) -> bool { + false + } +} diff --git a/listings/applications/erc721/src/mocks/receiver.cairo b/listings/applications/erc721/src/mocks/receiver.cairo new file mode 100644 index 00000000..6beb3abd --- /dev/null +++ b/listings/applications/erc721/src/mocks/receiver.cairo @@ -0,0 +1,50 @@ +const SUCCESS: felt252 = 'SUCCESS'; + +#[starknet::contract] +pub mod ERC721ReceiverMock { + use openzeppelin_introspection::src5::SRC5Component; + use erc721::erc721::IERC721Receiver; + use starknet::ContractAddress; + + component!(path: SRC5Component, storage: src5, event: SRC5Event); + + // SRC5 + #[abi(embed_v0)] + impl SRC5Impl = SRC5Component::SRC5Impl; + + #[storage] + pub struct Storage { + #[substorage(v0)] + pub src5: SRC5Component::Storage + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + SRC5Event: SRC5Component::Event + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.src5.register_interface(IERC721_RECEIVER_ID); + } + + #[abi(per_item)] + impl ExternalImpl of IERC721Receiver { + #[external(v0)] + fn on_erc721_received( + self: @ContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span + ) -> felt252 { + if *data.at(0) == super::SUCCESS { + IERC721_RECEIVER_ID + } else { + 0 + } + } + } +} \ No newline at end of file diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index ca10091e..cf2d8265 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -8,6 +8,9 @@ use snforge_std::{ }; use starknet::{ContractAddress, contract_address_const}; +pub const SUCCESS: felt252 = 'SUCCESS'; +pub const FAILURE: felt252 = 'FAILURE'; +pub const PUBKEY: felt252 = 'PUBKEY'; pub const TOKEN_ID: u256 = 21; pub fn CALLER() -> ContractAddress { @@ -38,14 +41,37 @@ pub fn ZERO() -> ContractAddress { contract_address_const::<0>() } -fn deploy() -> (IERC721Dispatcher, ContractAddress) { - let contract = declare("ERC721").unwrap().contract_class(); +pub fn DATA(success: bool) -> Span { + let value = if success { + SUCCESS + } else { + FAILURE + }; + array![value].span() +} + +fn deploy_account() -> ContractAddress { + let contract = declare("AccountMock").unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@array![PUBKEY]).unwrap(); + contract_address +} + +fn deploy_receiver() -> ContractAddress { + let contract = declare("ERC721ReceiverMock").unwrap().contract_class(); let (contract_address, _) = contract.deploy(@array![]).unwrap(); - (IERC721Dispatcher { contract_address }, contract_address) + contract_address +} + +fn deploy_non_receiver() -> ContractAddress { + let contract = declare("NonReceiverMock").unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@array![]).unwrap(); + contract_address } fn setup() -> (IERC721Dispatcher, ContractAddress) { - let (contract, contract_address) = deploy(); + let contract = declare("ERC721").unwrap().contract_class(); + let (contract_address, _) = contract.deploy(@array![]).unwrap(); + let contract = IERC721Dispatcher { contract_address }; contract.mint(OWNER(), TOKEN_ID); (contract, contract_address) } @@ -394,6 +420,188 @@ fn test_transfer_from_unauthorized() { contract.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); } +// +// safe_transfer_from +// + +#[test] +fn test_safe_transfer_from_to_account() { + let (mut contract, contract_address) = setup(); + let account = deploy_account(); + let mut spy = spy_events(); + let token_id = TOKEN_ID; + let owner = OWNER(); + + assert_state_before_transfer(contract, owner, account, token_id); + + start_cheat_caller_address(contract_address, owner); + contract.safe_transfer_from(owner, account, token_id, DATA(true)); + spy + .assert_emitted( + @array![ + (contract_address, Event::Transfer(Transfer { from: owner, to: account, token_id })) + ] + ); + + assert_state_after_transfer(contract, owner, account, token_id); +} + +#[test] +fn test_safe_transfer_from_to_receiver() { + let (mut contract, contract_address) = setup(); + let receiver = deploy_receiver(); + let mut spy = spy_events(); + let token_id = TOKEN_ID; + let owner = OWNER(); + + assert_state_before_transfer(contract, owner, receiver, token_id); + + start_cheat_caller_address(contract_address, owner); + contract.safe_transfer_from(owner, receiver, token_id, DATA(true)); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: receiver, token_id }) + ) + ] + ); + + assert_state_after_transfer(contract, owner, receiver, token_id); +} + +#[test] +#[should_panic(expected: ('ERC721: safe transfer failed',))] +fn test_safe_transfer_from_to_receiver_failure() { + let (mut contract, contract_address) = setup(); + let receiver = deploy_receiver(); + let token_id = TOKEN_ID; + let owner = OWNER(); + + start_cheat_caller_address(contract_address, owner); + contract.safe_transfer_from(owner, receiver, token_id, DATA(false)); +} + +#[test] +#[ignore] // REASON: should_panic attribute not fit for complex panic messages. +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +fn test_safe_transfer_from_to_non_receiver() { + let (mut contract, contract_address) = setup(); + let recipient = deploy_non_receiver(); + let token_id = TOKEN_ID; + let owner = OWNER(); + + start_cheat_caller_address(contract_address, owner); + contract.safe_transfer_from(owner, recipient, token_id, DATA(true)); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_safe_transfer_from_nonexistent() { + let (mut contract, contract_address) = setup(); + start_cheat_caller_address(contract_address, OWNER()); + contract.safe_transfer_from(ZERO(), RECIPIENT(), TOKEN_ID, DATA(true)); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test_safe_transfer_from_to_zero() { + let (mut contract, contract_address) = setup(); + start_cheat_caller_address(contract_address, OWNER()); + contract.safe_transfer_from(OWNER(), ZERO(), TOKEN_ID, DATA(true)); +} + +#[test] +fn test_safe_transfer_from_to_owner() { + let (mut contract, contract_address) = setup(); + let token_id = TOKEN_ID; + let owner = deploy_receiver(); + + assert_eq!(contract.owner_of(token_id), owner); + assert_eq!(contract.balance_of(owner), 1); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, owner); + contract.safe_transfer_from(owner, owner, token_id, DATA(true)); + spy + .assert_emitted( + @array![ + (contract_address, Event::Transfer(Transfer { from: owner, to: owner, token_id })) + ] + ); + + assert_eq!(contract.owner_of(token_id), owner); + assert_eq!(contract.balance_of(owner), 1); +} + +#[test] +fn test_safe_transfer_from_approved() { + let (mut contract, contract_address) = setup(); + let receiver = deploy_receiver(); + let token_id = TOKEN_ID; + let owner = OWNER(); + + assert_state_before_transfer(contract, owner, receiver, token_id); + + start_cheat_caller_address(contract_address, owner); + contract.approve(OPERATOR(), token_id); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OPERATOR()); + contract.safe_transfer_from(owner, receiver, token_id, DATA(true)); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: receiver, token_id }) + ) + ] + ); + + assert_state_after_transfer(contract, owner, receiver, token_id); +} + +#[test] +fn test_safe_transfer_from_approved_for_all() { + let (mut contract, contract_address) = setup(); + let receiver = deploy_receiver(); + let token_id = TOKEN_ID; + let owner = OWNER(); + + assert_state_before_transfer(contract, owner, receiver, token_id); + + start_cheat_caller_address(contract_address, owner); + contract.set_approval_for_all(OPERATOR(), true); + + let mut spy = spy_events(); + + start_cheat_caller_address(contract_address, OPERATOR()); + contract.safe_transfer_from(owner, receiver, token_id, DATA(true)); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: receiver, token_id }) + ) + ] + ); + + assert_state_after_transfer(contract, owner, receiver, token_id); +} + +#[test] +#[should_panic(expected: ('ERC721: unauthorized caller',))] +fn test_safe_transfer_from_unauthorized() { + let (mut contract, contract_address) = setup(); + start_cheat_caller_address(contract_address, OTHER()); + contract.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); +} + // // Helpers // From 9e7ab1df0d0c698132b84889d6fa81a465baceac Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 11:11:28 +0200 Subject: [PATCH 13/50] Add mint tests --- listings/applications/erc721/src/tests.cairo | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index cf2d8265..2d6f0d73 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -602,6 +602,49 @@ fn test_safe_transfer_from_unauthorized() { contract.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); } +// +// mint +// + +#[test] +fn test_mint() { + let (mut contract, contract_address) = setup(); + let mut spy = spy_events(); + let recipient = RECIPIENT(); + let token_id = TOKEN_ID; + + assert!(contract.balance_of(recipient).is_zero()); + + contract.mint(recipient, TOKEN_ID); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: ZERO(), to: recipient, token_id }) + ) + ] + ); + + assert_eq!(contract.owner_of(token_id), recipient); + assert_eq!(contract.balance_of(recipient), 1); + assert!(contract.get_approved(token_id).is_zero()); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid receiver',))] +fn test_mint_to_zero() { + let (mut contract, _) = setup(); + contract.mint(ZERO(), TOKEN_ID); +} + +#[test] +#[should_panic(expected: ('ERC721: token already minted',))] +fn test_mint_already_exist() { + let (mut contract, _) = setup(); + contract.mint(RECIPIENT(), TOKEN_ID); +} + // // Helpers // From 4cb576f51654cff14e460eb6ad8084b00f4bd748 Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 11:13:59 +0200 Subject: [PATCH 14/50] Add burn tests --- listings/applications/erc721/src/tests.cairo | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 2d6f0d73..5de85008 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -645,6 +645,46 @@ fn test_mint_already_exist() { contract.mint(RECIPIENT(), TOKEN_ID); } +// +// burn +// + +#[test] +fn test_burn() { + let (mut contract, contract_address) = setup(); + + start_cheat_caller_address(contract_address, OWNER()); + contract.approve(OTHER(), TOKEN_ID); + + assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); + assert_eq!(contract.balance_of(OWNER()), 1); + assert_eq!(contract.get_approved(TOKEN_ID), OTHER()); + + let mut spy = spy_events(); + + contract.burn(TOKEN_ID); + spy + .assert_emitted( + @array![ + ( + contract_address, + Event::Transfer(Transfer { from: OWNER(), to: ZERO(), token_id: TOKEN_ID }) + ) + ] + ); + + assert_eq!(contract.owner_of(TOKEN_ID), ZERO()); + assert_eq!(contract.balance_of(OWNER()), 0); + assert_eq!(contract.get_approved(TOKEN_ID), ZERO()); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test_burn_nonexistent() { + let (mut contract, _) = setup(); + contract.burn(TOKEN_ID); +} + // // Helpers // From 282fe6b5b550011c2a05e08962657a381cda8b9c Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 11:52:00 +0200 Subject: [PATCH 15/50] Add internal tests --- listings/applications/erc721/src/erc721.cairo | 10 +-- listings/applications/erc721/src/tests.cairo | 76 ++++++++++++++++++- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index 97c6853e..1ebab52e 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -51,11 +51,11 @@ pub mod ERC721 { use super::{IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID}; #[storage] - struct Storage { - owners: Map, - balances: Map, - approvals: Map, - operator_approvals: Map<(ContractAddress, ContractAddress), bool>, + pub struct Storage { + pub owners: Map, + pub balances: Map, + pub approvals: Map, + pub operator_approvals: Map<(ContractAddress, ContractAddress), bool>, } #[event] diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 5de85008..f9a36715 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -1,12 +1,13 @@ use core::num::traits::Zero; use erc721::erc721::{ - IERC721Dispatcher, IERC721DispatcherTrait, ERC721::{Event, Transfer, Approval, ApprovalForAll} + IERC721Dispatcher, IERC721DispatcherTrait, ERC721, IERC721, + ERC721::{Event, Transfer, Approval, ApprovalForAll, InternalTrait}, }; use snforge_std::{ - declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, spy_events, - EventSpyAssertionsTrait, + declare, test_address, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, + spy_events, EventSpyAssertionsTrait, }; -use starknet::{ContractAddress, contract_address_const}; +use starknet::{ContractAddress, contract_address_const,}; pub const SUCCESS: felt252 = 'SUCCESS'; pub const FAILURE: felt252 = 'FAILURE'; @@ -685,6 +686,73 @@ fn test_burn_nonexistent() { contract.burn(TOKEN_ID); } +// +// Internals +// + +fn setup_internals() -> ERC721::ContractState { + let mut state = ERC721::contract_state_for_testing(); + state.mint(OWNER(), TOKEN_ID); + state +} + +#[test] +fn test__require_owned() { + let mut state = setup_internals(); + let owner = state._require_owned(TOKEN_ID); + assert_eq!(owner, OWNER()); +} + +#[test] +#[should_panic(expected: ('ERC721: invalid token ID',))] +fn test__require_owned_non_existent() { + let mut state = setup_internals(); + state._require_owned(0x123); +} + +#[test] +fn test__is_approved_or_owner_owner() { + let mut state = setup_internals(); + let authorized = state._is_approved_or_owner(OWNER(), OWNER(), TOKEN_ID); + assert!(authorized); +} + +#[test] +fn test__is_approved_or_owner_approved_for_all() { + let mut state = setup_internals(); + + start_cheat_caller_address(test_address(), OWNER()); + state.set_approval_for_all(SPENDER(), true); + + let authorized = state._is_approved_or_owner(OWNER(), SPENDER(), TOKEN_ID); + assert!(authorized); +} + +#[test] +fn test__is_approved_or_owner_approved() { + let mut state = setup_internals(); + + start_cheat_caller_address(test_address(), OWNER()); + state.approve(SPENDER(), TOKEN_ID); + + let authorized = state._is_approved_or_owner(OWNER(), SPENDER(), TOKEN_ID); + assert!(authorized); +} + +#[test] +fn test__is_approved_or_owner_not_authorized() { + let mut state = setup_internals(); + let not_authorized = !state._is_approved_or_owner(OWNER(), CALLER(), TOKEN_ID); + assert!(not_authorized); +} + +#[test] +fn test__is_approved_or_owner_zero_address() { + let mut state = setup_internals(); + let not_authorized = !state._is_approved_or_owner(OWNER(), ZERO(), TOKEN_ID); + assert!(not_authorized); +} + // // Helpers // From e5e84e9006169af33ca0655e556acb899fc34d9f Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 14:43:37 +0200 Subject: [PATCH 16/50] Move interfaces into interfaces.cairo + fix build errors --- listings/applications/erc721/error.log | 5 -- listings/applications/erc721/src/erc721.cairo | 52 ++----------------- .../applications/erc721/src/interfaces.cairo | 43 +++++++++++++++ listings/applications/erc721/src/lib.cairo | 1 + listings/applications/erc721/src/mocks.cairo | 2 +- .../erc721/src/mocks/account.cairo | 2 +- .../erc721/src/mocks/receiver.cairo | 8 +-- listings/applications/erc721/src/tests.cairo | 6 +-- src/applications/erc721.md | 4 +- 9 files changed, 58 insertions(+), 65 deletions(-) delete mode 100644 listings/applications/erc721/error.log create mode 100644 listings/applications/erc721/src/interfaces.cairo diff --git a/listings/applications/erc721/error.log b/listings/applications/erc721/error.log deleted file mode 100644 index 2e85421c..00000000 --- a/listings/applications/erc721/error.log +++ /dev/null @@ -1,5 +0,0 @@ - Compiling snforge_scarb_plugin v0.2.0 - Finished `release` profile [optimized] target(s) in 0.05s - Compiling lib(erc721) erc721 v0.1.0 (/home/neshi/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) - Compiling starknet-contract(erc721) erc721 v0.1.0 (/home/neshi/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) -warn: external contracts not found for selectors: `openzeppelin_introspection::interface::ISRC5` diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index 1ebab52e..6f948f85 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -1,46 +1,3 @@ -use starknet::ContractAddress; - -// ANCHOR: interface -#[starknet::interface] -pub trait IERC721 { - fn balance_of(self: @TContractState, owner: ContractAddress) -> u256; - fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; - fn safe_transfer_from( - ref self: TContractState, - from: ContractAddress, - to: ContractAddress, - token_id: u256, - data: Span - ); - fn transfer_from( - ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256 - ); - fn approve(ref self: TContractState, approved: ContractAddress, token_id: u256); - fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool); - fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; - fn is_approved_for_all( - self: @TContractState, owner: ContractAddress, operator: ContractAddress - ) -> bool; - fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); - fn burn(ref self: TContractState, token_id: u256); -} - -pub const IERC721_RECEIVER_ID: felt252 = - 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; - -#[starknet::interface] -pub trait IERC721Receiver { - fn on_erc721_received( - self: @TContractState, - operator: ContractAddress, - from: ContractAddress, - token_id: u256, - data: Span - ) -> felt252; -} -// ANCHOR_END: interface - -// ANCHOR: erc721 #[starknet::contract] pub mod ERC721 { use core::num::traits::Zero; @@ -48,7 +5,9 @@ pub mod ERC721 { use starknet::ContractAddress; use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use openzeppelin_introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; - use super::{IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID}; + use erc721::interfaces::{ + IERC721, IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID + }; #[storage] pub struct Storage { @@ -99,7 +58,7 @@ pub mod ERC721 { } #[abi(embed_v0)] - impl ERC721 of super::IERC721 { + impl ERC721 of IERC721 { fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { self._require_owned(token_id) } @@ -231,6 +190,3 @@ pub mod ERC721 { } } } -// ANCHOR_END: erc721 - - diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo new file mode 100644 index 00000000..a385bcc9 --- /dev/null +++ b/listings/applications/erc721/src/interfaces.cairo @@ -0,0 +1,43 @@ +use starknet::ContractAddress; + +// ANCHOR: interface +#[starknet::interface] +pub trait IERC721 { + fn balance_of(self: @TContractState, owner: ContractAddress) -> u256; + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; + fn safe_transfer_from( + ref self: TContractState, + from: ContractAddress, + to: ContractAddress, + token_id: u256, + data: Span + ); + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ); + fn approve(ref self: TContractState, approved: ContractAddress, token_id: u256); + fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool); + fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; + fn is_approved_for_all( + self: @TContractState, owner: ContractAddress, operator: ContractAddress + ) -> bool; + fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); + fn burn(ref self: TContractState, token_id: u256); +} + +pub const IERC721_RECEIVER_ID: felt252 = + 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; + +#[starknet::interface] +pub trait IERC721Receiver { + fn on_erc721_received( + self: @TContractState, + operator: ContractAddress, + from: ContractAddress, + token_id: u256, + data: Span + ) -> felt252; +} +// ANCHOR_END: interface + + diff --git a/listings/applications/erc721/src/lib.cairo b/listings/applications/erc721/src/lib.cairo index 8aef62c8..c5084627 100644 --- a/listings/applications/erc721/src/lib.cairo +++ b/listings/applications/erc721/src/lib.cairo @@ -1,4 +1,5 @@ pub mod erc721; +pub mod interfaces; mod mocks; #[cfg(test)] diff --git a/listings/applications/erc721/src/mocks.cairo b/listings/applications/erc721/src/mocks.cairo index 3058ce37..0cffa4d7 100644 --- a/listings/applications/erc721/src/mocks.cairo +++ b/listings/applications/erc721/src/mocks.cairo @@ -4,4 +4,4 @@ pub mod non_receiver; pub use account::AccountMock; pub use non_receiver::NonReceiverMock; -pub use receiver::ERC721ReceiverMock; \ No newline at end of file +pub use receiver::ERC721ReceiverMock; diff --git a/listings/applications/erc721/src/mocks/account.cairo b/listings/applications/erc721/src/mocks/account.cairo index d26d2585..68533035 100644 --- a/listings/applications/erc721/src/mocks/account.cairo +++ b/listings/applications/erc721/src/mocks/account.cairo @@ -43,4 +43,4 @@ pub mod AccountMock { fn constructor(ref self: ContractState, public_key: felt252) { self.account.initializer(public_key); } -} \ No newline at end of file +} diff --git a/listings/applications/erc721/src/mocks/receiver.cairo b/listings/applications/erc721/src/mocks/receiver.cairo index 6beb3abd..1c18915c 100644 --- a/listings/applications/erc721/src/mocks/receiver.cairo +++ b/listings/applications/erc721/src/mocks/receiver.cairo @@ -3,7 +3,7 @@ const SUCCESS: felt252 = 'SUCCESS'; #[starknet::contract] pub mod ERC721ReceiverMock { use openzeppelin_introspection::src5::SRC5Component; - use erc721::erc721::IERC721Receiver; + use erc721::interfaces::{IERC721Receiver, IERC721_RECEIVER_ID}; use starknet::ContractAddress; component!(path: SRC5Component, storage: src5, event: SRC5Event); @@ -11,6 +11,7 @@ pub mod ERC721ReceiverMock { // SRC5 #[abi(embed_v0)] impl SRC5Impl = SRC5Component::SRC5Impl; + impl SRC5InternalImpl = SRC5Component::InternalImpl; #[storage] pub struct Storage { @@ -30,9 +31,8 @@ pub mod ERC721ReceiverMock { self.src5.register_interface(IERC721_RECEIVER_ID); } - #[abi(per_item)] + #[abi(embed_v0)] impl ExternalImpl of IERC721Receiver { - #[external(v0)] fn on_erc721_received( self: @ContractState, operator: ContractAddress, @@ -47,4 +47,4 @@ pub mod ERC721ReceiverMock { } } } -} \ No newline at end of file +} diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index f9a36715..9d5d2125 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -1,8 +1,6 @@ use core::num::traits::Zero; -use erc721::erc721::{ - IERC721Dispatcher, IERC721DispatcherTrait, ERC721, IERC721, - ERC721::{Event, Transfer, Approval, ApprovalForAll, InternalTrait}, -}; +use erc721::interfaces::{IERC721, IERC721Dispatcher, IERC721DispatcherTrait,}; +use erc721::erc721::{ERC721, ERC721::{Event, Transfer, Approval, ApprovalForAll, InternalTrait},}; use snforge_std::{ declare, test_address, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, spy_events, EventSpyAssertionsTrait, diff --git a/src/applications/erc721.md b/src/applications/erc721.md index 4dc4cab3..266cd408 100644 --- a/src/applications/erc721.md +++ b/src/applications/erc721.md @@ -5,7 +5,7 @@ Contracts that follow the [ERC721 Standard](https://eips.ethereum.org/EIPS/eip-7 To create an ERC721 contract, it must implement the following interface: ```rust -{{#include ../../listings/applications/721/src/token.cairo:interface}} +{{#include ../../listings/applications/721/src/interfaces.cairo:interface}} ``` Becase function names in Starknet should be written in _snake_case_, the Starknet ERC721 interface is slightly different from the Solidity ERC721 interface which uses _camelCase_. @@ -13,7 +13,7 @@ Becase function names in Starknet should be written in _snake_case_, the Starkne Here's an implementation of the ERC721 interface in Cairo: ```rust -{{#include ../../listings/applications/721/src/token.cairo:erc721}} +{{#include ../../listings/applications/721/src/erc721.cairo}} ``` There are other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc721) one. From 6bf7d66a622f9e3d5978553cb34dfc015306cfea Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 15:41:14 +0200 Subject: [PATCH 17/50] fix tests --- listings/applications/erc721/error.log | 16 +++++ listings/applications/erc721/src/erc721.cairo | 1 + listings/applications/erc721/src/tests.cairo | 65 ++++++++++++------- 3 files changed, 57 insertions(+), 25 deletions(-) create mode 100644 listings/applications/erc721/error.log diff --git a/listings/applications/erc721/error.log b/listings/applications/erc721/error.log new file mode 100644 index 00000000..585dfbd4 --- /dev/null +++ b/listings/applications/erc721/error.log @@ -0,0 +1,16 @@ + Compiling snforge_scarb_plugin v0.2.0 + Finished `release` profile [optimized] target(s) in 0.11s + Compiling lib(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) + Compiling starknet-contract(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) +warn: external contracts not found for selectors: `openzeppelin_introspection::interface::ISRC5` + Finished release target(s) in 42 seconds +[WARNING] Package snforge_std version does not meet the recommended version requirement =0.31.0, it might result in unexpected behaviour + Compiling snforge_scarb_plugin v0.2.0 + Finished `release` profile [optimized] target(s) in 0.11s + Compiling lib(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) + Compiling starknet-contract(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) +warn: external contracts not found for selectors: `openzeppelin_introspection::interface::ISRC5` + Finished release target(s) in 42 seconds + Compiling snforge_scarb_plugin v0.2.0 + Finished `release` profile [optimized] target(s) in 0.05s + Compiling test(erc721_unittest) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index 6f948f85..d5268a29 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -107,6 +107,7 @@ pub mod ERC721 { 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.approvals.write(token_id, to); self.emit(Transfer { from, to, token_id }); } diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 9d5d2125..4d8282a8 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -6,11 +6,13 @@ use snforge_std::{ spy_events, EventSpyAssertionsTrait, }; use starknet::{ContractAddress, contract_address_const,}; +use starknet::storage::StorageMapReadAccess; pub const SUCCESS: felt252 = 'SUCCESS'; pub const FAILURE: felt252 = 'FAILURE'; pub const PUBKEY: felt252 = 'PUBKEY'; pub const TOKEN_ID: u256 = 21; +pub const NONEXISTENT_TOKEN_ID: u256 = 7; pub fn CALLER() -> ContractAddress { contract_address_const::<'CALLER'>() @@ -68,13 +70,29 @@ fn deploy_non_receiver() -> ContractAddress { } fn setup() -> (IERC721Dispatcher, ContractAddress) { + _setup_optional_mint(true) +} + +fn setup_no_mint() -> (IERC721Dispatcher, ContractAddress) { + _setup_optional_mint(false) +} + +fn _setup_optional_mint(should_mint: bool) -> (IERC721Dispatcher, ContractAddress) { let contract = declare("ERC721").unwrap().contract_class(); let (contract_address, _) = contract.deploy(@array![]).unwrap(); let contract = IERC721Dispatcher { contract_address }; - contract.mint(OWNER(), TOKEN_ID); + if should_mint { + contract.mint(OWNER(), TOKEN_ID); + } (contract, contract_address) } +fn setup_internals() -> ERC721::ContractState { + let mut state = ERC721::contract_state_for_testing(); + state.mint(OWNER(), TOKEN_ID); + state +} + // // Getters // @@ -102,7 +120,7 @@ fn test_owner_of() { #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_owner_of_non_minted() { let (contract, _) = setup(); - contract.owner_of(7); + contract.owner_of(NONEXISTENT_TOKEN_ID); } #[test] @@ -122,7 +140,7 @@ fn test_get_approved() { #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_get_approved_nonexistent() { let (contract, _) = setup(); - contract.get_approved(7); + contract.get_approved(NONEXISTENT_TOKEN_ID); } // @@ -193,7 +211,7 @@ fn test_approve_from_unauthorized() { #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_approve_nonexistent() { let (mut contract, _) = setup(); - contract.approve(SPENDER(), TOKEN_ID); + contract.approve(SPENDER(), NONEXISTENT_TOKEN_ID); } #[test] @@ -318,7 +336,7 @@ fn test_transfer_from_owner() { fn test_transfer_from_nonexistent() { let (mut contract, contract_address) = setup(); start_cheat_caller_address(contract_address, OWNER()); - contract.transfer_from(ZERO(), RECIPIENT(), TOKEN_ID); + contract.transfer_from(ZERO(), RECIPIENT(), NONEXISTENT_TOKEN_ID); } #[test] @@ -500,7 +518,7 @@ fn test_safe_transfer_from_to_non_receiver() { fn test_safe_transfer_from_nonexistent() { let (mut contract, contract_address) = setup(); start_cheat_caller_address(contract_address, OWNER()); - contract.safe_transfer_from(ZERO(), RECIPIENT(), TOKEN_ID, DATA(true)); + contract.safe_transfer_from(ZERO(), RECIPIENT(), NONEXISTENT_TOKEN_ID, DATA(true)); } #[test] @@ -513,10 +531,12 @@ fn test_safe_transfer_from_to_zero() { #[test] fn test_safe_transfer_from_to_owner() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup_no_mint(); let token_id = TOKEN_ID; let owner = deploy_receiver(); + contract.mint(owner, token_id); + assert_eq!(contract.owner_of(token_id), owner); assert_eq!(contract.balance_of(owner), 1); @@ -650,18 +670,19 @@ fn test_mint_already_exist() { #[test] fn test_burn() { - let (mut contract, contract_address) = setup(); + let mut state = setup_internals(); + let contract_address = test_address(); start_cheat_caller_address(contract_address, OWNER()); - contract.approve(OTHER(), TOKEN_ID); + state.approve(OTHER(), TOKEN_ID); - assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); - assert_eq!(contract.balance_of(OWNER()), 1); - assert_eq!(contract.get_approved(TOKEN_ID), OTHER()); + assert_eq!(state.owner_of(TOKEN_ID), OWNER()); + assert_eq!(state.balance_of(OWNER()), 1); + assert_eq!(state.get_approved(TOKEN_ID), OTHER()); let mut spy = spy_events(); - contract.burn(TOKEN_ID); + state.burn(TOKEN_ID); spy .assert_emitted( @array![ @@ -672,28 +693,22 @@ fn test_burn() { ] ); - assert_eq!(contract.owner_of(TOKEN_ID), ZERO()); - assert_eq!(contract.balance_of(OWNER()), 0); - assert_eq!(contract.get_approved(TOKEN_ID), ZERO()); + assert_eq!(state.owners.read(TOKEN_ID), ZERO()); + assert_eq!(state.balance_of(OWNER()), 0); + assert_eq!(state.approvals.read(TOKEN_ID), ZERO()); } #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_burn_nonexistent() { let (mut contract, _) = setup(); - contract.burn(TOKEN_ID); + contract.burn(NONEXISTENT_TOKEN_ID); } // // Internals // -fn setup_internals() -> ERC721::ContractState { - let mut state = ERC721::contract_state_for_testing(); - state.mint(OWNER(), TOKEN_ID); - state -} - #[test] fn test__require_owned() { let mut state = setup_internals(); @@ -703,9 +718,9 @@ fn test__require_owned() { #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] -fn test__require_owned_non_existent() { +fn test__require_owned_nonexistent() { let mut state = setup_internals(); - state._require_owned(0x123); + state._require_owned(NONEXISTENT_TOKEN_ID); } #[test] From 69bc00a9cc4d2045f5ad23c369a73726003ae39b Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 17 Oct 2024 17:16:12 +0200 Subject: [PATCH 18/50] Fix approvals update in transfer_from --- listings/applications/erc721/error.log | 16 ---------------- listings/applications/erc721/src/erc721.cairo | 2 +- listings/applications/erc721/src/tests.cairo | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 listings/applications/erc721/error.log diff --git a/listings/applications/erc721/error.log b/listings/applications/erc721/error.log deleted file mode 100644 index 585dfbd4..00000000 --- a/listings/applications/erc721/error.log +++ /dev/null @@ -1,16 +0,0 @@ - Compiling snforge_scarb_plugin v0.2.0 - Finished `release` profile [optimized] target(s) in 0.11s - Compiling lib(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) - Compiling starknet-contract(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) -warn: external contracts not found for selectors: `openzeppelin_introspection::interface::ISRC5` - Finished release target(s) in 42 seconds -[WARNING] Package snforge_std version does not meet the recommended version requirement =0.31.0, it might result in unexpected behaviour - Compiling snforge_scarb_plugin v0.2.0 - Finished `release` profile [optimized] target(s) in 0.11s - Compiling lib(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) - Compiling starknet-contract(erc721) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) -warn: external contracts not found for selectors: `openzeppelin_introspection::interface::ISRC5` - Finished release target(s) in 42 seconds - Compiling snforge_scarb_plugin v0.2.0 - Finished `release` profile [optimized] target(s) in 0.05s - Compiling test(erc721_unittest) erc721 v0.1.0 (/home/nenad/repos/StarknetByExample/listings/applications/erc721/Scarb.toml) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index d5268a29..e6ce2db6 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -107,7 +107,7 @@ pub mod ERC721 { 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.approvals.write(token_id, to); + self.approvals.write(token_id, Zero::zero()); self.emit(Transfer { from, to, token_id }); } diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 4d8282a8..832b31a3 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -627,7 +627,7 @@ fn test_safe_transfer_from_unauthorized() { #[test] fn test_mint() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup_no_mint(); let mut spy = spy_events(); let recipient = RECIPIENT(); let token_id = TOKEN_ID; From 09b01b72b76d02988257f323555853630d9f93eb Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 18 Oct 2024 14:22:43 +0200 Subject: [PATCH 19/50] Remove redundant build-external-contracts section from Scarb.toml --- listings/applications/erc721/Scarb.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml index 6ee4ef9d..24eb61b2 100644 --- a/listings/applications/erc721/Scarb.toml +++ b/listings/applications/erc721/Scarb.toml @@ -17,4 +17,3 @@ assert_macros.workspace = true test.workspace = true [[target.starknet-contract]] -build-external-contracts = ["openzeppelin_introspection::interface::ISRC5"] From 9e79fb3fba55bee9c968fe0f48c1ecf6eeb70e2e Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 18 Oct 2024 14:36:42 +0200 Subject: [PATCH 20/50] Move snforge_std dep to dev deps --- listings/applications/erc721/Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml index 24eb61b2..8424f58c 100644 --- a/listings/applications/erc721/Scarb.toml +++ b/listings/applications/erc721/Scarb.toml @@ -8,10 +8,10 @@ edition = "2024_07" [dependencies] starknet.workspace = true openzeppelin.workspace = true -snforge_std.workspace = true [dev-dependencies] assert_macros.workspace = true +snforge_std.workspace = true [scripts] test.workspace = true From 20b34b1a55f1b56809f597fcd549f7af4411f869 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 18 Oct 2024 14:36:55 +0200 Subject: [PATCH 21/50] Set edition to workspace version --- listings/applications/erc721/Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml index 8424f58c..212e2098 100644 --- a/listings/applications/erc721/Scarb.toml +++ b/listings/applications/erc721/Scarb.toml @@ -1,7 +1,7 @@ [package] name = "erc721" version.workspace = true -edition = "2024_07" +edition.workspace = true [lib] From aa3c4a25f90f4b8500d05dedf6951895a27529b6 Mon Sep 17 00:00:00 2001 From: Nenad Date: Fri, 18 Oct 2024 14:50:44 +0200 Subject: [PATCH 22/50] Revert edition to specific version + remove [lib] --- listings/applications/erc721/Scarb.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml index 212e2098..59743c4c 100644 --- a/listings/applications/erc721/Scarb.toml +++ b/listings/applications/erc721/Scarb.toml @@ -1,9 +1,7 @@ [package] name = "erc721" version.workspace = true -edition.workspace = true - -[lib] +edition = "2024_07" [dependencies] starknet.workspace = true From 0650e1e036fd291d837a032024b806c2d608babb Mon Sep 17 00:00:00 2001 From: Nenad Date: Thu, 24 Oct 2024 10:05:00 +0200 Subject: [PATCH 23/50] Update edition to point to workspace --- listings/applications/erc721/Scarb.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml index 59743c4c..acb38eab 100644 --- a/listings/applications/erc721/Scarb.toml +++ b/listings/applications/erc721/Scarb.toml @@ -1,7 +1,7 @@ [package] name = "erc721" version.workspace = true -edition = "2024_07" +edition.workspace = true [dependencies] starknet.workspace = true From badf4dbdf5fbb669b3786925ebea25a667ecf2d6 Mon Sep 17 00:00:00 2001 From: Julio <30329843+julio4@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:37:59 +0700 Subject: [PATCH 24/50] Prepare release 2.8.2 (#257) * dep: fix patch to latest shikijs for cairo hl * fix(app): correct sidebar placement * fix(app): responsive content centering * fix(app): responsive content centering * doc: branch guidelines * fix(app): top section nav links --- CONTRIBUTING.md | 6 + package.json | 8 + patches/vocs.patch | 1416 ++------------------------------------------ pnpm-lock.yaml | 135 +++-- routes.ts | 4 - styles.css | 95 +-- 6 files changed, 183 insertions(+), 1481 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ac94b34..0543b466 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,12 @@ When contributing to this repository, please first discuss the change you wish t Join the telegram channel: https://t.me/StarknetByExample +The release branch is `main`. The development branch is `dev` and is considered stable (but not released yet). +When you want to contribute, please create a new branch from `dev` and open a pull request to merge your changes back into `dev`. +You should never open a pull request to merge your changes directly into `main`. + +The `dev` branch is deployed at https://starknet-by-example-dev.voyager.online/ + Please note we have a code of conduct, please follow it in all your interactions with the project. ## Table of Contents diff --git a/package.json b/package.json index 032ce81a..4d683183 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,14 @@ "pnpm": { "patchedDependencies": { "vocs": "patches/vocs.patch" + }, + "overrides": { + "vocs>shiki": "^1.23.0", + "vocs>@shikijs/core": "^1.23.0", + "vocs>@shikijs/types": "^1.23.0", + "vocs>@shikijs/rehype": "^1.23.0", + "vocs>@shikijs/twoslash": "^1.23.0", + "vocs>@shikijs/transformers": "^1.23.0" } } } diff --git a/patches/vocs.patch b/patches/vocs.patch index 0bd0899a..db1208c9 100644 --- a/patches/vocs.patch +++ b/patches/vocs.patch @@ -1,1394 +1,46 @@ -diff --git a/_lib/vite/plugins/cairo.json b/_lib/vite/plugins/cairo.json -new file mode 100644 -index 0000000000000000000000000000000000000000..9078642c2c1dc8b472469c269619b1898734459c ---- /dev/null -+++ b/_lib/vite/plugins/cairo.json -@@ -0,0 +1,996 @@ -+{ -+ "credits": "Pulled from: https://raw.githubusercontent.com/starkware-libs/cairo/main/vscode-cairo/syntaxes/cairo.tmLanguage.json. This grammar is heavily modified Rust grammar from VSCode repository: https://github.com/microsoft/vscode/blob/11f415d4d77f7739c202905ccd02e27774146b75/extensions/rust/syntaxes/rust.tmLanguage.json", -+ "name": "Cairo", -+ "scopeName": "source.cairo", -+ "patterns": [ -+ { -+ "comment": "boxed slice literal", -+ "begin": "(<)(\\[)", -+ "beginCaptures": { -+ "1": { -+ "name": "punctuation.brackets.angle.cairo" -+ }, -+ "2": { -+ "name": "punctuation.brackets.square.cairo" -+ } -+ }, -+ "end": ">", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.brackets.angle.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#gtypes" -+ }, -+ { -+ "include": "#lvariables" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#types" -+ } -+ ] -+ }, -+ { -+ "comment": "modules", -+ "match": "(mod)\\s+([a-z][A-Za-z0-9_]*)", -+ "captures": { -+ "1": { -+ "name": "storage.type.cairo" -+ }, -+ "2": { -+ "name": "entity.name.module.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "use statements", -+ "name": "meta.use.cairo", -+ "begin": "\\b(use)\\s", -+ "beginCaptures": { -+ "1": { -+ "name": "keyword.other.cairo" -+ } -+ }, -+ "end": ";", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.semi.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#keywords" -+ }, -+ { -+ "include": "#namespaces" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#types" -+ }, -+ { -+ "include": "#lvariables" -+ } -+ ] -+ }, -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#attributes" -+ }, -+ { -+ "include": "#lvariables" -+ }, -+ { -+ "include": "#constants" -+ }, -+ { -+ "include": "#gtypes" -+ }, -+ { -+ "include": "#functions" -+ }, -+ { -+ "include": "#types" -+ }, -+ { -+ "include": "#keywords" -+ }, -+ { -+ "include": "#macros" -+ }, -+ { -+ "include": "#namespaces" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#strings" -+ }, -+ { -+ "include": "#variables" -+ } -+ ], -+ "repository": { -+ "comments": { -+ "patterns": [ -+ { -+ "comment": "documentation comments", -+ "name": "comment.line.documentation.cairo", -+ "match": "(///).*$", -+ "captures": { -+ "1": { -+ "name": "punctuation.definition.comment.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "line comments", -+ "name": "comment.line.double-slash.cairo", -+ "match": "(//).*$", -+ "captures": { -+ "1": { -+ "name": "punctuation.definition.comment.cairo" -+ } -+ } -+ } -+ ] -+ }, -+ "block-comments": { -+ "patterns": [ -+ { -+ "comment": "empty block comments", -+ "name": "comment.block.cairo", -+ "match": "/\\*\\*/" -+ }, -+ { -+ "comment": "block documentation comments", -+ "name": "comment.block.documentation.cairo", -+ "begin": "/\\*\\*", -+ "end": "\\*/", -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ } -+ ] -+ }, -+ { -+ "comment": "block comments", -+ "name": "comment.block.cairo", -+ "begin": "/\\*(?!\\*)", -+ "end": "\\*/", -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ } -+ ] -+ } -+ ] -+ }, -+ "constants": { -+ "patterns": [ -+ { -+ "comment": "ALL CAPS constants", -+ "name": "constant.other.caps.cairo", -+ "match": "\\b[A-Z]{2}[A-Z0-9_]*\\b" -+ }, -+ { -+ "comment": "constant declarations", -+ "match": "\\b(const)\\s+([A-Z][A-Za-z0-9_]*)\\b", -+ "captures": { -+ "1": { -+ "name": "storage.type.cairo" -+ }, -+ "2": { -+ "name": "constant.other.caps.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "decimal integers and floats", -+ "name": "constant.numeric.decimal.cairo", -+ "match": "\\b\\d[\\d_]*(\\.?)[\\d_]*(?:(E|e)([+-]?)([\\d_]+))?(f32|f64|i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b", -+ "captures": { -+ "1": { -+ "name": "punctuation.separator.dot.decimal.cairo" -+ }, -+ "2": { -+ "name": "keyword.operator.exponent.cairo" -+ }, -+ "3": { -+ "name": "keyword.operator.exponent.sign.cairo" -+ }, -+ "4": { -+ "name": "constant.numeric.decimal.exponent.mantissa.cairo" -+ }, -+ "5": { -+ "name": "entity.name.type.numeric.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "hexadecimal integers", -+ "name": "constant.numeric.hex.cairo", -+ "match": "\\b0x[\\da-fA-F_]+(i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b", -+ "captures": { -+ "1": { -+ "name": "entity.name.type.numeric.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "octal integers", -+ "name": "constant.numeric.oct.cairo", -+ "match": "\\b0o[0-7_]+(i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b", -+ "captures": { -+ "1": { -+ "name": "entity.name.type.numeric.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "binary integers", -+ "name": "constant.numeric.bin.cairo", -+ "match": "\\b0b[01_]+(i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b", -+ "captures": { -+ "1": { -+ "name": "entity.name.type.numeric.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "booleans", -+ "name": "constant.language.bool.cairo", -+ "match": "\\b(true|false)\\b" -+ } -+ ] -+ }, -+ "escapes": { -+ "comment": "escapes: ASCII, byte, Unicode, quote, regex", -+ "name": "constant.character.escape.cairo", -+ "match": "(\\\\)(?:(?:(x[0-7][\\da-fA-F])|(u(\\{)[\\da-fA-F]{4,6}(\\}))|.))", -+ "captures": { -+ "1": { -+ "name": "constant.character.escape.backslash.cairo" -+ }, -+ "2": { -+ "name": "constant.character.escape.bit.cairo" -+ }, -+ "3": { -+ "name": "constant.character.escape.unicode.cairo" -+ }, -+ "4": { -+ "name": "constant.character.escape.unicode.punctuation.cairo" -+ }, -+ "5": { -+ "name": "constant.character.escape.unicode.punctuation.cairo" -+ } -+ } -+ }, -+ "attributes": { -+ "comment": "attributes", -+ "name": "meta.attribute.cairo", -+ "begin": "(#)(\\!?)(\\[)", -+ "beginCaptures": { -+ "1": { -+ "name": "punctuation.definition.attribute.cairo" -+ }, -+ "3": { -+ "name": "punctuation.brackets.attribute.cairo" -+ } -+ }, -+ "end": "\\]", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.brackets.attribute.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#keywords" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#strings" -+ }, -+ { -+ "include": "#gtypes" -+ }, -+ { -+ "include": "#types" -+ } -+ ] -+ }, -+ "functions": { -+ "patterns": [ -+ { -+ "comment": "pub as a function", -+ "match": "\\b(pub)(\\()", -+ "captures": { -+ "1": { -+ "name": "keyword.other.cairo" -+ }, -+ "2": { -+ "name": "punctuation.brackets.round.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "function definition", -+ "name": "meta.function.definition.cairo", -+ "begin": "\\b(fn)\\s+([A-Za-z0-9_]+)((\\()|(<))", -+ "beginCaptures": { -+ "1": { -+ "name": "keyword.other.fn.cairo" -+ }, -+ "2": { -+ "name": "entity.name.function.cairo" -+ }, -+ "4": { -+ "name": "punctuation.brackets.round.cairo" -+ }, -+ "5": { -+ "name": "punctuation.brackets.angle.cairo" -+ } -+ }, -+ "end": "\\{|;", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.brackets.curly.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#keywords" -+ }, -+ { -+ "include": "#lvariables" -+ }, -+ { -+ "include": "#constants" -+ }, -+ { -+ "include": "#gtypes" -+ }, -+ { -+ "include": "#functions" -+ }, -+ { -+ "include": "#macros" -+ }, -+ { -+ "include": "#namespaces" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#strings" -+ }, -+ { -+ "include": "#types" -+ }, -+ { -+ "include": "#variables" -+ } -+ ] -+ }, -+ { -+ "comment": "function/method calls, chaining", -+ "name": "meta.function.call.cairo", -+ "begin": "([A-Za-z0-9_]+)(\\()", -+ "beginCaptures": { -+ "1": { -+ "name": "entity.name.function.cairo" -+ }, -+ "2": { -+ "name": "punctuation.brackets.round.cairo" -+ } -+ }, -+ "end": "\\)", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.brackets.round.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#attributes" -+ }, -+ { -+ "include": "#keywords" -+ }, -+ { -+ "include": "#lvariables" -+ }, -+ { -+ "include": "#constants" -+ }, -+ { -+ "include": "#gtypes" -+ }, -+ { -+ "include": "#functions" -+ }, -+ { -+ "include": "#macros" -+ }, -+ { -+ "include": "#namespaces" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#strings" -+ }, -+ { -+ "include": "#types" -+ }, -+ { -+ "include": "#variables" -+ } -+ ] -+ }, -+ { -+ "comment": "function/method calls with turbofish", -+ "name": "meta.function.call.cairo", -+ "begin": "([A-Za-z0-9_]+)(?=::<.*>\\()", -+ "beginCaptures": { -+ "1": { -+ "name": "entity.name.function.cairo" -+ } -+ }, -+ "end": "\\)", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.brackets.round.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#attributes" -+ }, -+ { -+ "include": "#keywords" -+ }, -+ { -+ "include": "#lvariables" -+ }, -+ { -+ "include": "#constants" -+ }, -+ { -+ "include": "#gtypes" -+ }, -+ { -+ "include": "#functions" -+ }, -+ { -+ "include": "#lifetimes" -+ }, -+ { -+ "include": "#macros" -+ }, -+ { -+ "include": "#namespaces" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#strings" -+ }, -+ { -+ "include": "#types" -+ }, -+ { -+ "include": "#variables" -+ } -+ ] -+ } -+ ] -+ }, -+ "keywords": { -+ "patterns": [ -+ { -+ "comment": "control flow keywords", -+ "name": "keyword.control.cairo", -+ "match": "\\b(break|continue|do|else|for|if|loop|match|return|try|while|yield)\\b" -+ }, -+ { -+ "comment": "storage keywords", -+ "name": "keyword.other.cairo storage.type.cairo", -+ "match": "\\b(extern|let|macro|mod)\\b" -+ }, -+ { -+ "comment": "const keyword", -+ "name": "storage.modifier.cairo", -+ "match": "\\b(const)\\b" -+ }, -+ { -+ "comment": "type keyword", -+ "name": "keyword.declaration.type.cairo storage.type.cairo", -+ "match": "\\b(type)\\b" -+ }, -+ { -+ "comment": "enum keyword", -+ "name": "keyword.declaration.enum.cairo storage.type.cairo", -+ "match": "\\b(enum)\\b" -+ }, -+ { -+ "comment": "trait keyword", -+ "name": "keyword.declaration.trait.cairo storage.type.cairo", -+ "match": "\\b(trait)\\b" -+ }, -+ { -+ "comment": "struct keyword", -+ "name": "keyword.declaration.struct.cairo storage.type.cairo", -+ "match": "\\b(struct)\\b" -+ }, -+ { -+ "comment": "storage modifiers", -+ "name": "storage.modifier.cairo", -+ "match": "\\b(ref|static)\\b" -+ }, -+ { -+ "comment": "other keywords", -+ "name": "keyword.other.cairo", -+ "match": "\\b(as|dyn|move|impl|implicits|in|nopanic|of|priv|pub|static_assert|typeof|unsafe|use|where|with)\\b" -+ }, -+ { -+ "comment": "fn", -+ "name": "keyword.other.fn.cairo", -+ "match": "\\bfn\\b" -+ }, -+ { -+ "comment": "crate", -+ "name": "keyword.other.crate.cairo", -+ "match": "\\bcrate\\b" -+ }, -+ { -+ "comment": "mut", -+ "name": "storage.modifier.mut.cairo", -+ "match": "\\bmut\\b" -+ }, -+ { -+ "comment": "logical operators", -+ "name": "keyword.operator.logical.cairo", -+ "match": "(\\^|\\||\\|\\||&&|<<|>>|!)(?!=)" -+ }, -+ { -+ "comment": "logical AND, borrow references", -+ "name": "keyword.operator.borrow.and.cairo", -+ "match": "&(?![&=])" -+ }, -+ { -+ "comment": "assignment operators", -+ "name": "keyword.operator.assignment.cairo", -+ "match": "(\\+=|-=|\\*=|/=|%=|\\^=|&=|\\|=|<<=|>>=)" -+ }, -+ { -+ "comment": "single equal", -+ "name": "keyword.operator.assignment.equal.cairo", -+ "match": "(?])=(?!=|>)" -+ }, -+ { -+ "comment": "comparison operators", -+ "name": "keyword.operator.comparison.cairo", -+ "match": "(=(=)?(?!>)|!=|<=|(?=)" -+ }, -+ { -+ "comment": "math operators", -+ "name": "keyword.operator.math.cairo", -+ "match": "(([+%]|(\\*(?!\\w)))(?!=))|(-(?!>))|(/(?!/))" -+ }, -+ { -+ "comment": "less than, greater than (special case)", -+ "match": "(?:\\b|(?:(\\))|(\\])|(\\})))[ \\t]+([<>])[ \\t]+(?:\\b|(?:(\\()|(\\[)|(\\{)))", -+ "captures": { -+ "1": { -+ "name": "punctuation.brackets.round.cairo" -+ }, -+ "2": { -+ "name": "punctuation.brackets.square.cairo" -+ }, -+ "3": { -+ "name": "punctuation.brackets.curly.cairo" -+ }, -+ "4": { -+ "name": "keyword.operator.comparison.cairo" -+ }, -+ "5": { -+ "name": "punctuation.brackets.round.cairo" -+ }, -+ "6": { -+ "name": "punctuation.brackets.square.cairo" -+ }, -+ "7": { -+ "name": "punctuation.brackets.curly.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "namespace operator", -+ "name": "keyword.operator.namespace.cairo", -+ "match": "::" -+ }, -+ { -+ "comment": "desnap", -+ "match": "(\\*)(?=\\w+)", -+ "captures": { -+ "1": { -+ "name": "keyword.operator.desnap.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "snap", -+ "name": "keyword.operator.snap.cairo", -+ "match": "@" -+ }, -+ { -+ "comment": "dot access", -+ "name": "keyword.operator.access.dot.cairo", -+ "match": "\\.(?!\\.)" -+ }, -+ { -+ "comment": "ranges, range patterns", -+ "name": "keyword.operator.range.cairo", -+ "match": "\\.{2}(=|\\.)?" -+ }, -+ { -+ "comment": "colon", -+ "name": "keyword.operator.key-value.cairo", -+ "match": ":(?!:)" -+ }, -+ { -+ "comment": "dashrocket, skinny arrow", -+ "name": "keyword.operator.arrow.skinny.cairo", -+ "match": "->" -+ }, -+ { -+ "comment": "hashrocket, fat arrow", -+ "name": "keyword.operator.arrow.fat.cairo", -+ "match": "=>" -+ }, -+ { -+ "comment": "dollar macros", -+ "name": "keyword.operator.macro.dollar.cairo", -+ "match": "\\$" -+ }, -+ { -+ "comment": "question mark operator, questionably sized, macro kleene matcher", -+ "name": "keyword.operator.question.cairo", -+ "match": "\\?" -+ } -+ ] -+ }, -+ "interpolations": { -+ "comment": "curly brace interpolations", -+ "name": "meta.interpolation.cairo", -+ "match": "({)[^\"{}]*(})", -+ "captures": { -+ "1": { -+ "name": "punctuation.definition.interpolation.cairo" -+ }, -+ "2": { -+ "name": "punctuation.definition.interpolation.cairo" -+ } -+ } -+ }, -+ "macros": { -+ "patterns": [ -+ { -+ "comment": "macros", -+ "name": "meta.macro.cairo", -+ "match": "(([a-z_][A-Za-z0-9_]*!)|([A-Z_][A-Za-z0-9_]*!))", -+ "captures": { -+ "2": { -+ "name": "entity.name.function.macro.cairo" -+ }, -+ "3": { -+ "name": "entity.name.type.macro.cairo" -+ } -+ } -+ } -+ ] -+ }, -+ "namespaces": { -+ "patterns": [ -+ { -+ "comment": "namespace (non-type, non-function path segment)", -+ "match": "(?", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.brackets.angle.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#block-comments" -+ }, -+ { -+ "include": "#comments" -+ }, -+ { -+ "include": "#keywords" -+ }, -+ { -+ "include": "#lvariables" -+ }, -+ { -+ "include": "#punctuation" -+ }, -+ { -+ "include": "#types" -+ }, -+ { -+ "include": "#variables" -+ } -+ ] -+ }, -+ { -+ "comment": "primitive types", -+ "name": "entity.name.type.primitive.cairo", -+ "match": "\\b(bool|never)\\b" -+ }, -+ { -+ "comment": "trait declarations", -+ "match": "\\b(trait)\\s+(_?[A-Z][A-Za-z0-9_]*)\\b", -+ "captures": { -+ "1": { -+ "name": "keyword.declaration.trait.cairo storage.type.cairo" -+ }, -+ "2": { -+ "name": "entity.name.type.trait.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "struct declarations", -+ "match": "\\b(struct)\\s+(_?[A-Z][A-Za-z0-9_]*)\\b", -+ "captures": { -+ "1": { -+ "name": "keyword.declaration.struct.cairo storage.type.cairo" -+ }, -+ "2": { -+ "name": "entity.name.type.struct.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "enum declarations", -+ "match": "\\b(enum)\\s+(_?[A-Z][A-Za-z0-9_]*)\\b", -+ "captures": { -+ "1": { -+ "name": "keyword.declaration.enum.cairo storage.type.cairo" -+ }, -+ "2": { -+ "name": "entity.name.type.enum.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "type declarations", -+ "match": "\\b(type)\\s+(_?[A-Z][A-Za-z0-9_]*)\\b", -+ "captures": { -+ "1": { -+ "name": "keyword.declaration.type.cairo storage.type.cairo" -+ }, -+ "2": { -+ "name": "entity.name.type.declaration.cairo" -+ } -+ } -+ }, -+ { -+ "comment": "types", -+ "name": "entity.name.type.cairo", -+ "match": "\\b_?[A-Z][A-Za-z0-9_]*\\b(?!!)" -+ } -+ ] -+ }, -+ "gtypes": { -+ "patterns": [ -+ { -+ "comment": "option types", -+ "name": "entity.name.type.option.cairo", -+ "match": "\\b(Some|None)\\b" -+ }, -+ { -+ "comment": "result types", -+ "name": "entity.name.type.result.cairo", -+ "match": "\\b(Ok|Err)\\b" -+ } -+ ] -+ }, -+ "punctuation": { -+ "patterns": [ -+ { -+ "comment": "comma", -+ "name": "punctuation.comma.cairo", -+ "match": "," -+ }, -+ { -+ "comment": "curly braces", -+ "name": "punctuation.brackets.curly.cairo", -+ "match": "[{}]" -+ }, -+ { -+ "comment": "parentheses, round brackets", -+ "name": "punctuation.brackets.round.cairo", -+ "match": "[()]" -+ }, -+ { -+ "comment": "semicolon", -+ "name": "punctuation.semi.cairo", -+ "match": ";" -+ }, -+ { -+ "comment": "square brackets", -+ "name": "punctuation.brackets.square.cairo", -+ "match": "[\\[\\]]" -+ }, -+ { -+ "comment": "angle brackets", -+ "name": "punctuation.brackets.angle.cairo", -+ "match": "(?]" -+ } -+ ] -+ }, -+ "strings": { -+ "patterns": [ -+ { -+ "comment": "double-quoted byte array strings", -+ "name": "string.quoted.double.cairo", -+ "begin": "(\")", -+ "beginCaptures": { -+ "1": { -+ "name": "punctuation.definition.string.bytearray.cairo" -+ } -+ }, -+ "end": "\"", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.definition.string.bytearray.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#escapes" -+ }, -+ { -+ "include": "#interpolations" -+ } -+ ] -+ }, -+ { -+ "comment": "single-quoted short strings", -+ "name": "string.quoted.single.cairo", -+ "begin": "(')", -+ "beginCaptures": { -+ "1": { -+ "name": "punctuation.definition.string.short.cairo" -+ } -+ }, -+ "end": "'", -+ "endCaptures": { -+ "0": { -+ "name": "punctuation.definition.string.short.cairo" -+ } -+ }, -+ "patterns": [ -+ { -+ "include": "#escapes" -+ }, -+ { -+ "include": "#interpolations" -+ } -+ ] -+ } -+ ] -+ }, -+ "lvariables": { -+ "patterns": [ -+ { -+ "comment": "super", -+ "name": "variable.language.super.cairo", -+ "match": "\\bsuper\\b" -+ } -+ ] -+ }, -+ "variables": { -+ "patterns": [ -+ { -+ "comment": "variables", -+ "name": "variable.other.cairo", -+ "match": "\\b(? [ -- remarkDirective, -- remarkInferFrontmatter, -- remarkFrontmatter, -- remarkMdxFrontmatter, -- remarkGfm, -- remarkLinks, -- remarkBlogPosts, -- remarkCallout, -- remarkCode, -- remarkCodeGroup, -- remarkDetails, -- remarkFilename, -- remarkSponsors, -- remarkSteps, -- remarkStrongBlock, -- remarkSubheading, -- remarkTwoslash, -- remarkAuthors, -- ...(markdown?.remarkPlugins || []), -+ remarkDirective, -+ remarkInferFrontmatter, -+ remarkFrontmatter, -+ remarkMdxFrontmatter, -+ remarkGfm, -+ remarkLinks, -+ remarkBlogPosts, -+ remarkCallout, -+ remarkCode, -+ remarkCodeGroup, -+ remarkDetails, -+ remarkFilename, -+ remarkSponsors, -+ remarkSteps, -+ remarkStrongBlock, -+ remarkSubheading, -+ remarkTwoslash, -+ remarkAuthors, -+ ...(markdown?.remarkPlugins || []), - ]; - const defaultThemes = { -- dark: 'github-dark-dimmed', -- light: 'github-light', -+ dark: "github-dark-dimmed", -+ light: "github-light", - }; --export const getRehypePlugins = ({ markdown, rootDir = '', twoslash = {}, } = {}) => [ -- rehypeSlug, -- [ -- rehypeShiki, -- { -- transformers: [ -- transformerLineNumbers(), -- transformerNotationDiff(), -- transformerNotationFocus(), -- transformerNotationHighlight(), -- transformerNotationWordHighlight(), -- transformerNotationInclude({ rootDir }), -- transformerEmptyLine(), -- transformerTagLine(), -- transformerTitle(), -- twoslash !== false -- ? transformerTwoslash({ -- explicitTrigger: true, -- renderer: twoslashRenderer(), -- twoslasher, -- twoslashOptions: { -- ...twoslash, -- customTags: [ -- 'allowErrors', -- ...(defaultTwoslashOptions.customTags ?? []), -- ...(twoslash.customTags ?? []), -- ], -- compilerOptions: { -- ...(twoslash.compilerOptions ?? {}), -- ...defaultTwoslashOptions.compilerOptions, -- }, -- }, -- }) -- : null, -- transformerSplitIdentifiers(), -- ].filter(Boolean), -- themes: defaultThemes, -- ...markdown?.code, -- }, -- ], -- [ -- rehypeInlineShiki, -- { -- themes: defaultThemes, -- ...markdown?.code, -- }, -- ], -- rehypeShikiDisplayNotation, -- [ -- rehypeAutolinkHeadings, -- { -- behavior: 'append', -- content() { -- return [h('div', { dataAutolinkIcon: true })]; -- }, -- }, -- ], -- ...(markdown?.rehypePlugins || []), -+export const getRehypePlugins = ({ -+ markdown, -+ rootDir = "", -+ twoslash = {}, -+} = {}) => [ -+ rehypeSlug, -+ [ -+ // Patch -+ rehypeShikiFromHighlighter, -+ highlighter, -+ // Patch end -+ { -+ transformers: [ -+ transformerLineNumbers(), -+ transformerNotationDiff(), -+ transformerNotationFocus(), -+ transformerNotationHighlight(), -+ transformerNotationWordHighlight(), -+ transformerNotationInclude({ rootDir }), -+ transformerEmptyLine(), -+ transformerTagLine(), -+ transformerTitle(), -+ twoslash !== false -+ ? transformerTwoslash({ -+ explicitTrigger: true, -+ renderer: twoslashRenderer(), -+ twoslasher, -+ twoslashOptions: { -+ ...twoslash, -+ customTags: [ -+ "allowErrors", -+ ...(defaultTwoslashOptions.customTags ?? []), -+ ...(twoslash.customTags ?? []), -+ ], -+ compilerOptions: { -+ ...(twoslash.compilerOptions ?? {}), -+ ...defaultTwoslashOptions.compilerOptions, -+ }, -+ }, -+ }) -+ : null, -+ transformerSplitIdentifiers(), -+ ].filter(Boolean), -+ themes: defaultThemes, -+ ...markdown?.code, -+ }, -+ ], -+ [ -+ rehypeInlineShiki, -+ { -+ langs, -+ themes: defaultThemes, -+ ...markdown?.code, -+ }, -+ ], -+ rehypeShikiDisplayNotation, -+ [ -+ rehypeAutolinkHeadings, -+ { -+ behavior: "append", -+ content() { -+ return [h("div", { dataAutolinkIcon: true })]; -+ }, -+ }, -+ ], -+ ...(markdown?.rehypePlugins || []), - ]; - export async function mdx() { -- const { config } = await resolveVocsConfig(); -- const { markdown, rootDir, twoslash } = config; -- const remarkPlugins = getRemarkPlugins({ markdown }); -- const rehypePlugins = getRehypePlugins({ markdown, rootDir, twoslash }); -- return [ -- mdxPlugin({ -- providerImportSource: 'vocs/mdx-react', -- remarkPlugins, -- rehypePlugins, -- }), -- ]; -+ const { config } = await resolveVocsConfig(); -+ const { markdown, rootDir, twoslash } = config; -+ const remarkPlugins = getRemarkPlugins({ markdown }); -+ const rehypePlugins = getRehypePlugins({ markdown, rootDir, twoslash }); -+ return [ -+ mdxPlugin({ -+ providerImportSource: "vocs/mdx-react", -+ remarkPlugins, -+ rehypePlugins, -+ }), -+ ]; - } - //# sourceMappingURL=mdx.js.map diff --git a/_lib/vite/plugins/rehype/inline-shiki.js b/_lib/vite/plugins/rehype/inline-shiki.js -index 1e2a7426a5a80afa1bdff24388a8849b49e04f2e..20332be7836e5ca932f59747c3de697430b1d711 100644 +index 1e2a7426a5a80afa1bdff24388a8849b49e04f2e..a8394cddd3968f6c153eed58481a80fb5a8e1b09 100644 --- a/_lib/vite/plugins/rehype/inline-shiki.js +++ b/_lib/vite/plugins/rehype/inline-shiki.js -@@ -1,30 +1,51 @@ --import { bundledLanguages, getSingletonHighlighter } from 'shiki'; --import { visit } from 'unist-util-visit'; -+import { bundledLanguages, getSingletonHighlighter } from "shiki"; -+import { visit } from "unist-util-visit"; - const inlineShikiRegex = /(.*){:(.*)}$/; - let promise; - export const rehypeInlineShiki = function (options = {}) { -- const themeNames = ('themes' in options ? Object.values(options.themes) : [options.theme]).filter(Boolean); -- const langs = options.langs || Object.keys(bundledLanguages); -- return async function (tree) { -- if (!promise) -- promise = getSingletonHighlighter({ -- themes: themeNames, -- langs, -- }); -- const highlighter = await promise; -- return visit(tree, 'element', (node, index, parent) => { +@@ -13,16 +13,33 @@ export const rehypeInlineShiki = function (options = {}) { + }); + const highlighter = await promise; + return visit(tree, 'element', (node, index, parent) => { - if (node.tagName !== 'code') - return; -- const match = node.children[0]?.value?.match(inlineShikiRegex); ++ if (node.tagName !== "code") return; ++ // ignore math ++ const classes = Array.isArray(node.properties.className) ++ ? node.properties.className ++ : []; ++ const languageMath = classes.includes("language-math"); ++ const mathDisplay = classes.includes("math-display"); ++ const mathInline = classes.includes("math-inline"); ++ if (languageMath || mathDisplay || mathInline) { ++ return; ++ } ++ + const match = node.children[0]?.value?.match(inlineShikiRegex); - if (!match) - return; - const [, code, lang] = match; - const hast = highlighter.codeToHast(code, { ...options, lang }); -- const inlineCode = hast.children[0].children[0]; ++ let hast; ++ if (match) { ++ const [, code, lang] = match; ++ hast = highlighter.codeToHast(code, { ...options, lang }); ++ } else { ++ if (!node.children[0] || node.children[0].type !== "text") return; ++ const code = node.children[0].value; ++ hast = highlighter.codeToHast(code, { ++ ...options, ++ lang: "cairo", ++ }); ++ } + const inlineCode = hast.children[0].children[0]; - if (!inlineCode) - return; -- parent?.children.splice(index ?? 0, 1, inlineCode); -+ const themeNames = ( -+ "themes" in options ? Object.values(options.themes) : [options.theme] -+ ).filter(Boolean); -+ const langs = options.langs || Object.keys(bundledLanguages); -+ return async function (tree) { -+ if (!promise) -+ promise = getSingletonHighlighter({ -+ themes: themeNames, -+ langs, -+ }); -+ const highlighter = await promise; -+ return visit(tree, "element", (node, index, parent) => { -+ if (node.tagName !== "code") return; -+ // ignore math -+ const classes = Array.isArray(node.properties.className) -+ ? node.properties.className -+ : []; -+ const languageMath = classes.includes("language-math"); -+ const mathDisplay = classes.includes("math-display"); -+ const mathInline = classes.includes("math-inline"); -+ if (languageMath || mathDisplay || mathInline) { -+ return; -+ } -+ -+ const match = node.children[0]?.value?.match(inlineShikiRegex); -+ if (match) { -+ const [, code, lang] = match; -+ const hast = highlighter.codeToHast(code, { ...options, lang }); -+ const inlineCode = hast.children[0].children[0]; -+ if (!inlineCode) return; -+ parent?.children.splice(index ?? 0, 1, inlineCode); -+ } else { -+ if (!node.children[0] || node.children[0].type !== "text") return; -+ const code = node.children[0].value; -+ const hast = highlighter.codeToHast(code, { -+ ...options, -+ lang: "Cairo", ++ if (!inlineCode) return; + parent?.children.splice(index ?? 0, 1, inlineCode); }); -- }; -+ const inlineCode = hast.children[0].children[0]; -+ if (!inlineCode) return; -+ parent?.children.splice(index ?? 0, 1, inlineCode); -+ } -+ }); -+ }; - }; - //# sourceMappingURL=inline-shiki.js.map + }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c6d19f9..575a437a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,17 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + vocs>shiki: ^1.23.0 + vocs>@shikijs/core: ^1.23.0 + vocs>@shikijs/types: ^1.23.0 + vocs>@shikijs/rehype: ^1.23.0 + vocs>@shikijs/twoslash: ^1.23.0 + vocs>@shikijs/transformers: ^1.23.0 + patchedDependencies: vocs: - hash: p6z7kxjkom4q2uemolyyrxbtiy + hash: 7wumpnts656yvepr4seo2mjn34 path: patches/vocs.patch importers: @@ -33,7 +41,7 @@ importers: version: 5.6.3 vocs: specifier: 1.0.0-alpha.62 - version: 1.0.0-alpha.62(patch_hash=p6z7kxjkom4q2uemolyyrxbtiy)(@types/node@22.8.6)(@types/react-dom@18.3.1)(@types/react@18.3.12)(acorn@8.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3) + version: 1.0.0-alpha.62(patch_hash=7wumpnts656yvepr4seo2mjn34)(@types/node@22.8.6)(@types/react-dom@18.3.1)(@types/react@18.3.12)(acorn@8.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3) devDependencies: '@types/react-dom': specifier: ^18.3.1 @@ -981,26 +989,26 @@ packages: cpu: [x64] os: [win32] - '@shikijs/core@1.22.2': - resolution: {integrity: sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==} + '@shikijs/core@1.23.1': + resolution: {integrity: sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==} - '@shikijs/engine-javascript@1.22.2': - resolution: {integrity: sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==} + '@shikijs/engine-javascript@1.23.1': + resolution: {integrity: sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==} - '@shikijs/engine-oniguruma@1.22.2': - resolution: {integrity: sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==} + '@shikijs/engine-oniguruma@1.23.1': + resolution: {integrity: sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==} - '@shikijs/rehype@1.22.2': - resolution: {integrity: sha512-A0RHgiYR5uiHvddwHehBN9j8PhOvfT6/GebSTWrapur6M+fD/4i3mlfUv7aFK4b+4GQ1R42L8fC5N98whZjNcg==} + '@shikijs/rehype@1.23.1': + resolution: {integrity: sha512-PH5bpMDEc4nBP62Ci3lUqkxBWRTm8cdE+eY9er5QD50jAWQxhXcc1Aeax1AlyrASrtjTwCkI22M6N9iSn5p+bQ==} - '@shikijs/transformers@1.22.2': - resolution: {integrity: sha512-8f78OiBa6pZDoZ53lYTmuvpFPlWtevn23bzG+azpPVvZg7ITax57o/K3TC91eYL3OMJOO0onPbgnQyZjRos8XQ==} + '@shikijs/transformers@1.23.1': + resolution: {integrity: sha512-yQ2Cn0M9i46p30KwbyIzLvKDk+dQNU+lj88RGO0XEj54Hn4Cof1bZoDb9xBRWxFE4R8nmK63w7oHnJwvOtt0NQ==} - '@shikijs/twoslash@1.22.2': - resolution: {integrity: sha512-4R3A7aH/toZgtlveXHKk01nIsvn8hjAfPJ1aT550zcV4qK6vK/tfaEyYtaljOaY1wig2l5+8sKjNSEz3PcSiEw==} + '@shikijs/twoslash@1.23.1': + resolution: {integrity: sha512-Qj/+CGAF6TdcRjPDQn1bxyKD8ejnV7VJLqCHzob1uCbwQlJTI5z0gUVAgpqS55z4vdV1Mrx2IpCTl9glhC0l3A==} - '@shikijs/types@1.22.2': - resolution: {integrity: sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==} + '@shikijs/types@1.23.1': + resolution: {integrity: sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==} '@shikijs/vscode-textmate@9.3.0': resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} @@ -1375,6 +1383,9 @@ packages: electron-to-chromium@1.5.50: resolution: {integrity: sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==} + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -2042,8 +2053,8 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - oniguruma-to-js@0.4.3: - resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} + oniguruma-to-es@0.4.1: + resolution: {integrity: sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==} ora@7.0.1: resolution: {integrity: sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==} @@ -2282,8 +2293,14 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - regex@4.4.0: - resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} + regex-recursion@4.2.1: + resolution: {integrity: sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@5.0.2: + resolution: {integrity: sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==} rehype-autolink-headings@7.1.0: resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} @@ -2379,8 +2396,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@1.22.2: - resolution: {integrity: sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==} + shiki@1.23.1: + resolution: {integrity: sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==} signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3489,49 +3506,49 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.3': optional: true - '@shikijs/core@1.22.2': + '@shikijs/core@1.23.1': dependencies: - '@shikijs/engine-javascript': 1.22.2 - '@shikijs/engine-oniguruma': 1.22.2 - '@shikijs/types': 1.22.2 + '@shikijs/engine-javascript': 1.23.1 + '@shikijs/engine-oniguruma': 1.23.1 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 hast-util-to-html: 9.0.3 - '@shikijs/engine-javascript@1.22.2': + '@shikijs/engine-javascript@1.23.1': dependencies: - '@shikijs/types': 1.22.2 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 - oniguruma-to-js: 0.4.3 + oniguruma-to-es: 0.4.1 - '@shikijs/engine-oniguruma@1.22.2': + '@shikijs/engine-oniguruma@1.23.1': dependencies: - '@shikijs/types': 1.22.2 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 - '@shikijs/rehype@1.22.2': + '@shikijs/rehype@1.23.1': dependencies: - '@shikijs/types': 1.22.2 + '@shikijs/types': 1.23.1 '@types/hast': 3.0.4 hast-util-to-string: 3.0.1 - shiki: 1.22.2 + shiki: 1.23.1 unified: 11.0.5 unist-util-visit: 5.0.0 - '@shikijs/transformers@1.22.2': + '@shikijs/transformers@1.23.1': dependencies: - shiki: 1.22.2 + shiki: 1.23.1 - '@shikijs/twoslash@1.22.2(typescript@5.6.3)': + '@shikijs/twoslash@1.23.1(typescript@5.6.3)': dependencies: - '@shikijs/core': 1.22.2 - '@shikijs/types': 1.22.2 + '@shikijs/core': 1.23.1 + '@shikijs/types': 1.23.1 twoslash: 0.2.12(typescript@5.6.3) transitivePeerDependencies: - supports-color - typescript - '@shikijs/types@1.22.2': + '@shikijs/types@1.23.1': dependencies: '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 @@ -3929,6 +3946,8 @@ snapshots: electron-to-chromium@1.5.50: {} + emoji-regex-xs@1.0.0: {} + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -5011,9 +5030,11 @@ snapshots: dependencies: mimic-fn: 2.1.0 - oniguruma-to-js@0.4.3: + oniguruma-to-es@0.4.1: dependencies: - regex: 4.4.0 + emoji-regex-xs: 1.0.0 + regex: 5.0.2 + regex-recursion: 4.2.1 ora@7.0.1: dependencies: @@ -5260,7 +5281,15 @@ snapshots: regenerator-runtime@0.14.1: {} - regex@4.4.0: {} + regex-recursion@4.2.1: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@5.0.2: + dependencies: + regex-utilities: 2.3.0 rehype-autolink-headings@7.1.0: dependencies: @@ -5467,12 +5496,12 @@ snapshots: shebang-regex@3.0.0: {} - shiki@1.22.2: + shiki@1.23.1: dependencies: - '@shikijs/core': 1.22.2 - '@shikijs/engine-javascript': 1.22.2 - '@shikijs/engine-oniguruma': 1.22.2 - '@shikijs/types': 1.22.2 + '@shikijs/core': 1.23.1 + '@shikijs/engine-javascript': 1.23.1 + '@shikijs/engine-oniguruma': 1.23.1 + '@shikijs/types': 1.23.1 '@shikijs/vscode-textmate': 9.3.0 '@types/hast': 3.0.4 @@ -5766,7 +5795,7 @@ snapshots: '@types/node': 22.8.6 fsevents: 2.3.3 - vocs@1.0.0-alpha.62(patch_hash=p6z7kxjkom4q2uemolyyrxbtiy)(@types/node@22.8.6)(@types/react-dom@18.3.1)(@types/react@18.3.12)(acorn@8.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3): + vocs@1.0.0-alpha.62(patch_hash=7wumpnts656yvepr4seo2mjn34)(@types/node@22.8.6)(@types/react-dom@18.3.1)(@types/react@18.3.12)(acorn@8.14.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.3)(typescript@5.6.3): dependencies: '@floating-ui/react': 0.26.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@hono/node-server': 1.13.4(hono@3.12.12) @@ -5781,9 +5810,9 @@ snapshots: '@radix-ui/react-navigation-menu': 1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tabs': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@shikijs/rehype': 1.22.2 - '@shikijs/transformers': 1.22.2 - '@shikijs/twoslash': 1.22.2(typescript@5.6.3) + '@shikijs/rehype': 1.23.1 + '@shikijs/transformers': 1.23.1 + '@shikijs/twoslash': 1.23.1(typescript@5.6.3) '@vanilla-extract/css': 1.16.0 '@vanilla-extract/dynamic': 2.1.2 '@vanilla-extract/vite-plugin': 3.9.5(@types/node@22.8.6)(vite@5.4.10(@types/node@22.8.6)) @@ -5823,7 +5852,7 @@ snapshots: remark-mdx-frontmatter: 4.0.0 remark-parse: 11.0.0 serve-static: 1.16.2 - shiki: 1.22.2 + shiki: 1.23.1 tailwindcss: 3.4.14 toml: 3.0.0 twoslash: 0.2.12(typescript@5.6.3) diff --git a/routes.ts b/routes.ts index 1969da23..d8348c7f 100644 --- a/routes.ts +++ b/routes.ts @@ -7,7 +7,6 @@ const config: Sidebar = [ }, { text: "Getting Started", - link: "/getting-started/basics/storage", items: [ // { // text: "Local environment setup" @@ -98,7 +97,6 @@ const config: Sidebar = [ }, { text: "Components", - link: "/components/how_to", items: [ { text: "Components How-To", @@ -120,7 +118,6 @@ const config: Sidebar = [ }, { text: "Applications", - link: "/applications/upgradeable_contract", items: [ { text: " Upgradeable Contract", @@ -174,7 +171,6 @@ const config: Sidebar = [ }, { text: "Advanced concepts", - link: "/advanced-concepts/write_to_any_slot", items: [ { text: "Writing to any storage slot", diff --git a/styles.css b/styles.css index 2ba7b331..4aeb1128 100644 --- a/styles.css +++ b/styles.css @@ -60,48 +60,47 @@ section.vocs_Sidebar_level } /* Make sidebar collapsible */ -.vocs_DocsLayout_gutterLeft { - top: 0; - left: 0; - bottom: 0; -} -.vocs_DocsLayout_gutterLeft { - margin-left: 0; - transition: margin 0.3s ease; -} -.sidebar_hidden .vocs_DocsLayout_gutterLeft { - margin-left: calc(var(--vocs_DocsLayout_leftGutterWidth) * -1); -}; - -.vocs_DocsLayout_content_withSidebar { - margin-left: var(--vocs_DocsLayout_leftGutterWidth) -} -.sidebar_hidden .vocs_DocsLayout_content_withSidebar { - margin-left: 0 !important; -} -.vocs_DocsLayout_content_withSidebar { - transition: margin 300ms ease !important; - max-width: unset; -} - -.vocs_DesktopTopNav_logo a { - margin-top: 2px !important; -} - -.vocs_Sidebar_logo { - padding-top: 0px !important; -} - -.sidebar_toggle { - position: absolute; - top: 0; - right: calc(var(--vocs-topNav_height) * -1); - height: var(--vocs-topNav_height); - width: var(--vocs-topNav_height); -} - -.sidebar_toggle button { - padding: var(--vocs-space_8); +@media screen and (min-width: 1080px) { + .vocs_DocsLayout_gutterLeft { + margin-left: 0; + transition: margin 0.3s ease; + width: var(--vocs-sidebar_width); + } + .sidebar_hidden .vocs_DocsLayout_gutterLeft { + margin-left: calc(var(--vocs-sidebar_width) * -1); + } + + .vocs_DocsLayout_content_withSidebar { + margin-left: var(--vocs-sidebar_width); + } + .sidebar_hidden .vocs_DocsLayout_content_withSidebar { + margin-left: 0 !important; + } + .vocs_DocsLayout_content_withSidebar { + margin-left: var(--vocs-sidebar_width); + transition: margin 300ms ease !important; + max-width: unset; + } + + .vocs_DesktopTopNav_logo a { + margin-top: 2px !important; + } + + .vocs_Sidebar_logo { + padding-top: 0px !important; + } + + .sidebar_toggle { + position: absolute; + top: 0; + right: calc(var(--vocs-topNav_height) * -1); + height: var(--vocs-topNav_height); + width: var(--vocs-topNav_height); + } + + .sidebar_toggle button { + padding: var(--vocs-space_8); + } } /* END sidebar */ @@ -120,3 +119,15 @@ section.vocs_Sidebar_level font-size: var(--vocs-fontSize_20); padding-bottom: 0.875em; } + +.vocs_DesktopTopNav_logoWrapper { + left: 22px; + justify-content: start; +} + +/* Force show theme switcher */ +@media screen and (max-width: 1280px) { + .vocs_DesktopTopNav_hideCompact { + display: block; + } +} From 615a273a7fcd25f6d01f166dbbdd16d24715f83d Mon Sep 17 00:00:00 2001 From: julio4 <30329843+julio4@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:06:54 +0700 Subject: [PATCH 25/50] fix(app): correct sidebar placement --- styles.css | 7 ------- 1 file changed, 7 deletions(-) diff --git a/styles.css b/styles.css index 4aeb1128..742942b0 100644 --- a/styles.css +++ b/styles.css @@ -124,10 +124,3 @@ section.vocs_Sidebar_level left: 22px; justify-content: start; } - -/* Force show theme switcher */ -@media screen and (max-width: 1280px) { - .vocs_DesktopTopNav_hideCompact { - display: block; - } -} From c390ac7d39555c6c204e62903070bec88f2635b1 Mon Sep 17 00:00:00 2001 From: julio4 <30329843+julio4@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:51:08 +0700 Subject: [PATCH 26/50] fix(app): responsive content centering --- styles.css | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/styles.css b/styles.css index 742942b0..e4e1cbe7 100644 --- a/styles.css +++ b/styles.css @@ -60,27 +60,31 @@ section.vocs_Sidebar_level } /* Make sidebar collapsible */ -@media screen and (min-width: 1080px) { - .vocs_DocsLayout_gutterLeft { - margin-left: 0; - transition: margin 0.3s ease; - width: var(--vocs-sidebar_width); - } - .sidebar_hidden .vocs_DocsLayout_gutterLeft { - margin-left: calc(var(--vocs-sidebar_width) * -1); - } +.vocs_DocsLayout_gutterLeft { + top: 0; + left: 0; + bottom: 0; +} +.vocs_DocsLayout_gutterLeft { + margin-left: 0; + transition: margin 0.3s ease; + width: var(--vocs-sidebar_width); +} +.sidebar_hidden .vocs_DocsLayout_gutterLeft { + margin-left: calc(var(--vocs-sidebar_width) * -1); +}; - .vocs_DocsLayout_content_withSidebar { - margin-left: var(--vocs-sidebar_width); - } - .sidebar_hidden .vocs_DocsLayout_content_withSidebar { - margin-left: 0 !important; - } - .vocs_DocsLayout_content_withSidebar { - margin-left: var(--vocs-sidebar_width); - transition: margin 300ms ease !important; - max-width: unset; - } +.vocs_DocsLayout_content_withSidebar { + margin-left: var(--vocs-sidebar_width); +} +.sidebar_hidden .vocs_DocsLayout_content_withSidebar { + margin-left: 0 !important; +} +.vocs_DocsLayout_content_withSidebar { + margin-left: var(--vocs-sidebar_width); + transition: margin 300ms ease !important; + max-width: unset; +} .vocs_DesktopTopNav_logo a { margin-top: 2px !important; From a87235b0d46579677927608d68d945b253933f53 Mon Sep 17 00:00:00 2001 From: julio4 <30329843+julio4@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:25:05 +0700 Subject: [PATCH 27/50] fix(app): responsive content centering --- styles.css | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/styles.css b/styles.css index e4e1cbe7..4aeb1128 100644 --- a/styles.css +++ b/styles.css @@ -60,31 +60,27 @@ section.vocs_Sidebar_level } /* Make sidebar collapsible */ -.vocs_DocsLayout_gutterLeft { - top: 0; - left: 0; - bottom: 0; -} -.vocs_DocsLayout_gutterLeft { - margin-left: 0; - transition: margin 0.3s ease; - width: var(--vocs-sidebar_width); -} -.sidebar_hidden .vocs_DocsLayout_gutterLeft { - margin-left: calc(var(--vocs-sidebar_width) * -1); -}; +@media screen and (min-width: 1080px) { + .vocs_DocsLayout_gutterLeft { + margin-left: 0; + transition: margin 0.3s ease; + width: var(--vocs-sidebar_width); + } + .sidebar_hidden .vocs_DocsLayout_gutterLeft { + margin-left: calc(var(--vocs-sidebar_width) * -1); + } -.vocs_DocsLayout_content_withSidebar { - margin-left: var(--vocs-sidebar_width); -} -.sidebar_hidden .vocs_DocsLayout_content_withSidebar { - margin-left: 0 !important; -} -.vocs_DocsLayout_content_withSidebar { - margin-left: var(--vocs-sidebar_width); - transition: margin 300ms ease !important; - max-width: unset; -} + .vocs_DocsLayout_content_withSidebar { + margin-left: var(--vocs-sidebar_width); + } + .sidebar_hidden .vocs_DocsLayout_content_withSidebar { + margin-left: 0 !important; + } + .vocs_DocsLayout_content_withSidebar { + margin-left: var(--vocs-sidebar_width); + transition: margin 300ms ease !important; + max-width: unset; + } .vocs_DesktopTopNav_logo a { margin-top: 2px !important; @@ -128,3 +124,10 @@ section.vocs_Sidebar_level left: 22px; justify-content: start; } + +/* Force show theme switcher */ +@media screen and (max-width: 1280px) { + .vocs_DesktopTopNav_hideCompact { + display: block; + } +} From 273e2a6ee363128cd92a3a3d8e7b030c5b61ddb9 Mon Sep 17 00:00:00 2001 From: Julio <30329843+julio4@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:14:12 +0700 Subject: [PATCH 28/50] Update issue templates, Close #273 close #273 --- .github/ISSUE_TEMPLATE/bug_report.md | 23 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature-request.md | 16 ++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..7c8cc313 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Checklist:** +- Using the correct tooling versions, from https://github.com/NethermindEth/StarknetByExample/blob/dev/.tool-versions diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..36b7339b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,16 @@ +--- +name: Feature request +about: Suggest an idea of example or concept to showcase +title: '' +labels: enhancement +assignees: '' + +--- + +**Describe the concept/idea you would like to see** + +**Is it a new example or a modification/extension of a current one** +If you consider editing some content, explain exactly why and what. + +**Additional context** +Add any other context or screenshots about the request here. From 4e9607c6bfa76bba89bc43cfa4f1354f9b20c50c Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 13:35:26 +0100 Subject: [PATCH 29/50] fix erc20 url to OZ one --- pages/applications/erc20.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/applications/erc20.md b/pages/applications/erc20.md index 85970633..ac51726f 100644 --- a/pages/applications/erc20.md +++ b/pages/applications/erc20.md @@ -17,4 +17,4 @@ Here's an implementation of the ERC20 interface in Cairo: // [!include ~/listings/applications/erc20/src/token.cairo:erc20] ``` -There's several other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.7.0/erc20) or the [Cairo By Example](https://cairo-by-example.com/examples/erc20/) ones. +There's several other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.20.0/erc20) or the [Cairo By Example](https://cairo-by-example.com/examples/erc20/) ones. From b92ae564a69aa450bb7d95de8b6faa4587d4ef18 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 13:40:17 +0100 Subject: [PATCH 30/50] Add comment encouring devs to read the EIP --- pages/applications/erc721.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pages/applications/erc721.md b/pages/applications/erc721.md index baf1c265..01256fda 100644 --- a/pages/applications/erc721.md +++ b/pages/applications/erc721.md @@ -2,6 +2,10 @@ Contracts that follow the [ERC721 Standard](https://eips.ethereum.org/EIPS/eip-721) are called ERC721 tokens. They are used to represent non-fungible assets. +:::note +For a deeper understanding of the ERC721 interface specifications and its functionality, we highly recommend reading the EIP in detail. +::: + To create an ERC721 contract, it must implement the following interface: ```cairo From d30b450695d61864cf4f5648f61967fe53ac6205 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 14:00:00 +0100 Subject: [PATCH 31/50] add optional Metadata & Enumerable interfaces --- listings/applications/erc721/src/erc721.cairo | 2 +- .../applications/erc721/src/interfaces.cairo | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index e6ce2db6..402de803 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -6,7 +6,7 @@ pub mod ERC721 { use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; use openzeppelin_introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; use erc721::interfaces::{ - IERC721, IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID + IERC721, IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID, }; #[storage] diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index bb30bae3..592ea826 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -1,5 +1,8 @@ use starknet::ContractAddress; +pub const IERC721_RECEIVER_ID: felt252 = + 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; + // [!region interface] #[starknet::interface] pub trait IERC721 { @@ -25,8 +28,19 @@ pub trait IERC721 { fn burn(ref self: TContractState, token_id: u256); } -pub const IERC721_RECEIVER_ID: felt252 = - 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; +#[starknet::interface] +pub trait IERC721Metadata { + fn name(self: @TState) -> ByteArray; + fn symbol(self: @TState) -> ByteArray; + fn token_uri(self: @TState, token_id: u256) -> ByteArray; +} + +#[starknet::interface] +pub trait IERC721Enumerable { + fn total_supply(self: @TState) -> u256; + fn token_by_index(self: @TState, index: u256) -> u256; + fn token_of_owner_by_index(self: @TState, owner: ContractAddress, index: u256) -> u256; +} #[starknet::interface] pub trait IERC721Receiver { From e8307c653304966adc37a1f637ad26ee27dc1701 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 14:01:30 +0100 Subject: [PATCH 32/50] make mint & burn internal --- listings/applications/erc721/src/erc721.cairo | 6 +++--- listings/applications/erc721/src/interfaces.cairo | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index 402de803..ceb25030 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -131,7 +131,10 @@ pub mod ERC721 { ) -> bool { self.operator_approvals.read((owner, operator)) } + } + #[generate_trait] + pub impl InternalImpl of InternalTrait { fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { assert(!to.is_zero(), Errors::INVALID_RECEIVER); assert(self.owners.read(token_id).is_zero(), Errors::ALREADY_MINTED); @@ -152,10 +155,7 @@ pub mod ERC721 { self.emit(Transfer { from: owner, to: Zero::zero(), token_id }); } - } - #[generate_trait] - pub impl InternalImpl of InternalTrait { fn _require_owned(self: @ContractState, token_id: u256) -> ContractAddress { let owner = self.owners.read(token_id); assert(!owner.is_zero(), Errors::INVALID_TOKEN_ID); diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index 592ea826..8ec31a82 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -24,8 +24,6 @@ pub trait IERC721 { fn is_approved_for_all( self: @TContractState, owner: ContractAddress, operator: ContractAddress ) -> bool; - fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); - fn burn(ref self: TContractState, token_id: u256); } #[starknet::interface] From b0493081a5dfeaf6046c7dd769ee51c7aaebacbd Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 14:03:52 +0100 Subject: [PATCH 33/50] add comment for safe_transfer_from --- listings/applications/erc721/src/interfaces.cairo | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index 8ec31a82..f35af282 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -8,6 +8,10 @@ pub const IERC721_RECEIVER_ID: felt252 = pub trait IERC721 { fn balance_of(self: @TContractState, owner: ContractAddress) -> u256; fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; + // NOTE: The function `safe_transfer_from(address _from, address _to, uint256 _tokenId)` is not + // included because the same behavior can be achieved by calling `safe_transfer_from(from, to, + // tokenId, data)` with an empty `data` parameter. + // This approach reduces redundancy in the contract's interface. fn safe_transfer_from( ref self: TContractState, from: ContractAddress, From c5c8bb1e3a7d16fb153cf5f6bbee8df81aeeb5e2 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 15:27:03 +0100 Subject: [PATCH 34/50] implement burn and mint as additional interfaces --- listings/applications/erc721/src/erc721.cairo | 19 +- .../applications/erc721/src/interfaces.cairo | 26 ++- listings/applications/erc721/src/tests.cairo | 194 +++++++++--------- 3 files changed, 130 insertions(+), 109 deletions(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index ceb25030..e40e8377 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -7,6 +7,7 @@ pub mod ERC721 { use openzeppelin_introspection::interface::{ISRC5Dispatcher, ISRC5DispatcherTrait}; use erc721::interfaces::{ IERC721, IERC721ReceiverDispatcher, IERC721ReceiverDispatcherTrait, IERC721_RECEIVER_ID, + IERC721Mintable, IERC721Burnable, }; #[storage] @@ -133,9 +134,23 @@ pub mod ERC721 { } } + #[abi(embed_v0)] + pub impl ERC721Burnable of IERC721Burnable { + fn burn(ref self: ContractState, token_id: u256) { + self._burn(token_id) + } + } + + #[abi(embed_v0)] + pub impl ERC721Mintable of IERC721Mintable { + fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + self._mint(to, token_id) + } + } + #[generate_trait] pub impl InternalImpl of InternalTrait { - fn mint(ref self: ContractState, to: ContractAddress, token_id: u256) { + fn _mint(ref self: ContractState, to: ContractAddress, token_id: u256) { assert(!to.is_zero(), Errors::INVALID_RECEIVER); assert(self.owners.read(token_id).is_zero(), Errors::ALREADY_MINTED); @@ -145,7 +160,7 @@ pub mod ERC721 { self.emit(Transfer { from: Zero::zero(), to, token_id }); } - fn burn(ref self: ContractState, token_id: u256) { + fn _burn(ref self: ContractState, token_id: u256) { let owner = self._require_owned(token_id); self.balances.write(owner, self.balances.read(owner) - 1); diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index f35af282..13d0d0df 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -31,17 +31,27 @@ pub trait IERC721 { } #[starknet::interface] -pub trait IERC721Metadata { - fn name(self: @TState) -> ByteArray; - fn symbol(self: @TState) -> ByteArray; - fn token_uri(self: @TState, token_id: u256) -> ByteArray; +pub trait IERC721Metadata { + fn name(self: @TContractState) -> ByteArray; + fn symbol(self: @TContractState) -> ByteArray; + fn token_uri(self: @TContractState, token_id: u256) -> ByteArray; } #[starknet::interface] -pub trait IERC721Enumerable { - fn total_supply(self: @TState) -> u256; - fn token_by_index(self: @TState, index: u256) -> u256; - fn token_of_owner_by_index(self: @TState, owner: ContractAddress, index: u256) -> u256; +pub trait IERC721Enumerable { + fn total_supply(self: @TContractState) -> u256; + fn token_by_index(self: @TContractState, index: u256) -> u256; + fn token_of_owner_by_index(self: @TContractState, owner: ContractAddress, index: u256) -> u256; +} + +#[starknet::interface] +pub trait IERC721Mintable { + fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); +} + +#[starknet::interface] +pub trait IERC721Burnable { + fn burn(ref self: TContractState, token_id: u256); } #[starknet::interface] diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 832b31a3..43c65ae5 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -1,5 +1,9 @@ use core::num::traits::Zero; -use erc721::interfaces::{IERC721, IERC721Dispatcher, IERC721DispatcherTrait,}; +use erc721::interfaces::{ + IERC721, IERC721Dispatcher, IERC721DispatcherTrait, IERC721Mintable, IERC721MintableDispatcher, + IERC721MintableDispatcherTrait, IERC721Burnable, IERC721BurnableDispatcher, + IERC721BurnableDispatcherTrait +}; use erc721::erc721::{ERC721, ERC721::{Event, Transfer, Approval, ApprovalForAll, InternalTrait},}; use snforge_std::{ declare, test_address, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, @@ -69,21 +73,12 @@ fn deploy_non_receiver() -> ContractAddress { contract_address } -fn setup() -> (IERC721Dispatcher, ContractAddress) { - _setup_optional_mint(true) -} - -fn setup_no_mint() -> (IERC721Dispatcher, ContractAddress) { - _setup_optional_mint(false) -} - -fn _setup_optional_mint(should_mint: bool) -> (IERC721Dispatcher, ContractAddress) { +fn setup(mint_to: ContractAddress) -> (IERC721Dispatcher, ContractAddress) { let contract = declare("ERC721").unwrap().contract_class(); let (contract_address, _) = contract.deploy(@array![]).unwrap(); + let contract = IERC721MintableDispatcher { contract_address }; + contract.mint(mint_to, TOKEN_ID); let contract = IERC721Dispatcher { contract_address }; - if should_mint { - contract.mint(OWNER(), TOKEN_ID); - } (contract, contract_address) } @@ -99,33 +94,33 @@ fn setup_internals() -> ERC721::ContractState { #[test] fn test_balance_of() { - let (contract, _) = setup(); + let (contract, _) = setup(OWNER()); assert_eq!(contract.balance_of(OWNER()), 1); } #[test] #[should_panic(expected: ('ERC721: invalid account',))] fn test_balance_of_zero() { - let (contract, _) = setup(); + let (contract, _) = setup(OWNER()); contract.balance_of(ZERO()); } #[test] fn test_owner_of() { - let (contract, _) = setup(); + let (contract, _) = setup(OWNER()); assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); } #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_owner_of_non_minted() { - let (contract, _) = setup(); + let (contract, _) = setup(OWNER()); contract.owner_of(NONEXISTENT_TOKEN_ID); } #[test] fn test_get_approved() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); let spender = SPENDER(); let token_id = TOKEN_ID; @@ -139,7 +134,7 @@ fn test_get_approved() { #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_get_approved_nonexistent() { - let (contract, _) = setup(); + let (contract, _) = setup(OWNER()); contract.get_approved(NONEXISTENT_TOKEN_ID); } @@ -149,7 +144,7 @@ fn test_get_approved_nonexistent() { #[test] fn test_approve_from_owner() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); let mut spy = spy_events(); start_cheat_caller_address(contract_address, OWNER()); @@ -173,7 +168,7 @@ fn test_approve_from_owner() { #[test] fn test_approve_from_operator() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OWNER()); contract.set_approval_for_all(OPERATOR(), true); @@ -201,7 +196,7 @@ fn test_approve_from_operator() { #[test] #[should_panic(expected: ('ERC721: unauthorized caller',))] fn test_approve_from_unauthorized() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OTHER()); contract.approve(SPENDER(), TOKEN_ID); @@ -210,13 +205,13 @@ fn test_approve_from_unauthorized() { #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_approve_nonexistent() { - let (mut contract, _) = setup(); + let (mut contract, _) = setup(OWNER()); contract.approve(SPENDER(), NONEXISTENT_TOKEN_ID); } #[test] fn test_approve_auth_is_approved_for_all() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); let auth = CALLER(); start_cheat_caller_address(contract_address, OWNER()); @@ -248,7 +243,7 @@ fn test_approve_auth_is_approved_for_all() { #[test] fn test_set_approval_for_all() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); let mut spy = spy_events(); start_cheat_caller_address(contract_address, OWNER()); @@ -291,7 +286,7 @@ fn test_set_approval_for_all() { #[test] #[should_panic(expected: ('ERC721: invalid operator',))] fn test_set_approval_for_all_invalid_operator() { - let (mut contract, _) = setup(); + let (mut contract, _) = setup(OWNER()); contract.set_approval_for_all(ZERO(), true); } @@ -301,40 +296,39 @@ fn test_set_approval_for_all_invalid_operator() { #[test] fn test_transfer_from_owner() { - let (mut contract, contract_address) = setup(); - let token_id = TOKEN_ID; let owner = OWNER(); let recipient = RECIPIENT(); + let (mut contract, contract_address) = setup(owner); // set approval to check reset start_cheat_caller_address(contract_address, owner); - contract.approve(OTHER(), token_id); + contract.approve(OTHER(), TOKEN_ID); - assert_state_before_transfer(contract, owner, recipient, token_id); + assert_state_before_transfer(contract, owner, recipient, TOKEN_ID); - let approved = contract.get_approved(token_id); + let approved = contract.get_approved(TOKEN_ID); assert_eq!(approved, OTHER()); let mut spy = spy_events(); - contract.transfer_from(owner, recipient, token_id); + contract.transfer_from(owner, recipient, TOKEN_ID); spy .assert_emitted( @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: recipient, token_id }) + Event::Transfer(Transfer { from: owner, to: recipient, token_id: TOKEN_ID }) ) ] ); - assert_state_after_transfer(contract, owner, recipient, token_id); + assert_state_after_transfer(contract, owner, recipient, TOKEN_ID); } #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_transfer_from_nonexistent() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OWNER()); contract.transfer_from(ZERO(), RECIPIENT(), NONEXISTENT_TOKEN_ID); } @@ -342,41 +336,42 @@ fn test_transfer_from_nonexistent() { #[test] #[should_panic(expected: ('ERC721: invalid receiver',))] fn test_transfer_from_to_zero() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OWNER()); contract.transfer_from(OWNER(), ZERO(), TOKEN_ID); } #[test] fn test_transfer_from_to_owner() { - let (mut contract, contract_address) = setup(); + let owner = OWNER(); + let (mut contract, contract_address) = setup(owner); let mut spy = spy_events(); - assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); - assert_eq!(contract.balance_of(OWNER()), 1); + assert_eq!(contract.owner_of(TOKEN_ID), owner); + assert_eq!(contract.balance_of(owner), 1); - start_cheat_caller_address(contract_address, OWNER()); - contract.transfer_from(OWNER(), OWNER(), TOKEN_ID); + start_cheat_caller_address(contract_address, owner); + contract.transfer_from(owner, owner, TOKEN_ID); spy .assert_emitted( @array![ ( contract_address, - Event::Transfer(Transfer { from: OWNER(), to: OWNER(), token_id: TOKEN_ID }) + Event::Transfer(Transfer { from: owner, to: owner, token_id: TOKEN_ID }) ) ] ); - assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); - assert_eq!(contract.balance_of(OWNER()), 1); + assert_eq!(contract.owner_of(TOKEN_ID), owner); + assert_eq!(contract.balance_of(owner), 1); } #[test] fn test_transfer_from_approved() { - let (mut contract, contract_address) = setup(); let token_id = TOKEN_ID; let owner = OWNER(); let recipient = RECIPIENT(); + let (mut contract, contract_address) = setup(owner); assert_state_before_transfer(contract, owner, recipient, token_id); @@ -402,10 +397,10 @@ fn test_transfer_from_approved() { #[test] fn test_transfer_from_approved_for_all() { - let (mut contract, contract_address) = setup(); let token_id = TOKEN_ID; let owner = OWNER(); let recipient = RECIPIENT(); + let (mut contract, contract_address) = setup(owner); assert_state_before_transfer(contract, owner, recipient, token_id); @@ -432,7 +427,7 @@ fn test_transfer_from_approved_for_all() { #[test] #[should_panic(expected: ('ERC721: unauthorized caller',))] fn test_transfer_from_unauthorized() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OTHER()); contract.transfer_from(OWNER(), RECIPIENT(), TOKEN_ID); } @@ -443,80 +438,79 @@ fn test_transfer_from_unauthorized() { #[test] fn test_safe_transfer_from_to_account() { - let (mut contract, contract_address) = setup(); let account = deploy_account(); let mut spy = spy_events(); - let token_id = TOKEN_ID; let owner = OWNER(); + let (mut contract, contract_address) = setup(owner); - assert_state_before_transfer(contract, owner, account, token_id); + assert_state_before_transfer(contract, owner, account, TOKEN_ID); start_cheat_caller_address(contract_address, owner); - contract.safe_transfer_from(owner, account, token_id, DATA(true)); + contract.safe_transfer_from(owner, account, TOKEN_ID, DATA(true)); spy .assert_emitted( @array![ - (contract_address, Event::Transfer(Transfer { from: owner, to: account, token_id })) + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: account, token_id: TOKEN_ID }) + ) ] ); - assert_state_after_transfer(contract, owner, account, token_id); + assert_state_after_transfer(contract, owner, account, TOKEN_ID); } #[test] fn test_safe_transfer_from_to_receiver() { - let (mut contract, contract_address) = setup(); let receiver = deploy_receiver(); let mut spy = spy_events(); - let token_id = TOKEN_ID; let owner = OWNER(); + let (mut contract, contract_address) = setup(owner); - assert_state_before_transfer(contract, owner, receiver, token_id); + assert_state_before_transfer(contract, owner, receiver, TOKEN_ID); start_cheat_caller_address(contract_address, owner); - contract.safe_transfer_from(owner, receiver, token_id, DATA(true)); + contract.safe_transfer_from(owner, receiver, TOKEN_ID, DATA(true)); spy .assert_emitted( @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: receiver, token_id }) + Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }) ) ] ); - assert_state_after_transfer(contract, owner, receiver, token_id); + assert_state_after_transfer(contract, owner, receiver, TOKEN_ID); } #[test] #[should_panic(expected: ('ERC721: safe transfer failed',))] fn test_safe_transfer_from_to_receiver_failure() { - let (mut contract, contract_address) = setup(); let receiver = deploy_receiver(); - let token_id = TOKEN_ID; let owner = OWNER(); + let (mut contract, contract_address) = setup(owner); start_cheat_caller_address(contract_address, owner); - contract.safe_transfer_from(owner, receiver, token_id, DATA(false)); + contract.safe_transfer_from(owner, receiver, TOKEN_ID, DATA(false)); } #[test] #[ignore] // REASON: should_panic attribute not fit for complex panic messages. #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] fn test_safe_transfer_from_to_non_receiver() { - let (mut contract, contract_address) = setup(); let recipient = deploy_non_receiver(); - let token_id = TOKEN_ID; let owner = OWNER(); + let (mut contract, contract_address) = setup(owner); start_cheat_caller_address(contract_address, owner); - contract.safe_transfer_from(owner, recipient, token_id, DATA(true)); + contract.safe_transfer_from(owner, recipient, TOKEN_ID, DATA(true)); } #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_safe_transfer_from_nonexistent() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OWNER()); contract.safe_transfer_from(ZERO(), RECIPIENT(), NONEXISTENT_TOKEN_ID, DATA(true)); } @@ -524,74 +518,72 @@ fn test_safe_transfer_from_nonexistent() { #[test] #[should_panic(expected: ('ERC721: invalid receiver',))] fn test_safe_transfer_from_to_zero() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OWNER()); contract.safe_transfer_from(OWNER(), ZERO(), TOKEN_ID, DATA(true)); } #[test] fn test_safe_transfer_from_to_owner() { - let (mut contract, contract_address) = setup_no_mint(); - let token_id = TOKEN_ID; let owner = deploy_receiver(); + let (mut contract, contract_address) = setup(owner); - contract.mint(owner, token_id); - - assert_eq!(contract.owner_of(token_id), owner); + assert_eq!(contract.owner_of(TOKEN_ID), owner); assert_eq!(contract.balance_of(owner), 1); let mut spy = spy_events(); start_cheat_caller_address(contract_address, owner); - contract.safe_transfer_from(owner, owner, token_id, DATA(true)); + contract.safe_transfer_from(owner, owner, TOKEN_ID, DATA(true)); spy .assert_emitted( @array![ - (contract_address, Event::Transfer(Transfer { from: owner, to: owner, token_id })) + ( + contract_address, + Event::Transfer(Transfer { from: owner, to: owner, token_id: TOKEN_ID }) + ) ] ); - assert_eq!(contract.owner_of(token_id), owner); + assert_eq!(contract.owner_of(TOKEN_ID), owner); assert_eq!(contract.balance_of(owner), 1); } #[test] fn test_safe_transfer_from_approved() { - let (mut contract, contract_address) = setup(); let receiver = deploy_receiver(); - let token_id = TOKEN_ID; let owner = OWNER(); + let (mut contract, contract_address) = setup(owner); - assert_state_before_transfer(contract, owner, receiver, token_id); + assert_state_before_transfer(contract, owner, receiver, TOKEN_ID); start_cheat_caller_address(contract_address, owner); - contract.approve(OPERATOR(), token_id); + contract.approve(OPERATOR(), TOKEN_ID); let mut spy = spy_events(); start_cheat_caller_address(contract_address, OPERATOR()); - contract.safe_transfer_from(owner, receiver, token_id, DATA(true)); + contract.safe_transfer_from(owner, receiver, TOKEN_ID, DATA(true)); spy .assert_emitted( @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: receiver, token_id }) + Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }) ) ] ); - assert_state_after_transfer(contract, owner, receiver, token_id); + assert_state_after_transfer(contract, owner, receiver, TOKEN_ID); } #[test] fn test_safe_transfer_from_approved_for_all() { - let (mut contract, contract_address) = setup(); let receiver = deploy_receiver(); - let token_id = TOKEN_ID; let owner = OWNER(); + let (mut contract, contract_address) = setup(owner); - assert_state_before_transfer(contract, owner, receiver, token_id); + assert_state_before_transfer(contract, owner, receiver, TOKEN_ID); start_cheat_caller_address(contract_address, owner); contract.set_approval_for_all(OPERATOR(), true); @@ -599,24 +591,24 @@ fn test_safe_transfer_from_approved_for_all() { let mut spy = spy_events(); start_cheat_caller_address(contract_address, OPERATOR()); - contract.safe_transfer_from(owner, receiver, token_id, DATA(true)); + contract.safe_transfer_from(owner, receiver, TOKEN_ID, DATA(true)); spy .assert_emitted( @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: receiver, token_id }) + Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }) ) ] ); - assert_state_after_transfer(contract, owner, receiver, token_id); + assert_state_after_transfer(contract, owner, receiver, TOKEN_ID); } #[test] #[should_panic(expected: ('ERC721: unauthorized caller',))] fn test_safe_transfer_from_unauthorized() { - let (mut contract, contract_address) = setup(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OTHER()); contract.safe_transfer_from(OWNER(), RECIPIENT(), TOKEN_ID, DATA(true)); } @@ -627,40 +619,43 @@ fn test_safe_transfer_from_unauthorized() { #[test] fn test_mint() { - let (mut contract, contract_address) = setup_no_mint(); let mut spy = spy_events(); let recipient = RECIPIENT(); - let token_id = TOKEN_ID; + let (mut contract, contract_address) = setup(OWNER()); assert!(contract.balance_of(recipient).is_zero()); - contract.mint(recipient, TOKEN_ID); + { + let mut contract = IERC721MintableDispatcher { contract_address }; + contract.mint(recipient, TOKEN_ID); + } + spy .assert_emitted( @array![ ( contract_address, - Event::Transfer(Transfer { from: ZERO(), to: recipient, token_id }) + Event::Transfer(Transfer { from: ZERO(), to: recipient, token_id: TOKEN_ID }) ) ] ); - assert_eq!(contract.owner_of(token_id), recipient); + assert_eq!(contract.owner_of(TOKEN_ID), recipient); assert_eq!(contract.balance_of(recipient), 1); - assert!(contract.get_approved(token_id).is_zero()); + assert!(contract.get_approved(TOKEN_ID).is_zero()); } #[test] #[should_panic(expected: ('ERC721: invalid receiver',))] fn test_mint_to_zero() { - let (mut contract, _) = setup(); - contract.mint(ZERO(), TOKEN_ID); + setup(ZERO()); } #[test] #[should_panic(expected: ('ERC721: token already minted',))] fn test_mint_already_exist() { - let (mut contract, _) = setup(); + let (_, contract_address) = setup(OWNER()); + let mut contract = IERC721MintableDispatcher { contract_address }; contract.mint(RECIPIENT(), TOKEN_ID); } @@ -701,7 +696,8 @@ fn test_burn() { #[test] #[should_panic(expected: ('ERC721: invalid token ID',))] fn test_burn_nonexistent() { - let (mut contract, _) = setup(); + let (_, contract_address) = setup(OWNER()); + let mut contract = IERC721BurnableDispatcher { contract_address }; contract.burn(NONEXISTENT_TOKEN_ID); } From 815118b94e698fa21f791b0e2e72e25e0f0343fb Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 16:00:46 +0100 Subject: [PATCH 35/50] fix tests --- listings/applications/erc721/src/tests.cairo | 56 +++++++++++++------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 43c65ae5..6db40159 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -1,8 +1,8 @@ use core::num::traits::Zero; use erc721::interfaces::{ - IERC721, IERC721Dispatcher, IERC721DispatcherTrait, IERC721Mintable, IERC721MintableDispatcher, - IERC721MintableDispatcherTrait, IERC721Burnable, IERC721BurnableDispatcher, - IERC721BurnableDispatcherTrait + IERC721, IERC721Dispatcher, IERC721DispatcherTrait, IERC721SafeDispatcher, + IERC721SafeDispatcherTrait, IERC721Mintable, IERC721MintableDispatcher, + IERC721MintableDispatcherTrait, IERC721BurnableDispatcher, IERC721BurnableDispatcherTrait }; use erc721::erc721::{ERC721, ERC721::{Event, Transfer, Approval, ApprovalForAll, InternalTrait},}; use snforge_std::{ @@ -10,7 +10,6 @@ use snforge_std::{ spy_events, EventSpyAssertionsTrait, }; use starknet::{ContractAddress, contract_address_const,}; -use starknet::storage::StorageMapReadAccess; pub const SUCCESS: felt252 = 'SUCCESS'; pub const FAILURE: felt252 = 'FAILURE'; @@ -76,8 +75,10 @@ fn deploy_non_receiver() -> ContractAddress { fn setup(mint_to: ContractAddress) -> (IERC721Dispatcher, ContractAddress) { let contract = declare("ERC721").unwrap().contract_class(); let (contract_address, _) = contract.deploy(@array![]).unwrap(); - let contract = IERC721MintableDispatcher { contract_address }; - contract.mint(mint_to, TOKEN_ID); + if mint_to != ZERO() { + let contract = IERC721MintableDispatcher { contract_address }; + contract.mint(mint_to, TOKEN_ID); + } let contract = IERC721Dispatcher { contract_address }; (contract, contract_address) } @@ -621,7 +622,7 @@ fn test_safe_transfer_from_unauthorized() { fn test_mint() { let mut spy = spy_events(); let recipient = RECIPIENT(); - let (mut contract, contract_address) = setup(OWNER()); + let (mut contract, contract_address) = setup(ZERO()); assert!(contract.balance_of(recipient).is_zero()); @@ -648,7 +649,9 @@ fn test_mint() { #[test] #[should_panic(expected: ('ERC721: invalid receiver',))] fn test_mint_to_zero() { - setup(ZERO()); + let (_, contract_address) = setup(ZERO()); + let mut contract = IERC721MintableDispatcher { contract_address }; + contract.mint(ZERO(), TOKEN_ID); } #[test] @@ -664,20 +667,25 @@ fn test_mint_already_exist() { // #[test] +#[feature("safe_dispatcher")] fn test_burn() { - let mut state = setup_internals(); - let contract_address = test_address(); + let (mut contract, contract_address) = setup(OWNER()); start_cheat_caller_address(contract_address, OWNER()); - state.approve(OTHER(), TOKEN_ID); + // we test that approvals get removed when burning + contract.approve(OTHER(), TOKEN_ID); - assert_eq!(state.owner_of(TOKEN_ID), OWNER()); - assert_eq!(state.balance_of(OWNER()), 1); - assert_eq!(state.get_approved(TOKEN_ID), OTHER()); + assert_eq!(contract.owner_of(TOKEN_ID), OWNER()); + assert_eq!(contract.balance_of(OWNER()), 1); + assert_eq!(contract.get_approved(TOKEN_ID), OTHER()); let mut spy = spy_events(); - state.burn(TOKEN_ID); + { + let mut contract = IERC721BurnableDispatcher { contract_address }; + contract.burn(TOKEN_ID); + } + spy .assert_emitted( @array![ @@ -688,9 +696,21 @@ fn test_burn() { ] ); - assert_eq!(state.owners.read(TOKEN_ID), ZERO()); - assert_eq!(state.balance_of(OWNER()), 0); - assert_eq!(state.approvals.read(TOKEN_ID), ZERO()); + assert_eq!(contract.balance_of(OWNER()), 0); + + let contract = IERC721SafeDispatcher { contract_address }; + match contract.owner_of(TOKEN_ID) { + Result::Ok(_) => panic!("`owner_of` did not panic"), + Result::Err(panic_data) => { + assert(*panic_data.at(0) == 'ERC721: invalid token ID', *panic_data.at(0)); + } + }; + match contract.get_approved(TOKEN_ID) { + Result::Ok(_) => panic!("`get_approved` did not panic"), + Result::Err(panic_data) => { + assert(*panic_data.at(0) == 'ERC721: invalid token ID', *panic_data.at(0)); + } + }; } #[test] From 3fc8031017b42ab9d51c9f9039fcd061ccbfc4c6 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 16:04:49 +0100 Subject: [PATCH 36/50] add comment above metadata & enumerable --- .../applications/erc721/src/interfaces.cairo | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index 13d0d0df..540cfc5c 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -30,20 +30,6 @@ pub trait IERC721 { ) -> bool; } -#[starknet::interface] -pub trait IERC721Metadata { - fn name(self: @TContractState) -> ByteArray; - fn symbol(self: @TContractState) -> ByteArray; - fn token_uri(self: @TContractState, token_id: u256) -> ByteArray; -} - -#[starknet::interface] -pub trait IERC721Enumerable { - fn total_supply(self: @TContractState) -> u256; - fn token_by_index(self: @TContractState, index: u256) -> u256; - fn token_of_owner_by_index(self: @TContractState, owner: ContractAddress, index: u256) -> u256; -} - #[starknet::interface] pub trait IERC721Mintable { fn mint(ref self: TContractState, to: ContractAddress, token_id: u256); @@ -64,6 +50,26 @@ pub trait IERC721Receiver { data: Span ) -> felt252; } + +// The `IERC721Metadata` and `IERC721Enumerable` interfaces are included here +// as optional extensions to the ERC721 standard. While they provide additional +// functionality (such as token metadata and enumeration), they are not +// implemented in this example. Including these interfaces demonstrates how they +// can be integrated and serves as a starting point for developers who wish to +// extend the functionality. +#[starknet::interface] +pub trait IERC721Metadata { + fn name(self: @TContractState) -> ByteArray; + fn symbol(self: @TContractState) -> ByteArray; + fn token_uri(self: @TContractState, token_id: u256) -> ByteArray; +} + +#[starknet::interface] +pub trait IERC721Enumerable { + fn total_supply(self: @TContractState) -> u256; + fn token_by_index(self: @TContractState, index: u256) -> u256; + fn token_of_owner_by_index(self: @TContractState, owner: ContractAddress, index: u256) -> u256; +} // [!endregion interface] From 65ffa82cffcad2db0ce09579f9444d8119ce69d8 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 16:06:14 +0100 Subject: [PATCH 37/50] fix links in erc721.md --- pages/applications/erc721.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/applications/erc721.md b/pages/applications/erc721.md index 01256fda..33caf5d6 100644 --- a/pages/applications/erc721.md +++ b/pages/applications/erc721.md @@ -9,7 +9,7 @@ For a deeper understanding of the ERC721 interface specifications and its functi To create an ERC721 contract, it must implement the following interface: ```cairo -// [!include ~/listings/applications/721/src/interfaces.cairo:interface] +// [!include ~/listings/applications/erc721/src/interfaces.cairo:interface] ``` Because function names in Starknet should be written in _snake_case_, the Starknet ERC721 interface is slightly different from the Solidity ERC721 interface which uses _camelCase_. @@ -17,7 +17,7 @@ Because function names in Starknet should be written in _snake_case_, the Starkn Here's an implementation of the ERC721 interface in Cairo: ```cairo -// [!include ~/listings/applications/721/src/erc721.cairo] +// [!include ~/listings/applications/erc721/src/erc721.cairo] ``` There are other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.20.0/erc721) one. From f6983ba9b3e9dd5620814902f939ff3d831ae0cb Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 16:10:00 +0100 Subject: [PATCH 38/50] refactor interfaces.cairo --- listings/applications/erc721/src/interfaces.cairo | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index 540cfc5c..8d7d3727 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -1,17 +1,14 @@ use starknet::ContractAddress; -pub const IERC721_RECEIVER_ID: felt252 = - 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; - // [!region interface] #[starknet::interface] pub trait IERC721 { fn balance_of(self: @TContractState, owner: ContractAddress) -> u256; fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; - // NOTE: The function `safe_transfer_from(address _from, address _to, uint256 _tokenId)` is not - // included because the same behavior can be achieved by calling `safe_transfer_from(from, to, - // tokenId, data)` with an empty `data` parameter. - // This approach reduces redundancy in the contract's interface. + // The function `safe_transfer_from(address _from, address _to, uint256 _tokenId)` + // is not included because the same behavior can be achieved by calling + // `safe_transfer_from(from, to, tokenId, data)` with an empty `data` + // parameter. This approach reduces redundancy in the contract's interface. fn safe_transfer_from( ref self: TContractState, from: ContractAddress, @@ -40,6 +37,9 @@ pub trait IERC721Burnable { fn burn(ref self: TContractState, token_id: u256); } +pub const IERC721_RECEIVER_ID: felt252 = + 0x3a0dff5f70d80458ad14ae37bb182a728e3c8cdda0402a5daa86620bdf910bc; + #[starknet::interface] pub trait IERC721Receiver { fn on_erc721_received( From c8ac7267c7d697c7b5b5f1fe94e887d5781d4fb7 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 16:10:10 +0100 Subject: [PATCH 39/50] move erc721 route below erc20 --- routes.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routes.ts b/routes.ts index 621c1057..3456d7db 100644 --- a/routes.ts +++ b/routes.ts @@ -131,6 +131,10 @@ const config: Sidebar = [ text: "ERC20 Token", link: "/applications/erc20", }, + { + text: "ERC721 NFT", + link: "/applications/erc721", + }, { text: "NFT Dutch Auction", link: "/applications/nft_dutch_auction", @@ -167,10 +171,6 @@ const config: Sidebar = [ text: "Random Number Generator", link: "/applications/random_number_generator", }, - { - text: "ERC721 NFT", - link: "/applications/erc721", - }, ], }, { From de7ba302f94dd1f89fa3536968a5fda408031892 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 16:37:57 +0100 Subject: [PATCH 40/50] remove redundant newline from interfaces.cairo --- listings/applications/erc721/src/interfaces.cairo | 1 - 1 file changed, 1 deletion(-) diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index 8d7d3727..993dce7e 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -72,4 +72,3 @@ pub trait IERC721Enumerable { } // [!endregion interface] - From 954167869e4a2fbdac4bc61aa592bccdce584f90 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Sat, 7 Dec 2024 16:56:12 +0100 Subject: [PATCH 41/50] fix test_safe_transfer_from_to_non_receiver --- listings/applications/erc721/src/tests.cairo | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 6db40159..4d202b98 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -497,15 +497,14 @@ fn test_safe_transfer_from_to_receiver_failure() { } #[test] -#[ignore] // REASON: should_panic attribute not fit for complex panic messages. -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND',))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED',))] fn test_safe_transfer_from_to_non_receiver() { - let recipient = deploy_non_receiver(); + let none_receiver = deploy_non_receiver(); let owner = OWNER(); let (mut contract, contract_address) = setup(owner); start_cheat_caller_address(contract_address, owner); - contract.safe_transfer_from(owner, recipient, TOKEN_ID, DATA(true)); + contract.safe_transfer_from(owner, none_receiver, TOKEN_ID, DATA(true)); } #[test] From d37bfed92110c595fad414f409952d266f02ff9b Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 3 Mar 2025 15:40:37 +0100 Subject: [PATCH 42/50] revert changes to pnpm-lock --- pnpm-lock.yaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f1e3d7c..6b7b88cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1383,10 +1383,6 @@ packages: emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} - - emoji-regex-xs@1.0.0: - resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} - emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -3496,6 +3492,9 @@ snapshots: '@rollup/rollup-win32-ia32-msvc@4.34.6': optional: true + '@rollup/rollup-win32-x64-msvc@4.34.6': + optional: true + '@shikijs/core@1.23.1': dependencies: '@shikijs/engine-javascript': 1.23.1 @@ -3936,9 +3935,6 @@ snapshots: electron-to-chromium@1.5.96: {} emoji-regex-xs@1.0.0: {} - - emoji-regex-xs@1.0.0: {} - emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} From 82ab46a8576e399b7de133622d62b797ace045b8 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 3 Mar 2025 15:41:34 +0100 Subject: [PATCH 43/50] revert changes to pnpm-lock --- pnpm-lock.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b7b88cc..06575621 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1383,6 +1383,7 @@ packages: emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -3935,6 +3936,7 @@ snapshots: electron-to-chromium@1.5.96: {} emoji-regex-xs@1.0.0: {} + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} From e3d04fa1b8dcc40ce9454c95646d05996d287af9 Mon Sep 17 00:00:00 2001 From: Nenad Date: Mon, 3 Mar 2025 15:46:43 +0100 Subject: [PATCH 44/50] make erc721 cairo link version-agnostic --- pages/applications/erc721.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/applications/erc721.md b/pages/applications/erc721.md index 33caf5d6..6421f62b 100644 --- a/pages/applications/erc721.md +++ b/pages/applications/erc721.md @@ -20,4 +20,4 @@ Here's an implementation of the ERC721 interface in Cairo: // [!include ~/listings/applications/erc721/src/erc721.cairo] ``` -There are other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/0.20.0/erc721) one. +There are other implementations, such as the [Open Zeppelin](https://docs.openzeppelin.com/contracts-cairo/erc721) one. From 67bd3fd9a7e22dd0d8144977bc847c9768cf727c Mon Sep 17 00:00:00 2001 From: Nenad Date: Tue, 4 Mar 2025 11:49:40 +0100 Subject: [PATCH 45/50] update openzeppelin dep declarations --- listings/applications/erc721/Scarb.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/listings/applications/erc721/Scarb.toml b/listings/applications/erc721/Scarb.toml index acb38eab..b69dad80 100644 --- a/listings/applications/erc721/Scarb.toml +++ b/listings/applications/erc721/Scarb.toml @@ -5,7 +5,8 @@ edition.workspace = true [dependencies] starknet.workspace = true -openzeppelin.workspace = true +openzeppelin_account.workspace = true +openzeppelin_introspection.workspace = true [dev-dependencies] assert_macros.workspace = true From e0c5803149e4098dc4b4be7d29045783e8b3cf43 Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 5 Mar 2025 11:16:43 +0100 Subject: [PATCH 46/50] fix fmt errors --- listings/applications/erc721/src/erc721.cairo | 24 ++-- .../applications/erc721/src/interfaces.cairo | 9 +- .../erc721/src/mocks/account.cairo | 4 +- .../erc721/src/mocks/receiver.cairo | 6 +- listings/applications/erc721/src/tests.cairo | 122 +++++++++--------- 5 files changed, 82 insertions(+), 83 deletions(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index e40e8377..c44d6ba2 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -30,21 +30,21 @@ pub mod ERC721 { pub struct Transfer { pub from: ContractAddress, pub to: ContractAddress, - pub token_id: u256 + pub token_id: u256, } #[derive(Drop, starknet::Event)] pub struct Approval { pub owner: ContractAddress, pub approved: ContractAddress, - pub token_id: u256 + pub token_id: u256, } #[derive(Drop, starknet::Event)] pub struct ApprovalForAll { pub owner: ContractAddress, pub operator: ContractAddress, - pub approved: bool + pub approved: bool, } pub mod Errors { @@ -70,7 +70,7 @@ pub mod ERC721 { } fn set_approval_for_all( - ref self: ContractState, operator: ContractAddress, approved: bool + ref self: ContractState, operator: ContractAddress, approved: bool, ) { assert(!operator.is_zero(), Errors::INVALID_OPERATOR); let owner = get_caller_address(); @@ -82,7 +82,7 @@ pub mod ERC721 { let owner = self._require_owned(token_id); let caller = get_caller_address(); assert( - caller == owner || self.is_approved_for_all(owner, caller), Errors::UNAUTHORIZED + caller == owner || self.is_approved_for_all(owner, caller), Errors::UNAUTHORIZED, ); self.approvals.write(token_id, approved); @@ -95,14 +95,14 @@ pub mod ERC721 { } fn transfer_from( - ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, ) { let previous_owner = self._require_owned(token_id); assert(from == previous_owner, Errors::INVALID_SENDER); assert(!to.is_zero(), Errors::INVALID_RECEIVER); assert( self._is_approved_or_owner(from, get_caller_address(), token_id), - Errors::UNAUTHORIZED + Errors::UNAUTHORIZED, ); self.balances.write(from, self.balances.read(from) - 1); @@ -118,17 +118,17 @@ pub mod ERC721 { from: ContractAddress, to: ContractAddress, token_id: u256, - data: Span + data: Span, ) { Self::transfer_from(ref self, from, to, token_id); assert( self._check_on_erc721_received(from, to, token_id, data), - Errors::SAFE_TRANSFER_FAILED + Errors::SAFE_TRANSFER_FAILED, ); } fn is_approved_for_all( - self: @ContractState, owner: ContractAddress, operator: ContractAddress + self: @ContractState, owner: ContractAddress, operator: ContractAddress, ) -> bool { self.operator_approvals.read((owner, operator)) } @@ -178,7 +178,7 @@ pub mod ERC721 { } fn _is_approved_or_owner( - self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256 + self: @ContractState, owner: ContractAddress, spender: ContractAddress, token_id: u256, ) -> bool { !spender.is_zero() && (owner == spender @@ -191,7 +191,7 @@ pub mod ERC721 { from: ContractAddress, to: ContractAddress, token_id: u256, - data: Span + data: Span, ) -> bool { let src5_dispatcher = ISRC5Dispatcher { contract_address: to }; diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index 993dce7e..bcd1294c 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -14,16 +14,16 @@ pub trait IERC721 { from: ContractAddress, to: ContractAddress, token_id: u256, - data: Span + data: Span, ); fn transfer_from( - ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256 + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256, ); fn approve(ref self: TContractState, approved: ContractAddress, token_id: u256); fn set_approval_for_all(ref self: TContractState, operator: ContractAddress, approved: bool); fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; fn is_approved_for_all( - self: @TContractState, owner: ContractAddress, operator: ContractAddress + self: @TContractState, owner: ContractAddress, operator: ContractAddress, ) -> bool; } @@ -47,7 +47,7 @@ pub trait IERC721Receiver { operator: ContractAddress, from: ContractAddress, token_id: u256, - data: Span + data: Span, ) -> felt252; } @@ -71,4 +71,3 @@ pub trait IERC721Enumerable { fn token_of_owner_by_index(self: @TContractState, owner: ContractAddress, index: u256) -> u256; } // [!endregion interface] - diff --git a/listings/applications/erc721/src/mocks/account.cairo b/listings/applications/erc721/src/mocks/account.cairo index 68533035..2a617c17 100644 --- a/listings/applications/erc721/src/mocks/account.cairo +++ b/listings/applications/erc721/src/mocks/account.cairo @@ -27,7 +27,7 @@ pub mod AccountMock { #[substorage(v0)] pub account: AccountComponent::Storage, #[substorage(v0)] - pub src5: SRC5Component::Storage + pub src5: SRC5Component::Storage, } #[event] @@ -36,7 +36,7 @@ pub mod AccountMock { #[flat] AccountEvent: AccountComponent::Event, #[flat] - SRC5Event: SRC5Component::Event + SRC5Event: SRC5Component::Event, } #[constructor] diff --git a/listings/applications/erc721/src/mocks/receiver.cairo b/listings/applications/erc721/src/mocks/receiver.cairo index 1c18915c..abd186d8 100644 --- a/listings/applications/erc721/src/mocks/receiver.cairo +++ b/listings/applications/erc721/src/mocks/receiver.cairo @@ -16,14 +16,14 @@ pub mod ERC721ReceiverMock { #[storage] pub struct Storage { #[substorage(v0)] - pub src5: SRC5Component::Storage + pub src5: SRC5Component::Storage, } #[event] #[derive(Drop, starknet::Event)] enum Event { #[flat] - SRC5Event: SRC5Component::Event + SRC5Event: SRC5Component::Event, } #[constructor] @@ -38,7 +38,7 @@ pub mod ERC721ReceiverMock { operator: ContractAddress, from: ContractAddress, token_id: u256, - data: Span + data: Span, ) -> felt252 { if *data.at(0) == super::SUCCESS { IERC721_RECEIVER_ID diff --git a/listings/applications/erc721/src/tests.cairo b/listings/applications/erc721/src/tests.cairo index 4d202b98..5b19ac56 100644 --- a/listings/applications/erc721/src/tests.cairo +++ b/listings/applications/erc721/src/tests.cairo @@ -2,14 +2,14 @@ use core::num::traits::Zero; use erc721::interfaces::{ IERC721, IERC721Dispatcher, IERC721DispatcherTrait, IERC721SafeDispatcher, IERC721SafeDispatcherTrait, IERC721Mintable, IERC721MintableDispatcher, - IERC721MintableDispatcherTrait, IERC721BurnableDispatcher, IERC721BurnableDispatcherTrait + IERC721MintableDispatcherTrait, IERC721BurnableDispatcher, IERC721BurnableDispatcherTrait, }; -use erc721::erc721::{ERC721, ERC721::{Event, Transfer, Approval, ApprovalForAll, InternalTrait},}; +use erc721::erc721::{ERC721, ERC721::{Event, Transfer, Approval, ApprovalForAll, InternalTrait}}; use snforge_std::{ declare, test_address, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, spy_events, EventSpyAssertionsTrait, }; -use starknet::{ContractAddress, contract_address_const,}; +use starknet::{ContractAddress, contract_address_const}; pub const SUCCESS: felt252 = 'SUCCESS'; pub const FAILURE: felt252 = 'FAILURE'; @@ -157,10 +157,10 @@ fn test_approve_from_owner() { ( contract_address, Event::Approval( - Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID } - ) - ) - ] + Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID }, + ), + ), + ], ); let approved = contract.get_approved(TOKEN_ID); @@ -184,10 +184,10 @@ fn test_approve_from_operator() { ( contract_address, Event::Approval( - Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID } - ) - ) - ] + Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID }, + ), + ), + ], ); let approved = contract.get_approved(TOKEN_ID); @@ -228,10 +228,10 @@ fn test_approve_auth_is_approved_for_all() { ( contract_address, Event::Approval( - Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID } - ) - ) - ] + Approval { owner: OWNER(), approved: SPENDER(), token_id: TOKEN_ID }, + ), + ), + ], ); let approved = contract.get_approved(TOKEN_ID); @@ -258,10 +258,10 @@ fn test_set_approval_for_all() { ( contract_address, Event::ApprovalForAll( - ApprovalForAll { owner: OWNER(), operator: OPERATOR(), approved: true } - ) - ) - ] + ApprovalForAll { owner: OWNER(), operator: OPERATOR(), approved: true }, + ), + ), + ], ); let is_approved_for_all = contract.is_approved_for_all(OWNER(), OPERATOR()); @@ -274,10 +274,10 @@ fn test_set_approval_for_all() { ( contract_address, Event::ApprovalForAll( - ApprovalForAll { owner: OWNER(), operator: OPERATOR(), approved: false } - ) - ) - ] + ApprovalForAll { owner: OWNER(), operator: OPERATOR(), approved: false }, + ), + ), + ], ); let not_approved_for_all = !contract.is_approved_for_all(OWNER(), OPERATOR()); @@ -318,9 +318,9 @@ fn test_transfer_from_owner() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: recipient, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: owner, to: recipient, token_id: TOKEN_ID }), + ), + ], ); assert_state_after_transfer(contract, owner, recipient, TOKEN_ID); @@ -358,9 +358,9 @@ fn test_transfer_from_to_owner() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: owner, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: owner, to: owner, token_id: TOKEN_ID }), + ), + ], ); assert_eq!(contract.owner_of(TOKEN_ID), owner); @@ -388,9 +388,9 @@ fn test_transfer_from_approved() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: recipient, token_id }) - ) - ] + Event::Transfer(Transfer { from: owner, to: recipient, token_id }), + ), + ], ); assert_state_after_transfer(contract, owner, recipient, token_id); @@ -417,9 +417,9 @@ fn test_transfer_from_approved_for_all() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: recipient, token_id }) - ) - ] + Event::Transfer(Transfer { from: owner, to: recipient, token_id }), + ), + ], ); assert_state_after_transfer(contract, owner, recipient, token_id); @@ -453,9 +453,9 @@ fn test_safe_transfer_from_to_account() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: account, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: owner, to: account, token_id: TOKEN_ID }), + ), + ], ); assert_state_after_transfer(contract, owner, account, TOKEN_ID); @@ -477,9 +477,9 @@ fn test_safe_transfer_from_to_receiver() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }), + ), + ], ); assert_state_after_transfer(contract, owner, receiver, TOKEN_ID); @@ -497,7 +497,7 @@ fn test_safe_transfer_from_to_receiver_failure() { } #[test] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED',))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_safe_transfer_from_to_non_receiver() { let none_receiver = deploy_non_receiver(); let owner = OWNER(); @@ -540,9 +540,9 @@ fn test_safe_transfer_from_to_owner() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: owner, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: owner, to: owner, token_id: TOKEN_ID }), + ), + ], ); assert_eq!(contract.owner_of(TOKEN_ID), owner); @@ -569,9 +569,9 @@ fn test_safe_transfer_from_approved() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }), + ), + ], ); assert_state_after_transfer(contract, owner, receiver, TOKEN_ID); @@ -597,9 +597,9 @@ fn test_safe_transfer_from_approved_for_all() { @array![ ( contract_address, - Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: owner, to: receiver, token_id: TOKEN_ID }), + ), + ], ); assert_state_after_transfer(contract, owner, receiver, TOKEN_ID); @@ -635,9 +635,9 @@ fn test_mint() { @array![ ( contract_address, - Event::Transfer(Transfer { from: ZERO(), to: recipient, token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: ZERO(), to: recipient, token_id: TOKEN_ID }), + ), + ], ); assert_eq!(contract.owner_of(TOKEN_ID), recipient); @@ -690,9 +690,9 @@ fn test_burn() { @array![ ( contract_address, - Event::Transfer(Transfer { from: OWNER(), to: ZERO(), token_id: TOKEN_ID }) - ) - ] + Event::Transfer(Transfer { from: OWNER(), to: ZERO(), token_id: TOKEN_ID }), + ), + ], ); assert_eq!(contract.balance_of(OWNER()), 0); @@ -702,13 +702,13 @@ fn test_burn() { Result::Ok(_) => panic!("`owner_of` did not panic"), Result::Err(panic_data) => { assert(*panic_data.at(0) == 'ERC721: invalid token ID', *panic_data.at(0)); - } + }, }; match contract.get_approved(TOKEN_ID) { Result::Ok(_) => panic!("`get_approved` did not panic"), Result::Err(panic_data) => { assert(*panic_data.at(0) == 'ERC721: invalid token ID', *panic_data.at(0)); - } + }, }; } @@ -786,7 +786,7 @@ fn test__is_approved_or_owner_zero_address() { // fn assert_state_before_transfer( - contract: IERC721Dispatcher, owner: ContractAddress, recipient: ContractAddress, token_id: u256 + contract: IERC721Dispatcher, owner: ContractAddress, recipient: ContractAddress, token_id: u256, ) { assert_eq!(contract.owner_of(token_id), owner); assert_eq!(contract.balance_of(owner), 1); @@ -794,7 +794,7 @@ fn assert_state_before_transfer( } fn assert_state_after_transfer( - contract: IERC721Dispatcher, owner: ContractAddress, recipient: ContractAddress, token_id: u256 + contract: IERC721Dispatcher, owner: ContractAddress, recipient: ContractAddress, token_id: u256, ) { assert_eq!(contract.owner_of(token_id), recipient); assert_eq!(contract.balance_of(owner), 0); From 9d9dbe4d245534b95c4395b07663a2804c9d43ab Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 5 Mar 2025 11:17:23 +0100 Subject: [PATCH 47/50] fix fmt errors --- listings/applications/erc721/src/erc721.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listings/applications/erc721/src/erc721.cairo b/listings/applications/erc721/src/erc721.cairo index c44d6ba2..5dcb23ad 100644 --- a/listings/applications/erc721/src/erc721.cairo +++ b/listings/applications/erc721/src/erc721.cairo @@ -198,7 +198,7 @@ pub mod ERC721 { if src5_dispatcher.supports_interface(IERC721_RECEIVER_ID) { IERC721ReceiverDispatcher { contract_address: to } .on_erc721_received( - get_caller_address(), from, token_id, data + get_caller_address(), from, token_id, data, ) == IERC721_RECEIVER_ID } else { src5_dispatcher.supports_interface(openzeppelin_account::interface::ISRC6_ID) From 74b266164ce9e3fb534f16034c85b8b4598158ab Mon Sep 17 00:00:00 2001 From: Nenad Date: Wed, 5 Mar 2025 11:37:49 +0100 Subject: [PATCH 48/50] fix Scarb.lock --- Scarb.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Scarb.lock b/Scarb.lock index 28dde33f..1916bfcd 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -96,7 +96,8 @@ version = "0.1.0" name = "erc721" version = "0.1.0" dependencies = [ - "openzeppelin", + "openzeppelin_account", + "openzeppelin_introspection", "snforge_std", ] From a50fea394434b34e6a357ab719bd3af3595d5ccd Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 5 Mar 2025 11:38:44 +0100 Subject: [PATCH 49/50] add missing newlines in interfaces.cairo --- listings/applications/erc721/src/interfaces.cairo | 2 ++ listings/getting-started/constructor/error.log | 1 + 2 files changed, 3 insertions(+) create mode 100644 listings/getting-started/constructor/error.log diff --git a/listings/applications/erc721/src/interfaces.cairo b/listings/applications/erc721/src/interfaces.cairo index bcd1294c..af620bb0 100644 --- a/listings/applications/erc721/src/interfaces.cairo +++ b/listings/applications/erc721/src/interfaces.cairo @@ -71,3 +71,5 @@ pub trait IERC721Enumerable { fn token_of_owner_by_index(self: @TContractState, owner: ContractAddress, index: u256) -> u256; } // [!endregion interface] + + diff --git a/listings/getting-started/constructor/error.log b/listings/getting-started/constructor/error.log new file mode 100644 index 00000000..a729eedb --- /dev/null +++ b/listings/getting-started/constructor/error.log @@ -0,0 +1 @@ +warn: `edition` field not set in `[package]` section for package `pragma_lib` From 8c66ea09066db53b5c0b1b58f845b2a80d8dca92 Mon Sep 17 00:00:00 2001 From: 0xNeshi Date: Wed, 5 Mar 2025 11:39:14 +0100 Subject: [PATCH 50/50] remove error.log --- listings/getting-started/constructor/error.log | 1 - 1 file changed, 1 deletion(-) delete mode 100644 listings/getting-started/constructor/error.log diff --git a/listings/getting-started/constructor/error.log b/listings/getting-started/constructor/error.log deleted file mode 100644 index a729eedb..00000000 --- a/listings/getting-started/constructor/error.log +++ /dev/null @@ -1 +0,0 @@ -warn: `edition` field not set in `[package]` section for package `pragma_lib`