referrerpolicy=no-referrer-when-downgrade

pallet_uniques/
functions.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//! Various pieces of common functionality.
19
20use super::*;
21use frame_support::{
22	ensure,
23	traits::{ExistenceRequirement, Get},
24};
25use sp_runtime::{DispatchError, DispatchResult};
26
27impl<T: Config<I>, I: 'static> Pallet<T, I> {
28	/// Perform a transfer of an item from one account to another within a collection.
29	///
30	/// # Errors
31	/// This function returns a dispatch error in the following cases:
32	/// - The collection or item does not exist
33	///   ([`UnknownCollection`](crate::Error::UnknownCollection)).
34	/// - The collection is frozen, and no transfers are allowed ([`Frozen`](crate::Error::Frozen)).
35	/// - The item is locked, and transfers are not permitted ([`Locked`](crate::Error::Locked)).
36	/// - The `with_details` closure returns an error.
37	pub fn do_transfer(
38		collection: T::CollectionId,
39		item: T::ItemId,
40		dest: T::AccountId,
41		with_details: impl FnOnce(
42			&CollectionDetailsFor<T, I>,
43			&mut ItemDetailsFor<T, I>,
44		) -> DispatchResult,
45	) -> DispatchResult {
46		let collection_details =
47			Collection::<T, I>::get(&collection).ok_or(Error::<T, I>::UnknownCollection)?;
48		ensure!(!collection_details.is_frozen, Error::<T, I>::Frozen);
49		ensure!(!T::Locker::is_locked(collection.clone(), item), Error::<T, I>::Locked);
50
51		let mut details =
52			Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownCollection)?;
53		ensure!(!details.is_frozen, Error::<T, I>::Frozen);
54		with_details(&collection_details, &mut details)?;
55
56		Account::<T, I>::remove((&details.owner, &collection, &item));
57		Account::<T, I>::insert((&dest, &collection, &item), ());
58		let origin = details.owner;
59		details.owner = dest;
60
61		// The approved account has to be reset to `None`, because otherwise pre-approve attack
62		// would be possible, where the owner can approve their second account before making the
63		// transaction and then claiming the item back.
64		details.approved = None;
65
66		Item::<T, I>::insert(&collection, &item, &details);
67		ItemPriceOf::<T, I>::remove(&collection, &item);
68
69		Self::deposit_event(Event::Transferred {
70			collection,
71			item,
72			from: origin,
73			to: details.owner,
74		});
75		Ok(())
76	}
77
78	/// Create a new collection with the provided details.
79	///
80	/// # Errors
81	/// This function returns a dispatch error in the following cases:
82	/// - If the collection ID is already in use ([`InUse`](crate::Error::InUse)).
83	/// - If reserving the deposit fails (e.g., insufficient funds).
84	pub fn do_create_collection(
85		collection: T::CollectionId,
86		owner: T::AccountId,
87		admin: T::AccountId,
88		deposit: DepositBalanceOf<T, I>,
89		free_holding: bool,
90		event: Event<T, I>,
91	) -> DispatchResult {
92		ensure!(!Collection::<T, I>::contains_key(collection.clone()), Error::<T, I>::InUse);
93
94		T::Currency::reserve(&owner, deposit)?;
95
96		Collection::<T, I>::insert(
97			collection.clone(),
98			CollectionDetails {
99				owner: owner.clone(),
100				issuer: admin.clone(),
101				admin: admin.clone(),
102				freezer: admin,
103				total_deposit: deposit,
104				free_holding,
105				items: 0,
106				item_metadatas: 0,
107				attributes: 0,
108				is_frozen: false,
109			},
110		);
111
112		CollectionAccount::<T, I>::insert(&owner, &collection, ());
113		Self::deposit_event(event);
114		Ok(())
115	}
116
117	/// Destroy a collection along with its associated items and metadata.
118	///
119	/// # Errors
120	/// This function returns a dispatch error in the following cases:
121	/// - The collection does not exist ([`UnknownCollection`](crate::Error::UnknownCollection)).
122	/// - The provided witness does not match the actual counts
123	///   ([`BadWitness`](crate::Error::BadWitness)).
124	/// - The caller is not the owner of the collection
125	///   ([`NoPermission`](crate::Error::NoPermission)).
126	pub fn do_destroy_collection(
127		collection: T::CollectionId,
128		witness: DestroyWitness,
129		maybe_check_owner: Option<T::AccountId>,
130	) -> Result<DestroyWitness, DispatchError> {
131		Collection::<T, I>::try_mutate_exists(collection.clone(), |maybe_details| {
132			let collection_details =
133				maybe_details.take().ok_or(Error::<T, I>::UnknownCollection)?;
134			if let Some(check_owner) = maybe_check_owner {
135				ensure!(collection_details.owner == check_owner, Error::<T, I>::NoPermission);
136			}
137			ensure!(collection_details.items == witness.items, Error::<T, I>::BadWitness);
138			ensure!(
139				collection_details.item_metadatas == witness.item_metadatas,
140				Error::<T, I>::BadWitness
141			);
142			ensure!(collection_details.attributes == witness.attributes, Error::<T, I>::BadWitness);
143
144			for (item, details) in Item::<T, I>::drain_prefix(&collection) {
145				Account::<T, I>::remove((&details.owner, &collection, &item));
146			}
147			#[allow(deprecated)]
148			ItemMetadataOf::<T, I>::remove_prefix(&collection, None);
149			#[allow(deprecated)]
150			ItemPriceOf::<T, I>::remove_prefix(&collection, None);
151			CollectionMetadataOf::<T, I>::remove(&collection);
152			#[allow(deprecated)]
153			Attribute::<T, I>::remove_prefix((&collection,), None);
154			CollectionAccount::<T, I>::remove(&collection_details.owner, &collection);
155			T::Currency::unreserve(&collection_details.owner, collection_details.total_deposit);
156			CollectionMaxSupply::<T, I>::remove(&collection);
157
158			Self::deposit_event(Event::Destroyed { collection });
159
160			Ok(DestroyWitness {
161				items: collection_details.items,
162				item_metadatas: collection_details.item_metadatas,
163				attributes: collection_details.attributes,
164			})
165		})
166	}
167
168	/// Mint (create) a new item within a collection and assign ownership to an account.
169	///
170	/// # Errors
171	/// This function returns a dispatch error in the following cases:
172	/// - The item already exists in the collection
173	///   ([`AlreadyExists`](crate::Error::AlreadyExists)).
174	/// - The collection does not exist ([`UnknownCollection`](crate::Error::UnknownCollection)).
175	/// - The provided closure `with_details` returns an error.
176	/// - The collection has reached its maximum supply
177	///   ([`MaxSupplyReached`](crate::Error::MaxSupplyReached)).
178	/// - An arithmetic overflow occurs when incrementing the number of items in the collection.
179	/// - The currency reserve operation for the item deposit fails for any reason.
180	pub fn do_mint(
181		collection: T::CollectionId,
182		item: T::ItemId,
183		owner: T::AccountId,
184		with_details: impl FnOnce(&CollectionDetailsFor<T, I>) -> DispatchResult,
185	) -> DispatchResult {
186		ensure!(
187			!Item::<T, I>::contains_key(collection.clone(), item),
188			Error::<T, I>::AlreadyExists
189		);
190
191		Collection::<T, I>::try_mutate(
192			&collection,
193			|maybe_collection_details| -> DispatchResult {
194				let collection_details =
195					maybe_collection_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
196
197				with_details(collection_details)?;
198
199				if let Ok(max_supply) = CollectionMaxSupply::<T, I>::try_get(&collection) {
200					ensure!(collection_details.items < max_supply, Error::<T, I>::MaxSupplyReached);
201				}
202
203				let items =
204					collection_details.items.checked_add(1).ok_or(ArithmeticError::Overflow)?;
205				collection_details.items = items;
206
207				let deposit = match collection_details.free_holding {
208					true => Zero::zero(),
209					false => T::ItemDeposit::get(),
210				};
211				T::Currency::reserve(&collection_details.owner, deposit)?;
212				collection_details.total_deposit += deposit;
213
214				let owner = owner.clone();
215				Account::<T, I>::insert((&owner, &collection, &item), ());
216				let details = ItemDetails { owner, approved: None, is_frozen: false, deposit };
217				Item::<T, I>::insert(&collection, &item, details);
218				Ok(())
219			},
220		)?;
221
222		Self::deposit_event(Event::Issued { collection, item, owner });
223		Ok(())
224	}
225
226	/// Burn (destroy) an item from a collection.
227	///
228	/// # Errors
229	/// This function returns a `Dispatch` error in the following cases:
230	/// - The item is locked and burns are not permitted ([`Locked`](crate::Error::Locked)).
231	/// - The collection or item does not exist
232	///   ([`UnknownCollection`](crate::Error::UnknownCollection)).
233	/// - The `with_details` closure returns an error.
234	pub fn do_burn(
235		collection: T::CollectionId,
236		item: T::ItemId,
237		with_details: impl FnOnce(&CollectionDetailsFor<T, I>, &ItemDetailsFor<T, I>) -> DispatchResult,
238	) -> DispatchResult {
239		ensure!(!T::Locker::is_locked(collection.clone(), item), Error::<T, I>::Locked);
240		let owner = Collection::<T, I>::try_mutate(
241			&collection,
242			|maybe_collection_details| -> Result<T::AccountId, DispatchError> {
243				let collection_details =
244					maybe_collection_details.as_mut().ok_or(Error::<T, I>::UnknownCollection)?;
245
246				// TODO should it be UnknownItem instead of UnknownCollection?
247				let details = Item::<T, I>::get(&collection, &item)
248					.ok_or(Error::<T, I>::UnknownCollection)?;
249				with_details(collection_details, &details)?;
250
251				// Return the deposit.
252				T::Currency::unreserve(&collection_details.owner, details.deposit);
253				collection_details.total_deposit.saturating_reduce(details.deposit);
254				collection_details.items.saturating_dec();
255				Ok(details.owner)
256			},
257		)?;
258
259		Item::<T, I>::remove(&collection, &item);
260		Account::<T, I>::remove((&owner, &collection, &item));
261		ItemPriceOf::<T, I>::remove(&collection, &item);
262
263		Self::deposit_event(Event::Burned { collection, item, owner });
264		Ok(())
265	}
266
267	/// Set or remove the price for an item in a collection.
268	///
269	/// # Errors
270	/// This function returns a dispatch error in the following cases:
271	/// - The item or collection does not exist ([`UnknownItem`](crate::Error::UnknownItem) or
272	///   [`UnknownCollection`](crate::Error::UnknownCollection)).
273	/// - The sender is not the owner of the item ([`NoPermission`](crate::Error::NoPermission)).
274	pub fn do_set_price(
275		collection: T::CollectionId,
276		item: T::ItemId,
277		sender: T::AccountId,
278		price: Option<ItemPrice<T, I>>,
279		whitelisted_buyer: Option<T::AccountId>,
280	) -> DispatchResult {
281		let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
282		ensure!(details.owner == sender, Error::<T, I>::NoPermission);
283
284		if let Some(ref price) = price {
285			ItemPriceOf::<T, I>::insert(&collection, &item, (price, whitelisted_buyer.clone()));
286			Self::deposit_event(Event::ItemPriceSet {
287				collection,
288				item,
289				price: *price,
290				whitelisted_buyer,
291			});
292		} else {
293			ItemPriceOf::<T, I>::remove(&collection, &item);
294			Self::deposit_event(Event::ItemPriceRemoved { collection, item });
295		}
296
297		Ok(())
298	}
299
300	/// Buy an item from a collection.
301	///
302	/// # Errors
303	/// This function returns a dispatch error in the following cases:
304	/// - The item or collection does not exist ([`UnknownItem`](crate::Error::UnknownItem) or
305	///   [`UnknownCollection`](crate::Error::UnknownCollection)).
306	/// - The buyer is the current owner of the item ([`NoPermission`](crate::Error::NoPermission)).
307	/// - The item is not for sale ([`NotForSale`](crate::Error::NotForSale)).
308	/// - The bid price is lower than the item's sale price
309	///   ([`BidTooLow`](crate::Error::BidTooLow)).
310	/// - The item is set to be sold only to a specific buyer, and the provided buyer is not the
311	///   whitelisted buyer ([`NoPermission`](crate::Error::NoPermission)).
312	/// - The currency transfer between the buyer and the owner fails for any reason.
313	pub fn do_buy_item(
314		collection: T::CollectionId,
315		item: T::ItemId,
316		buyer: T::AccountId,
317		bid_price: ItemPrice<T, I>,
318	) -> DispatchResult {
319		let details = Item::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::UnknownItem)?;
320		ensure!(details.owner != buyer, Error::<T, I>::NoPermission);
321
322		let price_info =
323			ItemPriceOf::<T, I>::get(&collection, &item).ok_or(Error::<T, I>::NotForSale)?;
324
325		ensure!(bid_price >= price_info.0, Error::<T, I>::BidTooLow);
326
327		if let Some(only_buyer) = price_info.1 {
328			ensure!(only_buyer == buyer, Error::<T, I>::NoPermission);
329		}
330
331		T::Currency::transfer(
332			&buyer,
333			&details.owner,
334			price_info.0,
335			ExistenceRequirement::KeepAlive,
336		)?;
337
338		let old_owner = details.owner.clone();
339
340		Self::do_transfer(collection.clone(), item, buyer.clone(), |_, _| Ok(()))?;
341
342		Self::deposit_event(Event::ItemBought {
343			collection,
344			item,
345			price: price_info.0,
346			seller: old_owner,
347			buyer,
348		});
349
350		Ok(())
351	}
352}