diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index be083efcc0706..5b6cb2ec574d3 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -1491,6 +1491,7 @@ parameter_types! { pub const KeyLimit: u32 = 32; pub const ValueLimit: u32 = 256; pub const ApprovalsLimit: u32 = 20; + pub const ItemAttributesApprovalsLimit: u32 = 20; pub const MaxTips: u32 = 10; pub const MaxDeadlineDuration: BlockNumber = 12 * 30 * DAYS; } @@ -1535,6 +1536,7 @@ impl pallet_nfts::Config for Runtime { type KeyLimit = KeyLimit; type ValueLimit = ValueLimit; type ApprovalsLimit = ApprovalsLimit; + type ItemAttributesApprovalsLimit = ItemAttributesApprovalsLimit; type MaxTips = MaxTips; type MaxDeadlineDuration = MaxDeadlineDuration; type Features = Features; diff --git a/frame/nfts/src/benchmarking.rs b/frame/nfts/src/benchmarking.rs index 61407abd9f985..5e1b0237ca3ec 100644 --- a/frame/nfts/src/benchmarking.rs +++ b/frame/nfts/src/benchmarking.rs @@ -114,6 +114,7 @@ fn add_item_attribute, I: 'static>( SystemOrigin::Signed(caller.clone()).into(), T::Helper::collection(0), Some(item), + AttributeNamespace::CollectionOwner, key.clone(), vec![0; T::ValueLimit::get() as usize].try_into().unwrap(), )); @@ -338,10 +339,38 @@ benchmarks_instance_pallet! { let (collection, caller, _) = create_collection::(); let (item, ..) = mint_item::(0); - add_item_metadata::(item); - }: _(SystemOrigin::Signed(caller), collection, Some(item), key.clone(), value.clone()) + }: _(SystemOrigin::Signed(caller), collection, Some(item), AttributeNamespace::CollectionOwner, key.clone(), value.clone()) + verify { + assert_last_event::( + Event::AttributeSet { + collection, + maybe_item: Some(item), + namespace: AttributeNamespace::CollectionOwner, + key, + value, + } + .into(), + ); + } + + force_set_attribute { + let key: BoundedVec<_, _> = vec![0u8; T::KeyLimit::get() as usize].try_into().unwrap(); + let value: BoundedVec<_, _> = vec![0u8; T::ValueLimit::get() as usize].try_into().unwrap(); + + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + }: _(SystemOrigin::Root, Some(caller), collection, Some(item), AttributeNamespace::CollectionOwner, key.clone(), value.clone()) verify { - assert_last_event::(Event::AttributeSet { collection, maybe_item: Some(item), key, value }.into()); + assert_last_event::( + Event::AttributeSet { + collection, + maybe_item: Some(item), + namespace: AttributeNamespace::CollectionOwner, + key, + value, + } + .into(), + ); } clear_attribute { @@ -349,9 +378,76 @@ benchmarks_instance_pallet! { let (item, ..) = mint_item::(0); add_item_metadata::(item); let (key, ..) = add_item_attribute::(item); - }: _(SystemOrigin::Signed(caller), collection, Some(item), key.clone()) + }: _(SystemOrigin::Signed(caller), collection, Some(item), AttributeNamespace::CollectionOwner, key.clone()) + verify { + assert_last_event::( + Event::AttributeCleared { + collection, + maybe_item: Some(item), + namespace: AttributeNamespace::CollectionOwner, + key, + }.into(), + ); + } + + approve_item_attributes { + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + }: _(SystemOrigin::Signed(caller), collection, item, target_lookup) verify { - assert_last_event::(Event::AttributeCleared { collection, maybe_item: Some(item), key }.into()); + assert_last_event::( + Event::ItemAttributesApprovalAdded { + collection, + item, + delegate: target, + } + .into(), + ); + } + + cancel_item_attributes_approval { + let n in 0 .. 1_000; + + let (collection, caller, _) = create_collection::(); + let (item, ..) = mint_item::(0); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + Nfts::::approve_item_attributes( + SystemOrigin::Signed(caller.clone()).into(), + collection, + item, + target_lookup.clone(), + )?; + T::Currency::make_free_balance_be(&target, DepositBalanceOf::::max_value()); + let value: BoundedVec<_, _> = vec![0u8; T::ValueLimit::get() as usize].try_into().unwrap(); + for i in 0..n { + let mut key = vec![0u8; T::KeyLimit::get() as usize]; + let mut s = Vec::from((i as u16).to_be_bytes()); + key.truncate(s.len()); + key.append(&mut s); + + Nfts::::set_attribute( + SystemOrigin::Signed(target.clone()).into(), + T::Helper::collection(0), + Some(item), + AttributeNamespace::Account(target.clone()), + key.try_into().unwrap(), + value.clone(), + )?; + } + let witness = CancelAttributesApprovalWitness { account_attributes: n }; + }: _(SystemOrigin::Signed(caller), collection, item, target_lookup, witness) + verify { + assert_last_event::( + Event::ItemAttributesApprovalRemoved { + collection, + item, + delegate: target, + } + .into(), + ); } set_metadata { diff --git a/frame/nfts/src/features/attributes.rs b/frame/nfts/src/features/attributes.rs index 85c1e0b302d12..0d65a1169323b 100644 --- a/frame/nfts/src/features/attributes.rs +++ b/frame/nfts/src/features/attributes.rs @@ -20,9 +20,10 @@ use frame_support::pallet_prelude::*; impl, I: 'static> Pallet { pub(crate) fn do_set_attribute( - maybe_check_owner: Option, + origin: T::AccountId, collection: T::CollectionId, maybe_item: Option, + namespace: AttributeNamespace, key: BoundedVec, value: BoundedVec, ) -> DispatchResult { @@ -34,90 +35,273 @@ impl, I: 'static> Pallet { let mut collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - if let Some(check_owner) = &maybe_check_owner { - ensure!(check_owner == &collection_details.owner, Error::::NoPermission); - } + ensure!( + Self::is_valid_namespace( + &origin, + &namespace, + &collection, + &collection_details.owner, + &maybe_item, + )?, + Error::::NoPermission + ); let collection_config = Self::get_collection_config(&collection)?; - match maybe_item { - None => { - ensure!( - collection_config.is_setting_enabled(CollectionSetting::UnlockedAttributes), - Error::::LockedCollectionAttributes - ) - }, - Some(item) => { - let maybe_is_locked = Self::get_item_config(&collection, &item) - .map(|c| c.has_disabled_setting(ItemSetting::UnlockedAttributes))?; - ensure!(!maybe_is_locked, Error::::LockedItemAttributes); + // for the `CollectionOwner` namespace we need to check if the collection/item is not locked + match namespace { + AttributeNamespace::CollectionOwner => match maybe_item { + None => { + ensure!( + collection_config.is_setting_enabled(CollectionSetting::UnlockedAttributes), + Error::::LockedCollectionAttributes + ) + }, + Some(item) => { + let maybe_is_locked = Self::get_item_config(&collection, &item) + .map(|c| c.has_disabled_setting(ItemSetting::UnlockedAttributes))?; + ensure!(!maybe_is_locked, Error::::LockedItemAttributes); + }, }, - }; + _ => (), + } - let attribute = Attribute::::get((collection, maybe_item, &key)); + let attribute = Attribute::::get((collection, maybe_item, &namespace, &key)); if attribute.is_none() { collection_details.attributes.saturating_inc(); } - let old_deposit = attribute.map_or(Zero::zero(), |m| m.1); - collection_details.total_deposit.saturating_reduce(old_deposit); + + let old_deposit = + attribute.map_or(AttributeDeposit { account: None, amount: Zero::zero() }, |m| m.1); + let mut deposit = Zero::zero(); - if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) && - maybe_check_owner.is_some() - { + if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) { deposit = T::DepositPerByte::get() .saturating_mul(((key.len() + value.len()) as u32).into()) .saturating_add(T::AttributeDepositBase::get()); } - collection_details.total_deposit.saturating_accrue(deposit); - if deposit > old_deposit { - T::Currency::reserve(&collection_details.owner, deposit - old_deposit)?; - } else if deposit < old_deposit { - T::Currency::unreserve(&collection_details.owner, old_deposit - deposit); + + // NOTE: when we transfer an item, we don't move attributes in the ItemOwner namespace. + // When the new owner updates the same attribute, we will update the depositor record + // and return the deposit to the previous owner. + if old_deposit.account.is_some() && old_deposit.account != Some(origin.clone()) { + T::Currency::unreserve(&old_deposit.account.unwrap(), old_deposit.amount); + T::Currency::reserve(&origin, deposit)?; + } else if deposit > old_deposit.amount { + T::Currency::reserve(&origin, deposit - old_deposit.amount)?; + } else if deposit < old_deposit.amount { + T::Currency::unreserve(&origin, old_deposit.amount - deposit); } - Attribute::::insert((&collection, maybe_item, &key), (&value, deposit)); + // NOTE: we don't track the depositor in the CollectionOwner namespace as it's always a + // collection's owner. This simplifies the collection's transfer to another owner. + let deposit_owner = match namespace { + AttributeNamespace::CollectionOwner => { + collection_details.owner_deposit.saturating_accrue(deposit); + collection_details.owner_deposit.saturating_reduce(old_deposit.amount); + None + }, + _ => Some(origin), + }; + + Attribute::::insert( + (&collection, maybe_item, &namespace, &key), + (&value, AttributeDeposit { account: deposit_owner, amount: deposit }), + ); Collection::::insert(collection, &collection_details); - Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value }); + Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace }); Ok(()) } - pub(crate) fn do_clear_attribute( - maybe_check_owner: Option, + pub(crate) fn do_force_set_attribute( + set_as: Option, collection: T::CollectionId, maybe_item: Option, + namespace: AttributeNamespace, key: BoundedVec, + value: BoundedVec, ) -> DispatchResult { let mut collection_details = Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; - if let Some(check_owner) = &maybe_check_owner { - ensure!(check_owner == &collection_details.owner, Error::::NoPermission); + + let attribute = Attribute::::get((collection, maybe_item, &namespace, &key)); + if let Some((_, deposit)) = attribute { + if deposit.account != set_as && deposit.amount != Zero::zero() { + if let Some(deposit_account) = deposit.account { + T::Currency::unreserve(&deposit_account, deposit.amount); + } + } + } else { + collection_details.attributes.saturating_inc(); } - if maybe_check_owner.is_some() { - match maybe_item { - None => { - let collection_config = Self::get_collection_config(&collection)?; + Attribute::::insert( + (&collection, maybe_item, &namespace, &key), + (&value, AttributeDeposit { account: set_as, amount: Zero::zero() }), + ); + Collection::::insert(collection, &collection_details); + Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace }); + Ok(()) + } + + pub(crate) fn do_clear_attribute( + maybe_check_owner: Option, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + ) -> DispatchResult { + if let Some((_, deposit)) = + Attribute::::take((collection, maybe_item, &namespace, &key)) + { + let mut collection_details = + Collection::::get(&collection).ok_or(Error::::UnknownCollection)?; + + if let Some(check_owner) = &maybe_check_owner { + if deposit.account != maybe_check_owner { ensure!( - collection_config.is_setting_enabled(CollectionSetting::UnlockedAttributes), - Error::::LockedCollectionAttributes - ) - }, - Some(item) => { - // NOTE: if the item was previously burned, the ItemConfigOf record might - // not exist. In that case, we allow to clear the attribute. - let maybe_is_locked = Self::get_item_config(&collection, &item) - .map_or(false, |c| c.has_disabled_setting(ItemSetting::UnlockedAttributes)); - ensure!(!maybe_is_locked, Error::::LockedItemAttributes); - }, - }; - } + Self::is_valid_namespace( + &check_owner, + &namespace, + &collection, + &collection_details.owner, + &maybe_item, + )?, + Error::::NoPermission + ); + } + + // can't clear `CollectionOwner` type attributes if the collection/item is locked + match namespace { + AttributeNamespace::CollectionOwner => match maybe_item { + None => { + let collection_config = Self::get_collection_config(&collection)?; + ensure!( + collection_config + .is_setting_enabled(CollectionSetting::UnlockedAttributes), + Error::::LockedCollectionAttributes + ) + }, + Some(item) => { + // NOTE: if the item was previously burned, the ItemConfigOf record + // might not exist. In that case, we allow to clear the attribute. + let maybe_is_locked = Self::get_item_config(&collection, &item) + .map_or(false, |c| { + c.has_disabled_setting(ItemSetting::UnlockedAttributes) + }); + ensure!(!maybe_is_locked, Error::::LockedItemAttributes); + }, + }, + _ => (), + }; + } - if let Some((_, deposit)) = Attribute::::take((collection, maybe_item, &key)) { collection_details.attributes.saturating_dec(); - collection_details.total_deposit.saturating_reduce(deposit); - T::Currency::unreserve(&collection_details.owner, deposit); + match namespace { + AttributeNamespace::CollectionOwner => { + collection_details.owner_deposit.saturating_reduce(deposit.amount); + T::Currency::unreserve(&collection_details.owner, deposit.amount); + }, + _ => (), + }; + if let Some(deposit_account) = deposit.account { + T::Currency::unreserve(&deposit_account, deposit.amount); + } Collection::::insert(collection, &collection_details); - Self::deposit_event(Event::AttributeCleared { collection, maybe_item, key }); + Self::deposit_event(Event::AttributeCleared { collection, maybe_item, key, namespace }); } Ok(()) } + + pub(crate) fn do_approve_item_attributes( + check_origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Attributes), + Error::::MethodDisabled + ); + + let details = Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + ensure!(check_origin == details.owner, Error::::NoPermission); + + ItemAttributesApprovalsOf::::try_mutate(collection, item, |approvals| { + approvals + .try_insert(delegate.clone()) + .map_err(|_| Error::::ReachedApprovalLimit)?; + + Self::deposit_event(Event::ItemAttributesApprovalAdded { collection, item, delegate }); + Ok(()) + }) + } + + pub(crate) fn do_cancel_item_attributes_approval( + check_origin: T::AccountId, + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + witness: CancelAttributesApprovalWitness, + ) -> DispatchResult { + ensure!( + Self::is_pallet_feature_enabled(PalletFeature::Attributes), + Error::::MethodDisabled + ); + + let details = Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + ensure!(check_origin == details.owner, Error::::NoPermission); + + ItemAttributesApprovalsOf::::try_mutate(collection, item, |approvals| { + approvals.remove(&delegate); + + let mut attributes: u32 = 0; + let mut deposited: DepositBalanceOf = Zero::zero(); + for (_, (_, deposit)) in Attribute::::drain_prefix(( + &collection, + Some(item), + AttributeNamespace::Account(delegate.clone()), + )) { + attributes.saturating_inc(); + deposited = deposited.saturating_add(deposit.amount); + } + ensure!(attributes <= witness.account_attributes, Error::::BadWitness); + + if !deposited.is_zero() { + T::Currency::unreserve(&delegate, deposited); + } + + Self::deposit_event(Event::ItemAttributesApprovalRemoved { + collection, + item, + delegate, + }); + Ok(()) + }) + } + + fn is_valid_namespace( + origin: &T::AccountId, + namespace: &AttributeNamespace, + collection: &T::CollectionId, + collection_owner: &T::AccountId, + maybe_item: &Option, + ) -> Result { + let mut result = false; + match namespace { + AttributeNamespace::CollectionOwner => result = origin == collection_owner, + AttributeNamespace::ItemOwner => + if let Some(item) = maybe_item { + let item_details = + Item::::get(&collection, &item).ok_or(Error::::UnknownItem)?; + result = origin == &item_details.owner + }, + AttributeNamespace::Account(account_id) => + if let Some(item) = maybe_item { + let approvals = ItemAttributesApprovalsOf::::get(&collection, &item); + result = account_id == origin && approvals.contains(&origin) + }, + _ => (), + }; + Ok(result) + } } diff --git a/frame/nfts/src/features/create_delete_collection.rs b/frame/nfts/src/features/create_delete_collection.rs index b9530e88b18cd..86625bf49efb2 100644 --- a/frame/nfts/src/features/create_delete_collection.rs +++ b/frame/nfts/src/features/create_delete_collection.rs @@ -35,7 +35,7 @@ impl, I: 'static> Pallet { collection, CollectionDetails { owner: owner.clone(), - total_deposit: deposit, + owner_deposit: deposit, items: 0, item_metadatas: 0, attributes: 0, @@ -90,12 +90,21 @@ impl, I: 'static> Pallet { PendingSwapOf::::remove_prefix(&collection, None); CollectionMetadataOf::::remove(&collection); Self::clear_roles(&collection)?; - #[allow(deprecated)] - Attribute::::remove_prefix((&collection,), None); + + for (_, (_, deposit)) in Attribute::::drain_prefix((&collection,)) { + if !deposit.amount.is_zero() { + if let Some(account) = deposit.account { + T::Currency::unreserve(&account, deposit.amount); + } + } + } + CollectionAccount::::remove(&collection_details.owner, &collection); - T::Currency::unreserve(&collection_details.owner, collection_details.total_deposit); + T::Currency::unreserve(&collection_details.owner, collection_details.owner_deposit); CollectionConfigOf::::remove(&collection); let _ = ItemConfigOf::::clear_prefix(&collection, witness.items, None); + let _ = + ItemAttributesApprovalsOf::::clear_prefix(&collection, witness.items, None); Self::deposit_event(Event::Destroyed { collection }); diff --git a/frame/nfts/src/features/create_delete_item.rs b/frame/nfts/src/features/create_delete_item.rs index 10670f4b10c1c..bae1d02c8ad6b 100644 --- a/frame/nfts/src/features/create_delete_item.rs +++ b/frame/nfts/src/features/create_delete_item.rs @@ -109,6 +109,7 @@ impl, I: 'static> Pallet { Account::::remove((&owner, &collection, &item)); ItemPriceOf::::remove(&collection, &item); PendingSwapOf::::remove(&collection, &item); + ItemAttributesApprovalsOf::::remove(&collection, &item); // NOTE: if item's settings are not empty (e.g. item's metadata is locked) // then we keep the record and don't remove it diff --git a/frame/nfts/src/features/metadata.rs b/frame/nfts/src/features/metadata.rs index 0b0a337197d9b..3a12dbe64f2f4 100644 --- a/frame/nfts/src/features/metadata.rs +++ b/frame/nfts/src/features/metadata.rs @@ -46,7 +46,7 @@ impl, I: 'static> Pallet { collection_details.item_metadatas.saturating_inc(); } let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); - collection_details.total_deposit.saturating_reduce(old_deposit); + collection_details.owner_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) && maybe_check_owner.is_some() @@ -60,7 +60,7 @@ impl, I: 'static> Pallet { } else if deposit < old_deposit { T::Currency::unreserve(&collection_details.owner, old_deposit - deposit); } - collection_details.total_deposit.saturating_accrue(deposit); + collection_details.owner_deposit.saturating_accrue(deposit); *metadata = Some(ItemMetadata { deposit, data: data.clone() }); @@ -93,7 +93,7 @@ impl, I: 'static> Pallet { } let deposit = metadata.take().ok_or(Error::::UnknownItem)?.deposit; T::Currency::unreserve(&collection_details.owner, deposit); - collection_details.total_deposit.saturating_reduce(deposit); + collection_details.owner_deposit.saturating_reduce(deposit); Collection::::insert(&collection, &collection_details); Self::deposit_event(Event::MetadataCleared { collection, item }); @@ -121,7 +121,7 @@ impl, I: 'static> Pallet { CollectionMetadataOf::::try_mutate_exists(collection, |metadata| { let old_deposit = metadata.take().map_or(Zero::zero(), |m| m.deposit); - details.total_deposit.saturating_reduce(old_deposit); + details.owner_deposit.saturating_reduce(old_deposit); let mut deposit = Zero::zero(); if maybe_check_owner.is_some() && collection_config.is_setting_enabled(CollectionSetting::DepositRequired) @@ -135,7 +135,7 @@ impl, I: 'static> Pallet { } else if deposit < old_deposit { T::Currency::unreserve(&details.owner, old_deposit - deposit); } - details.total_deposit.saturating_accrue(deposit); + details.owner_deposit.saturating_accrue(deposit); Collection::::insert(&collection, details); diff --git a/frame/nfts/src/features/transfer.rs b/frame/nfts/src/features/transfer.rs index 7ebad853902a9..7d6ae3553a361 100644 --- a/frame/nfts/src/features/transfer.rs +++ b/frame/nfts/src/features/transfer.rs @@ -100,11 +100,12 @@ impl, I: 'static> Pallet { T::Currency::repatriate_reserved( &details.owner, &owner, - details.total_deposit, + details.owner_deposit, Reserved, )?; CollectionAccount::::remove(&details.owner, &collection); CollectionAccount::::insert(&owner, &collection, ()); + details.owner = owner.clone(); OwnershipAcceptance::::remove(&owner); @@ -150,7 +151,7 @@ impl, I: 'static> Pallet { T::Currency::repatriate_reserved( &details.owner, &owner, - details.total_deposit, + details.owner_deposit, Reserved, )?; diff --git a/frame/nfts/src/impl_nonfungibles.rs b/frame/nfts/src/impl_nonfungibles.rs index b42147e6687d9..a9e05a6f41ce9 100644 --- a/frame/nfts/src/impl_nonfungibles.rs +++ b/frame/nfts/src/impl_nonfungibles.rs @@ -49,6 +49,7 @@ impl, I: 'static> Inspect<::AccountId> for Palle fn attribute( collection: &Self::CollectionId, item: &Self::ItemId, + namespace: &AttributeNamespace<::AccountId>, key: &[u8], ) -> Option> { if key.is_empty() { @@ -56,7 +57,7 @@ impl, I: 'static> Inspect<::AccountId> for Palle ItemMetadataOf::::get(collection, item).map(|m| m.data.into()) } else { let key = BoundedSlice::<_, _>::try_from(key).ok()?; - Attribute::::get((collection, Some(item), key)).map(|a| a.0.into()) + Attribute::::get((collection, Some(item), namespace, key)).map(|a| a.0.into()) } } @@ -71,7 +72,13 @@ impl, I: 'static> Inspect<::AccountId> for Palle CollectionMetadataOf::::get(collection).map(|m| m.data.into()) } else { let key = BoundedSlice::<_, _>::try_from(key).ok()?; - Attribute::::get((collection, Option::::None, key)).map(|a| a.0.into()) + Attribute::::get(( + collection, + Option::::None, + AttributeNamespace::CollectionOwner, + key, + )) + .map(|a| a.0.into()) } } diff --git a/frame/nfts/src/lib.rs b/frame/nfts/src/lib.rs index 0f3d3c89c2932..8de9f3103e7c2 100644 --- a/frame/nfts/src/lib.rs +++ b/frame/nfts/src/lib.rs @@ -44,11 +44,10 @@ pub mod macros; pub mod weights; use codec::{Decode, Encode}; -use frame_support::{ - traits::{ - tokens::Locker, BalanceStatus::Reserved, Currency, EnsureOriginWithArg, ReservableCurrency, - }, - BoundedBTreeMap, +use frame_support::traits::{ + tokens::{AttributeNamespace, Locker}, + BalanceStatus::Reserved, + Currency, EnsureOriginWithArg, ReservableCurrency, }; use frame_system::Config as SystemConfig; use sp_runtime::{ @@ -156,6 +155,10 @@ pub mod pallet { #[pallet::constant] type ApprovalsLimit: Get; + /// The maximum attributes approvals an item could have. + #[pallet::constant] + type ItemAttributesApprovalsLimit: Get; + /// The max number of tips a user could send. #[pallet::constant] type MaxTips: Get; @@ -271,9 +274,10 @@ pub mod pallet { ( NMapKey, NMapKey>, + NMapKey>, NMapKey>, ), - (BoundedVec, DepositBalanceOf), + (BoundedVec, AttributeDepositOf), OptionQuery, >; @@ -289,6 +293,18 @@ pub mod pallet { OptionQuery, >; + /// Item attribute approvals. + #[pallet::storage] + pub(super) type ItemAttributesApprovalsOf, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::CollectionId, + Blake2_128Concat, + T::ItemId, + ItemAttributesApprovals, + ValueQuery, + >; + /// Stores the `CollectionId` that is going to be used for the next collection. /// This gets incremented by 1 whenever a new collection is created. #[pallet::storage] @@ -412,12 +428,26 @@ pub mod pallet { maybe_item: Option, key: BoundedVec, value: BoundedVec, + namespace: AttributeNamespace, }, /// Attribute metadata has been cleared for a `collection` or `item`. AttributeCleared { collection: T::CollectionId, maybe_item: Option, key: BoundedVec, + namespace: AttributeNamespace, + }, + /// A new approval to modify item attributes was added. + ItemAttributesApprovalAdded { + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, + }, + /// A new approval to modify item attributes was removed. + ItemAttributesApprovalRemoved { + collection: T::CollectionId, + item: T::ItemId, + delegate: T::AccountId, }, /// Ownership acceptance has changed for an account. OwnershipAcceptanceChanged { who: T::AccountId, maybe_collection: Option }, @@ -867,7 +897,7 @@ pub mod pallet { /// /// Origin must be Signed and the sender should be the Owner of the `collection`. /// - /// - `collection`: The collection to be frozen. + /// - `collection`: The collection of the items to be reevaluated. /// - `items`: The items of the collection whose deposits will be reevaluated. /// /// NOTE: This exists as a best-effort function. Any items which are unknown or @@ -1208,15 +1238,20 @@ pub mod pallet { /// Set an attribute for a collection or item. /// - /// Origin must be either `ForceOrigin` or Signed and the sender should be the Owner of the - /// `collection`. + /// Origin must be Signed and must conform to the namespace ruleset: + /// - `CollectionOwner` namespace could be modified by the `collection` owner only; + /// - `ItemOwner` namespace could be modified by the `maybe_item` owner only. `maybe_item` + /// should be set in that case; + /// - `Account(AccountId)` namespace could be modified only when the `origin` was given a + /// permission to do so; /// - /// If the origin is Signed, then funds of signer are reserved according to the formula: - /// `MetadataDepositBase + DepositPerByte * (key.len + value.len)` taking into + /// The funds of `origin` are reserved according to the formula: + /// `AttributeDepositBase + DepositPerByte * (key.len + value.len)` taking into /// account any already reserved funds. /// /// - `collection`: The identifier of the collection whose item's metadata to set. /// - `maybe_item`: The identifier of the item whose metadata to set. + /// - `namespace`: Attribute's namespace. /// - `key`: The key of the attribute. /// - `value`: The value to which to set the attribute. /// @@ -1228,13 +1263,43 @@ pub mod pallet { origin: OriginFor, collection: T::CollectionId, maybe_item: Option, + namespace: AttributeNamespace, key: BoundedVec, value: BoundedVec, ) -> DispatchResult { - let maybe_check_owner = T::ForceOrigin::try_origin(origin) - .map(|_| None) - .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; - Self::do_set_attribute(maybe_check_owner, collection, maybe_item, key, value) + let origin = ensure_signed(origin)?; + Self::do_set_attribute(origin, collection, maybe_item, namespace, key, value) + } + + /// Force-set an attribute for a collection or item. + /// + /// Origin must be `ForceOrigin`. + /// + /// If the attribute already exists and it was set by another account, the deposit + /// will be returned to the previous owner. + /// + /// - `set_as`: An optional owner of the attribute. + /// - `collection`: The identifier of the collection whose item's metadata to set. + /// - `maybe_item`: The identifier of the item whose metadata to set. + /// - `namespace`: Attribute's namespace. + /// - `key`: The key of the attribute. + /// - `value`: The value to which to set the attribute. + /// + /// Emits `AttributeSet`. + /// + /// Weight: `O(1)` + #[pallet::weight(T::WeightInfo::force_set_attribute())] + pub fn force_set_attribute( + origin: OriginFor, + set_as: Option, + collection: T::CollectionId, + maybe_item: Option, + namespace: AttributeNamespace, + key: BoundedVec, + value: BoundedVec, + ) -> DispatchResult { + T::ForceOrigin::ensure_origin(origin)?; + Self::do_force_set_attribute(set_as, collection, maybe_item, namespace, key, value) } /// Clear an attribute for a collection or item. @@ -1246,6 +1311,7 @@ pub mod pallet { /// /// - `collection`: The identifier of the collection whose item's metadata to clear. /// - `maybe_item`: The identifier of the item whose metadata to clear. + /// - `namespace`: Attribute's namespace. /// - `key`: The key of the attribute. /// /// Emits `AttributeCleared`. @@ -1256,12 +1322,57 @@ pub mod pallet { origin: OriginFor, collection: T::CollectionId, maybe_item: Option, + namespace: AttributeNamespace, key: BoundedVec, ) -> DispatchResult { let maybe_check_owner = T::ForceOrigin::try_origin(origin) .map(|_| None) .or_else(|origin| ensure_signed(origin).map(Some).map_err(DispatchError::from))?; - Self::do_clear_attribute(maybe_check_owner, collection, maybe_item, key) + Self::do_clear_attribute(maybe_check_owner, collection, maybe_item, namespace, key) + } + + /// Approve item's attributes to be changed by a delegated third-party account. + /// + /// Origin must be Signed and must be an owner of the `item`. + /// + /// - `collection`: A collection of the item. + /// - `item`: The item that holds attributes. + /// - `delegate`: The account to delegate permission to change attributes of the item. + /// + /// Emits `ItemAttributesApprovalAdded` on success. + #[pallet::weight(T::WeightInfo::approve_item_attributes())] + pub fn approve_item_attributes( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + delegate: AccountIdLookupOf, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let delegate = T::Lookup::lookup(delegate)?; + Self::do_approve_item_attributes(origin, collection, item, delegate) + } + + /// Cancel the previously provided approval to change item's attributes. + /// All the previously set attributes by the `delegate` will be removed. + /// + /// Origin must be Signed and must be an owner of the `item`. + /// + /// - `collection`: Collection that the item is contained within. + /// - `item`: The item that holds attributes. + /// - `delegate`: The previously approved account to remove. + /// + /// Emits `ItemAttributesApprovalRemoved` on success. + #[pallet::weight(T::WeightInfo::cancel_item_attributes_approval())] + pub fn cancel_item_attributes_approval( + origin: OriginFor, + collection: T::CollectionId, + item: T::ItemId, + delegate: AccountIdLookupOf, + witness: CancelAttributesApprovalWitness, + ) -> DispatchResult { + let origin = ensure_signed(origin)?; + let delegate = T::Lookup::lookup(delegate)?; + Self::do_cancel_item_attributes_approval(origin, collection, item, delegate, witness) } /// Set the metadata for an item. diff --git a/frame/nfts/src/mock.rs b/frame/nfts/src/mock.rs index bbd1625710500..f814b209d5f78 100644 --- a/frame/nfts/src/mock.rs +++ b/frame/nfts/src/mock.rs @@ -105,6 +105,7 @@ impl Config for Test { type KeyLimit = ConstU32<50>; type ValueLimit = ConstU32<50>; type ApprovalsLimit = ConstU32<10>; + type ItemAttributesApprovalsLimit = ConstU32<2>; type MaxTips = ConstU32<10>; type MaxDeadlineDuration = ConstU64<10000>; type Features = Features; diff --git a/frame/nfts/src/tests.rs b/frame/nfts/src/tests.rs index b58c81b1d70f8..1e057a8b58d6d 100644 --- a/frame/nfts/src/tests.rs +++ b/frame/nfts/src/tests.rs @@ -25,6 +25,7 @@ use frame_support::{ traits::{tokens::nonfungibles_v2::Destroy, Currency, Get}, }; use pallet_balances::Error as BalancesError; +use sp_core::bounded::BoundedVec; use sp_std::prelude::*; fn items() -> Vec<(u64, u32, u32)> { @@ -67,11 +68,12 @@ macro_rules! bvec { } } -fn attributes(collection: u32) -> Vec<(Option, Vec, Vec)> { +fn attributes(collection: u32) -> Vec<(Option, AttributeNamespace, Vec, Vec)> { let mut s: Vec<_> = Attribute::::iter_prefix((collection,)) - .map(|(k, v)| (k.0, k.1.into(), v.0.into())) + .map(|(k, v)| (k.0, k.1, k.2.into(), v.0.into())) .collect(); - s.sort(); + s.sort_by_key(|k: &(Option, AttributeNamespace, Vec, Vec)| k.0); + s.sort_by_key(|k: &(Option, AttributeNamespace, Vec, Vec)| k.2.clone()); s } @@ -81,6 +83,12 @@ fn approvals(collection_id: u32, item_id: u32) -> Vec<(u64, Option)> { s } +fn item_attributes_approvals(collection_id: u32, item_id: u32) -> Vec { + let approvals = ItemAttributesApprovalsOf::::get(collection_id, item_id); + let s: Vec<_> = approvals.into_iter().collect(); + s +} + fn events() -> Vec> { let result = System::events() .into_iter() @@ -583,7 +591,7 @@ fn set_item_metadata_should_work() { } #[test] -fn set_attribute_should_work() { +fn set_collection_owner_attributes_should_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); @@ -594,34 +602,73 @@ fn set_attribute_should_work() { )); assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, None)); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0])); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![0])); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![1], bvec![0])); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![1], + bvec![0], + )); assert_eq!( attributes(0), vec![ - (None, bvec![0], bvec![0]), - (Some(0), bvec![0], bvec![0]), - (Some(0), bvec![1], bvec![0]), + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![1], bvec![0]), ] ); assert_eq!(Balances::reserved_balance(1), 10); + assert_eq!(Collection::::get(0).unwrap().owner_deposit, 9); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0; 10])); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0; 10], + )); assert_eq!( attributes(0), vec![ - (None, bvec![0], bvec![0; 10]), - (Some(0), bvec![0], bvec![0]), - (Some(0), bvec![1], bvec![0]), + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![1], bvec![0]), ] ); assert_eq!(Balances::reserved_balance(1), 19); + assert_eq!(Collection::::get(0).unwrap().owner_deposit, 18); - assert_ok!(Nfts::clear_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![1])); + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![1], + )); assert_eq!( attributes(0), - vec![(None, bvec![0], bvec![0; 10]), (Some(0), bvec![0], bvec![0]),] + vec![ + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + ] ); assert_eq!(Balances::reserved_balance(1), 16); @@ -633,27 +680,301 @@ fn set_attribute_should_work() { } #[test] -fn set_attribute_should_respect_lock() { +fn set_item_owner_attributes_should_work() { new_test_ext().execute_with(|| { Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); + Balances::make_free_balance_be(&3, 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + collection_config_with_all_settings_enabled() + )); + assert_ok!(Nfts::force_mint(RuntimeOrigin::signed(1), 0, 0, 2, default_item_config())); + + // can't set for the collection + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + None, + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + // can't set for the non-owned item + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![1], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![2], + bvec![0], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![1], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(2), 9); + + // validate an attribute can be updated + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0; 10], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::ItemOwner, bvec![1], bvec![0]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(2), 18); + + // validate only item's owner (or the root) can remove an attribute + assert_noop!( + Nfts::clear_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![1], + ), + Error::::NoPermission, + ); + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![1], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]) + ] + ); + assert_eq!(Balances::reserved_balance(2), 15); + + // transfer item + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(2), 0, 0, 3)); + + // validate the attribute are still here & the deposit belongs to the previous owner + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 10]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]) + ] + ); + let key: BoundedVec<_, _> = bvec![0]; + let (_, deposit) = + Attribute::::get((0, Some(0), AttributeNamespace::ItemOwner, &key)).unwrap(); + assert_eq!(deposit.account, Some(2)); + assert_eq!(deposit.amount, 12); + + // on attribute update the deposit should be returned to the previous owner + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(3), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + bvec![0; 11], + )); + let (_, deposit) = + Attribute::::get((0, Some(0), AttributeNamespace::ItemOwner, &key)).unwrap(); + assert_eq!(deposit.account, Some(3)); + assert_eq!(deposit.amount, 13); + assert_eq!(Balances::reserved_balance(2), 3); + assert_eq!(Balances::reserved_balance(3), 13); + + // validate attributes on item deletion + assert_ok!(Nfts::burn(RuntimeOrigin::signed(3), 0, 0, None)); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::ItemOwner, bvec![0], bvec![0; 11]), + (Some(0), AttributeNamespace::ItemOwner, bvec![2], bvec![0]) + ] + ); + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(3), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![0], + )); + assert_ok!(Nfts::clear_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::ItemOwner, + bvec![2], + )); + assert_eq!(Balances::reserved_balance(2), 0); + assert_eq!(Balances::reserved_balance(3), 0); + }); +} + +#[test] +fn set_external_account_attributes_should_work() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + Balances::make_free_balance_be(&2, 100); assert_ok!(Nfts::force_create( RuntimeOrigin::root(), 1, collection_config_with_all_settings_enabled() )); + assert_ok!(Nfts::force_mint(RuntimeOrigin::signed(1), 0, 0, 1, default_item_config())); + assert_ok!(Nfts::approve_item_attributes(RuntimeOrigin::signed(1), 0, 0, 2)); + + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::Account(1), + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::Account(2), + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::Account(2), + bvec![1], + bvec![0], + )); + assert_eq!( + attributes(0), + vec![ + (Some(0), AttributeNamespace::Account(2), bvec![0], bvec![0]), + (Some(0), AttributeNamespace::Account(2), bvec![1], bvec![0]), + ] + ); + assert_eq!(Balances::reserved_balance(2), 6); + + // remove permission to set attributes + assert_ok!(Nfts::cancel_item_attributes_approval( + RuntimeOrigin::signed(1), + 0, + 0, + 2, + CancelAttributesApprovalWitness { account_attributes: 2 }, + )); + assert_eq!(attributes(0), vec![]); + assert_eq!(Balances::reserved_balance(2), 0); + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(2), + 0, + Some(0), + AttributeNamespace::Account(2), + bvec![0], + bvec![0], + ), + Error::::NoPermission, + ); + }); +} + +#[test] +fn set_attribute_should_respect_lock() { + new_test_ext().execute_with(|| { + Balances::make_free_balance_be(&1, 100); + + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + 1, + collection_config_with_all_settings_enabled(), + )); assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 0, None)); assert_ok!(Nfts::mint(RuntimeOrigin::signed(1), 0, 1, None)); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0])); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![0])); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(1), bvec![0], bvec![0])); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(1), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + )); assert_eq!( attributes(0), vec![ - (None, bvec![0], bvec![0]), - (Some(0), bvec![0], bvec![0]), - (Some(1), bvec![0], bvec![0]), + (None, AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(0), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), + (Some(1), AttributeNamespace::CollectionOwner, bvec![0], bvec![0]), ] ); assert_eq!(Balances::reserved_balance(1), 11); @@ -666,16 +987,47 @@ fn set_attribute_should_respect_lock() { )); let e = Error::::LockedCollectionAttributes; - assert_noop!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, None, bvec![0], bvec![0]), e); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![1])); + assert_noop!( + Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + None, + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![0], + ), + e + ); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![1], + )); assert_ok!(Nfts::lock_item_properties(RuntimeOrigin::signed(1), 0, 0, false, true)); let e = Error::::LockedItemAttributes; assert_noop!( - Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(0), bvec![0], bvec![1]), + Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(0), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![1], + ), e ); - assert_ok!(Nfts::set_attribute(RuntimeOrigin::signed(1), 0, Some(1), bvec![0], bvec![1])); + assert_ok!(Nfts::set_attribute( + RuntimeOrigin::signed(1), + 0, + Some(1), + AttributeNamespace::CollectionOwner, + bvec![0], + bvec![1], + )); }); } @@ -1905,8 +2257,9 @@ fn pallet_level_feature_flags_should_work() { RuntimeOrigin::signed(user_id), collection_id, None, + AttributeNamespace::CollectionOwner, + bvec![0], bvec![0], - bvec![0] ), Error::::MethodDisabled ); @@ -1942,3 +2295,58 @@ fn group_roles_by_account_should_work() { assert_eq!(account_to_role, expect); }) } + +#[test] +fn add_remove_item_attributes_approval_should_work() { + new_test_ext().execute_with(|| { + let user_1 = 1; + let user_2 = 2; + let user_3 = 3; + let user_4 = 4; + let collection_id = 0; + let item_id = 0; + + assert_ok!(Nfts::force_create(RuntimeOrigin::root(), user_1, default_collection_config())); + assert_ok!(Nfts::mint(RuntimeOrigin::signed(user_1), collection_id, item_id, None)); + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1), + collection_id, + item_id, + user_2, + )); + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![user_2]); + + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1), + collection_id, + item_id, + user_3, + )); + assert_ok!(Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1), + collection_id, + item_id, + user_2, + )); + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![user_2, user_3]); + + assert_noop!( + Nfts::approve_item_attributes( + RuntimeOrigin::signed(user_1), + collection_id, + item_id, + user_4, + ), + Error::::ReachedApprovalLimit + ); + + assert_ok!(Nfts::cancel_item_attributes_approval( + RuntimeOrigin::signed(user_1), + collection_id, + item_id, + user_2, + CancelAttributesApprovalWitness { account_attributes: 1 }, + )); + assert_eq!(item_attributes_approvals(collection_id, item_id), vec![user_3]); + }) +} diff --git a/frame/nfts/src/types.rs b/frame/nfts/src/types.rs index d57f62be97f39..c12ae39877d46 100644 --- a/frame/nfts/src/types.rs +++ b/frame/nfts/src/types.rs @@ -24,6 +24,7 @@ use enumflags2::{bitflags, BitFlags}; use frame_support::{ pallet_prelude::{BoundedVec, MaxEncodedLen}, traits::Get, + BoundedBTreeMap, BoundedBTreeSet, }; use scale_info::{build::Fields, meta_type, Path, Type, TypeInfo, TypeParameter}; @@ -36,8 +37,12 @@ pub(super) type ApprovalsOf = BoundedBTreeMap< Option<::BlockNumber>, >::ApprovalsLimit, >; +pub(super) type ItemAttributesApprovals = + BoundedBTreeSet<::AccountId, >::ItemAttributesApprovalsLimit>; pub(super) type ItemDepositOf = ItemDeposit, ::AccountId>; +pub(super) type AttributeDepositOf = + AttributeDeposit, ::AccountId>; pub(super) type ItemDetailsFor = ItemDetails<::AccountId, ItemDepositOf, ApprovalsOf>; pub(super) type BalanceOf = @@ -65,9 +70,9 @@ impl_incrementable!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); pub struct CollectionDetails { /// Collection's owner. pub(super) owner: AccountId, - /// The total balance deposited for the all storage associated with this collection. - /// Used by `destroy`. - pub(super) total_deposit: DepositBalance, + /// The total balance deposited by the owner for the all storage data associated with this + /// collection. Used by `destroy`. + pub(super) owner_deposit: DepositBalance, /// The total number of outstanding items of this collection. pub(super) items: u32, /// The total number of outstanding item metadata of this collection. @@ -100,6 +105,13 @@ impl CollectionDetails { } } +/// Witness data for items mint transactions. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)] +pub struct MintWitness { + /// Provide the id of the item in a required collection. + pub owner_of_item: ItemId, +} + /// Information concerning the ownership of a single unique item. #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, TypeInfo, MaxEncodedLen)] pub struct ItemDetails { @@ -173,6 +185,15 @@ pub struct PendingSwap { pub(super) deadline: Deadline, } +/// Information about the reserved attribute deposit. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] +pub struct AttributeDeposit { + /// A depositor account. + pub(super) account: Option, + /// An amount that gets reserved. + pub(super) amount: DepositBalance, +} + #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub enum PriceDirection { Send, @@ -265,9 +286,9 @@ impl Default for MintSettings { - /// Provide the id of the item in a required collection. - pub owner_of_item: ItemId, +pub struct CancelAttributesApprovalWitness { + /// An amount of attributes previously created by account. + pub account_attributes: u32, } #[derive( diff --git a/frame/nfts/src/weights.rs b/frame/nfts/src/weights.rs index f254726ca19f2..a7eb3773f2ae8 100644 --- a/frame/nfts/src/weights.rs +++ b/frame/nfts/src/weights.rs @@ -63,7 +63,10 @@ pub trait WeightInfo { fn force_collection_config() -> Weight; fn lock_item_properties() -> Weight; fn set_attribute() -> Weight; + fn force_set_attribute() -> Weight; fn clear_attribute() -> Weight; + fn approve_item_attributes() -> Weight; + fn cancel_item_attributes_approval() -> Weight; fn set_metadata() -> Weight; fn clear_metadata() -> Weight; fn set_collection_metadata() -> Weight; @@ -258,12 +261,39 @@ impl WeightInfo for SubstrateWeight { // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Attribute (r:1 w:1) + fn force_set_attribute() -> Weight { + Weight::from_ref_time(53_019_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts Attribute (r:1 w:1) fn clear_attribute() -> Weight { Weight::from_ref_time(52_530_000 as u64) .saturating_add(T::DbWeight::get().reads(4 as u64)) .saturating_add(T::DbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts Attribute (r:1 w:1) + fn approve_item_attributes() -> Weight { + Weight::from_ref_time(52_530_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts Attribute (r:1 w:1) + fn cancel_item_attributes_approval() -> Weight { + Weight::from_ref_time(52_530_000 as u64) + .saturating_add(T::DbWeight::get().reads(4 as u64)) + .saturating_add(T::DbWeight::get().writes(2 as u64)) + } + // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts InstanceMetadataOf (r:1 w:1) @@ -566,12 +596,39 @@ impl WeightInfo for () { // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts Attribute (r:1 w:1) + fn force_set_attribute() -> Weight { + Weight::from_ref_time(53_019_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts Attribute (r:1 w:1) fn clear_attribute() -> Weight { Weight::from_ref_time(52_530_000 as u64) .saturating_add(RocksDbWeight::get().reads(4 as u64)) .saturating_add(RocksDbWeight::get().writes(2 as u64)) } // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts Attribute (r:1 w:1) + fn approve_item_attributes() -> Weight { + Weight::from_ref_time(52_530_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Nfts Class (r:1 w:1) + // Storage: Nfts CollectionConfigOf (r:1 w:0) + // Storage: Nfts ItemConfigOf (r:1 w:0) + // Storage: Nfts Attribute (r:1 w:1) + fn cancel_item_attributes_approval() -> Weight { + Weight::from_ref_time(52_530_000 as u64) + .saturating_add(RocksDbWeight::get().reads(4 as u64)) + .saturating_add(RocksDbWeight::get().writes(2 as u64)) + } + // Storage: Nfts Class (r:1 w:1) // Storage: Nfts ItemConfigOf (r:1 w:0) // Storage: Nfts CollectionConfigOf (r:1 w:0) // Storage: Nfts InstanceMetadataOf (r:1 w:1) diff --git a/frame/support/src/lib.rs b/frame/support/src/lib.rs index 84e416e50544d..a5d7c36de2c6c 100644 --- a/frame/support/src/lib.rs +++ b/frame/support/src/lib.rs @@ -115,7 +115,7 @@ pub use sp_runtime::{ self, print, traits::Printable, ConsensusEngineId, MAX_MODULE_ERROR_ENCODED_SIZE, }; -use codec::{Decode, Encode}; +use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::TypeId; @@ -127,7 +127,7 @@ pub const LOG_TARGET: &str = "runtime::frame-support"; pub enum Never {} /// A pallet identifier. These are per pallet and should be stored in a registry somewhere. -#[derive(Clone, Copy, Eq, PartialEq, Encode, Decode, TypeInfo)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, MaxEncodedLen)] pub struct PalletId(pub [u8; 8]); impl TypeId for PalletId { diff --git a/frame/support/src/traits/tokens.rs b/frame/support/src/traits/tokens.rs index b3b3b4b7d90b1..03a24bd3ba9c8 100644 --- a/frame/support/src/traits/tokens.rs +++ b/frame/support/src/traits/tokens.rs @@ -28,6 +28,6 @@ pub mod nonfungibles; pub mod nonfungibles_v2; pub use imbalance::Imbalance; pub use misc::{ - AssetId, Balance, BalanceConversion, BalanceStatus, DepositConsequence, ExistenceRequirement, - Locker, WithdrawConsequence, WithdrawReasons, + AssetId, AttributeNamespace, Balance, BalanceConversion, BalanceStatus, DepositConsequence, + ExistenceRequirement, Locker, WithdrawConsequence, WithdrawReasons, }; diff --git a/frame/support/src/traits/tokens/misc.rs b/frame/support/src/traits/tokens/misc.rs index 294d0e89c8b9e..f0b172841aa84 100644 --- a/frame/support/src/traits/tokens/misc.rs +++ b/frame/support/src/traits/tokens/misc.rs @@ -17,6 +17,7 @@ //! Miscellaneous types. +use crate::PalletId; use codec::{Decode, Encode, FullCodec, MaxEncodedLen}; use sp_arithmetic::traits::{AtLeast32BitUnsigned, Zero}; use sp_core::RuntimeDebug; @@ -126,6 +127,21 @@ pub enum BalanceStatus { Reserved, } +/// Attribute namespaces for non-fungible tokens. +#[derive( + Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, +)] +pub enum AttributeNamespace { + /// An attribute was set by the pallet. + Pallet(PalletId), + /// An attribute was set by collection's owner. + CollectionOwner, + /// An attribute was set by item's owner. + ItemOwner, + /// An attribute was set by pre-approved account. + Account(AccountId), +} + bitflags::bitflags! { /// Reasons for moving funds out of an account. #[derive(Encode, Decode, MaxEncodedLen)] diff --git a/frame/support/src/traits/tokens/nonfungible_v2.rs b/frame/support/src/traits/tokens/nonfungible_v2.rs index 4f610d9b80a05..cd091791821ed 100644 --- a/frame/support/src/traits/tokens/nonfungible_v2.rs +++ b/frame/support/src/traits/tokens/nonfungible_v2.rs @@ -25,7 +25,10 @@ //! use. use super::nonfungibles_v2 as nonfungibles; -use crate::{dispatch::DispatchResult, traits::Get}; +use crate::{ + dispatch::DispatchResult, + traits::{tokens::misc::AttributeNamespace, Get}, +}; use codec::{Decode, Encode}; use sp_runtime::TokenError; use sp_std::prelude::*; @@ -42,15 +45,23 @@ pub trait Inspect { /// Returns the attribute value of `item` corresponding to `key`. /// /// By default this is `None`; no attributes are defined. - fn attribute(_item: &Self::ItemId, _key: &[u8]) -> Option> { + fn attribute( + _item: &Self::ItemId, + _namespace: &AttributeNamespace, + _key: &[u8], + ) -> Option> { None } /// Returns the strongly-typed attribute value of `item` corresponding to `key`. /// /// By default this just attempts to use `attribute`. - fn typed_attribute(item: &Self::ItemId, key: &K) -> Option { - key.using_encoded(|d| Self::attribute(item, d)) + fn typed_attribute( + item: &Self::ItemId, + namespace: &AttributeNamespace, + key: &K, + ) -> Option { + key.using_encoded(|d| Self::attribute(item, namespace, d)) .and_then(|v| V::decode(&mut &v[..]).ok()) } @@ -137,11 +148,19 @@ impl< fn owner(item: &Self::ItemId) -> Option { >::owner(&A::get(), item) } - fn attribute(item: &Self::ItemId, key: &[u8]) -> Option> { - >::attribute(&A::get(), item, key) + fn attribute( + item: &Self::ItemId, + namespace: &AttributeNamespace, + key: &[u8], + ) -> Option> { + >::attribute(&A::get(), item, namespace, key) } - fn typed_attribute(item: &Self::ItemId, key: &K) -> Option { - >::typed_attribute(&A::get(), item, key) + fn typed_attribute( + item: &Self::ItemId, + namespace: &AttributeNamespace, + key: &K, + ) -> Option { + >::typed_attribute(&A::get(), item, namespace, key) } fn can_transfer(item: &Self::ItemId) -> bool { >::can_transfer(&A::get(), item) diff --git a/frame/support/src/traits/tokens/nonfungibles_v2.rs b/frame/support/src/traits/tokens/nonfungibles_v2.rs index 0aec193f68fcb..5b93ca832d4f1 100644 --- a/frame/support/src/traits/tokens/nonfungibles_v2.rs +++ b/frame/support/src/traits/tokens/nonfungibles_v2.rs @@ -27,7 +27,10 @@ //! Implementations of these traits may be converted to implementations of corresponding //! `nonfungible` traits by using the `nonfungible::ItemOf` type adapter. -use crate::dispatch::{DispatchError, DispatchResult}; +use crate::{ + dispatch::{DispatchError, DispatchResult}, + traits::tokens::misc::AttributeNamespace, +}; use codec::{Decode, Encode}; use sp_runtime::TokenError; use sp_std::prelude::*; @@ -58,6 +61,7 @@ pub trait Inspect { fn attribute( _collection: &Self::CollectionId, _item: &Self::ItemId, + _namespace: &AttributeNamespace, _key: &[u8], ) -> Option> { None @@ -70,9 +74,10 @@ pub trait Inspect { fn typed_attribute( collection: &Self::CollectionId, item: &Self::ItemId, + namespace: &AttributeNamespace, key: &K, ) -> Option { - key.using_encoded(|d| Self::attribute(collection, item, d)) + key.using_encoded(|d| Self::attribute(collection, item, namespace, d)) .and_then(|v| V::decode(&mut &v[..]).ok()) } diff --git a/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr b/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr index a5ec31a9bb4e7..bb49e11679028 100644 --- a/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr +++ b/frame/support/test/tests/pallet_ui/dev_mode_without_arg_max_encoded_len.stderr @@ -13,5 +13,5 @@ error[E0277]: the trait bound `Vec: MaxEncodedLen` is not satisfied (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5) (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6) (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6, TupleElement7) - and 78 others + and 80 others = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageMyStorage, Vec>` to implement `StorageInfoTrait` diff --git a/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr b/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr index 42ef5a34e4c30..999d8585c221a 100644 --- a/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr +++ b/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen.stderr @@ -28,7 +28,7 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied <&[(T,)] as EncodeLike>> <&[(T,)] as EncodeLike>> <&[T] as EncodeLike>> - and 278 others + and 279 others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo, Bar>` to implement `PartialStorageInfoTrait` @@ -69,7 +69,7 @@ error[E0277]: the trait bound `Bar: TypeInfo` is not satisfied (A, B, C, D) (A, B, C, D, E) (A, B, C, D, E, F) - and 161 others + and 162 others = note: required for `Bar` to implement `StaticTypeInfo` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo, Bar>` to implement `StorageEntryMetadataBuilder` @@ -103,7 +103,7 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied <&[(T,)] as EncodeLike>> <&[(T,)] as EncodeLike>> <&[T] as EncodeLike>> - and 278 others + and 279 others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo, Bar>` to implement `StorageEntryMetadataBuilder` diff --git a/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr b/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr index 461d63ebb0d9c..e2870ffb9e86f 100644 --- a/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr +++ b/frame/support/test/tests/pallet_ui/storage_ensure_span_are_ok_on_wrong_gen_unnamed.stderr @@ -28,7 +28,7 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied <&[(T,)] as EncodeLike>> <&[(T,)] as EncodeLike>> <&[T] as EncodeLike>> - and 278 others + and 279 others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo, Bar>` to implement `PartialStorageInfoTrait` @@ -69,7 +69,7 @@ error[E0277]: the trait bound `Bar: TypeInfo` is not satisfied (A, B, C, D) (A, B, C, D, E) (A, B, C, D, E, F) - and 161 others + and 162 others = note: required for `Bar` to implement `StaticTypeInfo` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo, Bar>` to implement `StorageEntryMetadataBuilder` @@ -103,7 +103,7 @@ error[E0277]: the trait bound `Bar: EncodeLike` is not satisfied <&[(T,)] as EncodeLike>> <&[(T,)] as EncodeLike>> <&[T] as EncodeLike>> - and 278 others + and 279 others = note: required for `Bar` to implement `FullEncode` = note: required for `Bar` to implement `FullCodec` = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo, Bar>` to implement `StorageEntryMetadataBuilder` diff --git a/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr b/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr index cce9fa70b3da5..d5b0c3b50a5ac 100644 --- a/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr +++ b/frame/support/test/tests/pallet_ui/storage_info_unsatisfied.stderr @@ -13,5 +13,5 @@ error[E0277]: the trait bound `Bar: MaxEncodedLen` is not satisfied (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5) (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6) (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6, TupleElement7) - and 78 others + and 80 others = note: required for `frame_support::pallet_prelude::StorageValue<_GeneratedPrefixForStorageFoo, Bar>` to implement `StorageInfoTrait` diff --git a/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr b/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr index 877485dda2084..6b174d13c5778 100644 --- a/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr +++ b/frame/support/test/tests/pallet_ui/storage_info_unsatisfied_nmap.stderr @@ -13,6 +13,6 @@ error[E0277]: the trait bound `Bar: MaxEncodedLen` is not satisfied (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5) (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6) (TupleElement0, TupleElement1, TupleElement2, TupleElement3, TupleElement4, TupleElement5, TupleElement6, TupleElement7) - and 78 others + and 80 others = note: required for `Key` to implement `KeyGeneratorMaxEncodedLen` = note: required for `frame_support::pallet_prelude::StorageNMap<_GeneratedPrefixForStorageFoo, Key, u32>` to implement `StorageInfoTrait`