--- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -18,7 +18,7 @@ use core::ops::{Deref, DerefMut}; use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder}; -use sp_std::vec::Vec; +use sp_std::{vec::Vec, collections::btree_map::BTreeMap}; use pallet_evm::account::CrossAccountId; use frame_support::{ dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo}, @@ -35,7 +35,8 @@ FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits, CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit, budget::Budget, COLLECTION_FIELD_LIMIT, CollectionField, - PhantomType, + PhantomType, Property, Properties, PropertiesPermissionMap, PropertyKey, PropertyPermission, + PropertiesError, }; pub use pallet::*; use sp_core::H160; @@ -288,6 +289,10 @@ T::CrossAccountId, u128, ), + + CollectionPropertySet(CollectionId, Property), + + TokenPropertySet(CollectionId, TokenId, Property), } #[pallet::error] @@ -319,7 +324,6 @@ CollectionLimitBoundsExceeded, /// Tried to enable permissions which are only permitted to be disabled OwnerPermissionsCantBeReverted, - /// Collection settings not allowing items transferring TransferNotAllowed, /// Account token limit exceeded per collection @@ -372,6 +376,25 @@ QueryKind = OptionQuery, >; + /// Collection properties + #[pallet::storage] + pub type CollectionProperties = StorageMap< + Hasher = Blake2_128Concat, + Key = CollectionId, + Value = Properties, + QueryKind = ValueQuery, + OnEmpty = up_data_structs::CollectionProperties, + >; + + #[pallet::storage] + #[pallet::getter(fn property_permission)] + pub type CollectionPropertyPermissions = StorageMap< + Hasher = Blake2_128Concat, + Key = CollectionId, + Value = PropertiesPermissionMap, + QueryKind = ValueQuery, + >; + /// Large variable-size collection fields are extracted here #[pallet::storage] pub type CollectionData = StorageNMap< @@ -538,6 +561,7 @@ sponsorship, limits, meta_update_permission, + .. } = >::get(collection)?; Some(RpcCollection { name: name.into_inner(), @@ -615,8 +639,25 @@ .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits)) .unwrap_or_else(|| Ok(CollectionLimits::default()))?, meta_update_permission: data.meta_update_permission.unwrap_or_default(), + // token_property_permissions: data.token_property_permissions.unwrap_or_default(), + // properties: Properties::from_collection_props_vec(data.properties)? }; + CollectionProperties::::insert( + id, + Properties::from_collection_props_vec(data.properties)?, + ); + + let token_props_permissions: PropertiesPermissionMap = data + .token_property_permissions + .into_iter() + .map(|property| (property.key, property.permission)) + .collect::>() + .try_into() + .map_err(|_| PropertiesError::PropertyLimitReached)?; + + CollectionPropertyPermissions::::insert(id, token_props_permissions); + // Take a (non-refundable) deposit of collection creation { let mut imbalance = @@ -688,6 +729,34 @@ Ok(()) } + pub fn change_collection_property( + collection: &CollectionHandle, + sender: &T::CrossAccountId, + property: Property, + ) -> DispatchResult { + collection.check_is_owner_or_admin(sender)?; + + CollectionProperties::::get(collection.id).try_change_property(property)?; + + Ok(()) + } + + pub fn change_property_permission( + collection: &CollectionHandle, + sender: &T::CrossAccountId, + property_key: PropertyKey, + permission: PropertyPermission, + ) -> DispatchResult { + collection.check_is_owner_or_admin(sender)?; + + CollectionPropertyPermissions::::try_mutate(collection.id, |permissions| { + permissions.try_insert(property_key, permission) + }) + .map_err(|_| PropertiesError::PropertyLimitReached)?; + + Ok(()) + } + fn set_field_raw( collection_id: CollectionId, field: CollectionField, @@ -840,6 +909,7 @@ fn create_multiple_items(amount: u32) -> Weight; fn create_multiple_items_ex(cost: &CreateItemExData) -> Weight; fn burn_item() -> Weight; + fn set_property() -> Weight; fn transfer() -> Weight; fn approve() -> Weight; fn transfer_from() -> Weight; @@ -875,6 +945,19 @@ amount: u128, ) -> DispatchResultWithPostInfo; + fn change_collection_property( + &self, + sender: T::CrossAccountId, + property: Property, + ) -> DispatchResultWithPostInfo; + + fn change_token_property( + &self, + sender: T::CrossAccountId, + token_id: TokenId, + property: Property, + ) -> DispatchResultWithPostInfo; + fn transfer( &self, sender: T::CrossAccountId, --- a/pallets/fungible/src/common.rs +++ b/pallets/fungible/src/common.rs @@ -21,7 +21,7 @@ use pallet_common::{CommonCollectionOperations, CommonWeightInfo, with_weight}; use sp_runtime::ArithmeticError; use sp_std::{vec::Vec, vec}; -use up_data_structs::CustomDataLimit; +use up_data_structs::{CustomDataLimit, Property}; use crate::{ Allowance, Balance, Config, Error, FungibleHandle, Pallet, SelfWeightOf, weights::WeightInfo, @@ -50,6 +50,10 @@ >::burn_item() } + fn set_property() -> Weight { + >::set_property() + } + fn transfer() -> Weight { >::transfer() } @@ -225,6 +229,23 @@ ) } + fn change_collection_property( + &self, + _sender: T::CrossAccountId, + _property: Property, + ) -> DispatchResultWithPostInfo { + fail!(>::PropertiesNotAllowed) + } + + fn change_token_property( + &self, + _sender: T::CrossAccountId, + _token_id: TokenId, + _property: Property, + ) -> DispatchResultWithPostInfo { + fail!(>::PropertiesNotAllowed) + } + fn set_variable_metadata( &self, _sender: T::CrossAccountId, --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -61,6 +61,8 @@ FungibleItemsDontHaveData, /// Fungible token does not support nested FungibleDisallowsNesting, + /// Item properties are not allowed + PropertiesNotAllowed, } #[pallet::config] --- a/pallets/fungible/src/weights.rs +++ b/pallets/fungible/src/weights.rs @@ -35,6 +35,7 @@ fn create_item() -> Weight; fn create_multiple_items_ex(b: u32, ) -> Weight; fn burn_item() -> Weight; + fn set_property() -> Weight; fn transfer() -> Weight; fn approve() -> Weight; fn transfer_from() -> Weight; @@ -69,6 +70,12 @@ .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } + + fn set_property() -> Weight { + // Error + 0 + } + // Storage: Fungible Balance (r:2 w:2) fn transfer() -> Weight { (17_713_000 as Weight) @@ -126,6 +133,12 @@ .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } + + fn set_property() -> Weight { + // Error + 0 + } + // Storage: Fungible Balance (r:2 w:2) fn transfer() -> Weight { (17_713_000 as Weight) --- a/pallets/nonfungible/src/common.rs +++ b/pallets/nonfungible/src/common.rs @@ -17,7 +17,9 @@ use core::marker::PhantomData; use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight, BoundedVec}; -use up_data_structs::{TokenId, CustomDataLimit, CreateItemExData, CollectionId, budget::Budget}; +use up_data_structs::{ + TokenId, CustomDataLimit, CreateItemExData, CollectionId, budget::Budget, Property, +}; use pallet_common::{CommonCollectionOperations, CommonWeightInfo, with_weight}; use sp_runtime::DispatchError; use sp_std::vec::Vec; @@ -48,6 +50,10 @@ >::burn_item() } + fn set_property() -> Weight { + >::set_property() + } + fn transfer() -> Weight { >::transfer() } @@ -235,6 +241,32 @@ } } + fn change_collection_property( + &self, + sender: T::CrossAccountId, + property: Property, + ) -> DispatchResultWithPostInfo { + // let token_id = None; + with_weight( + // >::change_property(self, &sender, token_id, property), + Ok(()), + >::set_property(), + ) + } + + fn change_token_property( + &self, + sender: T::CrossAccountId, + token_id: TokenId, + property: Property, + ) -> DispatchResultWithPostInfo { + with_weight( + // >::change_property(self, &sender, Some(token_id), property), + Ok(()), + >::set_property(), + ) + } + fn set_variable_metadata( &self, sender: T::CrossAccountId, --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -20,7 +20,7 @@ use frame_support::{BoundedVec, ensure, fail}; use up_data_structs::{ AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData, - mapping::TokenAddressMapping, NestingRule, budget::Budget, + mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission, }; use pallet_evm::account::CrossAccountId; use pallet_common::{ @@ -94,6 +94,14 @@ QueryKind = OptionQuery, >; + #[pallet::storage] + pub type TokenProperties = StorageNMap< + Key = (Key, Key), + Value = up_data_structs::Properties, + QueryKind = ValueQuery, + OnEmpty = up_data_structs::TokenProperties, + >; + /// Used to enumerate tokens owned by account #[pallet::storage] pub type Owned = StorageNMap< @@ -246,6 +254,56 @@ Ok(()) } + pub fn change_token_property( + collection: &NonfungibleHandle, + sender: &T::CrossAccountId, + token_id: TokenId, + property: Property, + ) -> DispatchResult { + let permission = >::property_permission(collection.id) + .get(&property.key) + .map(|p| p.clone()) + .unwrap_or(PropertyPermission::None); + + let check_token_owner = || -> DispatchResult { + let token_data = >::get((collection.id, token_id)) + .ok_or(>::TokenNotFound)?; + + ensure!(&token_data.owner == sender, >::NoPermission); + + Ok(()) + }; + + let is_property_exists = TokenProperties::::get((collection.id, token_id)) + .get_property(&property.key) + .is_some(); + + match (permission, is_property_exists) { + (PropertyPermission::AdminConst, false) => { + collection.check_is_owner_or_admin(sender)? + } + (PropertyPermission::Admin, _) => collection.check_is_owner_or_admin(sender)?, + (PropertyPermission::ItemOwnerConst, false) => check_token_owner()?, + (PropertyPermission::ItemOwner, _) => check_token_owner()?, + (PropertyPermission::ItemOwnerOrAdmin, _) => { + check_token_owner().or(collection.check_is_owner_or_admin(sender))?; + } + _ => return Err(>::NoPermission.into()), + } + + >::try_mutate((collection.id, token_id), |properties| { + properties.try_change_property(property.clone()) + })?; + + >::deposit_event(CommonEvent::TokenPropertySet( + collection.id, + token_id, + property, + )); + + Ok(()) + } + pub fn transfer( collection: &NonfungibleHandle, from: &T::CrossAccountId, --- a/pallets/nonfungible/src/weights.rs +++ b/pallets/nonfungible/src/weights.rs @@ -36,6 +36,7 @@ fn create_multiple_items(b: u32, ) -> Weight; fn create_multiple_items_ex(b: u32, ) -> Weight; fn burn_item() -> Weight; + fn set_property() -> Weight; fn transfer() -> Weight; fn approve() -> Weight; fn transfer_from() -> Weight; @@ -90,6 +91,12 @@ .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } + + fn set_property() -> Weight { + // TODO calculate appropriate weight + 50_000_000 as Weight + } + // Storage: Nonfungible TokenData (r:1 w:1) // Storage: Nonfungible AccountBalance (r:2 w:2) // Storage: Nonfungible Allowance (r:1 w:0) @@ -179,6 +186,12 @@ .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } + + fn set_property() -> Weight { + // TODO calculate appropriate weight + 50_000_000 as Weight + } + // Storage: Nonfungible TokenData (r:1 w:1) // Storage: Nonfungible AccountBalance (r:2 w:2) // Storage: Nonfungible Allowance (r:1 w:0) --- a/pallets/refungible/src/common.rs +++ b/pallets/refungible/src/common.rs @@ -20,7 +20,7 @@ use frame_support::{dispatch::DispatchResultWithPostInfo, fail, weights::Weight, BoundedVec}; use up_data_structs::{ CollectionId, TokenId, CustomDataLimit, CreateItemExData, CreateRefungibleExData, - budget::Budget, + budget::Budget, Property, }; use pallet_common::{CommonCollectionOperations, CommonWeightInfo, with_weight}; use sp_runtime::DispatchError; @@ -66,6 +66,10 @@ max_weight_of!(burn_item_partial(), burn_item_fully()) } + fn set_property() -> Weight { + >::set_property() + } + fn transfer() -> Weight { max_weight_of!( transfer_normal(), @@ -244,6 +248,23 @@ ) } + fn change_collection_property( + &self, + _sender: T::CrossAccountId, + _property: Property, + ) -> DispatchResultWithPostInfo { + fail!(>::PropertiesNotAllowed) + } + + fn change_token_property( + &self, + _sender: T::CrossAccountId, + _token_id: TokenId, + _property: Property, + ) -> DispatchResultWithPostInfo { + fail!(>::PropertiesNotAllowed) + } + fn set_variable_metadata( &self, sender: T::CrossAccountId, --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -62,6 +62,8 @@ WrongRefungiblePieces, /// Refungible token can't nest other tokens RefungibleDisallowsNesting, + /// Item properties are not allowed + PropertiesNotAllowed, } #[pallet::config] --- a/pallets/refungible/src/weights.rs +++ b/pallets/refungible/src/weights.rs @@ -38,6 +38,7 @@ fn create_multiple_items_ex_multiple_owners(b: u32, ) -> Weight; fn burn_item_partial() -> Weight; fn burn_item_fully() -> Weight; + fn set_property() -> Weight; fn transfer_normal() -> Weight; fn transfer_creating() -> Weight; fn transfer_removing() -> Weight; @@ -129,6 +130,12 @@ .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(6 as Weight)) } + + fn set_property() -> Weight { + // Error + 0 + } + // Storage: Refungible Balance (r:2 w:2) fn transfer_normal() -> Weight { (19_766_000 as Weight) @@ -297,6 +304,12 @@ .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(6 as Weight)) } + + fn set_property() -> Weight { + // Error + 0 + } + // Storage: Refungible Balance (r:2 w:2) fn transfer_normal() -> Weight { (19_766_000 as Weight) --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -39,7 +39,7 @@ MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH, AccessMode, CreateItemData, CollectionLimits, CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, MetaUpdatePermission, CreateCollectionData, CustomDataLimit, - CreateItemExData, budget, CollectionField, + CreateItemExData, budget, CollectionField, Property, }; use pallet_evm::account::CrossAccountId; use pallet_common::{ --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -22,13 +22,14 @@ }; use frame_support::{ storage::{bounded_btree_map::BoundedBTreeMap, bounded_btree_set::BoundedBTreeSet}, + traits::Get, }; #[cfg(feature = "serde")] use serde::{Serialize, Deserialize}; use sp_core::U256; -use sp_runtime::{ArithmeticError, sp_std::prelude::Vec}; +use sp_runtime::{ArithmeticError, sp_std::prelude::Vec, DispatchError}; use codec::{Decode, Encode, EncodeLike, MaxEncodedLen}; use frame_support::{BoundedVec, traits::ConstU32}; use derivative::Derivative; @@ -85,6 +86,26 @@ pub const MAX_COLLECTION_DESCRIPTION_LENGTH: u32 = 256; pub const MAX_TOKEN_PREFIX_LENGTH: u32 = 16; +pub const MAX_PROPERTY_KEY_LENGTH: u32 = 256; +pub const MAX_PROPERTY_VALUE_LENGTH: u32 = 32768; +pub const MAX_PROPERTIES_PER_ITEM: u32 = 64; + +// pub const MAX_PROPERTY_KEYS_OVERALL_LENGTH: u32 = MAX_PROPERTY_KEY_LENGTH * MAX_PROPERTIES_PER_ITEM; +pub const MAX_COLLECTION_PROPERTIES_SIZE: u32 = 40960; +pub const MAX_TOKEN_PROPERTIES_SIZE: u32 = 32768; + +pub const MAX_COLLECTION_PROPERTIES_ENCODE_LEN: u32 = + MAX_PROPERTIES_PER_ITEM * MAX_PROPERTY_KEY_LENGTH + MAX_COLLECTION_PROPERTIES_SIZE; + +pub struct MaxPropertiesPermissionsEncodeLen; + +impl Get for MaxPropertiesPermissionsEncodeLen { + fn get() -> u32 { + MAX_PROPERTIES_PER_ITEM * MAX_PROPERTY_KEY_LENGTH + + ::max_encoded_len() as u32 + } +} + /// How much items can be created per single /// create_many call pub const MAX_ITEMS_PER_BATCH: u32 = 200; @@ -310,31 +331,32 @@ OffchainSchema, } -#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, Debug, Derivative, MaxEncodedLen)] -#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] -#[derivative(Default(bound = ""))] +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, Derivative, MaxEncodedLen)] +#[derivative(Debug, Default(bound = ""))] pub struct CreateCollectionData { #[derivative(Default(value = "CollectionMode::NFT"))] pub mode: CollectionMode, pub access: Option, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] pub name: BoundedVec>, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] pub description: BoundedVec>, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] pub token_prefix: BoundedVec>, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] pub offchain_schema: BoundedVec>, pub schema_version: Option, pub pending_sponsor: Option, pub limits: Option, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] pub variable_on_chain_schema: BoundedVec>, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] pub const_on_chain_schema: BoundedVec>, pub meta_update_permission: Option, + pub token_property_permissions: CollectionPropertiesPermissionsVec, + pub properties: CollectionPropertiesVec, } +pub type CollectionPropertiesPermissionsVec = + BoundedVec; + +pub type CollectionPropertiesVec = + BoundedVec>; + #[derive(Encode, Decode, Debug, Clone, PartialEq, TypeInfo)] #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] pub struct NftItemType { @@ -607,3 +629,128 @@ 0 } } + +pub type PropertyKey = BoundedVec>; +pub type PropertyValue = BoundedVec>; + +#[derive(Encode, Decode, TypeInfo, Debug, MaxEncodedLen, PartialEq, Clone)] +pub enum PropertyPermission { + None, + AdminConst, + Admin, + ItemOwnerConst, + ItemOwner, + ItemOwnerOrAdmin, +} + +#[derive(Encode, Decode, Debug, TypeInfo, Clone, PartialEq, MaxEncodedLen)] +pub struct Property { + pub key: PropertyKey, + pub value: PropertyValue, +} + +#[derive(Encode, Decode, TypeInfo, Debug, MaxEncodedLen, PartialEq, Clone)] +pub struct PropertyKeyPermission { + pub key: PropertyKey, + pub permission: PropertyPermission, +} + +pub enum PropertiesError { + NoSpaceForProperty, + PropertyLimitReached, +} + +impl From for DispatchError { + fn from(error: PropertiesError) -> Self { + match error { + PropertiesError::NoSpaceForProperty => DispatchError::Other("no space for property"), + PropertiesError::PropertyLimitReached => { + DispatchError::Other("property key limit reached") + } + } + } +} + +pub type PropertiesMap = + BoundedBTreeMap>; +pub type PropertiesPermissionMap = + BoundedBTreeMap>; + +#[derive(Encode, Decode, TypeInfo, Clone, PartialEq, MaxEncodedLen)] +pub struct Properties { + map: PropertiesMap, + consumed_space: u32, + space_limit: u32, +} + +impl Properties { + pub fn new(space_limit: u32) -> Self { + Self { + map: BoundedBTreeMap::new(), + consumed_space: 0, + space_limit, + } + } + + pub fn from_collection_props_vec( + data: CollectionPropertiesVec, + ) -> Result { + let mut props = Self::new(MAX_COLLECTION_PROPERTIES_SIZE); + + for property in data.into_iter() { + props.try_change_property(property)?; + } + + Ok(props) + } + + pub fn try_change_property(&mut self, property: Property) -> Result<(), PropertiesError> { + let value_len = property.value.len(); + + if self.consumed_space as usize + value_len > self.space_limit as usize { + return Err(PropertiesError::NoSpaceForProperty); + } + + self.map + .try_insert(property.key, property.value) + .map_err(|_| PropertiesError::PropertyLimitReached)?; + + self.consumed_space += value_len as u32; + + Ok(()) + } + + pub fn get_property(&self, key: &PropertyKey) -> Option<&PropertyValue> { + self.map.get(key) + } +} + +pub struct CollectionProperties; + +impl Get for CollectionProperties { + fn get() -> Properties { + Properties::new(MAX_COLLECTION_PROPERTIES_SIZE) + } +} + +pub struct TokenProperties; + +impl Get for TokenProperties { + fn get() -> Properties { + Properties::new(MAX_TOKEN_PROPERTIES_SIZE) + } +} + +// #[cfg(not(feature = "std"))] +// fn properties_map_debug(_properties: &PropertiesMap, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { +// write!(f, "") +// } + +// #[cfg(not(feature = "std"))] +// fn opt_properties_permissions_map_debug(properties: &Option, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { +// if properties.is_some() { +// write!(f, "Some()") +// } else { +// write!(f, "None") +// } +// } --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -16,7 +16,9 @@ #![cfg_attr(not(feature = "std"), no_std)] -use up_data_structs::{CollectionId, TokenId, RpcCollection, CollectionStats, CollectionLimits}; +use up_data_structs::{ + CollectionId, TokenId, RpcCollection, CollectionStats, CollectionLimits, Property, +}; use sp_std::vec::Vec; use codec::Decode; use sp_runtime::DispatchError; --- a/runtime/common/src/weights.rs +++ b/runtime/common/src/weights.rs @@ -54,6 +54,10 @@ dispatch_weight::() + max_weight_of!(burn_item()) } + fn set_property() -> Weight { + dispatch_weight::() + max_weight_of!(set_property()) + } + fn transfer() -> Weight { dispatch_weight::() + max_weight_of!(transfer()) }