git.delta.rocks / unique-network / refs/commits / 87b090179c33

difftreelog

source

pallets/nonfungible/src/lib.rs42.0 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Nonfungible Pallet18//!19//! The Nonfungible pallet provides functionality for handling nonfungible collections and tokens.20//!21//! - [`Config`]22//! - [`NonfungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`](common::CommonWeights)25//!26//! ## Overview27//!28//! The Nonfungible pallet provides functions for:29//!30//! - NFT collection creation and removal31//! - Minting and burning of NFT tokens32//! - Retrieving account balances33//! - Transfering NFT tokens34//! - Setting and checking allowance for NFT tokens35//! - Setting properties and permissions for NFT collections and tokens36//! - Nesting and unnesting tokens37//!38//! ### Terminology39//!40//! - **NFT token:** Non fungible token.41//!42//! - **NFT Collection:** A collection of NFT tokens. All NFT tokens are part of a collection.43//!   Each collection can define it's own properties, properties for it's tokens and set of permissions.44//!45//! - **Balance:** Number of NFT tokens owned by an account46//!47//! - **Allowance:** NFT tokens owned by one account that another account is allowed to make operations on48//!49//! - **Burning:** The process of “deleting” a token from a collection and from50//!   an account balance of the owner.51//!52//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting53//!   owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in54//!   it's child token i.e. parent-child relationship graph shouldn't have cycles.55//!56//! - **Properties:** Key-Values pairs. Token properties are attached to a token. Collection properties are57//!   attached to a collection. Set of permissions could be defined for each property.58//!59//! ### Implementations60//!61//! The Nonfungible pallet provides implementations for the following traits. If these traits provide62//! the functionality that you need, then you can avoid coupling with the Nonfungible pallet.63//!64//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight65//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing66//!   with collections67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create NFT collection. NFT collection can be configured to allow or deny access for73//!   some accounts.74//! - `destroy_collection` - Destroy exising NFT collection. There should be no tokens in the collection.75//! - `burn` - Burn NFT token owned by account.76//! - `transfer` - Transfer NFT token. Transfers should be enabled for NFT collection.77//!   Nests the NFT token if it is sent to another token.78//! - `create_item` - Mint NFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account.80//! - `set_token_property` - Set token property value.81//! - `delete_token_property` - Remove property from the token.82//! - `set_collection_properties` - Set collection properties.83//! - `delete_collection_properties` - Remove properties from the collection.84//! - `set_property_permission` - Set collection property permission.85//! - `set_token_property_permissions` - Set token property permissions.86//!87//! ## Assumptions88//!89//! * To perform operations on tokens sender should be in collection's allow list if collection access mode is `AllowList`.9091#![cfg_attr(not(feature = "std"), no_std)]9293use erc::ERC721Events;94use evm_coder::ToLog;95use frame_support::{96	BoundedVec, ensure, fail, transactional,97	storage::with_transaction,98	pallet_prelude::DispatchResultWithPostInfo,99	pallet_prelude::Weight,100	dispatch::{PostDispatchInfo, Pays},101};102use up_data_structs::{103	AccessMode, CollectionId, CollectionFlags, CustomDataLimit, TokenId, CreateCollectionData,104	CreateNftExData, mapping::TokenAddressMapping, budget::Budget, Property, PropertyKey,105	PropertyValue, PropertyKeyPermission, PropertyScope, TrySetProperty, TokenChild,106	AuxPropertyValue, PropertiesPermissionMap, TokenProperties as TokenPropertiesT,107};108use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};109use pallet_common::{110	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,111	eth::collection_id_to_address, SelfWeightOf as PalletCommonWeightOf,112	weights::WeightInfo as CommonWeightInfo, helpers::add_weight_to_post_info,113};114use pallet_structure::{Pallet as PalletStructure, Error as StructureError};115use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};116use sp_core::{Get, H160};117use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};118use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};119use core::ops::Deref;120use codec::{Encode, Decode, MaxEncodedLen};121use scale_info::TypeInfo;122123pub use pallet::*;124use weights::WeightInfo;125#[cfg(feature = "runtime-benchmarks")]126pub mod benchmarking;127pub mod common;128pub mod erc;129pub mod weights;130131pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::Config>::CrossAccountId>;132pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;133134/// Token data, stored independently from other data used to describe it135/// for the convenience of database access. Notably contains the owner account address.136#[struct_versioning::versioned(version = 2, upper)]137#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]138pub struct ItemData<CrossAccountId> {139	#[version(..2)]140	pub const_data: BoundedVec<u8, CustomDataLimit>,141142	#[version(..2)]143	pub variable_data: BoundedVec<u8, CustomDataLimit>,144145	pub owner: CrossAccountId,146}147148#[frame_support::pallet]149pub mod pallet {150	use super::*;151	use frame_support::{152		Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,153	};154	use up_data_structs::{CollectionId, TokenId};155	use super::weights::WeightInfo;156157	#[pallet::error]158	pub enum Error<T> {159		/// Not Nonfungible item data used to mint in Nonfungible collection.160		NotNonfungibleDataUsedToMintFungibleCollectionToken,161		/// Used amount > 1 with NFT162		NonfungibleItemsHaveNoAmount,163		/// Unable to burn NFT with children164		CantBurnNftWithChildren,165	}166167	#[pallet::config]168	pub trait Config:169		frame_system::Config170		+ pallet_common::Config171		+ pallet_structure::Config172		+ pallet_evm::Config173	{174		type WeightInfo: WeightInfo;175	}176177	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);178179	#[pallet::pallet]180	#[pallet::storage_version(STORAGE_VERSION)]181	pub struct Pallet<T>(_);182183	/// Total amount of minted tokens in a collection.184	#[pallet::storage]185	pub type TokensMinted<T: Config> =186		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;187188	/// Amount of burnt tokens in a collection.189	#[pallet::storage]190	pub type TokensBurnt<T: Config> =191		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;192193	/// Token data, used to partially describe a token.194	#[pallet::storage]195	pub type TokenData<T: Config> = StorageNMap<196		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),197		Value = ItemData<T::CrossAccountId>,198		QueryKind = OptionQuery,199	>;200201	/// Map of key-value pairs, describing the metadata of a token.202	#[pallet::storage]203	#[pallet::getter(fn token_properties)]204	pub type TokenProperties<T: Config> = StorageNMap<205		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),206		Value = TokenPropertiesT,207		QueryKind = ValueQuery,208	>;209210	/// Custom data of a token that is serialized to bytes,211	/// primarily reserved for on-chain operations,212	/// normally obscured from the external users.213	///214	/// Auxiliary properties are slightly different from215	/// usual [`TokenProperties`] due to an unlimited number216	/// and separately stored and written-to key-value pairs.217	///218	/// Currently unused.219	#[pallet::storage]220	#[pallet::getter(fn token_aux_property)]221	pub type TokenAuxProperties<T: Config> = StorageNMap<222		Key = (223			Key<Twox64Concat, CollectionId>,224			Key<Twox64Concat, TokenId>,225			Key<Twox64Concat, PropertyScope>,226			Key<Twox64Concat, PropertyKey>,227		),228		Value = AuxPropertyValue,229		QueryKind = OptionQuery,230	>;231232	/// Used to enumerate tokens owned by account.233	#[pallet::storage]234	pub type Owned<T: Config> = StorageNMap<235		Key = (236			Key<Twox64Concat, CollectionId>,237			Key<Blake2_128Concat, T::CrossAccountId>,238			Key<Twox64Concat, TokenId>,239		),240		Value = bool,241		QueryKind = ValueQuery,242	>;243244	/// Used to enumerate token's children.245	#[pallet::storage]246	#[pallet::getter(fn token_children)]247	pub type TokenChildren<T: Config> = StorageNMap<248		Key = (249			Key<Twox64Concat, CollectionId>,250			Key<Twox64Concat, TokenId>,251			Key<Twox64Concat, (CollectionId, TokenId)>,252		),253		Value = bool,254		QueryKind = ValueQuery,255	>;256257	/// Amount of tokens owned by an account in a collection.258	#[pallet::storage]259	pub type AccountBalance<T: Config> = StorageNMap<260		Key = (261			Key<Twox64Concat, CollectionId>,262			Key<Blake2_128Concat, T::CrossAccountId>,263		),264		Value = u32,265		QueryKind = ValueQuery,266	>;267268	/// Allowance set by a token owner for another user to perform one of certain transactions on a token.269	#[pallet::storage]270	pub type Allowance<T: Config> = StorageNMap<271		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),272		Value = T::CrossAccountId,273		QueryKind = OptionQuery,274	>;275276	/// Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.277	#[pallet::storage]278	pub type CollectionAllowance<T: Config> = StorageNMap<279		Key = (280			Key<Twox64Concat, CollectionId>,281			Key<Blake2_128Concat, T::CrossAccountId>,282			Key<Blake2_128Concat, T::CrossAccountId>,283		),284		Value = bool,285		QueryKind = ValueQuery,286	>;287288	#[pallet::genesis_config]289	pub struct GenesisConfig<T>(PhantomData<T>);290291	#[cfg(feature = "std")]292	impl<T: Config> Default for GenesisConfig<T> {293		fn default() -> Self {294			Self(Default::default())295		}296	}297298	#[pallet::genesis_build]299	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {300		fn build(&self) {301			StorageVersion::new(1).put::<Pallet<T>>();302		}303	}304}305306pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);307impl<T: Config> NonfungibleHandle<T> {308	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {309		Self(inner)310	}311	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {312		self.0313	}314	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {315		&mut self.0316	}317}318319impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {320	fn recorder(&self) -> &SubstrateRecorder<T> {321		self.0.recorder()322	}323	fn into_recorder(self) -> SubstrateRecorder<T> {324		self.0.into_recorder()325	}326}327impl<T: Config> Deref for NonfungibleHandle<T> {328	type Target = pallet_common::CollectionHandle<T>;329330	fn deref(&self) -> &Self::Target {331		&self.0332	}333}334335impl<T: Config> Pallet<T> {336	/// Get number of NFT tokens in collection.337	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {338		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)339	}340341	/// Check that NFT token exists.342	///343	/// - `token`: Token ID.344	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {345		<TokenData<T>>::contains_key((collection.id, token))346	}347348	/// Set the token property with the scope.349	///350	/// - `property`: Contains key-value pair.351	pub fn set_scoped_token_property(352		collection_id: CollectionId,353		token_id: TokenId,354		scope: PropertyScope,355		property: Property,356	) -> DispatchResult {357		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {358			properties.try_scoped_set(scope, property.key, property.value)359		})360		.map_err(<CommonError<T>>::from)?;361362		Ok(())363	}364365	/// Batch operation to set multiple properties with the same scope.366	pub fn set_scoped_token_properties(367		collection_id: CollectionId,368		token_id: TokenId,369		scope: PropertyScope,370		properties: impl Iterator<Item = Property>,371	) -> DispatchResult {372		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {373			stored_properties.try_scoped_set_from_iter(scope, properties)374		})375		.map_err(<CommonError<T>>::from)?;376377		Ok(())378	}379380	/// Add or edit auxiliary data for the property.381	///382	/// - `f`: function that adds or edits auxiliary data.383	pub fn try_mutate_token_aux_property<R, E>(384		collection_id: CollectionId,385		token_id: TokenId,386		scope: PropertyScope,387		key: PropertyKey,388		f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,389	) -> Result<R, E> {390		<TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)391	}392393	/// Remove auxiliary data for the property.394	pub fn remove_token_aux_property(395		collection_id: CollectionId,396		token_id: TokenId,397		scope: PropertyScope,398		key: PropertyKey,399	) {400		<TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));401	}402403	/// Get all auxiliary data in a given scope.404	///405	/// Returns iterator over Property Key - Data pairs.406	pub fn iterate_token_aux_properties(407		collection_id: CollectionId,408		token_id: TokenId,409		scope: PropertyScope,410	) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {411		<TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))412	}413414	/// Get ID of the last minted token415	pub fn current_token_id(collection_id: CollectionId) -> TokenId {416		TokenId(<TokensMinted<T>>::get(collection_id))417	}418}419420// unchecked calls skips any permission checks421impl<T: Config> Pallet<T> {422	/// Create NFT collection423	///424	/// `init_collection` will take non-refundable deposit for collection creation.425	///426	/// - `data`: Contains settings for collection limits and permissions.427	pub fn init_collection(428		owner: T::CrossAccountId,429		payer: T::CrossAccountId,430		data: CreateCollectionData<T::AccountId>,431		flags: CollectionFlags,432	) -> Result<CollectionId, DispatchError> {433		<PalletCommon<T>>::init_collection(owner, payer, data, flags)434	}435436	/// Destroy NFT collection437	///438	/// `destroy_collection` will throw error if collection contains any tokens.439	/// Only owner can destroy collection.440	pub fn destroy_collection(441		collection: NonfungibleHandle<T>,442		sender: &T::CrossAccountId,443	) -> DispatchResult {444		let id = collection.id;445446		if Self::collection_has_tokens(id) {447			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());448		}449450		// =========451452		PalletCommon::destroy_collection(collection.0, sender)?;453454		let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);455		let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);456		let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);457		<TokensMinted<T>>::remove(id);458		<TokensBurnt<T>>::remove(id);459		let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);460		let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);461		let _ = <CollectionAllowance<T>>::clear_prefix((id,), u32::MAX, None);462		Ok(())463	}464465	/// Burn NFT token466	///467	/// `burn` removes `token` from the `collection`, from it's owner and from the parent token468	/// if the token is nested.469	/// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.470	/// Also removes all corresponding properties and auxiliary properties.471	///472	/// - `token`: Token that should be burned473	/// - `collection`: Collection that contains the token474	pub fn burn(475		collection: &NonfungibleHandle<T>,476		sender: &T::CrossAccountId,477		token: TokenId,478	) -> DispatchResult {479		let token_data =480			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;481		ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);482483		if collection.permissions.access() == AccessMode::AllowList {484			collection.check_allowlist(sender)?;485		}486487		if Self::token_has_children(collection.id, token) {488			return Err(<Error<T>>::CantBurnNftWithChildren.into());489		}490491		let burnt = <TokensBurnt<T>>::get(collection.id)492			.checked_add(1)493			.ok_or(ArithmeticError::Overflow)?;494495		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))496			.checked_sub(1)497			.ok_or(ArithmeticError::Overflow)?;498499		// =========500501		if balance == 0 {502			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));503		} else {504			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);505		}506507		<PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);508509		<Owned<T>>::remove((collection.id, &token_data.owner, token));510		<TokensBurnt<T>>::insert(collection.id, burnt);511		<TokenData<T>>::remove((collection.id, token));512		<TokenProperties<T>>::remove((collection.id, token));513		let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);514		let old_spender = <Allowance<T>>::take((collection.id, token));515516		if let Some(old_spender) = old_spender {517			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(518				collection.id,519				token,520				token_data.owner.clone(),521				old_spender,522				0,523			));524		}525526		<PalletEvm<T>>::deposit_log(527			ERC721Events::Transfer {528				from: *token_data.owner.as_eth(),529				to: H160::default(),530				token_id: token.into(),531			}532			.to_log(collection_id_to_address(collection.id)),533		);534		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(535			collection.id,536			token,537			token_data.owner,538			1,539		));540		Ok(())541	}542543	/// Same as [`burn`] but burns all the tokens that are nested in the token first544	///545	/// - `self_budget`: Limit for searching children in depth.546	/// - `breadth_budget`: Limit of breadth of searching children.547	///548	/// [`burn`]: struct.Pallet.html#method.burn549	#[transactional]550	pub fn burn_recursively(551		collection: &NonfungibleHandle<T>,552		sender: &T::CrossAccountId,553		token: TokenId,554		self_budget: &dyn Budget,555		breadth_budget: &dyn Budget,556	) -> DispatchResultWithPostInfo {557		ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);558559		let current_token_account =560			T::CrossTokenAddressMapping::token_to_address(collection.id, token);561562		let mut weight = Weight::zero();563564		// This method is transactional, if user in fact doesn't have permissions to remove token -565		// tokens removed here will be restored after rejected transaction566		for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {567			ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);568			let PostDispatchInfo { actual_weight, .. } =569				<PalletStructure<T>>::burn_item_recursively(570					current_token_account.clone(),571					collection,572					token,573					self_budget,574					breadth_budget,575				)?;576			if let Some(actual_weight) = actual_weight {577				weight = weight.saturating_add(actual_weight);578			}579		}580581		Self::burn(collection, sender, token)?;582		DispatchResultWithPostInfo::Ok(PostDispatchInfo {583			actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),584			pays_fee: Pays::Yes,585		})586	}587588	/// A batch operation to add, edit or remove properties for a token.589	///590	/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.591	/// - `is_token_create`: Indicates that method is called during token initialization.592	///   Allows to bypass ownership check.593	///594	/// All affected properties should have `mutable` permission595	/// to be **deleted** or to be **set more than once**,596	/// and the sender should have permission to edit those properties.597	///598	/// This function fires an event for each property change.599	/// In case of an error, all the changes (including the events) will be reverted600	/// since the function is transactional.601	#[transactional]602	fn modify_token_properties(603		collection: &NonfungibleHandle<T>,604		sender: &T::CrossAccountId,605		token_id: TokenId,606		properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,607		is_token_create: bool,608		nesting_budget: &dyn Budget,609	) -> DispatchResult {610		let is_token_owner = || {611			let is_owned = <PalletStructure<T>>::check_indirectly_owned(612				sender.clone(),613				collection.id,614				token_id,615				None,616				nesting_budget,617			)?;618619			Ok(is_owned)620		};621622		let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));623624		<PalletCommon<T>>::modify_token_properties(625			collection,626			sender,627			token_id,628			properties_updates,629			is_token_create,630			stored_properties,631			is_token_owner,632			|properties| <TokenProperties<T>>::set((collection.id, token_id), properties),633			erc::ERC721TokenEvent::TokenChanged {634				token_id: token_id.into(),635			}636			.to_log(T::ContractAddress::get()),637		)638	}639640	/// Batch operation to add or edit properties for the token641	///642	/// Same as [`modify_token_properties`] but doesn't allow to remove properties643	///644	/// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties645	pub fn set_token_properties(646		collection: &NonfungibleHandle<T>,647		sender: &T::CrossAccountId,648		token_id: TokenId,649		properties: impl Iterator<Item = Property>,650		is_token_create: bool,651		nesting_budget: &dyn Budget,652	) -> DispatchResult {653		Self::modify_token_properties(654			collection,655			sender,656			token_id,657			properties.map(|p| (p.key, Some(p.value))),658			is_token_create,659			nesting_budget,660		)661	}662663	/// Add or edit single property for the token664	///665	/// Calls [`set_token_properties`] internally666	///667	/// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties668	pub fn set_token_property(669		collection: &NonfungibleHandle<T>,670		sender: &T::CrossAccountId,671		token_id: TokenId,672		property: Property,673		nesting_budget: &dyn Budget,674	) -> DispatchResult {675		let is_token_create = false;676677		Self::set_token_properties(678			collection,679			sender,680			token_id,681			[property].into_iter(),682			is_token_create,683			nesting_budget,684		)685	}686687	/// Batch operation to remove properties from the token688	///689	/// Same as [`modify_token_properties`] but doesn't allow to add or edit properties690	///691	/// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties692	pub fn delete_token_properties(693		collection: &NonfungibleHandle<T>,694		sender: &T::CrossAccountId,695		token_id: TokenId,696		property_keys: impl Iterator<Item = PropertyKey>,697		nesting_budget: &dyn Budget,698	) -> DispatchResult {699		let is_token_create = false;700701		Self::modify_token_properties(702			collection,703			sender,704			token_id,705			property_keys.into_iter().map(|key| (key, None)),706			is_token_create,707			nesting_budget,708		)709	}710711	/// Remove single property from the token712	///713	/// Calls [`delete_token_properties`] internally714	///715	/// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties716	pub fn delete_token_property(717		collection: &NonfungibleHandle<T>,718		sender: &T::CrossAccountId,719		token_id: TokenId,720		property_key: PropertyKey,721		nesting_budget: &dyn Budget,722	) -> DispatchResult {723		Self::delete_token_properties(724			collection,725			sender,726			token_id,727			[property_key].into_iter(),728			nesting_budget,729		)730	}731732	/// Add or edit properties for the collection733	pub fn set_collection_properties(734		collection: &NonfungibleHandle<T>,735		sender: &T::CrossAccountId,736		properties: Vec<Property>,737	) -> DispatchResult {738		<PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())739	}740741	/// Remove properties from the collection742	pub fn delete_collection_properties(743		collection: &CollectionHandle<T>,744		sender: &T::CrossAccountId,745		property_keys: Vec<PropertyKey>,746	) -> DispatchResult {747		<PalletCommon<T>>::delete_collection_properties(748			collection,749			sender,750			property_keys.into_iter(),751		)752	}753754	/// Set property permissions for the token.755	///756	/// Sender should be the owner or admin of token's collection.757	pub fn set_token_property_permissions(758		collection: &CollectionHandle<T>,759		sender: &T::CrossAccountId,760		property_permissions: Vec<PropertyKeyPermission>,761	) -> DispatchResult {762		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)763	}764765	/// Set property permissions for the token with scope.766	///767	/// Sender should be the owner or admin of token's collection.768	pub fn set_scoped_token_property_permissions(769		collection: &CollectionHandle<T>,770		sender: &T::CrossAccountId,771		scope: PropertyScope,772		property_permissions: Vec<PropertyKeyPermission>,773	) -> DispatchResult {774		<PalletCommon<T>>::set_scoped_token_property_permissions(775			collection,776			sender,777			scope,778			property_permissions,779		)780	}781782	pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {783		<PalletCommon<T>>::property_permissions(collection_id)784	}785786	pub fn check_token_immediate_ownership(787		collection: &NonfungibleHandle<T>,788		token: TokenId,789		possible_owner: &T::CrossAccountId,790	) -> DispatchResult {791		let token_data =792			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;793		ensure!(794			&token_data.owner == possible_owner,795			<CommonError<T>>::NoPermission796		);797		Ok(())798	}799800	/// Transfer NFT token from one account to another.801	///802	/// `from` account stops being the owner and `to` account becomes the owner of the token.803	/// If `to` is token than `to` becomes owner of the token and the token become nested.804	/// Unnests token from previous parent if it was nested before.805	/// Removes allowance for the token if there was any.806	/// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.807	///808	/// - `nesting_budget`: Limit for token nesting depth809	pub fn transfer(810		collection: &NonfungibleHandle<T>,811		from: &T::CrossAccountId,812		to: &T::CrossAccountId,813		token: TokenId,814		nesting_budget: &dyn Budget,815	) -> DispatchResultWithPostInfo {816		ensure!(817			collection.limits.transfers_enabled(),818			<CommonError<T>>::TransferNotAllowed819		);820821		let mut actual_weight = <SelfWeightOf<T>>::transfer_raw();822		let token_data =823			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;824		ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);825826		if collection.permissions.access() == AccessMode::AllowList {827			collection.check_allowlist(from)?;828			collection.check_allowlist(to)?;829			actual_weight += <PalletCommonWeightOf<T>>::check_accesslist() * 2;830		}831		<PalletCommon<T>>::ensure_correct_receiver(to)?;832833		let balance_from = <AccountBalance<T>>::get((collection.id, from))834			.checked_sub(1)835			.ok_or(<CommonError<T>>::TokenValueTooLow)?;836		let balance_to = if from != to {837			let balance_to = <AccountBalance<T>>::get((collection.id, to))838				.checked_add(1)839				.ok_or(ArithmeticError::Overflow)?;840841			ensure!(842				balance_to < collection.limits.account_token_ownership_limit(),843				<CommonError<T>>::AccountTokenLimitExceeded,844			);845846			Some(balance_to)847		} else {848			None849		};850851		<PalletStructure<T>>::nest_if_sent_to_token(852			from.clone(),853			to,854			collection.id,855			token,856			nesting_budget,857		)?;858859		// =========860861		<PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);862863		<TokenData<T>>::insert(864			(collection.id, token),865			ItemData {866				owner: to.clone(),867				..token_data868			},869		);870871		if let Some(balance_to) = balance_to {872			// from != to873			if balance_from == 0 {874				<AccountBalance<T>>::remove((collection.id, from));875			} else {876				<AccountBalance<T>>::insert((collection.id, from), balance_from);877			}878			<AccountBalance<T>>::insert((collection.id, to), balance_to);879			<Owned<T>>::remove((collection.id, from, token));880			<Owned<T>>::insert((collection.id, to, token), true);881		}882		Self::set_allowance_unchecked(collection, from, token, None, true);883884		<PalletEvm<T>>::deposit_log(885			ERC721Events::Transfer {886				from: *from.as_eth(),887				to: *to.as_eth(),888				token_id: token.into(),889			}890			.to_log(collection_id_to_address(collection.id)),891		);892		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(893			collection.id,894			token,895			from.clone(),896			to.clone(),897			1,898		));899900		Ok(PostDispatchInfo {901			actual_weight: Some(actual_weight),902			pays_fee: Pays::Yes,903		})904	}905906	/// Batch operation to mint multiple NFT tokens.907	///908	/// The sender should be the owner/admin of the collection or collection should be configured909	/// to allow public minting.910	/// Throws if amount of tokens reached it's limit for the collection or if caller reached911	/// token ownership limit.912	///913	/// - `data`: Contains list of token properties and users who will become the owners of the914	///   corresponging tokens.915	/// - `nesting_budget`: Limit for token nesting depth916	pub fn create_multiple_items(917		collection: &NonfungibleHandle<T>,918		sender: &T::CrossAccountId,919		data: Vec<CreateItemData<T>>,920		nesting_budget: &dyn Budget,921	) -> DispatchResult {922		if !collection.is_owner_or_admin(sender) {923			ensure!(924				collection.permissions.mint_mode(),925				<CommonError<T>>::PublicMintingNotAllowed926			);927			collection.check_allowlist(sender)?;928929			for item in data.iter() {930				collection.check_allowlist(&item.owner)?;931			}932		}933934		for data in data.iter() {935			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;936		}937938		let first_token = <TokensMinted<T>>::get(collection.id);939		let tokens_minted = first_token940			.checked_add(data.len() as u32)941			.ok_or(ArithmeticError::Overflow)?;942		ensure!(943			tokens_minted <= collection.limits.token_limit(),944			<CommonError<T>>::CollectionTokenLimitExceeded945		);946947		let mut balances = BTreeMap::new();948		for data in &data {949			let balance = balances950				.entry(&data.owner)951				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));952			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;953954			ensure!(955				*balance <= collection.limits.account_token_ownership_limit(),956				<CommonError<T>>::AccountTokenLimitExceeded,957			);958		}959960		for (i, data) in data.iter().enumerate() {961			let token = TokenId(first_token + i as u32 + 1);962963			<PalletStructure<T>>::check_nesting(964				sender.clone(),965				&data.owner,966				collection.id,967				token,968				nesting_budget,969			)?;970		}971972		// =========973974		with_transaction(|| {975			for (i, data) in data.iter().enumerate() {976				let token = first_token + i as u32 + 1;977978				<TokenData<T>>::insert(979					(collection.id, token),980					ItemData {981						// const_data: data.const_data.clone(),982						owner: data.owner.clone(),983					},984				);985986				<PalletStructure<T>>::nest_if_sent_to_token_unchecked(987					&data.owner,988					collection.id,989					TokenId(token),990				);991992				if let Err(e) = Self::set_token_properties(993					collection,994					sender,995					TokenId(token),996					data.properties.clone().into_iter(),997					true,998					nesting_budget,999				) {1000					return TransactionOutcome::Rollback(Err(e));1001				}1002			}1003			TransactionOutcome::Commit(Ok(()))1004		})?;10051006		<TokensMinted<T>>::insert(collection.id, tokens_minted);1007		for (account, balance) in balances {1008			<AccountBalance<T>>::insert((collection.id, account), balance);1009		}1010		for (i, data) in data.into_iter().enumerate() {1011			let token = first_token + i as u32 + 1;1012			<Owned<T>>::insert((collection.id, &data.owner, token), true);10131014			<PalletEvm<T>>::deposit_log(1015				ERC721Events::Transfer {1016					from: H160::default(),1017					to: *data.owner.as_eth(),1018					token_id: token.into(),1019				}1020				.to_log(collection_id_to_address(collection.id)),1021			);1022			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1023				collection.id,1024				TokenId(token),1025				data.owner.clone(),1026				1,1027			));1028		}1029		Ok(())1030	}10311032	pub fn set_allowance_unchecked(1033		collection: &NonfungibleHandle<T>,1034		sender: &T::CrossAccountId,1035		token: TokenId,1036		spender: Option<&T::CrossAccountId>,1037		assume_implicit_eth: bool,1038	) {1039		if let Some(spender) = spender {1040			let old_spender = <Allowance<T>>::get((collection.id, token));1041			<Allowance<T>>::insert((collection.id, token), spender);1042			// In ERC721 there is only one possible approved user of token, so we set1043			// approved user to spender1044			<PalletEvm<T>>::deposit_log(1045				ERC721Events::Approval {1046					owner: *sender.as_eth(),1047					approved: *spender.as_eth(),1048					token_id: token.into(),1049				}1050				.to_log(collection_id_to_address(collection.id)),1051			);1052			// In Unique chain, any token can have any amount of approved users, so we need to1053			// set allowance of old owner to 0, and allowance of new owner to 11054			if old_spender.as_ref() != Some(spender) {1055				if let Some(old_owner) = old_spender {1056					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(1057						collection.id,1058						token,1059						sender.clone(),1060						old_owner,1061						0,1062					));1063				}1064				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(1065					collection.id,1066					token,1067					sender.clone(),1068					spender.clone(),1069					1,1070				));1071			}1072		} else {1073			let old_spender = <Allowance<T>>::take((collection.id, token));1074			if !assume_implicit_eth {1075				// In ERC721 there is only one possible approved user of token, so we set1076				// approved user to zero address1077				<PalletEvm<T>>::deposit_log(1078					ERC721Events::Approval {1079						owner: *sender.as_eth(),1080						approved: H160::default(),1081						token_id: token.into(),1082					}1083					.to_log(collection_id_to_address(collection.id)),1084				);1085			}1086			// In Unique chain, any token can have any amount of approved users, so we need to1087			// set allowance of old owner to 01088			if let Some(old_spender) = old_spender {1089				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(1090					collection.id,1091					token,1092					sender.clone(),1093					old_spender,1094					0,1095				));1096			}1097		}1098	}10991100	pub fn get_allowance(1101		collection: &NonfungibleHandle<T>,1102		token_id: TokenId,1103	) -> Result<Option<T::CrossAccountId>, DispatchError> {1104		ensure!(1105			<TokenData<T>>::get((collection.id, token_id)).is_some(),1106			<CommonError<T>>::TokenNotFound1107		);1108		Ok(<Allowance<T>>::get((collection.id, token_id)))1109	}11101111	/// Set allowance for the spender to `transfer` or `burn` sender's token.1112	///1113	/// - `token`: Token the spender is allowed to `transfer` or `burn`.1114	pub fn set_allowance(1115		collection: &NonfungibleHandle<T>,1116		sender: &T::CrossAccountId,1117		token: TokenId,1118		spender: Option<&T::CrossAccountId>,1119	) -> DispatchResult {1120		if collection.permissions.access() == AccessMode::AllowList {1121			collection.check_allowlist(sender)?;1122			if let Some(spender) = spender {1123				collection.check_allowlist(spender)?;1124			}1125		}11261127		if let Some(spender) = spender {1128			<PalletCommon<T>>::ensure_correct_receiver(spender)?;1129		}11301131		let token_data =1132			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1133		if &token_data.owner != sender {1134			ensure!(1135				collection.ignores_owned_amount(sender),1136				<CommonError<T>>::CantApproveMoreThanOwned1137			);1138		}11391140		// =========11411142		Self::set_allowance_unchecked(collection, sender, token, spender, false);1143		Ok(())1144	}11451146	/// Set allowance for the spender to `transfer` or `burn` sender's token from eth mirror.1147	///1148	/// - `from`: Address of sender's eth mirror.1149	/// - `to`: Adress of spender.1150	/// - `token`: Token the spender is allowed to `transfer` or `burn`.1151	pub fn set_allowance_from(1152		collection: &NonfungibleHandle<T>,1153		sender: &T::CrossAccountId,1154		from: &T::CrossAccountId,1155		token: TokenId,1156		to: Option<&T::CrossAccountId>,1157	) -> DispatchResult {1158		if collection.permissions.access() == AccessMode::AllowList {1159			collection.check_allowlist(sender)?;1160			collection.check_allowlist(from)?;1161			if let Some(to) = to {1162				collection.check_allowlist(to)?;1163			}1164		}11651166		if let Some(to) = to {1167			<PalletCommon<T>>::ensure_correct_receiver(to)?;1168		}11691170		ensure!(1171			sender.conv_eq(from),1172			<CommonError<T>>::AddressIsNotEthMirror1173		);11741175		let token_data =1176			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1177		if token_data.owner != *from {1178			ensure!(1179				collection.limits.owner_can_transfer()1180					&& (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from)),1181				<CommonError<T>>::CantApproveMoreThanOwned1182			);1183		}11841185		// =========11861187		Self::set_allowance_unchecked(collection, from, token, to, false);1188		Ok(())1189	}11901191	/// Checks allowance for the spender to use the token.1192	fn check_allowed(1193		collection: &NonfungibleHandle<T>,1194		spender: &T::CrossAccountId,1195		from: &T::CrossAccountId,1196		token: TokenId,1197		nesting_budget: &dyn Budget,1198	) -> DispatchResult {1199		if spender.conv_eq(from) {1200			return Ok(());1201		}1202		if collection.permissions.access() == AccessMode::AllowList {1203			// `from`, `to` checked in [`transfer`]1204			collection.check_allowlist(spender)?;1205		}12061207		if collection.ignores_token_restrictions(spender) {1208			return Ok(());1209		}12101211		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1212			ensure!(1213				<PalletStructure<T>>::check_indirectly_owned(1214					spender.clone(),1215					source.0,1216					source.1,1217					None,1218					nesting_budget1219				)?,1220				<CommonError<T>>::ApprovedValueTooLow,1221			);1222			return Ok(());1223		}1224		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1225			return Ok(());1226		}1227		if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1228			return Ok(());1229		}12301231		Err(<CommonError<T>>::ApprovedValueTooLow.into())1232	}12331234	/// Transfer NFT token from one account to another.1235	///1236	/// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1237	/// The owner should set allowance for the spender to transfer token.1238	///1239	/// [`transfer`]: struct.Pallet.html#method.transfer1240	pub fn transfer_from(1241		collection: &NonfungibleHandle<T>,1242		spender: &T::CrossAccountId,1243		from: &T::CrossAccountId,1244		to: &T::CrossAccountId,1245		token: TokenId,1246		nesting_budget: &dyn Budget,1247	) -> DispatchResultWithPostInfo {1248		Self::check_allowed(collection, spender, from, token, nesting_budget)?;12491250		// =========12511252		// Allowance is reset in [`transfer`]1253		let mut result = Self::transfer(collection, from, to, token, nesting_budget);1254		add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());1255		result1256	}12571258	/// Burn NFT token for `from` account.1259	///1260	/// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1261	/// set allowance for the spender to burn token.1262	///1263	/// [`burn`]: struct.Pallet.html#method.burn1264	pub fn burn_from(1265		collection: &NonfungibleHandle<T>,1266		spender: &T::CrossAccountId,1267		from: &T::CrossAccountId,1268		token: TokenId,1269		nesting_budget: &dyn Budget,1270	) -> DispatchResult {1271		Self::check_allowed(collection, spender, from, token, nesting_budget)?;12721273		// =========12741275		Self::burn(collection, from, token)1276	}12771278	/// Check that `from` token could be nested in `under` token.1279	///1280	pub fn check_nesting(1281		handle: &NonfungibleHandle<T>,1282		sender: T::CrossAccountId,1283		from: (CollectionId, TokenId),1284		under: TokenId,1285		nesting_budget: &dyn Budget,1286	) -> DispatchResult {1287		let nesting = handle.permissions.nesting();12881289		#[cfg(not(feature = "runtime-benchmarks"))]1290		let permissive = false;1291		#[cfg(feature = "runtime-benchmarks")]1292		let permissive = nesting.permissive;12931294		if permissive {1295			ensure!(1296				<TokenData<T>>::contains_key((handle.id, under)),1297				<CommonError<T>>::TokenNotFound1298			);1299		} else if nesting.token_owner1300			&& <PalletStructure<T>>::check_indirectly_owned(1301				sender.clone(),1302				handle.id,1303				under,1304				Some(from),1305				nesting_budget,1306			)? {1307			// Pass, token existence and ouroboros checks are done in `check_indirectly_owned`1308		} else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1309			// token existence and ouroboros checks are done in `get_checked_topmost_owner`1310			let _ = <PalletStructure<T>>::get_checked_topmost_owner(1311				handle.id,1312				under,1313				Some(from),1314				nesting_budget,1315			)?1316			.ok_or(<CommonError<T>>::TokenNotFound)?;1317		} else {1318			fail!(<CommonError<T>>::UserIsNotAllowedToNest);1319		}13201321		if let Some(whitelist) = &nesting.restricted {1322			ensure!(1323				whitelist.contains(&from.0),1324				<CommonError<T>>::SourceCollectionIsNotAllowedToNest1325			);1326		}1327		Ok(())1328	}13291330	fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1331		if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1332			<TokenChildren<T>>::insert((under.0, under.1, to_nest), true);1333		}1334	}13351336	fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1337		if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1338			<TokenChildren<T>>::remove((under.0, under.1, to_unnest));1339		}1340	}13411342	fn collection_has_tokens(collection_id: CollectionId) -> bool {1343		<TokenData<T>>::iter_prefix((collection_id,))1344			.next()1345			.is_some()1346	}13471348	fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1349		<TokenChildren<T>>::iter_prefix((collection_id, token_id))1350			.next()1351			.is_some()1352	}13531354	pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1355		<TokenChildren<T>>::iter_prefix((collection_id, token_id))1356			.map(|((child_collection_id, child_id), _)| TokenChild {1357				collection: child_collection_id,1358				token: child_id,1359			})1360			.collect()1361	}13621363	/// Mint single NFT token.1364	///1365	/// Delegated to [`create_multiple_items`]1366	///1367	/// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1368	pub fn create_item(1369		collection: &NonfungibleHandle<T>,1370		sender: &T::CrossAccountId,1371		data: CreateItemData<T>,1372		nesting_budget: &dyn Budget,1373	) -> DispatchResult {1374		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1375	}13761377	/// Sets or unsets the approval of a given operator.1378	///1379	/// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1380	/// - `owner`: Token owner1381	/// - `operator`: Operator1382	/// - `approve`: Should operator status be granted or revoked?1383	pub fn set_allowance_for_all(1384		collection: &NonfungibleHandle<T>,1385		owner: &T::CrossAccountId,1386		operator: &T::CrossAccountId,1387		approve: bool,1388	) -> DispatchResult {1389		<PalletCommon<T>>::set_allowance_for_all(1390			collection,1391			owner,1392			operator,1393			approve,1394			|| <CollectionAllowance<T>>::insert((collection.id, owner, operator), approve),1395			ERC721Events::ApprovalForAll {1396				owner: *owner.as_eth(),1397				operator: *operator.as_eth(),1398				approved: approve,1399			}1400			.to_log(collection_id_to_address(collection.id)),1401		)1402	}14031404	/// Tells whether the given `owner` approves the `operator`.1405	pub fn allowance_for_all(1406		collection: &NonfungibleHandle<T>,1407		owner: &T::CrossAccountId,1408		operator: &T::CrossAccountId,1409	) -> bool {1410		<CollectionAllowance<T>>::get((collection.id, owner, operator))1411	}14121413	pub fn repair_item(collection: &NonfungibleHandle<T>, token: TokenId) -> DispatchResult {1414		<TokenProperties<T>>::mutate((collection.id, token), |properties| {1415			properties.recompute_consumed_space();1416		});14171418		Ok(())1419	}1420}