referrerpolicy=no-referrer-when-downgrade

pallet_nfts/features/
create_delete_item.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 perform functionality associated with minting and burning
19//! items for the NFTs pallet.
20
21use crate::*;
22use frame_support::{pallet_prelude::*, traits::ExistenceRequirement};
23
24impl<T: Config<I>, I: 'static> Pallet<T, I> {
25	/// Mint a new unique item with the given `collection`, `item`, and other minting configuration
26	/// details.
27	///
28	/// This function performs the minting of a new unique item. It checks if the item does not
29	/// already exist in the given collection, and if the max supply limit (if configured) is not
30	/// reached. It also reserves the required deposit for the item and sets the item details
31	/// accordingly.
32	///
33	/// # Errors
34	///
35	/// This function returns a dispatch error in the following cases:
36	/// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)).
37	/// - If the item already exists in the collection
38	///   ([`AlreadyExists`](crate::Error::AlreadyExists)).
39	/// - If the item configuration already exists
40	///   ([`InconsistentItemConfig`](crate::Error::InconsistentItemConfig)).
41	/// - If the max supply limit (if configured) for the collection is reached
42	///   ([`MaxSupplyReached`](crate::Error::MaxSupplyReached)).
43	/// - If any error occurs in the `with_details_and_config` closure.
44	pub fn do_mint(
45		collection: T::CollectionId,
46		item: T::ItemId,
47		maybe_depositor: Option<T::AccountId>,
48		mint_to: T::AccountId,
49		item_config: ItemConfig,
50		with_details_and_config: impl FnOnce(
51			&CollectionDetailsFor<T, I>,
52			&CollectionConfigFor<T, I>,
53		) -> DispatchResult,
54	) -> DispatchResult {
55		ensure!(!Item::<T, I>::contains_key(collection, item), Error::<T, I>::AlreadyExists);
56
57		Collection::<T, I>::try_mutate(
58			&collection,
59			|maybe_collection_details| -> DispatchResult {
60				let collection_details =
61					maybe_collection_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
62
63				let collection_config = Self::get_collection_config(&collection)?;
64				with_details_and_config(collection_details, &collection_config)?;
65
66				if let Some(max_supply) = collection_config.max_supply {
67					ensure!(collection_details.items < max_supply, Error::<T, I>::MaxSupplyReached);
68				}
69
70				collection_details.items.saturating_inc();
71
72				let collection_config = Self::get_collection_config(&collection)?;
73				let deposit_amount = match collection_config
74					.is_setting_enabled(CollectionSetting::DepositRequired)
75				{
76					true => T::ItemDeposit::get(),
77					false => Zero::zero(),
78				};
79				let deposit_account = match maybe_depositor {
80					None => collection_details.owner.clone(),
81					Some(depositor) => depositor,
82				};
83
84				let item_owner = mint_to.clone();
85				Account::<T, I>::insert((&item_owner, &collection, &item), ());
86
87				if let Ok(existing_config) = ItemConfigOf::<T, I>::try_get(&collection, &item) {
88					ensure!(existing_config == item_config, Error::<T, I>::InconsistentItemConfig);
89				} else {
90					ItemConfigOf::<T, I>::insert(&collection, &item, item_config);
91					collection_details.item_configs.saturating_inc();
92				}
93
94				T::Currency::reserve(&deposit_account, deposit_amount)?;
95
96				let deposit = ItemDeposit { account: deposit_account, amount: deposit_amount };
97				let details = ItemDetails {
98					owner: item_owner,
99					approvals: ApprovalsOf::<T, I>::default(),
100					deposit,
101				};
102				Item::<T, I>::insert(&collection, &item, details);
103				Ok(())
104			},
105		)?;
106
107		Self::deposit_event(Event::Issued { collection, item, owner: mint_to });
108		Ok(())
109	}
110
111	/// Mints a new item using a pre-signed message.
112	///
113	/// This function allows minting a new item using a pre-signed message. The minting process is
114	/// similar to the regular minting process, but it is performed by a pre-authorized account. The
115	/// `mint_to` account receives the newly minted item. The minting process is configurable
116	/// through the provided `mint_data`. The attributes, metadata, and price of the item are set
117	/// according to the provided `mint_data`. The `with_details_and_config` closure is called to
118	/// validate the provided `collection_details` and `collection_config` before minting the item.
119	///
120	/// - `mint_to`: The account that receives the newly minted item.
121	/// - `mint_data`: The pre-signed minting data containing the `collection`, `item`,
122	///   `attributes`, `metadata`, `deadline`, `only_account`, and `mint_price`.
123	/// - `signer`: The account that is authorized to mint the item using the pre-signed message.
124	pub(crate) fn do_mint_pre_signed(
125		mint_to: T::AccountId,
126		mint_data: PreSignedMintOf<T, I>,
127		signer: T::AccountId,
128	) -> DispatchResult {
129		let PreSignedMint {
130			collection,
131			item,
132			attributes,
133			metadata,
134			deadline,
135			only_account,
136			mint_price,
137		} = mint_data;
138		let metadata = Self::construct_metadata(metadata)?;
139
140		ensure!(
141			attributes.len() <= T::MaxAttributesPerCall::get() as usize,
142			Error::<T, I>::MaxAttributesLimitReached
143		);
144		if let Some(account) = only_account {
145			ensure!(account == mint_to, Error::<T, I>::WrongOrigin);
146		}
147
148		let now = T::BlockNumberProvider::current_block_number();
149		ensure!(deadline >= now, Error::<T, I>::DeadlineExpired);
150
151		ensure!(
152			Self::has_role(&collection, &signer, CollectionRole::Issuer),
153			Error::<T, I>::NoPermission
154		);
155
156		let item_config = ItemConfig { settings: Self::get_default_item_settings(&collection)? };
157		Self::do_mint(
158			collection,
159			item,
160			Some(mint_to.clone()),
161			mint_to.clone(),
162			item_config,
163			|collection_details, _| {
164				if let Some(price) = mint_price {
165					T::Currency::transfer(
166						&mint_to,
167						&collection_details.owner,
168						price,
169						ExistenceRequirement::KeepAlive,
170					)?;
171				}
172				Ok(())
173			},
174		)?;
175		let admin_account = Self::find_account_by_role(&collection, CollectionRole::Admin);
176		if let Some(admin_account) = admin_account {
177			for (key, value) in attributes {
178				Self::do_set_attribute(
179					admin_account.clone(),
180					collection,
181					Some(item),
182					AttributeNamespace::CollectionOwner,
183					Self::construct_attribute_key(key)?,
184					Self::construct_attribute_value(value)?,
185					mint_to.clone(),
186				)?;
187			}
188			if !metadata.len().is_zero() {
189				Self::do_set_item_metadata(
190					Some(admin_account.clone()),
191					collection,
192					item,
193					metadata,
194					Some(mint_to.clone()),
195				)?;
196			}
197		}
198		Ok(())
199	}
200
201	/// Burns the specified item with the given `collection`, `item`, and `with_details`.
202	///
203	/// # Errors
204	///
205	/// This function returns a dispatch error in the following cases:
206	/// - If the collection ID is invalid ([`UnknownCollection`](crate::Error::UnknownCollection)).
207	/// - If the item is locked ([`ItemLocked`](crate::Error::ItemLocked)).
208	pub fn do_burn(
209		collection: T::CollectionId,
210		item: T::ItemId,
211		with_details: impl FnOnce(&ItemDetailsFor<T, I>) -> DispatchResult,
212	) -> DispatchResult {
213		ensure!(!T::Locker::is_locked(collection, item), Error::<T, I>::ItemLocked);
214		ensure!(
215			!Self::has_system_attribute(&collection, &item, PalletAttributes::TransferDisabled)?,
216			Error::<T, I>::ItemLocked
217		);
218		let item_config = Self::get_item_config(&collection, &item)?;
219		// NOTE: if item's settings are not empty (e.g. item's metadata is locked)
220		// then we keep the config record and don't remove it
221		let remove_config = !item_config.has_disabled_settings();
222		let owner = Collection::<T, I>::try_mutate(
223			&collection,
224			|maybe_collection_details| -> Result<T::AccountId, DispatchError> {
225				let collection_details =
226					maybe_collection_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
227				let details = Item::<T, I>::get(&collection, &item)
228					.ok_or(Error::<T, I>::UnknownCollection)?;
229				with_details(&details)?;
230
231				// Return the deposit.
232				T::Currency::unreserve(&details.deposit.account, details.deposit.amount);
233				collection_details.items.saturating_dec();
234
235				if remove_config {
236					collection_details.item_configs.saturating_dec();
237				}
238
239				// Clear the metadata if it's not locked.
240				if item_config.is_setting_enabled(ItemSetting::UnlockedMetadata) {
241					if let Some(metadata) = ItemMetadataOf::<T, I>::take(&collection, &item) {
242						let depositor_account =
243							metadata.deposit.account.unwrap_or(collection_details.owner.clone());
244
245						T::Currency::unreserve(&depositor_account, metadata.deposit.amount);
246						collection_details.item_metadatas.saturating_dec();
247
248						if depositor_account == collection_details.owner {
249							collection_details
250								.owner_deposit
251								.saturating_reduce(metadata.deposit.amount);
252						}
253					}
254				}
255
256				Ok(details.owner)
257			},
258		)?;
259
260		Item::<T, I>::remove(&collection, &item);
261		Account::<T, I>::remove((&owner, &collection, &item));
262		ItemPriceOf::<T, I>::remove(&collection, &item);
263		PendingSwapOf::<T, I>::remove(&collection, &item);
264		ItemAttributesApprovalsOf::<T, I>::remove(&collection, &item);
265
266		if remove_config {
267			ItemConfigOf::<T, I>::remove(&collection, &item);
268		}
269
270		Self::deposit_event(Event::Burned { collection, item, owner });
271		Ok(())
272	}
273}