git.delta.rocks / unique-network / refs/commits / 764dfd599fff

difftreelog

source

pallets/nonfungible/src/lib.rs25.7 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use evm_coder::ToLog;21use frame_support::{BoundedVec, ensure, fail, transactional, storage::with_transaction};22use up_data_structs::{23	AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,24	mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,25	PropertyKey, PropertyKeyPermission, Properties, PropertyScope, TrySetProperty,26};27use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};28use pallet_common::{29	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,30	dispatch::CollectionDispatch, eth::collection_id_to_address,31};32use pallet_structure::Pallet as PalletStructure;33use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};34use sp_core::H160;35use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};36use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap, collections::btree_set::BTreeSet};37use core::ops::Deref;38use codec::{Encode, Decode, MaxEncodedLen};39use scale_info::TypeInfo;4041pub use pallet::*;42#[cfg(feature = "runtime-benchmarks")]43pub mod benchmarking;44pub mod common;45pub mod erc;46pub mod weights;4748pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;49pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5051#[struct_versioning::versioned(version = 2, upper)]52#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]53pub struct ItemData<CrossAccountId> {54	#[version(..2)]55	pub const_data: BoundedVec<u8, CustomDataLimit>,5657	#[version(..2)]58	pub variable_data: BoundedVec<u8, CustomDataLimit>,5960	pub owner: CrossAccountId,61}6263#[frame_support::pallet]64pub mod pallet {65	use super::*;66	use frame_support::{67		Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, traits::StorageVersion,68	};69	use frame_system::pallet_prelude::*;70	use up_data_structs::{CollectionId, TokenId};71	use super::weights::WeightInfo;7273	#[pallet::error]74	pub enum Error<T> {75		/// Not Nonfungible item data used to mint in Nonfungible collection.76		NotNonfungibleDataUsedToMintFungibleCollectionToken,77		/// Used amount > 1 with NFT78		NonfungibleItemsHaveNoAmount,79	}8081	#[pallet::config]82	pub trait Config:83		frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config84	{85		type WeightInfo: WeightInfo;86	}8788	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);8990	#[pallet::pallet]91	#[pallet::storage_version(STORAGE_VERSION)]92	#[pallet::generate_store(pub(super) trait Store)]93	pub struct Pallet<T>(_);9495	#[pallet::storage]96	pub type TokensMinted<T: Config> =97		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;98	#[pallet::storage]99	pub type TokensBurnt<T: Config> =100		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;101102	#[pallet::storage]103	pub type TokenData<T: Config> = StorageNMap<104		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),105		Value = ItemData<T::CrossAccountId>,106		QueryKind = OptionQuery,107	>;108109	#[pallet::storage]110	#[pallet::getter(fn token_properties)]111	pub type TokenProperties<T: Config> = StorageNMap<112		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),113		Value = Properties,114		QueryKind = ValueQuery,115		OnEmpty = up_data_structs::TokenProperties,116	>;117118	/// Used to enumerate tokens owned by account119	#[pallet::storage]120	pub type Owned<T: Config> = StorageNMap<121		Key = (122			Key<Twox64Concat, CollectionId>,123			Key<Blake2_128Concat, T::CrossAccountId>,124			Key<Twox64Concat, TokenId>,125		),126		Value = bool,127		QueryKind = ValueQuery,128	>;129130	#[pallet::storage]131	pub type AccountBalance<T: Config> = StorageNMap<132		Key = (133			Key<Twox64Concat, CollectionId>,134			Key<Blake2_128Concat, T::CrossAccountId>,135		),136		Value = u32,137		QueryKind = ValueQuery,138	>;139140	#[pallet::storage]141	pub type Allowance<T: Config> = StorageNMap<142		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),143		Value = T::CrossAccountId,144		QueryKind = OptionQuery,145	>;146147	#[pallet::hooks]148	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {149		fn on_runtime_upgrade() -> Weight {150			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {151				let mut had_consts = BTreeSet::new();152				<TokenData<T>>::translate::<ItemDataVersion1<T::CrossAccountId>, _>(|(collection, token), v| {153					let mut props = vec![];154					if !v.const_data.is_empty() {155						props.push(Property {156							key: b"_old_constData".to_vec().try_into().unwrap(),157							value: v.const_data.clone().into_inner().try_into().expect("const too long"),158						});159						had_consts.insert(collection);160					}161					if !v.variable_data.is_empty() {162						props.push(Property {163							key: b"_old_variableData".to_vec().try_into().unwrap(),164							value: v.variable_data.clone().into_inner().try_into().expect("variable too long"),165						})166					}167					if !props.is_empty() {168						Self::set_scoped_token_properties(169							collection,170							token,171							PropertyScope::None,172							props.into_iter(),173						).expect("existing token data exceeds property storage");174					}175					Some(<ItemDataVersion2<T::CrossAccountId>>::from(v))176				});177				for collection in had_consts {178					<PalletCommon<T>>::set_property_permission_unchecked(179						collection,180						PropertyKeyPermission {181							key: b"_old_constData".to_vec().try_into().unwrap(),182							permission: PropertyPermission {183								mutable: false,184								collection_admin: true,185								token_owner: false,186							},187						}188					).expect("failed to configure permission");189				}190			}191192			0193		}194	}195}196197pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);198impl<T: Config> NonfungibleHandle<T> {199	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {200		Self(inner)201	}202	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {203		self.0204	}205	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {206		&mut self.0207	}208}209impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {210	fn recorder(&self) -> &SubstrateRecorder<T> {211		self.0.recorder()212	}213	fn into_recorder(self) -> SubstrateRecorder<T> {214		self.0.into_recorder()215	}216}217impl<T: Config> Deref for NonfungibleHandle<T> {218	type Target = pallet_common::CollectionHandle<T>;219220	fn deref(&self) -> &Self::Target {221		&self.0222	}223}224225impl<T: Config> Pallet<T> {226	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {227		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)228	}229	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {230		<TokenData<T>>::contains_key((collection.id, token))231	}232233	pub fn set_scoped_token_property(234		collection_id: CollectionId,235		token_id: TokenId,236		scope: PropertyScope,237		property: Property,238	) -> DispatchResult {239		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {240			properties.try_scoped_set(scope, property.key, property.value)241		})242		.map_err(<CommonError<T>>::from)?;243244		Ok(())245	}246247	pub fn set_scoped_token_properties(248		collection_id: CollectionId,249		token_id: TokenId,250		scope: PropertyScope,251		properties: impl Iterator<Item=Property>,252	) -> DispatchResult {253		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {254			stored_properties.try_scoped_set_from_iter(scope, properties)255		})256		.map_err(<CommonError<T>>::from)?;257258		Ok(())259	}260261	pub fn current_token_id(collection_id: CollectionId) -> TokenId {262		TokenId(<TokensMinted<T>>::get(collection_id))263	}264}265266// unchecked calls skips any permission checks267impl<T: Config> Pallet<T> {268	pub fn init_collection(269		owner: T::AccountId,270		data: CreateCollectionData<T::AccountId>,271	) -> Result<CollectionId, DispatchError> {272		<PalletCommon<T>>::init_collection(owner, data)273	}274	pub fn destroy_collection(275		collection: NonfungibleHandle<T>,276		sender: &T::CrossAccountId,277	) -> DispatchResult {278		let id = collection.id;279280		// =========281282		PalletCommon::destroy_collection(collection.0, sender)?;283284		<TokenData<T>>::remove_prefix((id,), None);285		<Owned<T>>::remove_prefix((id,), None);286		<TokensMinted<T>>::remove(id);287		<TokensBurnt<T>>::remove(id);288		<Allowance<T>>::remove_prefix((id,), None);289		<AccountBalance<T>>::remove_prefix((id,), None);290		Ok(())291	}292293	pub fn burn(294		collection: &NonfungibleHandle<T>,295		sender: &T::CrossAccountId,296		token: TokenId,297	) -> DispatchResult {298		let token_data =299			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;300		ensure!(301			&token_data.owner == sender302				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),303			<CommonError<T>>::NoPermission304		);305306		if collection.permissions.access() == AccessMode::AllowList {307			collection.check_allowlist(sender)?;308		}309310		let burnt = <TokensBurnt<T>>::get(collection.id)311			.checked_add(1)312			.ok_or(ArithmeticError::Overflow)?;313314		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))315			.checked_sub(1)316			.ok_or(ArithmeticError::Overflow)?;317318		if balance == 0 {319			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));320		} else {321			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);322		}323		// =========324325		<Owned<T>>::remove((collection.id, &token_data.owner, token));326		<TokensBurnt<T>>::insert(collection.id, burnt);327		<TokenData<T>>::remove((collection.id, token));328		<TokenProperties<T>>::remove((collection.id, token));329		let old_spender = <Allowance<T>>::take((collection.id, token));330331		if let Some(old_spender) = old_spender {332			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(333				collection.id,334				token,335				sender.clone(),336				old_spender,337				0,338			));339		}340341		<PalletEvm<T>>::deposit_log(342			ERC721Events::Transfer {343				from: *token_data.owner.as_eth(),344				to: H160::default(),345				token_id: token.into(),346			}347			.to_log(collection_id_to_address(collection.id)),348		);349		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(350			collection.id,351			token,352			token_data.owner,353			1,354		));355		Ok(())356	}357358	pub fn set_token_property(359		collection: &NonfungibleHandle<T>,360		sender: &T::CrossAccountId,361		token_id: TokenId,362		property: Property,363	) -> DispatchResult {364		Self::check_token_change_permission(collection, sender, token_id, &property.key)?;365366		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {367			let property = property.clone();368			properties.try_set(property.key, property.value)369		})370		.map_err(<CommonError<T>>::from)?;371372		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(373			collection.id,374			token_id,375			property.key,376		));377378		Ok(())379	}380381	#[transactional]382	pub fn set_token_properties(383		collection: &NonfungibleHandle<T>,384		sender: &T::CrossAccountId,385		token_id: TokenId,386		properties: Vec<Property>,387	) -> DispatchResult {388		for property in properties {389			Self::set_token_property(collection, sender, token_id, property)?;390		}391392		Ok(())393	}394395	pub fn delete_token_property(396		collection: &NonfungibleHandle<T>,397		sender: &T::CrossAccountId,398		token_id: TokenId,399		property_key: PropertyKey,400	) -> DispatchResult {401		Self::check_token_change_permission(collection, sender, token_id, &property_key)?;402403		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {404			properties.remove(&property_key)405		})406		.map_err(<CommonError<T>>::from)?;407408		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(409			collection.id,410			token_id,411			property_key,412		));413414		Ok(())415	}416417	fn check_token_change_permission(418		collection: &NonfungibleHandle<T>,419		sender: &T::CrossAccountId,420		token_id: TokenId,421		property_key: &PropertyKey,422	) -> DispatchResult {423		let permission = <PalletCommon<T>>::property_permissions(collection.id)424			.get(property_key)425			.cloned()426			.unwrap_or_else(PropertyPermission::none);427428		let token_data = <TokenData<T>>::get((collection.id, token_id))429			.ok_or(<CommonError<T>>::TokenNotFound)?;430431		let check_token_owner = || -> DispatchResult {432			ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);433			Ok(())434		};435436		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))437			.get(property_key)438			.is_some();439440		match permission {441			PropertyPermission { mutable: false, .. } if is_property_exists => {442				Err(<CommonError<T>>::NoPermission.into())443			}444445			PropertyPermission {446				collection_admin,447				token_owner,448				..449			} => {450				let mut check_result = Err(<CommonError<T>>::NoPermission.into());451452				if collection_admin {453					check_result = collection.check_is_owner_or_admin(sender);454				}455456				if token_owner {457					check_result.or_else(|_| check_token_owner())458				} else {459					check_result460				}461			}462		}463	}464465	#[transactional]466	pub fn delete_token_properties(467		collection: &NonfungibleHandle<T>,468		sender: &T::CrossAccountId,469		token_id: TokenId,470		property_keys: Vec<PropertyKey>,471	) -> DispatchResult {472		for key in property_keys {473			Self::delete_token_property(collection, sender, token_id, key)?;474		}475476		Ok(())477	}478479	pub fn set_collection_properties(480		collection: &NonfungibleHandle<T>,481		sender: &T::CrossAccountId,482		properties: Vec<Property>,483	) -> DispatchResult {484		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)485	}486487	pub fn delete_collection_properties(488		collection: &CollectionHandle<T>,489		sender: &T::CrossAccountId,490		property_keys: Vec<PropertyKey>,491	) -> DispatchResult {492		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)493	}494495	pub fn set_property_permissions(496		collection: &CollectionHandle<T>,497		sender: &T::CrossAccountId,498		property_permissions: Vec<PropertyKeyPermission>,499	) -> DispatchResult {500		<PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)501	}502503	pub fn set_property_permission(504		collection: &CollectionHandle<T>,505		sender: &T::CrossAccountId,506		permission: PropertyKeyPermission,507	) -> DispatchResult {508		<PalletCommon<T>>::set_property_permission(collection, sender, permission)509	}510511	pub fn transfer(512		collection: &NonfungibleHandle<T>,513		from: &T::CrossAccountId,514		to: &T::CrossAccountId,515		token: TokenId,516		nesting_budget: &dyn Budget,517	) -> DispatchResult {518		ensure!(519			collection.limits.transfers_enabled(),520			<CommonError<T>>::TransferNotAllowed521		);522523		let token_data =524			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;525		// TODO: require sender to be token, owner, require admins to go through transfer_from526		ensure!(527			&token_data.owner == from528				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),529			<CommonError<T>>::NoPermission530		);531532		if collection.permissions.access() == AccessMode::AllowList {533			collection.check_allowlist(from)?;534			collection.check_allowlist(to)?;535		}536		<PalletCommon<T>>::ensure_correct_receiver(to)?;537538		let balance_from = <AccountBalance<T>>::get((collection.id, from))539			.checked_sub(1)540			.ok_or(<CommonError<T>>::TokenValueTooLow)?;541		let balance_to = if from != to {542			let balance_to = <AccountBalance<T>>::get((collection.id, to))543				.checked_add(1)544				.ok_or(ArithmeticError::Overflow)?;545546			ensure!(547				balance_to < collection.limits.account_token_ownership_limit(),548				<CommonError<T>>::AccountTokenLimitExceeded,549			);550551			Some(balance_to)552		} else {553			None554		};555556		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {557			let handle = <CollectionHandle<T>>::try_get(target.0)?;558			let dispatch = T::CollectionDispatch::dispatch(handle);559			let dispatch = dispatch.as_dyn();560561			dispatch.check_nesting(562				from.clone(),563				(collection.id, token),564				target.1,565				nesting_budget,566			)?;567		}568569		// =========570571		<TokenData<T>>::insert(572			(collection.id, token),573			ItemData {574				owner: to.clone(),575				..token_data576			},577		);578579		if let Some(balance_to) = balance_to {580			// from != to581			if balance_from == 0 {582				<AccountBalance<T>>::remove((collection.id, from));583			} else {584				<AccountBalance<T>>::insert((collection.id, from), balance_from);585			}586			<AccountBalance<T>>::insert((collection.id, to), balance_to);587			<Owned<T>>::remove((collection.id, from, token));588			<Owned<T>>::insert((collection.id, to, token), true);589		}590		Self::set_allowance_unchecked(collection, from, token, None, true);591592		<PalletEvm<T>>::deposit_log(593			ERC721Events::Transfer {594				from: *from.as_eth(),595				to: *to.as_eth(),596				token_id: token.into(),597			}598			.to_log(collection_id_to_address(collection.id)),599		);600		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(601			collection.id,602			token,603			from.clone(),604			to.clone(),605			1,606		));607		Ok(())608	}609610	pub fn create_multiple_items(611		collection: &NonfungibleHandle<T>,612		sender: &T::CrossAccountId,613		data: Vec<CreateItemData<T>>,614		nesting_budget: &dyn Budget,615	) -> DispatchResult {616		if !collection.is_owner_or_admin(sender) {617			ensure!(618				collection.permissions.mint_mode(),619				<CommonError<T>>::PublicMintingNotAllowed620			);621			collection.check_allowlist(sender)?;622623			for item in data.iter() {624				collection.check_allowlist(&item.owner)?;625			}626		}627628		for data in data.iter() {629			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;630		}631632		let first_token = <TokensMinted<T>>::get(collection.id);633		let tokens_minted = first_token634			.checked_add(data.len() as u32)635			.ok_or(ArithmeticError::Overflow)?;636		ensure!(637			tokens_minted <= collection.limits.token_limit(),638			<CommonError<T>>::CollectionTokenLimitExceeded639		);640641		let mut balances = BTreeMap::new();642		for data in &data {643			let balance = balances644				.entry(&data.owner)645				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));646			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;647648			ensure!(649				*balance <= collection.limits.account_token_ownership_limit(),650				<CommonError<T>>::AccountTokenLimitExceeded,651			);652		}653654		for (i, data) in data.iter().enumerate() {655			let token = TokenId(first_token + i as u32 + 1);656			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {657				let handle = <CollectionHandle<T>>::try_get(target.0)?;658				let dispatch = T::CollectionDispatch::dispatch(handle);659				let dispatch = dispatch.as_dyn();660				dispatch.check_nesting(661					sender.clone(),662					(collection.id, token),663					target.1,664					nesting_budget,665				)?;666			}667		}668669		// =========670671		with_transaction(|| {672			for (i, data) in data.iter().enumerate() {673				let token = first_token + i as u32 + 1;674675				<TokenData<T>>::insert(676					(collection.id, token),677					ItemData {678						// const_data: data.const_data.clone(),679						owner: data.owner.clone(),680					},681				);682683				if let Err(e) = Self::set_token_properties(684					collection,685					sender,686					TokenId(token),687					data.properties.clone().into_inner(),688				) {689					return TransactionOutcome::Rollback(Err(e));690				}691			}692			TransactionOutcome::Commit(Ok(()))693		})?;694695		<TokensMinted<T>>::insert(collection.id, tokens_minted);696		for (account, balance) in balances {697			<AccountBalance<T>>::insert((collection.id, account), balance);698		}699		for (i, data) in data.into_iter().enumerate() {700			let token = first_token + i as u32 + 1;701			<Owned<T>>::insert((collection.id, &data.owner, token), true);702703			<PalletEvm<T>>::deposit_log(704				ERC721Events::Transfer {705					from: H160::default(),706					to: *data.owner.as_eth(),707					token_id: token.into(),708				}709				.to_log(collection_id_to_address(collection.id)),710			);711			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(712				collection.id,713				TokenId(token),714				data.owner.clone(),715				1,716			));717		}718		Ok(())719	}720721	pub fn set_allowance_unchecked(722		collection: &NonfungibleHandle<T>,723		sender: &T::CrossAccountId,724		token: TokenId,725		spender: Option<&T::CrossAccountId>,726		assume_implicit_eth: bool,727	) {728		if let Some(spender) = spender {729			let old_spender = <Allowance<T>>::get((collection.id, token));730			<Allowance<T>>::insert((collection.id, token), spender);731			// In ERC721 there is only one possible approved user of token, so we set732			// approved user to spender733			<PalletEvm<T>>::deposit_log(734				ERC721Events::Approval {735					owner: *sender.as_eth(),736					approved: *spender.as_eth(),737					token_id: token.into(),738				}739				.to_log(collection_id_to_address(collection.id)),740			);741			// In Unique chain, any token can have any amount of approved users, so we need to742			// set allowance of old owner to 0, and allowance of new owner to 1743			if old_spender.as_ref() != Some(spender) {744				if let Some(old_owner) = old_spender {745					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(746						collection.id,747						token,748						sender.clone(),749						old_owner,750						0,751					));752				}753				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(754					collection.id,755					token,756					sender.clone(),757					spender.clone(),758					1,759				));760			}761		} else {762			let old_spender = <Allowance<T>>::take((collection.id, token));763			if !assume_implicit_eth {764				// In ERC721 there is only one possible approved user of token, so we set765				// approved user to zero address766				<PalletEvm<T>>::deposit_log(767					ERC721Events::Approval {768						owner: *sender.as_eth(),769						approved: H160::default(),770						token_id: token.into(),771					}772					.to_log(collection_id_to_address(collection.id)),773				);774			}775			// In Unique chain, any token can have any amount of approved users, so we need to776			// set allowance of old owner to 0777			if let Some(old_spender) = old_spender {778				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(779					collection.id,780					token,781					sender.clone(),782					old_spender,783					0,784				));785			}786		}787	}788789	pub fn set_allowance(790		collection: &NonfungibleHandle<T>,791		sender: &T::CrossAccountId,792		token: TokenId,793		spender: Option<&T::CrossAccountId>,794	) -> DispatchResult {795		if collection.permissions.access() == AccessMode::AllowList {796			collection.check_allowlist(sender)?;797			if let Some(spender) = spender {798				collection.check_allowlist(spender)?;799			}800		}801802		if let Some(spender) = spender {803			<PalletCommon<T>>::ensure_correct_receiver(spender)?;804		}805		let token_data =806			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;807		if &token_data.owner != sender {808			ensure!(809				collection.ignores_owned_amount(sender),810				<CommonError<T>>::CantApproveMoreThanOwned811			);812		}813814		// =========815816		Self::set_allowance_unchecked(collection, sender, token, spender, false);817		Ok(())818	}819820	fn check_allowed(821		collection: &NonfungibleHandle<T>,822		spender: &T::CrossAccountId,823		from: &T::CrossAccountId,824		token: TokenId,825		nesting_budget: &dyn Budget,826	) -> DispatchResult {827		if spender.conv_eq(from) {828			return Ok(());829		}830		if collection.permissions.access() == AccessMode::AllowList {831			// `from`, `to` checked in [`transfer`]832			collection.check_allowlist(spender)?;833		}834		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {835			// TODO: should collection owner be allowed to perform this transfer?836			ensure!(837				<PalletStructure<T>>::check_indirectly_owned(838					spender.clone(),839					source.0,840					source.1,841					None,842					nesting_budget843				)?,844				<CommonError<T>>::ApprovedValueTooLow,845			);846			return Ok(());847		}848		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {849			return Ok(());850		}851		ensure!(852			collection.ignores_allowance(spender),853			<CommonError<T>>::ApprovedValueTooLow854		);855		Ok(())856	}857858	pub fn transfer_from(859		collection: &NonfungibleHandle<T>,860		spender: &T::CrossAccountId,861		from: &T::CrossAccountId,862		to: &T::CrossAccountId,863		token: TokenId,864		nesting_budget: &dyn Budget,865	) -> DispatchResult {866		Self::check_allowed(collection, spender, from, token, nesting_budget)?;867868		// =========869870		// Allowance is reset in [`transfer`]871		Self::transfer(collection, from, to, token, nesting_budget)872	}873874	pub fn burn_from(875		collection: &NonfungibleHandle<T>,876		spender: &T::CrossAccountId,877		from: &T::CrossAccountId,878		token: TokenId,879		nesting_budget: &dyn Budget,880	) -> DispatchResult {881		Self::check_allowed(collection, spender, from, token, nesting_budget)?;882883		// =========884885		Self::burn(collection, from, token)886	}887888	pub fn check_nesting(889		handle: &NonfungibleHandle<T>,890		sender: T::CrossAccountId,891		from: (CollectionId, TokenId),892		under: TokenId,893		nesting_budget: &dyn Budget,894	) -> DispatchResult {895		fn ensure_sender_allowed<T: Config>(896			collection: CollectionId,897			token: TokenId,898			for_nest: (CollectionId, TokenId),899			sender: T::CrossAccountId,900			budget: &dyn Budget,901		) -> DispatchResult {902			ensure!(903				<PalletStructure<T>>::check_indirectly_owned(904					sender,905					collection,906					token,907					Some(for_nest),908					budget909				)?,910				<CommonError<T>>::OnlyOwnerAllowedToNest,911			);912			Ok(())913		}914		match handle.permissions.nesting() {915			NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),916			NestingRule::Owner => {917				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?918			}919			NestingRule::OwnerRestricted(whitelist) => {920				ensure!(921					whitelist.contains(&from.0),922					<CommonError<T>>::SourceCollectionIsNotAllowedToNest923				);924				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?925			}926		}927		Ok(())928	}929930	/// Delegated to `create_multiple_items`931	pub fn create_item(932		collection: &NonfungibleHandle<T>,933		sender: &T::CrossAccountId,934		data: CreateItemData<T>,935		nesting_budget: &dyn Budget,936	) -> DispatchResult {937		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)938	}939}