diff --git a/contracts/feature-tests/basic-features/src/basic_features_main.rs b/contracts/feature-tests/basic-features/src/basic_features_main.rs index 2f0cb9a830..be30d0dde5 100644 --- a/contracts/feature-tests/basic-features/src/basic_features_main.rs +++ b/contracts/feature-tests/basic-features/src/basic_features_main.rs @@ -27,6 +27,7 @@ pub mod storage_mapper_linked_list; pub mod storage_mapper_map; pub mod storage_mapper_map_storage; pub mod storage_mapper_non_fungible_token; +pub mod storage_mapper_object_to_id; pub mod storage_mapper_queue; pub mod storage_mapper_set; pub mod storage_mapper_single; @@ -77,6 +78,7 @@ pub trait BasicFeatures: + non_zero_features::TypeFeatures + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + storage_mapper_get_at_address::StorageMapperGetAtAddress + + storage_mapper_object_to_id::ObjectToIdMapperFeatures { #[init] fn init(&self) {} diff --git a/contracts/feature-tests/basic-features/src/storage_mapper_object_to_id.rs b/contracts/feature-tests/basic-features/src/storage_mapper_object_to_id.rs new file mode 100644 index 0000000000..b1fea81df9 --- /dev/null +++ b/contracts/feature-tests/basic-features/src/storage_mapper_object_to_id.rs @@ -0,0 +1,64 @@ +use crate::types::ExampleStructManaged; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait ObjectToIdMapperFeatures { + #[endpoint] + fn object_to_id_mapper_get_id(&self, object: ExampleStructManaged) -> AddressId { + self.object_ids().get_id(&object) + } + + #[endpoint] + fn object_to_id_mapper_get_id_non_zero( + &self, + object: ExampleStructManaged, + ) -> AddressId { + self.object_ids().get_id_non_zero(&object) + } + + #[endpoint] + fn object_to_id_mapper_get_object( + &self, + object_id: ObjectId, + ) -> Option> { + self.object_ids().get_object(object_id) + } + + #[endpoint] + fn object_to_id_mapper_contains(&self, object_id: ObjectId) -> bool { + self.object_ids().contains_id(object_id) + } + + #[endpoint] + fn object_to_id_mapper_set(&self, object: &ExampleStructManaged) -> AddressId { + self.object_ids().insert_new(object) + } + + #[endpoint] + fn object_to_id_mapper_get_id_or_insert( + &self, + object: ExampleStructManaged, + ) -> AddressId { + self.object_ids().get_id_or_insert(object) + } + + #[endpoint] + fn object_to_id_mapper_remove_by_id( + &self, + object_id: ObjectId, + ) -> Option> { + self.object_ids().remove_by_id(object_id) + } + + #[endpoint] + fn address_to_id_mapper_remove_by_address( + &self, + object: ExampleStructManaged, + ) -> AddressId { + self.object_ids().remove_by_object(&object) + } + + #[storage_mapper("object_ids")] + fn object_ids(&self) -> ObjectToIdMapper>; +} diff --git a/framework/base/src/storage/mappers.rs b/framework/base/src/storage/mappers.rs index b6bac7f06b..1d7d94fc77 100644 --- a/framework/base/src/storage/mappers.rs +++ b/framework/base/src/storage/mappers.rs @@ -4,6 +4,7 @@ mod linked_list_mapper; mod map_mapper; mod map_storage_mapper; mod mapper; +mod object_to_id_mapper; mod ordered_binary_tree_mapper; mod queue_mapper; mod set_mapper; @@ -21,6 +22,7 @@ pub use linked_list_mapper::{LinkedListMapper, LinkedListNode}; pub use map_mapper::MapMapper; pub use map_storage_mapper::MapStorageMapper; pub use mapper::{StorageClearable, StorageMapper}; +pub use object_to_id_mapper::{ObjectId, ObjectToIdMapper}; pub use ordered_binary_tree_mapper::{ NodeId, OrderedBinaryTreeMapper, OrderedBinaryTreeNode, NULL_NODE_ID, }; diff --git a/framework/base/src/storage/mappers/object_to_id_mapper.rs b/framework/base/src/storage/mappers/object_to_id_mapper.rs new file mode 100644 index 0000000000..ec32be57e8 --- /dev/null +++ b/framework/base/src/storage/mappers/object_to_id_mapper.rs @@ -0,0 +1,199 @@ +use core::{borrow::Borrow, marker::PhantomData}; + +use multiversx_sc_codec::{NestedDecode, NestedEncode, TopDecode, TopEncode}; + +use crate::{ + api::StorageMapperApi, + imports::{ErrorApiImpl, ManagedType}, + storage::StorageKey, + storage_clear, storage_set, +}; + +use super::{ + set_mapper::{CurrentStorage, StorageAddress}, + StorageMapper, +}; + +static ID_SUFFIX: &[u8] = b"id"; +static OBJECT_SUFFIX: &[u8] = b"object"; +static UNKNOW_OBJECT_ERR_MSG: &[u8] = b"Unknown object"; +static LAST_ID_SUFFIX: &[u8] = b"lastId"; + +pub type ObjectId = u64; +pub const NULL_ID: ObjectId = 0; + +pub struct ObjectToIdMapper +where + SA: StorageMapperApi, + A: StorageAddress, + T: TopEncode + TopDecode + NestedEncode + NestedDecode + 'static, +{ + _phantom_api: PhantomData, + _phantom_item: PhantomData, + address: A, + base_key: StorageKey, +} + +impl StorageMapper for ObjectToIdMapper +where + SA: StorageMapperApi, + T: TopEncode + TopDecode + NestedEncode + NestedDecode, +{ + #[inline] + fn new(base_key: StorageKey) -> Self { + ObjectToIdMapper { + _phantom_api: PhantomData, + _phantom_item: PhantomData, + address: CurrentStorage, + base_key, + } + } +} + +impl ObjectToIdMapper +where + SA: StorageMapperApi, + A: StorageAddress, + T: TopEncode + TopDecode + NestedEncode + NestedDecode, +{ + pub fn contains_id(&self, id: ObjectId) -> bool { + let key = self.id_to_object_key(id); + self.address.address_storage_get_len(key.as_ref()) != 0 + } + + pub fn get_id(&self, object: BT) -> ObjectId + where + BT: Borrow, + { + let key = self.object_to_id_key(object); + self.address.address_storage_get(key.as_ref()) + } + + pub fn get_id_non_zero(&self, object: BT) -> ObjectId + where + BT: Borrow, + { + let id = self.get_id(object); + if id == NULL_ID { + SA::error_api_impl().signal_error(UNKNOW_OBJECT_ERR_MSG); + } + id + } + + pub fn get_object(&self, id: ObjectId) -> Option { + let key = self.id_to_object_key(id); + if self.address.address_storage_get_len(key.as_ref()) == 0 { + return None; + } + let object = self.address.address_storage_get(key.as_ref()); + Some(object) + } + + fn id_to_object_key(&self, id: ObjectId) -> StorageKey { + let mut item_key = self.base_key.clone(); + item_key.append_bytes(ID_SUFFIX); + item_key.append_item(&id); + + item_key + } + + fn object_to_id_key(&self, object: BT) -> StorageKey + where + BT: Borrow, + { + let mut item_key = self.base_key.clone(); + item_key.append_bytes(OBJECT_SUFFIX); + item_key.append_item(object.borrow()); + + item_key + } + + fn last_id_key(&self) -> StorageKey { + let mut item_key = self.base_key.clone(); + item_key.append_bytes(LAST_ID_SUFFIX); + + item_key + } + + pub fn get_last_id(&self) -> ObjectId { + self.address + .address_storage_get(self.last_id_key().as_ref()) + } +} + +impl ObjectToIdMapper +where + SA: StorageMapperApi, + T: TopEncode + TopDecode + NestedEncode + NestedDecode, +{ + pub fn get_id_or_insert(&self, object: T) -> ObjectId { + let current_id = self + .address + .address_storage_get(self.object_to_id_key(&object).as_ref()); + if current_id != 0 { + return current_id; + } + + self.insert_object(object) + } + + pub fn insert_new(&self, object: BT) -> ObjectId + where + BT: Borrow, + { + let existing_id = self.get_id(object.borrow()); + if existing_id != NULL_ID { + SA::error_api_impl().signal_error(b"Object already exists"); + } + + self.insert_object(object) + } + + pub fn remove_by_id(&self, id: ObjectId) -> Option { + let object = self.get_object(id)?; + self.remove_entry(id, &object); + + Some(object) + } + + pub fn remove_by_object(&self, object: BT) -> ObjectId + where + BT: Borrow, + { + let current_id = self.get_id(object.borrow()); + if current_id != NULL_ID { + self.remove_entry(current_id, object); + } + + current_id + } + + fn insert_object(&self, object: BT) -> ObjectId + where + BT: Borrow, + { + let new_id = self.get_last_id() + 1; + storage_set(self.id_to_object_key(new_id).as_ref(), &object.borrow()); + storage_set(self.object_to_id_key(object).as_ref(), &new_id); + + self.set_last_id(new_id); + + new_id + } + + fn set_last_id(&self, last_id: ObjectId) { + if last_id == 0 { + SA::error_api_impl().signal_error(b"ID Overflow"); + } + + storage_set(self.last_id_key().as_ref(), &last_id); + } + + fn remove_entry(&self, id: ObjectId, object: BT) + where + BT: Borrow, + { + storage_clear(self.object_to_id_key(object).as_ref()); + storage_clear(self.id_to_object_key(id).as_ref()); + } +}