referrerpolicy=no-referrer-when-downgrade

pallet_nfts/features/
attributes.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! This module contains helper methods to configure attributes for items and collections in the
19//! NFTs pallet.
20//! The bitflag [`PalletFeature::Attributes`] needs to be set in [`Config::Features`] for NFTs
21//! to have the functionality defined in this module.
22
23use crate::*;
24use frame_support::pallet_prelude::*;
25
26impl<T: Config<I>, I: 'static> Pallet<T, I> {
27	/// Sets the attribute of an item or a collection.
28	///
29	/// This function is used to set an attribute for an item or a collection. It checks the
30	/// provided `namespace` and verifies the permission of the caller to perform the action. The
31	/// `collection` and `maybe_item` parameters specify the target for the attribute.
32	///
33	/// - `origin`: The account attempting to set the attribute.
34	/// - `collection`: The identifier of the collection to which the item belongs, or the
35	///   collection itself if setting a collection attribute.
36	/// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if
37	///   setting a collection attribute.
38	/// - `namespace`: The namespace in which the attribute is being set. It can be either
39	///   `CollectionOwner`, `ItemOwner`, or `Account` (pre-approved external address).
40	/// - `key`: The key of the attribute. It should be a vector of bytes within the limits defined
41	///   by `T::KeyLimit`.
42	/// - `value`: The value of the attribute. It should be a vector of bytes within the limits
43	///   defined by `T::ValueLimit`.
44	/// - `depositor`: The account that is paying the deposit for the attribute.
45	///
46	/// Note: For the `CollectionOwner` namespace, the collection/item must have the
47	/// `UnlockedAttributes` setting enabled.
48	/// The deposit for setting an attribute is based on the `T::DepositPerByte` and
49	/// `T::AttributeDepositBase` configuration.
50	pub(crate) fn do_set_attribute(
51		origin: T::AccountId,
52		collection: T::CollectionId,
53		maybe_item: Option<T::ItemId>,
54		namespace: AttributeNamespace<T::AccountId>,
55		key: BoundedVec<u8, T::KeyLimit>,
56		value: BoundedVec<u8, T::ValueLimit>,
57		depositor: T::AccountId,
58	) -> DispatchResult {
59		ensure!(
60			Self::is_pallet_feature_enabled(PalletFeature::Attributes),
61			Error::<T, I>::MethodDisabled
62		);
63
64		ensure!(
65			Self::is_valid_namespace(&origin, &namespace, &collection, &maybe_item)?,
66			Error::<T, I>::NoPermission
67		);
68
69		let collection_config = Self::get_collection_config(&collection)?;
70		// for the `CollectionOwner` namespace we need to check if the collection/item is not locked
71		match namespace {
72			AttributeNamespace::CollectionOwner => match maybe_item {
73				None => {
74					ensure!(
75						collection_config.is_setting_enabled(CollectionSetting::UnlockedAttributes),
76						Error::<T, I>::LockedCollectionAttributes
77					)
78				},
79				Some(item) => {
80					let maybe_is_locked = Self::get_item_config(&collection, &item)
81						.map(|c| c.has_disabled_setting(ItemSetting::UnlockedAttributes))?;
82					ensure!(!maybe_is_locked, Error::<T, I>::LockedItemAttributes);
83				},
84			},
85			_ => (),
86		}
87
88		let mut collection_details =
89			Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
90
91		let attribute = Attribute::<T, I>::get((collection, maybe_item, &namespace, &key));
92		let attribute_exists = attribute.is_some();
93		if !attribute_exists {
94			collection_details.attributes.saturating_inc();
95		}
96
97		let old_deposit =
98			attribute.map_or(AttributeDeposit { account: None, amount: Zero::zero() }, |m| m.1);
99
100		let mut deposit = Zero::zero();
101		// disabled DepositRequired setting only affects the CollectionOwner namespace
102		if collection_config.is_setting_enabled(CollectionSetting::DepositRequired) ||
103			namespace != AttributeNamespace::CollectionOwner
104		{
105			deposit = T::DepositPerByte::get()
106				.saturating_mul(((key.len() + value.len()) as u32).into())
107				.saturating_add(T::AttributeDepositBase::get());
108		}
109
110		let is_collection_owner_namespace = namespace == AttributeNamespace::CollectionOwner;
111		let is_depositor_collection_owner =
112			is_collection_owner_namespace && collection_details.owner == depositor;
113
114		// NOTE: in the CollectionOwner namespace if the depositor is `None` that means the deposit
115		// was paid by the collection's owner.
116		let old_depositor =
117			if is_collection_owner_namespace && old_deposit.account.is_none() && attribute_exists {
118				Some(collection_details.owner.clone())
119			} else {
120				old_deposit.account
121			};
122		let depositor_has_changed = old_depositor != Some(depositor.clone());
123
124		// NOTE: when we transfer an item, we don't move attributes in the ItemOwner namespace.
125		// When the new owner updates the same attribute, we will update the depositor record
126		// and return the deposit to the previous owner.
127		if depositor_has_changed {
128			if let Some(old_depositor) = old_depositor {
129				T::Currency::unreserve(&old_depositor, old_deposit.amount);
130			}
131			T::Currency::reserve(&depositor, deposit)?;
132		} else if deposit > old_deposit.amount {
133			T::Currency::reserve(&depositor, deposit - old_deposit.amount)?;
134		} else if deposit < old_deposit.amount {
135			T::Currency::unreserve(&depositor, old_deposit.amount - deposit);
136		}
137
138		if is_depositor_collection_owner {
139			if !depositor_has_changed {
140				collection_details.owner_deposit.saturating_reduce(old_deposit.amount);
141			}
142			collection_details.owner_deposit.saturating_accrue(deposit);
143		}
144
145		let new_deposit_owner = match is_depositor_collection_owner {
146			true => None,
147			false => Some(depositor),
148		};
149		Attribute::<T, I>::insert(
150			(&collection, maybe_item, &namespace, &key),
151			(&value, AttributeDeposit { account: new_deposit_owner, amount: deposit }),
152		);
153
154		Collection::<T, I>::insert(collection, &collection_details);
155		Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace });
156		Ok(())
157	}
158
159	/// Sets the attribute of an item or a collection without performing deposit checks.
160	///
161	/// This function is used to force-set an attribute for an item or a collection without
162	/// performing the deposit checks. It bypasses the deposit requirement and should only be used
163	/// in specific situations where deposit checks are not necessary or handled separately.
164	///
165	/// - `set_as`: The account that would normally pay for the deposit.
166	/// - `collection`: The identifier of the collection to which the item belongs, or the
167	///   collection itself if setting a collection attribute.
168	/// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if
169	///   setting a collection attribute.
170	/// - `namespace`: The namespace in which the attribute is being set. It can be either
171	///   `CollectionOwner`, `ItemOwner`, or `Account` (pre-approved external address).
172	/// - `key`: The key of the attribute. It should be a vector of bytes within the limits defined
173	///   by `T::KeyLimit`.
174	/// - `value`: The value of the attribute. It should be a vector of bytes within the limits
175	///   defined by `T::ValueLimit`.
176	pub(crate) fn do_force_set_attribute(
177		set_as: Option<T::AccountId>,
178		collection: T::CollectionId,
179		maybe_item: Option<T::ItemId>,
180		namespace: AttributeNamespace<T::AccountId>,
181		key: BoundedVec<u8, T::KeyLimit>,
182		value: BoundedVec<u8, T::ValueLimit>,
183	) -> DispatchResult {
184		let mut collection_details =
185			Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
186
187		let attribute = Attribute::<T, I>::get((collection, maybe_item, &namespace, &key));
188		if let Some((_, deposit)) = attribute {
189			if deposit.account != set_as && deposit.amount != Zero::zero() {
190				if let Some(deposit_account) = deposit.account {
191					T::Currency::unreserve(&deposit_account, deposit.amount);
192				}
193			}
194		} else {
195			collection_details.attributes.saturating_inc();
196		}
197
198		Attribute::<T, I>::insert(
199			(&collection, maybe_item, &namespace, &key),
200			(&value, AttributeDeposit { account: set_as, amount: Zero::zero() }),
201		);
202		Collection::<T, I>::insert(collection, &collection_details);
203		Self::deposit_event(Event::AttributeSet { collection, maybe_item, key, value, namespace });
204		Ok(())
205	}
206
207	/// Sets multiple attributes for an item or a collection.
208	///
209	/// This function checks the pre-signed data is valid and updates the attributes of an item or
210	/// collection. It is limited by [`Config::MaxAttributesPerCall`] to prevent excessive storage
211	/// consumption in a single transaction.
212	///
213	/// - `origin`: The account initiating the transaction.
214	/// - `data`: The data containing the details of the pre-signed attributes to be set.
215	/// - `signer`: The account of the pre-signed attributes signer.
216	pub(crate) fn do_set_attributes_pre_signed(
217		origin: T::AccountId,
218		data: PreSignedAttributesOf<T, I>,
219		signer: T::AccountId,
220	) -> DispatchResult {
221		let PreSignedAttributes { collection, item, attributes, namespace, deadline } = data;
222
223		ensure!(
224			attributes.len() <= T::MaxAttributesPerCall::get() as usize,
225			Error::<T, I>::MaxAttributesLimitReached
226		);
227
228		let now = T::BlockNumberProvider::current_block_number();
229		ensure!(deadline >= now, Error::<T, I>::DeadlineExpired);
230
231		let item_details =
232			Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
233		ensure!(item_details.owner == origin, Error::<T, I>::NoPermission);
234
235		// Only the CollectionOwner and Account() namespaces could be updated in this way.
236		// For the Account() namespace we check and set the approval if it wasn't set before.
237		match &namespace {
238			AttributeNamespace::CollectionOwner => {},
239			AttributeNamespace::Account(account) => {
240				ensure!(account == &signer, Error::<T, I>::NoPermission);
241				let approvals = ItemAttributesApprovalsOf::<T, I>::get(&collection, &item);
242				if !approvals.contains(account) {
243					Self::do_approve_item_attributes(
244						origin.clone(),
245						collection,
246						item,
247						account.clone(),
248					)?;
249				}
250			},
251			_ => return Err(Error::<T, I>::WrongNamespace.into()),
252		}
253
254		for (key, value) in attributes {
255			Self::do_set_attribute(
256				signer.clone(),
257				collection,
258				Some(item),
259				namespace.clone(),
260				Self::construct_attribute_key(key)?,
261				Self::construct_attribute_value(value)?,
262				origin.clone(),
263			)?;
264		}
265		Self::deposit_event(Event::PreSignedAttributesSet { collection, item, namespace });
266		Ok(())
267	}
268
269	/// Clears an attribute of an item or a collection.
270	///
271	/// This function allows clearing an attribute from an item or a collection. It verifies the
272	/// permission of the caller to perform the action based on the provided `namespace` and
273	/// `depositor` account. The deposit associated with the attribute, if any, will be unreserved.
274	///
275	/// - `maybe_check_origin`: An optional account that acts as an additional security check when
276	/// clearing the attribute. This can be `None` if no additional check is required.
277	/// - `collection`: The identifier of the collection to which the item belongs, or the
278	///   collection itself if clearing a collection attribute.
279	/// - `maybe_item`: The identifier of the item to which the attribute belongs, or `None` if
280	///   clearing a collection attribute.
281	/// - `namespace`: The namespace in which the attribute is being cleared. It can be either
282	///   `CollectionOwner`, `ItemOwner`, or `Account`.
283	/// - `key`: The key of the attribute to be cleared. It should be a vector of bytes within the
284	///   limits defined by `T::KeyLimit`.
285	pub(crate) fn do_clear_attribute(
286		maybe_check_origin: Option<T::AccountId>,
287		collection: T::CollectionId,
288		maybe_item: Option<T::ItemId>,
289		namespace: AttributeNamespace<T::AccountId>,
290		key: BoundedVec<u8, T::KeyLimit>,
291	) -> DispatchResult {
292		let (_, deposit) = Attribute::<T, I>::take((collection, maybe_item, &namespace, &key))
293			.ok_or(Error::<T, I>::AttributeNotFound)?;
294
295		if let Some(check_origin) = &maybe_check_origin {
296			// validate the provided namespace when it's not a root call and the caller is not
297			// the same as the `deposit.account` (e.g. the deposit was paid by different account)
298			if deposit.account != maybe_check_origin {
299				ensure!(
300					Self::is_valid_namespace(&check_origin, &namespace, &collection, &maybe_item)?,
301					Error::<T, I>::NoPermission
302				);
303			}
304
305			// can't clear `CollectionOwner` type attributes if the collection/item is locked
306			match namespace {
307				AttributeNamespace::CollectionOwner => match maybe_item {
308					None => {
309						let collection_config = Self::get_collection_config(&collection)?;
310						ensure!(
311							collection_config
312								.is_setting_enabled(CollectionSetting::UnlockedAttributes),
313							Error::<T, I>::LockedCollectionAttributes
314						)
315					},
316					Some(item) => {
317						// NOTE: if the item was previously burned, the ItemConfigOf record
318						// might not exist. In that case, we allow to clear the attribute.
319						let maybe_is_locked = Self::get_item_config(&collection, &item)
320							.map_or(None, |c| {
321								Some(c.has_disabled_setting(ItemSetting::UnlockedAttributes))
322							});
323						if let Some(is_locked) = maybe_is_locked {
324							ensure!(!is_locked, Error::<T, I>::LockedItemAttributes);
325							// Only the collection's admin can clear attributes in that namespace.
326							// e.g. in off-chain mints, the attribute's depositor will be the item's
327							// owner, that's why we need to do this extra check.
328							ensure!(
329								Self::has_role(&collection, &check_origin, CollectionRole::Admin),
330								Error::<T, I>::NoPermission
331							);
332						}
333					},
334				},
335				_ => (),
336			};
337		}
338
339		let mut collection_details =
340			Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
341
342		collection_details.attributes.saturating_dec();
343
344		match deposit.account {
345			Some(deposit_account) => {
346				T::Currency::unreserve(&deposit_account, deposit.amount);
347			},
348			None if namespace == AttributeNamespace::CollectionOwner => {
349				collection_details.owner_deposit.saturating_reduce(deposit.amount);
350				T::Currency::unreserve(&collection_details.owner, deposit.amount);
351			},
352			_ => (),
353		}
354
355		Collection::<T, I>::insert(collection, &collection_details);
356		Self::deposit_event(Event::AttributeCleared { collection, maybe_item, key, namespace });
357
358		Ok(())
359	}
360
361	/// Approves a delegate to set attributes on behalf of the item's owner.
362	///
363	/// This function allows the owner of an item to approve a delegate to set attributes in the
364	/// `Account(delegate)` namespace. The maximum number of approvals is determined by
365	/// the configuration `T::MaxAttributesApprovals`.
366	///
367	/// - `check_origin`: The account of the item's owner attempting to approve the delegate.
368	/// - `collection`: The identifier of the collection to which the item belongs.
369	/// - `item`: The identifier of the item for which the delegate is being approved.
370	/// - `delegate`: The account that is being approved to set attributes on behalf of the item's
371	///   owner.
372	pub(crate) fn do_approve_item_attributes(
373		check_origin: T::AccountId,
374		collection: T::CollectionId,
375		item: T::ItemId,
376		delegate: T::AccountId,
377	) -> DispatchResult {
378		ensure!(
379			Self::is_pallet_feature_enabled(PalletFeature::Attributes),
380			Error::<T, I>::MethodDisabled
381		);
382
383		let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
384		ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
385
386		ItemAttributesApprovalsOf::<T, I>::try_mutate(collection, item, |approvals| {
387			approvals
388				.try_insert(delegate.clone())
389				.map_err(|_| Error::<T, I>::ReachedApprovalLimit)?;
390
391			Self::deposit_event(Event::ItemAttributesApprovalAdded { collection, item, delegate });
392			Ok(())
393		})
394	}
395
396	/// Cancels the approval of an item's attributes by a delegate.
397	///
398	/// This function allows the owner of an item to cancel the approval of a delegate to set
399	/// attributes in the `Account(delegate)` namespace. The delegate's approval is removed, in
400	/// addition to attributes the `delegate` previously created, and any unreserved deposit
401	/// is returned. The number of attributes that the delegate has set for the item must
402	/// not exceed the `account_attributes` provided in the `witness`.
403	/// This function is used to prevent unintended or malicious cancellations.
404	///
405	/// - `check_origin`: The account of the item's owner attempting to cancel the delegate's
406	///   approval.
407	/// - `collection`: The identifier of the collection to which the item belongs.
408	/// - `item`: The identifier of the item for which the delegate's approval is being canceled.
409	/// - `delegate`: The account whose approval is being canceled.
410	/// - `witness`: The witness containing the number of attributes set by the delegate for the
411	///   item.
412	pub(crate) fn do_cancel_item_attributes_approval(
413		check_origin: T::AccountId,
414		collection: T::CollectionId,
415		item: T::ItemId,
416		delegate: T::AccountId,
417		witness: CancelAttributesApprovalWitness,
418	) -> DispatchResult {
419		ensure!(
420			Self::is_pallet_feature_enabled(PalletFeature::Attributes),
421			Error::<T, I>::MethodDisabled
422		);
423
424		let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
425		ensure!(check_origin == details.owner, Error::<T, I>::NoPermission);
426
427		ItemAttributesApprovalsOf::<T, I>::try_mutate(collection, item, |approvals| {
428			approvals.remove(&delegate);
429
430			let mut attributes: u32 = 0;
431			let mut deposited: DepositBalanceOf<T, I> = Zero::zero();
432			for (_, (_, deposit)) in Attribute::<T, I>::drain_prefix((
433				&collection,
434				Some(item),
435				AttributeNamespace::Account(delegate.clone()),
436			)) {
437				attributes.saturating_inc();
438				deposited = deposited.saturating_add(deposit.amount);
439			}
440			ensure!(attributes <= witness.account_attributes, Error::<T, I>::BadWitness);
441
442			if !deposited.is_zero() {
443				T::Currency::unreserve(&delegate, deposited);
444			}
445
446			Self::deposit_event(Event::ItemAttributesApprovalRemoved {
447				collection,
448				item,
449				delegate,
450			});
451			Ok(())
452		})
453	}
454
455	/// A helper method to check whether an attribute namespace is valid.
456	fn is_valid_namespace(
457		origin: &T::AccountId,
458		namespace: &AttributeNamespace<T::AccountId>,
459		collection: &T::CollectionId,
460		maybe_item: &Option<T::ItemId>,
461	) -> Result<bool, DispatchError> {
462		let mut result = false;
463		match namespace {
464			AttributeNamespace::CollectionOwner =>
465				result = Self::has_role(&collection, &origin, CollectionRole::Admin),
466			AttributeNamespace::ItemOwner =>
467				if let Some(item) = maybe_item {
468					let item_details =
469						Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
470					result = origin == &item_details.owner
471				},
472			AttributeNamespace::Account(account_id) =>
473				if let Some(item) = maybe_item {
474					let approvals = ItemAttributesApprovalsOf::<T, I>::get(&collection, &item);
475					result = account_id == origin && approvals.contains(&origin)
476				},
477			_ => (),
478		};
479		Ok(result)
480	}
481
482	/// A helper method to construct an attribute's key.
483	///
484	/// # Errors
485	///
486	/// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the
487	/// provided attribute `key` is too long.
488	pub fn construct_attribute_key(
489		key: Vec<u8>,
490	) -> Result<BoundedVec<u8, T::KeyLimit>, DispatchError> {
491		Ok(BoundedVec::try_from(key).map_err(|_| Error::<T, I>::IncorrectData)?)
492	}
493
494	/// A helper method to construct an attribute's value.
495	///
496	/// # Errors
497	///
498	/// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the
499	/// provided `value` is too long.
500	pub fn construct_attribute_value(
501		value: Vec<u8>,
502	) -> Result<BoundedVec<u8, T::ValueLimit>, DispatchError> {
503		Ok(BoundedVec::try_from(value).map_err(|_| Error::<T, I>::IncorrectData)?)
504	}
505
506	/// A helper method to check whether a system attribute is set for a given item.
507	///
508	/// # Errors
509	///
510	/// This function returns an [`IncorrectData`](crate::Error::IncorrectData) error if the
511	/// provided pallet attribute is too long.
512	pub fn has_system_attribute(
513		collection: &T::CollectionId,
514		item: &T::ItemId,
515		attribute_key: PalletAttributes<T::CollectionId>,
516	) -> Result<bool, DispatchError> {
517		let attribute = (
518			&collection,
519			Some(item),
520			AttributeNamespace::Pallet,
521			&Self::construct_attribute_key(attribute_key.encode())?,
522		);
523		Ok(Attribute::<T, I>::contains_key(attribute))
524	}
525}