diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c060051ae4..3c7ba11e6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,7 @@ jobs: - run: | chmod +x /tmp/bins/sozo /tmp/bins/sozo --manifest-path crates/dojo/core/Scarb.toml test + /tmp/bins/sozo --manifest-path crates/dojo/core-cairo-test/Scarb.toml test dojo-spawn-and-move-example-test: needs: build @@ -148,6 +149,7 @@ jobs: - run: | chmod +x /tmp/bins/sozo /tmp/bins/sozo --manifest-path examples/spawn-and-move/Scarb.toml test + /tmp/bins/sozo --manifest-path examples/simple/Scarb.toml test clippy: runs-on: ubuntu-latest-4-cores diff --git a/crates/dojo/core-cairo-test/src/lib.cairo b/crates/dojo/core-cairo-test/src/lib.cairo index d1170f442d..3e9ca8017e 100644 --- a/crates/dojo/core-cairo-test/src/lib.cairo +++ b/crates/dojo/core-cairo-test/src/lib.cairo @@ -10,7 +10,7 @@ pub use utils::{GasCounter, assert_array, GasCounterTrait}; #[cfg(target: "test")] pub use world::{ deploy_contract, deploy_with_world_address, spawn_test_world, NamespaceDef, TestResource, - ContractDef, ContractDefTrait + ContractDef, ContractDefTrait, WorldStorageTestTrait, }; #[cfg(test)] @@ -45,8 +45,8 @@ mod tests { mod world { mod acl; //mod entities; - //mod resources; - //mod world; + //mod resources; + mod world; } mod utils { diff --git a/crates/dojo/core-cairo-test/src/tests/contract.cairo b/crates/dojo/core-cairo-test/src/tests/contract.cairo index d206303612..1b3b9d0162 100644 --- a/crates/dojo/core-cairo-test/src/tests/contract.cairo +++ b/crates/dojo/core-cairo-test/src/tests/contract.cairo @@ -69,6 +69,7 @@ pub mod test_contract_upgrade { #[available_gas(7000000)] fn test_upgrade_from_world() { let world = deploy_world(); + let world = world.dispatcher; let base_address = world .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); @@ -85,6 +86,7 @@ fn test_upgrade_from_world() { #[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_upgrade_from_world_not_world_provider() { let world = deploy_world(); + let world = world.dispatcher; let _ = world .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); @@ -98,6 +100,7 @@ fn test_upgrade_from_world_not_world_provider() { #[should_panic(expected: ('must be called by world', 'ENTRYPOINT_FAILED'))] fn test_upgrade_direct() { let world = deploy_world(); + let world = world.dispatcher; let base_address = world .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); @@ -174,6 +177,7 @@ mod invalid_model_world { )] fn test_register_namespace_empty_name() { let world = deploy_world(); + let world = world.dispatcher; world.register_namespace(""); } diff --git a/crates/dojo/core-cairo-test/src/tests/helpers.cairo b/crates/dojo/core-cairo-test/src/tests/helpers.cairo index 68f07d0d53..ca53dda73f 100644 --- a/crates/dojo/core-cairo-test/src/tests/helpers.cairo +++ b/crates/dojo/core-cairo-test/src/tests/helpers.cairo @@ -1,11 +1,11 @@ use starknet::ContractAddress; -use dojo::world::{ - IWorldDispatcher, IWorldDispatcherTrait, IWorldTestDispatcher, IWorldTestDispatcherTrait -}; +use dojo::world::{IWorldDispatcher, WorldStorage, WorldStorageTrait}; use dojo::model::Model; -use crate::world::{spawn_test_world, NamespaceDef, TestResource, ContractDefTrait}; +use crate::world::{ + spawn_test_world, NamespaceDef, TestResource, ContractDefTrait, WorldStorageTestTrait +}; pub const DOJO_NSH: felt252 = 0x309e09669bc1fdc1dd6563a7ef862aa6227c97d099d08cc7b81bad58a7443fa; @@ -165,90 +165,73 @@ pub enum Weapon { pub trait Ibar { fn set_foo(self: @TContractState, a: felt252, b: u128); fn delete_foo(self: @TContractState); - fn delete_foo_macro(self: @TContractState, foo: Foo); - fn set_char(self: @TContractState, a: felt252, b: u32); } -#[starknet::contract] +#[dojo::contract] pub mod bar { use core::traits::Into; - use starknet::{get_caller_address, ContractAddress}; - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - use dojo::model::{Model, ModelIndex}; - use super::DOJO_NSH; + use starknet::{get_caller_address}; + use dojo::model::{ModelStorage, ModelPtr}; - use super::{Foo, IWorldDispatcher, IWorldDispatcherTrait}; + use super::{Foo, IWorldDispatcher}; #[storage] struct Storage { world: IWorldDispatcher, } - #[constructor] - fn constructor(ref self: ContractState, world: ContractAddress) { - self.world.write(IWorldDispatcher { contract_address: world }) - } #[abi(embed_v0)] impl IbarImpl of super::Ibar { - fn set_foo( - self: @ContractState, a: felt252, b: u128 - ) { // set!(self.world.read(), Foo { caller: get_caller_address(), a, b }); + fn set_foo(self: @ContractState, a: felt252, b: u128) { + let mut world = self.world(@"dojo"); + world.write_model(@Foo { caller: get_caller_address(), a, b }); } fn delete_foo(self: @ContractState) { - self - .world - .read() - .delete_entity( - Model::::selector(DOJO_NSH), - ModelIndex::Keys([get_caller_address().into()].span()), - Model::::layout() - ); - } - - fn delete_foo_macro( - self: @ContractState, foo: Foo - ) { //delete!(self.world.read(), Foo { caller: foo.caller, a: foo.a, b: foo.b }); + let mut world = self.world(@"dojo"); + let ptr = ModelPtr::< + Foo + >::Id(core::poseidon::poseidon_hash_span([get_caller_address().into()].span())); + world.erase_model_ptr(ptr); } - - fn set_char(self: @ContractState, a: felt252, b: u32) {} } } /// Deploys an empty world with the `dojo` namespace. -pub fn deploy_world() -> IWorldDispatcher { +pub fn deploy_world() -> WorldStorage { let namespace_def = NamespaceDef { namespace: "dojo", resources: [].span(), }; - spawn_test_world([namespace_def].span()).dispatcher + spawn_test_world([namespace_def].span()) } /// Deploys an empty world with the `dojo` namespace and registers the `foo` model. /// No permissions are granted. -pub fn deploy_world_and_foo() -> (IWorldDispatcher, felt252) { - let world = deploy_world(); - world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(DOJO_NSH); +pub fn deploy_world_and_foo() -> (WorldStorage, felt252) { + let namespace_def = NamespaceDef { + namespace: "dojo", resources: [TestResource::Model(m_Foo::TEST_CLASS_HASH)].span(), + }; - (world, foo_selector) + (spawn_test_world([namespace_def].span()), Model::::selector(DOJO_NSH)) } /// Deploys an empty world with the `dojo` namespace and registers the `foo` model. /// Grants the `bar` contract writer permissions to the `foo` model. -pub fn deploy_world_and_bar() -> (IWorldDispatcher, IbarDispatcher) { +pub fn deploy_world_and_bar() -> (WorldStorage, IbarDispatcher) { let namespace_def = NamespaceDef { namespace: "dojo", resources: [ - TestResource::Model(m_Foo::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Contract(ContractDefTrait::new(bar::TEST_CLASS_HASH, "bar")), + TestResource::Model(m_Foo::TEST_CLASS_HASH), + TestResource::Contract(bar::TEST_CLASS_HASH), ].span(), }; - let world = spawn_test_world([namespace_def].span()).dispatcher; - let bar_address = IWorldTestDispatcher { contract_address: world.contract_address } - .dojo_contract_address(selector_from_tag!("dojo-bar")); + let bar_def = ContractDefTrait::new(@"dojo", @"bar") + .with_writer_of([Model::::selector(DOJO_NSH)].span()); - let bar_contract = IbarDispatcher { contract_address: bar_address }; + let mut world = spawn_test_world([namespace_def].span()); + world.sync_perms_and_inits([bar_def].span()); - world.grant_writer(Model::::selector(DOJO_NSH), bar_address); + let (bar_address, _) = world.dns(@"bar").unwrap(); + let bar_contract = IbarDispatcher { contract_address: bar_address }; (world, bar_contract) } diff --git a/crates/dojo/core-cairo-test/src/tests/world/acl.cairo b/crates/dojo/core-cairo-test/src/tests/world/acl.cairo index 62becfe90d..f899b51dfb 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/acl.cairo +++ b/crates/dojo/core-cairo-test/src/tests/world/acl.cairo @@ -10,6 +10,8 @@ use crate::tests::expanded::selector_attack::{attacker_model, attacker_contract} fn test_owner() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; + let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -34,6 +36,7 @@ fn test_owner() { #[should_panic(expected: ("Resource `42` is not registered", 'ENTRYPOINT_FAILED'))] fn test_grant_owner_not_registered_resource() { let world = deploy_world(); + let world = world.dispatcher; // 42 is not a registered resource ID world.grant_owner(42, 69.try_into().unwrap()); @@ -43,6 +46,7 @@ fn test_grant_owner_not_registered_resource() { #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_grant_owner_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -65,6 +69,7 @@ fn test_grant_owner_through_malicious_contract() { )] fn test_grant_owner_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -79,6 +84,7 @@ fn test_grant_owner_fails_for_non_owner() { #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_revoke_owner_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -102,6 +108,7 @@ fn test_revoke_owner_through_malicious_contract() { )] fn test_revoke_owner_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -118,6 +125,7 @@ fn test_revoke_owner_fails_for_non_owner() { #[available_gas(6000000)] fn test_writer() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; assert(!world.is_writer(foo_selector, 69.try_into().unwrap()), 'should not be writer'); @@ -131,6 +139,7 @@ fn test_writer() { #[test] fn test_writer_not_registered_resource() { let world = deploy_world(); + let world = world.dispatcher; // 42 is not a registered resource ID !world.is_writer(42, 69.try_into().unwrap()); @@ -140,6 +149,7 @@ fn test_writer_not_registered_resource() { #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_grant_writer_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -162,6 +172,7 @@ fn test_grant_writer_through_malicious_contract() { )] fn test_grant_writer_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -176,6 +187,7 @@ fn test_grant_writer_fails_for_non_owner() { #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_revoke_writer_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -199,6 +211,7 @@ fn test_revoke_writer_through_malicious_contract() { )] fn test_revoke_writer_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -221,6 +234,7 @@ fn test_revoke_writer_fails_for_non_owner() { )] fn test_not_writer_with_known_contract() { let (world, _) = deploy_world_and_foo(); + let world = world.dispatcher; let account = starknet::contract_address_const::<0xb0b>(); world.grant_owner(bytearray_hash(@"dojo"), account); @@ -259,6 +273,7 @@ fn test_register_model_namespace_not_owner() { // Owner deploys the world and register Foo model. let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; assert(world.is_owner(foo_selector, owner), 'should be owner'); @@ -290,6 +305,7 @@ fn test_register_contract_namespace_not_owner() { // Owner deploys the world and register Foo model. let (world, foo_selector) = deploy_world_and_foo(); + let world = world.dispatcher; assert(world.is_owner(foo_selector, owner), 'should be owner'); diff --git a/crates/dojo/core-cairo-test/src/tests/world/world.cairo b/crates/dojo/core-cairo-test/src/tests/world/world.cairo index f8c80333cb..e394feb314 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/world.cairo +++ b/crates/dojo/core-cairo-test/src/tests/world/world.cairo @@ -1,59 +1,26 @@ use dojo::world::Resource; -use dojo::world::world::Event; -use dojo::model::Model; +use dojo::world::world::Event as WorldEvent; use dojo::utils::bytearray_hash; use dojo::world::{ IWorldDispatcher, IWorldDispatcherTrait, IUpgradeableWorldDispatcher, - IUpgradeableWorldDispatcherTrait + IUpgradeableWorldDispatcherTrait, WorldStorageTrait }; -use dojo::tests::helpers::{ - IbarDispatcher, IbarDispatcherTrait, drop_all_events, deploy_world_and_bar, Foo, foo, bar, - Character, character, test_contract, test_contract_with_dojo_init_args, SimpleEvent, - simple_event, SimpleEventEmitter -}; -use dojo::utils::test::{spawn_test_world, deploy_with_world_address, GasCounterTrait}; +use dojo::model::ModelStorage; +use dojo::event::{Event, EventStorage}; -#[starknet::interface] -trait IMetadataOnly { - fn selector(self: @T) -> felt252; - fn name(self: @T) -> ByteArray; - fn namespace(self: @T) -> ByteArray; - fn namespace_hash(self: @T) -> felt252; -} - -#[starknet::contract] -mod resource_metadata_malicious { - use dojo::model::{ModelDefinition, ResourceMetadata}; - use dojo::utils::bytearray_hash; - - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelName of super::IMetadataOnly { - fn selector(self: @ContractState) -> felt252 { - ModelDefinition::::selector() - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn namespace_hash(self: @ContractState) -> felt252 { - bytearray_hash(@Self::namespace(self)) - } - - fn name(self: @ContractState) -> ByteArray { - "invalid_model_name" - } - } -} +use crate::tests::helpers::{ + IbarDispatcherTrait, drop_all_events, deploy_world_and_bar, Foo, m_Foo, test_contract, + test_contract_with_dojo_init_args, SimpleEvent, e_SimpleEvent, deploy_world +}; +use crate::{spawn_test_world, ContractDefTrait, NamespaceDef, TestResource, WorldStorageTestTrait}; #[test] #[available_gas(20000000)] fn test_model() { let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); + let world = world.dispatcher; + + world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); } #[test] @@ -62,7 +29,7 @@ fn test_system() { bar_contract.set_foo(1337, 1337); - let stored: Foo = get!(world, starknet::get_caller_address(), Foo); + let stored: Foo = world.read_model(starknet::get_caller_address()); assert(stored.a == 1337, 'data not stored'); assert(stored.b == 1337, 'data not stored'); } @@ -71,16 +38,14 @@ fn test_system() { fn test_delete() { let (world, bar_contract) = deploy_world_and_bar(); - // set model bar_contract.set_foo(1337, 1337); - let stored: Foo = get!(world, starknet::get_caller_address(), Foo); + let stored: Foo = world.read_model(starknet::get_caller_address()); assert(stored.a == 1337, 'data not stored'); assert(stored.b == 1337, 'data not stored'); - // delete model - bar_contract.delete_foo_macro(stored); + bar_contract.delete_foo(); - let deleted: Foo = get!(world, starknet::get_caller_address(), Foo); + let deleted: Foo = world.read_model(starknet::get_caller_address()); assert(deleted.a == 0, 'data not deleted'); assert(deleted.b == 0, 'data not deleted'); } @@ -89,9 +54,10 @@ fn test_delete() { #[available_gas(6000000)] fn test_contract_getter() { let world = deploy_world(); + let world = world.dispatcher; let address = world - .register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); if let Resource::Contract((contract_address, namespace_hash)) = world .resource(selector_from_tag!("dojo-test_contract")) { @@ -106,28 +72,34 @@ fn test_contract_getter() { fn test_emit() { let bob = starknet::contract_address_const::<0xb0b>(); - let world = deploy_world(); - world.register_event(simple_event::TEST_CLASS_HASH.try_into().unwrap()); - world.grant_writer(dojo::event::Event::::selector(), bob); + let namespace_def = NamespaceDef { + namespace: "dojo", resources: [TestResource::Event(e_SimpleEvent::TEST_CLASS_HASH),].span(), + }; + + let mut world = spawn_test_world([namespace_def].span()); - drop_all_events(world.contract_address); + let bob_def = ContractDefTrait::new_address(bob) + .with_writer_of([world.resource_selector(@"SimpleEvent")].span()); + world.sync_perms_and_inits([bob_def].span()); + + drop_all_events(world.dispatcher.contract_address); starknet::testing::set_contract_address(bob); let simple_event = SimpleEvent { id: 2, data: (3, 4) }; - simple_event.emit(world); + world.emit_event(@simple_event); - let event = starknet::testing::pop_log::(world.contract_address); + let event = starknet::testing::pop_log::(world.dispatcher.contract_address); assert(event.is_some(), 'no event'); - if let Event::EventEmitted(event) = event.unwrap() { + if let WorldEvent::EventEmitted(event) = event.unwrap() { assert( - event.event_selector == dojo::event::Event::::selector(), + event.selector == Event::::selector(world.namespace_hash), 'bad event selector' ); assert(event.system_address == bob, 'bad system address'); - assert(event.historical, 'bad historical value'); + assert(!event.historical, 'bad historical value'); assert(event.keys == [2].span(), 'bad keys'); assert(event.values == [3, 4].span(), 'bad values'); } else { @@ -135,29 +107,10 @@ fn test_emit() { } } - -// Utils -fn deploy_world() -> IWorldDispatcher { - spawn_test_world(["dojo"].span(), [].span()) -} - #[test] fn test_execute_multiple_worlds() { - // Deploy world contract - let world1 = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(),); - let contract_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world1); - - world1.grant_writer(Model::::selector(), contract_address); - - let bar1_contract = IbarDispatcher { contract_address }; - - // Deploy another world contract - let world2 = spawn_test_world(["dojo"].span(), [foo::TEST_CLASS_HASH].span(),); - let contract_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world2); - - world2.grant_writer(Model::::selector(), contract_address); - - let bar2_contract = IbarDispatcher { contract_address }; + let (world1, bar1_contract) = deploy_world_and_bar(); + let (world2, bar2_contract) = deploy_world_and_bar(); let alice = starknet::contract_address_const::<0x1337>(); starknet::testing::set_contract_address(alice); @@ -165,57 +118,13 @@ fn test_execute_multiple_worlds() { bar1_contract.set_foo(1337, 1337); bar2_contract.set_foo(7331, 7331); - let data1 = get!(world1, alice, Foo); - let data2 = get!(world2, alice, Foo); + let data1: Foo = world1.read_model(alice); + let data2: Foo = world2.read_model(alice); assert(data1.a == 1337, 'data1 not stored'); assert(data2.a == 7331, 'data2 not stored'); } -#[test] -#[available_gas(60000000)] -fn bench_execute() { - let (world, bar_contract) = deploy_world_and_bar(); - - let alice = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_contract_address(alice); - - let gas = GasCounterTrait::start(); - - bar_contract.set_foo(1337, 1337); - gas.end("foo set call"); - - let gas = GasCounterTrait::start(); - let data = get!(world, alice, Foo); - gas.end("foo get macro"); - - assert(data.a == 1337, 'data not stored'); -} - -#[test] -fn bench_execute_complex() { - let world = spawn_test_world(["dojo"].span(), [character::TEST_CLASS_HASH].span(),); - let contract_address = deploy_with_world_address(bar::TEST_CLASS_HASH, world); - let bar_contract = IbarDispatcher { contract_address }; - - world.grant_writer(Model::::selector(), contract_address); - - let alice = starknet::contract_address_const::<0xa11ce>(); - starknet::testing::set_contract_address(alice); - - let gas = GasCounterTrait::start(); - - bar_contract.set_char(1337, 1337); - gas.end("char set call"); - - let gas = GasCounterTrait::start(); - - let data = get!(world, alice, Character); - gas.end("char get macro"); - - assert(data.heigth == 1337, 'data not stored'); -} - #[starknet::interface] trait IWorldUpgrade { fn hello(self: @TContractState) -> felt252; @@ -242,8 +151,8 @@ mod worldupgrade { #[test] #[available_gas(60000000)] fn test_upgradeable_world() { - // Deploy world contract let world = deploy_world(); + let world = world.dispatcher; let mut upgradeable_world_dispatcher = IUpgradeableWorldDispatcher { contract_address: world.contract_address @@ -259,8 +168,8 @@ fn test_upgradeable_world() { #[available_gas(60000000)] #[should_panic(expected: ('invalid class_hash', 'ENTRYPOINT_FAILED'))] fn test_upgradeable_world_with_class_hash_zero() { - // Deploy world contract let world = deploy_world(); + let world = world.dispatcher; let admin = starknet::contract_address_const::<0x1337>(); starknet::testing::set_account_contract_address(admin); @@ -280,6 +189,7 @@ fn test_upgradeable_world_with_class_hash_zero() { fn test_upgradeable_world_from_non_owner() { // Deploy world contract let world = deploy_world(); + let world = world.dispatcher; let not_owner = starknet::contract_address_const::<0x1337>(); starknet::testing::set_contract_address(not_owner); @@ -295,18 +205,22 @@ fn test_upgradeable_world_from_non_owner() { #[available_gas(6000000)] fn test_constructor_default() { let world = deploy_world(); + let world = world.dispatcher; + let _address = world - .register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); } #[test] fn test_can_call_init_only_world() { let world = deploy_world(); + let world = world.dispatcher; + let address = world - .register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); let expected_panic: ByteArray = - "Only the world can init contract `dojo-test_contract`, but caller is `0`"; + "Only the world can init contract `test_contract`, but caller is `0`"; match starknet::syscalls::call_contract_syscall( address, dojo::world::world::DOJO_INIT_SELECTOR, [].span() @@ -320,7 +234,7 @@ fn test_can_call_init_only_world() { s.pop_back().unwrap(); let e_str: ByteArray = Serde::deserialize(ref s).expect('failed deser'); - + println!("e_str: {}", e_str); assert_eq!(e_str, expected_panic); } } @@ -331,8 +245,10 @@ fn test_can_call_init_only_world() { #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_can_call_init_only_owner() { let world = deploy_world(); + let world = world.dispatcher; + let _address = world - .register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); let bob = starknet::contract_address_const::<0x1337>(); starknet::testing::set_contract_address(bob); @@ -344,8 +260,10 @@ fn test_can_call_init_only_owner() { #[available_gas(6000000)] fn test_can_call_init_default() { let world = deploy_world(); + let world = world.dispatcher; + let _address = world - .register_contract('salt1', test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); world.init_contract(selector_from_tag!("dojo-test_contract"), [].span()); } @@ -354,9 +272,11 @@ fn test_can_call_init_default() { #[available_gas(6000000)] fn test_can_call_init_args() { let world = deploy_world(); + let world = world.dispatcher; + let _address = world .register_contract( - 'salt1', test_contract_with_dojo_init_args::TEST_CLASS_HASH.try_into().unwrap() + 'salt1', "dojo", test_contract_with_dojo_init_args::TEST_CLASS_HASH.try_into().unwrap() ); world.init_contract(selector_from_tag!("dojo-test_contract_with_dojo_init_args"), [1].span()); @@ -365,13 +285,15 @@ fn test_can_call_init_args() { #[test] fn test_can_call_init_only_world_args() { let world = deploy_world(); + let world = world.dispatcher; + let address = world .register_contract( - 'salt1', test_contract_with_dojo_init_args::TEST_CLASS_HASH.try_into().unwrap() + 'salt1', "dojo", test_contract_with_dojo_init_args::TEST_CLASS_HASH.try_into().unwrap() ); let expected_panic: ByteArray = - "Only the world can init contract `dojo-test_contract_with_dojo_init_args`, but caller is `0`"; + "Only the world can init contract `test_contract_with_dojo_init_args`, but caller is `0`"; match starknet::syscalls::call_contract_syscall( address, dojo::world::world::DOJO_INIT_SELECTOR, [123].span() diff --git a/crates/dojo/core-cairo-test/src/world.cairo b/crates/dojo/core-cairo-test/src/world.cairo index 0b8449d959..8e3910fc21 100644 --- a/crates/dojo/core-cairo-test/src/world.cairo +++ b/crates/dojo/core-cairo-test/src/world.cairo @@ -2,50 +2,85 @@ use core::option::OptionTrait; use core::result::ResultTrait; use core::traits::{Into, TryInto}; -use starknet::{ContractAddress, ClassHash, syscalls::deploy_syscall}; +use starknet::{ContractAddress, syscalls::deploy_syscall}; use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait, WorldStorageTrait, WorldStorage}; +pub type TestClassHash = felt252; + /// In Cairo test runner, all the classes are expected to be declared already. /// If a contract belong to an other crate, it must be added to the `build-external-contract`, /// event for testing, since Scarb does not do that automatically anymore. +/// +/// The [`TestResource`] enum uses a felt252 to represent the class hash, this avoids +/// having to write `bar::TEST_CLASS_HASH.try_into().unwrap()` in the test file, simply use +/// `bar::TEST_CLASS_HASH`. #[derive(Drop)] pub enum TestResource { - Event: ClassHash, - Model: ClassHash, - Contract: ContractDef, + Event: TestClassHash, + Model: TestClassHash, + Contract: TestClassHash, } -#[derive(Drop)] -pub struct NamespaceDef { - pub namespace: ByteArray, - pub resources: Span, +#[derive(Drop, Copy)] +pub enum ContractDescriptor { + /// Address of the contract. + Address: ContractAddress, + /// Namespace and name of the contract. + Named: (@ByteArray, @ByteArray), } -#[derive(Drop)] +/// Definition of a contract to register in the world. +/// +/// You can use this struct for a dojo contract, but also for an external contract. +/// The only difference is the `init_calldata`, which is only used for dojo contracts. +/// If the `contract` is an external contract (hence an address), then `init_calldata` is ignored. +#[derive(Drop, Copy)] pub struct ContractDef { - /// Class hash, use `felt252` instead of `ClassHash` as TEST_CLASS_HASH is a `felt252`. - pub class_hash: felt252, - /// Name of the contract. - pub name: ByteArray, - /// Calldata for dojo_init. - pub init_calldata: Span, + /// The contract to grant permission to. + pub contract: ContractDescriptor, /// Selectors of the resources that the contract is granted writer access to. pub writer_of: Span, /// Selector of the resource that the contract is the owner of. pub owner_of: Span, + /// Calldata for dojo_init. + pub init_calldata: Span, +} + +#[derive(Drop)] +pub struct NamespaceDef { + pub namespace: ByteArray, + pub resources: Span, } #[generate_trait] pub impl ContractDefImpl of ContractDefTrait { - fn new(class_hash: felt252, name: ByteArray) -> ContractDef { + fn new(namespace: @ByteArray, name: @ByteArray,) -> ContractDef { + ContractDef { + contract: ContractDescriptor::Named((namespace, name)), + writer_of: [].span(), + owner_of: [].span(), + init_calldata: [].span() + } + } + + fn new_address(address: ContractAddress) -> ContractDef { ContractDef { - class_hash, name, init_calldata: [].span(), writer_of: [].span(), owner_of: [].span() + contract: ContractDescriptor::Address(address), + writer_of: [].span(), + owner_of: [].span(), + init_calldata: [].span() } } fn with_init_calldata(mut self: ContractDef, init_calldata: Span) -> ContractDef { - self.init_calldata = init_calldata; + match self.contract { + ContractDescriptor::Address(_) => panic!( + "Cannot set init_calldata for address descriptor" + ), + ContractDescriptor::Named(_) => self.init_calldata = init_calldata, + }; + self } @@ -92,6 +127,10 @@ pub fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) - /// Spawns a test world registering provided resources into namespaces. /// +/// This function only deploys the world and registers the resources, it does not initialize the +/// contracts or any permissions. +/// The first namespace is used as the default namespace when [`WorldStorage`] is returned. +/// /// # Arguments /// /// * `namespaces_defs` - Definitions of namespaces to register. @@ -122,37 +161,64 @@ pub fn spawn_test_world(namespaces_defs: Span) -> WorldStorage { first_namespace = Option::Some(namespace.clone()); } - let namespace_hash = dojo::utils::bytearray_hash(@namespace); - for r in ns .resources .clone() { match r { - TestResource::Event(ch) => { world.register_event(namespace.clone(), *ch); }, - TestResource::Model(ch) => { world.register_model(namespace.clone(), *ch); }, - TestResource::Contract(def) => { - let class_hash: ClassHash = (*def.class_hash).try_into().unwrap(); - let contract_address = world - .register_contract(*def.class_hash, namespace.clone(), class_hash); - - for target in *def - .writer_of { - world.grant_writer(*target, contract_address); - }; - - for target in *def - .owner_of { - world.grant_owner(*target, contract_address); - }; - - let selector = dojo::utils::selector_from_namespace_and_name( - namespace_hash, def.name - ); - world.init_contract(selector, *def.init_calldata); + TestResource::Event(ch) => { + world.register_event(namespace.clone(), (*ch).try_into().unwrap()); + }, + TestResource::Model(ch) => { + world.register_model(namespace.clone(), (*ch).try_into().unwrap()); }, + TestResource::Contract(ch) => { + world.register_contract(*ch, namespace.clone(), (*ch).try_into().unwrap()); + } } } }; WorldStorageTrait::new(world, @first_namespace.unwrap()) } + +#[generate_trait] +pub impl WorldStorageInternalTestImpl of WorldStorageTestTrait { + fn sync_perms_and_inits(self: @WorldStorage, contracts: Span) { + // First, sync permissions as sozo is doing. + for c in contracts { + let contract_address = match c.contract { + ContractDescriptor::Address(address) => *address, + ContractDescriptor::Named(( + namespace, name + )) => { + let selector = dojo::utils::selector_from_names(*namespace, *name); + match (*self.dispatcher).resource(selector) { + dojo::world::Resource::Contract((address, _)) => address, + _ => panic!("Contract not found"), + } + }, + }; + + for w in *c.writer_of { + (*self.dispatcher).grant_writer(*w, contract_address); + }; + + for o in *c.owner_of { + (*self.dispatcher).grant_owner(*o, contract_address); + }; + }; + + // Then, calls the dojo_init for each contract that is a dojo contract. + for c in contracts { + match c.contract { + ContractDescriptor::Address(_) => {}, + ContractDescriptor::Named(( + namespace, name + )) => { + let selector = dojo::utils::selector_from_names(*namespace, *name); + (*self.dispatcher).init_contract(selector, *c.init_calldata); + } + } + }; + } +} diff --git a/crates/dojo/core/src/world/storage.cairo b/crates/dojo/core/src/world/storage.cairo index 3956742ea4..e922d882e5 100644 --- a/crates/dojo/core/src/world/storage.cairo +++ b/crates/dojo/core/src/world/storage.cairo @@ -40,8 +40,8 @@ pub impl WorldStorageInternalImpl of WorldStorageTrait { } } - fn contract_selector(self: @WorldStorage, contract_name: @ByteArray) -> felt252 { - dojo::utils::selector_from_namespace_and_name(*self.namespace_hash, contract_name) + fn resource_selector(self: @WorldStorage, name: @ByteArray) -> felt252 { + dojo::utils::selector_from_namespace_and_name(*self.namespace_hash, name) } } diff --git a/examples/simple/src/lib.cairo b/examples/simple/src/lib.cairo index f7ec1ea258..42f9dc16f6 100644 --- a/examples/simple/src/lib.cairo +++ b/examples/simple/src/lib.cairo @@ -115,24 +115,28 @@ pub mod c3 {} #[cfg(test)] mod tests { use dojo::model::ModelStorage; - use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource, ContractDefTrait}; + use dojo_cairo_test::{ + spawn_test_world, NamespaceDef, TestResource, ContractDefTrait, WorldStorageTestTrait + }; use super::{c1, m_M, M}; #[test] fn test_1() { let ndef = NamespaceDef { namespace: "ns", resources: [ - TestResource::Model(m_M::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Contract( - ContractDefTrait::new(c1::TEST_CLASS_HASH, "c1") - .with_init_calldata([0xff].span()) - .with_writer_of([dojo::utils::bytearray_hash(@"ns")].span()) - ) + TestResource::Model(m_M::TEST_CLASS_HASH), + TestResource::Contract(c1::TEST_CLASS_HASH), ].span() }; let world = spawn_test_world([ndef].span()); + let c1_def = ContractDefTrait::new(@"ns", @"c1") + .with_writer_of([dojo::utils::bytearray_hash(@"ns")].span()) + .with_init_calldata([0xff].span()); + + world.sync_perms_and_inits([c1_def].span()); + let m: M = world.read_model(0); assert!(m.v == 0xff, "invalid b"); //let m2 = M { a: 120, b: 244, }; diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index 10095145b0..36d78fa6cd 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -210,7 +210,10 @@ pub mod actions { mod tests { use dojo::model::{ModelStorage, ModelValueStorage, ModelStorageTest}; use dojo::world::WorldStorageTrait; - use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource, ContractDefTrait}; + use dojo_cairo_test::{ + spawn_test_world, NamespaceDef, TestResource, ContractDefTrait, ContractDef, + WorldStorageTestTrait + }; use super::{actions, IActionsDispatcher, IActionsDispatcherTrait}; use dojo_examples::models::{Position, PositionValue, m_Position, Moves, m_Moves, Direction,}; @@ -218,19 +221,23 @@ mod tests { fn namespace_def() -> NamespaceDef { let ndef = NamespaceDef { namespace: "ns", resources: [ - TestResource::Model(m_Position::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Model(m_Moves::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Event(actions::e_Moved::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Contract( - ContractDefTrait::new(actions::TEST_CLASS_HASH, "actions") - .with_writer_of([dojo::utils::bytearray_hash(@"ns")].span()) - ) + TestResource::Model(m_Position::TEST_CLASS_HASH), + TestResource::Model(m_Moves::TEST_CLASS_HASH), + TestResource::Event(actions::e_Moved::TEST_CLASS_HASH), + TestResource::Contract(actions::TEST_CLASS_HASH), ].span() }; ndef } + fn contract_defs() -> Span { + [ + ContractDefTrait::new(@"ns", @"actions") + .with_writer_of([dojo::utils::bytearray_hash(@"ns")].span()) + ].span() + } + #[test] fn test_world_test_set() { let caller = starknet::contract_address_const::<0x0>(); @@ -238,6 +245,8 @@ mod tests { let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); + world.sync_perms_and_inits(contract_defs()); + // Without having the permission, we can set data into the dojo database for the given // models. let mut position: Position = world.read_model(caller); @@ -272,6 +281,7 @@ mod tests { let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); + world.sync_perms_and_inits(contract_defs()); let (actions_system_addr, _) = world.dns(@"actions").unwrap(); let actions_system = IActionsDispatcher { contract_address: actions_system_addr }; diff --git a/scripts/cairo_fmt.sh b/scripts/cairo_fmt.sh index c5977f9cb7..f504323222 100755 --- a/scripts/cairo_fmt.sh +++ b/scripts/cairo_fmt.sh @@ -1,6 +1,12 @@ #!/bin/bash -scarb --manifest-path examples/spawn-and-move/Scarb.toml fmt --check -scarb --manifest-path examples/simple/Scarb.toml fmt --check -scarb --manifest-path crates/dojo/core/Scarb.toml fmt --check -scarb --manifest-path crates/dojo/core-cairo-test/Scarb.toml fmt --check +option="--check" + +if [ "$1" == "--fix" ]; then + option="" +fi + +scarb --manifest-path examples/spawn-and-move/Scarb.toml fmt $option +scarb --manifest-path examples/simple/Scarb.toml fmt $option +scarb --manifest-path crates/dojo/core/Scarb.toml fmt $option +scarb --manifest-path crates/dojo/core-cairo-test/Scarb.toml fmt $option