git.delta.rocks / unique-network / refs/commits / 5626c4ea6486

difftreelog

fix remove pallet_balance from nonfungible pallet

Grigoriy Simonov2023-06-05parent: #742f829.patch.diff
in: master

3 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6970,7 +6970,6 @@
  "frame-benchmarking",
  "frame-support",
  "frame-system",
- "pallet-balances",
  "pallet-common",
  "pallet-evm",
  "pallet-evm-coder-substrate",
modifiedpallets/nonfungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/nonfungible/Cargo.toml
+++ b/pallets/nonfungible/Cargo.toml
@@ -12,7 +12,6 @@
 frame-benchmarking = { workspace = true, optional = true }
 frame-support = { workspace = true }
 frame-system = { workspace = true }
-pallet-balances = { workspace = true }
 pallet-common = { workspace = true }
 pallet-evm = { workspace = true }
 pallet-evm-coder-substrate = { workspace = true }
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
before · pallets/nonfungible/src/lib.rs
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		+ pallet_balances::Config174	{175		type WeightInfo: WeightInfo;176	}177178	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);179180	#[pallet::pallet]181	#[pallet::storage_version(STORAGE_VERSION)]182	pub struct Pallet<T>(_);183184	/// Total amount of minted tokens in a collection.185	#[pallet::storage]186	pub type TokensMinted<T: Config> =187		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;188189	/// Amount of burnt tokens in a collection.190	#[pallet::storage]191	pub type TokensBurnt<T: Config> =192		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;193194	/// Token data, used to partially describe a token.195	#[pallet::storage]196	pub type TokenData<T: Config> = StorageNMap<197		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),198		Value = ItemData<T::CrossAccountId>,199		QueryKind = OptionQuery,200	>;201202	/// Map of key-value pairs, describing the metadata of a token.203	#[pallet::storage]204	#[pallet::getter(fn token_properties)]205	pub type TokenProperties<T: Config> = StorageNMap<206		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),207		Value = TokenPropertiesT,208		QueryKind = ValueQuery,209	>;210211	/// Custom data of a token that is serialized to bytes,212	/// primarily reserved for on-chain operations,213	/// normally obscured from the external users.214	///215	/// Auxiliary properties are slightly different from216	/// usual [`TokenProperties`] due to an unlimited number217	/// and separately stored and written-to key-value pairs.218	///219	/// Currently unused.220	#[pallet::storage]221	#[pallet::getter(fn token_aux_property)]222	pub type TokenAuxProperties<T: Config> = StorageNMap<223		Key = (224			Key<Twox64Concat, CollectionId>,225			Key<Twox64Concat, TokenId>,226			Key<Twox64Concat, PropertyScope>,227			Key<Twox64Concat, PropertyKey>,228		),229		Value = AuxPropertyValue,230		QueryKind = OptionQuery,231	>;232233	/// Used to enumerate tokens owned by account.234	#[pallet::storage]235	pub type Owned<T: Config> = StorageNMap<236		Key = (237			Key<Twox64Concat, CollectionId>,238			Key<Blake2_128Concat, T::CrossAccountId>,239			Key<Twox64Concat, TokenId>,240		),241		Value = bool,242		QueryKind = ValueQuery,243	>;244245	/// Used to enumerate token's children.246	#[pallet::storage]247	#[pallet::getter(fn token_children)]248	pub type TokenChildren<T: Config> = StorageNMap<249		Key = (250			Key<Twox64Concat, CollectionId>,251			Key<Twox64Concat, TokenId>,252			Key<Twox64Concat, (CollectionId, TokenId)>,253		),254		Value = bool,255		QueryKind = ValueQuery,256	>;257258	/// Amount of tokens owned by an account in a collection.259	#[pallet::storage]260	pub type AccountBalance<T: Config> = StorageNMap<261		Key = (262			Key<Twox64Concat, CollectionId>,263			Key<Blake2_128Concat, T::CrossAccountId>,264		),265		Value = u32,266		QueryKind = ValueQuery,267	>;268269	/// Allowance set by a token owner for another user to perform one of certain transactions on a token.270	#[pallet::storage]271	pub type Allowance<T: Config> = StorageNMap<272		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),273		Value = T::CrossAccountId,274		QueryKind = OptionQuery,275	>;276277	/// Operator set by a wallet owner that could perform certain transactions on all tokens in the wallet.278	#[pallet::storage]279	pub type CollectionAllowance<T: Config> = StorageNMap<280		Key = (281			Key<Twox64Concat, CollectionId>,282			Key<Blake2_128Concat, T::CrossAccountId>,283			Key<Blake2_128Concat, T::CrossAccountId>,284		),285		Value = bool,286		QueryKind = ValueQuery,287	>;288289	#[pallet::genesis_config]290	pub struct GenesisConfig<T>(PhantomData<T>);291292	#[cfg(feature = "std")]293	impl<T: Config> Default for GenesisConfig<T> {294		fn default() -> Self {295			Self(Default::default())296		}297	}298299	#[pallet::genesis_build]300	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {301		fn build(&self) {302			StorageVersion::new(1).put::<Pallet<T>>();303		}304	}305}306307pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);308impl<T: Config> NonfungibleHandle<T> {309	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {310		Self(inner)311	}312	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {313		self.0314	}315	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {316		&mut self.0317	}318}319320impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {321	fn recorder(&self) -> &SubstrateRecorder<T> {322		self.0.recorder()323	}324	fn into_recorder(self) -> SubstrateRecorder<T> {325		self.0.into_recorder()326	}327}328impl<T: Config> Deref for NonfungibleHandle<T> {329	type Target = pallet_common::CollectionHandle<T>;330331	fn deref(&self) -> &Self::Target {332		&self.0333	}334}335336impl<T: Config> Pallet<T> {337	/// Get number of NFT tokens in collection.338	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {339		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)340	}341342	/// Check that NFT token exists.343	///344	/// - `token`: Token ID.345	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {346		<TokenData<T>>::contains_key((collection.id, token))347	}348349	/// Set the token property with the scope.350	///351	/// - `property`: Contains key-value pair.352	pub fn set_scoped_token_property(353		collection_id: CollectionId,354		token_id: TokenId,355		scope: PropertyScope,356		property: Property,357	) -> DispatchResult {358		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {359			properties.try_scoped_set(scope, property.key, property.value)360		})361		.map_err(<CommonError<T>>::from)?;362363		Ok(())364	}365366	/// Batch operation to set multiple properties with the same scope.367	pub fn set_scoped_token_properties(368		collection_id: CollectionId,369		token_id: TokenId,370		scope: PropertyScope,371		properties: impl Iterator<Item = Property>,372	) -> DispatchResult {373		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {374			stored_properties.try_scoped_set_from_iter(scope, properties)375		})376		.map_err(<CommonError<T>>::from)?;377378		Ok(())379	}380381	/// Add or edit auxiliary data for the property.382	///383	/// - `f`: function that adds or edits auxiliary data.384	pub fn try_mutate_token_aux_property<R, E>(385		collection_id: CollectionId,386		token_id: TokenId,387		scope: PropertyScope,388		key: PropertyKey,389		f: impl FnOnce(&mut Option<AuxPropertyValue>) -> Result<R, E>,390	) -> Result<R, E> {391		<TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)392	}393394	/// Remove auxiliary data for the property.395	pub fn remove_token_aux_property(396		collection_id: CollectionId,397		token_id: TokenId,398		scope: PropertyScope,399		key: PropertyKey,400	) {401		<TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));402	}403404	/// Get all auxiliary data in a given scope.405	///406	/// Returns iterator over Property Key - Data pairs.407	pub fn iterate_token_aux_properties(408		collection_id: CollectionId,409		token_id: TokenId,410		scope: PropertyScope,411	) -> impl Iterator<Item = (PropertyKey, AuxPropertyValue)> {412		<TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))413	}414415	/// Get ID of the last minted token416	pub fn current_token_id(collection_id: CollectionId) -> TokenId {417		TokenId(<TokensMinted<T>>::get(collection_id))418	}419}420421// unchecked calls skips any permission checks422impl<T: Config> Pallet<T> {423	/// Create NFT collection424	///425	/// `init_collection` will take non-refundable deposit for collection creation.426	///427	/// - `data`: Contains settings for collection limits and permissions.428	pub fn init_collection(429		owner: T::CrossAccountId,430		payer: T::CrossAccountId,431		data: CreateCollectionData<T::AccountId>,432		flags: CollectionFlags,433	) -> Result<CollectionId, DispatchError> {434		<PalletCommon<T>>::init_collection(owner, payer, data, flags)435	}436437	/// Destroy NFT collection438	///439	/// `destroy_collection` will throw error if collection contains any tokens.440	/// Only owner can destroy collection.441	pub fn destroy_collection(442		collection: NonfungibleHandle<T>,443		sender: &T::CrossAccountId,444	) -> DispatchResult {445		let id = collection.id;446447		if Self::collection_has_tokens(id) {448			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());449		}450451		// =========452453		PalletCommon::destroy_collection(collection.0, sender)?;454455		let _ = <TokenData<T>>::clear_prefix((id,), u32::MAX, None);456		let _ = <TokenChildren<T>>::clear_prefix((id,), u32::MAX, None);457		let _ = <Owned<T>>::clear_prefix((id,), u32::MAX, None);458		<TokensMinted<T>>::remove(id);459		<TokensBurnt<T>>::remove(id);460		let _ = <Allowance<T>>::clear_prefix((id,), u32::MAX, None);461		let _ = <AccountBalance<T>>::clear_prefix((id,), u32::MAX, None);462		let _ = <CollectionAllowance<T>>::clear_prefix((id,), u32::MAX, None);463		Ok(())464	}465466	/// Burn NFT token467	///468	/// `burn` removes `token` from the `collection`, from it's owner and from the parent token469	/// if the token is nested.470	/// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.471	/// Also removes all corresponding properties and auxiliary properties.472	///473	/// - `token`: Token that should be burned474	/// - `collection`: Collection that contains the token475	pub fn burn(476		collection: &NonfungibleHandle<T>,477		sender: &T::CrossAccountId,478		token: TokenId,479	) -> DispatchResult {480		let token_data =481			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;482		ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);483484		if collection.permissions.access() == AccessMode::AllowList {485			collection.check_allowlist(sender)?;486		}487488		if Self::token_has_children(collection.id, token) {489			return Err(<Error<T>>::CantBurnNftWithChildren.into());490		}491492		let burnt = <TokensBurnt<T>>::get(collection.id)493			.checked_add(1)494			.ok_or(ArithmeticError::Overflow)?;495496		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))497			.checked_sub(1)498			.ok_or(ArithmeticError::Overflow)?;499500		// =========501502		if balance == 0 {503			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));504		} else {505			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);506		}507508		<PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);509510		<Owned<T>>::remove((collection.id, &token_data.owner, token));511		<TokensBurnt<T>>::insert(collection.id, burnt);512		<TokenData<T>>::remove((collection.id, token));513		<TokenProperties<T>>::remove((collection.id, token));514		let _ = <TokenAuxProperties<T>>::clear_prefix((collection.id, token), u32::MAX, None);515		let old_spender = <Allowance<T>>::take((collection.id, token));516517		if let Some(old_spender) = old_spender {518			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(519				collection.id,520				token,521				token_data.owner.clone(),522				old_spender,523				0,524			));525		}526527		<PalletEvm<T>>::deposit_log(528			ERC721Events::Transfer {529				from: *token_data.owner.as_eth(),530				to: H160::default(),531				token_id: token.into(),532			}533			.to_log(collection_id_to_address(collection.id)),534		);535		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(536			collection.id,537			token,538			token_data.owner,539			1,540		));541		Ok(())542	}543544	/// Same as [`burn`] but burns all the tokens that are nested in the token first545	///546	/// - `self_budget`: Limit for searching children in depth.547	/// - `breadth_budget`: Limit of breadth of searching children.548	///549	/// [`burn`]: struct.Pallet.html#method.burn550	#[transactional]551	pub fn burn_recursively(552		collection: &NonfungibleHandle<T>,553		sender: &T::CrossAccountId,554		token: TokenId,555		self_budget: &dyn Budget,556		breadth_budget: &dyn Budget,557	) -> DispatchResultWithPostInfo {558		ensure!(self_budget.consume(), <StructureError<T>>::DepthLimit,);559560		let current_token_account =561			T::CrossTokenAddressMapping::token_to_address(collection.id, token);562563		let mut weight = Weight::zero();564565		// This method is transactional, if user in fact doesn't have permissions to remove token -566		// tokens removed here will be restored after rejected transaction567		for ((collection, token), _) in <TokenChildren<T>>::iter_prefix((collection.id, token)) {568			ensure!(breadth_budget.consume(), <StructureError<T>>::BreadthLimit,);569			let PostDispatchInfo { actual_weight, .. } =570				<PalletStructure<T>>::burn_item_recursively(571					current_token_account.clone(),572					collection,573					token,574					self_budget,575					breadth_budget,576				)?;577			if let Some(actual_weight) = actual_weight {578				weight = weight.saturating_add(actual_weight);579			}580		}581582		Self::burn(collection, sender, token)?;583		DispatchResultWithPostInfo::Ok(PostDispatchInfo {584			actual_weight: Some(weight + <SelfWeightOf<T>>::burn_item()),585			pays_fee: Pays::Yes,586		})587	}588589	/// A batch operation to add, edit or remove properties for a token.590	///591	/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.592	/// - `is_token_create`: Indicates that method is called during token initialization.593	///   Allows to bypass ownership check.594	///595	/// All affected properties should have `mutable` permission596	/// to be **deleted** or to be **set more than once**,597	/// and the sender should have permission to edit those properties.598	///599	/// This function fires an event for each property change.600	/// In case of an error, all the changes (including the events) will be reverted601	/// since the function is transactional.602	#[transactional]603	fn modify_token_properties(604		collection: &NonfungibleHandle<T>,605		sender: &T::CrossAccountId,606		token_id: TokenId,607		properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,608		is_token_create: bool,609		nesting_budget: &dyn Budget,610	) -> DispatchResult {611		let is_token_owner = || {612			let is_owned = <PalletStructure<T>>::check_indirectly_owned(613				sender.clone(),614				collection.id,615				token_id,616				None,617				nesting_budget,618			)?;619620			Ok(is_owned)621		};622623		let stored_properties = <TokenProperties<T>>::get((collection.id, token_id));624625		<PalletCommon<T>>::modify_token_properties(626			collection,627			sender,628			token_id,629			properties_updates,630			is_token_create,631			stored_properties,632			is_token_owner,633			|properties| <TokenProperties<T>>::set((collection.id, token_id), properties),634			erc::ERC721TokenEvent::TokenChanged {635				token_id: token_id.into(),636			}637			.to_log(T::ContractAddress::get()),638		)639	}640641	/// Batch operation to add or edit properties for the token642	///643	/// Same as [`modify_token_properties`] but doesn't allow to remove properties644	///645	/// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties646	pub fn set_token_properties(647		collection: &NonfungibleHandle<T>,648		sender: &T::CrossAccountId,649		token_id: TokenId,650		properties: impl Iterator<Item = Property>,651		is_token_create: bool,652		nesting_budget: &dyn Budget,653	) -> DispatchResult {654		Self::modify_token_properties(655			collection,656			sender,657			token_id,658			properties.map(|p| (p.key, Some(p.value))),659			is_token_create,660			nesting_budget,661		)662	}663664	/// Add or edit single property for the token665	///666	/// Calls [`set_token_properties`] internally667	///668	/// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties669	pub fn set_token_property(670		collection: &NonfungibleHandle<T>,671		sender: &T::CrossAccountId,672		token_id: TokenId,673		property: Property,674		nesting_budget: &dyn Budget,675	) -> DispatchResult {676		let is_token_create = false;677678		Self::set_token_properties(679			collection,680			sender,681			token_id,682			[property].into_iter(),683			is_token_create,684			nesting_budget,685		)686	}687688	/// Batch operation to remove properties from the token689	///690	/// Same as [`modify_token_properties`] but doesn't allow to add or edit properties691	///692	/// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties693	pub fn delete_token_properties(694		collection: &NonfungibleHandle<T>,695		sender: &T::CrossAccountId,696		token_id: TokenId,697		property_keys: impl Iterator<Item = PropertyKey>,698		nesting_budget: &dyn Budget,699	) -> DispatchResult {700		let is_token_create = false;701702		Self::modify_token_properties(703			collection,704			sender,705			token_id,706			property_keys.into_iter().map(|key| (key, None)),707			is_token_create,708			nesting_budget,709		)710	}711712	/// Remove single property from the token713	///714	/// Calls [`delete_token_properties`] internally715	///716	/// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties717	pub fn delete_token_property(718		collection: &NonfungibleHandle<T>,719		sender: &T::CrossAccountId,720		token_id: TokenId,721		property_key: PropertyKey,722		nesting_budget: &dyn Budget,723	) -> DispatchResult {724		Self::delete_token_properties(725			collection,726			sender,727			token_id,728			[property_key].into_iter(),729			nesting_budget,730		)731	}732733	/// Add or edit properties for the collection734	pub fn set_collection_properties(735		collection: &NonfungibleHandle<T>,736		sender: &T::CrossAccountId,737		properties: Vec<Property>,738	) -> DispatchResult {739		<PalletCommon<T>>::set_collection_properties(collection, sender, properties.into_iter())740	}741742	/// Remove properties from the collection743	pub fn delete_collection_properties(744		collection: &CollectionHandle<T>,745		sender: &T::CrossAccountId,746		property_keys: Vec<PropertyKey>,747	) -> DispatchResult {748		<PalletCommon<T>>::delete_collection_properties(749			collection,750			sender,751			property_keys.into_iter(),752		)753	}754755	/// Set property permissions for the token.756	///757	/// Sender should be the owner or admin of token's collection.758	pub fn set_token_property_permissions(759		collection: &CollectionHandle<T>,760		sender: &T::CrossAccountId,761		property_permissions: Vec<PropertyKeyPermission>,762	) -> DispatchResult {763		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)764	}765766	/// Set property permissions for the token with scope.767	///768	/// Sender should be the owner or admin of token's collection.769	pub fn set_scoped_token_property_permissions(770		collection: &CollectionHandle<T>,771		sender: &T::CrossAccountId,772		scope: PropertyScope,773		property_permissions: Vec<PropertyKeyPermission>,774	) -> DispatchResult {775		<PalletCommon<T>>::set_scoped_token_property_permissions(776			collection,777			sender,778			scope,779			property_permissions,780		)781	}782783	pub fn token_property_permission(collection_id: CollectionId) -> PropertiesPermissionMap {784		<PalletCommon<T>>::property_permissions(collection_id)785	}786787	pub fn check_token_immediate_ownership(788		collection: &NonfungibleHandle<T>,789		token: TokenId,790		possible_owner: &T::CrossAccountId,791	) -> DispatchResult {792		let token_data =793			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;794		ensure!(795			&token_data.owner == possible_owner,796			<CommonError<T>>::NoPermission797		);798		Ok(())799	}800801	/// Transfer NFT token from one account to another.802	///803	/// `from` account stops being the owner and `to` account becomes the owner of the token.804	/// If `to` is token than `to` becomes owner of the token and the token become nested.805	/// Unnests token from previous parent if it was nested before.806	/// Removes allowance for the token if there was any.807	/// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.808	///809	/// - `nesting_budget`: Limit for token nesting depth810	pub fn transfer(811		collection: &NonfungibleHandle<T>,812		from: &T::CrossAccountId,813		to: &T::CrossAccountId,814		token: TokenId,815		nesting_budget: &dyn Budget,816	) -> DispatchResultWithPostInfo {817		ensure!(818			collection.limits.transfers_enabled(),819			<CommonError<T>>::TransferNotAllowed820		);821822		let mut actual_weight = <SelfWeightOf<T>>::transfer_raw();823		let token_data =824			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;825		ensure!(&token_data.owner == from, <CommonError<T>>::NoPermission);826827		if collection.permissions.access() == AccessMode::AllowList {828			collection.check_allowlist(from)?;829			collection.check_allowlist(to)?;830			actual_weight += <PalletCommonWeightOf<T>>::check_accesslist() * 2;831		}832		<PalletCommon<T>>::ensure_correct_receiver(to)?;833834		let balance_from = <AccountBalance<T>>::get((collection.id, from))835			.checked_sub(1)836			.ok_or(<CommonError<T>>::TokenValueTooLow)?;837		let balance_to = if from != to {838			let balance_to = <AccountBalance<T>>::get((collection.id, to))839				.checked_add(1)840				.ok_or(ArithmeticError::Overflow)?;841842			ensure!(843				balance_to < collection.limits.account_token_ownership_limit(),844				<CommonError<T>>::AccountTokenLimitExceeded,845			);846847			Some(balance_to)848		} else {849			None850		};851852		<PalletStructure<T>>::nest_if_sent_to_token(853			from.clone(),854			to,855			collection.id,856			token,857			nesting_budget,858		)?;859860		// =========861862		<PalletStructure<T>>::unnest_if_nested(&token_data.owner, collection.id, token);863864		<TokenData<T>>::insert(865			(collection.id, token),866			ItemData {867				owner: to.clone(),868				..token_data869			},870		);871872		if let Some(balance_to) = balance_to {873			// from != to874			if balance_from == 0 {875				<AccountBalance<T>>::remove((collection.id, from));876			} else {877				<AccountBalance<T>>::insert((collection.id, from), balance_from);878			}879			<AccountBalance<T>>::insert((collection.id, to), balance_to);880			<Owned<T>>::remove((collection.id, from, token));881			<Owned<T>>::insert((collection.id, to, token), true);882		}883		Self::set_allowance_unchecked(collection, from, token, None, true);884885		<PalletEvm<T>>::deposit_log(886			ERC721Events::Transfer {887				from: *from.as_eth(),888				to: *to.as_eth(),889				token_id: token.into(),890			}891			.to_log(collection_id_to_address(collection.id)),892		);893		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(894			collection.id,895			token,896			from.clone(),897			to.clone(),898			1,899		));900901		Ok(PostDispatchInfo {902			actual_weight: Some(actual_weight),903			pays_fee: Pays::Yes,904		})905	}906907	/// Batch operation to mint multiple NFT tokens.908	///909	/// The sender should be the owner/admin of the collection or collection should be configured910	/// to allow public minting.911	/// Throws if amount of tokens reached it's limit for the collection or if caller reached912	/// token ownership limit.913	///914	/// - `data`: Contains list of token properties and users who will become the owners of the915	///   corresponging tokens.916	/// - `nesting_budget`: Limit for token nesting depth917	pub fn create_multiple_items(918		collection: &NonfungibleHandle<T>,919		sender: &T::CrossAccountId,920		data: Vec<CreateItemData<T>>,921		nesting_budget: &dyn Budget,922	) -> DispatchResult {923		if !collection.is_owner_or_admin(sender) {924			ensure!(925				collection.permissions.mint_mode(),926				<CommonError<T>>::PublicMintingNotAllowed927			);928			collection.check_allowlist(sender)?;929930			for item in data.iter() {931				collection.check_allowlist(&item.owner)?;932			}933		}934935		for data in data.iter() {936			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;937		}938939		let first_token = <TokensMinted<T>>::get(collection.id);940		let tokens_minted = first_token941			.checked_add(data.len() as u32)942			.ok_or(ArithmeticError::Overflow)?;943		ensure!(944			tokens_minted <= collection.limits.token_limit(),945			<CommonError<T>>::CollectionTokenLimitExceeded946		);947948		let mut balances = BTreeMap::new();949		for data in &data {950			let balance = balances951				.entry(&data.owner)952				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));953			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;954955			ensure!(956				*balance <= collection.limits.account_token_ownership_limit(),957				<CommonError<T>>::AccountTokenLimitExceeded,958			);959		}960961		for (i, data) in data.iter().enumerate() {962			let token = TokenId(first_token + i as u32 + 1);963964			<PalletStructure<T>>::check_nesting(965				sender.clone(),966				&data.owner,967				collection.id,968				token,969				nesting_budget,970			)?;971		}972973		// =========974975		with_transaction(|| {976			for (i, data) in data.iter().enumerate() {977				let token = first_token + i as u32 + 1;978979				<TokenData<T>>::insert(980					(collection.id, token),981					ItemData {982						// const_data: data.const_data.clone(),983						owner: data.owner.clone(),984					},985				);986987				<PalletStructure<T>>::nest_if_sent_to_token_unchecked(988					&data.owner,989					collection.id,990					TokenId(token),991				);992993				if let Err(e) = Self::set_token_properties(994					collection,995					sender,996					TokenId(token),997					data.properties.clone().into_iter(),998					true,999					nesting_budget,1000				) {1001					return TransactionOutcome::Rollback(Err(e));1002				}1003			}1004			TransactionOutcome::Commit(Ok(()))1005		})?;10061007		<TokensMinted<T>>::insert(collection.id, tokens_minted);1008		for (account, balance) in balances {1009			<AccountBalance<T>>::insert((collection.id, account), balance);1010		}1011		for (i, data) in data.into_iter().enumerate() {1012			let token = first_token + i as u32 + 1;1013			<Owned<T>>::insert((collection.id, &data.owner, token), true);10141015			<PalletEvm<T>>::deposit_log(1016				ERC721Events::Transfer {1017					from: H160::default(),1018					to: *data.owner.as_eth(),1019					token_id: token.into(),1020				}1021				.to_log(collection_id_to_address(collection.id)),1022			);1023			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1024				collection.id,1025				TokenId(token),1026				data.owner.clone(),1027				1,1028			));1029		}1030		Ok(())1031	}10321033	pub fn set_allowance_unchecked(1034		collection: &NonfungibleHandle<T>,1035		sender: &T::CrossAccountId,1036		token: TokenId,1037		spender: Option<&T::CrossAccountId>,1038		assume_implicit_eth: bool,1039	) {1040		if let Some(spender) = spender {1041			let old_spender = <Allowance<T>>::get((collection.id, token));1042			<Allowance<T>>::insert((collection.id, token), spender);1043			// In ERC721 there is only one possible approved user of token, so we set1044			// approved user to spender1045			<PalletEvm<T>>::deposit_log(1046				ERC721Events::Approval {1047					owner: *sender.as_eth(),1048					approved: *spender.as_eth(),1049					token_id: token.into(),1050				}1051				.to_log(collection_id_to_address(collection.id)),1052			);1053			// In Unique chain, any token can have any amount of approved users, so we need to1054			// set allowance of old owner to 0, and allowance of new owner to 11055			if old_spender.as_ref() != Some(spender) {1056				if let Some(old_owner) = old_spender {1057					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(1058						collection.id,1059						token,1060						sender.clone(),1061						old_owner,1062						0,1063					));1064				}1065				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(1066					collection.id,1067					token,1068					sender.clone(),1069					spender.clone(),1070					1,1071				));1072			}1073		} else {1074			let old_spender = <Allowance<T>>::take((collection.id, token));1075			if !assume_implicit_eth {1076				// In ERC721 there is only one possible approved user of token, so we set1077				// approved user to zero address1078				<PalletEvm<T>>::deposit_log(1079					ERC721Events::Approval {1080						owner: *sender.as_eth(),1081						approved: H160::default(),1082						token_id: token.into(),1083					}1084					.to_log(collection_id_to_address(collection.id)),1085				);1086			}1087			// In Unique chain, any token can have any amount of approved users, so we need to1088			// set allowance of old owner to 01089			if let Some(old_spender) = old_spender {1090				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(1091					collection.id,1092					token,1093					sender.clone(),1094					old_spender,1095					0,1096				));1097			}1098		}1099	}11001101	pub fn get_allowance(1102		collection: &NonfungibleHandle<T>,1103		token_id: TokenId,1104	) -> Result<Option<T::CrossAccountId>, DispatchError> {1105		ensure!(1106			<TokenData<T>>::get((collection.id, token_id)).is_some(),1107			<CommonError<T>>::TokenNotFound1108		);1109		Ok(<Allowance<T>>::get((collection.id, token_id)))1110	}11111112	/// Set allowance for the spender to `transfer` or `burn` sender's token.1113	///1114	/// - `token`: Token the spender is allowed to `transfer` or `burn`.1115	pub fn set_allowance(1116		collection: &NonfungibleHandle<T>,1117		sender: &T::CrossAccountId,1118		token: TokenId,1119		spender: Option<&T::CrossAccountId>,1120	) -> DispatchResult {1121		if collection.permissions.access() == AccessMode::AllowList {1122			collection.check_allowlist(sender)?;1123			if let Some(spender) = spender {1124				collection.check_allowlist(spender)?;1125			}1126		}11271128		if let Some(spender) = spender {1129			<PalletCommon<T>>::ensure_correct_receiver(spender)?;1130		}11311132		let token_data =1133			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1134		if &token_data.owner != sender {1135			ensure!(1136				collection.ignores_owned_amount(sender),1137				<CommonError<T>>::CantApproveMoreThanOwned1138			);1139		}11401141		// =========11421143		Self::set_allowance_unchecked(collection, sender, token, spender, false);1144		Ok(())1145	}11461147	/// Set allowance for the spender to `transfer` or `burn` sender's token from eth mirror.1148	///1149	/// - `from`: Address of sender's eth mirror.1150	/// - `to`: Adress of spender.1151	/// - `token`: Token the spender is allowed to `transfer` or `burn`.1152	pub fn set_allowance_from(1153		collection: &NonfungibleHandle<T>,1154		sender: &T::CrossAccountId,1155		from: &T::CrossAccountId,1156		token: TokenId,1157		to: Option<&T::CrossAccountId>,1158	) -> DispatchResult {1159		if collection.permissions.access() == AccessMode::AllowList {1160			collection.check_allowlist(sender)?;1161			collection.check_allowlist(from)?;1162			if let Some(to) = to {1163				collection.check_allowlist(to)?;1164			}1165		}11661167		if let Some(to) = to {1168			<PalletCommon<T>>::ensure_correct_receiver(to)?;1169		}11701171		ensure!(1172			sender.conv_eq(from),1173			<CommonError<T>>::AddressIsNotEthMirror1174		);11751176		let token_data =1177			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;1178		if token_data.owner != *from {1179			ensure!(1180				collection.limits.owner_can_transfer()1181					&& (collection.is_owner_or_admin(sender) || collection.is_owner_or_admin(from)),1182				<CommonError<T>>::CantApproveMoreThanOwned1183			);1184		}11851186		// =========11871188		Self::set_allowance_unchecked(collection, from, token, to, false);1189		Ok(())1190	}11911192	/// Checks allowance for the spender to use the token.1193	fn check_allowed(1194		collection: &NonfungibleHandle<T>,1195		spender: &T::CrossAccountId,1196		from: &T::CrossAccountId,1197		token: TokenId,1198		nesting_budget: &dyn Budget,1199	) -> DispatchResult {1200		if spender.conv_eq(from) {1201			return Ok(());1202		}1203		if collection.permissions.access() == AccessMode::AllowList {1204			// `from`, `to` checked in [`transfer`]1205			collection.check_allowlist(spender)?;1206		}12071208		if collection.ignores_token_restrictions(spender) {1209			return Ok(());1210		}12111212		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1213			ensure!(1214				<PalletStructure<T>>::check_indirectly_owned(1215					spender.clone(),1216					source.0,1217					source.1,1218					None,1219					nesting_budget1220				)?,1221				<CommonError<T>>::ApprovedValueTooLow,1222			);1223			return Ok(());1224		}1225		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {1226			return Ok(());1227		}1228		if <CollectionAllowance<T>>::get((collection.id, from, spender)) {1229			return Ok(());1230		}12311232		Err(<CommonError<T>>::ApprovedValueTooLow.into())1233	}12341235	/// Transfer NFT token from one account to another.1236	///1237	/// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.1238	/// The owner should set allowance for the spender to transfer token.1239	///1240	/// [`transfer`]: struct.Pallet.html#method.transfer1241	pub fn transfer_from(1242		collection: &NonfungibleHandle<T>,1243		spender: &T::CrossAccountId,1244		from: &T::CrossAccountId,1245		to: &T::CrossAccountId,1246		token: TokenId,1247		nesting_budget: &dyn Budget,1248	) -> DispatchResultWithPostInfo {1249		Self::check_allowed(collection, spender, from, token, nesting_budget)?;12501251		// =========12521253		// Allowance is reset in [`transfer`]1254		let mut result = Self::transfer(collection, from, to, token, nesting_budget);1255		add_weight_to_post_info(&mut result, <SelfWeightOf<T>>::check_allowed_raw());1256		result1257	}12581259	/// Burn NFT token for `from` account.1260	///1261	/// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should1262	/// set allowance for the spender to burn token.1263	///1264	/// [`burn`]: struct.Pallet.html#method.burn1265	pub fn burn_from(1266		collection: &NonfungibleHandle<T>,1267		spender: &T::CrossAccountId,1268		from: &T::CrossAccountId,1269		token: TokenId,1270		nesting_budget: &dyn Budget,1271	) -> DispatchResult {1272		Self::check_allowed(collection, spender, from, token, nesting_budget)?;12731274		// =========12751276		Self::burn(collection, from, token)1277	}12781279	/// Check that `from` token could be nested in `under` token.1280	///1281	pub fn check_nesting(1282		handle: &NonfungibleHandle<T>,1283		sender: T::CrossAccountId,1284		from: (CollectionId, TokenId),1285		under: TokenId,1286		nesting_budget: &dyn Budget,1287	) -> DispatchResult {1288		let nesting = handle.permissions.nesting();12891290		#[cfg(not(feature = "runtime-benchmarks"))]1291		let permissive = false;1292		#[cfg(feature = "runtime-benchmarks")]1293		let permissive = nesting.permissive;12941295		if permissive {1296			ensure!(1297				<TokenData<T>>::contains_key((handle.id, under)),1298				<CommonError<T>>::TokenNotFound1299			);1300		} else if nesting.token_owner1301			&& <PalletStructure<T>>::check_indirectly_owned(1302				sender.clone(),1303				handle.id,1304				under,1305				Some(from),1306				nesting_budget,1307			)? {1308			// Pass, token existence and ouroboros checks are done in `check_indirectly_owned`1309		} else if nesting.collection_admin && handle.is_owner_or_admin(&sender) {1310			// token existence and ouroboros checks are done in `get_checked_topmost_owner`1311			let _ = <PalletStructure<T>>::get_checked_topmost_owner(1312				handle.id,1313				under,1314				Some(from),1315				nesting_budget,1316			)?1317			.ok_or(<CommonError<T>>::TokenNotFound)?;1318		} else {1319			fail!(<CommonError<T>>::UserIsNotAllowedToNest);1320		}13211322		if let Some(whitelist) = &nesting.restricted {1323			ensure!(1324				whitelist.contains(&from.0),1325				<CommonError<T>>::SourceCollectionIsNotAllowedToNest1326			);1327		}1328		Ok(())1329	}13301331	fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {1332		if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1333			<TokenChildren<T>>::insert((under.0, under.1, to_nest), true);1334		}1335	}13361337	fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {1338		if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {1339			<TokenChildren<T>>::remove((under.0, under.1, to_unnest));1340		}1341	}13421343	fn collection_has_tokens(collection_id: CollectionId) -> bool {1344		<TokenData<T>>::iter_prefix((collection_id,))1345			.next()1346			.is_some()1347	}13481349	fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {1350		<TokenChildren<T>>::iter_prefix((collection_id, token_id))1351			.next()1352			.is_some()1353	}13541355	pub fn token_children_ids(collection_id: CollectionId, token_id: TokenId) -> Vec<TokenChild> {1356		<TokenChildren<T>>::iter_prefix((collection_id, token_id))1357			.map(|((child_collection_id, child_id), _)| TokenChild {1358				collection: child_collection_id,1359				token: child_id,1360			})1361			.collect()1362	}13631364	/// Mint single NFT token.1365	///1366	/// Delegated to [`create_multiple_items`]1367	///1368	/// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items1369	pub fn create_item(1370		collection: &NonfungibleHandle<T>,1371		sender: &T::CrossAccountId,1372		data: CreateItemData<T>,1373		nesting_budget: &dyn Budget,1374	) -> DispatchResult {1375		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1376	}13771378	/// Sets or unsets the approval of a given operator.1379	///1380	/// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1381	/// - `owner`: Token owner1382	/// - `operator`: Operator1383	/// - `approve`: Should operator status be granted or revoked?1384	pub fn set_allowance_for_all(1385		collection: &NonfungibleHandle<T>,1386		owner: &T::CrossAccountId,1387		operator: &T::CrossAccountId,1388		approve: bool,1389	) -> DispatchResult {1390		<PalletCommon<T>>::set_allowance_for_all(1391			collection,1392			owner,1393			operator,1394			approve,1395			|| <CollectionAllowance<T>>::insert((collection.id, owner, operator), approve),1396			ERC721Events::ApprovalForAll {1397				owner: *owner.as_eth(),1398				operator: *operator.as_eth(),1399				approved: approve,1400			}1401			.to_log(collection_id_to_address(collection.id)),1402		)1403	}14041405	/// Tells whether the given `owner` approves the `operator`.1406	pub fn allowance_for_all(1407		collection: &NonfungibleHandle<T>,1408		owner: &T::CrossAccountId,1409		operator: &T::CrossAccountId,1410	) -> bool {1411		<CollectionAllowance<T>>::get((collection.id, owner, operator))1412	}14131414	pub fn repair_item(collection: &NonfungibleHandle<T>, token: TokenId) -> DispatchResult {1415		<TokenProperties<T>>::mutate((collection.id, token), |properties| {1416			properties.recompute_consumed_space();1417		});14181419		Ok(())1420	}1421}
after · pallets/nonfungible/src/lib.rs
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}