From 59becbf9ffac39284644d29888a84638dc275308 Mon Sep 17 00:00:00 2001 From: glihm Date: Tue, 5 Nov 2024 15:54:05 -0600 Subject: [PATCH] feat: add model upgradeability checks --- crates/contracts/src/contract/interface.cairo | 4 +- crates/contracts/src/event/component.cairo | 31 +++++++ crates/contracts/src/event/event.cairo | 8 +- crates/contracts/src/event/interface.cairo | 10 +-- crates/contracts/src/lib.cairo | 18 ++-- crates/contracts/src/meta/interface.cairo | 21 +++++ crates/contracts/src/meta/introspect.cairo | 25 ++++++ crates/contracts/src/meta/layout.cairo | 15 ++++ crates/contracts/src/model/component.cairo | 21 +++-- crates/contracts/src/model/definition.cairo | 8 +- crates/contracts/src/model/interface.cairo | 9 +- crates/contracts/src/model/model.cairo | 54 +++++++++--- crates/contracts/src/model/model_value.cairo | 6 -- crates/contracts/src/model/storage.cairo | 23 +++--- crates/contracts/src/world/errors.cairo | 18 ++++ crates/contracts/src/world/storage.cairo | 82 ++++++++++--------- .../contracts/src/world/world_contract.cairo | 82 ++++++++++++++----- 17 files changed, 307 insertions(+), 128 deletions(-) create mode 100644 crates/contracts/src/event/component.cairo create mode 100644 crates/contracts/src/meta/interface.cairo diff --git a/crates/contracts/src/contract/interface.cairo b/crates/contracts/src/contract/interface.cairo index 51a952e..f8d3003 100644 --- a/crates/contracts/src/contract/interface.cairo +++ b/crates/contracts/src/contract/interface.cairo @@ -1,4 +1,2 @@ #[starknet::interface] -pub trait IContract { - fn dojo_name(self: @T) -> ByteArray; -} +pub trait IContract {} diff --git a/crates/contracts/src/event/component.cairo b/crates/contracts/src/event/component.cairo new file mode 100644 index 0000000..4a661d8 --- /dev/null +++ b/crates/contracts/src/event/component.cairo @@ -0,0 +1,31 @@ +use dojo::event::{Event, IEvent, EventDefinition}; +use dojo::meta::{Layout, introspect::Struct}; + +#[starknet::embeddable] +pub impl IDeployedEventImpl< + TContractState, E, +Event +> of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @TContractState) -> ByteArray { + Event::::name() + } +} + +#[starknet::embeddable] +pub impl IStoredEventImpl< + TContractState, E, +Event +> of dojo::meta::interface::IStoredResource { + fn schema(self: @TContractState) -> Struct { + Event::::schema() + } + + fn layout(self: @TContractState) -> Layout { + Event::::layout() + } +} + +#[starknet::embeddable] +pub impl IEventImpl> of IEvent { + fn definition(self: @TContractState) -> EventDefinition { + Event::::definition() + } +} diff --git a/crates/contracts/src/event/event.cairo b/crates/contracts/src/event/event.cairo index 0a5cf99..ab0e46d 100644 --- a/crates/contracts/src/event/event.cairo +++ b/crates/contracts/src/event/event.cairo @@ -1,20 +1,20 @@ use dojo::meta::Layout; -use dojo::meta::introspect::Ty; +use dojo::meta::introspect::Struct; #[derive(Drop, Serde, Debug, PartialEq)] pub struct EventDefinition { pub name: ByteArray, pub layout: Layout, - pub schema: Ty + pub schema: Struct } pub trait Event { fn name() -> ByteArray; fn definition() -> EventDefinition; fn layout() -> Layout; - fn schema() -> Ty; + fn schema() -> Struct; fn keys(self: @T) -> Span; fn values(self: @T) -> Span; - /// Returns the selector of the model computed for the given namespace hash. + /// Returns the selector of the event 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 index 4bc5b4f..3dcc8df 100644 --- a/crates/contracts/src/event/interface.cairo +++ b/crates/contracts/src/event/interface.cairo @@ -1,12 +1,4 @@ -use dojo::meta::Layout; -use dojo::meta::introspect::Ty; - -use super::EventDefinition; - #[starknet::interface] pub trait IEvent { - fn dojo_name(self: @T) -> ByteArray; - fn definition(self: @T) -> EventDefinition; - fn layout(self: @T) -> Layout; - fn schema(self: @T) -> Ty; + fn definition(self: @T) -> super::EventDefinition; } diff --git a/crates/contracts/src/lib.cairo b/crates/contracts/src/lib.cairo index e08ce06..f2271b1 100644 --- a/crates/contracts/src/lib.cairo +++ b/crates/contracts/src/lib.cairo @@ -9,6 +9,8 @@ pub mod contract { } pub mod event { + pub mod component; + pub mod event; pub use event::{Event, EventDefinition}; @@ -20,11 +22,17 @@ pub mod event { } pub mod meta { + pub mod interface; + pub use interface::{ + IDeployedResource, IDeployedResourceDispatcher, IDeployedResourceDispatcherTrait, + IStoredResource, IStoredResourceDispatcher, IStoredResourceDispatcherTrait + }; + pub mod introspect; - pub use introspect::{Introspect, Ty}; + pub use introspect::{Introspect, Ty, StructCompareTrait}; pub mod layout; - pub use layout::{Layout, FieldLayout}; + pub use layout::{Layout, FieldLayout, LayoutCompareTrait}; } pub mod model { @@ -34,7 +42,7 @@ pub mod model { pub use definition::{ModelIndex, ModelDefinition, ModelDef}; pub mod model; - pub use model::{Model, KeyParser}; + pub use model::{Model, KeyParser, ModelPtr}; pub mod model_value; pub use model_value::{ModelValue, ModelValueKey}; @@ -46,9 +54,7 @@ pub mod model { pub use metadata::ResourceMetadata; pub mod storage; - pub use storage::{ - ModelStorage, ModelStorageTest, ModelValueStorage, ModelValueStorageTest, ModelPtr, - }; + pub use storage::{ModelStorage, ModelStorageTest, ModelValueStorage, ModelValueStorageTest,}; } pub mod storage { diff --git a/crates/contracts/src/meta/interface.cairo b/crates/contracts/src/meta/interface.cairo new file mode 100644 index 0000000..51d9dc6 --- /dev/null +++ b/crates/contracts/src/meta/interface.cairo @@ -0,0 +1,21 @@ +use dojo::meta::Layout; +use dojo::meta::introspect::Struct; + +/// The `IDeployedResource` starknet interface. +/// +/// This is the interface used by offchain components and other contracts +/// to get some info such Dojo name from a deployed resource. +#[starknet::interface] +pub trait IDeployedResource { + fn dojo_name(self: @T) -> ByteArray; +} + +/// The `IStoredResource` starknet interface. +/// +/// This is the interface used by offchain components and other contracts +/// to access to storage related data of a deployed resource. +#[starknet::interface] +pub trait IStoredResource { + fn layout(self: @T) -> Layout; + fn schema(self: @T) -> Struct; +} diff --git a/crates/contracts/src/meta/introspect.cairo b/crates/contracts/src/meta/introspect.cairo index 145fab1..448ae94 100644 --- a/crates/contracts/src/meta/introspect.cairo +++ b/crates/contracts/src/meta/introspect.cairo @@ -35,6 +35,31 @@ pub struct Member { pub ty: Ty } +#[generate_trait] +pub impl StructCompareImpl of StructCompareTrait { + fn is_an_upgrade_of(self: @Struct, old: @Struct) -> bool { + if self.name != old.name + || self.attrs != old.attrs + || (*self.children).len() < (*old.children).len() { + return false; + } + + let mut i = 0; + + loop { + if i >= (*old.children).len() { + break true; + } + + if *old.children[i] != *self.children[i] { + break false; + } + + i += 1; + } + } +} + pub trait Introspect { fn size() -> Option; fn layout() -> Layout; diff --git a/crates/contracts/src/meta/layout.cairo b/crates/contracts/src/meta/layout.cairo index 69bd49c..ce82b52 100644 --- a/crates/contracts/src/meta/layout.cairo +++ b/crates/contracts/src/meta/layout.cairo @@ -22,6 +22,21 @@ pub enum Layout { Enum: Span, } +#[generate_trait] +pub impl LayoutCompareImpl of LayoutCompareTrait { + fn is_same_type_of(self: @Layout, old: @Layout) -> bool { + match (self, old) { + (Layout::Fixed(_), Layout::Fixed(_)) => true, + (Layout::Struct(_), Layout::Struct(_)) => true, + (Layout::Tuple(_), Layout::Tuple(_)) => true, + (Layout::Array(_), Layout::Array(_)) => true, + (Layout::ByteArray, Layout::ByteArray) => true, + (Layout::Enum(_), Layout::Enum(_)) => true, + _ => false + } + } +} + /// Compute the full size in bytes of a layout, when all the fields /// are bit-packed. /// Could be None if at least a field has a dynamic size. diff --git a/crates/contracts/src/model/component.cairo b/crates/contracts/src/model/component.cairo index 1a2556d..2a7cea2 100644 --- a/crates/contracts/src/model/component.cairo +++ b/crates/contracts/src/model/component.cairo @@ -1,23 +1,30 @@ -use dojo::{model::{Model, IModel, ModelDef}, meta::{Layout, Ty}}; +use dojo::model::{Model, IModel, ModelDef}; +use dojo::meta::{Layout, introspect::Struct}; #[starknet::embeddable] -pub impl IModelImpl> of IModel { +pub impl IDeployedModelImpl< + TContractState, M, +Model +> of dojo::meta::IDeployedResource { fn dojo_name(self: @TContractState) -> ByteArray { Model::::name() } +} - fn version(self: @TContractState) -> u8 { - Model::::version() - } - - fn schema(self: @TContractState) -> Ty { +#[starknet::embeddable] +pub impl IStoredModelImpl< + TContractState, M, +Model +> of dojo::meta::IStoredResource { + fn schema(self: @TContractState) -> Struct { Model::::schema() } fn layout(self: @TContractState) -> Layout { Model::::layout() } +} +#[starknet::embeddable] +pub impl IModelImpl> of IModel { fn unpacked_size(self: @TContractState) -> Option { Model::::unpacked_size() } diff --git a/crates/contracts/src/model/definition.cairo b/crates/contracts/src/model/definition.cairo index a7b5cc7..27c27eb 100644 --- a/crates/contracts/src/model/definition.cairo +++ b/crates/contracts/src/model/definition.cairo @@ -1,4 +1,4 @@ -use dojo::meta::{Layout, introspect::Ty}; +use dojo::meta::{Layout, introspect::Struct}; /// The `ModelIndex` provides encapsulation for different ways to access /// a model's data. @@ -19,9 +19,8 @@ pub enum ModelIndex { /// Definition of the model containing all the fields that makes up a model. pub trait ModelDefinition { fn name() -> ByteArray; - fn version() -> u8; fn layout() -> Layout; - fn schema() -> Ty; + fn schema() -> Struct; fn size() -> Option; } @@ -29,9 +28,8 @@ pub trait ModelDefinition { #[derive(Drop, Serde, Debug, PartialEq)] pub struct ModelDef { pub name: ByteArray, - pub version: u8, pub layout: Layout, - pub schema: Ty, + pub schema: Struct, pub packed_size: Option, pub unpacked_size: Option, } diff --git a/crates/contracts/src/model/interface.cairo b/crates/contracts/src/model/interface.cairo index df33064..c466ed2 100644 --- a/crates/contracts/src/model/interface.cairo +++ b/crates/contracts/src/model/interface.cairo @@ -1,17 +1,10 @@ -use dojo::meta::{Layout, Ty}; -use dojo::model::ModelDef; - /// The `IModel` starknet interface. /// /// This is the interface used by offchain components and other contracts /// to interact with deployed models. #[starknet::interface] pub trait IModel { - fn dojo_name(self: @T) -> ByteArray; - fn version(self: @T) -> u8; - fn layout(self: @T) -> Layout; - fn schema(self: @T) -> Ty; fn unpacked_size(self: @T) -> Option; fn packed_size(self: @T) -> Option; - fn definition(self: @T) -> ModelDef; + fn definition(self: @T) -> dojo::model::ModelDef; } diff --git a/crates/contracts/src/model/model.cairo b/crates/contracts/src/model/model.cairo index 7a3bcd9..0744537 100644 --- a/crates/contracts/src/model/model.cairo +++ b/crates/contracts/src/model/model.cairo @@ -1,8 +1,17 @@ -use dojo::{meta::{Layout, introspect::Ty, layout::compute_packed_size}, utils::entity_id_from_keys}; +use dojo::{ + meta::{Layout, introspect::Struct, layout::compute_packed_size}, + utils::{entity_id_from_keys, find_model_field_layout, entity_id_from_key} +}; use super::{ModelDefinition, ModelDef}; - /// Trait `KeyParser` defines a trait for parsing keys from a given model. +/// +/// A pointer to a model, which can be expressed by an entity id. +#[derive(Copy, Drop, Serde, Debug, PartialEq)] +pub struct ModelPtr { + pub id: felt252, +} + pub trait KeyParser { /// Parses the key from the given model. fn parse_key(self: @M) -> K; @@ -33,12 +42,12 @@ pub trait Model { /// Returns the name of the model. (TODO: internalizing the name_hash could reduce poseidon /// costs). fn name() -> ByteArray; - /// Returns the version of the model. - fn version() -> u8; /// Returns the schema of the model. - fn schema() -> Ty; + fn schema() -> Struct; /// Returns the memory layout of the model. fn layout() -> Layout; + /// Returns the layout of a field in the model. + fn field_layout(field_selector: felt252) -> Option; /// Returns the unpacked size of the model. Only applicable for fixed size models. fn unpacked_size() -> Option; /// Returns the packed size of the model. Only applicable for fixed size models. @@ -49,6 +58,14 @@ pub trait Model { fn definition() -> ModelDef; /// Returns the selector of the model computed for the given namespace hash. fn selector(namespace_hash: felt252) -> felt252; + /// Returns the pointer to the model from the key. + fn ptr_from_key, +Drop>(key: K) -> ModelPtr; + /// Returns the pointer to the model from the keys. + fn ptr_from_keys(keys: Span) -> ModelPtr; + /// Returns the pointer to the model from the entity id. + fn ptr_from_id(entity_id: felt252) -> ModelPtr; + /// Returns the ptr of the model. + fn ptr(self: @M) -> ModelPtr; } pub impl ModelImpl, +ModelDefinition, +Serde> of Model { @@ -84,15 +101,15 @@ pub impl ModelImpl, +ModelDefinition, +Serde> of Model< dojo::utils::selector_from_namespace_and_name(namespace_hash, @Self::name()) } - fn version() -> u8 { - ModelDefinition::::version() - } - fn layout() -> Layout { ModelDefinition::::layout() } - fn schema() -> Ty { + fn field_layout(field_selector: felt252) -> Option { + find_model_field_layout(Self::layout(), field_selector) + } + + fn schema() -> Struct { ModelDefinition::::schema() } @@ -111,11 +128,26 @@ pub impl ModelImpl, +ModelDefinition, +Serde> of Model< fn definition() -> ModelDef { ModelDef { name: Self::name(), - version: Self::version(), layout: Self::layout(), schema: Self::schema(), packed_size: Self::packed_size(), unpacked_size: Self::unpacked_size() } } + + fn ptr_from_key, +Drop>(key: K) -> ModelPtr { + ModelPtr { id: entity_id_from_key(@key) } + } + + fn ptr_from_keys(keys: Span) -> ModelPtr { + ModelPtr { id: entity_id_from_keys(keys) } + } + + fn ptr_from_id(entity_id: felt252) -> ModelPtr { + ModelPtr:: { id: entity_id } + } + + fn ptr(self: @M) -> ModelPtr { + ModelPtr:: { id: self.entity_id() } + } } diff --git a/crates/contracts/src/model/model_value.cairo b/crates/contracts/src/model/model_value.cairo index eb0468b..b8c5d67 100644 --- a/crates/contracts/src/model/model_value.cairo +++ b/crates/contracts/src/model/model_value.cairo @@ -18,8 +18,6 @@ pub trait ModelValue { 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. @@ -44,10 +42,6 @@ pub impl ModelValueImpl, +ModelDefinition, +ModelValueParser> ModelDefinition::::name() } - fn version() -> u8 { - ModelDefinition::::version() - } - fn layout() -> Layout { ModelDefinition::::layout() } diff --git a/crates/contracts/src/model/storage.cairo b/crates/contracts/src/model/storage.cairo index ca9336b..9530348 100644 --- a/crates/contracts/src/model/storage.cairo +++ b/crates/contracts/src/model/storage.cairo @@ -1,17 +1,7 @@ -use dojo::model::model_value::ModelValueKey; +use dojo::{model::{ModelPtr, model_value::ModelValueKey}}; // TODO: define the right interface for member accesses. -/// A pointer to a model, which can be expressed by an entity id. -/// It's different from `ModelIndex` which is used for low level accesses. -#[derive(Copy, Drop, Serde, Debug, PartialEq)] -pub enum ModelPtr { - // The id of the model. - Id: felt252, - // The keys of the model as span. - Keys: Span, -} - /// A `ModelStorage` trait that abstracts where the storage is. /// /// Currently it's only world storage, but this will be useful when we have other @@ -41,9 +31,18 @@ pub trait ModelStorage { /// The ptr is mostly used for type inferrence. fn erase_model_ptr(ref self: S, ptr: ModelPtr); - /// Deletes multiple models of type `M` using the provided entity ids. + /// Deletes a model of type `M` using the provided entity id. + /// The ptr is mostly used for type inferrence. fn erase_models_ptrs(ref self: S, ptrs: Span>); + /// Retrieves a model of type `M` using the provided entity idref . + fn read_member>(self: @S, ptr: ModelPtr, field_selector: felt252) -> T; + + /// Retrieves a model of type `M` using the provided entity id. + fn write_member, +Drop>( + ref self: S, ptr: ModelPtr, field_selector: felt252, value: T + ); + /// Returns the current namespace hash. fn namespace_hash(self: @S) -> felt252; } diff --git a/crates/contracts/src/world/errors.cairo b/crates/contracts/src/world/errors.cairo index b96823b..0f377c9 100644 --- a/crates/contracts/src/world/errors.cairo +++ b/crates/contracts/src/world/errors.cairo @@ -81,3 +81,21 @@ pub fn no_world_owner(caller: ContractAddress, target: @ByteArray) -> ByteArray pub fn invalid_naming(kind: ByteArray, what: @ByteArray) -> ByteArray { format!("{kind} `{what}` is invalid according to Dojo naming rules: ^[a-zA-Z0-9_]+$") } + +pub fn invalid_resource_schema_upgrade(namespace: @ByteArray, name: @ByteArray) -> ByteArray { + format!("Invalid new schema to upgrade the resource `{}-{}`", namespace, name) +} + +pub fn invalid_resource_layout_upgrade(namespace: @ByteArray, name: @ByteArray) -> ByteArray { + format!("Invalid new layout to upgrade the resource `{}-{}`", namespace, name) +} + +pub fn invalid_resource_version_upgrade( + namespace: @ByteArray, name: @ByteArray, expected_version: u8 +) -> ByteArray { + format!("The new resource version of `{}-{}` should be {}", namespace, name, expected_version) +} + +pub fn inconsistent_namespaces(old_hash: felt252, new_hash: felt252) -> ByteArray { + format!("Inconsistent namespaces (old: {old_hash} new: {new_hash}") +} diff --git a/crates/contracts/src/world/storage.cairo b/crates/contracts/src/world/storage.cairo index 1ea1c07..3c9a8ff 100644 --- a/crates/contracts/src/world/storage.cairo +++ b/crates/contracts/src/world/storage.cairo @@ -6,7 +6,8 @@ use dojo::model::{Model, ModelIndex, ModelValueKey, ModelValue, ModelStorage, Mo use dojo::event::{Event, EventStorage}; use dojo::meta::Layout; use dojo::utils::{ - entity_id_from_key, entity_id_from_keys, serialize_inline, find_model_field_layout + entity_id_from_key, entity_id_from_keys, serialize_inline, find_model_field_layout, + deserialize_unwrap }; use starknet::{ContractAddress, ClassHash}; @@ -16,6 +17,13 @@ pub struct WorldStorage { pub namespace_hash: felt252, } +fn field_layout_unwrap>(field_selector: felt252) -> Layout { + match Model::::field_layout(field_selector) { + Option::Some(layout) => layout, + Option::None => panic_with_felt252('bad member id') + } +} + #[generate_trait] pub impl WorldStorageInternalImpl of WorldStorageTrait { fn new(world: IWorldDispatcher, namespace: @ByteArray) -> WorldStorage { @@ -152,43 +160,56 @@ pub impl ModelStorageWorldStorageImpl, +Drop> of ModelStorage) { - let mut keys: Array = array![]; + let mut ids: Array = array![]; for m in models { - keys.append(ModelIndex::Keys(Model::::keys(*m))); + ids.append(ModelIndex::Id(Model::::entity_id(*m))); }; IWorldDispatcherTrait::delete_entities( self.dispatcher, Model::::selector(self.namespace_hash), - keys.span(), + ids.span(), Model::::layout() ); } fn erase_model_ptr(ref self: WorldStorage, ptr: ModelPtr) { - let entity_id = match ptr { - ModelPtr::Id(id) => id, - ModelPtr::Keys(keys) => entity_id_from_keys(keys), - }; - IWorldDispatcherTrait::delete_entity( self.dispatcher, Model::::selector(self.namespace_hash), - ModelIndex::Id(entity_id), + ModelIndex::Id(ptr.id), Model::::layout() ); } + fn read_member>( + self: @WorldStorage, ptr: ModelPtr, field_selector: felt252 + ) -> T { + deserialize_unwrap( + IWorldDispatcherTrait::entity( + *self.dispatcher, + Model::::selector(*self.namespace_hash), + ModelIndex::MemberId((ptr.id, field_selector)), + field_layout_unwrap::(field_selector) + ) + ) + } + fn write_member, +Drop>( + ref self: WorldStorage, ptr: ModelPtr, field_selector: felt252, value: T + ) { + IWorldDispatcherTrait::set_entity( + self.dispatcher, + Model::::selector(self.namespace_hash), + ModelIndex::MemberId((ptr.id, field_selector)), + serialize_inline(@value), + field_layout_unwrap::(field_selector) + ); + } + fn erase_models_ptrs(ref self: WorldStorage, ptrs: Span>) { let mut indexes: Array = array![]; - for p in ptrs { - indexes - .append( - match p { - ModelPtr::Id(id) => ModelIndex::Id(*id), - ModelPtr::Keys(keys) => ModelIndex::Id(entity_id_from_keys(*keys)), - } - ); + for ptr in ptrs { + indexes.append(ModelIndex::Id(*ptr.id)); }; IWorldDispatcherTrait::delete_entities( @@ -358,7 +379,7 @@ pub impl EventStorageTestWorldStorageImpl< /// checks. #[cfg(target: "test")] pub impl ModelStorageTestWorldStorageImpl< - M, +Model + M, +Model, +Drop > of dojo::model::ModelStorageTest { fn write_model_test(ref self: WorldStorage, model: @M) { let world_test = dojo::world::IWorldTestDispatcher { @@ -399,11 +420,6 @@ pub impl ModelStorageTestWorldStorageImpl< } fn erase_model_ptr_test(ref self: WorldStorage, ptr: ModelPtr) { - let entity_id = match ptr { - ModelPtr::Id(id) => id, - ModelPtr::Keys(keys) => entity_id_from_keys(keys), - }; - let world_test = dojo::world::IWorldTestDispatcher { contract_address: self.dispatcher.contract_address }; @@ -411,32 +427,21 @@ pub impl ModelStorageTestWorldStorageImpl< dojo::world::IWorldTestDispatcherTrait::delete_entity_test( world_test, Model::::selector(self.namespace_hash), - ModelIndex::Id(entity_id), + ModelIndex::Id(ptr.id), Model::::layout() ); } fn erase_models_ptrs_test(ref self: WorldStorage, ptrs: Span>) { - let mut ids: Array = array![]; - for p in ptrs { - ids - .append( - match p { - ModelPtr::Id(id) => *id, - ModelPtr::Keys(keys) => entity_id_from_keys(*keys), - } - ); - }; - let world_test = dojo::world::IWorldTestDispatcher { contract_address: self.dispatcher.contract_address }; - for i in ids { + for ptr in ptrs { dojo::world::IWorldTestDispatcherTrait::delete_entity_test( world_test, Model::::selector(self.namespace_hash), - ModelIndex::Id(i), + ModelIndex::Id(*ptr.id), Model::::layout() ); } @@ -533,3 +538,4 @@ fn get_serialized_member( 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 321d542..b1192e2 100644 --- a/crates/contracts/src/world/world_contract.cairo +++ b/crates/contracts/src/world/world_contract.cairo @@ -39,12 +39,12 @@ pub mod world { use dojo::contract::components::upgradeable::{ IUpgradeableDispatcher, IUpgradeableDispatcherTrait }; - use dojo::contract::{IContractDispatcher, IContractDispatcherTrait}; - use dojo::meta::Layout; - use dojo::model::{ - Model, ResourceMetadata, metadata, ModelIndex, IModelDispatcher, IModelDispatcherTrait + use dojo::meta::{ + Layout, IStoredResourceDispatcher, IStoredResourceDispatcherTrait, + IDeployedResourceDispatcher, IDeployedResourceDispatcherTrait, LayoutCompareTrait, + StructCompareTrait }; - use dojo::event::{IEventDispatcher, IEventDispatcherTrait}; + use dojo::model::{Model, ResourceMetadata, metadata, ModelIndex}; use dojo::storage; use dojo::utils::{entity_id_from_keys, bytearray_hash, selector_from_namespace_and_name}; use dojo::world::{IWorld, IUpgradeableWorld, Resource, ResourceIsNoneTrait}; @@ -347,7 +347,7 @@ pub mod world { storage::entity_model::write_model_entity( metadata::resource_metadata_selector(internal_ns_hash), - metadata.resource_id, + entity_id_from_keys([metadata.resource_id].span()), metadata.values(), Model::::layout() ); @@ -426,7 +426,7 @@ pub mod world { .unwrap_syscall(); self.events_salt.write(salt + 1); - let event = IEventDispatcher { contract_address }; + let event = IDeployedResourceDispatcher { contract_address }; let event_name = event.dojo_name(); self.assert_name(@event_name); @@ -472,7 +472,7 @@ pub mod world { let namespace_hash = bytearray_hash(@namespace); - let event = IEventDispatcher { contract_address: new_contract_address }; + let event = IDeployedResourceDispatcher { contract_address: new_contract_address }; let event_name = event.dojo_name(); let event_selector = selector_from_namespace_and_name(namespace_hash, @event_name); @@ -498,6 +498,11 @@ pub mod world { ) }; + self + .assert_resource_upgradability( + @namespace, @event_name, prev_address, new_contract_address + ); + self .resources .write(event_selector, Resource::Event((new_contract_address, namespace_hash))); @@ -525,7 +530,7 @@ pub mod world { .unwrap_syscall(); self.models_salt.write(salt + 1); - let model = IModelDispatcher { contract_address }; + let model = IDeployedResourceDispatcher { contract_address }; let model_name = model.dojo_name(); self.assert_name(@model_name); @@ -571,7 +576,7 @@ pub mod world { let namespace_hash = bytearray_hash(@namespace); - let model = IModelDispatcher { contract_address: new_contract_address }; + let model = IDeployedResourceDispatcher { contract_address: new_contract_address }; let model_name = model.dojo_name(); let model_selector = selector_from_namespace_and_name(namespace_hash, @model_name); @@ -597,8 +602,10 @@ pub mod world { ) }; - // 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 + .assert_resource_upgradability( + @namespace, @model_name, prev_address, new_contract_address + ); self .resources @@ -648,7 +655,7 @@ pub mod world { let namespace_hash = bytearray_hash(@namespace); - let contract = IContractDispatcher { contract_address }; + let contract = IDeployedResourceDispatcher { contract_address }; let contract_name = contract.dojo_name(); let contract_selector = selector_from_namespace_and_name( namespace_hash, @contract_name @@ -703,7 +710,7 @@ pub mod world { let namespace_hash = bytearray_hash(@namespace); - let contract = IContractDispatcher { contract_address: new_contract_address }; + let contract = IDeployedResourceDispatcher { contract_address: new_contract_address }; let contract_name = contract.dojo_name(); let contract_selector = selector_from_namespace_and_name( namespace_hash, @contract_name @@ -738,7 +745,7 @@ pub mod world { fn init_contract(ref self: ContractState, selector: felt252, init_calldata: Span) { if let Resource::Contract((contract_address, _)) = self.resources.read(selector) { if self.initialized_contracts.read(selector) { - let dispatcher = IContractDispatcher { contract_address }; + let dispatcher = IDeployedResourceDispatcher { contract_address }; panic_with_byte_array( @errors::contract_already_initialized(@dispatcher.dojo_name()) ); @@ -1043,6 +1050,43 @@ pub mod world { } } + /// Panics if a resource is not upgradable. + /// + /// Upgradable means: + /// - the layout type must remain the same (Struct or Fixed), + /// - existing fields cannot be changed or moved inside the resource, + /// - new fields can only be appended at the end of the resource. + /// + /// # Arguments + /// * `namespace` - the namespace of the resource. + /// * `name` - the name of the resource. + /// * `prev_address` - the address of the current resource. + /// * `new_address` - the address of the newly deployed resource. + /// + fn assert_resource_upgradability( + self: @ContractState, + namespace: @ByteArray, + name: @ByteArray, + prev_address: ContractAddress, + new_address: ContractAddress + ) { + let resource = IStoredResourceDispatcher { contract_address: prev_address }; + let old_layout = resource.layout(); + let old_schema = resource.schema(); + + let new_resource = IStoredResourceDispatcher { contract_address: new_address }; + let new_layout = new_resource.layout(); + let new_schema = new_resource.schema(); + + if !new_layout.is_same_type_of(@old_layout) { + panic_with_byte_array(@errors::invalid_resource_layout_upgrade(namespace, name)); + } + + if !new_schema.is_an_upgrade_of(@old_schema) { + panic_with_byte_array(@errors::invalid_resource_schema_upgrade(namespace, name)); + } + } + /// Panics with the caller details. /// /// # Arguments @@ -1059,19 +1103,19 @@ pub mod world { Resource::Contract(( contract_address, _ )) => { - let d = IContractDispatcher { contract_address }; + let d = IDeployedResourceDispatcher { contract_address }; format!("contract (or its namespace) `{}`", d.dojo_name()) }, Resource::Event(( contract_address, _ )) => { - let d = IEventDispatcher { contract_address }; + let d = IDeployedResourceDispatcher { contract_address }; format!("event (or its namespace) `{}`", d.dojo_name()) }, Resource::Model(( contract_address, _ )) => { - let d = IModelDispatcher { contract_address }; + let d = IDeployedResourceDispatcher { contract_address }; format!("model (or its namespace) `{}`", d.dojo_name()) }, Resource::Namespace(ns) => { format!("namespace `{}`", ns) }, @@ -1090,7 +1134,7 @@ 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 = IContractDispatcher { contract_address: caller }; + let d = IDeployedResourceDispatcher { contract_address: caller }; format!("Contract `{}`", d.dojo_name()) };