diff --git a/.vscode/launch.json b/.vscode/launch.json index c4e9c9fd50..dde9a7d911 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,27 +1,22 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in 'dojo-world'", - "cargo": { - "args": [ - "test", - "--no-run", - "--package=dojo-world", - "--lib" - ], - "filter": { - "name": "dojo-world", - "kind": "lib" - } - }, - "args": ["migration::compile_moves"], - "cwd": "${workspaceFolder}/crates/dojo-world" - }, - ] + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in 'dojo-world'", + "cargo": { + "args": ["test", "--no-run", "--package=dojo-world", "--lib"], + "filter": { + "name": "dojo-world", + "kind": "lib" + } + }, + "args": ["migration::compile_moves"], + "cwd": "${workspaceFolder}/crates/dojo-world" + } + ] } diff --git a/crates/dojo-core/src/database.cairo b/crates/dojo-core/src/database.cairo index 8a2630d66e..37f59866ce 100644 --- a/crates/dojo-core/src/database.cairo +++ b/crates/dojo-core/src/database.cairo @@ -15,6 +15,8 @@ mod utils; #[cfg(test)] mod utils_test; +use index::WhereCondition; + fn get( class_hash: starknet::ClassHash, table: felt252, key: felt252, offset: u8, length: usize, layout: Span ) -> Span { @@ -35,30 +37,51 @@ fn set( storage::set_many(0, keys.span(), offset, value, layout); } +fn set_with_index( + class_hash: starknet::ClassHash, table: felt252, key: felt252, offset: u8, value: Span, layout: Span +) { + set(class_hash, table, key, offset, value, layout); + index::create(0, table, key); +} + fn del(class_hash: starknet::ClassHash, table: felt252, key: felt252) { index::delete(0, table, key); } -// returns a tuple of spans, first contains the entity IDs, -// second the deserialized entities themselves -fn all( - class_hash: starknet::ClassHash, model: felt252, partition: felt252, length: usize, layout: Span +// Query all entities that meet a criteria. If no index is defined, +// Returns a tuple of spans, first contains the entity IDs, +// second the deserialized entities themselves. +fn scan( + class_hash: starknet::ClassHash, model: felt252, where: Option, values_length: usize, values_layout: Span ) -> (Span, Span>) { - let table = { - if partition == 0.into() { - model - } else { + match where { + Option::Some(clause) => { let mut serialized = ArrayTrait::new(); model.serialize(ref serialized); - partition.serialize(ref serialized); - let hash = poseidon_hash_span(serialized.span()); - hash.into() + clause.key.serialize(ref serialized); + let index = poseidon_hash_span(serialized.span()); + + let all_ids = index::get_by_key(0, index, clause.value); + (all_ids.span(), get_by_ids(class_hash, index, all_ids.span(), values_length, values_layout)) + }, + + // If no `where` clause is defined, we return all values. + Option::None(_) => { + let all_ids = index::query(0, model, Option::None); + (all_ids, get_by_ids(class_hash, model, all_ids, values_length, values_layout)) } - }; + } +} - let all_ids = index::get(0, table); - let mut ids = all_ids.span(); +/// Returns entries on the given ids. +/// # Arguments +/// * `class_hash` - The class hash of the contract. +/// * `table` - The table to get the entries from. +/// * `all_ids` - The ids of the entries to get. +/// * `length` - The length of the entries. +fn get_by_ids(class_hash: starknet::ClassHash, table: felt252, all_ids: Span, length: u32, layout: Span) -> Span> { let mut entities: Array> = ArrayTrait::new(); + let mut ids = all_ids; loop { match ids.pop_front() { Option::Some(id) => { @@ -70,7 +93,7 @@ fn all( entities.append(value); }, Option::None(_) => { - break (all_ids.span(), entities.span()); + break entities.span(); } }; } diff --git a/crates/dojo-core/src/database/index.cairo b/crates/dojo-core/src/database/index.cairo index 488b1cde82..4a1a514fc4 100644 --- a/crates/dojo-core/src/database/index.cairo +++ b/crates/dojo-core/src/database/index.cairo @@ -6,6 +6,12 @@ use serde::Serde; use dojo::database::storage; +#[derive(Copy, Drop)] +struct WhereCondition { + key: felt252, + value: felt252, +} + fn create(address_domain: u32, index: felt252, id: felt252) { if exists(address_domain, index, id) { return (); @@ -18,6 +24,12 @@ fn create(address_domain: u32, index: felt252, id: felt252) { storage::set(address_domain, build_index_key(index, index_len), id); } +/// Deletes an entry from the main index, as well as from each of the keys. +/// # Arguments +/// * address_domain - The address domain to write to. +/// * index - The index to write to. +/// * id - The id of the entry. +/// # Returns fn delete(address_domain: u32, index: felt252, id: felt252) { if !exists(address_domain, index, id) { return (); @@ -42,11 +54,59 @@ fn exists(address_domain: u32, index: felt252, id: felt252) -> bool { storage::get(address_domain, build_index_item_key(index, id)) != 0 } -fn get(address_domain: u32, index: felt252) -> Array { +fn query(address_domain: u32, table: felt252, where: Option) -> Span { let mut res = ArrayTrait::new(); - let index_len_key = build_index_len_key(index); - let index_len = storage::get(address_domain, index_len_key); + match where { + Option::Some(clause) => { + let mut serialized = ArrayTrait::new(); + table.serialize(ref serialized); + clause.key.serialize(ref serialized); + let index = poseidon_hash_span(serialized.span()); + + let index_len_key = build_index_len_key(index); + let index_len = storage::get(address_domain, index_len_key); + let mut idx = 0; + + loop { + if idx == index_len { + break (); + } + let id = storage::get(address_domain, build_index_key(index, idx)); + res.append(id); + } + }, + + // If no `where` clause is defined, we return all values. + Option::None(_) => { + let index_len_key = build_index_len_key(table); + let index_len = storage::get(address_domain, index_len_key); + let mut idx = 0; + + loop { + if idx == index_len { + break (); + } + + res.append(storage::get(address_domain, build_index_key(table, idx))); + idx += 1; + }; + } + } + + res.span() +} + +/// Returns all the entries that hold a given key +/// # Arguments +/// * address_domain - The address domain to write to. +/// * index - The index to read from. +/// * key - The key return values from. +fn get_by_key(address_domain: u32, index: felt252, key: felt252) -> Array { + let mut res = ArrayTrait::new(); + let specific_len_key = build_index_specific_key_len(index, key); + let index_len = storage::get(address_domain, specific_len_key); + let mut idx = 0; loop { @@ -54,38 +114,41 @@ fn get(address_domain: u32, index: felt252) -> Array { break (); } - res.append(storage::get(address_domain, build_index_key(index, idx))); + let specific_key = build_index_specific_key(index, key, idx); + let id = storage::get(address_domain, specific_key); + res.append(id); + idx += 1; }; res } -fn index_key_prefix() -> Array { - let mut prefix = ArrayTrait::new(); - prefix.append('dojo_index'); - prefix -} - fn build_index_len_key(index: felt252) -> Span { - let mut index_len_key = index_key_prefix(); - index_len_key.append('index_lens'); - index_len_key.append(index); - index_len_key.span() + array!['dojo_index_lens', index].span() } fn build_index_key(index: felt252, idx: felt252) -> Span { - let mut key = index_key_prefix(); - key.append('indexes'); - key.append(index); - key.append(idx); - key.span() + array!['dojo_indexes', index, idx].span() } fn build_index_item_key(index: felt252, id: felt252) -> Span { - let mut index_len_key = index_key_prefix(); - index_len_key.append('index_ids'); - index_len_key.append(index); - index_len_key.append(id); - index_len_key.span() + array!['dojo_index_ids', index, id].span() +} + +/// Key for a length of index for a given key. +/// # Arguments +/// * index - The index to write to. +/// * key - The key to write. +fn build_index_specific_key_len(index: felt252, key: felt252) -> Span { + array!['dojo_index_key_len', index, key].span() } + +/// Key for an index of a given key. +/// # Arguments +/// * index - The index to write to. +/// * key - The key to write. +/// * idx - The position in the index. +fn build_index_specific_key(index: felt252, key: felt252, idx: felt252) -> Span { + array!['dojo_index_key', index, key, idx].span() +} \ No newline at end of file diff --git a/crates/dojo-core/src/database/index_test.cairo b/crates/dojo-core/src/database/index_test.cairo index e45c4353ac..2d3aa22a6b 100644 --- a/crates/dojo-core/src/database/index_test.cairo +++ b/crates/dojo-core/src/database/index_test.cairo @@ -1,25 +1,28 @@ use array::ArrayTrait; use traits::Into; +use debug::PrintTrait; +use option::OptionTrait; + use dojo::database::index; #[test] #[available_gas(2000000)] fn test_index_entity() { - let no_query = index::get(0, 69); + let no_query = index::query(0, 69, Option::None(())); assert(no_query.len() == 0, 'entity indexed'); index::create(0, 69, 420); - let query = index::get(0, 69); + let query = index::query(0, 69, Option::None(())); assert(query.len() == 1, 'entity not indexed'); assert(*query.at(0) == 420, 'entity value incorrect'); index::create(0, 69, 420); - let noop_query = index::get(0, 69); + let noop_query = index::query(0, 69, Option::None(())); assert(noop_query.len() == 1, 'index should be noop'); index::create(0, 69, 1337); - let two_query = index::get(0, 69); + let two_query = index::query(0, 69, Option::None(())); assert(two_query.len() == 2, 'index should have two query'); assert(*two_query.at(1) == 1337, 'entity value incorrect'); } @@ -28,7 +31,7 @@ fn test_index_entity() { #[available_gas(2000000)] fn test_entity_delete_basic() { index::create(0, 69, 420); - let query = index::get(0, 69); + let query = index::query(0, 69, Option::None(())); assert(query.len() == 1, 'entity not indexed'); assert(*query.at(0) == 420, 'entity value incorrect'); @@ -37,7 +40,7 @@ fn test_entity_delete_basic() { index::delete(0, 69, 420); assert(!index::exists(0, 69, 420), 'entity should not exist'); - let no_query = index::get(0, 69); + let no_query = index::query(0, 69, Option::None(())); assert(no_query.len() == 0, 'index should have no query'); } @@ -48,10 +51,10 @@ fn test_entity_query_delete_shuffle() { index::create(0, table, 10); index::create(0, table, 20); index::create(0, table, 30); - assert(index::get(0, table).len() == 3, 'wrong size'); + assert(index::query(0, table, Option::None(())).len() == 3, 'wrong size'); index::delete(0, table, 10); - let entities = index::get(0, table); + let entities = index::query(0, table, Option::None(())); assert(entities.len() == 2, 'wrong size'); assert(*entities.at(0) == 30, 'idx 0 not 30'); assert(*entities.at(1) == 20, 'idx 1 not 20'); @@ -60,6 +63,6 @@ fn test_entity_query_delete_shuffle() { #[test] #[available_gas(20000000)] fn test_entity_query_delete_non_existing() { - assert(index::get(0, 69).len() == 0, 'table len != 0'); + assert(index::query(0, 69, Option::None(())).len() == 0, 'table len != 0'); index::delete(0, 69, 999); // deleting non-existing should not panic } diff --git a/crates/dojo-core/src/database_test.cairo b/crates/dojo-core/src/database_test.cairo index e329ef0f45..d4b8c8c710 100644 --- a/crates/dojo-core/src/database_test.cairo +++ b/crates/dojo-core/src/database_test.cairo @@ -9,7 +9,8 @@ use starknet::syscalls::deploy_syscall; use starknet::class_hash::{Felt252TryIntoClassHash, ClassHash}; use dojo::world::{IWorldDispatcher}; use dojo::executor::executor; -use dojo::database::{get, set, del, all}; +use dojo::database::{get, set, set_with_index, del, scan}; +use dojo::database::index::WhereCondition; #[test] #[available_gas(1000000)] @@ -118,19 +119,19 @@ fn test_database_del() { #[test] #[available_gas(10000000)] -fn test_database_all() { - let mut even = ArrayTrait::new(); - even.append(0x2); - even.append(0x4); - - let mut odd = ArrayTrait::new(); - odd.append(0x1); - odd.append(0x3); +fn test_database_scan() { + let even = array![2, 4].span(); + let odd = array![1, 3].span(); + let layout = array![251, 251].span(); let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); - set(class_hash, 'table', 'even', 0, even.span(), array![251, 251].span()); - set(class_hash, 'table', 'odd', 0, odd.span(), array![251, 251].span()); - - let base = starknet::storage_base_address_from_felt252('table'); - let (keys, values) = all(class_hash, 'table', 0, 2, array![251, 251].span()); + set_with_index(class_hash, 'table', 'even', 0, even, layout); + set_with_index(class_hash, 'table', 'odd', 0, odd, layout); + + let (keys, values) = scan(class_hash, 'table', Option::None(()), 2, layout); + assert(keys.len() == 2, 'Wrong number of keys!'); + assert(values.len() == 2, 'Wrong number of values!'); + assert(*keys.at(0) == 'even', 'Wrong key at index 0!'); + assert(*(*values.at(0)).at(0) == 2, 'Wrong value at index 0!'); + assert(*(*values.at(0)).at(1) == 4, 'Wrong value at index 1!'); } diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index f95b70ba99..fb792c34e3 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -25,7 +25,7 @@ trait IWorld { layout: Span ); fn entities( - self: @T, model: felt252, index: felt252, length: usize, layout: Span + self: @T, model: felt252, index: Option, values: Span, values_length: usize, values_layout: Span ) -> (Span, Span>); fn set_executor(ref self: T, contract_address: ContractAddress); fn executor(self: @T) -> ContractAddress; @@ -53,9 +53,11 @@ mod world { }; use dojo::database; + use dojo::database::index::WhereCondition; use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; use dojo::world::{IWorldDispatcher, IWorld}; + const NAME_ENTRYPOINT: felt252 = 0x0361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60; @@ -314,7 +316,7 @@ mod world { let key = poseidon::poseidon_hash_span(keys); let model_class_hash = self.models.read(model); - database::set(model_class_hash, model, key, offset, values, layout); + database::set_with_index(model_class_hash, model, key, offset, values, layout); EventEmitter::emit(ref self, StoreSetRecord { table: model, keys, offset, values }); } @@ -369,20 +371,20 @@ mod world { /// /// * `model` - The name of the model to be retrieved. /// * `index` - The index to be retrieved. + /// * `values` - The query to be used to find the entity. + /// * `length` - The length of the model values. /// /// # Returns /// /// * `Span` - The entity IDs. /// * `Span>` - The entities. fn entities( - self: @ContractState, - model: felt252, - index: felt252, - length: usize, - layout: Span + self: @ContractState, model: felt252, index: Option, values: Span, values_length: usize, values_layout: Span ) -> (Span, Span>) { let class_hash = self.models.read(model); - database::all(class_hash, model.into(), index, length, layout) + + assert(values.len() == 0, 'Queries by values not impl'); + database::scan(class_hash, model, Option::None(()), values_length, values_layout) } /// Sets the executor contract address. diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index 5eac4ebb1f..15271cc838 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -170,6 +170,38 @@ fn deploy_world() -> IWorldDispatcher { spawn_test_world(array![]) } +#[test] +#[available_gas(60000000)] +fn test_entities() { + // Deploy world contract + let world = spawn_test_world(array![foo::TEST_CLASS_HASH],); + + let bar_contract = IbarDispatcher { + contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) + }; + + let alice = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_contract_address(alice); + bar_contract.set_foo(1337, 1337); + + let mut keys = ArrayTrait::new(); + keys.append(0); + + let mut query_keys = ArrayTrait::new().span(); + let layout = array![251].span(); + let (keys, values) = world.entities('Foo', Option::None(()), query_keys, 2, layout); + assert(keys.len() == 1, 'No keys found for any!'); + + // query_keys.append(0x1337); + // let (keys, values) = world.entities('Foo', 42, query_keys.span(), 2, layout); + // assert(keys.len() == 1, 'No keys found!'); + + // let mut query_keys = ArrayTrait::new(); + // query_keys.append(0x1338); + // let (keys, values) = world.entities('Foo', 42, query_keys.span(), 2, layout); + // assert(keys.len() == 0, 'Keys found!'); +} + #[test] #[available_gas(6000000)] fn test_owner() { @@ -335,3 +367,5 @@ fn test_execute_multiple_worlds() { assert(data1.a == 1337, 'data1 not stored'); assert(data2.a == 7331, 'data2 not stored'); } + + diff --git a/crates/dojo-erc/src/tests/test_erc1155.cairo b/crates/dojo-erc/src/tests/test_erc1155.cairo new file mode 100644 index 0000000000..5ce73b8230 --- /dev/null +++ b/crates/dojo-erc/src/tests/test_erc1155.cairo @@ -0,0 +1,629 @@ +use zeroable::Zeroable; +use traits::{Into, Default, IndexView}; +use option::OptionTrait; +use array::ArrayTrait; +use serde::Serde; +use starknet::ContractAddress; +use starknet::testing::set_contract_address; + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::tests::test_utils::impersonate; +use dojo_erc::tests::test_erc1155_utils::{ + spawn_world, deploy_erc1155, deploy_default, deploy_testcase1, ZERO, USER1, USER2, DEPLOYER, + PROXY +}; + +use dojo_erc::erc165::interface::IERC165_ID; +use dojo_erc::erc1155::interface::{ + IERC1155A, IERC1155ADispatcher, IERC1155ADispatcherTrait, IERC1155_ID, IERC1155_METADATA_ID, + IERC1155_RECEIVER_ID +}; + +use dojo_erc::erc1155::erc1155::ERC1155::{Event, TransferSingle, TransferBatch, ApprovalForAll}; + + +#[test] +#[available_gas(30000000)] +fn test_deploy() { + let world = spawn_world(DEPLOYER()); + let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); + let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; + assert(erc1155.owner() == DEPLOYER(), 'invalid owner'); +} + +#[test] +#[available_gas(30000000)] +fn test_deploy_default() { + let (world, erc1155) = deploy_default(); + assert(erc1155.owner() == DEPLOYER(), 'invalid owner'); +} + + +// +// supports_interface +// + +#[test] +#[available_gas(30000000)] +fn test_should_support_interfaces() { + let (world, erc1155) = deploy_default(); + + assert(erc1155.supports_interface(IERC165_ID) == true, 'should support erc165'); + assert(erc1155.supports_interface(IERC1155_ID) == true, 'should support erc1155'); + assert( + erc1155.supports_interface(IERC1155_METADATA_ID) == true, 'should support erc1155_metadata' + ); +} + +// +// uri +// + +#[test] +#[available_gas(30000000)] +fn test_uri() { + let (world, erc1155) = deploy_default(); + assert(erc1155.uri(64) == 'uri', 'invalid uri'); +} + + +// +// behaves like an ERC1155 +// + +// +// balance_of +// +#[test] +#[available_gas(30000000)] +#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] +fn test_balance_of_zero_address() { + //reverts when queried about the zero address + + let (world, erc1155) = deploy_default(); + erc1155.balance_of(ZERO(), 0); // should panic +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_empty_balance() { + // when accounts don't own tokens + // returns zero for given addresses + let (world, erc1155) = deploy_default(); + assert(erc1155.balance_of(USER1(), 0) == 0, 'should be 0'); + assert(erc1155.balance_of(USER1(), 69) == 0, 'should be 0'); + assert(erc1155.balance_of(USER2(), 0) == 0, 'should be 0'); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_with_tokens() { + // when accounts own some tokens + // returns the amount of tokens owned by the given addresses + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 0, 1, array![]); + erc1155.mint(USER1(), 69, 42, array![]); + erc1155.mint(USER2(), 69, 5, array![]); + + assert(erc1155.balance_of(USER1(), 0) == 1, 'should be 1'); + assert(erc1155.balance_of(USER1(), 69) == 42, 'should be 42'); + assert(erc1155.balance_of(USER2(), 69) == 5, 'should be 5'); +} + +// +// balance_of_batch +// + +#[test] +#[available_gas(30000000)] +#[should_panic(expected: ('ERC1155: invalid length', 'ENTRYPOINT_FAILED',))] +fn test_balance_of_batch_with_invalid_input() { + // reverts when input arrays don't match up + let (world, erc1155) = deploy_default(); + erc1155.balance_of_batch(array![USER1(), USER2()], array![0]); + erc1155.balance_of_batch(array![USER1()], array![0, 1, 2]); +} + +#[test] +#[available_gas(30000000)] +#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] +fn test_balance_of_batch_address_zero() { + // reverts when input arrays don't match up + let (world, erc1155) = deploy_default(); + erc1155.balance_of_batch(array![USER1(), ZERO()], array![0, 1]); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_batch_empty_account() { + // when accounts don't own tokens + // returns zeros for each account + let (world, erc1155) = deploy_default(); + let balances = erc1155.balance_of_batch(array![USER1(), USER1(), USER1()], array![0, 1, 5]); + let bals = @balances; + assert(balances.len() == 3, 'should be 3'); + assert(bals[0] == @0_u256, 'should be 0'); + assert(bals[1] == @0_u256, 'should be 0'); + assert(bals[2] == @0_u256, 'should be 0'); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_batch_with_tokens() { + // when accounts own some tokens + // returns amounts owned by each account in order passed + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 0, 1, array![]); + erc1155.mint(USER1(), 69, 42, array![]); + erc1155.mint(USER2(), 69, 2, array![]); + + let balances = erc1155.balance_of_batch(array![USER1(), USER1(), USER2()], array![0, 69, 69]); + let bals = @balances; + assert(balances.len() == 3, 'should be 3'); + assert(bals[0] == @1_u256, 'should be 1'); + assert(bals[1] == @42_u256, 'should be 42'); + assert(bals[2] == @2_u256, 'should be 2'); +} + +#[test] +#[available_gas(30000000)] +fn test_balance_of_batch_with_tokens_2() { + // when accounts own some tokens + // returns multiple times the balance of the same address when asked + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 0, 1, array![]); + erc1155.mint(USER2(), 69, 2, array![]); + + let balances = erc1155.balance_of_batch(array![USER1(), USER2(), USER1()], array![0, 69, 0]); + let bals = @balances; + assert(balances.len() == 3, 'should be 3'); + assert(bals[0] == @1_u256, 'should be 1'); + assert(bals[1] == @2_u256, 'should be 2'); + assert(bals[2] == @1_u256, 'should be 1'); +} + + +// +// balance_of_batch +// + +#[test] +#[available_gas(30000000)] +fn test_set_approval_for_all() { + // sets approval status which can be queried via is_approved_for_all + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(PROXY(), true); + assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); +} + +#[test] +#[available_gas(30000000)] +fn test_set_approval_for_all_emit_event() { + // set_approval_for_all emits ApprovalForAll event + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(PROXY(), true); + + // ApprovalForAll + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER1(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + + +#[test] +#[available_gas(30000000)] +fn test_set_unset_approval_for_all() { + // sets approval status which can be queried via is_approved_for_all + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(PROXY(), true); + assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); + erc1155.set_approval_for_all(PROXY(), false); + assert(erc1155.is_approved_for_all(USER1(), PROXY()) == false, 'should be false'); +} + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_set_approval_for_all_on_self() { + // reverts if attempting to approve self as an operator + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(USER1(), true); // should panic +} + +// +// safe_transfer_from +// + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_safe_transfer_from_more_than_balance() { + // reverts when transferring more than balance + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_transfer_from(USER1(), USER2(), 1, 999, array![]); // should panic +} + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_safe_transfer_to_zero() { + // reverts when transferring to zero address + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_transfer_from(USER1(), ZERO(), 1, 1, array![]); // should panic +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_debit_sender() { + // debits transferred balance from sender + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before = erc1155.balance_of(USER1(), 1); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); + let balance_after = erc1155.balance_of(USER1(), 1); + + assert(balance_after == balance_before - 1, 'invalid balance after'); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_credit_receiver() { + // credits transferred balance to receiver + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before = erc1155.balance_of(USER2(), 1); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); + let balance_after = erc1155.balance_of(USER2(), 1); + + assert(balance_after == balance_before + 1, 'invalid balance after'); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_preserve_existing_balances() { + // preserves existing balances which are not transferred by multiTokenHolder + let (world, erc1155) = deploy_testcase1(); + + // impersonate user1 + impersonate(USER1()); + + let balance_before_2 = erc1155.balance_of(USER2(), 2); + let balance_before_3 = erc1155.balance_of(USER2(), 3); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); + let balance_after_2 = erc1155.balance_of(USER2(), 2); + let balance_after_3 = erc1155.balance_of(USER2(), 3); + + assert(balance_after_2 == balance_before_2, 'should be equal'); + assert(balance_after_3 == balance_before_3, 'should be equal'); +} + +#[test] +#[available_gas(30000000)] +#[should_panic()] +fn test_safe_transfer_from_unapproved_operator() { + // when called by an operator on behalf of the multiTokenHolder + // when operator is not approved by multiTokenHolder + + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER2()); + + erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); // should panic +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_from_approved_operator() { + // when called by an operator on behalf of the multiTokenHolder + // when operator is approved by multiTokenHolder + let (world, erc1155) = deploy_testcase1(); + + impersonate(PROXY()); + + let balance_before = erc1155.balance_of(USER1(), 1); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 2, array![]); + let balance_after = erc1155.balance_of(USER1(), 1); + + assert(balance_after == balance_before - 2, 'invalid balance'); +} + +#[test] +#[available_gas(50000000)] +fn test_safe_transfer_from_approved_operator_preserve_operator_balance() { + // when called by an operator on behalf of the multiTokenHolder + // preserves operator's balances not involved in the transfer + let (world, erc1155) = deploy_testcase1(); + + impersonate(PROXY()); + + let balance_before_1 = erc1155.balance_of(PROXY(), 1); + let balance_before_2 = erc1155.balance_of(PROXY(), 2); + let balance_before_3 = erc1155.balance_of(PROXY(), 3); + erc1155.safe_transfer_from(USER1(), USER2(), 1, 2, array![]); + let balance_after_1 = erc1155.balance_of(PROXY(), 1); + let balance_after_2 = erc1155.balance_of(PROXY(), 2); + let balance_after_3 = erc1155.balance_of(PROXY(), 3); + + assert(balance_before_1 == balance_after_1, 'should be equal'); + assert(balance_before_2 == balance_after_2, 'should be equal'); + assert(balance_before_3 == balance_after_3, 'should be equal'); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_transfer_from_zero_address() { + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_transfer_from(ZERO(), USER1(), 1, 1, array![]); +} + +// +// safe_batch_transfer_from +// + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_more_than_balance() { + // reverts when transferring amount more than any of balances + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 999, 1], array![]); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_mismatching_array_len() { + // reverts when ids array length doesn't match amounts array length + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 1], array![]); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_to_zero_address() { + // reverts when transferring to zero address + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(USER1(), ZERO(), array![1, 2], array![1, 1], array![]); +} + + +#[test] +#[available_gas(60000000)] +fn test_safe_batch_transfer_from_debits_sender() { + // debits transferred balances from sender + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before_1 = erc1155.balance_of(USER1(), 1); + let balance_before_2 = erc1155.balance_of(USER1(), 2); + let balance_before_3 = erc1155.balance_of(USER1(), 3); + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); + let balance_after_1 = erc1155.balance_of(USER1(), 1); + let balance_after_2 = erc1155.balance_of(USER1(), 2); + let balance_after_3 = erc1155.balance_of(USER1(), 3); + + assert(balance_before_1 - 1 == balance_after_1, 'invalid balance'); + assert(balance_before_2 - 10 == balance_after_2, 'invalid balance'); + assert(balance_before_3 - 20 == balance_after_3, 'invalid balance'); +} + + +#[test] +#[available_gas(60000000)] +fn test_safe_batch_transfer_from_credits_recipient() { + // credits transferred balances to receiver + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before_1 = erc1155.balance_of(USER2(), 1); + let balance_before_2 = erc1155.balance_of(USER2(), 2); + let balance_before_3 = erc1155.balance_of(USER2(), 3); + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); + let balance_after_1 = erc1155.balance_of(USER2(), 1); + let balance_after_2 = erc1155.balance_of(USER2(), 2); + let balance_after_3 = erc1155.balance_of(USER2(), 3); + + assert(balance_before_1 + 1 == balance_after_1, 'invalid balance'); + assert(balance_before_2 + 10 == balance_after_2, 'invalid balance'); + assert(balance_before_1 + 20 == balance_after_3, 'invalid balance'); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_unapproved_operator() { + // when called by an operator on behalf of the multiTokenHolder + // when operator is not approved by multiTokenHolder + + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER2()); + + erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); +} + +#[test] +#[available_gas(60000000)] +fn test_safe_batch_transfer_from_approved_operator_preserve_operator_balance() { + // when called by an operator on behalf of the multiTokenHolder + // preserves operator's balances not involved in the transfer + + let (world, erc1155) = deploy_testcase1(); + + impersonate(PROXY()); + + let balance_before_1 = erc1155.balance_of(PROXY(), 1); + let balance_before_2 = erc1155.balance_of(PROXY(), 2); + let balance_before_3 = erc1155.balance_of(PROXY(), 3); + + erc1155 + .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 10, 20], array![]); + + let balance_after_1 = erc1155.balance_of(PROXY(), 1); + let balance_after_2 = erc1155.balance_of(PROXY(), 2); + let balance_after_3 = erc1155.balance_of(PROXY(), 3); + + assert(balance_before_1 == balance_after_1, 'should be equal'); + assert(balance_before_2 == balance_after_2, 'should be equal'); + assert(balance_before_3 == balance_after_3, 'should be equal'); +} + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_zero_address() { + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(ZERO(), USER1(), array![1, 2], array![1, 1], array![]); +} + + +#[test] +#[available_gas(50000000)] +fn test_safe_batch_transfer_emit_transfer_batch_event() { + let (world, erc1155) = deploy_default(); + + // user1 token_id 1 x 10 + erc1155.mint(USER1(), 1, 10, array![]); + // user1 token_id 2 x 20 + erc1155.mint(USER1(), 2, 20, array![]); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); + + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 1, 10, array![]); + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 2, 20, array![]); + + // TransferBatch + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::TransferBatch( + TransferBatch { + operator: USER1(), + from: USER1(), + to: USER2(), + ids: array![1, 2], + values: array![1, 10] + } + ), + 'invalid TransferBatch event' + ); +} + + +// +// burn +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_non_existing_token_id() { + //reverts when burning a non-existent token id + let (world, erc1155) = deploy_default(); + + impersonate(USER1()); + erc1155.burn(USER1(), 69, 1); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_burn_emit_transfer_single_event() { + // burn should emit event + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 69, 5, array![]); + assert(erc1155.balance_of(USER1(), 69) == 5, 'invalid balance'); + + impersonate(USER1()); + + erc1155.burn(USER1(), 69, 1); + assert(erc1155.balance_of(USER1(), 69) == 4, 'invalid balance'); + + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 69,5,array![]) + + // TransferSingle + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::TransferSingle( + TransferSingle { operator: USER1(), from: USER1(), to: ZERO(), id: 69, value: 1 } + ), + 'invalid TransferSingle event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_more_than_owned() { + // reverts when burning more tokens than owned + let (world, erc1155) = deploy_default(); + erc1155.mint(USER1(), 69, 10, array![]); + + impersonate(USER1()); + + erc1155.burn(USER1(), 69, 1); + erc1155.burn(USER1(), 69, 10); // should panic +} +// TODO : to be continued + +// TODO : add test if we support IERC1155Receiver + + diff --git a/crates/dojo-erc/src/tests/test_erc721.cairo b/crates/dojo-erc/src/tests/test_erc721.cairo new file mode 100644 index 0000000000..fc23abc4f9 --- /dev/null +++ b/crates/dojo-erc/src/tests/test_erc721.cairo @@ -0,0 +1,862 @@ +use core::zeroable::Zeroable; +use core::traits::{Into, Default}; +use array::ArrayTrait; +use serde::Serde; +use starknet::ContractAddress; +use starknet::testing::set_contract_address; +use option::OptionTrait; + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +use dojo_erc::tests::test_utils::impersonate; +use dojo_erc::tests::test_erc721_utils::{ + spawn_world, deploy_erc721, deploy_default, deploy_testcase1, USER1, USER2, USER3, DEPLOYER, + ZERO, PROXY +}; + + +use dojo_erc::erc165::interface::IERC165_ID; +use dojo_erc::erc721::interface::{ + IERC721, IERC721ADispatcher, IERC721ADispatcherTrait, IERC721_ID, IERC721_METADATA_ID +}; +use dojo_erc::erc721::erc721::ERC721::{Event, Transfer, Approval, ApprovalForAll}; +// actually it's possible to mint -> burn -> mint -> ... +// todo : add Minted component to keep track of minted ids + +#[test] +#[available_gas(30000000)] +fn test_deploy() { + let world = spawn_world(DEPLOYER()); + let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); + let erc721 = IERC721ADispatcher { contract_address: erc721_address }; + + assert(erc721.owner() == DEPLOYER(), 'invalid owner'); + assert(erc721.name() == 'name', 'invalid name'); + assert(erc721.symbol() == 'symbol', 'invalid symbol'); +} + + +#[test] +#[available_gas(30000000)] +fn test_deploy_default() { + let (world, erc721) = deploy_default(); + assert(erc721.name() == 'name', 'invalid name'); +} + +// +// supports_interface +// + +#[test] +#[available_gas(30000000)] +fn test_should_support_interfaces() { + let (world, erc721) = deploy_default(); + + assert(erc721.supports_interface(IERC165_ID) == true, 'should support erc165'); + assert(erc721.supports_interface(IERC721_ID) == true, 'should support erc721'); + assert( + erc721.supports_interface(IERC721_METADATA_ID) == true, 'should support erc721_metadata' + ); +} + + +// +// behaves like an ERC721 +// + +// +// balance_of +// + +use debug::PrintTrait; + +#[test] +#[available_gas(60000000)] +fn test_balance_of_with_tokens() { + // returns the amount of tokens owned by the given address + + let (world, erc721) = deploy_testcase1(); + assert(erc721.balance_of(USER1()) == 3, 'should be 3'); + assert(erc721.balance_of(PROXY()) == 4, 'should be 4'); +} + +#[test] +#[available_gas(60000000)] +fn test_balance_of_with_no_tokens() { + // when the given address does not own any tokens + + let (world, erc721) = deploy_testcase1(); + assert(erc721.balance_of(USER3()) == 0, 'should be 0'); +} + + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_balance_of_zero_address() { + // when querying the zero address + + let (world, erc721) = deploy_testcase1(); + erc721.balance_of(ZERO()); +} + +// +// owner_of +// + +#[test] +#[available_gas(90000000)] +fn test_owner_of_existing_id() { + // when the given token ID was tracked by this token = for existing id + + let (world, erc721) = deploy_testcase1(); + assert(erc721.owner_of(1) == USER1(), 'should be user1'); + assert(erc721.owner_of(2) == USER1(), 'should be user1'); + assert(erc721.owner_of(3) == USER1(), 'should be user1'); + + assert(erc721.owner_of(10) == PROXY(), 'should be proxy'); + assert(erc721.owner_of(11) == PROXY(), 'should be proxy'); + assert(erc721.owner_of(12) == PROXY(), 'should be proxy'); + assert(erc721.owner_of(13) == PROXY(), 'should be proxy'); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_owner_of_non_existing_id() { + // when the given token ID was not tracked by this token = non existing id + + let (world, erc721) = deploy_testcase1(); + let owner_of_0 = erc721.owner_of(0); // should panic +} + +// +// transfers +// + +#[test] +#[available_gas(90000000)] +fn test_transfer_ownership() { + // transfers the ownership of the given token ID to the given address + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + let owner_of_1 = erc721.owner_of(1); + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 1); + assert(erc721.owner_of(1) == USER2(), 'invalid owner'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_event() { + // emits a Transfer event + + let (world, erc721) = deploy_default(); + + // mint + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 42); + + impersonate(USER2()); + erc721.burn(42); + + // mint + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: ZERO(), to: USER1(), token_id: 42 }), + 'invalid Transfer event' + ); + // transfer + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: USER1(), to: USER2(), token_id: 42 }), + 'invalid Transfer event' + ); + // burn + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: USER2(), to: ZERO(), token_id: 42 }), + 'invalid Transfer event' + ); +} + + +#[test] +#[available_gas(90000000)] +fn test_transfer_clear_approval() { + // clears the approval for the token ID + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + erc721.approve(PROXY(), 1); + assert(erc721.get_approved(1) == PROXY(), 'should be proxy'); + + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 1); + assert(erc721.get_approved(1).is_zero(), 'should be zero'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_adjusts_owners_balances() { + // adjusts owners balances + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_user1_before = erc721.balance_of(USER1()); + let balance_user2_before = erc721.balance_of(USER2()); + + // transfer token_id 1 to user2 + erc721.transfer(USER2(), 1); + + let balance_user1_after = erc721.balance_of(USER1()); + let balance_user2_after = erc721.balance_of(USER2()); + + assert(balance_user1_after == balance_user1_before - 1, 'invalid user1 balance'); + assert(balance_user2_after == balance_user2_before + 1, 'invalid user2 balance'); +} + + +#[test] +#[available_gas(90000000)] +fn test_transfer_from_approved() { + // when called by the approved individual + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + //user1 approve user2 for token_id 2 + erc721.approve(USER2(), 2); + + impersonate(USER2()); + + erc721.transfer_from(USER1(), USER2(), 2); + assert(erc721.owner_of(2) == USER2(), 'invalid owner'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_from_approved_operator() { + // when called by the operator + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + //user1 set_approval_for_all for proxy + erc721.set_approval_for_all(PROXY(), true); + + impersonate(PROXY()); + + erc721.transfer_from(USER1(), USER2(), 2); + assert(erc721.owner_of(2) == USER2(), 'invalid owner'); +} + +#[test] +#[available_gas(90000000)] +fn test_transfer_from_owner_without_approved() { + // when called by the owner without an approved user + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + erc721.approve(ZERO(), 2); + + erc721.transfer_from(USER1(), USER2(), 2); + assert(erc721.owner_of(2) == USER2(), 'invalid owner'); +} + + +#[test] +#[available_gas(90000000)] +fn test_transfer_to_owner() { + // when sent to the owner + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + let balance_before = erc721.balance_of(USER1()); + + assert(erc721.owner_of(3) == USER1(), 'invalid owner'); + erc721.transfer(USER1(), 3); + + // keeps ownership of the token + assert(erc721.owner_of(3) == USER1(), 'invalid owner'); + + // clears the approval for the token ID + assert(erc721.get_approved(3) == ZERO(), 'invalid approved'); + + //emits only a transfer event : cumbersome to test with pop_log + + //keeps the owner balance + let balance_after = erc721.balance_of(USER1()); + assert(balance_before == balance_after, 'invalid balance') +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_when_previous_owner_is_incorrect() { + // when the address of the previous owner is incorrect + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + //user2 owner token_id 10 + erc721.transfer_from(USER1(), PROXY(), 10); // should panic +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_when_sender_not_authorized() { + // when the sender is not authorized for the token id + let (world, erc721) = deploy_testcase1(); + + impersonate(PROXY()); + + //proxy is not authorized for USER2 + erc721.transfer_from(USER2(), PROXY(), 20); // should panic +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_when_token_id_doesnt_exists() { + // when the sender is not authorized for the token id + let (world, erc721) = deploy_testcase1(); + + impersonate(PROXY()); + + //proxy is authorized for USER1 but token_id 50 doesnt exists + erc721.transfer_from(USER1(), PROXY(), 50); // should panic +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_transfer_to_address_zero() { + // when the address to transfer the token to is the zero address + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + erc721.transfer(ZERO(), 1); // should panic +} + +// +// approval +// + +// when clearing approval + +#[test] +#[available_gas(90000000)] +fn test_approval_when_clearing_with_prior_approval() { + // -when there was a prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + erc721.approve(PROXY(), 42); + + //revoke approve + erc721.approve(ZERO(), 42); + + // clears approval for the token + assert(erc721.get_approved(42) == ZERO(), 'invalid approved'); + + // emits an approval event + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop approve PROXY + + // approve ZERO + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: ZERO(), token_id: 42 }), + 'invalid Approval event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_when_clearing_without_prior_approval() { + // when clearing approval + // -when there was no prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + //revoke approve + erc721.approve(ZERO(), 42); + + // updates approval for the token + assert(erc721.get_approved(42) == ZERO(), 'invalid approved'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + + // approve ZERO + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: ZERO(), token_id: 42 }), + 'invalid Approval event' + ); +} + + +// when approving a non-zero address + +#[test] +#[available_gas(90000000)] +fn test_approval_non_zero_address_with_prior_approval() { + // -when there was a prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + erc721.approve(PROXY(), 42); + + // user1 approves user3 + erc721.approve(USER3(), 42); + + // set approval for the token + assert(erc721.get_approved(42) == USER3(), 'invalid approved'); + + // emits an approval event + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop approve PROXY + + // approve USER3 + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER3(), token_id: 42 }), + 'invalid Approval event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_non_zero_address_with_no_prior_approval() { + // -when there was no prior approval + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + // user1 approves user3 + erc721.approve(USER3(), 42); + + // set approval for the token + assert(erc721.get_approved(42) == USER3(), 'invalid approved'); + + // emits an approval event + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + + // approve USER3 + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER3(), token_id: 42 }), + 'invalid Approval event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_self_approve() { + // when the address that receives the approval is the owner + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 42); + + impersonate(USER1()); + + // user1 approves user1 + erc721.approve(USER1(), 42); // should panic +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_not_owned() { + // when the sender does not own the given token ID + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + // user1 approves user2 for token 20 + erc721.approve(USER2(), 20); // should panic +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_from_approved_sender() { + // when the sender is approved for the given token ID + + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + // user1 approve user3 + erc721.approve(USER3(), 1); + + impersonate(USER3()); + + // (ERC721: approve caller is not token owner or approved for all) + erc721.approve(USER2(), 1); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_approval_from_approved_operator() { + // when the sender is an operator + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 50); + + impersonate(USER1()); + + erc721.set_approval_for_all(PROXY(), true); + + impersonate(PROXY()); + + // proxy approves user2 for token 20 + erc721.approve(USER2(), 50); + + assert(erc721.get_approved(50) == USER2(), 'invalid approval'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address).unwrap(); // unpop mint + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all + + // approve + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Approval(Approval { owner: USER1(), to: USER2(), token_id: 50 }), + 'invalid Approval event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_unexisting_id() { + // when the given token ID does not exist + let (world, erc721) = deploy_testcase1(); + + impersonate(USER1()); + + // user1 approve user3 + erc721.approve(USER3(), 69); // should panic +} + +// +// approval_for_all +// + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_is_not_owner_no_operator_approval() { + // when the operator willing to approve is not the owner + // -when there is no operator approval set by the sender + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + // user2 set_approval_for_all PROXY + erc721.set_approval_for_all(PROXY(), true); + + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_is_not_owner_from_not_approved() { + // when the operator willing to approve is not the owner + // -when the operator was set as not approved + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + erc721.set_approval_for_all(PROXY(), false); + + // user2 set_approval_for_all PROXY + erc721.set_approval_for_all(PROXY(), true); + + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), false) + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_is_not_owner_can_unset_approval_for_all() { + // when the operator willing to approve is not the owner + // can unset the operator approval + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + erc721.set_approval_for_all(PROXY(), false); + erc721.set_approval_for_all(PROXY(), true); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + erc721.set_approval_for_all(PROXY(), false); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == false, 'invalid is_approved_for_all'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), false) + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), true) + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: false } + ), + 'invalid ApprovalForAll event' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_approval_for_all_operator_with_operator_already_approved() { + // when the operator willing to approve is not the owner + // when the operator was already approved + let (world, erc721) = deploy_default(); + + impersonate(USER2()); + + erc721.set_approval_for_all(PROXY(), true); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + erc721.set_approval_for_all(PROXY(), true); + assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop set_approval_for_all(PROXY(), true) + + // ApproveForAll + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER2(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_approval_for_all_with_owner_as_operator() { + // when the operator is the owner + + let (world, erc721) = deploy_default(); + + impersonate(USER1()); + + erc721.set_approval_for_all(USER1(), true); // should panic +} + + +// +// get_approved +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_get_approved_unexisting_token() { + let (world, erc721) = deploy_default(); + + erc721.get_approved(420); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_get_approved_with_existing_token() { + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 420); + assert(erc721.get_approved(420) == ZERO(), 'invalid get_approved'); +} + + +#[test] +#[available_gas(90000000)] +fn test_get_approved_with_existing_token_and_approval() { + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 420); + + impersonate(USER1()); + + erc721.approve(PROXY(), 420); + assert(erc721.get_approved(420) == PROXY(), 'invalid get_approved'); +} + +// +// mint +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_mint_to_address_zero() { + // reverts with a null destination address + + let (world, erc721) = deploy_default(); + + erc721.mint(ZERO(), 69); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_mint() { + // reverts with a null destination address + + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 69); + + assert(erc721.balance_of(USER1()) == 1, 'invalid balance'); + + // Transfer + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: ZERO(), to: USER1(), token_id: 69 }), + 'invalid Transfer event' + ); +} + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_mint_existing_token_id() { + // reverts with a null destination address + + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 69); + erc721.mint(USER1(), 69); //should panic +} + + +// +// burn +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_non_existing_token_id() { + //reverts when burning a non-existent token id + let (world, erc721) = deploy_default(); + erc721.burn(69); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_burn_emit_events() { + // burn should emit event + let (world, erc721) = deploy_default(); + + erc721.mint(USER1(), 69); + assert(erc721.balance_of(USER1()) == 1, 'invalid balance'); + + impersonate(USER1()); + + erc721.burn(69); + assert(erc721.balance_of(USER1()) == 0, 'invalid balance'); + + let _: Event = starknet::testing::pop_log(erc721.contract_address) + .unwrap(); // unpop erc721.mint(USER1(), 69) + + // Transfer + assert( + @starknet::testing::pop_log(erc721.contract_address) + .unwrap() == @Event::Transfer(Transfer { from: USER1(), to: ZERO(), token_id: 69 }), + 'invalid Transfer event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_same_id_twice() { + // reverts when burning a token id that has been deleted + let (world, erc721) = deploy_default(); + erc721.mint(USER1(), 69); + erc721.burn(69); + erc721.burn(69); // should panic +} + +// +// token_uri +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_token_uri_for_non_existing_token_id() { + // reverts when queried for non existent token id + let (world, erc721) = deploy_default(); + erc721.token_uri(1234); // should panic +} + diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index b615549e45..bb70482980 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -8,7 +8,7 @@ test_manifest_file "world": { "name": "world", "address": null, - "class_hash": "0x260bc39baaf87e4310e5213178b5f7bea7344b9806b9d08101ec066aa7bc18", + "class_hash": "0xad01919d2f7edc172b74b0e258afb7bb44252582e02d5e245b88fcb488fb78", "abi": [ { "type": "impl", @@ -35,6 +35,20 @@ test_manifest_file } ] }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::felt252" + }, + { + "name": "None", + "type": "()" + } + ] + }, { "type": "struct", "name": "core::array::Span::>", @@ -188,14 +202,18 @@ test_manifest_file }, { "name": "index", - "type": "core::felt252" + "type": "core::option::Option::" }, { - "name": "length", + "name": "values", + "type": "core::array::Span::" + }, + { + "name": "values_length", "type": "core::integer::u32" }, { - "name": "layout", + "name": "values_layout", "type": "core::array::Span::" } ],