--- a/client/rpc/src/lib.rs +++ b/client/rpc/src/lib.rs @@ -19,7 +19,7 @@ use codec::Decode; use jsonrpc_core::{Error as RpcError, ErrorCode, Result}; use jsonrpc_derive::rpc; -use up_data_structs::{Collection, CollectionId, CollectionStats, CollectionLimits, TokenId}; +use up_data_structs::{RpcCollection, Collection, CollectionId, CollectionStats, CollectionLimits, TokenId}; use sp_api::{BlockId, BlockT, ProvideRuntimeApi, ApiExt}; use sp_blockchain::HeaderBackend; use up_rpc::UniqueApi as UniqueRuntimeApi; @@ -116,7 +116,7 @@ &self, collection: CollectionId, at: Option, - ) -> Result>>; + ) -> Result>>; #[rpc(name = "unique_collectionStats")] fn collection_stats(&self, at: Option) -> Result; @@ -235,7 +235,7 @@ pass_method!(allowlist(collection: CollectionId) -> Vec); pass_method!(allowed(collection: CollectionId, user: CrossAccountId) -> bool); pass_method!(last_token_id(collection: CollectionId) -> TokenId); - pass_method!(collection_by_id(collection: CollectionId) -> Option>); + pass_method!(collection_by_id(collection: CollectionId) -> Option>); pass_method!(collection_stats() -> CollectionStats); pass_method!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option); pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option); --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -29,11 +29,11 @@ }; use pallet_evm::GasWeightMapping; use up_data_structs::{ - COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH, + COLLECTION_NUMBER_LIMIT, Collection, RpcCollection, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH, COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, TokenId, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode, NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits, - CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit, budget::Budget, + CustomDataLimit, CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit, budget::Budget, COLLECTION_FIELD_LIMIT, CollectionField, }; pub use pallet::*; use sp_core::H160; @@ -352,6 +352,9 @@ OnlyOwnerAllowedToNest, /// Only tokens from specific collections may nest tokens under this SourceCollectionIsNotAllowedToNest, + + /// Tried to store more data than allowed in collection field + CollectionFieldSizeExceeded, } #[pallet::storage] @@ -369,6 +372,17 @@ QueryKind = OptionQuery, >; + /// Large variable-size collection fields are extracted here + #[pallet::storage] + pub type CollectionData = StorageNMap< + Key = ( + Key, + Key, + ), + Value = BoundedVec>, + QueryKind = ValueQuery, + >; + #[pallet::storage] pub type AdminAmount = StorageMap< Hasher = Blake2_128Concat, @@ -409,7 +423,26 @@ fn on_runtime_upgrade() -> Weight { if StorageVersion::get::>() < StorageVersion::new(1) { use up_data_structs::{CollectionVersion1, CollectionVersion2}; - >::translate_values::, _>(|v| { + >::translate::, _>(|id, v| { + Self::set_field_raw( + id, + CollectionField::OffchainSchema, + v.offchain_schema.clone().into_inner(), + ) + .expect("data has lower bounds than field"); + Self::set_field_raw( + id, + CollectionField::VariableOnChainSchema, + v.variable_on_chain_schema.clone().into_inner(), + ) + .expect("data has lower bounds than field"); + Self::set_field_raw( + id, + CollectionField::ConstOnChainSchema, + v.const_on_chain_schema.clone().into_inner(), + ) + .expect("data has lower bounds than field"); + Some(CollectionVersion2::from(v)) }); } @@ -483,6 +516,50 @@ Some(effective_limits) } + + pub fn rpc_collection(collection: CollectionId) -> Option> { + let Collection { + name, + description, + owner, + mode, + access, + token_prefix, + mint_mode, + schema_version, + sponsorship, + limits, + meta_update_permission, + } = >::get(collection)?; + Some(RpcCollection { + name: name.into_inner(), + description: description.into_inner(), + owner, + mode, + access, + token_prefix: token_prefix.into_inner(), + mint_mode, + schema_version, + sponsorship, + limits, + meta_update_permission, + offchain_schema: >::get(( + collection, + CollectionField::OffchainSchema, + )) + .into_inner(), + const_on_chain_schema: >::get(( + collection, + CollectionField::ConstOnChainSchema, + )) + .into_inner(), + variable_on_chain_schema: >::get(( + collection, + CollectionField::VariableOnChainSchema, + )) + .into_inner(), + }) + } } impl Pallet { @@ -520,14 +597,11 @@ access: data.access.unwrap_or_default(), description: data.description, token_prefix: data.token_prefix, - offchain_schema: data.offchain_schema, schema_version: data.schema_version.unwrap_or_default(), sponsorship: data .pending_sponsor .map(SponsorshipState::Unconfirmed) .unwrap_or_default(), - variable_on_chain_schema: data.variable_on_chain_schema, - const_on_chain_schema: data.const_on_chain_schema, limits: data .limits .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits)) @@ -557,6 +631,24 @@ >::put(created_count); >::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone())); >::insert(id, collection); + Self::set_field_raw( + id, + CollectionField::OffchainSchema, + data.offchain_schema.into_inner(), + ) + .expect("data has lower bounds than field"); + Self::set_field_raw( + id, + CollectionField::VariableOnChainSchema, + data.variable_on_chain_schema.into_inner(), + ) + .expect("data has lower bounds than field"); + Self::set_field_raw( + id, + CollectionField::ConstOnChainSchema, + data.const_on_chain_schema.into_inner(), + ) + .expect("data has lower bounds than field"); Ok(id) } @@ -579,6 +671,7 @@ >::put(destroyed_collections); >::remove(collection.id); + >::remove_prefix((collection.id,), None); >::remove(collection.id); >::remove_prefix((collection.id,), None); >::remove_prefix((collection.id,), None); @@ -587,6 +680,35 @@ Ok(()) } + fn set_field_raw( + collection_id: CollectionId, + field: CollectionField, + value: Vec, + ) -> DispatchResult { + if !value.is_empty() { + >::insert( + (collection_id, field), + BoundedVec::try_from(value).map_err(|_| >::CollectionFieldSizeExceeded)?, + ) + } else { + >::remove((collection_id, field)); + } + Ok(()) + } + + pub fn set_field( + collection: &CollectionHandle, + sender: &T::CrossAccountId, + field: CollectionField, + value: Vec, + ) -> DispatchResult { + collection.check_is_owner_or_admin(sender)?; + + // ========= + + Self::set_field_raw(collection.id, field, value) + } + pub fn toggle_allowlist( collection: &CollectionHandle, sender: &T::CrossAccountId, --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -43,7 +43,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, + CreateItemExData, budget, CollectionField, }; use pallet_evm::account::CrossAccountId; use pallet_common::{ @@ -1004,16 +1004,16 @@ schema: BoundedVec>, ) -> DispatchResult { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let mut target_collection = >::try_get(collection_id)?; - target_collection.check_is_owner_or_admin(&sender)?; + let collection = >::try_get(collection_id)?; + + // ========= - target_collection.offchain_schema = schema; + >::set_field(&collection, &sender, CollectionField::OffchainSchema, schema.into_inner())?; >::deposit_event(Event::::OffchainSchemaSet( collection_id )); - - target_collection.save() + Ok(()) } /// Set const on-chain data schema. @@ -1036,16 +1036,16 @@ schema: BoundedVec> ) -> DispatchResult { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let mut target_collection = >::try_get(collection_id)?; - target_collection.check_is_owner_or_admin(&sender)?; + let collection = >::try_get(collection_id)?; + + // ========= - target_collection.const_on_chain_schema = schema; + >::set_field(&collection, &sender, CollectionField::ConstOnChainSchema, schema.into_inner())?; >::deposit_event(Event::::ConstOnChainSchemaSet( collection_id )); - - target_collection.save() + Ok(()) } /// Set variable on-chain data schema. @@ -1068,16 +1068,16 @@ schema: BoundedVec> ) -> DispatchResult { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let mut target_collection = >::try_get(collection_id)?; - target_collection.check_is_owner_or_admin(&sender)?; + let collection = >::try_get(collection_id)?; + + // ========= - target_collection.variable_on_chain_schema = schema; + >::set_field(&collection, &sender, CollectionField::VariableOnChainSchema, schema.into_inner())?; >::deposit_event(Event::::VariableOnChainSchemaSet( collection_id )); - - target_collection.save() + Ok(()) } #[weight = >::set_collection_limits()] --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -78,6 +78,9 @@ pub const VARIABLE_ON_CHAIN_SCHEMA_LIMIT: u32 = 8192; pub const CONST_ON_CHAIN_SCHEMA_LIMIT: u32 = 32768; +pub const COLLECTION_FIELD_LIMIT: u32 = CONST_ON_CHAIN_SCHEMA_LIMIT; +// u32::max is not const: OFFCHAIN_SCHEMA_LIMIT.max(VARIABLE_ON_CHAIN_SCHEMA_LIMIT).max(CONST_ON_CHAIN_SCHEMA_LIMIT); + pub const MAX_COLLECTION_NAME_LENGTH: u32 = 64; pub const MAX_COLLECTION_DESCRIPTION_LENGTH: u32 = 256; pub const MAX_TOKEN_PREFIX_LENGTH: u32 = 16; @@ -248,22 +251,21 @@ } } +/// Used in storage #[struct_versioning::versioned(version = 2, upper)] #[derive(Encode, Decode, Clone, PartialEq, TypeInfo, MaxEncodedLen)] -#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] pub struct Collection { pub owner: AccountId, pub mode: CollectionMode, pub access: AccessMode, - #[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>, pub mint_mode: bool, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] + + #[version(..2)] pub offchain_schema: BoundedVec>, + pub schema_version: SchemaVersion, pub sponsorship: SponsorshipState, @@ -272,13 +274,42 @@ #[version(2.., upper(limits.into()))] pub limits: CollectionLimitsVersion2, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] + #[version(..2)] pub variable_on_chain_schema: BoundedVec>, - #[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))] + #[version(..2)] pub const_on_chain_schema: BoundedVec>, + pub meta_update_permission: MetaUpdatePermission, } +/// Used in RPC calls +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo)] +#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] +pub struct RpcCollection { + pub owner: AccountId, + pub mode: CollectionMode, + pub access: AccessMode, + pub name: Vec, + pub description: Vec, + pub token_prefix: Vec, + pub mint_mode: bool, + pub offchain_schema: Vec, + pub schema_version: SchemaVersion, + pub sponsorship: SponsorshipState, + pub limits: CollectionLimits, + pub variable_on_chain_schema: Vec, + pub const_on_chain_schema: Vec, + pub meta_update_permission: MetaUpdatePermission, +} + +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, MaxEncodedLen)] +#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] +pub enum CollectionField { + VariableOnChainSchema, + ConstOnChainSchema, + OffchainSchema, +} + #[derive(Encode, Decode, Clone, PartialEq, TypeInfo, Debug, Derivative, MaxEncodedLen)] #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] #[derivative(Default(bound = ""))] --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -16,7 +16,7 @@ #![cfg_attr(not(feature = "std"), no_std)] -use up_data_structs::{CollectionId, TokenId, Collection, CollectionStats, CollectionLimits}; +use up_data_structs::{CollectionId, TokenId, RpcCollection, Collection, CollectionStats, CollectionLimits}; use sp_std::vec::Vec; use codec::Decode; use sp_runtime::DispatchError; @@ -53,7 +53,7 @@ fn allowlist(collection: CollectionId) -> Result>; fn allowed(collection: CollectionId, user: CrossAccountId) -> Result; fn last_token_id(collection: CollectionId) -> Result; - fn collection_by_id(collection: CollectionId) -> Result>>; + fn collection_by_id(collection: CollectionId) -> Result>>; fn collection_stats() -> Result; fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result>; fn effective_collection_limits(collection_id: CollectionId) -> Result>; --- a/runtime/common/src/runtime_apis.rs +++ b/runtime/common/src/runtime_apis.rs @@ -58,8 +58,8 @@ fn last_token_id(collection: CollectionId) -> Result { dispatch_unique_runtime!(collection.last_token_id()) } - fn collection_by_id(collection: CollectionId) -> Result>, DispatchError> { - Ok(>::get(collection)) + fn collection_by_id(collection: CollectionId) -> Result>, DispatchError> { + Ok(>::rpc_collection(collection)) } fn collection_stats() -> Result { Ok(>::collection_stats()) --- a/runtime/opal/src/lib.rs +++ b/runtime/opal/src/lib.rs @@ -67,7 +67,7 @@ }, }; use up_data_structs::mapping::{EvmTokenAddressMapping, CrossTokenAddressMapping}; -use up_data_structs::{CollectionId, TokenId, CollectionStats, Collection}; +use up_data_structs::{CollectionId, TokenId, CollectionStats, Collection, RpcCollection}; // use pallet_contracts::weights::WeightInfo; // #[cfg(any(feature = "std", test))] use frame_system::{