diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91eb3b5..e65badc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,23 +11,13 @@ env: RUST_VERSION: 1.80.0 jobs: - test: - runs-on: ubuntu-latest-4-cores - container: - image: ghcr.io/dojoengine/dojo-core-dev:5995840 - steps: - - uses: actions/checkout@v3 - - uses: Swatinem/rust-cache@v2 - - run: | - scripts/tests.sh - cairofmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: software-mansion/setup-scarb@v1 with: - scarb-version: "2.7.1" + scarb-version: "2.8.4" - run: | bash scripts/cairo_fmt.sh --check diff --git a/crates/compiler/src/plugin/semantics/test_data/get b/crates/compiler/src/plugin/semantics/test_data/get index fb404d8..6a1d035 100644 --- a/crates/compiler/src/plugin/semantics/test_data/get +++ b/crates/compiler/src/plugin/semantics/test_data/get @@ -132,28 +132,9 @@ Block( pattern: Variable( __Health, ), - expr: FunctionCall( - ExprFunctionCall { - function: ?4::get::, - args: [ - Value( - Snapshot( - ExprSnapshot { - inner: Var( - LocalVarId(test::world), - ), - ty: @dojo::world::iworld::IWorldDispatcher, - }, - ), - ), - Value( - Var( - LocalVarId(test::key), - ), - ), - ], - coupon_arg: None, - ty: test::Health, + expr: Missing( + ExprMissing { + ty: , }, ), }, @@ -169,7 +150,17 @@ Block( ) //! > semantic_diagnostics -error: Trait has no implementation in context: dojo::model::model::ModelStore::. +error: Identifier not found. --> lib.cairo:12:1 get!(world, key, (Health)) ^************************^ + +warning[E0001]: Unused variable. Consider ignoring by prefixing with `_`. + --> lib.cairo:10:22 +fn test_func() { let key: felt252 = 0xb0b; + ^*^ + +warning[E0001]: Unused variable. Consider ignoring by prefixing with `_`. + --> lib.cairo:11:5 +let world = IWorldDispatcher{contract_address: 0x0.try_into().unwrap()}; { + ^***^ diff --git a/crates/compiler/src/plugin/semantics/test_data/set b/crates/compiler/src/plugin/semantics/test_data/set index a6e9918..292e422 100644 --- a/crates/compiler/src/plugin/semantics/test_data/set +++ b/crates/compiler/src/plugin/semantics/test_data/set @@ -84,52 +84,9 @@ Block( statements: [ Expr( StatementExpr { - expr: FunctionCall( - ExprFunctionCall { - function: dojo::model::model::ModelStoreImpl::, test::HealthSerde>, test::HealthDrop>::set, - args: [ - Value( - Var( - LocalVarId(test::world), - ), - ), - Value( - Snapshot( - ExprSnapshot { - inner: StructCtor( - ExprStructCtor { - concrete_struct_id: test::Health, - members: [ - ( - MemberId(test::id), - Literal( - ExprLiteral { - value: 2827, - ty: core::integer::u32, - }, - ), - ), - ( - MemberId(test::health), - Literal( - ExprLiteral { - value: 79, - ty: core::integer::u16, - }, - ), - ), - ], - base_struct: None, - ty: test::Health, - }, - ), - ty: @test::Health, - }, - ), - ), - ], - coupon_arg: None, - ty: (), + expr: Missing( + ExprMissing { + ty: , }, ), }, @@ -141,7 +98,57 @@ Block( ) //! > semantic_diagnostics +error: Identifier not found. + --> lib.cairo:12:1 +set!(world, (Health{id: 0xb0b, health: 79})) +^******************************************^ + warning[E0001]: Unused variable. Consider ignoring by prefixing with `_`. --> lib.cairo:10:22 fn test_func() { let key: felt252 = 0xb0b; ^*^ + +warning[E0001]: Unused variable. Consider ignoring by prefixing with `_`. + --> lib.cairo:11:5 +let world = IWorldDispatcher{contract_address: 0x0.try_into().unwrap()}; { + ^***^ + +error: Identifier not found. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ + +error: Impl not found. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ + +error: Identifier not found. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ + +error: Impl item function `HealthDefinitionImpl::namespace` is not a member of trait `ModelDefinition`. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ + +error: Impl item function `HealthDefinitionImpl::tag` is not a member of trait `ModelDefinition`. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ + +error: Impl item function `HealthDefinitionImpl::selector` is not a member of trait `ModelDefinition`. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ + +error: Impl item function `HealthDefinitionImpl::name_hash` is not a member of trait `ModelDefinition`. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ + +error: Impl item function `HealthDefinitionImpl::namespace_hash` is not a member of trait `ModelDefinition`. + --> lib.cairo:3:1 +#[derive(Copy, Drop, Serde)] +^**************************^ diff --git a/crates/contracts/Scarb.lock b/crates/contracts/Scarb.lock index 788a58a..0dc06ce 100644 --- a/crates/contracts/Scarb.lock +++ b/crates/contracts/Scarb.lock @@ -3,4 +3,12 @@ version = 1 [[package]] name = "dojo" -version = "1.0.0-rc.1" +version = "1.0.0-rc.0" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_plugin" +version = "2.8.4" +source = "git+https://github.com/dojoengine/dojo?branch=feat%2Fdojo-1-rc0#3bf1276163fe9dbfe0f65775de8363e358510c77" diff --git a/crates/contracts/Scarb.toml b/crates/contracts/Scarb.toml index 34946ab..d43256c 100644 --- a/crates/contracts/Scarb.toml +++ b/crates/contracts/Scarb.toml @@ -3,10 +3,14 @@ cairo-version = "=2.8.4" edition = "2024_07" description = "The Dojo Core library for autonomous worlds." name = "dojo" -version = "1.0.0-rc.1" +version = "1.0.0-rc.0" [dependencies] starknet = "=2.8.4" +dojo_plugin = { git = "https://github.com/dojoengine/dojo", branch = "feat/dojo-1-rc0" } + +[dev-dependencies] +cairo_test = "=2.8.4" [lib] diff --git a/crates/contracts/src/contract/components/upgradeable.cairo b/crates/contracts/src/contract/components/upgradeable.cairo index ff552ef..2ae357a 100644 --- a/crates/contracts/src/contract/components/upgradeable.cairo +++ b/crates/contracts/src/contract/components/upgradeable.cairo @@ -30,7 +30,7 @@ pub mod upgradeable_cpt { pub mod Errors { pub const INVALID_CLASS: felt252 = 'class_hash cannot be zero'; - pub const INVALID_CLASS_CONTENT: felt252 = 'class_hash not world provider'; + pub const INVALID_CLASS_CONTENT: felt252 = 'class_hash not Dojo IContract'; pub const INVALID_CALLER: felt252 = 'must be called by world'; pub const INVALID_WORLD_ADDRESS: felt252 = 'invalid world address'; } @@ -41,17 +41,19 @@ pub mod upgradeable_cpt { > of super::IUpgradeable> { fn upgrade(ref self: ComponentState, new_class_hash: ClassHash) { assert( - self.get_contract().world().contract_address.is_non_zero(), + self.get_contract().world_dispatcher().contract_address.is_non_zero(), Errors::INVALID_WORLD_ADDRESS ); assert( - get_caller_address() == self.get_contract().world().contract_address, + get_caller_address() == self.get_contract().world_dispatcher().contract_address, Errors::INVALID_CALLER ); assert(new_class_hash.is_non_zero(), Errors::INVALID_CLASS); + // Seems like the match doesn't catch the error is the entrypoint is + // not found. match starknet::syscalls::library_call_syscall( - new_class_hash, selector!("world"), [].span(), + new_class_hash, selector!("dojo_name"), [].span(), ) { Result::Ok(_) => { replace_class_syscall(new_class_hash).unwrap(); diff --git a/crates/contracts/src/contract/components/world_provider.cairo b/crates/contracts/src/contract/components/world_provider.cairo index efb8bc1..934947d 100644 --- a/crates/contracts/src/contract/components/world_provider.cairo +++ b/crates/contracts/src/contract/components/world_provider.cairo @@ -2,7 +2,7 @@ use dojo::world::IWorldDispatcher; #[starknet::interface] pub trait IWorldProvider { - fn world(self: @T) -> IWorldDispatcher; + fn world_dispatcher(self: @T) -> IWorldDispatcher; } #[starknet::component] @@ -21,7 +21,7 @@ pub mod world_provider_cpt { pub impl WorldProvider< TContractState, +HasComponent > of super::IWorldProvider> { - fn world(self: @ComponentState) -> IWorldDispatcher { + fn world_dispatcher(self: @ComponentState) -> IWorldDispatcher { self.world_dispatcher.read() } } diff --git a/crates/contracts/src/contract/contract.cairo b/crates/contracts/src/contract/contract.cairo deleted file mode 100644 index 0f358d2..0000000 --- a/crates/contracts/src/contract/contract.cairo +++ /dev/null @@ -1,10 +0,0 @@ -#[starknet::interface] -pub trait IContract { - fn name(self: @T) -> ByteArray; - fn namespace(self: @T) -> ByteArray; - fn tag(self: @T) -> ByteArray; - - fn name_hash(self: @T) -> felt252; - fn namespace_hash(self: @T) -> felt252; - fn selector(self: @T) -> felt252; -} diff --git a/crates/contracts/src/contract/interface.cairo b/crates/contracts/src/contract/interface.cairo new file mode 100644 index 0000000..51a952e --- /dev/null +++ b/crates/contracts/src/contract/interface.cairo @@ -0,0 +1,4 @@ +#[starknet::interface] +pub trait IContract { + fn dojo_name(self: @T) -> ByteArray; +} diff --git a/crates/contracts/src/event/event.cairo b/crates/contracts/src/event/event.cairo index 45a0460..16b0825 100644 --- a/crates/contracts/src/event/event.cairo +++ b/crates/contracts/src/event/event.cairo @@ -1,61 +1,23 @@ use dojo::meta::Layout; use dojo::meta::introspect::Ty; -use dojo::world::IWorldDispatcher; #[derive(Drop, Serde, Debug, PartialEq)] pub struct EventDefinition { pub name: ByteArray, - pub namespace: ByteArray, - pub namespace_selector: felt252, pub version: u8, pub layout: Layout, pub schema: Ty } pub trait Event { - fn emit(self: @T, world: IWorldDispatcher); - fn name() -> ByteArray; - fn namespace() -> ByteArray; - fn tag() -> ByteArray; - fn version() -> u8; - - fn selector() -> felt252; - fn instance_selector(self: @T) -> felt252; - - fn name_hash() -> felt252; - fn namespace_hash() -> felt252; - fn definition() -> EventDefinition; - fn layout() -> Layout; fn schema() -> Ty; - fn historical() -> bool; fn keys(self: @T) -> Span; fn values(self: @T) -> Span; -} - -#[starknet::interface] -pub trait IEvent { - fn name(self: @T) -> ByteArray; - fn namespace(self: @T) -> ByteArray; - fn tag(self: @T) -> ByteArray; - - fn version(self: @T) -> u8; - - fn selector(self: @T) -> felt252; - fn name_hash(self: @T) -> felt252; - fn namespace_hash(self: @T) -> felt252; - - fn definition(self: @T) -> EventDefinition; - - fn layout(self: @T) -> Layout; - fn schema(self: @T) -> Ty; -} - -#[cfg(target: "test")] -pub trait EventTest { - fn emit_test(self: @T, world: IWorldDispatcher); + /// Returns the selector of the model computed for the given namespace hash. + fn selector(namespace_hash: felt252) -> felt252; } diff --git a/crates/contracts/src/event/interface.cairo b/crates/contracts/src/event/interface.cairo new file mode 100644 index 0000000..0003657 --- /dev/null +++ b/crates/contracts/src/event/interface.cairo @@ -0,0 +1,13 @@ +use dojo::meta::Layout; +use dojo::meta::introspect::Ty; + +use super::EventDefinition; + +#[starknet::interface] +pub trait IEvent { + fn dojo_name(self: @T) -> ByteArray; + fn version(self: @T) -> u8; + fn definition(self: @T) -> EventDefinition; + fn layout(self: @T) -> Layout; + fn schema(self: @T) -> Ty; +} diff --git a/crates/contracts/src/event/storage.cairo b/crates/contracts/src/event/storage.cairo new file mode 100644 index 0000000..d399fd3 --- /dev/null +++ b/crates/contracts/src/event/storage.cairo @@ -0,0 +1,8 @@ +/// A `EventStorage` trait that abstracts where the storage is and how events are emitted. +pub trait EventStorage { + fn emit_event(ref self: S, event: @E); +} + +pub trait EventStorageTest { + fn emit_event_test(ref self: S, event: @E); +} diff --git a/crates/contracts/src/lib.cairo b/crates/contracts/src/lib.cairo index 9036231..0fa3052 100644 --- a/crates/contracts/src/lib.cairo +++ b/crates/contracts/src/lib.cairo @@ -1,6 +1,6 @@ pub mod contract { - pub mod contract; - pub use contract::{IContract, IContractDispatcher, IContractDispatcherTrait}; + pub mod interface; + pub use interface::{IContract, IContractDispatcher, IContractDispatcherTrait}; pub mod components { pub mod upgradeable; @@ -10,10 +10,13 @@ pub mod contract { pub mod event { pub mod event; - pub use event::{Event, EventDefinition, IEvent, IEventDispatcher, IEventDispatcherTrait}; + pub use event::{Event, EventDefinition}; - #[cfg(target: "test")] - pub use event::{EventTest}; + pub mod interface; + pub use interface::{IEvent, IEventDispatcher, IEventDispatcherTrait}; + + pub mod storage; + pub use storage::{EventStorage, EventStorageTest}; } pub mod meta { @@ -30,56 +33,41 @@ pub mod model { pub mod definition; pub use definition::{ModelIndex, ModelDefinition, ModelDef}; - pub mod members; - pub use members::{MemberStore}; - pub mod model; - pub use model::{Model, ModelStore}; + pub use model::{Model, KeyParser}; - pub mod entity; - pub use entity::{Entity, EntityStore}; + pub mod model_value; + pub use model_value::{ModelValue, ModelValueKey}; pub mod interface; pub use interface::{IModel, IModelDispatcher, IModelDispatcherTrait}; pub mod metadata; - pub use metadata::{ResourceMetadata, resource_metadata}; + pub use metadata::ResourceMetadata; + + pub mod storage; + pub use storage::{ + ModelStorage, ModelMemberStorage, ModelStorageTest, ModelValueStorage, ModelValueStorageTest + }; #[cfg(target: "test")] pub use model::{ModelTest}; #[cfg(target: "test")] - pub use entity::{ModelEntityTest}; + pub use model_value::{ModelValueTest}; } -pub(crate) mod storage { - pub(crate) mod database; - pub(crate) mod packing; - pub(crate) mod layout; - pub(crate) mod storage; - pub(crate) mod entity_model; +pub mod storage { + pub mod database; + pub mod packing; + pub mod layout; + pub mod storage; + pub mod entity_model; } pub mod utils { - // Since Scarb 2.6.0 there's an optimization that does not - // build tests for dependencies and it's not configurable. - // - // To expose correctly the test utils for a package using dojo-core, - // we need to it in the `lib` target or using the `#[cfg(target: "test")]` - // attribute. - // - // Since `test_utils` is using `TEST_CLASS_HASH` to factorize some deployment - // core, we place it under the test target manually. - #[cfg(target: "test")] - pub mod test; - - pub mod descriptor; - pub use descriptor::{ - Descriptor, DescriptorTrait, IDescriptorDispatcher, IDescriptorDispatcherTrait - }; - pub mod hash; - pub use hash::{bytearray_hash, selector_from_names}; + pub use hash::{bytearray_hash, selector_from_names, selector_from_namespace_and_name}; pub mod key; pub use key::{entity_id_from_keys, combine_key, entity_id_from_key}; @@ -114,43 +102,7 @@ pub mod world { mod world_contract; pub use world_contract::world; -} - -#[cfg(test)] -mod tests { - mod meta { - mod introspect; - } - - mod event { - mod event; - } - mod model { - mod model; - } - mod storage { - mod database; - mod packing; - mod storage; - } - mod contract; - mod benchmarks; - mod expanded { - pub(crate) mod selector_attack; - } - mod helpers; - mod world { - mod acl; - mod entities; - mod resources; - mod world; - } - mod utils { - mod hash; - mod key; - mod layout; - mod misc; - mod naming; - } + pub mod storage; + pub use storage::{WorldStorage, WorldStorageTrait}; } diff --git a/crates/contracts/src/model/component.cairo b/crates/contracts/src/model/component.cairo index 5a4b9ef..1a2556d 100644 --- a/crates/contracts/src/model/component.cairo +++ b/crates/contracts/src/model/component.cairo @@ -2,34 +2,14 @@ use dojo::{model::{Model, IModel, ModelDef}, meta::{Layout, Ty}}; #[starknet::embeddable] pub impl IModelImpl> of IModel { - fn name(self: @TContractState) -> ByteArray { + fn dojo_name(self: @TContractState) -> ByteArray { Model::::name() } - fn namespace(self: @TContractState) -> ByteArray { - Model::::namespace() - } - - fn tag(self: @TContractState) -> ByteArray { - Model::::tag() - } - fn version(self: @TContractState) -> u8 { Model::::version() } - fn selector(self: @TContractState) -> felt252 { - Model::::selector() - } - - fn name_hash(self: @TContractState) -> felt252 { - Model::::name_hash() - } - - fn namespace_hash(self: @TContractState) -> felt252 { - Model::::namespace_hash() - } - fn schema(self: @TContractState) -> Ty { Model::::schema() } diff --git a/crates/contracts/src/model/definition.cairo b/crates/contracts/src/model/definition.cairo index 2720dd1..a7b5cc7 100644 --- a/crates/contracts/src/model/definition.cairo +++ b/crates/contracts/src/model/definition.cairo @@ -19,25 +19,17 @@ pub enum ModelIndex { /// Definition of the model containing all the fields that makes up a model. pub trait ModelDefinition { fn name() -> ByteArray; - fn namespace() -> ByteArray; - fn tag() -> ByteArray; fn version() -> u8; - fn selector() -> felt252; - fn name_hash() -> felt252; - fn namespace_hash() -> felt252; fn layout() -> Layout; fn schema() -> Ty; fn size() -> Option; } +/// A plain struct with all the fields of a model definition. #[derive(Drop, Serde, Debug, PartialEq)] pub struct ModelDef { pub name: ByteArray, - pub namespace: ByteArray, pub version: u8, - pub selector: felt252, - pub name_hash: felt252, - pub namespace_hash: felt252, pub layout: Layout, pub schema: Ty, pub packed_size: Option, diff --git a/crates/contracts/src/model/entity.cairo b/crates/contracts/src/model/entity.cairo deleted file mode 100644 index 778867b..0000000 --- a/crates/contracts/src/model/entity.cairo +++ /dev/null @@ -1,203 +0,0 @@ -use dojo::{ - meta::{Layout}, model::{ModelDefinition, ModelIndex, members::{MemberStore},}, - world::{IWorldDispatcher, IWorldDispatcherTrait}, utils::entity_id_from_key, -}; - -pub trait EntityKey {} - -/// Trait `EntityParser` defines the interface for parsing and serializing entities of type `E`. -pub trait EntityParser { - /// Parses and returns the ID of the entity as a `felt252`. - fn parse_id(self: @E) -> felt252; - /// Serializes the values of the entity and returns them as a `Span`. - fn serialize_values(self: @E) -> Span; -} - -/// The `Entity` trait defines a set of methods that must be implemented by any entity type `E`. -/// This trait provides a standardized way to interact with entities, including retrieving their -/// identifiers, values, and metadata, as well as constructing entities from values. -pub trait Entity { - /// Returns the unique identifier of the entity, being a hash derived from the keys. - fn id(self: @E) -> felt252; - /// Returns a span of values associated with the entity, every field of a model - /// that is not a key. - fn values(self: @E) -> Span; - /// Constructs an entity from its identifier and values. - fn from_values(entity_id: felt252, ref values: Span) -> Option; - /// Returns the name of the entity type. - fn name() -> ByteArray; - /// Returns the namespace of the entity type. - fn namespace() -> ByteArray; - /// Returns the tag of the entity type. - fn tag() -> ByteArray; - /// Returns the version of the entity type. - fn version() -> u8; - /// Returns a unique selector for the entity type. - fn selector() -> felt252; - /// Returns the layout of the entity type. - fn layout() -> Layout; - /// Returns the hash of the entity type's name. - fn name_hash() -> felt252; - /// Returns the hash of the entity type's namespace. - fn namespace_hash() -> felt252; - /// Returns a selector for the entity. - fn instance_selector(self: @E) -> felt252; - /// Returns the layout of the entity. - fn instance_layout(self: @E) -> Layout; -} - -/// Trait `EntityStore` provides an interface for managing entities through a world dispatcher. -pub trait EntityStore { - /// Retrieves an entity based on a given key. The key in this context is a types containing - /// all the model keys. - fn get_entity, +Serde, +EntityKey>(self: @IWorldDispatcher, key: K) -> E; - /// Retrieves an entity based on its id. - fn get_entity_from_id(self: @IWorldDispatcher, entity_id: felt252) -> E; - /// Updates an entity in the store. - fn update(self: IWorldDispatcher, entity: @E); - /// Deletes an entity from the store. - fn delete_entity(self: IWorldDispatcher, entity: @E); - /// Deletes an entity based on its id. - fn delete_from_id(self: IWorldDispatcher, entity_id: felt252); - /// Retrieves a member from an entity based on its id and the member's id. - fn get_member_from_id>( - self: @IWorldDispatcher, entity_id: felt252, member_id: felt252 - ) -> T; - /// Updates a member of an entity based on its id and the member's id. - fn update_member_from_id>( - self: IWorldDispatcher, entity_id: felt252, member_id: felt252, value: T - ); -} - -pub impl EntityImpl, +ModelDefinition, +EntityParser> of Entity { - fn id(self: @E) -> felt252 { - EntityParser::::parse_id(self) - } - fn values(self: @E) -> Span { - EntityParser::::serialize_values(self) - } - fn from_values(entity_id: felt252, ref values: Span) -> Option { - let mut serialized: Array = array![entity_id]; - serialized.append_span(values); - let mut span = serialized.span(); - Serde::::deserialize(ref span) - } - fn name() -> ByteArray { - ModelDefinition::::name() - } - fn namespace() -> ByteArray { - ModelDefinition::::namespace() - } - fn tag() -> ByteArray { - ModelDefinition::::tag() - } - fn version() -> u8 { - ModelDefinition::::version() - } - fn selector() -> felt252 { - ModelDefinition::::selector() - } - fn layout() -> Layout { - ModelDefinition::::layout() - } - fn name_hash() -> felt252 { - ModelDefinition::::name_hash() - } - fn namespace_hash() -> felt252 { - ModelDefinition::::namespace_hash() - } - fn instance_selector(self: @E) -> felt252 { - ModelDefinition::::selector() - } - fn instance_layout(self: @E) -> Layout { - ModelDefinition::::layout() - } -} - -pub impl EntityStoreImpl, +Drop> of EntityStore { - fn get_entity, +Serde, +EntityKey>(self: @IWorldDispatcher, key: K) -> E { - Self::get_entity_from_id(self, entity_id_from_key(@key)) - } - - fn get_entity_from_id(self: @IWorldDispatcher, entity_id: felt252) -> E { - let mut values = IWorldDispatcherTrait::entity( - *self, Entity::::selector(), ModelIndex::Id(entity_id), Entity::::layout() - ); - match Entity::::from_values(entity_id, ref values) { - Option::Some(model) => model, - Option::None => { - panic!( - "Entity: deserialization failed. Ensure the length of the keys tuple is matching the number of #[key] fields in the model struct." - ) - } - } - } - - fn update(self: IWorldDispatcher, entity: @E) { - IWorldDispatcherTrait::set_entity( - self, - Entity::::selector(), - ModelIndex::Id(Entity::::id(entity)), - Entity::::values(entity), - Entity::::layout() - ); - } - fn delete_entity(self: IWorldDispatcher, entity: @E) { - Self::delete_from_id(self, Entity::::id(entity)); - } - fn delete_from_id(self: IWorldDispatcher, entity_id: felt252) { - IWorldDispatcherTrait::delete_entity( - self, Entity::::selector(), ModelIndex::Id(entity_id), Entity::::layout() - ); - } - - fn get_member_from_id>( - self: @IWorldDispatcher, entity_id: felt252, member_id: felt252 - ) -> T { - MemberStore::::get_member(self, entity_id, member_id) - } - - fn update_member_from_id>( - self: IWorldDispatcher, entity_id: felt252, member_id: felt252, value: T - ) { - MemberStore::::update_member(self, entity_id, member_id, value); - } -} - -/// Test implementation of the `ModelEntity` trait to bypass permission checks. -#[cfg(target: "test")] -pub trait ModelEntityTest { - fn update_test(self: @E, world: IWorldDispatcher); - fn delete_test(self: @E, world: IWorldDispatcher); -} - -/// Implementation of the `ModelEntityTest` trait for testing purposes, bypassing permission checks. -#[cfg(target: "test")] -pub impl ModelEntityTestImpl> of ModelEntityTest { - fn update_test(self: @E, world: IWorldDispatcher) { - let world_test = dojo::world::IWorldTestDispatcher { - contract_address: world.contract_address - }; - - dojo::world::IWorldTestDispatcherTrait::set_entity_test( - world_test, - Entity::::selector(), - ModelIndex::Id(Entity::::id(self)), - Entity::::values(self), - Entity::::layout() - ); - } - - fn delete_test(self: @E, world: IWorldDispatcher) { - let world_test = dojo::world::IWorldTestDispatcher { - contract_address: world.contract_address - }; - - dojo::world::IWorldTestDispatcherTrait::delete_entity_test( - world_test, - Entity::::selector(), - ModelIndex::Id(Entity::::id(self)), - Entity::::layout() - ); - } -} diff --git a/crates/contracts/src/model/interface.cairo b/crates/contracts/src/model/interface.cairo index 2f8bf32..df33064 100644 --- a/crates/contracts/src/model/interface.cairo +++ b/crates/contracts/src/model/interface.cairo @@ -7,13 +7,8 @@ use dojo::model::ModelDef; /// to interact with deployed models. #[starknet::interface] pub trait IModel { - fn name(self: @T) -> ByteArray; - fn namespace(self: @T) -> ByteArray; - fn tag(self: @T) -> ByteArray; + fn dojo_name(self: @T) -> ByteArray; fn version(self: @T) -> u8; - fn selector(self: @T) -> felt252; - fn name_hash(self: @T) -> felt252; - fn namespace_hash(self: @T) -> felt252; fn layout(self: @T) -> Layout; fn schema(self: @T) -> Ty; fn unpacked_size(self: @T) -> Option; diff --git a/crates/contracts/src/model/members.cairo b/crates/contracts/src/model/members.cairo deleted file mode 100644 index 39536e2..0000000 --- a/crates/contracts/src/model/members.cairo +++ /dev/null @@ -1,85 +0,0 @@ -use dojo::{ - utils::{find_model_field_layout, serialize_inline, deserialize_unwrap}, meta::Layout, - model::{ModelIndex, ModelDefinition}, world::{IWorldDispatcher, IWorldDispatcherTrait} -}; -use core::panic_with_felt252; - -/// The `MemberStore` trait. -/// -/// It provides a standardized way to interact with members of a model. -/// -/// # Template Parameters -/// - `M`: The type of the model. -/// - `T`: The type of the member. -pub trait MemberStore { - /// Retrieves a member of type `T` from a model of type `M` using the provided member id and key - /// of type `K`. - fn get_member(self: @IWorldDispatcher, entity_id: felt252, member_id: felt252) -> T; - /// Updates a member of type `T` within a model of type `M` using the provided member id, key of - /// type `K`, and new value of type `T`. - fn update_member(self: IWorldDispatcher, entity_id: felt252, member_id: felt252, value: T); -} - -/// Updates a serialized member of a model. -fn update_serialized_member( - world: IWorldDispatcher, - model_id: felt252, - layout: Layout, - entity_id: felt252, - member_id: felt252, - values: Span, -) { - match find_model_field_layout(layout, member_id) { - Option::Some(field_layout) => { - IWorldDispatcherTrait::set_entity( - world, model_id, ModelIndex::MemberId((entity_id, member_id)), values, field_layout, - ) - }, - Option::None => panic_with_felt252('bad member id') - } -} - -/// Retrieves a serialized member of a model. -fn get_serialized_member( - world: IWorldDispatcher, - model_id: felt252, - layout: Layout, - entity_id: felt252, - member_id: felt252, -) -> Span { - match find_model_field_layout(layout, member_id) { - Option::Some(field_layout) => { - IWorldDispatcherTrait::entity( - world, model_id, ModelIndex::MemberId((entity_id, member_id)), field_layout - ) - }, - Option::None => panic_with_felt252('bad member id') - } -} - -pub impl MemberStoreImpl, +Serde, +Drop> of MemberStore { - fn get_member(self: @IWorldDispatcher, entity_id: felt252, member_id: felt252) -> T { - deserialize_unwrap::< - T - >( - get_serialized_member( - *self, - ModelDefinition::::selector(), - ModelDefinition::::layout(), - entity_id, - member_id, - ) - ) - } - fn update_member(self: IWorldDispatcher, entity_id: felt252, member_id: felt252, value: T,) { - update_serialized_member( - self, - ModelDefinition::::selector(), - ModelDefinition::::layout(), - entity_id, - member_id, - serialize_inline::(@value) - ) - } -} - diff --git a/crates/contracts/src/model/metadata.cairo b/crates/contracts/src/model/metadata.cairo index 64fb049..512d4c1 100644 --- a/crates/contracts/src/model/metadata.cairo +++ b/crates/contracts/src/model/metadata.cairo @@ -1,184 +1,26 @@ //! ResourceMetadata model. //! -//! Manually expand to ensure that dojo-core -//! does not depend on dojo plugin to be built. -//! -use core::poseidon::poseidon_hash_span; - -use dojo::model::model::{ModelImpl, ModelParser, KeyParser}; -use dojo::meta::introspect::{Introspect, Ty, Struct, Member}; -use dojo::meta::{Layout, FieldLayout}; +use dojo::model::model::Model; use dojo::utils; -use dojo::utils::{serialize_inline}; - -pub fn initial_address() -> starknet::ContractAddress { - starknet::contract_address_const::<0>() -} -pub fn initial_class_hash() -> starknet::ClassHash { - starknet::class_hash::class_hash_const::< - 0x03f75587469e8101729b3b02a46150a3d99315bc9c5026d64f2e8a061e413255 - >() -} - -#[derive(Drop, Serde, PartialEq, Clone, Debug)] +#[derive(Introspect, Drop, Serde, PartialEq, Clone, Debug)] +#[dojo::model] pub struct ResourceMetadata { - // #[key] + #[key] pub resource_id: felt252, pub metadata_uri: ByteArray, } -pub impl ResourceMetadataDefinitionImpl of dojo::model::ModelDefinition { - #[inline(always)] - fn name() -> ByteArray { - "ResourceMetadata" - } - - #[inline(always)] - fn namespace() -> ByteArray { - "__DOJO__" - } - - #[inline(always)] - fn tag() -> ByteArray { - "__DOJO__-ResourceMetadata" - } - - #[inline(always)] - fn version() -> u8 { - 1 - } - - #[inline(always)] - fn selector() -> felt252 { - poseidon_hash_span([Self::namespace_hash(), Self::name_hash()].span()) - } - - #[inline(always)] - fn name_hash() -> felt252 { - utils::bytearray_hash(@Self::name()) - } - - #[inline(always)] - fn namespace_hash() -> felt252 { - utils::bytearray_hash(@Self::namespace()) - } - - #[inline(always)] - fn layout() -> Layout { - Introspect::::layout() - } - - #[inline(always)] - fn schema() -> Ty { - Introspect::::ty() - } - - #[inline(always)] - fn size() -> Option { - Introspect::::size() - } -} - - -pub impl ResourceMetadataModelKeyImpl of KeyParser { - #[inline(always)] - fn parse_key(self: @ResourceMetadata) -> felt252 { - *self.resource_id - } -} - -pub impl ResourceMetadataModelParser of ModelParser { - fn serialize_keys(self: @ResourceMetadata) -> Span { - [*self.resource_id].span() - } - fn serialize_values(self: @ResourceMetadata) -> Span { - serialize_inline(self.metadata_uri) - } +pub fn default_address() -> starknet::ContractAddress { + starknet::contract_address_const::<0>() } -pub impl ResourceMetadataModelImpl = ModelImpl; - - -pub impl ResourceMetadataIntrospect<> of Introspect> { - #[inline(always)] - fn size() -> Option { - Option::None - } - - #[inline(always)] - fn layout() -> Layout { - Layout::Struct( - [FieldLayout { selector: selector!("metadata_uri"), layout: Layout::ByteArray }].span() - ) - } - - #[inline(always)] - fn ty() -> Ty { - Ty::Struct( - Struct { - name: 'ResourceMetadata', attrs: [].span(), children: [ - Member { - name: 'resource_id', ty: Ty::Primitive('felt252'), attrs: ['key'].span() - }, - Member { name: 'metadata_uri', ty: Ty::ByteArray, attrs: [].span() } - ].span() - } - ) - } +pub fn default_class_hash() -> starknet::ClassHash { + starknet::class_hash::class_hash_const::<0>() } -#[starknet::contract] -pub mod resource_metadata { - use super::{ResourceMetadata}; - - use dojo::{meta::{Layout, Ty}, model::{ModelDef, Model}}; - - #[storage] - struct Storage {} - - #[external(v0)] - fn selector(self: @ContractState) -> felt252 { - Model::::selector() - } - - fn name(self: @ContractState) -> ByteArray { - Model::::name() - } - - fn version(self: @ContractState) -> u8 { - Model::::version() - } - - fn namespace(self: @ContractState) -> ByteArray { - Model::::namespace() - } - - #[external(v0)] - fn unpacked_size(self: @ContractState) -> Option { - Model::::unpacked_size() - } - - #[external(v0)] - fn packed_size(self: @ContractState) -> Option { - Model::::packed_size() - } - - #[external(v0)] - fn layout(self: @ContractState) -> Layout { - Model::::layout() - } - - #[external(v0)] - fn schema(self: @ContractState) -> Ty { - Model::::schema() - } - - #[external(v0)] - fn definition(self: @ContractState) -> ModelDef { - Model::::definition() - } - - #[external(v0)] - fn ensure_abi(self: @ContractState, model: ResourceMetadata) {} +pub fn resource_metadata_selector(default_namespace_hash: felt252) -> felt252 { + utils::selector_from_namespace_and_name( + default_namespace_hash, @Model::::name() + ) } diff --git a/crates/contracts/src/model/model.cairo b/crates/contracts/src/model/model.cairo index 2ee2f9a..9a6d941 100644 --- a/crates/contracts/src/model/model.cairo +++ b/crates/contracts/src/model/model.cairo @@ -1,9 +1,6 @@ -use dojo::{ - world::{IWorldDispatcher, IWorldDispatcherTrait}, - meta::{Layout, introspect::Ty, layout::compute_packed_size}, - model::{ModelDefinition, ModelDef, ModelIndex, members::MemberStore}, - utils::{entity_id_from_key, serialize_inline, entity_id_from_keys} -}; +use dojo::{meta::{Layout, introspect::Ty, layout::compute_packed_size}, utils::entity_id_from_keys}; + +use super::{ModelDefinition, ModelDef}; /// Trait `KeyParser` defines a trait for parsing keys from a given model. pub trait KeyParser { @@ -33,20 +30,11 @@ pub trait Model { fn values(self: @M) -> Span; /// Constructs a model from the given keys and values. fn from_values(ref keys: Span, ref values: Span) -> Option; - /// Returns the name of the model. + /// Returns the name of the model. (TODO: internalizing the name_hash could reduce poseidon + /// costs). fn name() -> ByteArray; - /// Returns the namespace of the model. - fn namespace() -> ByteArray; - /// Returns the tag of the model. - fn tag() -> ByteArray; /// Returns the version of the model. fn version() -> u8; - /// Returns the selector of the model. - fn selector() -> felt252; - /// Returns the name hash of the model. - fn name_hash() -> felt252; - /// Returns the namespace hash of the model. - fn namespace_hash() -> felt252; /// Returns the schema of the model. fn schema() -> Ty; /// Returns the memory layout of the model. @@ -56,38 +44,11 @@ pub trait Model { /// Returns the packed size of the model. Only applicable for fixed size models. fn packed_size() -> Option; /// Returns the instance selector of the model. - fn instance_selector(self: @M) -> felt252; - /// Returns the instance layout of the model. fn instance_layout(self: @M) -> Layout; /// Returns the definition of the model. fn definition() -> ModelDef; -} - -/// The `ModelStore` trait defines a set of methods for managing models through a world dispatcher. -/// -/// # Type Parameters -/// - `M`: The type of the model. -/// - `K`: The type of the key used to identify models. -/// - `T`: The type of the member within the model. -pub trait ModelStore { - /// Retrieves a model of type `M` using the provided key of type `K`. - fn get, +Serde>(self: @IWorldDispatcher, key: K) -> M; - /// Sets a model of type `M`. - fn set(self: IWorldDispatcher, model: @M); - /// Deletes a model of type `M`. - fn delete(self: IWorldDispatcher, model: @M); - /// Deletes a model of type `M` using the provided key of type `K`. - fn delete_from_key, +Serde>(self: IWorldDispatcher, key: K); - /// Retrieves a member of type `T` from a model of type `M` using the provided member id and key - /// of type `K`. - fn get_member, +Drop, +Drop, +Serde>( - self: @IWorldDispatcher, key: K, member_id: felt252 - ) -> T; - /// Updates a member of type `T` within a model of type `M` using the provided member id, key of - /// type `K`, and new value of type `T`. - fn update_member, +Drop, +Drop, +Serde>( - self: IWorldDispatcher, key: K, member_id: felt252, value: T - ); + /// Returns the selector of the model computed for the given namespace hash. + fn selector(namespace_hash: felt252) -> felt252; } pub impl ModelImpl, +ModelDefinition, +Serde> of Model { @@ -119,30 +80,14 @@ pub impl ModelImpl, +ModelDefinition, +Serde> of Model< ModelDefinition::::name() } - fn namespace() -> ByteArray { - ModelDefinition::::namespace() - } - - fn tag() -> ByteArray { - ModelDefinition::::tag() + fn selector(namespace_hash: felt252) -> felt252 { + dojo::utils::selector_from_namespace_and_name(namespace_hash, @Self::name()) } fn version() -> u8 { ModelDefinition::::version() } - fn selector() -> felt252 { - ModelDefinition::::selector() - } - - fn name_hash() -> felt252 { - ModelDefinition::::name_hash() - } - - fn namespace_hash() -> felt252 { - ModelDefinition::::namespace_hash() - } - fn layout() -> Layout { ModelDefinition::::layout() } @@ -159,10 +104,6 @@ pub impl ModelImpl, +ModelDefinition, +Serde> of Model< compute_packed_size(ModelDefinition::::layout()) } - fn instance_selector(self: @M) -> felt252 { - ModelDefinition::::selector() - } - fn instance_layout(self: @M) -> Layout { ModelDefinition::::layout() } @@ -170,11 +111,7 @@ pub impl ModelImpl, +ModelDefinition, +Serde> of Model< fn definition() -> ModelDef { ModelDef { name: Self::name(), - namespace: Self::namespace(), version: Self::version(), - selector: Self::selector(), - name_hash: Self::name_hash(), - namespace_hash: Self::namespace_hash(), layout: Self::layout(), schema: Self::schema(), packed_size: Self::packed_size(), @@ -183,99 +120,24 @@ pub impl ModelImpl, +ModelDefinition, +Serde> of Model< } } -pub impl ModelStoreImpl, +Drop> of ModelStore { - fn get, +Serde>(self: @IWorldDispatcher, key: K) -> M { - let mut keys = serialize_inline::(@key); - let mut values = IWorldDispatcherTrait::entity( - *self, Model::::selector(), ModelIndex::Keys(keys), Model::::layout() - ); - match Model::::from_values(ref keys, ref values) { - Option::Some(model) => model, - Option::None => { - panic!( - "Model: deserialization failed. Ensure the length of the keys tuple is matching the number of #[key] fields in the model struct." - ) - } - } - } - - fn set(self: IWorldDispatcher, model: @M) { - IWorldDispatcherTrait::set_entity( - self, - Model::::selector(), - ModelIndex::Keys(Model::::keys(model)), - Model::::values(model), - Model::::layout() - ); - } - - fn delete(self: IWorldDispatcher, model: @M) { - IWorldDispatcherTrait::delete_entity( - self, - Model::::selector(), - ModelIndex::Keys(Model::::keys(model)), - Model::::layout() - ); - } - - fn delete_from_key, +Serde>(self: IWorldDispatcher, key: K) { - IWorldDispatcherTrait::delete_entity( - self, - Model::::selector(), - ModelIndex::Keys(serialize_inline::(@key)), - Model::::layout() - ); - } - - fn get_member, +Drop, +Drop, +Serde>( - self: @IWorldDispatcher, key: K, member_id: felt252 - ) -> T { - MemberStore::::get_member(self, entity_id_from_key::(@key), member_id) - } - - fn update_member, +Drop, +Drop, +Serde>( - self: IWorldDispatcher, key: K, member_id: felt252, value: T - ) { - MemberStore::::update_member(self, entity_id_from_key::(@key), member_id, value); - } -} - /// The `ModelTest` trait. /// /// It provides a standardized way to interact with models for testing purposes, /// bypassing the permission checks. #[cfg(target: "test")] -pub trait ModelTest { - fn set_test(self: @T, world: IWorldDispatcher); - fn delete_test(self: @T, world: IWorldDispatcher); +pub trait ModelTest { + fn set_model_test(ref self: S, model: @M); + fn delete_model_test(ref self: S, model: @M); } /// The `ModelTestImpl` implementation for testing purposes. #[cfg(target: "test")] -pub impl ModelTestImpl> of ModelTest { - fn set_test(self: @M, world: dojo::world::IWorldDispatcher) { - let world_test = dojo::world::IWorldTestDispatcher { - contract_address: world.contract_address - }; - dojo::world::IWorldTestDispatcherTrait::set_entity_test( - world_test, - Model::::selector(), - ModelIndex::Keys(Model::keys(self)), - Model::::values(self), - Model::::layout() - ); +pub impl ModelTestImpl, +Model> of ModelTest { + fn set_model_test(ref self: S, model: @M) { + dojo::model::ModelStorageTest::::write_model_test(ref self, model); } - fn delete_test(self: @M, world: dojo::world::IWorldDispatcher) { - let world_test = dojo::world::IWorldTestDispatcher { - contract_address: world.contract_address - }; - - dojo::world::IWorldTestDispatcherTrait::delete_entity_test( - world_test, - Model::::selector(), - ModelIndex::Keys(Model::keys(self)), - Model::::layout() - ); + fn delete_model_test(ref self: S, model: @M) { + dojo::model::ModelStorageTest::::erase_model_test(ref self, model); } } diff --git a/crates/contracts/src/model/model_value.cairo b/crates/contracts/src/model/model_value.cairo new file mode 100644 index 0000000..17f2141 --- /dev/null +++ b/crates/contracts/src/model/model_value.cairo @@ -0,0 +1,86 @@ +use dojo::{meta::{Layout}, model::{ModelDefinition},}; + +pub trait ModelValueKey {} + +/// Trait `ModelValueParser` defines the interface for parsing and serializing entities of type `V`. +pub trait ModelValueParser { + /// Serializes the values of the model and returns them as a `Span`. + fn serialize_values(self: @V) -> Span; +} + +/// The `ModelValue` trait defines a set of methods that must be implemented by any model value type +/// `V`. +pub trait ModelValue { + /// Returns a span of values associated with the entity, every field of a model + /// that is not a key. + fn values(self: @V) -> Span; + /// Constructs a model value from its identifier and values. + fn from_values(entity_id: felt252, ref values: Span) -> Option; + /// Returns the name of the model value type. + fn name() -> ByteArray; + /// Returns the version of the model value type. + fn version() -> u8; + /// Returns the layout of the model value type. + fn layout() -> Layout; + /// Returns the layout of the model value. + fn instance_layout(self: @V) -> Layout; + /// Returns the selector of the model value type with the given namespace hash. + fn selector(namespace_hash: felt252) -> felt252; +} + +pub impl ModelValueImpl, +ModelDefinition, +ModelValueParser> of ModelValue { + fn values(self: @V) -> Span { + ModelValueParser::::serialize_values(self) + } + + fn from_values(entity_id: felt252, ref values: Span) -> Option { + let mut serialized: Array = array![entity_id]; + serialized.append_span(values); + let mut span = serialized.span(); + Serde::::deserialize(ref span) + } + + fn name() -> ByteArray { + ModelDefinition::::name() + } + + fn version() -> u8 { + ModelDefinition::::version() + } + + fn layout() -> Layout { + ModelDefinition::::layout() + } + + fn instance_layout(self: @V) -> Layout { + ModelDefinition::::layout() + } + + fn selector(namespace_hash: felt252) -> felt252 { + dojo::utils::selector_from_namespace_and_name(namespace_hash, @Self::name()) + } +} + + +/// Test implementation of the `ModelValueTest` trait to bypass permission checks. +#[cfg(target: "test")] +pub trait ModelValueTest { + fn update_test(ref self: S, entity_id: felt252, value: @V); + fn delete_test(ref self: S, entity_id: felt252); +} + +/// Implementation of the `ModelValueTest` trait for testing purposes, bypassing permission checks. +#[cfg(target: "test")] +pub impl ModelValueTestImpl< + S, V, +super::storage::ModelValueStorageTest, +ModelValue +> of ModelValueTest { + fn update_test(ref self: S, entity_id: felt252, value: @V) { + super::storage::ModelValueStorageTest::< + S, V + >::write_model_value_test(ref self, entity_id, value) + } + + fn delete_test(ref self: S, entity_id: felt252) { + super::storage::ModelValueStorageTest::::erase_model_value_test(ref self, entity_id) + } +} diff --git a/crates/contracts/src/model/storage.cairo b/crates/contracts/src/model/storage.cairo new file mode 100644 index 0000000..46d1131 --- /dev/null +++ b/crates/contracts/src/model/storage.cairo @@ -0,0 +1,86 @@ +use dojo::model::model_value::ModelValueKey; + +/// A `ModelStorage` trait that abstracts where the storage is. +/// +/// Currently it's only world storage, but this will be useful when we have other +/// storage solutions (micro worlds). +pub trait ModelStorage { + /// Sets a model of type `M`. + fn write_model(ref self: S, model: @M); + + /// Retrieves a model of type `M` using the provided key of type `K`. + fn read_model, +Serde>(self: @S, key: K) -> M; + + /// Deletes a model of type `M`. + fn erase_model(ref self: S, model: @M); + + /// Deletes a model of type `M` using the provided key of type `K`. + fn erase_model_from_key, +Serde>(ref self: S, key: K); + + /// Deletes a model of type `M` using the provided entity id. + fn erase_model_from_id(ref self: S, entity_id: felt252); + + /// Retrieves a member of type `T` from a model of type `M` using the provided member id and key + /// of type `K`. + fn read_member, +Drop, +Drop, +Serde>( + self: @S, key: K, member_id: felt252 + ) -> T; + + /// Updates a member of type `T` within a model of type `M` using the provided member id, key of + /// type `K`, and new value of type `T`. + fn write_member, +Drop, +Drop, +Serde>( + ref self: S, key: K, member_id: felt252, value: T + ); + + /// Returns the current namespace hash. + fn namespace_hash(self: @S) -> felt252; +} + +/// A `ModelMemberStorage` trait that abstracts where the storage is. +pub trait ModelMemberStorage { + /// Retrieves a member of type `T` for the given entity id and member id. + fn read_member_from_id(self: @S, entity_id: felt252, member_id: felt252) -> T; + + /// Updates a member of type `T` for the given entity id and member id. + fn write_member_from_id(ref self: S, entity_id: felt252, member_id: felt252, value: T); + + /// Returns the current namespace hash. + fn namespace_hash(self: @S) -> felt252; +} + +/// A `ModelValueStorage` trait that abstracts where the storage is. +pub trait ModelValueStorage { + /// Retrieves a model value of type `V` using the provided key of type `K`. + fn read_model_value, +Serde, +ModelValueKey>(self: @S, key: K) -> V; + + /// Retrieves a model value of type `V` using the provided entity id. + fn read_model_value_from_id(self: @S, entity_id: felt252) -> V; + + /// Updates a model value of type `V`. + fn write_model_value, +Serde, +ModelValueKey>( + ref self: S, key: K, value: @V + ); + + /// Updates a model value of type `V`. + fn write_model_value_from_id(ref self: S, entity_id: felt252, value: @V); +} + +/// A `ModelStorage` trait that abstracts where the storage is. +/// +/// Currently it's only world storage, but this will be useful when we have other +/// storage solutions (micro worlds). +pub trait ModelStorageTest { + /// Sets a model of type `M`. + fn write_model_test(ref self: S, model: @M); + /// Deletes a model of type `M`. + fn erase_model_test(ref self: S, model: @M); +} + +/// A `ModelValueStorageTest` trait that abstracts where the storage is and bypass the permission +/// checks. +pub trait ModelValueStorageTest { + /// Updates a model value of type `V`. + fn write_model_value_test(ref self: S, entity_id: felt252, value: @V); + /// Deletes a model value of type `V`. + fn erase_model_value_test(ref self: S, entity_id: felt252); +} diff --git a/crates/contracts/src/tests/contract.cairo b/crates/contracts/src/tests/contract.cairo deleted file mode 100644 index f20a72e..0000000 --- a/crates/contracts/src/tests/contract.cairo +++ /dev/null @@ -1,309 +0,0 @@ -use core::option::OptionTrait; -use core::traits::TryInto; - -use starknet::ClassHash; - -use dojo::contract::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; -use dojo::utils::test::{spawn_test_world}; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -#[starknet::contract] -pub mod contract_invalid_upgrade { - use dojo::contract::IContract; - - #[storage] - struct Storage {} - - #[abi(embed_v0)] - pub impl ContractImpl of IContract { - fn name(self: @ContractState) -> ByteArray { - "test_contract" - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn tag(self: @ContractState) -> ByteArray { - "dojo-test_contract" - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::namespace(self)) - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::name(self)) - } - - fn selector(self: @ContractState) -> felt252 { - selector_from_tag!("dojo-test_contract") - } - } -} - -#[dojo::contract] -mod test_contract {} - -#[starknet::interface] -pub trait IQuantumLeap { - fn plz_more_tps(self: @T) -> felt252; -} - -#[starknet::contract] -pub mod test_contract_upgrade { - use dojo::contract::IContract; - use dojo::world::IWorldDispatcher; - use dojo::contract::components::world_provider::IWorldProvider; - - #[storage] - struct Storage {} - - #[constructor] - fn constructor(ref self: ContractState) {} - - #[abi(embed_v0)] - pub impl QuantumLeap of super::IQuantumLeap { - fn plz_more_tps(self: @ContractState) -> felt252 { - 'daddy' - } - } - - #[abi(embed_v0)] - pub impl WorldProviderImpl of IWorldProvider { - fn world(self: @ContractState) -> IWorldDispatcher { - IWorldDispatcher { contract_address: starknet::contract_address_const::<'world'>() } - } - } - - #[abi(embed_v0)] - pub impl ContractImpl of IContract { - fn name(self: @ContractState) -> ByteArray { - "test_contract" - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn tag(self: @ContractState) -> ByteArray { - "dojo-test_contract" - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::namespace(self)) - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::name(self)) - } - - fn selector(self: @ContractState) -> felt252 { - selector_from_tag!("dojo-test_contract") - } - } -} - -// Utils -fn deploy_world() -> IWorldDispatcher { - spawn_test_world(["dojo"].span(), [].span()) -} - -#[test] -#[available_gas(7000000)] -fn test_upgrade_from_world() { - let world = deploy_world(); - - let base_address = world - .register_contract('salt', test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - - world.upgrade_contract(new_class_hash); - - let quantum_dispatcher = IQuantumLeapDispatcher { contract_address: base_address }; - assert(quantum_dispatcher.plz_more_tps() == 'daddy', 'quantum leap failed'); -} - -#[test] -#[available_gas(7000000)] -#[should_panic( - expected: ('class_hash not world provider', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED') -)] -fn test_upgrade_from_world_not_world_provider() { - let world = deploy_world(); - - let _ = world.register_contract('salt', test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = contract_invalid_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - - world.upgrade_contract(new_class_hash); -} - -#[test] -#[available_gas(6000000)] -#[should_panic(expected: ('must be called by world', 'ENTRYPOINT_FAILED'))] -fn test_upgrade_direct() { - let world = deploy_world(); - - let base_address = world - .register_contract('salt', test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - - let upgradeable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; - upgradeable_dispatcher.upgrade(new_class_hash); -} - -#[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; - fn name_hash(self: @T) -> felt252; -} - -#[starknet::contract] -mod invalid_legacy_model { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelMetadata of super::IMetadataOnly { - fn selector(self: @ContractState) -> felt252 { - // Pre-computed address of a contract deployed through the world. - 0x1b1edb46931b1a98d8c6ecf2703e8483ec1d85fb75b3e9c061eab383fc8f8f1 - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::namespace(self)) - } - - fn name(self: @ContractState) -> ByteArray { - "invalid_legacy_model" - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::name(self)) - } - } -} - -#[starknet::contract] -mod invalid_legacy_model_world { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelName of super::IMetadataOnly { - fn selector(self: @ContractState) -> felt252 { - // World address is 0, and not registered as deployed through the world - // as it's itself. - 0 - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::namespace(self)) - } - - fn name(self: @ContractState) -> ByteArray { - "invalid_legacy_model" - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::name(self)) - } - } -} - -#[starknet::contract] -mod invalid_model { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelSelector of super::IMetadataOnly { - fn selector(self: @ContractState) -> felt252 { - // Use the resource identifier of the contract deployed through the world - // instead of the address. - selector_from_tag!("dojo-test_contract") - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::namespace(self)) - } - - fn name(self: @ContractState) -> ByteArray { - "invalid_model" - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::name(self)) - } - } -} - -#[starknet::contract] -mod invalid_model_world { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelSelector of super::IMetadataOnly { - fn selector(self: @ContractState) -> felt252 { - // World address is 0, and not registered as deployed through the world - // as it's itself. - 0 - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::namespace(self)) - } - - fn name(self: @ContractState) -> ByteArray { - "invalid_model_world" - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@Self::name(self)) - } - } -} - -#[test] -#[available_gas(60000000)] -#[should_panic( - expected: ( - "Descriptor: `selector` mismatch, expected `926629585226688883233756580070288922289294279106806075757077946233183245741` but found `2368393732245529956313345237151518608283468650081902115301417183793437311044`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_deploy_from_world_invalid_model() { - let world = deploy_world(); - - let _ = world.register_contract(0, test_contract::TEST_CLASS_HASH.try_into().unwrap()); - - world.register_model(invalid_model::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[available_gas(6000000)] -#[should_panic(expected: ("Descriptor: selector `0` is a reserved selector", 'ENTRYPOINT_FAILED',))] -fn test_deploy_from_world_invalid_model_descriptor() { - let world = deploy_world(); - world.register_model(invalid_model_world::TEST_CLASS_HASH.try_into().unwrap()); -} diff --git a/crates/contracts/src/tests/helpers.cairo b/crates/contracts/src/tests/helpers.cairo deleted file mode 100644 index 9bb3bd0..0000000 --- a/crates/contracts/src/tests/helpers.cairo +++ /dev/null @@ -1,383 +0,0 @@ -use starknet::ContractAddress; - -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - -use dojo::model::Model; -use dojo::utils::test::{deploy_with_world_address, spawn_test_world}; - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -pub struct SimpleEvent { - #[key] - pub id: u32, - pub data: (felt252, felt252), -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] -pub struct Foo { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[starknet::contract] -pub mod foo_invalid_name { - use dojo::model::IModel; - - #[storage] - struct Storage {} - - #[abi(embed_v0)] - pub impl ModelImpl of IModel { - fn name(self: @ContractState) -> ByteArray { - "foo-bis" - } - - fn namespace(self: @ContractState) -> ByteArray { - "dojo" - } - - fn tag(self: @ContractState) -> ByteArray { - "dojo-foo-bis" - } - - fn version(self: @ContractState) -> u8 { - 1 - } - - fn selector(self: @ContractState) -> felt252 { - dojo::utils::selector_from_names(@"dojo", @"foo-bis") - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@"foo-bis") - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@"dojo") - } - - fn unpacked_size(self: @ContractState) -> Option { - Option::None - } - - fn packed_size(self: @ContractState) -> Option { - Option::None - } - - fn layout(self: @ContractState) -> dojo::meta::Layout { - dojo::meta::Layout::Fixed([].span()) - } - - fn schema(self: @ContractState) -> dojo::meta::introspect::Ty { - dojo::meta::introspect::Ty::Struct( - dojo::meta::introspect::Struct { - name: 'foo', attrs: [].span(), children: [].span() - } - ) - } - - fn definition(self: @ContractState) -> dojo::model::ModelDef { - dojo::model::ModelDef { - name: Self::name(self), - namespace: Self::namespace(self), - version: Self::version(self), - selector: Self::selector(self), - name_hash: Self::name_hash(self), - namespace_hash: Self::namespace_hash(self), - layout: Self::layout(self), - schema: Self::schema(self), - packed_size: Self::packed_size(self), - unpacked_size: Self::unpacked_size(self), - } - } - } -} - - -#[starknet::contract] -pub mod foo_invalid_namespace { - use dojo::model::IModel; - - #[storage] - struct Storage {} - - #[abi(embed_v0)] - pub impl ModelImpl of IModel { - fn name(self: @ContractState) -> ByteArray { - "foo" - } - - fn namespace(self: @ContractState) -> ByteArray { - "inv@lid n@mesp@ce" - } - - fn tag(self: @ContractState) -> ByteArray { - "inv@lid n@mesp@ce-foo" - } - - fn version(self: @ContractState) -> u8 { - 1 - } - - fn selector(self: @ContractState) -> felt252 { - dojo::utils::selector_from_names(@"inv@lid n@mesp@ce", @"foo") - } - - fn name_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@"foo") - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@"inv@lid n@mesp@ce") - } - - fn unpacked_size(self: @ContractState) -> Option { - Option::None - } - - fn packed_size(self: @ContractState) -> Option { - Option::None - } - - fn layout(self: @ContractState) -> dojo::meta::Layout { - dojo::meta::Layout::Fixed([].span()) - } - - fn schema(self: @ContractState) -> dojo::meta::introspect::Ty { - dojo::meta::introspect::Ty::Struct( - dojo::meta::introspect::Struct { - name: 'foo', attrs: [].span(), children: [].span() - } - ) - } - - fn definition(self: @ContractState) -> dojo::model::ModelDef { - dojo::model::ModelDef { - name: Self::name(self), - namespace: Self::namespace(self), - version: Self::version(self), - selector: Self::selector(self), - name_hash: Self::name_hash(self), - namespace_hash: Self::namespace_hash(self), - layout: Self::layout(self), - schema: Self::schema(self), - packed_size: Self::packed_size(self), - unpacked_size: Self::unpacked_size(self), - } - } - } -} - - -#[derive(Copy, Drop, Serde)] -#[dojo::model(namespace: "another_namespace")] -pub struct Buzz { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[dojo::interface] -pub trait IFooSetter { - fn set_foo(ref world: IWorldDispatcher, a: felt252, b: u128); -} - -#[dojo::contract] -pub mod foo_setter { - use super::{Foo, IFooSetter}; - - #[abi(embed_v0)] - impl IFooSetterImpl of IFooSetter { - fn set_foo(ref world: IWorldDispatcher, a: felt252, b: u128) { - set!(world, (Foo { caller: starknet::get_caller_address(), a, b })); - } - } -} - -#[dojo::contract] -pub mod test_contract {} - -#[dojo::contract] -pub mod test_contract_with_dojo_init_args { - use dojo::world::IWorldDispatcherTrait; - - fn dojo_init(ref world: IWorldDispatcher, _arg1: felt252) { - let _a = world.uuid(); - } -} - -#[dojo::contract(namespace: "buzz_namespace")] -pub mod buzz_contract {} - -#[derive(IntrospectPacked, Copy, Drop, Serde)] -pub struct Sword { - pub swordsmith: ContractAddress, - pub damage: u32, -} - -#[derive(IntrospectPacked, Copy, Drop, Serde)] -#[dojo::model] -pub struct Case { - #[key] - pub owner: ContractAddress, - pub sword: Sword, - pub material: felt252, -} - -#[derive(IntrospectPacked, Copy, Drop, Serde)] -#[dojo::model] -pub struct Character { - #[key] - pub caller: ContractAddress, - pub heigth: felt252, - pub abilities: Abilities, - pub stats: Stats, - pub weapon: Weapon, - pub gold: u32, -} - -#[derive(IntrospectPacked, Copy, Drop, Serde)] -pub struct Abilities { - pub strength: u8, - pub dexterity: u8, - pub constitution: u8, - pub intelligence: u8, - pub wisdom: u8, - pub charisma: u8, -} - -#[derive(IntrospectPacked, Copy, Drop, Serde)] -pub struct Stats { - pub kills: u128, - pub deaths: u16, - pub rests: u32, - pub hits: u64, - pub blocks: u32, - pub walked: felt252, - pub runned: felt252, - pub finished: bool, - pub romances: u16, -} - -#[derive(IntrospectPacked, Copy, Drop, Serde)] -pub enum Weapon { - DualWield: (Sword, Sword), - Fists: (Sword, Sword), // Introspect requires same arms -} - -#[starknet::interface] -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] -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::{Foo, IWorldDispatcher, IWorldDispatcherTrait}; - use super::{Character, Abilities, Stats, Weapon, Sword}; - - #[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 delete_foo(self: @ContractState) { - self - .world - .read() - .delete_entity( - Model::::selector(), - 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 }); - } - - fn set_char(self: @ContractState, a: felt252, b: u32) { - set!( - self.world.read(), - Character { - caller: get_caller_address(), - heigth: a, - abilities: Abilities { - strength: 0x12, - dexterity: 0x34, - constitution: 0x56, - intelligence: 0x78, - wisdom: 0x9a, - charisma: 0xbc, - }, - stats: Stats { - kills: 0x123456789abcdef, - deaths: 0x1234, - rests: 0x12345678, - hits: 0x123456789abcdef, - blocks: 0x12345678, - walked: 0x123456789abcdef, - runned: 0x123456789abcdef, - finished: true, - romances: 0x1234, - }, - weapon: Weapon::DualWield( - ( - Sword { swordsmith: get_caller_address(), damage: 0x12345678, }, - Sword { swordsmith: get_caller_address(), damage: 0x12345678, } - ) - ), - gold: b, - } - ); - } - } -} - -pub fn deploy_world() -> IWorldDispatcher { - spawn_test_world(["dojo"].span(), [].span()) -} - -pub fn deploy_world_and_bar() -> (IWorldDispatcher, IbarDispatcher) { - // Spawn empty world - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - // System contract - 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); - - (world, bar_contract) -} - -pub fn drop_all_events(address: ContractAddress) { - loop { - match starknet::testing::pop_log_raw(address) { - core::option::Option::Some(_) => {}, - core::option::Option::None => { break; }, - }; - } -} diff --git a/crates/contracts/src/tests/model/model.cairo b/crates/contracts/src/tests/model/model.cairo deleted file mode 100644 index 4061b60..0000000 --- a/crates/contracts/src/tests/model/model.cairo +++ /dev/null @@ -1,223 +0,0 @@ -use dojo::model::{Model, Entity, ModelStore, EntityStore}; -use dojo::world::{IWorldDispatcherTrait}; - -use dojo::tests::helpers::{deploy_world}; - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] -struct Foo { - #[key] - k1: u8, - #[key] - k2: felt252, - v1: u128, - v2: u32 -} - - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::model] -struct Foo2 { - #[key] - k1: u8, - #[key] - k2: felt252, - v1: u128, - v2: u32 -} - -#[test] -fn test_model_definition() { - let definition = dojo::model::Model::::definition(); - - assert_eq!(definition.name, dojo::model::Model::::name()); - assert_eq!(definition.namespace, dojo::model::Model::::namespace()); - assert_eq!(definition.namespace_hash, dojo::model::Model::::namespace_hash()); - assert_eq!(definition.version, dojo::model::Model::::version()); - assert_eq!(definition.layout, dojo::model::Model::::layout()); - assert_eq!(definition.schema, dojo::model::Model::::schema()); - assert_eq!(definition.packed_size, dojo::model::Model::::packed_size()); - assert_eq!(definition.unpacked_size, dojo::meta::introspect::Introspect::::size()); -} - -#[test] -fn test_id() { - let mvalues = FooEntity { __id: 1, v1: 3, v2: 4 }; - assert!(mvalues.id() == 1); -} - -#[test] -fn test_values() { - let mvalues = FooEntity { __id: 1, v1: 3, v2: 4 }; - let expected_values = [3, 4].span(); - - let values = mvalues.values(); - assert!(expected_values == values); -} - -#[test] -fn test_from_values() { - let mut values = [3, 4].span(); - - let model_entity: Option = Entity::from_values(1, ref values); - assert!(model_entity.is_some()); - let model_entity = model_entity.unwrap(); - assert!(model_entity.__id == 1 && model_entity.v1 == 3 && model_entity.v2 == 4); -} - -#[test] -fn test_from_values_bad_data() { - let mut values = [3].span(); - let res: Option = Entity::from_values(1, ref values); - assert!(res.is_none()); -} - -#[test] -fn test_get_and_update_entity() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - - let entity_id = foo.entity_id(); - let mut entity: FooEntity = world.get_entity(foo.key()); - assert_eq!(entity.__id, entity_id); - assert_eq!(entity.v1, entity.v1); - assert_eq!(entity.v2, entity.v2); - - entity.v1 = 12; - entity.v2 = 18; - - world.update(@entity); - - let read_values: FooEntity = world.get_entity_from_id(entity_id); - assert!(read_values.v1 == entity.v1 && read_values.v2 == entity.v2); -} - -#[test] -fn test_delete_entity() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - - let entity_id = foo.entity_id(); - let mut entity: FooEntity = world.get_entity_from_id(entity_id); - EntityStore::delete_entity(world, @entity); - - let read_values: FooEntity = world.get_entity_from_id(entity_id); - assert!(read_values.v1 == 0 && read_values.v2 == 0); -} - -#[test] -fn test_get_and_set_member_from_entity() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - - let v1: u128 = EntityStore::< - FooEntity - >::get_member_from_id(@world, foo.entity_id(), selector!("v1")); - - assert_eq!(v1, 3); - - let entity: FooEntity = world.get_entity_from_id(foo.entity_id()); - EntityStore::::update_member_from_id(world, entity.id(), selector!("v1"), 42); - - let entity: FooEntity = world.get_entity_from_id(foo.entity_id()); - assert_eq!(entity.v1, 42); -} - -#[test] -fn test_get_and_set_field_name() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - - let v1 = FooMembersStore::get_v1_from_id(@world, foo.entity_id()); - assert!(foo.v1 == v1); - - let _entity: FooEntity = world.get_entity_from_id(foo.entity_id()); - - FooMembersStore::update_v1_from_id(world, foo.entity_id(), 42); - - let v1 = FooMembersStore::get_v1_from_id(@world, foo.entity_id()); - assert!(v1 == 42); -} - -#[test] -fn test_get_and_set_from_model() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - - let read_entity: Foo = world.get((foo.k1, foo.k2)); - - assert!( - foo.k1 == read_entity.k1 - && foo.k2 == read_entity.k2 - && foo.v1 == read_entity.v1 - && foo.v2 == read_entity.v2 - ); -} - -#[test] -fn test_delete_from_model() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - world.delete(@foo); - - let read_entity: Foo = world.get((foo.k1, foo.k2)); - assert!( - read_entity.k1 == foo.k1 - && read_entity.k2 == foo.k2 - && read_entity.v1 == 0 - && read_entity.v2 == 0 - ); -} - -#[test] -fn test_get_and_set_member_from_model() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - let key: (u8, felt252) = foo.key(); - let v1: u128 = ModelStore::::get_member(@world, key, selector!("v1")); - - assert!(v1 == 3); - - ModelStore::::update_member(world, key, selector!("v1"), 42); - let foo: Foo = world.get((foo.k1, foo.k2)); - assert!(foo.v1 == 42); -} - -#[test] -fn test_get_and_set_field_name_from_model() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; - world.set(@foo); - - let v1 = FooMembersStore::get_v1(@world, (foo.k1, foo.k2)); - assert!(v1 == 3); - - FooMembersStore::update_v1(world, (foo.k1, foo.k2), 42); - - let v1 = FooMembersStore::get_v1(@world, (foo.k1, foo.k2)); - assert!(v1 == 42); -} - diff --git a/crates/contracts/src/tests/utils/hash.cairo b/crates/contracts/src/tests/utils/hash.cairo deleted file mode 100644 index 1c0d239..0000000 --- a/crates/contracts/src/tests/utils/hash.cairo +++ /dev/null @@ -1,28 +0,0 @@ -use dojo::model::Model; -use dojo::utils::{bytearray_hash, selector_from_names}; - -#[derive(Drop, Copy, Serde)] -#[dojo::model(namespace: "my_namespace")] -struct MyModel { - #[key] - x: u8, - y: u8 -} - -#[test] -fn test_hash_computation() { - // Be sure that the namespace hash computed in `dojo-lang` in Rust is equal - // to the one computed in Cairo by dojo::utils:hash - let namespace = Model::::namespace(); - let namespace_hash = Model::::namespace_hash(); - - assert(bytearray_hash(@namespace) == namespace_hash, 'invalid computed hash'); -} - -#[test] -fn test_selector_computation() { - let namespace = Model::::namespace(); - let name = Model::::name(); - let selector = selector_from_names(@namespace, @name); - assert(selector == Model::::selector(), 'invalid computed selector'); -} diff --git a/crates/contracts/src/utils/descriptor.cairo b/crates/contracts/src/utils/descriptor.cairo deleted file mode 100644 index 75f9971..0000000 --- a/crates/contracts/src/utils/descriptor.cairo +++ /dev/null @@ -1,151 +0,0 @@ -//! Descriptor is used to verify the consistency of the selector from the namespace and the name. -use core::num::traits::Zero; -use core::panics::panic_with_byte_array; -use core::poseidon::poseidon_hash_span; -use dojo::utils::bytearray_hash; -use starknet::ContractAddress; - -/// Interface for a world's resource descriptor. -#[starknet::interface] -pub trait IDescriptor { - fn selector(self: @T) -> felt252; - fn namespace_hash(self: @T) -> felt252; - fn name_hash(self: @T) -> felt252; - fn namespace(self: @T) -> ByteArray; - fn name(self: @T) -> ByteArray; - fn tag(self: @T) -> ByteArray; -} - -/// A descriptor of a resource used to verify consistency of the selector from the namespace and the -/// name. -/// -/// Fields are kept internal to ensure this struct can't be initialized with arbitrary values -/// to ensure consistency. -#[derive(Copy, Drop)] -pub struct Descriptor { - selector: felt252, - namespace_hash: felt252, - name_hash: felt252, - namespace: @ByteArray, - name: @ByteArray, -} - -/// Implements the PartialEq trait for the Descriptor, which only compares -/// the selector, since it is constructed in a consistent manner from the plain names. -impl PartialEqImpl of PartialEq { - fn eq(lhs: @Descriptor, rhs: @Descriptor) -> bool { - (*lhs).selector() == (*rhs).selector() - } - fn ne(lhs: @Descriptor, rhs: @Descriptor) -> bool { - (*lhs).selector() != (*rhs).selector() - } -} - -#[generate_trait] -pub impl DescriptorImpl of DescriptorTrait { - /// Initializes the descriptor from plain namespace and name. - /// - /// # Arguments - /// - /// * `namespace` - The namespace of the resource. - /// * `name` - The name of the resource. - /// - /// # Returns - /// - /// * `Descriptor` - The descriptor of the resource. - fn from_names(namespace: @ByteArray, name: @ByteArray) -> Descriptor { - let namespace_hash = bytearray_hash(namespace); - let name_hash = bytearray_hash(name); - let selector = poseidon_hash_span([namespace_hash, name_hash].span()); - - Descriptor { selector, namespace_hash, name_hash, namespace, name, } - } - - /// Initializes and asserts the descriptor from a deployed contract. - /// - /// # Arguments - /// - /// * `contract_address` - The contract address of the resource. - /// - /// # Returns - /// - /// * `Descriptor` - The descriptor of the resource. - fn from_contract_assert(contract_address: ContractAddress) -> Descriptor { - let d = IDescriptorDispatcher { contract_address }; - - let name = d.name(); - let namespace = d.namespace(); - let namespace_hash = d.namespace_hash(); - let name_hash = d.name_hash(); - let selector = d.selector(); - - let descriptor = Self::from_names(@namespace, @name); - descriptor.assert_hashes(selector, namespace_hash, name_hash); - descriptor - } - - /// Asserts the provided hashes to map the descriptor, which has been initialized from plain - /// names. - /// - /// # Arguments - /// - /// * `selector` - The selector of the resource. - /// * `namespace_hash` - The namespace hash of the resource. - /// * `name_hash` - The name hash of the resource. - fn assert_hashes( - self: @Descriptor, selector: felt252, namespace_hash: felt252, name_hash: felt252 - ) { - if selector.is_zero() { - panic_with_byte_array(@errors::reserved_selector(selector)); - } - - if *self.selector != selector { - panic_with_byte_array(@errors::mismatch(@"selector", *self.selector, selector)); - } - - if *self.namespace_hash != namespace_hash { - panic_with_byte_array( - @errors::mismatch(@"namespace_hash", *self.namespace_hash, namespace_hash) - ); - } - - if *self.name_hash != name_hash { - panic_with_byte_array(@errors::mismatch(@"name_hash", *self.name_hash, name_hash)); - } - } - - /// Gets the selector. - fn selector(self: @Descriptor) -> felt252 { - *self.selector - } - - /// Gets the namespace hash. - fn namespace_hash(self: @Descriptor) -> felt252 { - *self.namespace_hash - } - - /// Gets the name hash. - fn name_hash(self: @Descriptor) -> felt252 { - *self.name_hash - } - - /// Gets the namespace. - fn namespace(self: @Descriptor) -> @ByteArray { - *self.namespace - } - - /// Gets the name. - fn name(self: @Descriptor) -> @ByteArray { - *self.name - } -} - -mod errors { - pub fn mismatch(what: @ByteArray, expected: felt252, found: felt252) -> ByteArray { - format!("Descriptor: `{}` mismatch, expected `{}` but found `{}`", what, expected, found) - } - - pub fn reserved_selector(found: felt252) -> ByteArray { - format!("Descriptor: selector `{}` is a reserved selector", found) - } -} diff --git a/crates/contracts/src/utils/hash.cairo b/crates/contracts/src/utils/hash.cairo index d4e874a..d356c6f 100644 --- a/crates/contracts/src/utils/hash.cairo +++ b/crates/contracts/src/utils/hash.cairo @@ -12,3 +12,8 @@ pub fn bytearray_hash(data: @ByteArray) -> felt252 { pub fn selector_from_names(namespace: @ByteArray, name: @ByteArray) -> felt252 { poseidon_hash_span([bytearray_hash(namespace), bytearray_hash(name)].span()) } + +/// Computes the selector namespace hash and the name of the resource. +pub fn selector_from_namespace_and_name(namespace_hash: felt252, name: @ByteArray) -> felt252 { + poseidon_hash_span([namespace_hash, bytearray_hash(name)].span()) +} diff --git a/crates/contracts/src/utils/naming.cairo b/crates/contracts/src/utils/naming.cairo index 8191961..27275f8 100644 --- a/crates/contracts/src/utils/naming.cairo +++ b/crates/contracts/src/utils/naming.cairo @@ -14,7 +14,11 @@ pub fn is_name_valid(name: @ByteArray) -> bool { let mut i = 0; loop { if i >= name.len() { - break true; + if i > 0 { + break true; + } else { + break false; + } } let c = name.at(i).unwrap(); diff --git a/crates/contracts/src/utils/snf_test.cairo b/crates/contracts/src/utils/snf_test.cairo new file mode 100644 index 0000000..520e491 --- /dev/null +++ b/crates/contracts/src/utils/snf_test.cairo @@ -0,0 +1,109 @@ +use starknet::{ClassHash, ContractAddress}; +use snforge_std::{declare, ContractClassTrait, DeclareResultTrait}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, Resource}; +use core::panics::panic_with_byte_array; + +#[derive(Drop)] +pub enum TestResource { + Event: ByteArray, + Model: ByteArray, + Contract: ByteArray, +} + +#[derive(Drop)] +pub struct NamespaceDef { + pub namespace: ByteArray, + pub resources: Span, +} + +/// Spawns a test world registering namespaces and resources. +/// +/// # Arguments +/// +/// * `namespaces` - Namespaces to register. +/// * `resources` - Resources to register. +/// +/// # Returns +/// +/// * World dispatcher +pub fn spawn_test_world(namespaces_defs: Span) -> IWorldDispatcher { + let world_contract = declare("world").unwrap().contract_class(); + let class_hash_felt: felt252 = (*world_contract.class_hash).into(); + let (world_address, _) = world_contract.deploy(@array![class_hash_felt]).unwrap(); + + let world = IWorldDispatcher { contract_address: world_address }; + + for ns in namespaces_defs { + let namespace = ns.namespace.clone(); + world.register_namespace(namespace.clone()); + + for r in ns + .resources + .clone() { + match r { + TestResource::Event(name) => { + let ch: ClassHash = *declare(name.clone()) + .unwrap() + .contract_class() + .class_hash; + world.register_event(namespace.clone(), ch); + }, + TestResource::Model(name) => { + let ch: ClassHash = *declare(name.clone()) + .unwrap() + .contract_class() + .class_hash; + world.register_model(namespace.clone(), ch); + }, + TestResource::Contract(name) => { + let ch: ClassHash = *declare(name.clone()) + .unwrap() + .contract_class() + .class_hash; + let salt = dojo::utils::bytearray_hash(name); + world.register_contract(salt, namespace.clone(), ch); + }, + } + } + }; + + world +} + +/// Extension trait for world dispatcher to test resources. +pub trait WorldTestExt { + fn resource_contract_address( + self: IWorldDispatcher, namespace: ByteArray, name: ByteArray + ) -> ContractAddress; + fn resource_class_hash( + self: IWorldDispatcher, namespace: ByteArray, name: ByteArray + ) -> ClassHash; +} + +impl WorldTestExtImpl of WorldTestExt { + fn resource_contract_address( + self: IWorldDispatcher, namespace: ByteArray, name: ByteArray + ) -> ContractAddress { + match self.resource(dojo::utils::selector_from_names(@namespace, @name)) { + Resource::Contract((ca, _)) => ca, + Resource::Event((ca, _)) => ca, + Resource::Model((ca, _)) => ca, + _ => panic_with_byte_array( + @format!("Resource is not registered: {}-{}", namespace, name) + ) + } + } + + fn resource_class_hash( + self: IWorldDispatcher, namespace: ByteArray, name: ByteArray + ) -> ClassHash { + match self.resource(dojo::utils::selector_from_names(@namespace, @name)) { + Resource::Contract((_, ch)) => ch.try_into().unwrap(), + Resource::Event((_, ch)) => ch.try_into().unwrap(), + Resource::Model((_, ch)) => ch.try_into().unwrap(), + _ => panic_with_byte_array( + @format!("Resource is not registered: {}-{}", namespace, name) + ), + } + } +} diff --git a/crates/contracts/src/utils/test.cairo b/crates/contracts/src/utils/test.cairo deleted file mode 100644 index 5e6d2d2..0000000 --- a/crates/contracts/src/utils/test.cairo +++ /dev/null @@ -1,138 +0,0 @@ -use core::array::SpanTrait; -use core::option::OptionTrait; -use core::result::ResultTrait; -use core::traits::{Into, TryInto}; - -use starknet::{ContractAddress, syscalls::deploy_syscall}; - -use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; - -/// Deploy classhash with calldata for constructor -/// -/// # Arguments -/// -/// * `class_hash` - Class to deploy -/// * `calldata` - calldata for constructor -/// -/// # Returns -/// * address of contract deployed -pub fn deploy_contract(class_hash: felt252, calldata: Span) -> ContractAddress { - let (contract, _) = starknet::syscalls::deploy_syscall( - class_hash.try_into().unwrap(), 0, calldata, false - ) - .unwrap(); - contract -} - -/// Deploy classhash and passes in world address to constructor -/// -/// # Arguments -/// -/// * `class_hash` - Class to deploy -/// * `world` - World dispatcher to pass as world address -/// -/// # Returns -/// * address of contract deployed -pub fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) -> ContractAddress { - deploy_contract(class_hash, [world.contract_address.into()].span()) -} - -/// Spawns a test world registering namespaces and models. -/// -/// # Arguments -/// -/// * `namespaces` - Namespaces to register. -/// * `models` - Models to register. -/// -/// # Returns -/// -/// * World dispatcher -pub fn spawn_test_world(namespaces: Span, models: Span) -> IWorldDispatcher { - let salt = core::testing::get_available_gas(); - - let (world_address, _) = deploy_syscall( - world::TEST_CLASS_HASH.try_into().unwrap(), - salt.into(), - [world::TEST_CLASS_HASH].span(), - false - ) - .unwrap(); - - let world = IWorldDispatcher { contract_address: world_address }; - - // Register all namespaces to ensure correct registration of models. - let mut namespaces = namespaces; - while let Option::Some(namespace) = namespaces.pop_front() { - world.register_namespace(namespace.clone()); - }; - - // Register all models. - let mut index = 0; - loop { - if index == models.len() { - break (); - } - world.register_model((*models[index]).try_into().unwrap()); - index += 1; - }; - - world -} - -#[derive(Drop)] -pub struct GasCounter { - pub start: u128, -} - -#[generate_trait] -pub impl GasCounterImpl of GasCounterTrait { - fn start() -> GasCounter { - let start = core::testing::get_available_gas(); - core::gas::withdraw_gas().unwrap(); - GasCounter { start } - } - - fn end(self: GasCounter, name: ByteArray) { - let end = core::testing::get_available_gas(); - let gas_used = self.start - end; - - println!("# GAS # {}: {}", Self::pad_start(name, 18), gas_used); - core::gas::withdraw_gas().unwrap(); - } - - fn pad_start(str: ByteArray, len: u32) -> ByteArray { - let mut missing: ByteArray = ""; - let missing_len = if str.len() >= len { - 0 - } else { - len - str.len() - }; - - while missing.len() < missing_len { - missing.append(@"."); - }; - missing + str - } -} - -// assert that `value` and `expected` have the same size and the same content -pub fn assert_array(value: Span, expected: Span) { - assert!(value.len() == expected.len(), "Bad array length"); - - let mut i = 0; - loop { - if i >= value.len() { - break; - } - - assert!( - *value.at(i) == *expected.at(i), - "Bad array value [{}] (expected: {} got: {})", - i, - *expected.at(i), - *value.at(i) - ); - - i += 1; - } -} diff --git a/crates/contracts/src/world/errors.cairo b/crates/contracts/src/world/errors.cairo index df6f6c6..08a7162 100644 --- a/crates/contracts/src/world/errors.cairo +++ b/crates/contracts/src/world/errors.cairo @@ -38,7 +38,7 @@ pub fn contract_already_registered(namespace: @ByteArray, name: @ByteArray) -> B format!("Resource `{}-{}` is already registered", namespace, name) } -pub fn model_not_registered(namespace: @ByteArray, name: @ByteArray) -> ByteArray { +pub fn resource_not_registered_details(namespace: @ByteArray, name: @ByteArray) -> ByteArray { format!("Resource `{}-{}` is not registered", namespace, name) } diff --git a/crates/contracts/src/world/iworld.cairo b/crates/contracts/src/world/iworld.cairo index 46c7b98..7cdf54a 100644 --- a/crates/contracts/src/world/iworld.cairo +++ b/crates/contracts/src/world/iworld.cairo @@ -59,15 +59,17 @@ pub trait IWorld { /// /// # Arguments /// + /// * `namespace` - The namespace of the event to be registered. /// * `class_hash` - The class hash of the event to be registered. - fn register_event(ref self: T, class_hash: ClassHash); + fn register_event(ref self: T, namespace: ByteArray, class_hash: ClassHash); /// Registers a model in the world. /// /// # Arguments /// + /// * `namespace` - The namespace of the model to be registered. /// * `class_hash` - The class hash of the model to be registered. - fn register_model(ref self: T, class_hash: ClassHash); + fn register_model(ref self: T, namespace: ByteArray, class_hash: ClassHash); /// Registers and deploys a contract associated with the world and returns the address of newly /// deployed contract. @@ -75,8 +77,11 @@ pub trait IWorld { /// # Arguments /// /// * `salt` - The salt use for contract deployment. + /// * `namespace` - The namespace of the contract to be registered. /// * `class_hash` - The class hash of the contract. - fn register_contract(ref self: T, salt: felt252, class_hash: ClassHash) -> ContractAddress; + fn register_contract( + ref self: T, salt: felt252, namespace: ByteArray, class_hash: ClassHash + ) -> ContractAddress; /// Initializes a contract associated registered in the world. /// @@ -91,23 +96,26 @@ pub trait IWorld { /// /// # Arguments /// + /// * `namespace` - The namespace of the event to be upgraded. /// * `class_hash` - The class hash of the event to be upgraded. - fn upgrade_event(ref self: T, class_hash: ClassHash); + fn upgrade_event(ref self: T, namespace: ByteArray, class_hash: ClassHash); /// Upgrades a model in the world. /// /// # Arguments /// + /// * `namespace` - The namespace of the model to be upgraded. /// * `class_hash` - The class hash of the model to be upgraded. - fn upgrade_model(ref self: T, class_hash: ClassHash); + fn upgrade_model(ref self: T, namespace: ByteArray, class_hash: ClassHash); /// Upgrades an already deployed contract associated with the world and returns the new class /// hash. /// /// # Arguments /// + /// * `namespace` - The namespace of the contract to be upgraded. /// * `class_hash` - The class hash of the contract. - fn upgrade_contract(ref self: T, class_hash: ClassHash) -> ClassHash; + fn upgrade_contract(ref self: T, namespace: ByteArray, class_hash: ClassHash) -> ClassHash; /// Emits a custom event that was previously registered in the world. /// The dojo event emission is permissioned, since data are collected by @@ -256,4 +264,7 @@ pub trait IWorldTest { values: Span, historical: bool ); + + /// Returns the address of a registered contract, panics otherwise. + fn dojo_contract_address(self: @T, contract_selector: felt252) -> ContractAddress; } diff --git a/crates/contracts/src/world/storage.cairo b/crates/contracts/src/world/storage.cairo new file mode 100644 index 0000000..78a989b --- /dev/null +++ b/crates/contracts/src/world/storage.cairo @@ -0,0 +1,328 @@ +//! A simple storage abstraction for the world's storage. + +use core::panic_with_felt252; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo::model::{ + Model, ModelIndex, ModelDefinition, ModelMemberStorage, ModelValueKey, ModelValue, ModelStorage +}; +use dojo::event::{Event, EventStorage}; +use dojo::meta::Layout; +use dojo::utils::{ + entity_id_from_key, serialize_inline, deserialize_unwrap, find_model_field_layout +}; + +#[derive(Drop)] +pub struct WorldStorage { + pub world: IWorldDispatcher, + pub namespace: ByteArray, + pub namespace_hash: felt252, +} + +#[generate_trait] +pub impl WorldStorageInternalImpl of WorldStorageTrait { + fn new(world: IWorldDispatcher, namespace: ByteArray) -> WorldStorage { + let namespace_hash = dojo::utils::bytearray_hash(@namespace); + + WorldStorage { world, namespace, namespace_hash, } + } + + fn set_namespace(ref self: WorldStorage, namespace: ByteArray) { + self.namespace = namespace.clone(); + self.namespace_hash = dojo::utils::bytearray_hash(@namespace); + } +} + +pub impl EventStorageWorldStorageImpl> of EventStorage { + fn emit_event(ref self: WorldStorage, event: @E) { + dojo::world::IWorldDispatcherTrait::emit_event( + self.world, + Event::::selector(self.namespace_hash), + Event::::keys(event), + Event::::values(event), + Event::::historical() + ); + } +} + +pub impl ModelStorageWorldStorageImpl, +Drop> of ModelStorage { + fn read_model, +Serde>(self: @WorldStorage, key: K) -> M { + let mut keys = serialize_inline::(@key); + let mut values = IWorldDispatcherTrait::entity( + *self.world, + Model::::selector(*self.namespace_hash), + ModelIndex::Keys(keys), + Model::::layout() + ); + match Model::::from_values(ref keys, ref values) { + Option::Some(model) => model, + Option::None => { + panic!( + "Model: deserialization failed. Ensure the length of the keys tuple is matching the number of #[key] fields in the model struct." + ) + } + } + } + + fn write_model(ref self: WorldStorage, model: @M) { + IWorldDispatcherTrait::set_entity( + self.world, + Model::::selector(self.namespace_hash), + ModelIndex::Keys(Model::::keys(model)), + Model::::values(model), + Model::::layout() + ); + } + + fn erase_model(ref self: WorldStorage, model: @M) { + IWorldDispatcherTrait::delete_entity( + self.world, + Model::::selector(self.namespace_hash), + ModelIndex::Keys(Model::::keys(model)), + Model::::layout() + ); + } + + fn erase_model_from_key, +Serde>(ref self: WorldStorage, key: K) { + IWorldDispatcherTrait::delete_entity( + self.world, + Model::::selector(self.namespace_hash), + ModelIndex::Keys(serialize_inline::(@key)), + Model::::layout() + ); + } + + fn erase_model_from_id(ref self: WorldStorage, entity_id: felt252) { + IWorldDispatcherTrait::delete_entity( + self.world, + Model::::selector(self.namespace_hash), + ModelIndex::Id(entity_id), + Model::::layout() + ); + } + + fn read_member, +Drop, +Drop, +Serde>( + self: @WorldStorage, key: K, member_id: felt252 + ) -> T { + ModelMemberStorage::< + WorldStorage, M, T + >::read_member_from_id(self, entity_id_from_key::(@key), member_id) + } + + fn write_member, +Drop, +Drop, +Serde>( + ref self: WorldStorage, key: K, member_id: felt252, value: T + ) { + ModelMemberStorage::< + WorldStorage, M, T + >::write_member_from_id(ref self, entity_id_from_key::(@key), member_id, value); + } + + fn namespace_hash(self: @WorldStorage) -> felt252 { + *self.namespace_hash + } +} + +pub impl MemberModelStorageWorldStorageImpl< + M, T, +Model, +ModelDefinition, +Serde, +Drop +> of ModelMemberStorage { + fn read_member_from_id(self: @WorldStorage, entity_id: felt252, member_id: felt252) -> T { + deserialize_unwrap::< + T + >( + get_serialized_member( + *self.world, + Model::::selector(*self.namespace_hash), + ModelDefinition::::layout(), + entity_id, + member_id, + ) + ) + } + + fn write_member_from_id( + ref self: WorldStorage, entity_id: felt252, member_id: felt252, value: T, + ) { + update_serialized_member( + self.world, + Model::::selector(self.namespace_hash), + ModelDefinition::::layout(), + entity_id, + member_id, + serialize_inline::(@value) + ) + } + + fn namespace_hash(self: @WorldStorage) -> felt252 { + *self.namespace_hash + } +} + +impl ModelValueStorageWorldStorageImpl< + V, +ModelValue +> of dojo::model::ModelValueStorage { + fn read_model_value, +Serde, +ModelValueKey>( + self: @WorldStorage, key: K + ) -> V { + Self::read_model_value_from_id(self, entity_id_from_key(@key)) + } + + fn read_model_value_from_id(self: @WorldStorage, entity_id: felt252) -> V { + let mut values = IWorldDispatcherTrait::entity( + *self.world, + ModelValue::::selector(*self.namespace_hash), + ModelIndex::Id(entity_id), + ModelValue::::layout() + ); + match ModelValue::::from_values(entity_id, ref values) { + Option::Some(entity) => entity, + Option::None => { + panic!( + "Entity: deserialization failed. Ensure the length of the keys tuple is matching the number of #[key] fields in the model struct." + ) + } + } + } + + fn write_model_value, +Serde, +ModelValueKey>( + ref self: WorldStorage, key: K, value: @V + ) { + IWorldDispatcherTrait::set_entity( + self.world, + ModelValue::::selector(self.namespace_hash), + ModelIndex::Keys(serialize_inline::(@key)), + ModelValue::::values(value), + ModelValue::::layout() + ); + } + + fn write_model_value_from_id(ref self: WorldStorage, entity_id: felt252, value: @V) { + IWorldDispatcherTrait::set_entity( + self.world, + ModelValue::::selector(self.namespace_hash), + ModelIndex::Id(entity_id), + ModelValue::::values(value), + ModelValue::::layout() + ); + } +} + +#[cfg(target: "test")] +pub impl EventStorageTestWorldStorageImpl< + E, +Event +> of dojo::event::EventStorageTest { + fn emit_event_test(ref self: WorldStorage, event: @E) { + let world_test = dojo::world::IWorldTestDispatcher { + contract_address: self.world.contract_address + }; + dojo::world::IWorldTestDispatcherTrait::emit_event_test( + world_test, + Event::::selector(self.namespace_hash), + Event::::keys(event), + Event::::values(event), + Event::::historical() + ); + } +} + +/// Implementation of the `ModelStorageTest` trait for testing purposes, bypassing permission +/// checks. +#[cfg(target: "test")] +pub impl ModelStorageTestWorldStorageImpl< + M, +Model +> of dojo::model::ModelStorageTest { + fn write_model_test(ref self: WorldStorage, model: @M) { + let world_test = dojo::world::IWorldTestDispatcher { + contract_address: self.world.contract_address + }; + dojo::world::IWorldTestDispatcherTrait::set_entity_test( + world_test, + Model::::selector(self.namespace_hash), + ModelIndex::Keys(Model::keys(model)), + Model::::values(model), + Model::::layout() + ); + } + + fn erase_model_test(ref self: WorldStorage, model: @M) { + let world_test = dojo::world::IWorldTestDispatcher { + contract_address: self.world.contract_address + }; + + dojo::world::IWorldTestDispatcherTrait::delete_entity_test( + world_test, + Model::::selector(self.namespace_hash), + ModelIndex::Keys(Model::keys(model)), + Model::::layout() + ); + } +} + +/// Implementation of the `ModelValueStorageTest` trait for testing purposes, bypassing permission +/// checks. +#[cfg(target: "test")] +pub impl ModelValueStorageTestWorldStorageImpl< + V, +ModelValue +> of dojo::model::ModelValueStorageTest { + fn write_model_value_test(ref self: WorldStorage, entity_id: felt252, value: @V) { + let world_test = dojo::world::IWorldTestDispatcher { + contract_address: self.world.contract_address + }; + + dojo::world::IWorldTestDispatcherTrait::set_entity_test( + world_test, + ModelValue::::selector(self.namespace_hash), + ModelIndex::Id(entity_id), + ModelValue::::values(value), + ModelValue::::layout() + ); + } + + fn erase_model_value_test(ref self: WorldStorage, entity_id: felt252) { + let world_test = dojo::world::IWorldTestDispatcher { + contract_address: self.world.contract_address + }; + + dojo::world::IWorldTestDispatcherTrait::delete_entity_test( + world_test, + ModelValue::::selector(self.namespace_hash), + ModelIndex::Id(entity_id), + ModelValue::::layout() + ); + } +} + +/// Updates a serialized member of a model. +fn update_serialized_member( + world: IWorldDispatcher, + model_id: felt252, + layout: Layout, + entity_id: felt252, + member_id: felt252, + values: Span, +) { + match find_model_field_layout(layout, member_id) { + Option::Some(field_layout) => { + IWorldDispatcherTrait::set_entity( + world, model_id, ModelIndex::MemberId((entity_id, member_id)), values, field_layout, + ) + }, + Option::None => panic_with_felt252('bad member id') + } +} + +/// Retrieves a serialized member of a model. +fn get_serialized_member( + world: IWorldDispatcher, + model_id: felt252, + layout: Layout, + entity_id: felt252, + member_id: felt252, +) -> Span { + match find_model_field_layout(layout, member_id) { + Option::Some(field_layout) => { + IWorldDispatcherTrait::entity( + world, model_id, ModelIndex::MemberId((entity_id, member_id)), field_layout + ) + }, + Option::None => panic_with_felt252('bad member id') + } +} diff --git a/crates/contracts/src/world/world_contract.cairo b/crates/contracts/src/world/world_contract.cairo index 7af7b89..5914c66 100644 --- a/crates/contracts/src/world/world_contract.cairo +++ b/crates/contracts/src/world/world_contract.cairo @@ -41,12 +41,12 @@ pub mod world { }; use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; use dojo::meta::Layout; - use dojo::model::{Model, ResourceMetadata, metadata, ModelIndex}; - use dojo::storage; - use dojo::utils::{ - entity_id_from_keys, bytearray_hash, DescriptorTrait, IDescriptorDispatcher, - IDescriptorDispatcherTrait + use dojo::model::{ + Model, ResourceMetadata, metadata, ModelIndex, IModelDispatcher, IModelDispatcherTrait }; + use dojo::event::{IEventDispatcher, IEventDispatcherTrait}; + use dojo::storage; + use dojo::utils::{entity_id_from_keys, bytearray_hash, selector_from_namespace_and_name}; use dojo::world::{IWorld, IUpgradeableWorld, Resource, ResourceIsNoneTrait}; use super::Permission; @@ -90,7 +90,9 @@ pub mod world { #[derive(Drop, starknet::Event)] pub struct ContractRegistered { #[key] - pub selector: felt252, + pub name: ByteArray, + #[key] + pub namespace: ByteArray, pub address: ContractAddress, pub class_hash: ClassHash, pub salt: felt252, @@ -254,22 +256,24 @@ pub mod world { fn constructor(ref self: ContractState, world_class_hash: ClassHash) { let creator = starknet::get_tx_info().unbox().account_contract_address; + let (internal_ns, internal_ns_hash) = self.world_internal_namespace(); + + self.resources.write(internal_ns_hash, Resource::Namespace(internal_ns)); + self.owners.write((internal_ns_hash, creator), true); + self.resources.write(WORLD, Resource::World); + self.owners.write((WORLD, creator), true); + + // This model doesn't need to have the class hash or the contract address + // set since they are manually controlled by the world contract. self .resources .write( - Model::::selector(), + metadata::resource_metadata_selector(internal_ns_hash), Resource::Model( - (metadata::initial_address(), Model::::namespace_hash()) + (metadata::default_address(), metadata::default_class_hash().into()) ) ); - self.owners.write((WORLD, creator), true); - - let dojo_namespace = "__DOJO__"; - let dojo_namespace_hash = bytearray_hash(@dojo_namespace); - - self.resources.write(dojo_namespace_hash, Resource::Namespace(dojo_namespace)); - self.owners.write((dojo_namespace_hash, creator), true); self.emit(WorldSpawned { creator, class_hash: world_class_hash }); } @@ -311,17 +315,32 @@ pub mod world { } ); } + + fn dojo_contract_address( + self: @ContractState, contract_selector: felt252 + ) -> ContractAddress { + match self.resources.read(contract_selector) { + Resource::Contract((a, _)) => a, + _ => core::panics::panic_with_byte_array( + @format!("Contract not registered: {}", contract_selector) + ) + } + } } #[abi(embed_v0)] impl World of IWorld { fn metadata(self: @ContractState, resource_selector: felt252) -> ResourceMetadata { + let (_, internal_ns_hash) = self.world_internal_namespace(); + let mut values = storage::entity_model::read_model_entity( - Model::::selector(), + metadata::resource_metadata_selector(internal_ns_hash), entity_id_from_keys([resource_selector].span()), Model::::layout() ); + let mut keys = [resource_selector].span(); + match Model::::from_values(ref keys, ref values) { Option::Some(x) => x, Option::None => panic!("Model `ResourceMetadata`: deserialization failed.") @@ -331,11 +350,13 @@ pub mod world { fn set_metadata(ref self: ContractState, metadata: ResourceMetadata) { self.assert_caller_permissions(metadata.resource_id, Permission::Owner); + let (_, internal_ns_hash) = self.world_internal_namespace(); + storage::entity_model::write_model_entity( - metadata.instance_selector(), - metadata.entity_id(), + metadata::resource_metadata_selector(internal_ns_hash), + metadata.resource_id, metadata.values(), - metadata.instance_layout() + Model::::layout() ); self @@ -400,51 +421,53 @@ pub mod world { self.emit(WriterUpdated { resource, contract, value: false }); } - fn register_event(ref self: ContractState, class_hash: ClassHash) { + fn register_event(ref self: ContractState, namespace: ByteArray, class_hash: ClassHash) { let caller = get_caller_address(); let salt = self.events_salt.read(); + let namespace_hash = bytearray_hash(@namespace); + let (contract_address, _) = starknet::syscalls::deploy_syscall( class_hash, salt.into(), [].span(), false, ) .unwrap_syscall(); self.events_salt.write(salt + 1); - let descriptor = DescriptorTrait::from_contract_assert(contract_address); + let event = IEventDispatcher { contract_address }; + let event_name = event.dojo_name(); - if !self.is_namespace_registered(descriptor.namespace_hash()) { - panic_with_byte_array(@errors::namespace_not_registered(descriptor.namespace())); + self.assert_name(@event_name); + + let event_selector = selector_from_namespace_and_name(namespace_hash, @event_name); + + if !self.is_namespace_registered(namespace_hash) { + panic_with_byte_array(@errors::namespace_not_registered(@namespace)); } - self.assert_caller_permissions(descriptor.namespace_hash(), Permission::Owner); + self.assert_caller_permissions(namespace_hash, Permission::Owner); - let maybe_existing_event = self.resources.read(descriptor.selector()); + let maybe_existing_event = self.resources.read(event_selector); if !maybe_existing_event.is_unregistered() { - panic_with_byte_array( - @errors::event_already_registered(descriptor.namespace(), descriptor.name()) - ); + panic_with_byte_array(@errors::event_already_registered(@namespace, @event_name)); } self .resources - .write( - descriptor.selector(), - Resource::Event((contract_address, descriptor.namespace_hash())) - ); - self.owners.write((descriptor.selector(), caller), true); + .write(event_selector, Resource::Event((contract_address, namespace_hash))); + self.owners.write((event_selector, caller), true); self .emit( EventRegistered { - name: descriptor.name().clone(), - namespace: descriptor.namespace().clone(), + name: event_name.clone(), + namespace: namespace.clone(), address: contract_address, class_hash } ); } - fn upgrade_event(ref self: ContractState, class_hash: ClassHash) { + fn upgrade_event(ref self: ContractState, namespace: ByteArray, class_hash: ClassHash) { let salt = self.events_salt.read(); let (new_contract_address, _) = starknet::syscalls::deploy_syscall( @@ -454,48 +477,42 @@ pub mod world { self.events_salt.write(salt + 1); - let new_descriptor = DescriptorTrait::from_contract_assert(new_contract_address); + let namespace_hash = bytearray_hash(@namespace); - if !self.is_namespace_registered(new_descriptor.namespace_hash()) { - panic_with_byte_array( - @errors::namespace_not_registered(new_descriptor.namespace()) - ); + let event = IEventDispatcher { contract_address: new_contract_address }; + let event_name = event.dojo_name(); + let event_selector = selector_from_namespace_and_name(namespace_hash, @event_name); + + if !self.is_namespace_registered(namespace_hash) { + panic_with_byte_array(@errors::namespace_not_registered(@namespace)); } - self.assert_caller_permissions(new_descriptor.selector(), Permission::Owner); + self.assert_caller_permissions(event_selector, Permission::Owner); let mut prev_address = core::num::traits::Zero::::zero(); - // If the namespace or name of the event have been changed, the descriptor + // If the namespace or name of the event have been changed, the selector // will be different, hence not upgradeable. - match self.resources.read(new_descriptor.selector()) { + match self.resources.read(event_selector) { Resource::Event((model_address, _)) => { prev_address = model_address; }, Resource::Unregistered => { panic_with_byte_array( - @errors::event_not_registered( - new_descriptor.namespace(), new_descriptor.name() - ) + @errors::resource_not_registered_details(@namespace, @event_name) ) }, _ => panic_with_byte_array( - @errors::resource_conflict( - @format!("{}-{}", new_descriptor.namespace(), new_descriptor.name()), - @"event" - ) + @errors::resource_conflict(@format!("{}-{}", @namespace, @event_name), @"event") ) }; self .resources - .write( - new_descriptor.selector(), - Resource::Event((new_contract_address, new_descriptor.namespace_hash())) - ); + .write(event_selector, Resource::Event((new_contract_address, namespace_hash))); self .emit( EventUpgraded { - selector: new_descriptor.selector(), + selector: event_selector, prev_address, address: new_contract_address, class_hash, @@ -503,54 +520,53 @@ pub mod world { ); } - fn register_model(ref self: ContractState, class_hash: ClassHash) { + fn register_model(ref self: ContractState, namespace: ByteArray, class_hash: ClassHash) { let caller = get_caller_address(); let salt = self.models_salt.read(); + let namespace_hash = bytearray_hash(@namespace); + let (contract_address, _) = starknet::syscalls::deploy_syscall( class_hash, salt.into(), [].span(), false, ) .unwrap_syscall(); self.models_salt.write(salt + 1); - let descriptor = DescriptorTrait::from_contract_assert(contract_address); + let model = IModelDispatcher { contract_address }; + let model_name = model.dojo_name(); - self.assert_namespace(descriptor.namespace()); - self.assert_name(descriptor.name()); + self.assert_name(@model_name); - if !self.is_namespace_registered(descriptor.namespace_hash()) { - panic_with_byte_array(@errors::namespace_not_registered(descriptor.namespace())); + let model_selector = selector_from_namespace_and_name(namespace_hash, @model_name); + + if !self.is_namespace_registered(namespace_hash) { + panic_with_byte_array(@errors::namespace_not_registered(@namespace)); } - self.assert_caller_permissions(descriptor.namespace_hash(), Permission::Owner); + self.assert_caller_permissions(namespace_hash, Permission::Owner); - let maybe_existing_model = self.resources.read(descriptor.selector()); + let maybe_existing_model = self.resources.read(model_selector); if !maybe_existing_model.is_unregistered() { - panic_with_byte_array( - @errors::model_already_registered(descriptor.namespace(), descriptor.name()) - ); + panic_with_byte_array(@errors::model_already_registered(@namespace, @model_name)); } self .resources - .write( - descriptor.selector(), - Resource::Model((contract_address, descriptor.namespace_hash())) - ); - self.owners.write((descriptor.selector(), caller), true); + .write(model_selector, Resource::Model((contract_address, namespace_hash))); + self.owners.write((model_selector, caller), true); self .emit( ModelRegistered { - name: descriptor.name().clone(), - namespace: descriptor.namespace().clone(), + name: model_name.clone(), + namespace: namespace.clone(), address: contract_address, class_hash } ); } - fn upgrade_model(ref self: ContractState, class_hash: ClassHash) { + fn upgrade_model(ref self: ContractState, namespace: ByteArray, class_hash: ClassHash) { let salt = self.models_salt.read(); let (new_contract_address, _) = starknet::syscalls::deploy_syscall( @@ -560,51 +576,45 @@ pub mod world { self.models_salt.write(salt + 1); - let new_descriptor = DescriptorTrait::from_contract_assert(new_contract_address); + let namespace_hash = bytearray_hash(@namespace); - self.assert_namespace(new_descriptor.namespace()); - self.assert_name(new_descriptor.name()); + let model = IModelDispatcher { contract_address: new_contract_address }; + let model_name = model.dojo_name(); + let model_selector = selector_from_namespace_and_name(namespace_hash, @model_name); - if !self.is_namespace_registered(new_descriptor.namespace_hash()) { - panic_with_byte_array( - @errors::namespace_not_registered(new_descriptor.namespace()) - ); + if !self.is_namespace_registered(namespace_hash) { + panic_with_byte_array(@errors::namespace_not_registered(@namespace)); } - self.assert_caller_permissions(new_descriptor.selector(), Permission::Owner); + self.assert_caller_permissions(model_selector, Permission::Owner); let mut prev_address = core::num::traits::Zero::::zero(); - // If the namespace or name of the model have been changed, the descriptor - // will be different, hence not upgradeable. - match self.resources.read(new_descriptor.selector()) { + // If the namespace or name of the model have been changed, the selector + // will be different, hence detected as not registered as model. + match self.resources.read(model_selector) { Resource::Model((model_address, _)) => { prev_address = model_address; }, Resource::Unregistered => { panic_with_byte_array( - @errors::model_not_registered( - new_descriptor.namespace(), new_descriptor.name() - ) + @errors::resource_not_registered_details(@namespace, @model_name) ) }, _ => panic_with_byte_array( - @errors::resource_conflict( - @format!("{}-{}", new_descriptor.namespace(), new_descriptor.name()), - @"model" - ) + @errors::resource_conflict(@format!("{}-{}", @namespace, @model_name), @"model") ) }; + // TODO(@remy): check upgradeability with the actual content of the model. + // Use `prev_address` to get the previous model address and get `Ty` from it. + self .resources - .write( - new_descriptor.selector(), - Resource::Model((new_contract_address, new_descriptor.namespace_hash())) - ); + .write(model_selector, Resource::Model((new_contract_address, namespace_hash))); self .emit( ModelUpgraded { - selector: new_descriptor.selector(), + selector: model_selector, prev_address, address: new_contract_address, class_hash, @@ -636,85 +646,89 @@ pub mod world { } fn register_contract( - ref self: ContractState, salt: felt252, class_hash: ClassHash, + ref self: ContractState, salt: felt252, namespace: ByteArray, class_hash: ClassHash, ) -> ContractAddress { let caller = get_caller_address(); let (contract_address, _) = deploy_syscall(class_hash, salt, [].span(), false) .unwrap_syscall(); - let descriptor = DescriptorTrait::from_contract_assert(contract_address); + let namespace_hash = bytearray_hash(@namespace); - self.assert_namespace(descriptor.namespace()); - self.assert_name(descriptor.name()); + let contract = IContractDispatcher { contract_address }; + let contract_name = contract.dojo_name(); + let contract_selector = selector_from_namespace_and_name( + namespace_hash, @contract_name + ); - let maybe_existing_contract = self.resources.read(descriptor.selector()); + self.assert_name(@contract_name); + + let maybe_existing_contract = self.resources.read(contract_selector); if !maybe_existing_contract.is_unregistered() { panic_with_byte_array( - @errors::contract_already_registered(descriptor.namespace(), descriptor.name()) + @errors::contract_already_registered(@namespace, @contract_name) ); } - if !self.is_namespace_registered(descriptor.namespace_hash()) { - panic_with_byte_array(@errors::namespace_not_registered(descriptor.namespace())); + if !self.is_namespace_registered(namespace_hash) { + panic_with_byte_array(@errors::namespace_not_registered(@namespace)); } - self.assert_caller_permissions(descriptor.namespace_hash(), Permission::Owner); + self.assert_caller_permissions(namespace_hash, Permission::Owner); - self.owners.write((descriptor.selector(), caller), true); + self.owners.write((contract_selector, caller), true); self .resources - .write( - descriptor.selector(), - Resource::Contract((contract_address, descriptor.namespace_hash())) - ); + .write(contract_selector, Resource::Contract((contract_address, namespace_hash))); self .emit( ContractRegistered { - salt, - class_hash, - address: contract_address, - selector: descriptor.selector(), + salt, class_hash, address: contract_address, namespace, name: contract_name, } ); contract_address } - fn upgrade_contract(ref self: ContractState, class_hash: ClassHash) -> ClassHash { - // Using a library call is not safe as arbitrary code is executed. - // But deploying the contract we can check the descriptor. - // If a new syscall supports calling library code with safety checks, we could switch - // back to using it. But for now, this is the safest option even if it's more expensive. - let (check_address, _) = deploy_syscall( + fn upgrade_contract( + ref self: ContractState, namespace: ByteArray, class_hash: ClassHash + ) -> ClassHash { + let (new_contract_address, _) = deploy_syscall( class_hash, starknet::get_tx_info().unbox().transaction_hash, [].span(), false ) .unwrap_syscall(); - let new_descriptor = DescriptorTrait::from_contract_assert(check_address); - - self.assert_namespace(new_descriptor.namespace()); - self.assert_name(new_descriptor.name()); + let namespace_hash = bytearray_hash(@namespace); - if let Resource::Contract((contract_address, _)) = self - .resources - .read(new_descriptor.selector()) { - self.assert_caller_permissions(new_descriptor.selector(), Permission::Owner); - - let existing_descriptor = DescriptorTrait::from_contract_assert(contract_address); + let contract = IContractDispatcher { contract_address: new_contract_address }; + let contract_name = contract.dojo_name(); + let contract_selector = selector_from_namespace_and_name( + namespace_hash, @contract_name + ); - assert!( - existing_descriptor == new_descriptor, "invalid contract descriptor for upgrade" - ); + // If namespace and name are the same, the contract is already registered and we + // can upgrade it. + match self.resources.read(contract_selector) { + Resource::Contract(( + contract_address, _ + )) => { + self.assert_caller_permissions(contract_selector, Permission::Owner); - IUpgradeableDispatcher { contract_address }.upgrade(class_hash); - self.emit(ContractUpgraded { class_hash, selector: new_descriptor.selector() }); + IUpgradeableDispatcher { contract_address }.upgrade(class_hash); + self.emit(ContractUpgraded { class_hash, selector: contract_selector }); - class_hash - } else { - panic_with_byte_array( - @errors::resource_conflict(new_descriptor.name(), @"contract") + class_hash + }, + Resource::Unregistered => { + panic_with_byte_array( + @errors::resource_not_registered_details(@namespace, @contract_name) + ) + }, + _ => panic_with_byte_array( + @errors::resource_conflict( + @format!("{}-{}", @namespace, @contract_name), @"contract" + ) ) } } @@ -723,7 +737,9 @@ pub mod world { if let Resource::Contract((contract_address, _)) = self.resources.read(selector) { if self.initialized_contracts.read(selector) { let dispatcher = IContractDispatcher { contract_address }; - panic_with_byte_array(@errors::contract_already_initialized(@dispatcher.tag())); + panic_with_byte_array( + @errors::contract_already_initialized(@dispatcher.dojo_name()) + ); } else { self.assert_caller_permissions(selector, Permission::Owner); @@ -904,6 +920,7 @@ pub mod world { let namespace_hash = match self.resources.read(resource_selector) { Resource::Contract((_, namespace_hash)) => { namespace_hash }, Resource::Model((_, namespace_hash)) => { namespace_hash }, + Resource::Event((_, namespace_hash)) => { namespace_hash }, Resource::Unregistered => { panic_with_byte_array(@errors::resource_not_registered(resource_selector)) }, @@ -953,20 +970,20 @@ pub mod world { Resource::Contract(( contract_address, _ )) => { - let d = IDescriptorDispatcher { contract_address }; - format!("contract (or its namespace) `{}`", d.tag()) + let d = IContractDispatcher { contract_address }; + format!("contract (or its namespace) `{}`", d.dojo_name()) }, Resource::Event(( contract_address, _ )) => { - let d = IDescriptorDispatcher { contract_address }; - format!("event (or its namespace) `{}`", d.tag()) + let d = IEventDispatcher { contract_address }; + format!("event (or its namespace) `{}`", d.dojo_name()) }, Resource::Model(( contract_address, _ )) => { - let d = IDescriptorDispatcher { contract_address }; - format!("model (or its namespace) `{}`", d.tag()) + let d = IModelDispatcher { contract_address }; + format!("model (or its namespace) `{}`", d.dojo_name()) }, Resource::Namespace(ns) => { format!("namespace `{}`", ns) }, Resource::World => { format!("world") }, @@ -984,8 +1001,8 @@ pub mod world { // If the contract is not an account or a dojo contract, tests will display // "CONTRACT_NOT_DEPLOYED" as the error message. In production, the error message // will display "ENTRYPOINT_NOT_FOUND". - let d = IDescriptorDispatcher { contract_address: caller }; - format!("Contract `{}`", d.tag()) + let d = IContractDispatcher { contract_address: caller }; + format!("Contract `{}`", d.dojo_name()) }; panic_with_byte_array( @@ -1073,5 +1090,13 @@ pub mod world { ModelIndex::MemberId(_) => { panic_with_felt252(errors::DELETE_ENTITY_MEMBER); } } } + + /// Returns the hash of the internal namespace for a dojo world. + fn world_internal_namespace(self: @ContractState) -> (ByteArray, felt252) { + let name = "__DOJO__"; + let hash = bytearray_hash(@name); + + (name, hash) + } } } diff --git a/crates/core-cairo-test/Scarb.lock b/crates/core-cairo-test/Scarb.lock new file mode 100644 index 0000000..cdf3d84 --- /dev/null +++ b/crates/core-cairo-test/Scarb.lock @@ -0,0 +1,21 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "1.0.0-rc.0" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_cairo_test" +version = "1.0.0-rc.0" +dependencies = [ + "dojo", +] + +[[package]] +name = "dojo_plugin" +version = "2.8.4" +source = "git+https://github.com/dojoengine/dojo?branch=feat%2Fdojo-1-rc0#3bf1276163fe9dbfe0f65775de8363e358510c77" diff --git a/crates/core-cairo-test/Scarb.toml b/crates/core-cairo-test/Scarb.toml new file mode 100644 index 0000000..9fb3855 --- /dev/null +++ b/crates/core-cairo-test/Scarb.toml @@ -0,0 +1,18 @@ +[package] +cairo-version = "=2.8.4" +edition = "2024_07" +description = "Testing library for Dojo using Cairo test runner." +name = "dojo_cairo_test" +version = "1.0.0-rc.0" + +[[target.starknet-contract]] +build-external-contracts = ["dojo::world::world_contract::world"] + +[dependencies] +starknet = "=2.8.4" +dojo = { path = "../contracts" } + +[dev-dependencies] +cairo_test = "=2.8.4" + +[lib] diff --git a/crates/core-cairo-test/src/lib.cairo b/crates/core-cairo-test/src/lib.cairo new file mode 100644 index 0000000..d1170f4 --- /dev/null +++ b/crates/core-cairo-test/src/lib.cairo @@ -0,0 +1,59 @@ +//! Testing library for Dojo using Cairo test runner. + +#[cfg(target: "test")] +mod utils; +#[cfg(target: "test")] +mod world; + +#[cfg(target: "test")] +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 +}; + +#[cfg(test)] +mod tests { + mod meta { + mod introspect; + } + + mod event { + mod event; + } + + // mod model { + // mod model; + // } + + mod storage { + mod database; + mod packing; + mod storage; + } + + mod contract; + // mod benchmarks; + + mod expanded { + pub(crate) mod selector_attack; + } + + mod helpers; + + mod world { + mod acl; + //mod entities; + //mod resources; + //mod world; + } + + mod utils { + mod hash; + mod key; + mod layout; + mod misc; + mod naming; + } +} diff --git a/crates/contracts/src/tests/benchmarks.cairo b/crates/core-cairo-test/src/tests/benchmarks.cairo similarity index 95% rename from crates/contracts/src/tests/benchmarks.cairo rename to crates/core-cairo-test/src/tests/benchmarks.cairo index b3a89a9..2e588bf 100644 --- a/crates/contracts/src/tests/benchmarks.cairo +++ b/crates/core-cairo-test/src/tests/benchmarks.cairo @@ -15,8 +15,9 @@ use dojo::model::{Model, ModelIndex, ModelStore}; use dojo::storage::{database, storage}; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; -use dojo::tests::helpers::{Foo, Sword, Case, CaseStore, case, Character, Abilities, Stats, Weapon}; -use dojo::utils::test::{spawn_test_world, GasCounterTrait}; +use crate::tests::helpers::{Foo, Sword, Case, case, Character, Abilities, Stats, Weapon, DOJO_NSH}; +use crate::world::{spawn_test_world, NamespaceDef, TestResource}; +use crate::utils::GasCounterTrait; #[derive(Drop, Serde)] #[dojo::model] @@ -42,12 +43,16 @@ struct ComplexModel { } fn deploy_world() -> IWorldDispatcher { - spawn_test_world( - ["dojo"].span(), - [ - case::TEST_CLASS_HASH, case_not_packed::TEST_CLASS_HASH, complex_model::TEST_CLASS_HASH - ].span() - ) + let namespace_def = NamespaceDef { + namespace: "dojo", + resources: [ + TestResource::Model(case::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model(case_not_packed::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model(complex_model::TEST_CLASS_HASH.try_into().unwrap()), + ].span(), + }; + + spawn_test_world([namespace_def].span()) } #[test] @@ -462,7 +467,7 @@ fn test_benchmark_set_entity() { let gas = GasCounterTrait::start(); world .set_entity( - model_selector: Model::::selector(), + model_selector: Model::::selector(DOJO_NSH), index: ModelIndex::Keys(simple_entity_packed.keys()), values: simple_entity_packed.values(), layout: Model::::layout() diff --git a/crates/core-cairo-test/src/tests/contract.cairo b/crates/core-cairo-test/src/tests/contract.cairo new file mode 100644 index 0000000..b7211f1 --- /dev/null +++ b/crates/core-cairo-test/src/tests/contract.cairo @@ -0,0 +1,176 @@ +use core::option::OptionTrait; +use core::traits::TryInto; + +use starknet::ClassHash; + +use dojo::contract::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; +use dojo::world::IWorldDispatcherTrait; + +use crate::world::spawn_test_world; +use crate::tests::helpers::deploy_world; + +#[starknet::contract] +pub mod contract_invalid_upgrade { + #[storage] + struct Storage {} + + #[abi(per_item)] + #[generate_trait] + pub impl InvalidImpl of InvalidContractTrait { + #[external(v0)] + fn no_dojo_name(self: @ContractState) -> ByteArray { + "test_contract" + } + } +} + +#[dojo::contract] +mod test_contract {} + +#[starknet::interface] +pub trait IQuantumLeap { + fn plz_more_tps(self: @T) -> felt252; +} + +#[starknet::contract] +pub mod test_contract_upgrade { + use dojo::contract::IContract; + use dojo::world::IWorldDispatcher; + use dojo::contract::components::world_provider::IWorldProvider; + + #[storage] + struct Storage {} + + #[constructor] + fn constructor(ref self: ContractState) {} + + #[abi(embed_v0)] + pub impl QuantumLeap of super::IQuantumLeap { + fn plz_more_tps(self: @ContractState) -> felt252 { + 'daddy' + } + } + + #[abi(embed_v0)] + pub impl WorldProviderImpl of IWorldProvider { + fn world_dispatcher(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: starknet::contract_address_const::<'world'>() } + } + } + + #[abi(embed_v0)] + pub impl ContractImpl of IContract { + fn dojo_name(self: @ContractState) -> ByteArray { + "test_contract" + } + } +} + +#[test] +#[available_gas(7000000)] +fn test_upgrade_from_world() { + let world = deploy_world(); + + let base_address = world + .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + + world.upgrade_contract("dojo", new_class_hash); + + let quantum_dispatcher = IQuantumLeapDispatcher { contract_address: base_address }; + assert(quantum_dispatcher.plz_more_tps() == 'daddy', 'quantum leap failed'); +} + +#[test] +#[available_gas(7000000)] +#[should_panic( + expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED') +)] +fn test_upgrade_from_world_not_world_provider() { + let world = deploy_world(); + + let _ = world.register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + let new_class_hash: ClassHash = contract_invalid_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + + world.upgrade_contract("dojo", new_class_hash); +} + +#[test] +#[available_gas(6000000)] +#[should_panic(expected: ('must be called by world', 'ENTRYPOINT_FAILED'))] +fn test_upgrade_direct() { + let world = deploy_world(); + + let base_address = world + .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + + let upgradeable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; + upgradeable_dispatcher.upgrade(new_class_hash); +} + +#[starknet::interface] +trait IMetadataOnly { + fn dojo_name(self: @T) -> ByteArray; +} + +#[starknet::contract] +mod invalid_legacy_model { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl InvalidModelMetadata of super::IMetadataOnly { + fn dojo_name(self: @ContractState) -> ByteArray { + "invalid_legacy_model" + } + } +} + +#[starknet::contract] +mod invalid_legacy_model_world { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl InvalidModelName of super::IMetadataOnly { + fn dojo_name(self: @ContractState) -> ByteArray { + "invalid_legacy_model" + } + } +} + +#[starknet::contract] +mod invalid_model { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl InvalidModelSelector of super::IMetadataOnly { + fn dojo_name(self: @ContractState) -> ByteArray { + "invalid_model" + } + } +} + +#[starknet::contract] +mod invalid_model_world { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl InvalidModelSelector of super::IMetadataOnly { + fn dojo_name(self: @ContractState) -> ByteArray { + "invalid_model_world" + } + } +} + +#[test] +#[available_gas(6000000)] +#[should_panic(expected: ("Namespace `` is invalid according to Dojo naming rules: ^[a-zA-Z0-9_]+$", 'ENTRYPOINT_FAILED',))] +fn test_register_namespace_empty_name() { + let world = deploy_world(); + + world.register_namespace(""); +} diff --git a/crates/contracts/src/tests/event/event.cairo b/crates/core-cairo-test/src/tests/event/event.cairo similarity index 74% rename from crates/contracts/src/tests/event/event.cairo rename to crates/core-cairo-test/src/tests/event/event.cairo index b824419..2c8e3e3 100644 --- a/crates/contracts/src/tests/event/event.cairo +++ b/crates/core-cairo-test/src/tests/event/event.cairo @@ -13,8 +13,6 @@ fn test_event_definition() { let definition = dojo::event::Event::::definition(); assert_eq!(definition.name, dojo::event::Event::::name()); - assert_eq!(definition.namespace, dojo::event::Event::::namespace()); - assert_eq!(definition.namespace_selector, dojo::event::Event::::namespace_hash()); assert_eq!(definition.version, dojo::event::Event::::version()); assert_eq!(definition.layout, dojo::event::Event::::layout()); assert_eq!(definition.schema, dojo::event::Event::::schema()); diff --git a/crates/contracts/src/tests/expanded/selector_attack.cairo b/crates/core-cairo-test/src/tests/expanded/selector_attack.cairo similarity index 53% rename from crates/contracts/src/tests/expanded/selector_attack.cairo rename to crates/core-cairo-test/src/tests/expanded/selector_attack.cairo index bc27c3c..17f1266 100644 --- a/crates/contracts/src/tests/expanded/selector_attack.cairo +++ b/crates/core-cairo-test/src/tests/expanded/selector_attack.cairo @@ -15,35 +15,14 @@ pub mod attacker_contract { #[abi(embed_v0)] pub impl ContractImpl of IContract { - fn name(self: @ContractState) -> ByteArray { + fn dojo_name(self: @ContractState) -> ByteArray { "test_1" } - - fn namespace(self: @ContractState) -> ByteArray { - "ns1" - } - - fn tag(self: @ContractState) -> ByteArray { - "other tag" - } - - fn name_hash(self: @ContractState) -> felt252 { - 'name hash' - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@"atk") - } - - fn selector(self: @ContractState) -> felt252 { - // Targetting a resource that exists in an other namespace. - selector_from_tag!("dojo-Foo") - } } #[abi(embed_v0)] impl WorldProviderImpl of IWorldProvider { - fn world(self: @ContractState) -> IWorldDispatcher { + fn world_dispatcher(self: @ContractState) -> IWorldDispatcher { self.world_dispatcher.read() } } @@ -56,35 +35,14 @@ pub mod attacker_model { #[abi(embed_v0)] impl DojoModelImpl of dojo::model::IModel { - fn name(self: @ContractState) -> ByteArray { - "m1" - } - - fn namespace(self: @ContractState) -> ByteArray { - "ns1" - } - - fn tag(self: @ContractState) -> ByteArray { - "other tag" + fn dojo_name(self: @ContractState) -> ByteArray { + "foo" } fn version(self: @ContractState) -> u8 { 1 } - fn selector(self: @ContractState) -> felt252 { - // Targetting a resource that exists in an other namespace. - selector_from_tag!("dojo-Foo") - } - - fn name_hash(self: @ContractState) -> felt252 { - 'name hash' - } - - fn namespace_hash(self: @ContractState) -> felt252 { - dojo::utils::bytearray_hash(@"atk") - } - fn unpacked_size(self: @ContractState) -> Option { Option::None } @@ -103,12 +61,8 @@ pub mod attacker_model { fn definition(self: @ContractState) -> dojo::model::ModelDef { dojo::model::ModelDef { - name: Self::name(self), - namespace: Self::namespace(self), + name: Self::dojo_name(self), version: Self::version(self), - selector: Self::selector(self), - name_hash: Self::name_hash(self), - namespace_hash: Self::namespace_hash(self), layout: Self::layout(self), schema: Self::schema(self), packed_size: Self::packed_size(self), diff --git a/crates/core-cairo-test/src/tests/helpers.cairo b/crates/core-cairo-test/src/tests/helpers.cairo new file mode 100644 index 0000000..b885ed9 --- /dev/null +++ b/crates/core-cairo-test/src/tests/helpers.cairo @@ -0,0 +1,264 @@ +use starknet::ContractAddress; + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, IWorldTestDispatcher, IWorldTestDispatcherTrait}; +use dojo::model::{Model, ModelStorage}; + +use crate::world::{deploy_with_world_address, spawn_test_world, NamespaceDef, TestResource, ContractDefTrait}; + +pub const DOJO_NSH: felt252 = 0x309e09669bc1fdc1dd6563a7ef862aa6227c97d099d08cc7b81bad58a7443fa; + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::event] +pub struct SimpleEvent { + #[key] + pub id: u32, + pub data: (felt252, felt252), +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +pub struct Foo { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[starknet::contract] +pub mod foo_invalid_name { + use dojo::model::IModel; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + pub impl ModelImpl of IModel { + fn dojo_name(self: @ContractState) -> ByteArray { + "foo-bis" + } + + fn version(self: @ContractState) -> u8 { + 1 + } + + fn unpacked_size(self: @ContractState) -> Option { + Option::None + } + + fn packed_size(self: @ContractState) -> Option { + Option::None + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::Layout::Fixed([].span()) + } + + fn schema(self: @ContractState) -> dojo::meta::introspect::Ty { + dojo::meta::introspect::Ty::Struct( + dojo::meta::introspect::Struct { + name: 'foo', attrs: [].span(), children: [].span() + } + ) + } + + fn definition(self: @ContractState) -> dojo::model::ModelDef { + dojo::model::ModelDef { + name: Self::dojo_name(self), + version: Self::version(self), + layout: Self::layout(self), + schema: Self::schema(self), + packed_size: Self::packed_size(self), + unpacked_size: Self::unpacked_size(self), + } + } + } +} + +#[starknet::interface] +pub trait IFooSetter { + fn set_foo(ref self: T, a: felt252, b: u128); +} + +#[dojo::contract] +pub mod foo_setter { + use super::{Foo, IFooSetter}; + use dojo::model::ModelStorage; + + #[abi(embed_v0)] + impl IFooSetterImpl of IFooSetter { + fn set_foo(ref self: ContractState, a: felt252, b: u128) { + let mut world = self.world("dojo"); + world.write_model(@Foo { caller: starknet::get_caller_address(), a, b }); + } + } +} + +#[dojo::contract] +pub mod test_contract {} + +#[dojo::contract] +pub mod test_contract_with_dojo_init_args { + fn dojo_init(ref self: ContractState, arg1: felt252) { + let _a = arg1; + } +} + +#[derive(IntrospectPacked, Copy, Drop, Serde)] +pub struct Sword { + pub swordsmith: ContractAddress, + pub damage: u32, +} + +#[derive(IntrospectPacked, Copy, Drop, Serde)] +#[dojo::model] +pub struct Case { + #[key] + pub owner: ContractAddress, + pub sword: Sword, + pub material: felt252, +} + +#[derive(IntrospectPacked, Copy, Drop, Serde)] +#[dojo::model] +pub struct Character { + #[key] + pub caller: ContractAddress, + pub heigth: felt252, + pub abilities: Abilities, + pub stats: Stats, + pub weapon: Weapon, + pub gold: u32, +} + +#[derive(IntrospectPacked, Copy, Drop, Serde)] +pub struct Abilities { + pub strength: u8, + pub dexterity: u8, + pub constitution: u8, + pub intelligence: u8, + pub wisdom: u8, + pub charisma: u8, +} + +#[derive(IntrospectPacked, Copy, Drop, Serde)] +pub struct Stats { + pub kills: u128, + pub deaths: u16, + pub rests: u32, + pub hits: u64, + pub blocks: u32, + pub walked: felt252, + pub runned: felt252, + pub finished: bool, + pub romances: u16, +} + +#[derive(IntrospectPacked, Copy, Drop, Serde)] +pub enum Weapon { + DualWield: (Sword, Sword), + Fists: (Sword, Sword), // Introspect requires same arms +} + +#[starknet::interface] +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] +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 super::{Foo, IWorldDispatcher, IWorldDispatcherTrait}; + use super::{Character, Abilities, Stats, Weapon, Sword}; + + #[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 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 }); + } + + fn set_char(self: @ContractState, a: felt252, b: u32) { + } + } +} + +/// Deploys an empty world with the `dojo` namespace. +pub fn deploy_world() -> IWorldDispatcher { + let namespace_def = NamespaceDef { + namespace: "dojo", + resources: [].span(), + }; + + 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", foo::TEST_CLASS_HASH.try_into().unwrap()); + let foo_selector = Model::::selector(DOJO_NSH); + + (world, foo_selector) +} + +/// 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) { + let namespace_def = NamespaceDef { + namespace: "dojo", + resources: [ + TestResource::Model(foo::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Contract(ContractDefTrait::new(bar::TEST_CLASS_HASH, "bar")), + ].span(), + }; + + let world = spawn_test_world([namespace_def].span()); + let bar_address = IWorldTestDispatcher { contract_address: world.contract_address }.dojo_contract_address(selector_from_tag!("dojo-bar")); + + let bar_contract = IbarDispatcher { contract_address: bar_address }; + + world.grant_writer(Model::::selector(DOJO_NSH), bar_address); + + (world, bar_contract) +} + +pub fn drop_all_events(address: ContractAddress) { + loop { + match starknet::testing::pop_log_raw(address) { + core::option::Option::Some(_) => {}, + core::option::Option::None => { break; }, + }; + } +} diff --git a/crates/contracts/src/tests/meta/introspect.cairo b/crates/core-cairo-test/src/tests/meta/introspect.cairo similarity index 100% rename from crates/contracts/src/tests/meta/introspect.cairo rename to crates/core-cairo-test/src/tests/meta/introspect.cairo diff --git a/crates/core-cairo-test/src/tests/model/model.cairo b/crates/core-cairo-test/src/tests/model/model.cairo new file mode 100644 index 0000000..b652a71 --- /dev/null +++ b/crates/core-cairo-test/src/tests/model/model.cairo @@ -0,0 +1,207 @@ +use dojo::model::{Model, ModelValue, ModelStorage, ModelValueStorage, ModelMemberStorage}; +use dojo::world::{IWorldDispatcherTrait, WorldStorageTrait, WorldStorage}; + +use crate::tests::helpers::{deploy_world}; + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct Foo { + #[key] + k1: u8, + #[key] + k2: felt252, + v1: u128, + v2: u32 +} + + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +struct Foo2 { + #[key] + k1: u8, + #[key] + k2: felt252, + v1: u128, + v2: u32 +} + +#[test] +fn test_model_definition() { + let definition = dojo::model::Model::::definition(); + + assert_eq!(definition.name, dojo::model::Model::::name()); + assert_eq!(definition.version, dojo::model::Model::::version()); + assert_eq!(definition.layout, dojo::model::Model::::layout()); + assert_eq!(definition.schema, dojo::model::Model::::schema()); + assert_eq!(definition.packed_size, dojo::model::Model::::packed_size()); + assert_eq!(definition.unpacked_size, dojo::meta::introspect::Introspect::::size()); +} + +#[test] +fn test_values() { + let mvalues = FooValue { v1: 3, v2: 4 }; + let expected_values = [3, 4].span(); + + let values = mvalues.values(); + assert!(expected_values == values); +} + +#[test] +fn test_from_values() { + let mut values = [3, 4].span(); + + let model_values: Option = ModelValue::::from_values(1, ref values); + assert!(model_values.is_some()); + let model_values = model_values.unwrap(); + assert!(model_values.v1 == 3 && model_values.v2 == 4); +} + +#[test] +fn test_from_values_bad_data() { + let mut values = [3].span(); + let res: Option = ModelValue::::from_values(1, ref values); + assert!(res.is_none()); +} + +#[test] +fn test_get_and_update_model_value() { + let world = deploy_world(); + world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap()); + + let mut world = WorldStorageTrait::new(world, "dojo"); + + let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; + world.write_model(@foo); + + let entity_id = foo.entity_id(); + let mut model_value: FooValue = world.read_model_value(foo.key()); + assert_eq!(model_value.v1, foo.v1); + assert_eq!(model_value.v2, foo.v2); + + model_value.v1 = 12; + model_value.v2 = 18; + + world.write_model_value_from_id(entity_id, @model_value); + + let read_values: FooValue = world.read_model_value(foo.key()); + assert!(read_values.v1 == model_value.v1 && read_values.v2 == model_value.v2); +} + +#[test] +fn test_delete_model_value() { + let world = deploy_world(); + world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap()); + + let mut world = WorldStorageTrait::new(world, "dojo"); + + let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; + world.write_model(@foo); + + let entity_id = foo.entity_id(); + ModelStorage::::erase_model(ref world, @foo); + + let read_values: FooValue = world.read_model_value_from_id(entity_id); + assert!(read_values.v1 == 0 && read_values.v2 == 0); +} + +#[test] +fn test_get_and_set_field_name() { + let world = deploy_world(); + world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap()); + + let mut world = WorldStorageTrait::new(world, "dojo"); + + let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; + world.write_model(@foo); + + // Inference fails here, we need something better without too generics + // which also fails. + let v1 = world.read_member(foo.key(), selector!("v1")); + assert!(foo.v1 == v1); + + world.write_member_from_id(foo.entity_id(), selector!("v1"), 42); + + let v1 = world.read_member_from_id(foo.key(), selector!("v1")); + assert!(v1 == 42); +} + +#[test] +fn test_get_and_set_from_model() { + let world = deploy_world(); + world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap()); + + let mut world = WorldStorageTrait::new(world, "dojo"); + + let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; + world.write_model(@foo); + + let foo2: Foo = world.read_model((foo.k1, foo.k2)); + + assert!( + foo.k1 == foo2.k1 + && foo.k2 == foo2.k2 + && foo.v1 == foo2.v1 + && foo.v2 == foo2.v2 + ); +} + +#[test] +fn test_delete_from_model() { + let world = deploy_world(); + world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap()); + + let mut world = WorldStorageTrait::new(world, "dojo"); + + let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; + world.write_model(@foo); + world.erase_model(@foo); + + let foo2: Foo = world.read_model((foo.k1, foo.k2)); + assert!( + foo2.k1 == foo.k1 + && foo2.k2 == foo.k2 + && foo2.v1 == 0 + && foo2.v2 == 0 + ); +} + +#[test] +fn test_get_and_set_member_from_model() { + let world = deploy_world(); + world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap()); + + let mut world = WorldStorageTrait::new(world, "dojo"); + + let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; + world.write_model(@foo); + + let key: (u8, felt252) = foo.key(); + let v1: u128 = world.read_member(key, selector!("v1")); + + assert!(v1 == 3); + + world.write_member(key, selector!("v1"), 42); + let foo: Foo = world.read_model(key); + assert!(foo.v1 == 42); +} + +#[test] +fn test_get_and_set_field_name_from_model() { + let world = deploy_world(); + world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap()); + + let mut world = WorldStorageTrait::new(world, "dojo"); + + let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 }; + world.write_model(@foo); + + // Currently we don't have automatic field id computation. To be done. + // @remy/@ben. + + let v1 = world.read_member((foo.k1, foo.k2), selector!("v1")); + assert!(v1 == 3); + + world.write_member((foo.k1, foo.k2), selector!("v1"), 42); + assert!(v1 == 42); +} diff --git a/crates/contracts/src/tests/storage/database.cairo b/crates/core-cairo-test/src/tests/storage/database.cairo similarity index 100% rename from crates/contracts/src/tests/storage/database.cairo rename to crates/core-cairo-test/src/tests/storage/database.cairo diff --git a/crates/contracts/src/tests/storage/packing.cairo b/crates/core-cairo-test/src/tests/storage/packing.cairo similarity index 100% rename from crates/contracts/src/tests/storage/packing.cairo rename to crates/core-cairo-test/src/tests/storage/packing.cairo diff --git a/crates/contracts/src/tests/storage/storage.cairo b/crates/core-cairo-test/src/tests/storage/storage.cairo similarity index 100% rename from crates/contracts/src/tests/storage/storage.cairo rename to crates/core-cairo-test/src/tests/storage/storage.cairo diff --git a/crates/core-cairo-test/src/tests/utils/hash.cairo b/crates/core-cairo-test/src/tests/utils/hash.cairo new file mode 100644 index 0000000..45691f8 --- /dev/null +++ b/crates/core-cairo-test/src/tests/utils/hash.cairo @@ -0,0 +1,20 @@ +use dojo::model::Model; +use dojo::utils::{bytearray_hash, selector_from_names}; + +use crate::tests::helpers::DOJO_NSH; + +#[derive(Drop, Copy, Serde)] +#[dojo::model] +struct MyModel { + #[key] + x: u8, + y: u8 +} + +#[test] +fn test_selector_computation() { + let namespace = "dojo"; + let name = Model::::name(); + let selector = selector_from_names(@namespace, @name); + assert(selector == Model::::selector(DOJO_NSH), 'invalid computed selector'); +} diff --git a/crates/contracts/src/tests/utils/key.cairo b/crates/core-cairo-test/src/tests/utils/key.cairo similarity index 100% rename from crates/contracts/src/tests/utils/key.cairo rename to crates/core-cairo-test/src/tests/utils/key.cairo diff --git a/crates/contracts/src/tests/utils/layout.cairo b/crates/core-cairo-test/src/tests/utils/layout.cairo similarity index 100% rename from crates/contracts/src/tests/utils/layout.cairo rename to crates/core-cairo-test/src/tests/utils/layout.cairo diff --git a/crates/contracts/src/tests/utils/misc.cairo b/crates/core-cairo-test/src/tests/utils/misc.cairo similarity index 100% rename from crates/contracts/src/tests/utils/misc.cairo rename to crates/core-cairo-test/src/tests/utils/misc.cairo diff --git a/crates/contracts/src/tests/utils/naming.cairo b/crates/core-cairo-test/src/tests/utils/naming.cairo similarity index 100% rename from crates/contracts/src/tests/utils/naming.cairo rename to crates/core-cairo-test/src/tests/utils/naming.cairo diff --git a/crates/contracts/src/tests/world/acl.cairo b/crates/core-cairo-test/src/tests/world/acl.cairo similarity index 72% rename from crates/contracts/src/tests/world/acl.cairo rename to crates/core-cairo-test/src/tests/world/acl.cairo index b0e4bdd..b5ac148 100644 --- a/crates/contracts/src/tests/world/acl.cairo +++ b/crates/core-cairo-test/src/tests/world/acl.cairo @@ -1,17 +1,16 @@ -use dojo::model::Model; +use dojo::model::{Model, ModelStorage}; +use dojo::event::EventStorage; use dojo::utils::bytearray_hash; use dojo::world::IWorldDispatcherTrait; -use dojo::tests::helpers::{ - deploy_world, Foo, foo, foo_setter, IFooSetterDispatcher, IFooSetterDispatcherTrait +use crate::tests::helpers::{ + deploy_world, Foo, foo, foo_setter, IFooSetterDispatcher, IFooSetterDispatcherTrait, DOJO_NSH, deploy_world_and_foo }; -use dojo::tests::expanded::selector_attack::{attacker_contract, attacker_model}; +use crate::tests::expanded::selector_attack::{attacker_model, attacker_contract}; #[test] fn test_owner() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -45,9 +44,7 @@ fn test_grant_owner_not_registered_resource() { #[test] #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_grant_owner_through_malicious_contract() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -64,14 +61,12 @@ fn test_grant_owner_through_malicious_contract() { #[test] #[should_panic( expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `dojo-Foo`", + "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", 'ENTRYPOINT_FAILED' ) )] fn test_grant_owner_fails_for_non_owner() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -85,9 +80,7 @@ fn test_grant_owner_fails_for_non_owner() { #[test] #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_revoke_owner_through_malicious_contract() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -105,14 +98,12 @@ fn test_revoke_owner_through_malicious_contract() { #[test] #[should_panic( expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `dojo-Foo`", + "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", 'ENTRYPOINT_FAILED' ) )] fn test_revoke_owner_fails_for_non_owner() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -128,9 +119,7 @@ fn test_revoke_owner_fails_for_non_owner() { #[test] #[available_gas(6000000)] fn test_writer() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); assert(!world.is_writer(foo_selector, 69.try_into().unwrap()), 'should not be writer'); @@ -152,9 +141,7 @@ fn test_writer_not_registered_resource() { #[test] #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_grant_writer_through_malicious_contract() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -171,14 +158,12 @@ fn test_grant_writer_through_malicious_contract() { #[test] #[should_panic( expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `dojo-Foo`", + "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", 'ENTRYPOINT_FAILED' ) )] fn test_grant_writer_fails_for_non_owner() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -192,9 +177,7 @@ fn test_grant_writer_fails_for_non_owner() { #[test] #[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] fn test_revoke_writer_through_malicious_contract() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -212,14 +195,12 @@ fn test_revoke_writer_through_malicious_contract() { #[test] #[should_panic( expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `dojo-Foo`", + "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", 'ENTRYPOINT_FAILED' ) )] fn test_revoke_writer_fails_for_non_owner() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); @@ -235,14 +216,13 @@ fn test_revoke_writer_fails_for_non_owner() { #[test] #[should_panic( expected: ( - "Contract `dojo-foo_setter` does NOT have WRITER role on model (or its namespace) `dojo-Foo`", + "Contract `foo_setter` does NOT have WRITER role on model (or its namespace) `Foo`", 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED' ) )] fn test_not_writer_with_known_contract() { - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); + let (world, _) = deploy_world_and_foo(); let account = starknet::contract_address_const::<0xb0b>(); world.grant_owner(bytearray_hash(@"dojo"), account); @@ -253,9 +233,12 @@ fn test_not_writer_with_known_contract() { starknet::testing::set_contract_address(account); let contract_address = world - .register_contract('salt1', foo_setter::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", foo_setter::TEST_CLASS_HASH.try_into().unwrap()); + let d = IFooSetterDispatcher { contract_address }; d.set_foo(1, 2); + + core::panics::panic_with_byte_array(@"Contract `dojo-foo_setter` does NOT have WRITER role on model (or its namespace) `Foo`"); } /// Test that an attacker can't control the hashes of resources in other namespaces @@ -263,11 +246,11 @@ fn test_not_writer_with_known_contract() { #[test] #[should_panic( expected: ( - "Descriptor: `selector` mismatch, expected `131865267188622158278053964160834676621529874568090955194814616371745985007` but found `3123252206139358744730647958636922105676576163624049771737508399526017186883`", + "Account `7022365680606078322` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED', ) )] -fn test_attacker_control_hashes_model_registration() { +fn test_register_model_namespace_not_owner() { let owner = starknet::contract_address_const::<'owner'>(); let attacker = starknet::contract_address_const::<'attacker'>(); @@ -275,10 +258,7 @@ fn test_attacker_control_hashes_model_registration() { starknet::testing::set_contract_address(owner); // Owner deploys the world and register Foo model. - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo_selector = Model::::selector(); + let (world, foo_selector) = deploy_world_and_foo(); assert(world.is_owner(foo_selector, owner), 'should be owner'); @@ -288,8 +268,8 @@ fn test_attacker_control_hashes_model_registration() { // Attacker has control over the this namespace. world.register_namespace("atk"); - // Attacker can't take ownership of the Foo model. - world.register_model(attacker_model::TEST_CLASS_HASH.try_into().unwrap()); + // Attacker can't take ownership of the Foo model in the dojo namespace. + world.register_model("dojo", attacker_model::TEST_CLASS_HASH.try_into().unwrap()); } /// Test that an attacker can't control the hashes of resources in other namespaces @@ -297,11 +277,11 @@ fn test_attacker_control_hashes_model_registration() { #[test] #[should_panic( expected: ( - "Descriptor: `selector` mismatch, expected `2256968028355087182573300510211413559640627226911800172611266486245255986230` but found `3123252206139358744730647958636922105676576163624049771737508399526017186883`", + "Account `7022365680606078322` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED', ) )] -fn test_attacker_control_hashes_contract_deployment() { +fn test_register_contract_namespace_not_owner() { let owner = starknet::contract_address_const::<'owner'>(); let attacker = starknet::contract_address_const::<'attacker'>(); @@ -309,11 +289,7 @@ fn test_attacker_control_hashes_contract_deployment() { starknet::testing::set_contract_address(owner); // Owner deploys the world and register Foo model. - let world = deploy_world(); - world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - - let foo_selector = Model::::selector(); - + let (world, foo_selector) = deploy_world_and_foo(); assert(world.is_owner(foo_selector, owner), 'should be owner'); starknet::testing::set_contract_address(attacker); @@ -323,5 +299,5 @@ fn test_attacker_control_hashes_contract_deployment() { world.register_namespace("atk"); // Attacker can't take ownership of the Foo model. - world.register_contract('salt1', attacker_contract::TEST_CLASS_HASH.try_into().unwrap()); + world.register_contract('salt1', "dojo", attacker_contract::TEST_CLASS_HASH.try_into().unwrap()); } diff --git a/crates/contracts/src/tests/world/entities.cairo b/crates/core-cairo-test/src/tests/world/entities.cairo similarity index 100% rename from crates/contracts/src/tests/world/entities.cairo rename to crates/core-cairo-test/src/tests/world/entities.cairo diff --git a/crates/contracts/src/tests/world/resources.cairo b/crates/core-cairo-test/src/tests/world/resources.cairo similarity index 100% rename from crates/contracts/src/tests/world/resources.cairo rename to crates/core-cairo-test/src/tests/world/resources.cairo diff --git a/crates/contracts/src/tests/world/world.cairo b/crates/core-cairo-test/src/tests/world/world.cairo similarity index 100% rename from crates/contracts/src/tests/world/world.cairo rename to crates/core-cairo-test/src/tests/world/world.cairo diff --git a/crates/core-cairo-test/src/utils.cairo b/crates/core-cairo-test/src/utils.cairo new file mode 100644 index 0000000..12fc24a --- /dev/null +++ b/crates/core-cairo-test/src/utils.cairo @@ -0,0 +1,58 @@ + +#[derive(Drop)] +pub struct GasCounter { + pub start: u128, +} + +#[generate_trait] +pub impl GasCounterImpl of GasCounterTrait { + fn start() -> GasCounter { + let start = core::testing::get_available_gas(); + core::gas::withdraw_gas().unwrap(); + GasCounter { start } + } + + fn end(self: GasCounter, name: ByteArray) { + let end = core::testing::get_available_gas(); + let gas_used = self.start - end; + + println!("# GAS # {}: {}", Self::pad_start(name, 18), gas_used); + core::gas::withdraw_gas().unwrap(); + } + + fn pad_start(str: ByteArray, len: u32) -> ByteArray { + let mut missing: ByteArray = ""; + let missing_len = if str.len() >= len { + 0 + } else { + len - str.len() + }; + + while missing.len() < missing_len { + missing.append(@"."); + }; + missing + str + } +} + +// assert that `value` and `expected` have the same size and the same content +pub fn assert_array(value: Span, expected: Span) { + assert!(value.len() == expected.len(), "Bad array length"); + + let mut i = 0; + loop { + if i >= value.len() { + break; + } + + assert!( + *value.at(i) == *expected.at(i), + "Bad array value [{}] (expected: {} got: {})", + i, + *expected.at(i), + *value.at(i) + ); + + i += 1; + } +} diff --git a/crates/core-cairo-test/src/world.cairo b/crates/core-cairo-test/src/world.cairo new file mode 100644 index 0000000..2e29d1f --- /dev/null +++ b/crates/core-cairo-test/src/world.cairo @@ -0,0 +1,149 @@ +use core::option::OptionTrait; +use core::result::ResultTrait; +use core::traits::{Into, TryInto}; + +use starknet::{ContractAddress, ClassHash, syscalls::deploy_syscall}; + +use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; + +/// 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. +#[derive(Drop)] +pub enum TestResource { + Event: ClassHash, + Model: ClassHash, + Contract: ContractDef, +} + +#[derive(Drop)] +pub struct NamespaceDef { + pub namespace: ByteArray, + pub resources: Span, +} + +#[derive(Drop)] +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, + /// 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, +} + +#[generate_trait] +pub impl ContractDefImpl of ContractDefTrait { + fn new(class_hash: felt252, name: ByteArray) -> ContractDef { + ContractDef { class_hash, name, init_calldata: [].span(), writer_of: [].span(), owner_of: [].span() } + } + + fn with_init_calldata(mut self: ContractDef, init_calldata: Span) -> ContractDef { + self.init_calldata = init_calldata; + self + } + + fn with_writer_of(mut self: ContractDef, writer_of: Span) -> ContractDef { + self.writer_of = writer_of; + self + } + + fn with_owner_of(mut self: ContractDef, owner_of: Span) -> ContractDef { + self.owner_of = owner_of; + self + } +} + +/// Deploy classhash with calldata for constructor +/// +/// # Arguments +/// +/// * `class_hash` - Class to deploy +/// * `calldata` - calldata for constructor +/// +/// # Returns +/// * address of contract deployed +pub fn deploy_contract(class_hash: felt252, calldata: Span) -> ContractAddress { + let (contract, _) = starknet::syscalls::deploy_syscall( + class_hash.try_into().unwrap(), 0, calldata, false + ) + .unwrap(); + contract +} + +/// Deploy classhash and passes in world address to constructor +/// +/// # Arguments +/// +/// * `class_hash` - Class to deploy +/// * `world` - World dispatcher to pass as world address +/// +/// # Returns +/// * address of contract deployed +pub fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) -> ContractAddress { + deploy_contract(class_hash, [world.contract_address.into()].span()) +} + +/// Spawns a test world registering provided resources into namespaces. +/// +/// # Arguments +/// +/// * `namespaces_defs` - Definitions of namespaces to register. +/// +/// # Returns +/// +/// * World dispatcher +pub fn spawn_test_world(namespaces_defs: Span) -> IWorldDispatcher { + let salt = core::testing::get_available_gas(); + + let (world_address, _) = deploy_syscall( + world::TEST_CLASS_HASH.try_into().unwrap(), + salt.into(), + [world::TEST_CLASS_HASH].span(), + false + ) + .expect('world deploy failed'); + + let world = IWorldDispatcher { contract_address: world_address }; + + for ns in namespaces_defs { + let namespace = ns.namespace.clone(); + world.register_namespace(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); + }, + } + } + }; + + world +} diff --git a/examples/dojo_simple/Scarb.lock b/examples/dojo_simple/Scarb.lock deleted file mode 100644 index 1e51efe..0000000 --- a/examples/dojo_simple/Scarb.lock +++ /dev/null @@ -1,13 +0,0 @@ -# Code generated by scarb DO NOT EDIT. -version = 1 - -[[package]] -name = "dojo" -version = "1.0.0-rc.1" - -[[package]] -name = "dojo_simple" -version = "0.1.0" -dependencies = [ - "dojo", -] diff --git a/examples/dojo_simple/Scarb.toml b/examples/dojo_simple/Scarb.toml deleted file mode 100644 index 4826387..0000000 --- a/examples/dojo_simple/Scarb.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -cairo-version = "=2.8.4" -name = "dojo_simple" -version = "0.1.0" -edition = "2024_07" - -[dependencies] -dojo = { path = "../../crates/contracts" } - -[[target.dojo]] - -[features] -default = [] diff --git a/examples/dojo_simple/dojo_dev.toml b/examples/dojo_simple/dojo_dev.toml deleted file mode 100644 index c5848f1..0000000 --- a/examples/dojo_simple/dojo_dev.toml +++ /dev/null @@ -1,2 +0,0 @@ -[namespace] -default = "ds" diff --git a/examples/dojo_simple/src/lib.cairo b/examples/dojo_simple/src/lib.cairo deleted file mode 100644 index 43f3268..0000000 --- a/examples/dojo_simple/src/lib.cairo +++ /dev/null @@ -1,158 +0,0 @@ -pub mod models { - use starknet::ContractAddress; - - #[derive(Drop, Serde)] - #[dojo::model(namespace: "ns1")] - pub struct UnmappedModel { - #[key] - pub id: u32, - pub data: u32, - } - - #[derive(Drop, Serde)] - #[dojo::model] - pub struct Position { - #[key] - pub player: ContractAddress, - pub x: u32, - pub y: u32, - } - - #[derive(Drop, Serde)] - #[dojo::model] - pub struct ModelA { - #[key] - pub id: u32, - pub a: felt252, - } - - - #[derive(Drop, Serde)] - pub struct Point { - pub x: u32, - pub y: u32, - } -} - -pub mod events { - use starknet::ContractAddress; - - #[derive(Drop, Serde)] - #[dojo::event] - pub struct PositionUpdated { - #[key] - pub player: ContractAddress, - pub new_x: u32, - pub new_y: u32, - } -} - -#[dojo::interface] -pub trait IActions { - fn spawn(ref world: IWorldDispatcher); - fn despawn(ref world: IWorldDispatcher); - fn move(ref world: IWorldDispatcher, new_x: u32, new_y: u32); - fn get_position(world: @IWorldDispatcher) -> models::Point; - fn get_x(world: @IWorldDispatcher) -> u32; - fn get_y(world: @IWorldDispatcher) -> u32; -} - -#[dojo::contract] -pub mod actions { - use dojo::model::ModelStore; - use super::{ - IActions, - models::{Position, PositionEntity, PositionMembersStore, Point}, - events::PositionUpdated - }; - - #[derive(Drop, Serde)] - #[dojo::model] - pub struct ModelInContract { - #[key] - pub id: u32, - pub a: u8, - } - - fn dojo_init(ref world: IWorldDispatcher, id: u32, a: u8) { - let m = ModelInContract { id, a }; - world.set(@m); - } - - #[abi(embed_v0)] - impl ActionsImpl of IActions { - fn spawn(ref world: IWorldDispatcher) { - let caller = starknet::get_caller_address(); - - let position = Position { player: caller, x: 1, y: 2 }; - - world.set(@position); - let _position: Position = world.get(caller); - emit!(world, PositionUpdated { player: caller, new_x: 1, new_y: 2 }); - } - - fn despawn(ref world: IWorldDispatcher) { - let caller = starknet::get_caller_address(); - ModelStore::::delete_from_key(world, caller); - } - - fn move(ref world: IWorldDispatcher, new_x: u32, new_y: u32) { - let caller = starknet::get_caller_address(); - let mut position: Position = world.get(caller); - position.x = new_x; - position.y = new_y; - world.set(@position); - emit!(world, PositionUpdated { player: caller, new_x, new_y }); - } - - fn get_position(world: @IWorldDispatcher) -> Point { - let caller = starknet::get_caller_address(); - let entity: PositionEntity = world.get_entity(caller); - Point { x: entity.x, y: entity.y } - } - - fn get_x(world: @IWorldDispatcher) -> u32 { - let caller = starknet::get_caller_address(); - PositionMembersStore::get_x(@world, caller) - } - - fn get_y(world: @IWorldDispatcher) -> u32 { - let caller = starknet::get_caller_address(); - PositionMembersStore::get_y(@world, caller) - } - } -} - -#[starknet::contract] -pub mod sn_actions { - #[storage] - struct Storage {} -} - -#[cfg(test)] -mod tests { - use dojo::world::IWorldDispatcherTrait; - use dojo::model::ModelStore; - use super::actions::ModelInContract; - - #[test] - fn test_spawn_world_full() { - let _world = spawn_test_world!(); - } - - #[test] - fn test_dojo_init_flow() { - let world = spawn_test_world!(); - - let actions_addr = world - .register_contract('salt1', super::actions::TEST_CLASS_HASH.try_into().unwrap()); - - world.grant_writer(dojo::utils::bytearray_hash(@"ds"), actions_addr); - - world.init_contract(selector_from_tag!("ds-actions"), [10, 20].span()); - - let model: ModelInContract = world.get(10); - assert_eq!(model.a, 20); - } -} - diff --git a/examples/dojo_simple/.gitignore b/examples/simple/.gitignore similarity index 100% rename from examples/dojo_simple/.gitignore rename to examples/simple/.gitignore diff --git a/examples/simple/.snfoundry_cache/.prev_tests_failed b/examples/simple/.snfoundry_cache/.prev_tests_failed new file mode 100644 index 0000000..e69de29 diff --git a/examples/simple/Scarb.lock b/examples/simple/Scarb.lock new file mode 100644 index 0000000..b3e5340 --- /dev/null +++ b/examples/simple/Scarb.lock @@ -0,0 +1,29 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "1.0.0-rc.0" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_cairo_test" +version = "1.0.0-rc.0" +dependencies = [ + "dojo", +] + +[[package]] +name = "dojo_plugin" +version = "2.8.4" +source = "git+https://github.com/dojoengine/dojo?branch=feat%2Fdojo-1-rc0#3bf1276163fe9dbfe0f65775de8363e358510c77" + +[[package]] +name = "dojo_simple" +version = "0.1.0" +dependencies = [ + "dojo", + "dojo_cairo_test", +] diff --git a/examples/simple/Scarb.toml b/examples/simple/Scarb.toml new file mode 100644 index 0000000..3dafb37 --- /dev/null +++ b/examples/simple/Scarb.toml @@ -0,0 +1,20 @@ +[package] +cairo-version = "=2.8.4" +name = "dojo_simple" +version = "0.1.0" +edition = "2024_07" + +[[target.starknet-contract]] +sierra = true +build-external-contracts = ["dojo::world::world_contract::world"] + +[dependencies] +dojo = { path = "../../crates/contracts" } +starknet = "2.8.4" + +[dev-dependencies] +dojo_cairo_test = { path = "../../crates/core-cairo-test" } +cairo_test = "2.8.4" + +[features] +default = [] diff --git a/examples/simple/dojo_dev.toml b/examples/simple/dojo_dev.toml new file mode 100644 index 0000000..d34f3d1 --- /dev/null +++ b/examples/simple/dojo_dev.toml @@ -0,0 +1,27 @@ +[world] +description = "Simple world." +name = "simple" +seed = "simple" + +[namespace] +default = "ns" + +[env] +rpc_url = "http://localhost:5050/" +# Default account for katana with seed = 0 +account_address = "0x127fd5f1fe78a71f8bcd1fec63e3fe2f0486b6ecd5c86a0466c3a21fa5cfcec" +private_key = "0xc5b2fcab997346f3ea1c00b002ecf6f382c5f9c9659a3894eb783c5320f912" +world_address = "0x077c0dc7c1aba7f8842aff393ce6aa71fa675b4ced1bc927f7fc971b6acd92fc" + +[init_call_args] +"ns-c1" = ["0xfffe"] + +[writers] +"ns" = ["ns-c1", "ns-c2"] +"ns-M" = ["ns-c2", "ns-c1"] + +[owners] +"ns" = ["ns-c1"] + +[migration] +order_inits = ["ns-c2", "ns-c1"] diff --git a/examples/simple/manifest_dev.json b/examples/simple/manifest_dev.json new file mode 100644 index 0000000..cbafb15 --- /dev/null +++ b/examples/simple/manifest_dev.json @@ -0,0 +1,421 @@ +{ + "world": { + "class_hash": "0x1ebfeba7840a6308aa6676398c921ecbc1dcef75dbcbffdbd1bc0a1463fe7ed", + "address": "0x77c0dc7c1aba7f8842aff393ce6aa71fa675b4ced1bc927f7fc971b6acd92fc", + "seed": "simple", + "name": "simple" + }, + "contracts": [ + { + "address": "0x77762cce8276fce1945c201670882df9731e7663c062dd26553d48c24203469", + "class_hash": "0x7b3587958f67a798d004a7b51a56de74a325c25e63330f52cbc735670abbfa2", + "abi": [ + { + "type": "impl", + "name": "c1__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [ + { + "name": "arg1", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "MyInterfaceImpl", + "interface_name": "dojo_simple::MyInterface" + }, + { + "type": "interface", + "name": "dojo_simple::MyInterface", + "items": [ + { + "type": "function", + "name": "system_1", + "inputs": [ + { + "name": "a", + "type": "core::felt252" + }, + { + "name": "b", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "system_2", + "inputs": [ + { + "name": "a", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "system_3", + "inputs": [ + { + "name": "a", + "type": "core::felt252" + }, + { + "name": "b", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "dojo_simple::c1::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + } + ] + } + ], + "init_calldata": [ + "0xfffe" + ], + "tag": "c1", + "systems": [] + }, + { + "address": "0x4d68cd65198d01b1fafb81fd7181c9b65c1b6d094a5719fce7c687ebe9fb9b", + "class_hash": "0x1eef253239f61c49444c41990940fa8fee51b021d19e48c20d31f45bc465d46", + "abi": [ + { + "type": "impl", + "name": "c2__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "dojo_simple::c2::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + } + ] + } + ], + "init_calldata": [], + "tag": "c2", + "systems": [] + } + ], + "models": [ + { + "members": [], + "class_hash": "0x197e4e61b66fb3e155fbed747b2601e8a582e1d6df85fa984da79b0e131426a", + "tag": "M" + }, + { + "members": [], + "class_hash": "0x1d0ffd1e1f536860e3360d84423cf5afc6332507d0b3e6913143718d8f313b", + "tag": "M2" + } + ], + "events": [ + { + "members": [], + "class_hash": "0x3dad17a44f8f9f497b5d16a971c3800ab8a6481b0ec5a26bd8114eebd64016a", + "tag": "E" + } + ] +} \ No newline at end of file diff --git a/examples/simple/src/lib.cairo b/examples/simple/src/lib.cairo new file mode 100644 index 0000000..f199575 --- /dev/null +++ b/examples/simple/src/lib.cairo @@ -0,0 +1,115 @@ +#[starknet::contract] +pub mod sn_c1 { + #[storage] + struct Storage {} +} + +#[derive(Introspect, Drop, Serde)] +#[dojo::model] +pub struct M { + #[key] + pub a: felt252, + pub b: felt252, +} + +#[derive(Introspect, Drop, Serde)] +#[dojo::model] +pub struct M2 { + #[key] + pub a: u32, + pub b: u256, +} + +#[derive(Introspect, Drop, Serde)] +#[dojo::event] +pub struct E { + #[key] + pub a: felt252, + pub b: u32, +} + +#[starknet::interface] +pub trait MyInterface { + fn system_1(ref self: T, a: felt252, b: felt252); + fn system_2(self: @T, a: felt252) -> felt252; + fn system_3(self: @T, a: felt252, b: u32); +} + +#[dojo::contract] +pub mod c1 { + use super::{MyInterface, M, E}; + use dojo::model::ModelStorage; + use dojo::event::EventStorage; + + fn dojo_init(self: @ContractState, arg1: felt252) { + let m = M { a: 0, b: arg1, }; + + let mut world = self.world("ns"); + world.write_model(@m); + } + + #[abi(embed_v0)] + impl MyInterfaceImpl of MyInterface { + fn system_1(ref self: ContractState, a: felt252, b: felt252) { + let mut world = self.world("ns"); + + let m = M { a, b, }; + + world.write_model(@m) + } + + fn system_2(self: @ContractState, a: felt252) -> felt252 { + let world = self.world("ns"); + + let m: M = world.read_model(a); + + m.b + } + + fn system_3(self: @ContractState, a: felt252, b: u32) { + let mut world = self.world("ns"); + + let e = E { a, b, }; + + world.emit_event(@e); + } + } +} + +#[dojo::contract] +pub mod c2 {} + +#[cfg(test)] +mod tests { + use dojo::world::WorldStorageTrait; + use dojo::model::{ModelStorage, ModelStorageTest}; + use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource, ContractDefTrait}; + use super::{c1, m, M}; + + #[test] + fn test_1() { + let ndef = NamespaceDef { + namespace: "ns", resources: [ + TestResource::Model(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()) + ) + ].span() + }; + + let world = spawn_test_world([ndef].span()); + + let mut world = WorldStorageTrait::new(world, "ns"); + + let m: M = world.read_model(0); + assert!(m.b == 0xff, "invalid b"); + + let m2 = M { a: 120, b: 244, }; + + // `write_model_test` goes over permissions checks. + starknet::testing::set_contract_address(123.try_into().unwrap()); + world.write_model_test(@m2); + } +}