├── .gitignore ├── src ├── access.cairo ├── tests │ ├── mocks.cairo │ ├── utils │ │ ├── partial_eq.cairo │ │ └── zeroable.cairo │ ├── utils.cairo │ ├── mocks │ │ ├── receiver.cairo │ │ └── signer.cairo │ ├── test_rules_messages.cairo │ ├── constants.cairo │ ├── test_rules_data.cairo │ └── test_rules_tokens.cairo ├── utils.cairo ├── constants.cairo ├── lib.cairo ├── tests.cairo ├── core.cairo ├── core │ ├── voucher.cairo │ ├── messages.cairo │ ├── interface.cairo │ ├── data.cairo │ └── tokens.cairo ├── utils │ ├── partial_eq.cairo │ ├── zeroable.cairo │ └── storage.cairo └── access │ └── ownable.cairo ├── cairo_project.toml ├── README.md └── Scarb.toml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /src/access.cairo: -------------------------------------------------------------------------------- 1 | mod ownable; 2 | -------------------------------------------------------------------------------- /src/tests/mocks.cairo: -------------------------------------------------------------------------------- 1 | mod receiver; 2 | mod signer; 3 | -------------------------------------------------------------------------------- /cairo_project.toml: -------------------------------------------------------------------------------- 1 | [crate_roots] 2 | rules_tokens = "src" 3 | -------------------------------------------------------------------------------- /src/utils.cairo: -------------------------------------------------------------------------------- 1 | mod storage; 2 | mod zeroable; 3 | mod partial_eq; 4 | -------------------------------------------------------------------------------- /src/constants.cairo: -------------------------------------------------------------------------------- 1 | const U120_MAX: u128 = 0xffffffffffffffffffffffffffffff; // 2 ** 120 - 1 2 | -------------------------------------------------------------------------------- /src/lib.cairo: -------------------------------------------------------------------------------- 1 | mod core; 2 | mod access; 3 | mod utils; 4 | mod constants; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | -------------------------------------------------------------------------------- /src/tests.cairo: -------------------------------------------------------------------------------- 1 | mod test_rules_tokens; 2 | mod test_rules_data; 3 | mod test_rules_messages; 4 | mod utils; 5 | mod constants; 6 | mod mocks; 7 | -------------------------------------------------------------------------------- /src/core.cairo: -------------------------------------------------------------------------------- 1 | mod tokens; 2 | use tokens::{ RulesTokens, RulesTokensABIDispatcher, RulesTokensABIDispatcherTrait }; 3 | 4 | mod data; 5 | mod messages; 6 | mod voucher; 7 | 8 | mod interface; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rules protocol 2 | 3 | Core smart contracts of the Rules protocol. 4 | 5 | - for account contract, see [rules-account](https://github.com/ruleslabs/starknet-rules-account) repository. 6 | - for account contract, see [marketplace](https://github.com/ruleslabs/marketplace) repository. 7 | -------------------------------------------------------------------------------- /src/tests/utils/partial_eq.cairo: -------------------------------------------------------------------------------- 1 | use traits::PartialEq; 2 | 3 | // locals 4 | use rules_tokens::core::interface::{ Metadata }; 5 | 6 | impl MetadataEq of PartialEq { 7 | fn eq(lhs: @Metadata, rhs: @Metadata) -> bool { 8 | *lhs.hash == *rhs.hash 9 | } 10 | 11 | #[inline(always)] 12 | fn ne(lhs: @Metadata, rhs: @Metadata) -> bool { 13 | !(lhs == rhs) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tests/utils.cairo: -------------------------------------------------------------------------------- 1 | mod partial_eq; 2 | mod zeroable; 3 | 4 | use array::ArrayTrait; 5 | use core::result::ResultTrait; 6 | use option::OptionTrait; 7 | use starknet::class_hash::Felt252TryIntoClassHash; 8 | use traits::TryInto; 9 | 10 | fn deploy(contract_class_hash: felt252, calldata: Array) -> starknet::ContractAddress { 11 | let (address, _) = starknet::deploy_syscall(contract_class_hash.try_into().unwrap(), 0, calldata.span(), false) 12 | .unwrap(); 13 | 14 | address 15 | } 16 | -------------------------------------------------------------------------------- /src/tests/mocks/receiver.cairo: -------------------------------------------------------------------------------- 1 | #[starknet::contract] 2 | mod Receiver { 3 | use rules_account::account::interface::ISRC6_ID; 4 | 5 | // 6 | // Storage 7 | // 8 | 9 | #[storage] 10 | struct Storage { } 11 | 12 | // 13 | // Constructor 14 | // 15 | 16 | #[constructor] 17 | fn constructor(ref self: ContractState) { } 18 | 19 | // 20 | // impls 21 | // 22 | 23 | #[external(v0)] 24 | fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { 25 | interface_id == ISRC6_ID 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/tests/utils/zeroable.cairo: -------------------------------------------------------------------------------- 1 | use zeroable::Zeroable; 2 | 3 | // locals 4 | use rules_tokens::core::interface::{ Metadata }; 5 | use super::partial_eq::{ MetadataEq }; 6 | 7 | impl MetadataZeroable of Zeroable { 8 | fn zero() -> Metadata { 9 | Metadata { 10 | hash: array![].span(), 11 | } 12 | } 13 | 14 | #[inline(always)] 15 | fn is_zero(self: Metadata) -> bool { 16 | self == MetadataZeroable::zero() 17 | } 18 | 19 | #[inline(always)] 20 | fn is_non_zero(self: Metadata) -> bool { 21 | self != MetadataZeroable::zero() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Scarb.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rules_tokens" 3 | version = "0.1.0" 4 | 5 | [lib] 6 | name = "rules_tokens" 7 | 8 | # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest 9 | 10 | [dependencies] 11 | erc1155 = { git = "https://github.com/ruleslabs/starknet-erc-1155.git" } 12 | messages = { git = "https://github.com/ruleslabs/starknet-messages.git" } 13 | rules_utils = { git = "https://github.com/ruleslabs/starknet-utils.git" } 14 | starknet = ">=2.1.0-rc0" 15 | 16 | [[target.starknet-contract]] 17 | # Enable Sierra codegen. 18 | sierra = true 19 | 20 | # Enable CASM codegen. 21 | casm = true 22 | -------------------------------------------------------------------------------- /src/core/voucher.cairo: -------------------------------------------------------------------------------- 1 | use traits::Into; 2 | use box::BoxTrait; 3 | use messages::typed_data::common::hash_u256; 4 | use messages::typed_data::Message; 5 | 6 | // sn_keccak('Voucher(receiver:felt252,tokenId:u256,amount:u256,salt:felt252)u256(low:felt252,high:felt252)') 7 | const VOUCHER_TYPE_HASH: felt252 = 0x2b7b26b9be07bb06826bb14ffeb28e910317886010a72720cce19e1974bd232; 8 | 9 | #[derive(Serde, Copy, Drop)] 10 | struct Voucher { 11 | receiver: starknet::ContractAddress, 12 | token_id: u256, 13 | amount: u256, 14 | salt: felt252, 15 | } 16 | 17 | impl VoucherMessage of Message { 18 | #[inline(always)] 19 | fn compute_hash(self: @Voucher) -> felt252 { 20 | let mut hash = pedersen::pedersen(0, VOUCHER_TYPE_HASH); 21 | hash = pedersen::pedersen(hash, (*self.receiver).into()); 22 | hash = pedersen::pedersen(hash, hash_u256(*self.token_id)); 23 | hash = pedersen::pedersen(hash, hash_u256(*self.amount)); 24 | hash = pedersen::pedersen(hash, *self.salt); 25 | 26 | pedersen::pedersen(hash, 5) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/partial_eq.cairo: -------------------------------------------------------------------------------- 1 | use traits::PartialEq; 2 | 3 | // locals 4 | use rules_tokens::core::interface::{ Scarcity, CardModel, Pack }; 5 | 6 | impl ScarcityEq of PartialEq { 7 | fn eq(lhs: @Scarcity, rhs: @Scarcity) -> bool { 8 | (*lhs.max_supply == *rhs.max_supply) & (*lhs.name == *rhs.name) 9 | } 10 | 11 | #[inline(always)] 12 | fn ne(lhs: @Scarcity, rhs: @Scarcity) -> bool { 13 | !(lhs == rhs) 14 | } 15 | } 16 | 17 | impl CardModelEq of PartialEq { 18 | fn eq(lhs: @CardModel, rhs: @CardModel) -> bool { 19 | (*lhs.artist_name == *rhs.artist_name) & (*lhs.scarcity_id == *rhs.scarcity_id) & (*lhs.season == *rhs.season) 20 | } 21 | 22 | #[inline(always)] 23 | fn ne(lhs: @CardModel, rhs: @CardModel) -> bool { 24 | !(lhs == rhs) 25 | } 26 | } 27 | 28 | impl PackEq of PartialEq { 29 | fn eq(lhs: @Pack, rhs: @Pack) -> bool { 30 | (*lhs.name == *rhs.name) & (*lhs.season == *rhs.season) 31 | } 32 | 33 | #[inline(always)] 34 | fn ne(lhs: @Pack, rhs: @Pack) -> bool { 35 | !(lhs == rhs) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/tests/mocks/signer.cairo: -------------------------------------------------------------------------------- 1 | #[starknet::contract] 2 | mod Signer { 3 | use array::ArrayTrait; 4 | 5 | // locals 6 | use rules_account::account; 7 | use rules_account::account::Account; 8 | use rules_account::account::Account::{ InternalTrait as AccountInternalTrait }; 9 | 10 | // 11 | // Storage 12 | // 13 | 14 | #[storage] 15 | struct Storage { 16 | _public_key: felt252, 17 | } 18 | 19 | // 20 | // Constructor 21 | // 22 | 23 | #[constructor] 24 | fn constructor(ref self: ContractState, public_key_: felt252) { 25 | self._public_key.write(public_key_); 26 | } 27 | 28 | // 29 | // impl 30 | // 31 | 32 | #[external(v0)] 33 | fn is_valid_signature(self: @ContractState, message: felt252, signature: Array) -> felt252 { 34 | let account_self = Account::unsafe_new_contract_state(); 35 | 36 | if ( 37 | account_self._is_valid_signature(hash: message, signature: signature.span(), public_key: self._public_key.read()) 38 | ) { 39 | starknet::VALIDATED 40 | } else { 41 | 0 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/zeroable.cairo: -------------------------------------------------------------------------------- 1 | use zeroable::Zeroable; 2 | 3 | // locals 4 | use rules_tokens::core::interface::{ Scarcity, CardModel, Pack, Metadata }; 5 | use super::partial_eq::{ CardModelEq, PackEq, ScarcityEq }; 6 | 7 | impl CardModelZeroable of Zeroable { 8 | fn zero() -> CardModel { 9 | CardModel { 10 | artist_name: 0, 11 | season: 0, 12 | scarcity_id: 0, 13 | } 14 | } 15 | 16 | #[inline(always)] 17 | fn is_zero(self: CardModel) -> bool { 18 | self == CardModelZeroable::zero() 19 | } 20 | 21 | #[inline(always)] 22 | fn is_non_zero(self: CardModel) -> bool { 23 | self != CardModelZeroable::zero() 24 | } 25 | } 26 | 27 | impl ScarcityZeroable of Zeroable { 28 | fn zero() -> Scarcity { 29 | Scarcity { 30 | max_supply: 0, 31 | name: 0, 32 | } 33 | } 34 | 35 | #[inline(always)] 36 | fn is_zero(self: Scarcity) -> bool { 37 | self == ScarcityZeroable::zero() 38 | } 39 | 40 | #[inline(always)] 41 | fn is_non_zero(self: Scarcity) -> bool { 42 | self != ScarcityZeroable::zero() 43 | } 44 | } 45 | 46 | impl PackZeroable of Zeroable { 47 | fn zero() -> Pack { 48 | Pack { 49 | name: 0, 50 | season: 0, 51 | } 52 | } 53 | 54 | #[inline(always)] 55 | fn is_zero(self: Pack) -> bool { 56 | self == PackZeroable::zero() 57 | } 58 | 59 | #[inline(always)] 60 | fn is_non_zero(self: Pack) -> bool { 61 | self != PackZeroable::zero() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/tests/test_rules_messages.cairo: -------------------------------------------------------------------------------- 1 | use core::traits::Into; 2 | use array::ArrayTrait; 3 | use starknet::testing; 4 | 5 | // locals 6 | use rules_tokens::core::messages::RulesMessages; 7 | use rules_tokens::core::interface::IRulesMessages; 8 | use rules_tokens::core::messages::RulesMessages::{ ContractState as RulesMessagesContractState, InternalTrait }; 9 | 10 | use super::constants::{ 11 | CHAIN_ID, 12 | BLOCK_TIMESTAMP, 13 | VOUCHER_1, 14 | VOUCHER_SIGNATURE_1, 15 | VOUCHER_SIGNER_PUBLIC_KEY, 16 | }; 17 | use super::utils; 18 | use super::mocks::signer::Signer; 19 | 20 | // dispatchers 21 | use rules_account::account::{ AccountABIDispatcher, AccountABIDispatcherTrait }; 22 | 23 | fn setup() -> RulesMessagesContractState { 24 | // setup block timestamp 25 | testing::set_block_timestamp(BLOCK_TIMESTAMP()); 26 | 27 | // setup chain id to compute vouchers hashes 28 | testing::set_chain_id(CHAIN_ID()); 29 | 30 | // setup voucher signer - 0x1 31 | let voucher_signer = setup_signer(VOUCHER_SIGNER_PUBLIC_KEY()); 32 | 33 | let mut rules_messages = RulesMessages::unsafe_new_contract_state(); 34 | 35 | rules_messages.initializer(voucher_signer_: voucher_signer.contract_address); 36 | 37 | rules_messages 38 | } 39 | 40 | fn setup_signer(public_key: felt252) -> AccountABIDispatcher { 41 | let calldata = array![public_key]; 42 | 43 | let signer_address = utils::deploy(Signer::TEST_CLASS_HASH, calldata); 44 | AccountABIDispatcher { contract_address: signer_address } 45 | } 46 | 47 | // VOUCHER 48 | 49 | #[test] 50 | #[available_gas(20000000)] 51 | fn test_consume_valid_voucher_valid() { 52 | let mut rules_messages = setup(); 53 | 54 | let voucher_signer = rules_messages.voucher_signer(); 55 | 56 | let voucher = VOUCHER_1(); 57 | let signature = VOUCHER_SIGNATURE_1(); 58 | 59 | rules_messages.consume_valid_voucher(:voucher, :signature); 60 | } 61 | 62 | #[test] 63 | #[available_gas(20000000)] 64 | #[should_panic(expected: ('Invalid voucher signature',))] 65 | fn test_consume_valid_voucher_invalid() { 66 | let mut rules_messages = setup(); 67 | 68 | let mut voucher = VOUCHER_1(); 69 | voucher.amount += 1; 70 | let signature = VOUCHER_SIGNATURE_1(); 71 | 72 | rules_messages.consume_valid_voucher(:voucher, :signature); 73 | } 74 | 75 | #[test] 76 | #[available_gas(20000000)] 77 | #[should_panic(expected: ('Voucher already consumed',))] 78 | fn test_consume_valid_voucher_already_consumed() { 79 | let mut rules_messages = setup(); 80 | 81 | let mut voucher = VOUCHER_1(); 82 | let signature = VOUCHER_SIGNATURE_1(); 83 | 84 | rules_messages.consume_valid_voucher(:voucher, :signature); 85 | rules_messages.consume_valid_voucher(:voucher, :signature); 86 | } 87 | -------------------------------------------------------------------------------- /src/access/ownable.cairo: -------------------------------------------------------------------------------- 1 | #[starknet::interface] 2 | trait IOwnable { 3 | fn owner(self: @TContractState) -> starknet::ContractAddress; 4 | 5 | fn transfer_ownership(ref self: TContractState, new_owner: starknet::ContractAddress); 6 | 7 | fn renounce_ownership(ref self: TContractState); 8 | } 9 | 10 | #[starknet::contract] 11 | mod Ownable { 12 | use zeroable::Zeroable; 13 | 14 | // locals 15 | use rules_tokens::access::ownable; 16 | 17 | // 18 | // Storage 19 | // 20 | 21 | #[storage] 22 | struct Storage { 23 | _owner: starknet::ContractAddress 24 | } 25 | 26 | // 27 | // Events 28 | // 29 | 30 | #[event] 31 | #[derive(Drop, starknet::Event)] 32 | enum Event { 33 | OwnershipTransferred: OwnershipTransferred, 34 | } 35 | 36 | #[derive(Drop, starknet::Event)] 37 | struct OwnershipTransferred { 38 | previous_owner: starknet::ContractAddress, 39 | new_owner: starknet::ContractAddress, 40 | } 41 | 42 | // 43 | // Modifiers 44 | // 45 | 46 | #[generate_trait] 47 | impl ModifierImpl of ModifierTrait { 48 | fn assert_only_owner(self: @ContractState) { 49 | let owner = self._owner.read(); 50 | let caller = starknet::get_caller_address(); 51 | assert(!caller.is_zero(), 'Caller is the zero address'); 52 | assert(caller == owner, 'Caller is not the owner'); 53 | } 54 | } 55 | 56 | // 57 | // Ownable impl 58 | // 59 | 60 | #[external(v0)] 61 | impl IOwnableImpl of ownable::IOwnable { 62 | fn owner(self: @ContractState) -> starknet::ContractAddress { 63 | self._owner.read() 64 | } 65 | 66 | fn transfer_ownership(ref self: ContractState, new_owner: starknet::ContractAddress) { 67 | assert(!new_owner.is_zero(), 'New owner is the zero address'); 68 | self.assert_only_owner(); 69 | self._transfer_ownership(new_owner); 70 | } 71 | 72 | fn renounce_ownership(ref self: ContractState) { 73 | self.assert_only_owner(); 74 | self._transfer_ownership(Zeroable::zero()); 75 | } 76 | } 77 | 78 | // 79 | // Internals 80 | // 81 | 82 | #[generate_trait] 83 | impl InternalImpl of InternalTrait { 84 | fn initializer(ref self: ContractState) { 85 | let caller = starknet::get_caller_address(); 86 | self._transfer_ownership(caller); 87 | } 88 | 89 | fn _transfer_ownership(ref self: ContractState, new_owner: starknet::ContractAddress) { 90 | let previous_owner = self._owner.read(); 91 | self._owner.write(new_owner); 92 | 93 | // Events 94 | self.emit( 95 | Event::OwnershipTransferred( 96 | OwnershipTransferred { previous_owner, new_owner } 97 | ) 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/core/messages.cairo: -------------------------------------------------------------------------------- 1 | use array::{ SpanTrait, SpanSerde }; 2 | use zeroable::Zeroable; 3 | use messages::typed_data::typed_data::Domain; 4 | 5 | // locals 6 | use super::voucher::Voucher; 7 | 8 | fn DOMAIN() -> Domain { 9 | Domain { 10 | name: 'Rules', 11 | version: '1.1', 12 | } 13 | } 14 | 15 | #[starknet::interface] 16 | trait RulesMessagesABI { 17 | fn voucher_signer(self: @TContractState) -> starknet::ContractAddress; 18 | 19 | fn consume_valid_voucher(ref self: TContractState, voucher: Voucher, signature: Span); 20 | } 21 | 22 | #[starknet::contract] 23 | mod RulesMessages { 24 | use array::{ ArrayTrait, SpanTrait, SpanSerde }; 25 | use zeroable::Zeroable; 26 | use rules_account::account; 27 | use messages::messages::Messages; 28 | use messages::messages::Messages::{ InternalTrait as MessagesInternalTrait }; 29 | use messages::typed_data::TypedDataTrait; 30 | 31 | // locals 32 | use super::DOMAIN; 33 | use rules_utils::utils::zeroable::U64Zeroable; 34 | use rules_tokens::core; 35 | use super::super::interface::{ Voucher, IRulesMessages }; 36 | 37 | // dispatchers 38 | use rules_account::account::{ AccountABIDispatcher, AccountABIDispatcherTrait }; 39 | 40 | // 41 | // Storage 42 | // 43 | 44 | #[storage] 45 | struct Storage { 46 | _voucher_signer: starknet::ContractAddress, 47 | } 48 | 49 | // 50 | // Constructor 51 | // 52 | 53 | #[constructor] 54 | fn constructor(ref self: ContractState, voucher_signer_: starknet::ContractAddress) { 55 | self.initializer(:voucher_signer_); 56 | } 57 | 58 | // 59 | // impls 60 | // 61 | 62 | #[external(v0)] 63 | impl IRulesMessagesImpl of core::interface::IRulesMessages { 64 | fn voucher_signer(self: @ContractState) -> starknet::ContractAddress { 65 | self._voucher_signer.read() 66 | } 67 | 68 | fn consume_valid_voucher(ref self: ContractState, voucher: Voucher, signature: Span) { 69 | let mut messages_self = Messages::unsafe_new_contract_state(); 70 | 71 | let voucher_signer_ = self.voucher_signer(); 72 | 73 | // compute voucher message hash 74 | let hash = voucher.compute_hash_from(from: voucher_signer_, domain: DOMAIN()); 75 | 76 | // assert voucher has not been already consumed and consume it 77 | assert(!messages_self._is_message_consumed(:hash), 'Voucher already consumed'); 78 | messages_self._consume_message(:hash); 79 | 80 | // assert voucher signature is valid 81 | assert( 82 | messages_self._is_message_signature_valid(:hash, :signature, signer: voucher_signer_), 83 | 'Invalid voucher signature' 84 | ); 85 | } 86 | } 87 | 88 | // 89 | // Internals 90 | // 91 | 92 | #[generate_trait] 93 | impl InternalImpl of InternalTrait { 94 | // Init 95 | 96 | fn initializer(ref self: ContractState, voucher_signer_: starknet::ContractAddress) { 97 | self._voucher_signer.write(voucher_signer_); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/core/interface.cairo: -------------------------------------------------------------------------------- 1 | use traits::{ Into, TryInto }; 2 | use array::{ ArrayTrait, SpanSerde }; 3 | use zeroable::Zeroable; 4 | use option::OptionTrait; 5 | 6 | use super::voucher::Voucher; 7 | 8 | // Metadata 9 | 10 | #[derive(Serde, Copy, Drop)] 11 | struct Metadata { 12 | hash: Span, 13 | } 14 | 15 | // Scarcity 16 | 17 | #[derive(Serde, Copy, Drop)] 18 | struct Scarcity { 19 | max_supply: u128, 20 | name: felt252, 21 | } 22 | 23 | // Card model 24 | 25 | #[derive(Serde, Copy, Drop)] 26 | struct CardModel { 27 | artist_name: felt252, 28 | season: felt252, 29 | scarcity_id: felt252, 30 | } 31 | 32 | // Pack 33 | 34 | #[derive(Serde, Copy, Drop)] 35 | struct Pack { 36 | name: felt252, 37 | season: felt252, 38 | } 39 | 40 | // Token id 41 | 42 | #[derive(Serde, Drop)] 43 | struct TokenId { 44 | id: u256, 45 | } 46 | 47 | // TODO: pack support 48 | #[derive(Drop)] 49 | struct PackToken { 50 | pack_id: u128, 51 | id: u256, 52 | } 53 | 54 | #[derive(Drop)] 55 | struct CardToken { 56 | card_model_id: u128, 57 | serial_number: u128, 58 | id: u256, 59 | } 60 | 61 | enum Token { 62 | card: CardToken, 63 | pack: PackToken, 64 | } 65 | 66 | // 67 | // Interfaces 68 | // 69 | 70 | #[starknet::interface] 71 | trait IRulesTokens { 72 | fn contract_uri(self: @TContractState) -> Span; 73 | 74 | fn marketplace(self: @TContractState) -> starknet::ContractAddress; 75 | 76 | fn card_exists(self: @TContractState, card_token_id: u256) -> bool; 77 | 78 | fn set_contract_uri(ref self: TContractState, contract_uri_: Span); 79 | 80 | fn set_marketplace(ref self: TContractState, marketplace_: starknet::ContractAddress); 81 | 82 | fn redeem_voucher(ref self: TContractState, voucher: Voucher, signature: Span); 83 | 84 | fn redeem_voucher_to( 85 | ref self: TContractState, 86 | to: starknet::ContractAddress, 87 | voucher: Voucher, 88 | signature: Span 89 | ); 90 | 91 | fn set_royalties_receiver(ref self: TContractState, new_receiver: starknet::ContractAddress); 92 | 93 | fn set_royalties_percentage(ref self: TContractState, new_percentage: u16); 94 | } 95 | 96 | #[starknet::interface] 97 | trait IRulesTokensCamelCase { 98 | fn contractURI(self: @TContractState) -> Span; 99 | } 100 | 101 | #[starknet::interface] 102 | trait IRulesData { 103 | fn card_model(self: @TContractState, card_model_id: u128) -> CardModel; 104 | 105 | fn pack(self: @TContractState, pack_id: u128) -> Pack; 106 | 107 | fn card_model_image_metadata(self: @TContractState, card_model_id: u128) -> Metadata; 108 | 109 | fn card_model_animation_metadata(self: @TContractState, card_model_id: u128) -> Metadata; 110 | 111 | fn pack_image_metadata(self: @TContractState, pack_id: u128) -> Metadata; 112 | 113 | fn scarcity(self: @TContractState, season: felt252, scarcity_id: felt252) -> Scarcity; 114 | 115 | fn uncommon_scarcities_count(self: @TContractState, season: felt252) -> felt252; 116 | 117 | fn add_card_model( 118 | ref self: TContractState, 119 | new_card_model: CardModel, 120 | image_metadata: Metadata, 121 | animation_metadata: Metadata 122 | ) -> u128; 123 | 124 | fn add_pack(ref self: TContractState, new_pack: Pack, image_metadata: Metadata) -> u128; 125 | 126 | fn add_scarcity(ref self: TContractState, season: felt252, scarcity: Scarcity); 127 | 128 | fn set_card_model_metadata( 129 | ref self: TContractState, 130 | card_model_id: u128, 131 | image_metadata: Metadata, 132 | animation_metadata: Metadata 133 | ); 134 | 135 | fn set_pack_metadata(ref self: TContractState, pack_id: u128, image_metadata: Metadata); 136 | } 137 | 138 | #[starknet::interface] 139 | trait IRulesMessages { 140 | fn voucher_signer(self: @TContractState) -> starknet::ContractAddress; 141 | 142 | fn consume_valid_voucher(ref self: TContractState, voucher: Voucher, signature: Span); 143 | } 144 | -------------------------------------------------------------------------------- /src/tests/constants.cairo: -------------------------------------------------------------------------------- 1 | use array::ArrayTrait; 2 | 3 | use rules_utils::utils::base64::Base64; 4 | use rules_utils::utils::array::ArrayTraitExt; 5 | 6 | // locals 7 | use rules_tokens::core::data::{ RulesData, CardModelTrait, ScarcityTrait }; 8 | use rules_tokens::core::interface::{ CardModel, Scarcity, Pack, Metadata }; 9 | use rules_tokens::core::voucher::Voucher; 10 | 11 | fn METADATA() -> Metadata { 12 | let mut hash = array![]; 13 | hash.append('hash 1'); 14 | hash.append('hash 2'); 15 | 16 | Metadata { hash: hash.span() } 17 | } 18 | 19 | fn METADATA_2() -> Metadata { 20 | let mut hash = array![]; 21 | hash.append('hash 2'); 22 | hash.append('hash 1'); 23 | 24 | Metadata { hash: hash.span() } 25 | } 26 | 27 | fn INVALID_METADATA() -> Metadata { 28 | let mut hash = array![]; 29 | hash.append(1); 30 | hash.append(2); 31 | hash.append(3); 32 | 33 | Metadata { hash: hash.span() } 34 | } 35 | 36 | fn CARD_MODEL_1() -> CardModel { 37 | CardModel { 38 | artist_name: 'ju', 39 | season: 894, 40 | scarcity_id: 0, 41 | } 42 | } 43 | 44 | // 0x1eeb9e09cde37e1ddec3ac07df646ce0 45 | fn CARD_MODEL_2() -> CardModel { 46 | CardModel { 47 | artist_name: 'Double P', 48 | season: 1995, 49 | scarcity_id: 0, 50 | } 51 | } 52 | 53 | fn CARD_MODEL_3() -> CardModel { 54 | CardModel { 55 | artist_name: 'Sully', 56 | season: 33, 57 | scarcity_id: 1, 58 | } 59 | } 60 | 61 | fn CARD_MODEL_ID() -> u128 { 62 | 0xf0579640f29841cc5a94e67ec97ed9e2 63 | } 64 | 65 | fn PACK_1() -> Pack { 66 | Pack { 67 | name: 'Pack 1', 68 | season: 1, 69 | } 70 | } 71 | 72 | fn PACK_2() -> Pack { 73 | Pack { 74 | name: 'Pack 2', 75 | season: 2, 76 | } 77 | } 78 | 79 | fn PACK_ID_1() -> u128 { 80 | 0x1 81 | } 82 | 83 | fn CARD_TOKEN_ID_2() -> u256 { 84 | u256 { low: CARD_MODEL_2().id(), high: 0x42 } 85 | } 86 | 87 | fn COMMON_SCARCITY() -> Scarcity { 88 | ScarcityTrait::common() 89 | } 90 | 91 | fn SCARCITY() -> Scarcity { 92 | Scarcity { 93 | max_supply: 1, 94 | name: 'silver', 95 | } 96 | } 97 | 98 | fn SEASON() -> felt252 { 99 | 'I\'ll be dead until this season' 100 | } 101 | 102 | fn URI() -> Array { 103 | array![111, 222, 333] 104 | } 105 | 106 | fn CONTRACT_URI() -> Array { 107 | array![111, 222, 333] 108 | } 109 | 110 | fn CHAIN_ID() -> felt252 { 111 | 'SN_MAIN' 112 | } 113 | 114 | fn BLOCK_TIMESTAMP() -> u64 { 115 | 103374042_u64 116 | } 117 | 118 | // 119 | // METADATA URI 120 | // 121 | 122 | fn CARD_MODEL_2_URI() -> Array { 123 | let mut ret = array![ 124 | '{"image":"ipfs://hash 1hash 2",', 125 | '"animation_url":"ipfs://hash 2h', 126 | 'ash 1","name":"Double P - Commo', 127 | 'n #66","attributes":[{"trait_ty', 128 | 'pe":"Serial number","value":"66', 129 | '"},{"trait_type":"Artist","valu', 130 | 'e":"Double P"},{"trait_type":"R', 131 | 'arity","value":"Common"},{"trai', 132 | 't_type":"Season","value":"1995"', 133 | '}]}', 134 | ]; 135 | 136 | ret = ret.encode(); 137 | 138 | array!['data:application/json;base64,'].concat(@ret) 139 | } 140 | 141 | fn PACK_1_URI() -> Array { 142 | let mut ret = array![ 143 | '{"image":"ipfs://hash 1hash 2",', 144 | '"name":"Pack 1","attributes":[{', 145 | '"trait_type":"Season","value":"', 146 | '1"}]}', 147 | ]; 148 | 149 | ret = ret.encode(); 150 | 151 | array!['data:application/json;base64,'].concat(@ret) 152 | } 153 | 154 | // 155 | // VOUCHERS 156 | // 157 | 158 | fn VOUCHER_1() -> Voucher { 159 | Voucher { 160 | receiver: starknet::contract_address_const::<'receiver 1'>(), 161 | token_id: u256 { low: 'token id 1 low', high: 'token id 1 high' }, 162 | amount: u256 { low: 'amount 1 low', high: 'amount 1 high' }, 163 | salt: 1, 164 | } 165 | } 166 | 167 | // valid card voucher 168 | fn VOUCHER_2() -> Voucher { 169 | Voucher { 170 | receiver: RECEIVER_DEPLOYED_ADDRESS(), 171 | token_id: CARD_TOKEN_ID_2(), 172 | amount: u256 { low: 1, high: 0 }, 173 | salt: 1, 174 | } 175 | } 176 | 177 | fn VOUCHER_SIGNER() -> starknet::ContractAddress { 178 | starknet::contract_address_const::<'voucher signer'>() 179 | } 180 | 181 | fn VOUCHER_SIGNATURE_1() -> Span { 182 | array![ 183 | 3087695227963934782411443355974054330531912780999299366340358158172188798955, 184 | 2936225994738482437582710271434813684883822280549795930447609837161446520483, 185 | ].span() 186 | } 187 | 188 | fn VOUCHER_SIGNATURE_2() -> Span { 189 | array![ 190 | 1567101499423974405132552866654397941796461247734137894210715097651800024623, 191 | 2406489013391837256524712835539748966140428060639388300020587314195643879538, 192 | ].span() 193 | } 194 | 195 | fn VOUCHER_SIGNER_PUBLIC_KEY() -> felt252 { 196 | 0x1f3c942d7f492a37608cde0d77b884a5aa9e11d2919225968557370ddb5a5aa 197 | } 198 | 199 | // ADDRESSES 200 | 201 | fn RECEIVER_DEPLOYED_ADDRESS() -> starknet::ContractAddress { 202 | starknet::contract_address_const::<0x2>() 203 | } 204 | 205 | fn OTHER_RECEIVER_DEPLOYED_ADDRESS() -> starknet::ContractAddress { 206 | starknet::contract_address_const::<0x3>() 207 | } 208 | 209 | fn ZERO() -> starknet::ContractAddress { 210 | Zeroable::zero() 211 | } 212 | 213 | fn OWNER() -> starknet::ContractAddress { 214 | starknet::contract_address_const::<10>() 215 | } 216 | 217 | fn OTHER() -> starknet::ContractAddress { 218 | starknet::contract_address_const::<20>() 219 | } 220 | 221 | fn MARKETPLACE() -> starknet::ContractAddress { 222 | starknet::contract_address_const::<'marketplace'>() 223 | } 224 | 225 | fn ROYALTIES_RECEIVER() -> starknet::ContractAddress { 226 | starknet::contract_address_const::<'royalties receiver'>() 227 | } 228 | 229 | // MISC 230 | 231 | fn ROYALTIES_PERCENTAGE() -> u16 { 232 | 500 // 5% 233 | } 234 | -------------------------------------------------------------------------------- /src/utils/storage.cairo: -------------------------------------------------------------------------------- 1 | use array::{ ArrayTrait, SpanTrait }; 2 | use traits::{ Into, TryInto }; 3 | use option::OptionTrait; 4 | use starknet::{ 5 | Store, 6 | storage_address_from_base_and_offset, 7 | storage_read_syscall, 8 | storage_write_syscall, 9 | SyscallResult, 10 | StorageBaseAddress, 11 | }; 12 | use rules_utils::utils::storage::StoreSpanFelt252; 13 | 14 | // locals 15 | use rules_tokens::core::interface::{ Scarcity, CardModel, Pack, Metadata }; 16 | 17 | // Scarcity 18 | 19 | impl StoreScarcity of Store:: { 20 | fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult:: { 21 | StoreScarcity::read_at_offset(:address_domain, :base, offset: 0) 22 | } 23 | 24 | fn write(address_domain: u32, base: StorageBaseAddress, value: Scarcity) -> SyscallResult::<()> { 25 | StoreScarcity::write_at_offset(:address_domain, :base, offset: 0, :value) 26 | } 27 | 28 | fn read_at_offset(address_domain: u32, base: StorageBaseAddress, offset: u8) -> SyscallResult:: { 29 | Result::Ok( 30 | Scarcity { 31 | max_supply: storage_read_syscall( 32 | address_domain, storage_address_from_base_and_offset(base, offset) 33 | )?.try_into().unwrap(), 34 | name: storage_read_syscall( 35 | address_domain, storage_address_from_base_and_offset(base, offset + 1) 36 | )?, 37 | } 38 | ) 39 | } 40 | 41 | fn write_at_offset( 42 | address_domain: u32, 43 | base: StorageBaseAddress, 44 | offset: u8, 45 | value: Scarcity 46 | ) -> SyscallResult::<()> { 47 | storage_write_syscall( 48 | address_domain, 49 | storage_address_from_base_and_offset(base, offset), 50 | value.max_supply.into() 51 | )?; 52 | 53 | storage_write_syscall(address_domain, storage_address_from_base_and_offset(base, offset + 1), value.name) 54 | } 55 | 56 | fn size() -> u8 { 57 | 2 58 | } 59 | } 60 | 61 | // Card Model 62 | 63 | impl StoreCardModel of Store:: { 64 | fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult:: { 65 | StoreCardModel::read_at_offset(:address_domain, :base, offset: 0) 66 | } 67 | 68 | fn write(address_domain: u32, base: StorageBaseAddress, value: CardModel) -> SyscallResult::<()> { 69 | StoreCardModel::write_at_offset(:address_domain, :base, offset: 0, :value) 70 | } 71 | 72 | fn read_at_offset(address_domain: u32, base: StorageBaseAddress, offset: u8) -> SyscallResult { 73 | Result::Ok( 74 | CardModel { 75 | artist_name: storage_read_syscall( 76 | address_domain, storage_address_from_base_and_offset(base, offset) 77 | )?, 78 | season: storage_read_syscall( 79 | address_domain, storage_address_from_base_and_offset(base, offset + 1) 80 | )?, 81 | scarcity_id: storage_read_syscall( 82 | address_domain, storage_address_from_base_and_offset(base, offset + 2) 83 | )?, 84 | } 85 | ) 86 | } 87 | 88 | fn write_at_offset( 89 | address_domain: u32, 90 | base: StorageBaseAddress, 91 | offset: u8, 92 | value: CardModel 93 | ) -> SyscallResult::<()> { 94 | storage_write_syscall(address_domain, storage_address_from_base_and_offset(base, offset), value.artist_name)?; 95 | 96 | storage_write_syscall(address_domain, storage_address_from_base_and_offset(base, offset + 1), value.season)?; 97 | 98 | storage_write_syscall(address_domain, storage_address_from_base_and_offset(base, offset + 2), value.scarcity_id) 99 | } 100 | 101 | fn size() -> u8 { 102 | 3 103 | } 104 | } 105 | 106 | // Metadata 107 | 108 | impl StoreMetadata of Store:: { 109 | fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult:: { 110 | StoreMetadata::read_at_offset(:address_domain, :base, offset: 0) 111 | } 112 | 113 | fn write(address_domain: u32, base: StorageBaseAddress, value: Metadata) -> SyscallResult::<()> { 114 | StoreMetadata::write_at_offset(:address_domain, :base, offset: 0, :value) 115 | } 116 | 117 | fn read_at_offset(address_domain: u32, base: StorageBaseAddress, offset: u8) -> SyscallResult { 118 | Result::Ok( 119 | Metadata { 120 | hash: StoreSpanFelt252::read_at_offset(:address_domain, :base, :offset)?, 121 | } 122 | ) 123 | } 124 | 125 | fn write_at_offset( 126 | address_domain: u32, 127 | base: StorageBaseAddress, 128 | offset: u8, 129 | value: Metadata 130 | ) -> SyscallResult<()> { 131 | StoreSpanFelt252::write_at_offset(:address_domain, :base, :offset, value: value.hash) 132 | } 133 | 134 | fn size() -> u8 { 135 | StoreSpanFelt252::size() 136 | } 137 | } 138 | 139 | // Pack 140 | 141 | impl StorePack of Store:: { 142 | fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult:: { 143 | StorePack::read_at_offset(:address_domain, :base, offset: 0) 144 | } 145 | 146 | fn write(address_domain: u32, base: StorageBaseAddress, value: Pack) -> SyscallResult::<()> { 147 | StorePack::write_at_offset(:address_domain, :base, offset: 0, :value) 148 | } 149 | 150 | fn read_at_offset(address_domain: u32, base: StorageBaseAddress, offset: u8) -> SyscallResult { 151 | Result::Ok( 152 | Pack { 153 | name: storage_read_syscall(address_domain, storage_address_from_base_and_offset(base, offset))?, 154 | season: storage_read_syscall(address_domain, storage_address_from_base_and_offset(base, offset + 1))?, 155 | } 156 | ) 157 | } 158 | 159 | fn write_at_offset( 160 | address_domain: u32, 161 | base: StorageBaseAddress, 162 | offset: u8, 163 | value: Pack 164 | ) -> SyscallResult::<()> { 165 | storage_write_syscall(address_domain, storage_address_from_base_and_offset(base, offset), value.name)?; 166 | 167 | storage_write_syscall(address_domain, storage_address_from_base_and_offset(base, offset + 1), value.season) 168 | } 169 | 170 | fn size() -> u8 { 171 | 2 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/core/data.cairo: -------------------------------------------------------------------------------- 1 | use zeroable::Zeroable; 2 | use traits::Into; 3 | use integer::{ U128Zeroable, U256Zeroable }; 4 | 5 | // locals 6 | use super::interface::{ Scarcity, CardModel, Pack, Metadata }; 7 | 8 | const COMMON_SCARCITY_MAX_SUPPLY: u128 = 0xffffffffffffffffffffffffffffffff; 9 | const COMMON_SCARCITY_NAME: felt252 = 'Common'; 10 | 11 | #[starknet::interface] 12 | trait RulesDataABI { 13 | fn card_model(self: @TContractState, card_model_id: u128) -> CardModel; 14 | 15 | fn scarcity(self: @TContractState, season: felt252, scarcity_id: felt252) -> Scarcity; 16 | 17 | fn uncommon_scarcities_count(self: @TContractState, season: felt252) -> felt252; 18 | 19 | fn add_card_model( 20 | ref self: TContractState, 21 | new_card_model: CardModel, 22 | animation_metadata: Metadata, 23 | image_metadata: Metadata 24 | ) -> u128; 25 | 26 | fn add_scarcity(ref self: TContractState, season: felt252, scarcity: Scarcity); 27 | } 28 | 29 | #[starknet::contract] 30 | mod RulesData { 31 | use zeroable::Zeroable; 32 | use traits::Into; 33 | 34 | use rules_utils::utils::base64::Base64; 35 | use rules_utils::utils::array::ArrayTraitExt; 36 | use rules_utils::utils::strings::Strings; 37 | 38 | // locals 39 | use rules_tokens::core::interface; 40 | use rules_tokens::core::interface::{ IRulesData }; 41 | 42 | use rules_tokens::utils::storage::{ StoreScarcity, StoreCardModel, StorePack, StoreMetadata }; 43 | use rules_tokens::utils::zeroable::{ CardModelZeroable, PackZeroable, ScarcityZeroable }; 44 | 45 | use super::{ Scarcity, ScarcityTrait, CardModel, CardModelTrait, Pack, PackTrait, Metadata, MetadataTrait }; 46 | 47 | // 48 | // Storage 49 | // 50 | 51 | #[storage] 52 | struct Storage { 53 | // season -> uncommon scarcities count 54 | _uncommon_scarcities_count: LegacyMap, 55 | // (season, scarcity_id) -> Scarcity 56 | _scarcities: LegacyMap<(felt252, felt252), Scarcity>, 57 | 58 | // card_model_id -> CardModel 59 | _card_models: LegacyMap, 60 | // card_model_id -> Image Metadata 61 | _card_models_image_metadata: LegacyMap, 62 | // card_model_id -> Animation Metadata 63 | _card_models_animation_metadata: LegacyMap, 64 | 65 | // pack_id -> Pack 66 | _packs: LegacyMap, 67 | // number of packs already created 68 | _packs_count: u128, 69 | // pack_id -> Metadata 70 | _packs_image_metadata: LegacyMap, 71 | } 72 | 73 | // 74 | // Constructor 75 | // 76 | 77 | #[constructor] 78 | fn constructor(ref self: ContractState) { } 79 | 80 | // 81 | // IRulesData impl 82 | // 83 | 84 | impl IRulesDataImpl of interface::IRulesData { 85 | fn card_model(self: @ContractState, card_model_id: u128) -> CardModel { 86 | self._card_models.read(card_model_id) 87 | } 88 | 89 | fn pack(self: @ContractState, pack_id: u128) -> Pack { 90 | self._packs.read(pack_id) 91 | } 92 | 93 | fn card_model_image_metadata(self: @ContractState, card_model_id: u128) -> Metadata { 94 | self._card_models_image_metadata.read(card_model_id) 95 | } 96 | 97 | fn card_model_animation_metadata(self: @ContractState, card_model_id: u128) -> Metadata { 98 | self._card_models_animation_metadata.read(card_model_id) 99 | } 100 | 101 | fn pack_image_metadata(self: @ContractState, pack_id: u128) -> Metadata { 102 | self._packs_image_metadata.read(pack_id) 103 | } 104 | 105 | fn scarcity(self: @ContractState, season: felt252, scarcity_id: felt252) -> Scarcity { 106 | if (scarcity_id.is_zero()) { 107 | ScarcityTrait::common() 108 | } else { 109 | self._scarcities.read((season, scarcity_id)) 110 | } 111 | } 112 | 113 | fn uncommon_scarcities_count(self: @ContractState, season: felt252) -> felt252 { 114 | self._uncommon_scarcities_count.read(season) 115 | } 116 | 117 | fn add_card_model( 118 | ref self: ContractState, 119 | new_card_model: CardModel, 120 | image_metadata: Metadata, 121 | animation_metadata: Metadata 122 | ) -> u128 { 123 | // assert card model and metadata are valid 124 | assert(new_card_model.is_valid(), 'Invalid card model'); 125 | assert(image_metadata.is_valid(), 'Invalid image metadata'); 126 | assert(animation_metadata.is_valid(), 'Invalid animation metadata'); 127 | 128 | // assert card model does not already exists 129 | let card_model_id = new_card_model.id(); 130 | assert(self.card_model(:card_model_id).is_zero(), 'Card model already exists'); 131 | 132 | // assert scarcity exists 133 | let scarcity_ = self.scarcity(season: new_card_model.season, scarcity_id: new_card_model.scarcity_id); 134 | assert(scarcity_.is_non_zero(), 'Scarcity does not exists'); 135 | 136 | // save card model and metadata 137 | self._card_models.write(card_model_id, new_card_model); 138 | self._card_models_image_metadata.write(card_model_id, image_metadata); 139 | self._card_models_animation_metadata.write(card_model_id, animation_metadata); 140 | 141 | // return card model id 142 | card_model_id 143 | } 144 | 145 | fn add_pack(ref self: ContractState, new_pack: Pack, image_metadata: Metadata) -> u128 { 146 | // assert pack name and metadata are valid 147 | assert(new_pack.is_valid(), 'Invalid pack'); 148 | assert(image_metadata.is_valid(), 'Invalid image metadata'); 149 | 150 | // get new pack id 151 | let pack_id = self._packs_count.read() + 1; 152 | 153 | // save card model and metadata 154 | self._packs.write(pack_id, new_pack); 155 | self._packs_image_metadata.write(pack_id, image_metadata); 156 | 157 | // increase pack count 158 | self._packs_count.write(pack_id); 159 | 160 | // return card model id 161 | pack_id 162 | } 163 | 164 | fn add_scarcity(ref self: ContractState, season: felt252, scarcity: Scarcity) { 165 | assert(scarcity.is_valid(), 'Invalid scarcity'); 166 | 167 | // get new scarcities count 168 | let new_uncommon_scarcities_count = self.uncommon_scarcities_count(season) + 1; 169 | 170 | self._uncommon_scarcities_count.write(season, new_uncommon_scarcities_count); 171 | self._scarcities.write((season, new_uncommon_scarcities_count), scarcity); 172 | } 173 | 174 | // Set Metadata 175 | 176 | fn set_card_model_metadata( 177 | ref self: ContractState, 178 | card_model_id: u128, 179 | image_metadata: Metadata, 180 | animation_metadata: Metadata 181 | ) { 182 | // assert card model already exists 183 | assert(self.card_model(:card_model_id).is_non_zero(), 'Card model does not exists'); 184 | 185 | // save metadata 186 | self._card_models_image_metadata.write(card_model_id, image_metadata); 187 | self._card_models_animation_metadata.write(card_model_id, animation_metadata); 188 | } 189 | 190 | fn set_pack_metadata(ref self: ContractState, pack_id: u128, image_metadata: Metadata) { 191 | // assert card model already exists 192 | assert(self.pack(:pack_id).is_non_zero(), 'Pack does not exists'); 193 | 194 | // save metadata 195 | self._packs_image_metadata.write(pack_id, image_metadata); 196 | } 197 | } 198 | 199 | // 200 | // Internals 201 | // 202 | 203 | #[generate_trait] 204 | impl InternalImpl of InternalTrait { 205 | fn _card_uri(self: @ContractState, card_model_id: u128, serial_number: u128) -> Array { 206 | let card_model = self.card_model(:card_model_id); 207 | let scarcity = self.scarcity(season: card_model.season, scarcity_id: card_model.scarcity_id); 208 | let image_metadata = self.card_model_image_metadata(:card_model_id); 209 | let animation_metadata = self.card_model_animation_metadata(:card_model_id); 210 | 211 | let mut ret = array!['{"image":"ipfs://']; 212 | 213 | // append metadata 214 | ret = ret.concat(image_metadata.hash.snapshot); 215 | ret.append('","animation_url":"ipfs://'); 216 | ret = ret.concat(animation_metadata.hash.snapshot); 217 | 218 | // append onchain data 219 | let mut arr = array![ 220 | '","name":"', 221 | card_model.artist_name, 222 | ' - ', 223 | scarcity.name, 224 | ' #', 225 | serial_number.into().itoa(), 226 | '","attributes":[{"trait_type":"', 227 | 'Serial number","value":"', 228 | serial_number.into().itoa(), 229 | '"},{"trait_type":"Artist","valu', 230 | 'e":"', 231 | card_model.artist_name, 232 | '"},{"trait_type":"Rarity","valu', 233 | 'e":"', 234 | scarcity.name, 235 | '"},{"trait_type":"Season","valu', 236 | 'e":"', 237 | card_model.season.itoa(), 238 | '"}]}', 239 | ]; 240 | 241 | ret.append_all(ref :arr); 242 | 243 | // base64 encode and return 244 | ret = ret.encode(); 245 | 246 | // add data URL prefix 247 | arr = array!['data:application/json;base64,']; 248 | arr.append_all(ref arr: ret); 249 | 250 | arr 251 | } 252 | 253 | fn _pack_uri(self: @ContractState, pack_id: u128) -> Array { 254 | let pack = self.pack(:pack_id); 255 | let image_metadata = self.pack_image_metadata(:pack_id); 256 | 257 | let mut ret = array!['{"image":"ipfs://']; 258 | 259 | // append metadata 260 | ret = ret.concat(image_metadata.hash.snapshot); 261 | 262 | // append onchain data 263 | let mut arr = array![ 264 | '","name":"', 265 | pack.name, 266 | '","attributes":[{"trait_type":"', 267 | 'Season","value":"', 268 | pack.season.itoa(), 269 | '"}]}', 270 | ]; 271 | 272 | ret.append_all(ref :arr); 273 | 274 | // base64 encode and return 275 | ret = ret.encode(); 276 | 277 | // add data URL prefix 278 | arr = array!['data:application/json;base64,']; 279 | arr.append_all(ref arr: ret); 280 | 281 | arr 282 | } 283 | } 284 | } 285 | 286 | // Metadata trait 287 | 288 | trait MetadataTrait { 289 | fn is_valid(self: Metadata) -> bool; 290 | } 291 | 292 | impl MetadataImpl of MetadataTrait { 293 | fn is_valid(self: Metadata) -> bool { 294 | self.hash.len() == 2 295 | } 296 | } 297 | 298 | // Scarcity trait 299 | 300 | trait ScarcityTrait { 301 | fn is_valid(self: Scarcity) -> bool; 302 | 303 | fn common() -> Scarcity; 304 | } 305 | 306 | impl ScarcityImpl of ScarcityTrait { 307 | fn is_valid(self: Scarcity) -> bool { 308 | if (self.name.is_zero() | self.max_supply.is_zero()) { 309 | false 310 | } else { 311 | true 312 | } 313 | } 314 | 315 | fn common() -> Scarcity { 316 | Scarcity { 317 | max_supply: COMMON_SCARCITY_MAX_SUPPLY, 318 | name: COMMON_SCARCITY_NAME, 319 | } 320 | } 321 | } 322 | 323 | // Card model trait 324 | 325 | trait CardModelTrait { 326 | fn is_valid(self: CardModel) -> bool; 327 | 328 | fn id(self: CardModel) -> u128; 329 | } 330 | 331 | impl CardModelImpl of CardModelTrait { 332 | fn is_valid(self: CardModel) -> bool { 333 | if (self.artist_name.is_zero() | self.season.is_zero()) { 334 | false 335 | } else { 336 | true 337 | } 338 | } 339 | 340 | fn id(self: CardModel) -> u128 { 341 | let mut hash = pedersen::pedersen(0, self.artist_name); 342 | hash = pedersen::pedersen(hash, self.season); 343 | hash = pedersen::pedersen(hash, self.scarcity_id); 344 | hash = pedersen::pedersen(hash, 3); 345 | 346 | Into::::into(hash).low 347 | } 348 | } 349 | 350 | // Pack trait 351 | 352 | trait PackTrait { 353 | fn is_valid(self: Pack) -> bool; 354 | } 355 | 356 | impl PackImpl of PackTrait { 357 | fn is_valid(self: Pack) -> bool { 358 | self.name.is_non_zero() & self.season.is_non_zero() 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/tests/test_rules_data.cairo: -------------------------------------------------------------------------------- 1 | use zeroable::Zeroable; 2 | 3 | // locals 4 | use rules_tokens::core::data::{ RulesData, CardModelTrait, ScarcityTrait }; 5 | use rules_tokens::core::interface::{ IRulesData, CardModel, Pack, Scarcity, Metadata }; 6 | use rules_tokens::core::data::RulesData::{ 7 | ContractState as RulesDataContractState, 8 | _packs_count::InternalContractMemberStateTrait as RulesData_packs_countInternalContractMemberStateTrait, 9 | }; 10 | 11 | use rules_tokens::utils::partial_eq::{ CardModelEq, ScarcityEq, PackEq }; 12 | use rules_tokens::utils::zeroable::{ CardModelZeroable, ScarcityZeroable, PackZeroable }; 13 | 14 | use super::utils; 15 | use super::utils::partial_eq::MetadataEq; 16 | use super::utils::zeroable::MetadataZeroable; 17 | use super::constants::{ 18 | METADATA, 19 | METADATA_2, 20 | CARD_MODEL_1, 21 | CARD_MODEL_ID, 22 | COMMON_SCARCITY, 23 | SCARCITY, 24 | SEASON, 25 | INVALID_METADATA, 26 | PACK_1, 27 | PACK_ID_1, 28 | }; 29 | 30 | // dispatchers 31 | use rules_tokens::core::data::{ RulesDataABIDispatcher, RulesDataABIDispatcherTrait }; 32 | 33 | fn setup() -> RulesDataContractState { 34 | let mut rules_data = RulesData::unsafe_new_contract_state(); 35 | 36 | rules_data.add_card_model(CARD_MODEL_1(), METADATA(), METADATA_2()); 37 | 38 | rules_data.add_pack(PACK_1(), METADATA()); 39 | 40 | rules_data 41 | } 42 | 43 | // Card model 44 | 45 | #[test] 46 | #[available_gas(20000000)] 47 | fn test_card_model() { 48 | let mut rules_data = setup(); 49 | 50 | let card_model = CARD_MODEL_1(); 51 | let card_model_id = CARD_MODEL_ID(); 52 | 53 | assert(rules_data.card_model(:card_model_id) == card_model, 'Invalid card model'); 54 | } 55 | 56 | #[test] 57 | #[available_gas(20000000)] 58 | fn test_card_model_image_metadata() { 59 | let mut rules_data = setup(); 60 | 61 | let image_metadata = METADATA(); 62 | let card_model_id = CARD_MODEL_ID(); 63 | 64 | assert(rules_data.card_model_image_metadata(:card_model_id) == image_metadata, 'Invalid metadata'); 65 | } 66 | 67 | #[test] 68 | #[available_gas(20000000)] 69 | fn test_card_model_animation_metadata() { 70 | let mut rules_data = setup(); 71 | 72 | let animation_metadata = METADATA_2(); 73 | let card_model_id = CARD_MODEL_ID(); 74 | 75 | assert(rules_data.card_model_animation_metadata(:card_model_id) == animation_metadata, 'Invalid metadata'); 76 | } 77 | 78 | #[test] 79 | #[available_gas(20000000)] 80 | fn test_add_card_model_returns_valid_id() { 81 | let mut rules_data = RulesData::unsafe_new_contract_state(); 82 | 83 | let card_model = CARD_MODEL_1(); 84 | let image_metadata = METADATA(); 85 | let animation_metadata = METADATA_2(); 86 | 87 | let card_model_id = rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 88 | 89 | assert(card_model_id == CARD_MODEL_ID(), 'Invalid card model id'); 90 | } 91 | 92 | #[test] 93 | #[available_gas(20000000)] 94 | fn test_multiple_add_card_model() { 95 | let mut rules_data = setup(); 96 | 97 | let image_metadata = METADATA(); 98 | let animation_metadata = METADATA_2(); 99 | 100 | // add card model 101 | 102 | let mut card_model = CARD_MODEL_1(); 103 | card_model.artist_name += 1; 104 | 105 | assert_state_before_add_card_model(ref :rules_data, :card_model); 106 | 107 | let card_model_id = rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 108 | 109 | assert_state_after_add_card_model(ref :rules_data, :card_model, :image_metadata, :animation_metadata); 110 | 111 | // add card model 112 | 113 | card_model = CARD_MODEL_1(); 114 | card_model.season += 1; 115 | 116 | assert_state_before_add_card_model(ref :rules_data, :card_model); 117 | 118 | let card_model_id = rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 119 | 120 | assert_state_after_add_card_model(ref :rules_data, :card_model, :image_metadata, :animation_metadata); 121 | 122 | // add card model 123 | 124 | card_model = CARD_MODEL_1(); 125 | card_model.scarcity_id += 1; 126 | rules_data.add_scarcity(season: card_model.season, scarcity: SCARCITY()); 127 | 128 | assert_state_before_add_card_model(ref :rules_data, :card_model); 129 | 130 | let card_model_id = rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 131 | 132 | assert_state_after_add_card_model(ref :rules_data, :card_model, :image_metadata, :animation_metadata); 133 | } 134 | 135 | #[test] 136 | #[available_gas(20000000)] 137 | #[should_panic(expected: ('Card model already exists',))] 138 | fn test_add_card_model_already_exists() { 139 | let mut rules_data = setup(); 140 | 141 | let card_model = CARD_MODEL_1(); 142 | let image_metadata = METADATA(); 143 | let animation_metadata = METADATA_2(); 144 | 145 | rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 146 | } 147 | 148 | #[test] 149 | #[available_gas(20000000)] 150 | #[should_panic(expected: ('Invalid card model',))] 151 | fn test_add_card_model_invalid_artist_name() { 152 | let mut rules_data = setup(); 153 | 154 | let mut card_model = CARD_MODEL_1(); 155 | let image_metadata = METADATA(); 156 | let animation_metadata = METADATA_2(); 157 | 158 | card_model.artist_name = 0; 159 | 160 | rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 161 | } 162 | 163 | #[test] 164 | #[available_gas(20000000)] 165 | #[should_panic(expected: ('Invalid card model',))] 166 | fn test_add_card_model_invalid_season() { 167 | let mut rules_data = setup(); 168 | 169 | let mut card_model = CARD_MODEL_1(); 170 | let image_metadata = METADATA(); 171 | let animation_metadata = METADATA_2(); 172 | 173 | card_model.season = 0; 174 | 175 | rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 176 | } 177 | 178 | #[test] 179 | #[available_gas(20000000)] 180 | #[should_panic(expected: ('Scarcity does not exists',))] 181 | fn test_add_card_model_invalid_scarcity() { 182 | let mut rules_data = setup(); 183 | 184 | let mut card_model = CARD_MODEL_1(); 185 | let image_metadata = METADATA(); 186 | let animation_metadata = METADATA_2(); 187 | 188 | card_model.scarcity_id += 1; 189 | 190 | rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 191 | } 192 | 193 | #[test] 194 | #[available_gas(20000000)] 195 | #[should_panic(expected: ('Invalid image metadata',))] 196 | fn test_add_card_model_invalid_image_metadata() { 197 | let mut rules_data = setup(); 198 | 199 | let card_model = CARD_MODEL_1(); 200 | let image_metadata = INVALID_METADATA(); 201 | let animation_metadata = METADATA(); 202 | 203 | rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 204 | } 205 | 206 | #[test] 207 | #[available_gas(20000000)] 208 | #[should_panic(expected: ('Invalid animation metadata',))] 209 | fn test_add_card_model_invalid_animation_metadata() { 210 | let mut rules_data = setup(); 211 | 212 | let card_model = CARD_MODEL_1(); 213 | let image_metadata = METADATA(); 214 | let animation_metadata = INVALID_METADATA(); 215 | 216 | rules_data.add_card_model(new_card_model: card_model, :image_metadata, :animation_metadata); 217 | } 218 | 219 | // Scarcity 220 | 221 | #[test] 222 | #[available_gas(20000000)] 223 | fn test_common_scarcity() { 224 | let mut rules_data = setup(); 225 | 226 | assert(rules_data.scarcity(season: 0, scarcity_id: 0) == COMMON_SCARCITY(), 'Common scarcity'); 227 | assert( 228 | rules_data.scarcity(season: 'in a long, long time', scarcity_id: 0) == COMMON_SCARCITY(), 229 | 'Invalid common scarcity' 230 | ); 231 | } 232 | 233 | #[test] 234 | #[available_gas(20000000)] 235 | fn test_uncommon_scarcity() { 236 | let mut rules_data = setup(); 237 | 238 | let season = SEASON(); 239 | rules_data.add_scarcity(:season, scarcity: SCARCITY()); 240 | 241 | assert(rules_data.scarcity(:season, scarcity_id: 1) == SCARCITY(), 'Uncommon scarcity'); 242 | } 243 | 244 | #[test] 245 | #[available_gas(20000000)] 246 | fn test_uncommon_scarcities_count() { 247 | let mut rules_data = setup(); 248 | 249 | let season = SEASON(); 250 | 251 | assert(rules_data.uncommon_scarcities_count(:season).is_zero(), 'Uncommon scarcities count'); 252 | 253 | rules_data.add_scarcity(:season, scarcity: SCARCITY()); 254 | 255 | assert(rules_data.uncommon_scarcities_count(:season) == 1, 'Uncommon scarcities count'); 256 | 257 | // these tests are so boring, let me have some fun 258 | let mut i = 0; 259 | let how_much = 0x42; 260 | loop { 261 | if (i == how_much) { 262 | break (); 263 | } 264 | 265 | rules_data.add_scarcity(:season, scarcity: SCARCITY()); 266 | i += 1; 267 | }; 268 | 269 | // anyway, no one will ever read this text :| 270 | assert(rules_data.uncommon_scarcities_count(:season) == how_much + 1, 'Uncommon scarcities count'); 271 | 272 | assert(rules_data.uncommon_scarcities_count(season: season + 1).is_zero(), 'Uncommon scarcities count'); 273 | } 274 | 275 | #[test] 276 | #[available_gas(20000000)] 277 | fn test_add_scarcity() { 278 | let mut rules_data = setup(); 279 | 280 | let mut season = SEASON(); 281 | let mut scarcity = SCARCITY(); 282 | 283 | // add scarcity 284 | 285 | assert_state_before_add_scarcity(ref :rules_data, :season); 286 | 287 | rules_data.add_scarcity(:season, :scarcity); 288 | 289 | assert_state_after_add_scarcity(ref :rules_data, :season, :scarcity); 290 | 291 | // add scarcity 292 | 293 | season += 0x42; 294 | 295 | assert_state_before_add_scarcity(ref :rules_data, :season); 296 | 297 | rules_data.add_scarcity(:season, :scarcity); 298 | 299 | assert_state_after_add_scarcity(ref :rules_data, :season, :scarcity); 300 | 301 | // add scarcity 302 | 303 | scarcity.name = 'incredibly rare'; 304 | scarcity.max_supply = 1; 305 | 306 | assert_state_before_add_scarcity(ref :rules_data, :season); 307 | 308 | rules_data.add_scarcity(:season, :scarcity); 309 | 310 | assert_state_after_add_scarcity(ref :rules_data, :season, :scarcity); 311 | } 312 | 313 | #[test] 314 | #[available_gas(20000000)] 315 | #[should_panic(expected: ('Invalid scarcity',))] 316 | fn test_add_scarcity_invalid_max_supply() { 317 | let mut rules_data = setup(); 318 | 319 | let season = SEASON(); 320 | let mut scarcity = SCARCITY(); 321 | 322 | scarcity.max_supply = 0; 323 | 324 | rules_data.add_scarcity(:season, :scarcity); 325 | } 326 | 327 | #[test] 328 | #[available_gas(20000000)] 329 | #[should_panic(expected: ('Invalid scarcity',))] 330 | fn test_add_scarcity_invalid_name() { 331 | let mut rules_data = setup(); 332 | 333 | let season = SEASON(); 334 | let mut scarcity = SCARCITY(); 335 | 336 | scarcity.name = 0; 337 | 338 | rules_data.add_scarcity(:season, :scarcity); 339 | } 340 | 341 | // Pack 342 | 343 | #[test] 344 | #[available_gas(20000000)] 345 | fn test_pack() { 346 | let mut rules_data = setup(); 347 | 348 | let pack = PACK_1(); 349 | let pack_id = PACK_ID_1(); 350 | 351 | assert(rules_data.pack(:pack_id) == pack, 'Invalid pack'); 352 | } 353 | 354 | #[test] 355 | #[available_gas(20000000)] 356 | fn test_pack_metadata() { 357 | let mut rules_data = setup(); 358 | 359 | let image_metadata = METADATA(); 360 | let pack_id = PACK_ID_1(); 361 | 362 | assert(rules_data.pack_image_metadata(:pack_id) == image_metadata, 'Invalid metadata'); 363 | } 364 | 365 | #[test] 366 | #[available_gas(20000000)] 367 | fn test_add_pack_returns_valid_id() { 368 | let mut rules_data = RulesData::unsafe_new_contract_state(); 369 | 370 | let pack = PACK_1(); 371 | let image_metadata = METADATA(); 372 | 373 | let pack_id = rules_data.add_pack(new_pack: pack, :image_metadata); 374 | 375 | assert(pack_id == PACK_ID_1(), 'Invalid pack id'); 376 | } 377 | 378 | #[test] 379 | #[available_gas(20000000)] 380 | fn test_multiple_add_pack() { 381 | let mut rules_data = setup(); 382 | 383 | let image_metadata = METADATA(); 384 | 385 | // add pack 386 | 387 | let mut pack = PACK_1(); 388 | pack.name += 1; 389 | 390 | assert_state_before_add_pack(ref :rules_data); 391 | 392 | let card_model_id = rules_data.add_pack(new_pack: pack, :image_metadata); 393 | 394 | assert_state_after_add_pack(ref :rules_data, :pack, :image_metadata); 395 | 396 | // add pack 397 | 398 | pack.season += 100; 399 | 400 | assert_state_before_add_pack(ref :rules_data); 401 | 402 | let card_model_id = rules_data.add_pack(new_pack: pack, :image_metadata); 403 | 404 | assert_state_after_add_pack(ref :rules_data, :pack, :image_metadata); 405 | 406 | // add pack 407 | 408 | pack.name += '100'; 409 | 410 | assert_state_before_add_pack(ref :rules_data); 411 | 412 | let card_model_id = rules_data.add_pack(new_pack: pack, :image_metadata); 413 | 414 | assert_state_after_add_pack(ref :rules_data, :pack, :image_metadata); 415 | } 416 | 417 | #[test] 418 | #[available_gas(20000000)] 419 | #[should_panic(expected: ('Invalid pack',))] 420 | fn test_add_pack_invalid_name() { 421 | let mut rules_data = RulesData::unsafe_new_contract_state(); 422 | 423 | let mut pack = PACK_1(); 424 | let image_metadata = METADATA(); 425 | 426 | pack.name = 0; 427 | 428 | let card_model_id = rules_data.add_pack(new_pack: pack, :image_metadata); 429 | } 430 | 431 | #[test] 432 | #[available_gas(20000000)] 433 | #[should_panic(expected: ('Invalid pack',))] 434 | fn test_add_pack_invalid_season() { 435 | let mut rules_data = RulesData::unsafe_new_contract_state(); 436 | 437 | let mut pack = PACK_1(); 438 | let image_metadata = METADATA(); 439 | 440 | pack.season = 0; 441 | 442 | let card_model_id = rules_data.add_pack(new_pack: pack, :image_metadata); 443 | } 444 | 445 | #[test] 446 | #[available_gas(20000000)] 447 | #[should_panic(expected: ('Invalid image metadata',))] 448 | fn test_add_pack_invalid_image_metadata() { 449 | let mut rules_data = setup(); 450 | 451 | let pack = PACK_1(); 452 | let image_metadata = INVALID_METADATA(); 453 | 454 | rules_data.add_pack(new_pack: pack, :image_metadata); 455 | } 456 | 457 | // Set metadata 458 | 459 | #[test] 460 | #[available_gas(20000000)] 461 | fn test_set_card_model_metadata() { 462 | let mut rules_data = setup(); 463 | 464 | let card_model = CARD_MODEL_1(); 465 | let card_model_id = card_model.id(); 466 | let image_metadata = METADATA_2(); 467 | let animation_metadata = METADATA(); 468 | 469 | rules_data.set_card_model_metadata(:card_model_id, :image_metadata, :animation_metadata); 470 | 471 | assert_state_after_add_card_model(ref :rules_data, :card_model, :image_metadata, :animation_metadata); 472 | } 473 | 474 | #[test] 475 | #[available_gas(20000000)] 476 | #[should_panic(expected: ('Card model does not exists',))] 477 | fn test_set_card_model_metadata_does_not_exists() { 478 | let mut rules_data = setup(); 479 | 480 | let card_model = CARD_MODEL_1(); 481 | let card_model_id = card_model.id() + 1; 482 | let image_metadata = METADATA_2(); 483 | let animation_metadata = METADATA(); 484 | 485 | rules_data.set_card_model_metadata(:card_model_id, :image_metadata, :animation_metadata); 486 | } 487 | 488 | #[test] 489 | #[available_gas(20000000)] 490 | fn test_set_pack_metadata() { 491 | let mut rules_data = setup(); 492 | 493 | let pack_id = PACK_ID_1(); 494 | let image_metadata = METADATA_2(); 495 | 496 | rules_data.set_pack_metadata(:pack_id, :image_metadata); 497 | } 498 | 499 | // 500 | // Utils 501 | // 502 | 503 | // Card model 504 | 505 | fn assert_state_before_add_card_model(ref rules_data: RulesDataContractState, card_model: CardModel) { 506 | let card_model_id = card_model.id(); 507 | 508 | assert(rules_data.card_model(:card_model_id) == CardModelZeroable::zero(), 'card model before'); 509 | assert(rules_data.card_model_image_metadata(:card_model_id) == MetadataZeroable::zero(), 'image metadata before'); 510 | assert( 511 | rules_data.card_model_animation_metadata(:card_model_id) == MetadataZeroable::zero(), 512 | 'animation metadata before' 513 | ); 514 | } 515 | 516 | fn assert_state_after_add_card_model( 517 | ref rules_data: RulesDataContractState, 518 | card_model: CardModel, 519 | image_metadata: Metadata, 520 | animation_metadata: Metadata 521 | ) { 522 | let card_model_id = card_model.id(); 523 | 524 | assert(rules_data.card_model(:card_model_id) == card_model, 'card model after'); 525 | assert(rules_data.card_model_image_metadata(:card_model_id) == image_metadata, 'image metadata after'); 526 | assert(rules_data.card_model_animation_metadata(:card_model_id) == animation_metadata, 'animation metadata after'); 527 | } 528 | 529 | // Scarcity 530 | 531 | fn assert_state_before_add_scarcity(ref rules_data: RulesDataContractState, season: felt252) { 532 | let scarcity_id = rules_data.uncommon_scarcities_count(:season); 533 | 534 | assert(rules_data.scarcity(:season, scarcity_id: scarcity_id + 1) == ScarcityZeroable::zero(), 'scarcity before'); 535 | } 536 | 537 | fn assert_state_after_add_scarcity(ref rules_data: RulesDataContractState, season: felt252, scarcity: Scarcity) { 538 | let scarcity_id = rules_data.uncommon_scarcities_count(:season); 539 | 540 | assert(rules_data.scarcity(:season, scarcity_id: scarcity_id) == scarcity, 'scarcity after'); 541 | } 542 | 543 | // Pack 544 | 545 | fn assert_state_before_add_pack(ref rules_data: RulesDataContractState) { 546 | let pack_id = rules_data._packs_count.read() + 1; 547 | 548 | assert(rules_data.pack(:pack_id) == PackZeroable::zero(), 'pack before'); 549 | assert(rules_data.pack_image_metadata(:pack_id) == MetadataZeroable::zero(), 'image_metadata before'); 550 | } 551 | 552 | fn assert_state_after_add_pack(ref rules_data: RulesDataContractState, pack: Pack, image_metadata: Metadata) { 553 | let pack_id = rules_data._packs_count.read(); 554 | 555 | assert(rules_data.pack(:pack_id) == pack, 'pack after'); 556 | assert(rules_data.pack_image_metadata(:pack_id) == image_metadata, 'metadata after'); 557 | } 558 | -------------------------------------------------------------------------------- /src/core/tokens.cairo: -------------------------------------------------------------------------------- 1 | use array::{ SpanTrait, SpanSerde }; 2 | use zeroable::Zeroable; 3 | 4 | // locals 5 | use rules_tokens::constants; 6 | use super::interface::{ Token, TokenId, CardToken, PackToken, CardModel, Scarcity, Metadata, Voucher }; 7 | 8 | #[starknet::interface] 9 | trait RulesTokensABI { 10 | fn voucher_signer(self: @TContractState) -> starknet::ContractAddress; 11 | 12 | fn contract_uri(self: @TContractState) -> Span; 13 | 14 | fn marketplace(self: @TContractState) -> starknet::ContractAddress; 15 | 16 | fn card_model(self: @TContractState, card_model_id: u128) -> CardModel; 17 | 18 | fn card_model_metadata(self: @TContractState, card_model_id: u128) -> Metadata; 19 | 20 | fn scarcity(self: @TContractState, season: felt252, scarcity_id: felt252) -> Scarcity; 21 | 22 | fn uncommon_scarcities_count(self: @TContractState, season: felt252) -> felt252; 23 | 24 | fn royalty_info(self: @TContractState, token_id: u256, sale_price: u256) -> (starknet::ContractAddress, u256); 25 | 26 | fn set_contract_uri(ref self: TContractState, contract_uri_: Span); 27 | 28 | fn set_royalties_receiver(ref self: TContractState, new_receiver: starknet::ContractAddress); 29 | 30 | fn set_royalties_percentage(ref self: TContractState, new_percentage: u16); 31 | 32 | fn upgrade(ref self: TContractState, new_implementation: starknet::ClassHash); 33 | 34 | fn set_marketplace(ref self: TContractState, marketplace_: starknet::ContractAddress); 35 | 36 | fn add_card_model(ref self: TContractState, new_card_model: CardModel, metadata: Metadata) -> u128; 37 | 38 | fn add_pack(ref self: TContractState, new_pack_name: felt252, metadata: Metadata) -> u128; 39 | 40 | fn add_scarcity(ref self: TContractState, season: felt252, scarcity: Scarcity); 41 | 42 | fn redeem_voucher(ref self: TContractState, voucher: Voucher, signature: Span); 43 | 44 | fn owner(self: @TContractState) -> starknet::ContractAddress; 45 | 46 | fn transfer_ownership(ref self: TContractState, new_owner: starknet::ContractAddress); 47 | 48 | fn renounce_ownership(ref self: TContractState); 49 | } 50 | 51 | #[starknet::contract] 52 | mod RulesTokens { 53 | use traits::TryInto; 54 | use array::{ ArrayTrait, SpanTrait }; 55 | use zeroable::Zeroable; 56 | use integer::U128Zeroable; 57 | 58 | use erc1155::erc1155::ERC1155; 59 | use erc1155::erc1155::ERC1155::InternalTrait as ERC1155InternalTrait; 60 | use erc1155::erc1155::interface::{ IERC1155, IERC1155Camel, IERC1155Metadata }; 61 | 62 | use rules_utils::introspection::src5::SRC5; 63 | use rules_utils::introspection::interface::{ ISRC5, ISRC5Camel }; 64 | use rules_utils::utils::storage::StoreSpanFelt252; 65 | 66 | use rules_utils::royalties::erc2981::ERC2981; 67 | use rules_utils::royalties::erc2981::ERC2981::InternalTrait as ERC2981InternalTrait; 68 | use rules_utils::royalties::interface::IERC2981; 69 | 70 | use messages::typed_data::TypedDataTrait; 71 | 72 | // locals 73 | use rules_tokens::core::interface; 74 | use rules_tokens::core::interface::{ 75 | IRulesMessages, 76 | IRulesData, 77 | IRulesTokens, 78 | IRulesTokensCamelCase, 79 | Scarcity, 80 | CardModel, 81 | Pack, 82 | Metadata, 83 | Voucher, 84 | CardToken, 85 | PackToken, 86 | TokenId, 87 | Token, 88 | }; 89 | use rules_tokens::core::data::RulesData; 90 | use rules_tokens::core::data::RulesData::{ InternalTrait as RulesDataInternalTrait }; 91 | use rules_tokens::core::messages::RulesMessages; 92 | use rules_tokens::core::messages::RulesMessages::{ InternalTrait as RulesMessagesInternalTrait }; 93 | 94 | use rules_tokens::access::ownable::{ Ownable, IOwnable }; 95 | use rules_tokens::access::ownable::Ownable::{ 96 | ModifierTrait as OwnableModifierTrait, 97 | InternalTrait as OwnableInternalTrait, 98 | }; 99 | 100 | use rules_tokens::utils::zeroable::{ CardModelZeroable, PackZeroable }; 101 | use super::TokenIdTrait; 102 | 103 | // dispatchers 104 | use rules_account::account::{ AccountABIDispatcher, AccountABIDispatcherTrait }; 105 | 106 | // 107 | // Storage 108 | // 109 | 110 | #[storage] 111 | struct Storage { 112 | // card_token_id -> minted 113 | _minted_cards: LegacyMap, 114 | 115 | // Marketplace address 116 | _marketplace: starknet::ContractAddress, 117 | 118 | // Contract uri 119 | _contract_uri: Span, 120 | } 121 | 122 | // 123 | // Events 124 | // 125 | 126 | #[event] 127 | #[derive(Drop, starknet::Event)] 128 | enum Event { 129 | TransferSingle: TransferSingle, 130 | TransferBatch: TransferBatch, 131 | ApprovalForAll: ApprovalForAll, 132 | URI: URI, 133 | } 134 | 135 | #[derive(Drop, starknet::Event)] 136 | struct TransferSingle { 137 | operator: starknet::ContractAddress, 138 | from: starknet::ContractAddress, 139 | to: starknet::ContractAddress, 140 | id: u256, 141 | value: u256, 142 | } 143 | 144 | #[derive(Drop, starknet::Event)] 145 | struct TransferBatch { 146 | operator: starknet::ContractAddress, 147 | from: starknet::ContractAddress, 148 | to: starknet::ContractAddress, 149 | ids: Span, 150 | values: Span, 151 | } 152 | 153 | #[derive(Drop, starknet::Event)] 154 | struct ApprovalForAll { 155 | account: starknet::ContractAddress, 156 | operator: starknet::ContractAddress, 157 | approved: bool, 158 | } 159 | 160 | #[derive(Drop, starknet::Event)] 161 | struct URI { 162 | value: Span, 163 | id: u256, 164 | } 165 | 166 | // 167 | // Modifiers 168 | // 169 | 170 | #[generate_trait] 171 | impl ModifierImpl of ModifierTrait { 172 | fn _only_owner(self: @ContractState) { 173 | let mut ownable_self = Ownable::unsafe_new_contract_state(); 174 | 175 | ownable_self.assert_only_owner(); 176 | } 177 | } 178 | 179 | // 180 | // Constructor 181 | // 182 | 183 | #[constructor] 184 | fn constructor( 185 | ref self: ContractState, 186 | uri_: Span, 187 | owner_: starknet::ContractAddress, 188 | voucher_signer_: starknet::ContractAddress, 189 | contract_uri_: Span, 190 | marketplace_: starknet::ContractAddress, 191 | royalties_receiver_: starknet::ContractAddress, 192 | royalties_percentage_: u16 193 | ) { 194 | self.initializer( 195 | :uri_, 196 | :owner_, 197 | :voucher_signer_, 198 | :contract_uri_, 199 | :marketplace_, 200 | :royalties_receiver_, 201 | :royalties_percentage_ 202 | ); 203 | } 204 | 205 | // 206 | // Upgrade 207 | // 208 | 209 | // TODO: use Upgradeable impl with more custom call after upgrade 210 | 211 | #[generate_trait] 212 | #[external(v0)] 213 | impl UpgradeImpl of UpgradeTrait { 214 | fn upgrade(ref self: ContractState, new_implementation: starknet::ClassHash) { 215 | // Modifiers 216 | self._only_owner(); 217 | 218 | // Body 219 | 220 | // set new impl 221 | starknet::replace_class_syscall(new_implementation); 222 | } 223 | } 224 | 225 | // 226 | // Rules Tokens impl 227 | // 228 | 229 | #[external(v0)] 230 | impl IRulesTokensImpl of interface::IRulesTokens { 231 | fn contract_uri(self: @ContractState) -> Span { 232 | self._contract_uri.read() 233 | } 234 | 235 | fn marketplace(self: @ContractState) -> starknet::ContractAddress { 236 | self._marketplace.read() 237 | } 238 | 239 | fn card_exists(self: @ContractState, card_token_id: u256) -> bool { 240 | self._minted_cards.read(card_token_id) 241 | } 242 | 243 | fn set_contract_uri(ref self: ContractState, contract_uri_: Span) { 244 | // Modifiers 245 | self._only_owner(); 246 | 247 | // Body 248 | self._contract_uri.write(contract_uri_); 249 | } 250 | 251 | fn set_marketplace(ref self: ContractState, marketplace_: starknet::ContractAddress) { 252 | // Modifiers 253 | self._only_owner(); 254 | 255 | // Body 256 | self._marketplace.write(marketplace_); 257 | } 258 | 259 | fn redeem_voucher(ref self: ContractState, voucher: Voucher, signature: Span) { 260 | let mut rules_messages_self = RulesMessages::unsafe_new_contract_state(); 261 | 262 | rules_messages_self.consume_valid_voucher(:voucher, :signature); 263 | 264 | // mint token id 265 | self._mint(to: voucher.receiver, token_id: TokenIdTrait::new(id: voucher.token_id), amount: voucher.amount); 266 | } 267 | 268 | fn redeem_voucher_to( 269 | ref self: ContractState, 270 | to: starknet::ContractAddress, 271 | voucher: Voucher, 272 | signature: Span 273 | ) { 274 | // Body 275 | let mut rules_messages_self = RulesMessages::unsafe_new_contract_state(); 276 | 277 | rules_messages_self.consume_valid_voucher(:voucher, :signature); 278 | 279 | // mint token id 280 | self._mint(:to, token_id: TokenIdTrait::new(id: voucher.token_id), amount: voucher.amount); 281 | } 282 | 283 | // ERC2981 284 | 285 | fn set_royalties_receiver(ref self: ContractState, new_receiver: starknet::ContractAddress) { 286 | // Modifiers 287 | self._only_owner(); 288 | 289 | // Body 290 | let mut erc2981_self = ERC2981::unsafe_new_contract_state(); 291 | 292 | erc2981_self._set_royalty_receiver(:new_receiver); 293 | } 294 | 295 | fn set_royalties_percentage(ref self: ContractState, new_percentage: u16) { 296 | // Modifiers 297 | self._only_owner(); 298 | 299 | // Body 300 | let mut erc2981_self = ERC2981::unsafe_new_contract_state(); 301 | 302 | erc2981_self._set_royalty_percentage(:new_percentage); 303 | } 304 | } 305 | 306 | // 307 | // Rules Messages impl 308 | // 309 | 310 | #[external(v0)] 311 | impl IRulesMessagesImpl of interface::IRulesMessages { 312 | fn voucher_signer(self: @ContractState) -> starknet::ContractAddress { 313 | let rules_messages_self = RulesMessages::unsafe_new_contract_state(); 314 | 315 | rules_messages_self.voucher_signer() 316 | } 317 | 318 | fn consume_valid_voucher(ref self: ContractState, voucher: Voucher, signature: Span) { 319 | let mut rules_messages_self = RulesMessages::unsafe_new_contract_state(); 320 | 321 | rules_messages_self.consume_valid_voucher(:voucher, :signature); 322 | } 323 | } 324 | 325 | // 326 | // IRulesData impl 327 | // 328 | 329 | #[external(v0)] 330 | impl IRulesDataImpl of interface::IRulesData { 331 | fn card_model(self: @ContractState, card_model_id: u128) -> CardModel { 332 | let rules_data_self = RulesData::unsafe_new_contract_state(); 333 | 334 | rules_data_self.card_model(:card_model_id) 335 | } 336 | 337 | fn pack(self: @ContractState, pack_id: u128) -> Pack { 338 | let rules_data_self = RulesData::unsafe_new_contract_state(); 339 | 340 | rules_data_self.pack(:pack_id) 341 | } 342 | 343 | fn card_model_image_metadata(self: @ContractState, card_model_id: u128) -> Metadata { 344 | let rules_data_self = RulesData::unsafe_new_contract_state(); 345 | 346 | rules_data_self.card_model_image_metadata(:card_model_id) 347 | } 348 | 349 | fn card_model_animation_metadata(self: @ContractState, card_model_id: u128) -> Metadata { 350 | let rules_data_self = RulesData::unsafe_new_contract_state(); 351 | 352 | rules_data_self.card_model_animation_metadata(:card_model_id) 353 | } 354 | 355 | fn pack_image_metadata(self: @ContractState, pack_id: u128) -> Metadata { 356 | let rules_data_self = RulesData::unsafe_new_contract_state(); 357 | 358 | rules_data_self.pack_image_metadata(:pack_id) 359 | } 360 | 361 | fn scarcity(self: @ContractState, season: felt252, scarcity_id: felt252) -> Scarcity { 362 | let rules_data_self = RulesData::unsafe_new_contract_state(); 363 | 364 | rules_data_self.scarcity(:season, :scarcity_id) 365 | } 366 | 367 | fn uncommon_scarcities_count(self: @ContractState, season: felt252) -> felt252 { 368 | let rules_data_self = RulesData::unsafe_new_contract_state(); 369 | 370 | rules_data_self.uncommon_scarcities_count(:season) 371 | } 372 | 373 | fn add_card_model( 374 | ref self: ContractState, 375 | new_card_model: CardModel, 376 | image_metadata: Metadata, 377 | animation_metadata: Metadata, 378 | ) -> u128 { 379 | // Modifiers 380 | self._only_owner(); 381 | 382 | // Body 383 | let mut rules_data_self = RulesData::unsafe_new_contract_state(); 384 | 385 | rules_data_self.add_card_model(:new_card_model, :image_metadata, :animation_metadata) 386 | } 387 | 388 | fn add_pack(ref self: ContractState, new_pack: Pack, image_metadata: Metadata) -> u128 { 389 | // Modifiers 390 | self._only_owner(); 391 | 392 | // Body 393 | let mut rules_data_self = RulesData::unsafe_new_contract_state(); 394 | 395 | rules_data_self.add_pack(:new_pack, :image_metadata) 396 | } 397 | 398 | fn add_scarcity(ref self: ContractState, season: felt252, scarcity: Scarcity) { 399 | // Modifiers 400 | self._only_owner(); 401 | 402 | // Body 403 | let mut rules_data_self = RulesData::unsafe_new_contract_state(); 404 | 405 | rules_data_self.add_scarcity(:season, :scarcity) 406 | } 407 | 408 | fn set_card_model_metadata( 409 | ref self: ContractState, 410 | card_model_id: u128, 411 | image_metadata: Metadata, 412 | animation_metadata: Metadata 413 | ) { 414 | // Modifiers 415 | self._only_owner(); 416 | 417 | // Body 418 | let mut rules_data_self = RulesData::unsafe_new_contract_state(); 419 | 420 | rules_data_self.set_card_model_metadata(:card_model_id, :image_metadata, :animation_metadata) 421 | } 422 | 423 | fn set_pack_metadata(ref self: ContractState, pack_id: u128, image_metadata: Metadata) { 424 | // Modifiers 425 | self._only_owner(); 426 | 427 | // Body 428 | let mut rules_data_self = RulesData::unsafe_new_contract_state(); 429 | 430 | rules_data_self.set_pack_metadata(:pack_id, :image_metadata) 431 | } 432 | } 433 | 434 | // 435 | // Rules Tokens Camel case impl 436 | // 437 | 438 | #[external(v0)] 439 | impl RulesTokensCamelCase of interface::IRulesTokensCamelCase { 440 | fn contractURI(self: @ContractState) -> Span { 441 | self.contract_uri() 442 | } 443 | } 444 | 445 | // 446 | // ERC1155 impl 447 | // 448 | 449 | #[external(v0)] 450 | impl IERC1155Impl of IERC1155 { 451 | fn balance_of(self: @ContractState, account: starknet::ContractAddress, id: u256) -> u256 { 452 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 453 | 454 | erc1155_self.balance_of(:account, :id) 455 | } 456 | 457 | fn balance_of_batch( 458 | self: @ContractState, 459 | accounts: Span, 460 | ids: Span 461 | ) -> Span { 462 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 463 | 464 | erc1155_self.balance_of_batch(:accounts, :ids) 465 | } 466 | 467 | fn is_approved_for_all(self: @ContractState, 468 | account: starknet::ContractAddress, 469 | operator: starknet::ContractAddress 470 | ) -> bool { 471 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 472 | 473 | erc1155_self.is_approved_for_all(:account, :operator) 474 | } 475 | 476 | fn set_approval_for_all(ref self: ContractState, operator: starknet::ContractAddress, approved: bool) { 477 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 478 | 479 | erc1155_self.set_approval_for_all(:operator, :approved); 480 | } 481 | 482 | fn safe_transfer_from( 483 | ref self: ContractState, 484 | from: starknet::ContractAddress, 485 | to: starknet::ContractAddress, 486 | id: u256, 487 | amount: u256, 488 | data: Span 489 | ) { 490 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 491 | 492 | let caller = starknet::get_caller_address(); 493 | let marketplace = self.marketplace(); 494 | 495 | if (caller == marketplace) { 496 | erc1155_self._safe_transfer_from(:from, :to, :id, :amount, :data); 497 | } else { 498 | erc1155_self.safe_transfer_from(:from, :to, :id, :amount, :data); 499 | } 500 | } 501 | 502 | fn safe_batch_transfer_from( 503 | ref self: ContractState, 504 | from: starknet::ContractAddress, 505 | to: starknet::ContractAddress, 506 | ids: Span, 507 | amounts: Span, 508 | data: Span 509 | ) { 510 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 511 | 512 | erc1155_self.safe_batch_transfer_from(:from, :to, :ids, :amounts, :data); 513 | } 514 | 515 | fn transfer_from( 516 | ref self: ContractState, 517 | from: starknet::ContractAddress, 518 | to: starknet::ContractAddress, 519 | id: u256, 520 | amount: u256, 521 | ) { 522 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 523 | 524 | let caller = starknet::get_caller_address(); 525 | let marketplace = self.marketplace(); 526 | 527 | if (caller == marketplace) { 528 | erc1155_self._transfer_from(:from, :to, :id, :amount); 529 | } else { 530 | erc1155_self.transfer_from(:from, :to, :id, :amount); 531 | } 532 | } 533 | 534 | fn batch_transfer_from( 535 | ref self: ContractState, 536 | from: starknet::ContractAddress, 537 | to: starknet::ContractAddress, 538 | ids: Span, 539 | amounts: Span, 540 | ) { 541 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 542 | 543 | erc1155_self.batch_transfer_from(:from, :to, :ids, :amounts); 544 | } 545 | } 546 | 547 | // 548 | // IERC1155 Camel impl 549 | // 550 | 551 | #[external(v0)] 552 | impl IERC1155CamelImpl of IERC1155Camel { 553 | fn balanceOf(self: @ContractState, account: starknet::ContractAddress, id: u256) -> u256 { 554 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 555 | 556 | erc1155_self.balanceOf(:account, :id) 557 | } 558 | 559 | fn balanceOfBatch( 560 | self: @ContractState, 561 | accounts: Span, 562 | ids: Span 563 | ) -> Span { 564 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 565 | 566 | erc1155_self.balanceOfBatch(:accounts, :ids) 567 | } 568 | 569 | fn isApprovedForAll(self: @ContractState, 570 | account: starknet::ContractAddress, 571 | operator: starknet::ContractAddress 572 | ) -> bool { 573 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 574 | 575 | erc1155_self.isApprovedForAll(:account, :operator) 576 | } 577 | 578 | fn setApprovalForAll(ref self: ContractState, operator: starknet::ContractAddress, approved: bool) { 579 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 580 | 581 | erc1155_self.setApprovalForAll(:operator, :approved); 582 | } 583 | 584 | fn safeTransferFrom( 585 | ref self: ContractState, 586 | from: starknet::ContractAddress, 587 | to: starknet::ContractAddress, 588 | id: u256, 589 | amount: u256, 590 | data: Span 591 | ) { 592 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 593 | 594 | erc1155_self.safeTransferFrom(:from, :to, :id, :amount, :data); 595 | } 596 | 597 | fn safeBatchTransferFrom( 598 | ref self: ContractState, 599 | from: starknet::ContractAddress, 600 | to: starknet::ContractAddress, 601 | ids: Span, 602 | amounts: Span, 603 | data: Span 604 | ) { 605 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 606 | 607 | erc1155_self.safeBatchTransferFrom(:from, :to, :ids, :amounts, :data); 608 | } 609 | 610 | fn transferFrom( 611 | ref self: ContractState, 612 | from: starknet::ContractAddress, 613 | to: starknet::ContractAddress, 614 | id: u256, 615 | amount: u256, 616 | ) { 617 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 618 | 619 | erc1155_self.transferFrom(:from, :to, :id, :amount); 620 | } 621 | 622 | fn batchTransferFrom( 623 | ref self: ContractState, 624 | from: starknet::ContractAddress, 625 | to: starknet::ContractAddress, 626 | ids: Span, 627 | amounts: Span, 628 | ) { 629 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 630 | 631 | erc1155_self.batchTransferFrom(:from, :to, :ids, :amounts); 632 | } 633 | } 634 | 635 | 636 | // 637 | // IERC1155 Metadata impl 638 | // 639 | 640 | #[external(v0)] 641 | impl IERC1155MetadataImpl of IERC1155Metadata { 642 | fn uri(self: @ContractState, token_id: u256) -> Span { 643 | let rules_data_self = RulesData::unsafe_new_contract_state(); 644 | 645 | match (TokenIdTrait::new(id: token_id).parse()) { 646 | Token::card(card_token) => { 647 | rules_data_self 648 | ._card_uri(card_model_id: card_token.card_model_id, serial_number: card_token.serial_number) 649 | .span() 650 | }, 651 | Token::pack(pack_token) => { 652 | rules_data_self._pack_uri(pack_id: pack_token.pack_id).span() 653 | }, 654 | } 655 | } 656 | } 657 | 658 | // 659 | // ISRC5 impl 660 | // 661 | 662 | #[external(v0)] 663 | impl ISRC5Impl of ISRC5 { 664 | fn supports_interface(self: @ContractState, interface_id: felt252) -> bool { 665 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 666 | let erc2981_self = ERC2981::unsafe_new_contract_state(); 667 | 668 | erc1155_self.supports_interface(:interface_id) | 669 | erc2981_self.supports_interface(:interface_id) 670 | } 671 | } 672 | 673 | // 674 | // ISRC5 Camel impl 675 | // 676 | 677 | #[external(v0)] 678 | impl ISRC5CamelImpl of ISRC5Camel { 679 | fn supportsInterface(self: @ContractState, interfaceId: felt252) -> bool { 680 | let erc1155_self = ERC1155::unsafe_new_contract_state(); 681 | 682 | erc1155_self.supportsInterface(:interfaceId) 683 | } 684 | } 685 | 686 | // 687 | // IERC2981 impl 688 | // 689 | 690 | #[external(v0)] 691 | impl IERC2981Impl of IERC2981 { 692 | fn royalty_info(self: @ContractState, token_id: u256, sale_price: u256) -> (starknet::ContractAddress, u256) { 693 | let erc2981_self = ERC2981::unsafe_new_contract_state(); 694 | 695 | erc2981_self.royalty_info(:token_id, :sale_price) 696 | } 697 | } 698 | 699 | // 700 | // Ownable impl 701 | // 702 | 703 | #[external(v0)] 704 | impl IOwnableImpl of IOwnable { 705 | fn owner(self: @ContractState) -> starknet::ContractAddress { 706 | let ownable_self = Ownable::unsafe_new_contract_state(); 707 | 708 | ownable_self.owner() 709 | } 710 | 711 | fn transfer_ownership(ref self: ContractState, new_owner: starknet::ContractAddress) { 712 | let mut ownable_self = Ownable::unsafe_new_contract_state(); 713 | 714 | ownable_self.transfer_ownership(:new_owner); 715 | } 716 | 717 | fn renounce_ownership(ref self: ContractState) { 718 | let mut ownable_self = Ownable::unsafe_new_contract_state(); 719 | 720 | ownable_self.renounce_ownership(); 721 | } 722 | } 723 | 724 | // 725 | // Internals 726 | // 727 | 728 | #[generate_trait] 729 | impl InternalImpl of InternalTrait { 730 | 731 | // Init 732 | 733 | fn initializer( 734 | ref self: ContractState, 735 | uri_: Span, 736 | owner_: starknet::ContractAddress, 737 | voucher_signer_: starknet::ContractAddress, 738 | contract_uri_: Span, 739 | marketplace_: starknet::ContractAddress, 740 | royalties_receiver_: starknet::ContractAddress, 741 | royalties_percentage_: u16 742 | ) { 743 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 744 | let mut rules_messages_self = RulesMessages::unsafe_new_contract_state(); 745 | let mut ownable_self = Ownable::unsafe_new_contract_state(); 746 | let mut erc2981_self = ERC2981::unsafe_new_contract_state(); 747 | 748 | erc1155_self.initializer(:uri_,); 749 | rules_messages_self.initializer(:voucher_signer_); 750 | 751 | ownable_self._transfer_ownership(new_owner: owner_); 752 | 753 | self._contract_uri.write(contract_uri_); 754 | 755 | self._marketplace.write(marketplace_); 756 | 757 | erc2981_self._set_royalty_receiver(new_receiver: royalties_receiver_); 758 | erc2981_self._set_royalty_percentage(new_percentage: royalties_percentage_); 759 | } 760 | 761 | // Mint 762 | 763 | fn _mint(ref self: ContractState, to: starknet::ContractAddress, token_id: TokenId, amount: u256) { 764 | match (token_id.parse()) { 765 | Token::card(card_token) => { 766 | self._mint_card(:to, :card_token, :amount); 767 | }, 768 | Token::pack(pack_token) => { 769 | self._mint_pack(:to, :pack_token, :amount); 770 | }, 771 | } 772 | } 773 | 774 | fn _mint_card(ref self: ContractState, to: starknet::ContractAddress, card_token: CardToken, amount: u256) { 775 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 776 | 777 | // assert amount is valid 778 | assert(amount == 1, 'Card amount cannot exceed 1'); 779 | 780 | // assert card model exists 781 | let card_model_ = self.card_model(card_model_id: card_token.card_model_id); 782 | assert(card_model_.is_non_zero(), 'Card model does not exists'); 783 | 784 | // assert serial number is in a valid range: [1, scarcity max supply] 785 | let scarcity_ = self.scarcity(season: card_model_.season, scarcity_id: card_model_.scarcity_id); 786 | assert( 787 | card_token.serial_number.is_non_zero() & (card_token.serial_number <= scarcity_.max_supply), 788 | 'Serial number is out of range' 789 | ); 790 | 791 | // assert card does not already exists 792 | assert(!self.card_exists(card_token_id: card_token.id), 'Card already minted'); 793 | 794 | // save card as minted 795 | self._minted_cards.write(card_token.id, true); 796 | 797 | // mint token 798 | erc1155_self._unsafe_mint(:to, id: card_token.id, :amount); 799 | } 800 | 801 | fn _mint_pack(ref self: ContractState, to: starknet::ContractAddress, pack_token: PackToken, amount: u256) { 802 | let mut erc1155_self = ERC1155::unsafe_new_contract_state(); 803 | 804 | // assert pack exists 805 | let pack_ = self.pack(pack_id: pack_token.id.try_into().unwrap()); 806 | assert(pack_.is_non_zero(), 'Pack does not exists'); 807 | 808 | // mint token 809 | erc1155_self._unsafe_mint(:to, id: pack_token.id, :amount); 810 | } 811 | } 812 | } 813 | 814 | #[generate_trait] 815 | impl TokenIdImpl of TokenIdTrait { 816 | fn new(id: u256) -> TokenId { 817 | TokenId { id } 818 | } 819 | 820 | fn parse(self: TokenId) -> Token { 821 | if (self.id.high.is_non_zero()) { 822 | Token::card(CardToken { 823 | serial_number: self.id.high, 824 | card_model_id: self.id.low, 825 | id: self.id, 826 | }) 827 | } else { 828 | Token::pack(PackToken { 829 | pack_id: self.id.low, 830 | id: self.id, 831 | }) 832 | } 833 | } 834 | } 835 | -------------------------------------------------------------------------------- /src/tests/test_rules_tokens.cairo: -------------------------------------------------------------------------------- 1 | use array::{ ArrayTrait, SpanTrait, SpanPartialEq }; 2 | use traits::{ Into, TryInto }; 3 | use option::OptionTrait; 4 | use starknet::testing; 5 | use zeroable::Zeroable; 6 | use starknet::class_hash::Felt252TryIntoClassHash; 7 | use integer::U256Zeroable; 8 | use debug::PrintTrait; 9 | 10 | use erc1155::erc1155::interface::{ IERC1155_ID, IERC1155, IERC1155Metadata }; 11 | 12 | use rules_utils::introspection::interface::ISRC5; 13 | use rules_utils::royalties::interface::{ IERC2981_ID, IERC2981 }; 14 | use rules_utils::utils::array::ArrayTraitExt; 15 | 16 | // locals 17 | use rules_tokens::core::RulesTokens; 18 | use rules_tokens::core::interface::{ IRulesMessages, IRulesData, IRulesTokens }; 19 | use rules_tokens::core::tokens::RulesTokens::{ ContractState as RulesTokensContractState, InternalTrait, UpgradeTrait }; 20 | 21 | use rules_tokens::core::data::CardModelTrait; 22 | use rules_tokens::core::tokens::TokenIdTrait; 23 | use rules_tokens::core::voucher::Voucher; 24 | use super::mocks::signer::Signer; 25 | use super::mocks::receiver::Receiver; 26 | use super::utils; 27 | use super::constants::{ 28 | URI, 29 | CONTRACT_URI, 30 | CHAIN_ID, 31 | VOUCHER_1, 32 | VOUCHER_2, 33 | VOUCHER_SIGNER, 34 | VOUCHER_SIGNATURE_1, 35 | VOUCHER_SIGNATURE_2, 36 | VOUCHER_SIGNER_PUBLIC_KEY, 37 | CARD_MODEL_2, 38 | PACK_1, 39 | PACK_2, 40 | PACK_ID_1, 41 | METADATA, 42 | METADATA_2, 43 | RECEIVER_DEPLOYED_ADDRESS, 44 | OTHER_RECEIVER_DEPLOYED_ADDRESS, 45 | CARD_TOKEN_ID_2, 46 | SCARCITY, 47 | CARD_MODEL_3, 48 | OWNER, 49 | OTHER, 50 | ZERO, 51 | SEASON, 52 | MARKETPLACE, 53 | ROYALTIES_RECEIVER, 54 | ROYALTIES_PERCENTAGE, 55 | CARD_MODEL_2_URI, 56 | PACK_1_URI, 57 | }; 58 | 59 | // dispatchers 60 | use rules_account::account::{ AccountABIDispatcher, AccountABIDispatcherTrait }; 61 | use rules_tokens::core::{ RulesTokensABIDispatcher, RulesTokensABIDispatcherTrait }; 62 | 63 | fn setup() -> RulesTokensContractState { 64 | // setup chain id to compute vouchers hashes 65 | testing::set_chain_id(CHAIN_ID()); 66 | 67 | // setup voucher signer - 0x1 68 | let voucher_signer = setup_voucher_signer(); 69 | 70 | let mut rules_tokens = RulesTokens::unsafe_new_contract_state(); 71 | 72 | rules_tokens.initializer( 73 | uri_: URI().span(), 74 | owner_: OWNER(), 75 | voucher_signer_: voucher_signer.contract_address, 76 | contract_uri_: CONTRACT_URI().span(), 77 | marketplace_: MARKETPLACE(), 78 | royalties_receiver_: ROYALTIES_RECEIVER(), 79 | royalties_percentage_: ROYALTIES_PERCENTAGE() 80 | ); 81 | 82 | // create some card models, packs and scarcities 83 | let card_model_2 = CARD_MODEL_2(); 84 | let card_model_3 = CARD_MODEL_3(); 85 | let pack_1 = PACK_1(); 86 | let pack_2 = PACK_2(); 87 | let image_metadata = METADATA(); 88 | let animation_metadata = METADATA_2(); 89 | let scarcity = SCARCITY(); 90 | 91 | // Create card models packs and scarcities 92 | 93 | testing::set_caller_address(OWNER()); 94 | 95 | rules_tokens.add_scarcity(season: card_model_3.season, :scarcity); 96 | 97 | rules_tokens.add_card_model(new_card_model: card_model_2, :image_metadata, :animation_metadata); 98 | rules_tokens.add_card_model(new_card_model: card_model_3, :image_metadata, :animation_metadata); 99 | 100 | rules_tokens.add_pack(new_pack: pack_1, :image_metadata); 101 | rules_tokens.add_pack(new_pack: pack_2, :image_metadata); 102 | 103 | rules_tokens 104 | } 105 | 106 | fn setup_voucher_signer() -> AccountABIDispatcher { 107 | let calldata = array![VOUCHER_SIGNER_PUBLIC_KEY()]; 108 | 109 | let signer_address = utils::deploy(Signer::TEST_CLASS_HASH, calldata); 110 | AccountABIDispatcher { contract_address: signer_address } 111 | } 112 | 113 | // Always run it after `setup()` 114 | fn setup_receiver() -> AccountABIDispatcher { 115 | let receiver_address = utils::deploy(Receiver::TEST_CLASS_HASH, array![]); 116 | 117 | assert(receiver_address == RECEIVER_DEPLOYED_ADDRESS(), 'receiver setup failed'); 118 | 119 | AccountABIDispatcher { contract_address: receiver_address } 120 | } 121 | 122 | fn setup_other_receiver() -> AccountABIDispatcher { 123 | let receiver_address = utils::deploy(Receiver::TEST_CLASS_HASH, array![]); 124 | 125 | assert(receiver_address == OTHER_RECEIVER_DEPLOYED_ADDRESS(), 'receiver setup failed'); 126 | 127 | AccountABIDispatcher { contract_address: receiver_address } 128 | } 129 | 130 | // 131 | // TESTS 132 | // 133 | 134 | #[test] 135 | #[available_gas(20000000)] 136 | fn test_supports_interface() { 137 | let mut rules_tokens = setup(); 138 | 139 | assert(rules_tokens.supports_interface(interface_id: IERC1155_ID), 'Does not support IERC1155'); 140 | assert(rules_tokens.supports_interface(interface_id: IERC2981_ID), 'Does not support IERC2981'); 141 | } 142 | 143 | #[test] 144 | #[available_gas(20000000)] 145 | #[should_panic(expected: ('Invalid voucher signature',))] 146 | fn test_redeem_voucher_invalid_signature() { 147 | let mut rules_tokens = setup(); 148 | 149 | let mut voucher = VOUCHER_1(); 150 | voucher.salt += 1; 151 | let signature = VOUCHER_SIGNATURE_1(); 152 | 153 | rules_tokens.redeem_voucher(:voucher, :signature); 154 | } 155 | 156 | #[test] 157 | #[available_gas(20000000)] 158 | #[should_panic(expected: ('Voucher already consumed',))] 159 | fn test_redeem_voucher_already_consumed() { 160 | let mut rules_tokens = setup(); 161 | let receiver = setup_receiver(); 162 | 163 | let voucher = VOUCHER_2(); 164 | let signature = VOUCHER_SIGNATURE_2(); 165 | 166 | let card_model = CARD_MODEL_2(); 167 | let metadata = METADATA(); 168 | 169 | rules_tokens.redeem_voucher(:voucher, :signature); 170 | rules_tokens.redeem_voucher(:voucher, :signature); 171 | } 172 | 173 | // Card 174 | 175 | #[test] 176 | #[available_gas(20000000)] 177 | fn test_balance_of_after_redeem_voucher() { 178 | let mut rules_tokens = setup(); 179 | let receiver = setup_receiver(); 180 | 181 | let voucher = VOUCHER_2(); 182 | let signature = VOUCHER_SIGNATURE_2(); 183 | 184 | // create conditions to successfully redeem the voucher 185 | let card_model = CARD_MODEL_2(); 186 | let metadata = METADATA(); 187 | let card_token_id = CARD_TOKEN_ID_2(); 188 | 189 | assert( 190 | rules_tokens.balance_of(account: receiver.contract_address, id: card_token_id).is_zero(), 191 | 'balance of before' 192 | ); 193 | 194 | rules_tokens.redeem_voucher(:voucher, :signature); 195 | 196 | assert( 197 | rules_tokens.balance_of(account: receiver.contract_address, id: card_token_id) == voucher.amount, 198 | 'balance of after' 199 | ); 200 | } 201 | 202 | #[test] 203 | #[available_gas(20000000)] 204 | fn test_card_exists() { 205 | let mut rules_tokens = setup(); 206 | let receiver = setup_receiver(); 207 | 208 | let card_token_id = CARD_TOKEN_ID_2(); 209 | 210 | assert(!rules_tokens.card_exists(:card_token_id), 'card exists before'); 211 | 212 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 1); 213 | 214 | assert(rules_tokens.card_exists(:card_token_id), 'card exists after'); 215 | } 216 | 217 | // Mint cards 218 | 219 | #[test] 220 | #[available_gas(20000000)] 221 | #[should_panic(expected: ('Card already minted',))] 222 | fn test__mint_card_already_minted() { 223 | let mut rules_tokens = setup(); 224 | let receiver = setup_receiver(); 225 | 226 | let card_token_id = CARD_TOKEN_ID_2(); 227 | 228 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 1); 229 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 1); 230 | } 231 | 232 | #[test] 233 | #[available_gas(20000000)] 234 | #[should_panic(expected: ('Card model does not exists',))] 235 | fn test__mint_card_unknown_card_model() { 236 | let mut rules_tokens = setup(); 237 | let receiver = setup_receiver(); 238 | 239 | let card_token_id = CARD_TOKEN_ID_2(); 240 | 241 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id + 1), amount: 1); 242 | } 243 | 244 | #[test] 245 | #[available_gas(20000000)] 246 | #[should_panic(expected: ('Serial number is out of range',))] 247 | fn test__mint_card_out_of_range_serial_number() { 248 | let mut rules_tokens = setup(); 249 | let receiver = setup_receiver(); 250 | 251 | let scarcity = SCARCITY(); 252 | let card_token_id = u256 { low: CARD_MODEL_3().id(), high: scarcity.max_supply + 1 }; 253 | 254 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 1); 255 | } 256 | 257 | #[test] 258 | #[available_gas(20000000)] 259 | fn test__mint_card_in_range_serial_number() { 260 | let mut rules_tokens = setup(); 261 | let receiver = setup_receiver(); 262 | 263 | let scarcity = SCARCITY(); 264 | let card_token_id = u256 { low: CARD_MODEL_3().id(), high: scarcity.max_supply }; 265 | 266 | assert(!rules_tokens.card_exists(:card_token_id), 'card exists before'); 267 | 268 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 1); 269 | 270 | assert(rules_tokens.card_exists(:card_token_id), 'card exists after'); 271 | } 272 | 273 | #[test] 274 | #[available_gas(20000000)] 275 | #[should_panic(expected: ('Card amount cannot exceed 1',))] 276 | fn test__mint_card_invalid_amount() { 277 | let mut rules_tokens = setup(); 278 | let receiver = setup_receiver(); 279 | 280 | let card_token_id = CARD_TOKEN_ID_2(); 281 | 282 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 2); 283 | } 284 | 285 | // Mint packs 286 | 287 | #[test] 288 | #[available_gas(20000000)] 289 | fn test__mint_pack() { 290 | let mut rules_tokens = setup(); 291 | let receiver = setup_receiver(); 292 | 293 | let pack_id = PACK_ID_1(); 294 | let pack_token_id: u256 = pack_id.into(); 295 | let amount = 2; 296 | 297 | assert( 298 | rules_tokens.balance_of(account: receiver.contract_address, id: pack_token_id).is_zero(), 299 | 'pack balance before' 300 | ); 301 | 302 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: pack_token_id), amount: amount); 303 | 304 | assert( 305 | rules_tokens.balance_of(account: receiver.contract_address, id: pack_token_id) == amount, 306 | 'pack balance after' 307 | ); 308 | } 309 | 310 | #[test] 311 | #[available_gas(20000000)] 312 | #[should_panic(expected: ('Pack does not exists',))] 313 | fn test__mint_unknown_pack() { 314 | let mut rules_tokens = setup(); 315 | let receiver = setup_receiver(); 316 | 317 | let pack_token_id: u256 = 100; 318 | 319 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: pack_token_id), amount: 1); 320 | } 321 | 322 | // Upgrade 323 | 324 | #[test] 325 | #[available_gas(20000000)] 326 | #[should_panic(expected: ('Caller is not the owner',))] 327 | fn test_upgrade_unauthorized() { 328 | let mut rules_tokens = setup(); 329 | 330 | testing::set_caller_address(OTHER()); 331 | rules_tokens.upgrade(new_implementation: 'new implementation'.try_into().unwrap()); 332 | } 333 | 334 | #[test] 335 | #[available_gas(20000000)] 336 | #[should_panic(expected: ('Caller is the zero address',))] 337 | fn test_upgrade_from_zero() { 338 | let mut rules_tokens = setup(); 339 | 340 | testing::set_caller_address(ZERO()); 341 | rules_tokens.upgrade(new_implementation: 'new implementation'.try_into().unwrap()); 342 | } 343 | 344 | // Add scarcity 345 | 346 | #[test] 347 | #[available_gas(20000000)] 348 | #[should_panic(expected: ('Caller is the zero address',))] 349 | fn test_add_scarcity_from_zero() { 350 | let mut rules_tokens = setup(); 351 | 352 | let season = SEASON(); 353 | 354 | testing::set_caller_address(ZERO()); 355 | rules_tokens.add_scarcity(:season, scarcity: SCARCITY()); 356 | } 357 | 358 | #[test] 359 | #[available_gas(20000000)] 360 | #[should_panic(expected: ('Caller is not the owner',))] 361 | fn test_add_scarcity_unauthorized() { 362 | let mut rules_tokens = setup(); 363 | 364 | let season = SEASON(); 365 | 366 | testing::set_caller_address(OTHER()); 367 | rules_tokens.add_scarcity(:season, scarcity: SCARCITY()); 368 | } 369 | 370 | // Add card model 371 | 372 | #[test] 373 | #[available_gas(20000000)] 374 | #[should_panic(expected: ('Caller is the zero address',))] 375 | fn test_add_card_model_from_zero() { 376 | let mut rules_tokens = setup(); 377 | 378 | let card_model_2 = CARD_MODEL_2(); 379 | let image_metadata = METADATA(); 380 | let animation_metadata = METADATA(); 381 | 382 | testing::set_caller_address(ZERO()); 383 | rules_tokens.add_card_model(new_card_model: card_model_2, :image_metadata, :animation_metadata); 384 | } 385 | 386 | #[test] 387 | #[available_gas(20000000)] 388 | #[should_panic(expected: ('Caller is not the owner',))] 389 | fn test_add_card_model_unauthorized() { 390 | let mut rules_tokens = setup(); 391 | 392 | let card_model_2 = CARD_MODEL_2(); 393 | let image_metadata = METADATA(); 394 | let animation_metadata = METADATA(); 395 | 396 | testing::set_caller_address(OTHER()); 397 | rules_tokens.add_card_model(new_card_model: card_model_2, :image_metadata, :animation_metadata); 398 | } 399 | 400 | // Set card model metadata 401 | 402 | #[test] 403 | #[available_gas(20000000)] 404 | #[should_panic(expected: ('Caller is the zero address',))] 405 | fn test_set_card_model_metadata_from_zero() { 406 | let mut rules_tokens = setup(); 407 | 408 | let card_model = CARD_MODEL_2(); 409 | let card_model_id = card_model.id(); 410 | let image_metadata = METADATA(); 411 | let animation_metadata = METADATA(); 412 | 413 | testing::set_caller_address(ZERO()); 414 | rules_tokens.set_card_model_metadata(:card_model_id, :image_metadata, :animation_metadata); 415 | } 416 | 417 | #[test] 418 | #[available_gas(20000000)] 419 | #[should_panic(expected: ('Caller is not the owner',))] 420 | fn test_set_card_model_metadata_unauthorized() { 421 | let mut rules_tokens = setup(); 422 | 423 | let card_model = CARD_MODEL_2(); 424 | let card_model_id = card_model.id(); 425 | let image_metadata = METADATA(); 426 | let animation_metadata = METADATA(); 427 | 428 | testing::set_caller_address(OTHER()); 429 | rules_tokens.set_card_model_metadata(:card_model_id, :image_metadata, :animation_metadata); 430 | } 431 | 432 | // Set pack metadata 433 | 434 | #[test] 435 | #[available_gas(20000000)] 436 | #[should_panic(expected: ('Caller is the zero address',))] 437 | fn test_set_pack_metadata_from_zero() { 438 | let mut rules_tokens = setup(); 439 | 440 | let pack_id = PACK_ID_1(); 441 | let image_metadata = METADATA(); 442 | 443 | testing::set_caller_address(ZERO()); 444 | rules_tokens.set_pack_metadata(:pack_id, :image_metadata); 445 | } 446 | 447 | #[test] 448 | #[available_gas(20000000)] 449 | #[should_panic(expected: ('Caller is not the owner',))] 450 | fn test_set_pack_metadata_unauthorized() { 451 | let mut rules_tokens = setup(); 452 | 453 | let pack_id = PACK_ID_1(); 454 | let image_metadata = METADATA(); 455 | 456 | testing::set_caller_address(OTHER()); 457 | rules_tokens.set_pack_metadata(:pack_id, :image_metadata); 458 | } 459 | 460 | // Marketplace 461 | 462 | #[test] 463 | #[available_gas(20000000)] 464 | fn test_marketplace() { 465 | let mut rules_tokens = setup(); 466 | 467 | let marketplace = MARKETPLACE(); 468 | 469 | assert(rules_tokens.marketplace() == marketplace, 'Invalid marketplace address'); 470 | } 471 | 472 | #[test] 473 | #[available_gas(20000000)] 474 | fn test_set_marketplace() { 475 | let mut rules_tokens = setup(); 476 | 477 | let marketplace = MARKETPLACE(); 478 | let new_marketplace = OTHER(); 479 | 480 | assert(rules_tokens.marketplace() == marketplace, 'Invalid marketplace address'); 481 | 482 | rules_tokens.set_marketplace(marketplace_: new_marketplace); 483 | 484 | assert(rules_tokens.marketplace() == new_marketplace, 'Invalid marketplace address'); 485 | } 486 | 487 | #[test] 488 | #[available_gas(20000000)] 489 | #[should_panic(expected: ('Caller is the zero address',))] 490 | fn test_set_marketplace_from_zero() { 491 | let mut rules_tokens = setup(); 492 | 493 | let marketplace = MARKETPLACE(); 494 | let new_marketplace = OTHER(); 495 | 496 | testing::set_caller_address(ZERO()); 497 | rules_tokens.set_marketplace(marketplace_: new_marketplace); 498 | } 499 | 500 | #[test] 501 | #[available_gas(20000000)] 502 | #[should_panic(expected: ('Caller is not the owner',))] 503 | fn test_set_marketplace_unauthorized() { 504 | let mut rules_tokens = setup(); 505 | 506 | let marketplace = MARKETPLACE(); 507 | let new_marketplace = OTHER(); 508 | 509 | testing::set_caller_address(OTHER()); 510 | rules_tokens.set_marketplace(marketplace_: new_marketplace); 511 | } 512 | 513 | // Reedem voucher to 514 | 515 | #[test] 516 | #[available_gas(20000000)] 517 | fn test_balance_of_after_redeem_voucher_to_() { 518 | let mut rules_tokens = setup(); 519 | setup_receiver(); 520 | 521 | let receiver = setup_other_receiver(); 522 | 523 | let voucher = VOUCHER_2(); 524 | let signature = VOUCHER_SIGNATURE_2(); 525 | 526 | // create conditions to successfully redeem the voucher 527 | let card_model = CARD_MODEL_2(); 528 | let metadata = METADATA(); 529 | let card_token_id = CARD_TOKEN_ID_2(); 530 | 531 | assert( 532 | rules_tokens.balance_of(account: receiver.contract_address, id: card_token_id).is_zero(), 533 | 'balance of before' 534 | ); 535 | 536 | testing::set_caller_address(MARKETPLACE()); 537 | rules_tokens.redeem_voucher_to(to: receiver.contract_address, :voucher, :signature); 538 | 539 | assert( 540 | rules_tokens.balance_of(account: receiver.contract_address, id: card_token_id) == voucher.amount, 541 | 'balance of after' 542 | ); 543 | } 544 | 545 | #[test] 546 | #[available_gas(20000000)] 547 | #[should_panic(expected: ('Invalid voucher signature',))] 548 | fn test_redeem_voucher_to_invalid_signature() { 549 | let mut rules_tokens = setup(); 550 | let receiver = setup_receiver(); 551 | 552 | let mut voucher = VOUCHER_2(); 553 | voucher.salt += 1; 554 | let signature = VOUCHER_SIGNATURE_2(); 555 | 556 | let card_model = CARD_MODEL_2(); 557 | let metadata = METADATA(); 558 | 559 | testing::set_caller_address(MARKETPLACE()); 560 | rules_tokens.redeem_voucher_to(to: OTHER(), :voucher, :signature); 561 | } 562 | 563 | #[test] 564 | #[available_gas(20000000)] 565 | #[should_panic(expected: ('Voucher already consumed',))] 566 | fn test_redeem_voucher_to_already_consumed() { 567 | let mut rules_tokens = setup(); 568 | setup_receiver(); 569 | 570 | let receiver = setup_other_receiver(); 571 | 572 | let voucher = VOUCHER_2(); 573 | let signature = VOUCHER_SIGNATURE_2(); 574 | 575 | let card_model = CARD_MODEL_2(); 576 | let metadata = METADATA(); 577 | 578 | testing::set_caller_address(MARKETPLACE()); 579 | rules_tokens.redeem_voucher_to(to: receiver.contract_address, :voucher, :signature); 580 | rules_tokens.redeem_voucher_to(to: receiver.contract_address, :voucher, :signature); 581 | } 582 | 583 | // ERC2981 - Royalties 584 | 585 | #[test] 586 | #[available_gas(20000000)] 587 | fn test_royalty_info_amount_without_reminder() { 588 | let mut rules_tokens = setup(); 589 | 590 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 100); 591 | assert(royalty_amount == 5, 'Invalid royalty amount'); 592 | 593 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 20); 594 | assert(royalty_amount == 1, 'Invalid royalty amount'); 595 | 596 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 0xfffffff0); 597 | assert(royalty_amount == 0xccccccc, 'Invalid royalty amount'); 598 | 599 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 0); 600 | assert(royalty_amount == 0, 'Invalid royalty amount'); 601 | } 602 | 603 | #[test] 604 | #[available_gas(20000000)] 605 | fn test_royalty_info_amount_with_reminder() { 606 | let mut rules_tokens = setup(); 607 | 608 | let royalties_receiver = ROYALTIES_RECEIVER(); 609 | 610 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 101); 611 | assert(royalty_amount == 6, 'Invalid royalty amount'); 612 | 613 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 119); 614 | assert(royalty_amount == 6, 'Invalid royalty amount'); 615 | 616 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 19); 617 | assert(royalty_amount == 1, 'Invalid royalty amount'); 618 | 619 | let (_, royalty_amount) = rules_tokens.royalty_info(token_id: 0, sale_price: 1); 620 | assert(royalty_amount == 1, 'Invalid royalty amount'); 621 | } 622 | 623 | #[test] 624 | #[available_gas(20000000)] 625 | fn test_royalty_info_receiver() { 626 | let mut rules_tokens = setup(); 627 | 628 | let royalties_receiver = ROYALTIES_RECEIVER(); 629 | 630 | let (receiver, _) = rules_tokens.royalty_info(token_id: 100, sale_price: 100); 631 | assert(receiver == royalties_receiver, 'Invalid royalty receiver'); 632 | 633 | let (receiver, _) = rules_tokens.royalty_info(token_id: 20, sale_price: 20); 634 | assert(receiver == royalties_receiver, 'Invalid royalty receiver'); 635 | 636 | let (receiver, _) = rules_tokens.royalty_info(token_id: 0x42, sale_price: 0x42); 637 | assert(receiver == royalties_receiver, 'Invalid royalty receiver'); 638 | } 639 | 640 | #[test] 641 | #[available_gas(20000000)] 642 | fn test_set_royalty_receiver() { 643 | let mut rules_tokens = setup(); 644 | 645 | let new_royalties_receiver = OTHER(); 646 | 647 | rules_tokens.set_royalties_receiver(new_receiver: new_royalties_receiver); 648 | 649 | let (receiver, _) = rules_tokens.royalty_info(token_id: 0x42, sale_price: 0x42); 650 | assert(receiver == new_royalties_receiver, 'Invalid royalty receiver'); 651 | } 652 | 653 | #[test] 654 | #[available_gas(20000000)] 655 | #[should_panic(expected: ('Caller is not the owner',))] 656 | fn test_set_royalty_receiver_unauthorized() { 657 | let mut rules_tokens = setup(); 658 | 659 | testing::set_caller_address(OTHER()); 660 | rules_tokens.set_royalties_receiver(new_receiver: OTHER()); 661 | } 662 | 663 | #[test] 664 | #[available_gas(20000000)] 665 | #[should_panic(expected: ('Caller is the zero address',))] 666 | fn test_set_royalty_receiver_from_zero() { 667 | let mut rules_tokens = setup(); 668 | 669 | testing::set_caller_address(ZERO()); 670 | rules_tokens.set_royalties_receiver(new_receiver: OTHER()); 671 | } 672 | 673 | #[test] 674 | #[available_gas(20000000)] 675 | fn test_set_royalty_percentage_50() { 676 | let mut rules_tokens = setup(); 677 | 678 | rules_tokens.set_royalties_percentage(new_percentage: 5000); // 50% 679 | 680 | let (_, royalties_amount) = rules_tokens.royalty_info(token_id: 0x42, sale_price: 0x42); 681 | assert(royalties_amount == 0x21, 'Invalid royalty amount'); 682 | } 683 | 684 | #[test] 685 | #[available_gas(20000000)] 686 | fn test_set_royalty_percentage_100() { 687 | let mut rules_tokens = setup(); 688 | 689 | rules_tokens.set_royalties_percentage(new_percentage: 10000); // 100% 690 | 691 | let (_, royalties_amount) = rules_tokens.royalty_info(token_id: 0x42, sale_price: 0x42); 692 | assert(royalties_amount == 0x42, 'Invalid royalty amount'); 693 | } 694 | 695 | #[test] 696 | #[available_gas(20000000)] 697 | fn test_set_royalty_percentage_zero() { 698 | let mut rules_tokens = setup(); 699 | 700 | rules_tokens.set_royalties_percentage(new_percentage: 0); // 0% 701 | 702 | let (_, royalties_amount) = rules_tokens.royalty_info(token_id: 0x42, sale_price: 0x42); 703 | assert(royalties_amount == 0, 'Invalid royalty amount'); 704 | } 705 | 706 | #[test] 707 | #[available_gas(20000000)] 708 | #[should_panic(expected: ('Invalid percentage',))] 709 | fn test_set_royalty_percentage_above_100() { 710 | let mut rules_tokens = setup(); 711 | 712 | rules_tokens.set_royalties_percentage(new_percentage: 10001); // 100.01% 713 | } 714 | 715 | #[test] 716 | #[available_gas(20000000)] 717 | #[should_panic(expected: ('Caller is not the owner',))] 718 | fn test_set_royalty_percentage_unauthorized() { 719 | let mut rules_tokens = setup(); 720 | 721 | testing::set_caller_address(OTHER()); 722 | rules_tokens.set_royalties_percentage(new_percentage: 1); 723 | } 724 | 725 | #[test] 726 | #[available_gas(20000000)] 727 | #[should_panic(expected: ('Caller is the zero address',))] 728 | fn test_set_royalty_percentage_from_zero() { 729 | let mut rules_tokens = setup(); 730 | 731 | testing::set_caller_address(ZERO()); 732 | rules_tokens.set_royalties_percentage(new_percentage: 1); 733 | } 734 | 735 | // Tranfer from marketplace 736 | 737 | #[test] 738 | #[available_gas(20000000)] 739 | fn test_safe_transfer_from_marketplace() { 740 | let mut rules_tokens = setup(); 741 | let receiver = setup_receiver(); 742 | let other_receiver = setup_other_receiver(); 743 | 744 | let card_token_id = CARD_TOKEN_ID_2(); 745 | 746 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 1); 747 | 748 | testing::set_caller_address(MARKETPLACE()); 749 | rules_tokens.safe_transfer_from( 750 | from: receiver.contract_address, 751 | to: other_receiver.contract_address, 752 | id: card_token_id, 753 | amount: 1, 754 | data: array![].span() 755 | ); 756 | } 757 | 758 | #[test] 759 | #[available_gas(20000000)] 760 | #[should_panic(expected: ('ERC1155: caller not allowed',))] 761 | fn test_safe_transfer_from_unauthorized() { 762 | let mut rules_tokens = setup(); 763 | let receiver = setup_receiver(); 764 | let other_receiver = setup_other_receiver(); 765 | 766 | let card_token_id = CARD_TOKEN_ID_2(); 767 | 768 | rules_tokens._mint(to: receiver.contract_address, token_id: TokenIdTrait::new(id: card_token_id), amount: 1); 769 | 770 | testing::set_caller_address(OTHER()); 771 | rules_tokens.safe_transfer_from( 772 | from: receiver.contract_address, 773 | to: other_receiver.contract_address, 774 | id: card_token_id, 775 | amount: 1, 776 | data: array![].span() 777 | ); 778 | } 779 | 780 | // Contract URI 781 | 782 | #[test] 783 | #[available_gas(20000000)] 784 | fn test_contract_uri() { 785 | let mut rules_tokens = setup(); 786 | 787 | let contract_uri = CONTRACT_URI().span(); 788 | 789 | assert(rules_tokens.contract_uri() == contract_uri, 'Invalid contract URI address'); 790 | } 791 | 792 | #[test] 793 | #[available_gas(20000000)] 794 | fn test_set_contract_uri() { 795 | let mut rules_tokens = setup(); 796 | 797 | let contract_uri = CONTRACT_URI().span(); 798 | let new_contract_uri = URI().span(); 799 | 800 | assert(rules_tokens.contract_uri() == contract_uri, 'Invalid contract URI address'); 801 | 802 | rules_tokens.set_contract_uri(contract_uri_: new_contract_uri); 803 | 804 | assert(rules_tokens.contract_uri() == new_contract_uri, 'Invalid contract URI address'); 805 | } 806 | 807 | #[test] 808 | #[available_gas(20000000)] 809 | #[should_panic(expected: ('Caller is the zero address',))] 810 | fn test_set_contract_uri_from_zero() { 811 | let mut rules_tokens = setup(); 812 | 813 | let contract_uri = CONTRACT_URI().span(); 814 | let new_contract_uri = URI().span(); 815 | 816 | testing::set_caller_address(ZERO()); 817 | rules_tokens.set_contract_uri(contract_uri_: new_contract_uri); 818 | } 819 | 820 | #[test] 821 | #[available_gas(20000000)] 822 | #[should_panic(expected: ('Caller is not the owner',))] 823 | fn test_set_contract_uri_unauthorized() { 824 | let mut rules_tokens = setup(); 825 | 826 | let contract_uri = CONTRACT_URI().span(); 827 | let new_contract_uri = URI().span(); 828 | 829 | testing::set_caller_address(OTHER()); 830 | rules_tokens.set_contract_uri(contract_uri_: new_contract_uri); 831 | } 832 | 833 | // metadata URI 834 | 835 | #[test] 836 | #[available_gas(2000000000)] 837 | fn test_card_uri() { 838 | let mut rules_tokens = setup(); 839 | 840 | let token_id = CARD_TOKEN_ID_2(); 841 | 842 | let ret = rules_tokens.uri(:token_id); 843 | 844 | assert(ret == CARD_MODEL_2_URI().span(), 'Invalid token URI'); 845 | } 846 | 847 | #[test] 848 | #[available_gas(2000000000)] 849 | fn test_pack_uri() { 850 | let mut rules_tokens = setup(); 851 | 852 | let pack_id = PACK_ID_1(); 853 | let token_id: u256 = pack_id.into(); 854 | 855 | let ret = rules_tokens.uri(:token_id); 856 | 857 | assert(ret == PACK_1_URI().span(), 'Invalid token URI'); 858 | } 859 | --------------------------------------------------------------------------------