git.delta.rocks / unique-network / refs/commits / b08cb26894de

difftreelog

Merge pull request #355 from UniqueNetwork/feature/nft-children

Yaroslav Bolyukin2022-05-30parents: #764dfd5 #8cbbfc6.patch.diff
in: master
Structure children map

12 files changed

modifiedpallets/common/src/lib.rsdiffbeforeafterboth
before · pallets/common/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#![cfg_attr(not(feature = "std"), no_std)]1819extern crate alloc;2021use core::ops::{Deref, DerefMut};22use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};23use sp_std::vec::Vec;24use pallet_evm::account::CrossAccountId;25use frame_support::{26	dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},27	ensure,28	traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},29	BoundedVec,30	weights::Pays,31	transactional,32};33use pallet_evm::GasWeightMapping;34use up_data_structs::{35	COLLECTION_NUMBER_LIMIT,36	Collection,37	RpcCollection,38	CollectionId,39	CreateItemData,40	MAX_TOKEN_PREFIX_LENGTH,41	COLLECTION_ADMINS_LIMIT,42	TokenId,43	CollectionStats,44	MAX_TOKEN_OWNERSHIP,45	CollectionMode,46	NFT_SPONSOR_TRANSFER_TIMEOUT,47	FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,48	REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,49	MAX_SPONSOR_TIMEOUT,50	CUSTOM_DATA_LIMIT,51	CollectionLimits,52	CreateCollectionData,53	SponsorshipState,54	CreateItemExData,55	SponsoringRateLimit,56	budget::Budget,57	COLLECTION_FIELD_LIMIT,58	PhantomType,59	Property,60	Properties,61	PropertiesPermissionMap,62	PropertyKey,63	PropertyValue,64	PropertyPermission,65	PropertiesError,66	PropertyKeyPermission,67	TokenData,68	TrySetProperty,69	PropertyScope,70	// RMRK71	RmrkCollectionInfo,72	RmrkInstanceInfo,73	RmrkResourceInfo,74	RmrkPropertyInfo,75	RmrkBaseInfo,76	RmrkPartType,77	RmrkTheme,78	RmrkNftChild,79	CollectionPermissions,80	SchemaVersion,81};8283pub use pallet::*;84use sp_core::H160;85use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};86#[cfg(feature = "runtime-benchmarks")]87pub mod benchmarking;88pub mod dispatch;89pub mod erc;90pub mod eth;91pub mod weights;9293pub type SelfWeightOf<T> = <T as Config>::WeightInfo;9495#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]96pub struct CollectionHandle<T: Config> {97	pub id: CollectionId,98	collection: Collection<T::AccountId>,99	pub recorder: SubstrateRecorder<T>,100}101impl<T: Config> WithRecorder<T> for CollectionHandle<T> {102	fn recorder(&self) -> &SubstrateRecorder<T> {103		&self.recorder104	}105	fn into_recorder(self) -> SubstrateRecorder<T> {106		self.recorder107	}108}109impl<T: Config> CollectionHandle<T> {110	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {111		<CollectionById<T>>::get(id).map(|collection| Self {112			id,113			collection,114			recorder: SubstrateRecorder::new(gas_limit),115		})116	}117118	pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {119		<CollectionById<T>>::get(id).map(|collection| Self {120			id,121			collection,122			recorder,123		})124	}125126	pub fn new(id: CollectionId) -> Option<Self> {127		Self::new_with_gas_limit(id, u64::MAX)128	}129	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {130		Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)131	}132	pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {133		self.recorder134			.consume_gas(T::GasWeightMapping::weight_to_gas(135				<T as frame_system::Config>::DbWeight::get()136					.read137					.saturating_mul(reads),138			))139	}140	pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {141		self.recorder142			.consume_gas(T::GasWeightMapping::weight_to_gas(143				<T as frame_system::Config>::DbWeight::get()144					.write145					.saturating_mul(writes),146			))147	}148	pub fn save(self) -> DispatchResult {149		<CollectionById<T>>::insert(self.id, self.collection);150		Ok(())151	}152153	pub fn set_sponsor(&mut self, sponsor: T::AccountId) {154		self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);155	}156157	pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> bool {158		if self.collection.sponsorship.pending_sponsor() != Some(sender) {159			return false;160		};161162		self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());163		true164	}165}166impl<T: Config> Deref for CollectionHandle<T> {167	type Target = Collection<T::AccountId>;168169	fn deref(&self) -> &Self::Target {170		&self.collection171	}172}173174impl<T: Config> DerefMut for CollectionHandle<T> {175	fn deref_mut(&mut self) -> &mut Self::Target {176		&mut self.collection177	}178}179180impl<T: Config> CollectionHandle<T> {181	pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {182		ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);183		Ok(())184	}185	pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {186		*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))187	}188	pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {189		ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);190		Ok(())191	}192	pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {193		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)194	}195	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {196		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)197	}198	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {199		ensure!(200			<Allowlist<T>>::get((self.id, user)),201			<Error<T>>::AddressNotInAllowlist202		);203		Ok(())204	}205}206207#[frame_support::pallet]208pub mod pallet {209	use super::*;210	use pallet_evm::account;211	use dispatch::CollectionDispatch;212	use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};213	use frame_system::pallet_prelude::*;214	use frame_support::traits::Currency;215	use up_data_structs::{TokenId, mapping::TokenAddressMapping};216	use scale_info::TypeInfo;217	use weights::WeightInfo;218219	#[pallet::config]220	pub trait Config:221		frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo + account::Config222	{223		type WeightInfo: WeightInfo;224		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;225226		type Currency: Currency<Self::AccountId>;227228		#[pallet::constant]229		type CollectionCreationPrice: Get<230			<<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,231		>;232		type CollectionDispatch: CollectionDispatch<Self>;233234		type TreasuryAccountId: Get<Self::AccountId>;235236		type EvmTokenAddressMapping: TokenAddressMapping<H160>;237		type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;238	}239240	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);241242	#[pallet::pallet]243	#[pallet::storage_version(STORAGE_VERSION)]244	#[pallet::generate_store(pub(super) trait Store)]245	pub struct Pallet<T>(_);246247	#[pallet::extra_constants]248	impl<T: Config> Pallet<T> {249		pub fn collection_admins_limit() -> u32 {250			COLLECTION_ADMINS_LIMIT251		}252	}253254	#[pallet::event]255	#[pallet::generate_deposit(pub fn deposit_event)]256	pub enum Event<T: Config> {257		/// New collection was created258		///259		/// # Arguments260		///261		/// * collection_id: Globally unique identifier of newly created collection.262		///263		/// * mode: [CollectionMode] converted into u8.264		///265		/// * account_id: Collection owner.266		CollectionCreated(CollectionId, u8, T::AccountId),267268		/// New collection was destroyed269		///270		/// # Arguments271		///272		/// * collection_id: Globally unique identifier of collection.273		CollectionDestroyed(CollectionId),274275		/// New item was created.276		///277		/// # Arguments278		///279		/// * collection_id: Id of the collection where item was created.280		///281		/// * item_id: Id of an item. Unique within the collection.282		///283		/// * recipient: Owner of newly created item284		///285		/// * amount: Always 1 for NFT286		ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),287288		/// Collection item was burned.289		///290		/// # Arguments291		///292		/// * collection_id.293		///294		/// * item_id: Identifier of burned NFT.295		///296		/// * owner: which user has destroyed its tokens297		///298		/// * amount: Always 1 for NFT299		ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),300301		/// Item was transferred302		///303		/// * collection_id: Id of collection to which item is belong304		///305		/// * item_id: Id of an item306		///307		/// * sender: Original owner of item308		///309		/// * recipient: New owner of item310		///311		/// * amount: Always 1 for NFT312		Transfer(313			CollectionId,314			TokenId,315			T::CrossAccountId,316			T::CrossAccountId,317			u128,318		),319320		/// * collection_id321		///322		/// * item_id323		///324		/// * sender325		///326		/// * spender327		///328		/// * amount329		Approved(330			CollectionId,331			TokenId,332			T::CrossAccountId,333			T::CrossAccountId,334			u128,335		),336337		CollectionPropertySet(CollectionId, PropertyKey),338339		CollectionPropertyDeleted(CollectionId, PropertyKey),340341		TokenPropertySet(CollectionId, TokenId, PropertyKey),342343		TokenPropertyDeleted(CollectionId, TokenId, PropertyKey),344345		PropertyPermissionSet(CollectionId, PropertyKey),346	}347348	#[pallet::error]349	pub enum Error<T> {350		/// This collection does not exist.351		CollectionNotFound,352		/// Sender parameter and item owner must be equal.353		MustBeTokenOwner,354		/// No permission to perform action355		NoPermission,356		/// Collection is not in mint mode.357		PublicMintingNotAllowed,358		/// Address is not in allow list.359		AddressNotInAllowlist,360361		/// Collection name can not be longer than 63 char.362		CollectionNameLimitExceeded,363		/// Collection description can not be longer than 255 char.364		CollectionDescriptionLimitExceeded,365		/// Token prefix can not be longer than 15 char.366		CollectionTokenPrefixLimitExceeded,367		/// Total collections bound exceeded.368		TotalCollectionsLimitExceeded,369		/// Exceeded max admin count370		CollectionAdminCountExceeded,371		/// Collection limit bounds per collection exceeded372		CollectionLimitBoundsExceeded,373		/// Tried to enable permissions which are only permitted to be disabled374		OwnerPermissionsCantBeReverted,375		/// Collection settings not allowing items transferring376		TransferNotAllowed,377		/// Account token limit exceeded per collection378		AccountTokenLimitExceeded,379		/// Collection token limit exceeded380		CollectionTokenLimitExceeded,381		/// Metadata flag frozen382		MetadataFlagFrozen,383384		/// Item not exists.385		TokenNotFound,386		/// Item balance not enough.387		TokenValueTooLow,388		/// Requested value more than approved.389		ApprovedValueTooLow,390		/// Tried to approve more than owned391		CantApproveMoreThanOwned,392393		/// Can't transfer tokens to ethereum zero address394		AddressIsZero,395		/// Target collection doesn't supports this operation396		UnsupportedOperation,397398		/// Not sufficient founds to perform action399		NotSufficientFounds,400401		/// Collection has nesting disabled402		NestingIsDisabled,403		/// Only owner may nest tokens under this collection404		OnlyOwnerAllowedToNest,405		/// Only tokens from specific collections may nest tokens under this406		SourceCollectionIsNotAllowedToNest,407408		/// Tried to store more data than allowed in collection field409		CollectionFieldSizeExceeded,410411		/// Tried to store more property data than allowed412		NoSpaceForProperty,413414		/// Tried to store more property keys than allowed415		PropertyLimitReached,416417		/// Property key is too long418		PropertyKeyIsTooLong,419420		/// Only ASCII letters, digits, and '_', '-' are allowed421		InvalidCharacterInPropertyKey,422423		/// Empty property keys are forbidden424		EmptyPropertyKey,425	}426427	#[pallet::storage]428	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;429	#[pallet::storage]430	pub type DestroyedCollectionCount<T> =431		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;432433	/// Collection info434	#[pallet::storage]435	pub type CollectionById<T> = StorageMap<436		Hasher = Blake2_128Concat,437		Key = CollectionId,438		Value = Collection<<T as frame_system::Config>::AccountId>,439		QueryKind = OptionQuery,440	>;441442	/// Collection properties443	#[pallet::storage]444	#[pallet::getter(fn collection_properties)]445	pub type CollectionProperties<T> = StorageMap<446		Hasher = Blake2_128Concat,447		Key = CollectionId,448		Value = Properties,449		QueryKind = ValueQuery,450		OnEmpty = up_data_structs::CollectionProperties,451	>;452453	#[pallet::storage]454	#[pallet::getter(fn property_permissions)]455	pub type CollectionPropertyPermissions<T> = StorageMap<456		Hasher = Blake2_128Concat,457		Key = CollectionId,458		Value = PropertiesPermissionMap,459		QueryKind = ValueQuery,460	>;461462	#[pallet::storage]463	pub type AdminAmount<T> = StorageMap<464		Hasher = Blake2_128Concat,465		Key = CollectionId,466		Value = u32,467		QueryKind = ValueQuery,468	>;469470	/// List of collection admins471	#[pallet::storage]472	pub type IsAdmin<T: Config> = StorageNMap<473		Key = (474			Key<Blake2_128Concat, CollectionId>,475			Key<Blake2_128Concat, T::CrossAccountId>,476		),477		Value = bool,478		QueryKind = ValueQuery,479	>;480481	/// Allowlisted collection users482	#[pallet::storage]483	pub type Allowlist<T: Config> = StorageNMap<484		Key = (485			Key<Blake2_128Concat, CollectionId>,486			Key<Blake2_128Concat, T::CrossAccountId>,487		),488		Value = bool,489		QueryKind = ValueQuery,490	>;491492	/// Not used by code, exists only to provide some types to metadata493	#[pallet::storage]494	pub type DummyStorageValue<T: Config> = StorageValue<495		Value = (496			CollectionStats,497			CollectionId,498			TokenId,499			PhantomType<(500				TokenData<T::CrossAccountId>,501				RpcCollection<T::AccountId>,502503				// RMRK504				RmrkCollectionInfo<T::AccountId>,505				RmrkInstanceInfo<T::AccountId>,506				RmrkResourceInfo,507				RmrkPropertyInfo,508				RmrkBaseInfo<T::AccountId>,509				RmrkPartType,510				RmrkTheme,511				RmrkNftChild,512			)>,513		),514		QueryKind = OptionQuery,515	>;516517	#[pallet::hooks]518	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {519		fn on_runtime_upgrade() -> Weight {520			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {521				use up_data_structs::{CollectionVersion1, CollectionVersion2};522				<CollectionById<T>>::translate::<CollectionVersion1<T::AccountId>, _>(|id, v| {523					let mut props = Vec::new();524					if !v.offchain_schema.is_empty() {525						props.push(Property {526							key: b"_old_offchainSchema".to_vec().try_into().unwrap(),527							value: v.offchain_schema.clone().into_inner().try_into().expect("offchain schema too big"),528						});529					}530					if !v.variable_on_chain_schema.is_empty() {531						props.push(Property {532							key: b"_old_variableOnChainSchema".to_vec().try_into().unwrap(),533							value: v.variable_on_chain_schema.clone().into_inner().try_into().expect("offchain schema too big"),534						});535					}536					if !v.const_on_chain_schema.is_empty() {537						props.push(Property {538							key: b"_old_constOnChainSchema".to_vec().try_into().unwrap(),539							value: v.const_on_chain_schema.clone().into_inner().try_into().expect("offchain schema too big"),540						});541					}542					props.push(Property {543						key: b"_old_schemaVersion".to_vec().try_into().unwrap(),544						value: match v.schema_version {545							SchemaVersion::ImageURL => b"ImageUrl".as_slice(),546							SchemaVersion::Unique => b"Unique".as_slice(),547						}.to_vec().try_into().unwrap(),548					});549					Self::set_scoped_collection_properties(550						id,551						PropertyScope::None,552						props.into_iter(),553					).expect("existing data larger than properties");554					let mut new = CollectionVersion2::from(v.clone());555					new.permissions.access = Some(v.access);556					new.permissions.mint_mode = Some(v.mint_mode);557					Some(new)558				});559			}560561			0562		}563	}564}565566impl<T: Config> Pallet<T> {567	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens568	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {569		ensure!(570			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,571			<Error<T>>::AddressIsZero572		);573		Ok(())574	}575	pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {576		<IsAdmin<T>>::iter_prefix((collection,))577			.map(|(a, _)| a)578			.collect()579	}580	pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {581		<Allowlist<T>>::iter_prefix((collection,))582			.map(|(a, _)| a)583			.collect()584	}585	pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {586		<Allowlist<T>>::get((collection, user))587	}588	pub fn collection_stats() -> CollectionStats {589		let created = <CreatedCollectionCount<T>>::get();590		let destroyed = <DestroyedCollectionCount<T>>::get();591		CollectionStats {592			created: created.0,593			destroyed: destroyed.0,594			alive: created.0 - destroyed.0,595		}596	}597598	pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {599		let collection = <CollectionById<T>>::get(collection);600		if collection.is_none() {601			return None;602		}603604		let collection = collection.unwrap();605		let limits = collection.limits;606		let effective_limits = CollectionLimits {607			account_token_ownership_limit: Some(limits.account_token_ownership_limit()),608			sponsored_data_size: Some(limits.sponsored_data_size()),609			sponsored_data_rate_limit: Some(610				limits611					.sponsored_data_rate_limit612					.unwrap_or(SponsoringRateLimit::SponsoringDisabled),613			),614			token_limit: Some(limits.token_limit()),615			sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(616				match collection.mode {617					CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,618					CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,619					CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,620				},621			)),622			sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),623			owner_can_transfer: Some(limits.owner_can_transfer()),624			owner_can_destroy: Some(limits.owner_can_destroy()),625			transfers_enabled: Some(limits.transfers_enabled()),626		};627628		Some(effective_limits)629	}630631	pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {632		let Collection {633			name,634			description,635			owner,636			mode,637			token_prefix,638			sponsorship,639			limits,640			permissions,641		} = <CollectionById<T>>::get(collection)?;642643		let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)644			.into_iter()645			.map(|(key, permission)| PropertyKeyPermission {646				key,647				permission,648			})649			.collect();650651		let properties = <CollectionProperties<T>>::get(collection)652			.into_iter()653			.map(|(key, value)| Property {654				key,655				value,656			})657			.collect();658659		let permissions = CollectionPermissions {660			access: Some(permissions.access()),661			mint_mode: Some(permissions.mint_mode()),662			nesting: Some(permissions.nesting().clone()),663		};664665		Some(RpcCollection {666			name: name.into_inner(),667			description: description.into_inner(),668			owner,669			mode,670			token_prefix: token_prefix.into_inner(),671			sponsorship,672			limits,673			permissions,674			token_property_permissions,675			properties,676		})677	}678}679680macro_rules! limit_default {681	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{682		$(683			if let Some($new) = $new.$field {684				let $old = $old.$field($($arg)?);685				let _ = $new;686				let _ = $old;687				$check688			} else {689				$new.$field = $old.$field690			}691		)*692	}};693}694macro_rules! limit_default_clone {695	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{696		$(697			if let Some($new) = $new.$field.clone() {698				let $old = $old.$field($($arg)?);699				let _ = $new;700				let _ = $old;701				$check702			} else {703				$new.$field = $old.$field.clone()704			}705		)*706	}};707}708709impl<T: Config> Pallet<T> {710	pub fn init_collection(711		owner: T::AccountId,712		data: CreateCollectionData<T::AccountId>,713	) -> Result<CollectionId, DispatchError> {714		{715			ensure!(716				data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,717				Error::<T>::CollectionTokenPrefixLimitExceeded718			);719		}720721		let created_count = <CreatedCollectionCount<T>>::get()722			.0723			.checked_add(1)724			.ok_or(ArithmeticError::Overflow)?;725		let destroyed_count = <DestroyedCollectionCount<T>>::get().0;726		let id = CollectionId(created_count);727728		// bound Total number of collections729		ensure!(730			created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,731			<Error<T>>::TotalCollectionsLimitExceeded732		);733734		// =========735736		let collection = Collection {737			owner: owner.clone(),738			name: data.name,739			mode: data.mode.clone(),740			description: data.description,741			token_prefix: data.token_prefix,742			sponsorship: data743				.pending_sponsor744				.map(SponsorshipState::Unconfirmed)745				.unwrap_or_default(),746			limits: data747				.limits748				.map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))749				.unwrap_or_else(|| Ok(CollectionLimits::default()))?,750			permissions: data751				.permissions752				.map(|permissions| Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions))753				.unwrap_or_else(|| Ok(CollectionPermissions::default()))?,754		};755756		let mut collection_properties = up_data_structs::CollectionProperties::get();757		collection_properties758			.try_set_from_iter(data.properties.into_iter())759			.map_err(<Error<T>>::from)?;760761		CollectionProperties::<T>::insert(id, collection_properties);762763		let mut token_props_permissions = PropertiesPermissionMap::new();764		token_props_permissions765			.try_set_from_iter(data.token_property_permissions.into_iter())766			.map_err(<Error<T>>::from)?;767768		CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);769770		// Take a (non-refundable) deposit of collection creation771		{772			let mut imbalance =773				<<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();774			imbalance.subsume(775				<<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(776					&T::TreasuryAccountId::get(),777					T::CollectionCreationPrice::get(),778				),779			);780			<T as Config>::Currency::settle(781				&owner,782				imbalance,783				WithdrawReasons::TRANSFER,784				ExistenceRequirement::KeepAlive,785			)786			.map_err(|_| Error::<T>::NotSufficientFounds)?;787		}788789		<CreatedCollectionCount<T>>::put(created_count);790		<Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));791		<CollectionById<T>>::insert(id, collection);792		Ok(id)793	}794795	pub fn destroy_collection(796		collection: CollectionHandle<T>,797		sender: &T::CrossAccountId,798	) -> DispatchResult {799		ensure!(800			collection.limits.owner_can_destroy(),801			<Error<T>>::NoPermission,802		);803		collection.check_is_owner(sender)?;804805		let destroyed_collections = <DestroyedCollectionCount<T>>::get()806			.0807			.checked_add(1)808			.ok_or(ArithmeticError::Overflow)?;809810		// =========811812		<DestroyedCollectionCount<T>>::put(destroyed_collections);813		<CollectionById<T>>::remove(collection.id);814		<AdminAmount<T>>::remove(collection.id);815		<IsAdmin<T>>::remove_prefix((collection.id,), None);816		<Allowlist<T>>::remove_prefix((collection.id,), None);817		<CollectionProperties<T>>::remove(collection.id);818819		<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));820		Ok(())821	}822823	pub fn set_collection_property(824		collection: &CollectionHandle<T>,825		sender: &T::CrossAccountId,826		property: Property,827	) -> DispatchResult {828		collection.check_is_owner_or_admin(sender)?;829830		CollectionProperties::<T>::try_mutate(collection.id, |properties| {831			let property = property.clone();832			properties.try_set(property.key, property.value)833		})834		.map_err(<Error<T>>::from)?;835836		Self::deposit_event(Event::CollectionPropertySet(collection.id, property.key));837838		Ok(())839	}840841	pub fn set_scoped_collection_property(842		collection_id: CollectionId,843		scope: PropertyScope,844		property: Property,845	) -> DispatchResult {846		CollectionProperties::<T>::try_mutate(collection_id, |properties| {847			properties.try_scoped_set(scope, property.key, property.value)848		})849		.map_err(<Error<T>>::from)?;850851		Ok(())852	}853854	pub fn set_scoped_collection_properties(855		collection_id: CollectionId,856		scope: PropertyScope,857		properties: impl Iterator<Item = Property>,858	) -> DispatchResult {859		CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {860			stored_properties.try_scoped_set_from_iter(scope, properties)861		})862		.map_err(<Error<T>>::from)?;863864		Ok(())865	}866867	#[transactional]868	pub fn set_collection_properties(869		collection: &CollectionHandle<T>,870		sender: &T::CrossAccountId,871		properties: Vec<Property>,872	) -> DispatchResult {873		for property in properties {874			Self::set_collection_property(collection, sender, property)?;875		}876877		Ok(())878	}879880	pub fn delete_collection_property(881		collection: &CollectionHandle<T>,882		sender: &T::CrossAccountId,883		property_key: PropertyKey,884	) -> DispatchResult {885		collection.check_is_owner_or_admin(sender)?;886887		CollectionProperties::<T>::try_mutate(collection.id, |properties| {888			properties.remove(&property_key)889		})890		.map_err(<Error<T>>::from)?;891892		Self::deposit_event(Event::CollectionPropertyDeleted(893			collection.id,894			property_key,895		));896897		Ok(())898	}899900	#[transactional]901	pub fn delete_collection_properties(902		collection: &CollectionHandle<T>,903		sender: &T::CrossAccountId,904		property_keys: Vec<PropertyKey>,905	) -> DispatchResult {906		for key in property_keys {907			Self::delete_collection_property(collection, sender, key)?;908		}909910		Ok(())911	}912913	// For migrations914	pub fn set_property_permission_unchecked(915		collection: CollectionId,916		property_permission: PropertyKeyPermission,917	) -> DispatchResult {918		<CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {919			permissions.try_set(property_permission.key, property_permission.permission)920		})921		.map_err(<Error<T>>::from)?;922		Ok(())923	}924925	pub fn set_property_permission(926		collection: &CollectionHandle<T>,927		sender: &T::CrossAccountId,928		property_permission: PropertyKeyPermission,929	) -> DispatchResult {930		collection.check_is_owner_or_admin(sender)?;931932		let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);933		let current_permission = all_permissions.get(&property_permission.key);934		if matches![935			current_permission,936			Some(PropertyPermission { mutable: false, .. })937		] {938			return Err(<Error<T>>::NoPermission.into());939		}940941		CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {942			let property_permission = property_permission.clone();943			permissions.try_set(property_permission.key, property_permission.permission)944		})945		.map_err(<Error<T>>::from)?;946947		Self::deposit_event(Event::PropertyPermissionSet(948			collection.id,949			property_permission.key,950		));951952		Ok(())953	}954955	#[transactional]956	pub fn set_property_permissions(957		collection: &CollectionHandle<T>,958		sender: &T::CrossAccountId,959		property_permissions: Vec<PropertyKeyPermission>,960	) -> DispatchResult {961		for prop_pemission in property_permissions {962			Self::set_property_permission(collection, sender, prop_pemission)?;963		}964965		Ok(())966	}967968	pub fn get_collection_property(969		collection_id: CollectionId,970		key: &PropertyKey,971	) -> Option<PropertyValue> {972		Self::collection_properties(collection_id).get(key).cloned()973	}974975	pub fn bytes_keys_to_property_keys(976		keys: Vec<Vec<u8>>,977	) -> Result<Vec<PropertyKey>, DispatchError> {978		keys.into_iter()979			.map(|key| -> Result<PropertyKey, DispatchError> {980				key.try_into()981					.map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())982			})983			.collect::<Result<Vec<PropertyKey>, DispatchError>>()984	}985986	pub fn filter_collection_properties(987		collection_id: CollectionId,988		keys: Option<Vec<PropertyKey>>,989	) -> Result<Vec<Property>, DispatchError> {990		let properties = Self::collection_properties(collection_id);991992		let properties = keys993			.map(|keys| {994				keys.into_iter()995					.filter_map(|key| {996						properties.get(&key).map(|value| Property {997							key,998							value: value.clone(),999						})1000					})1001					.collect()1002			})1003			.unwrap_or_else(|| {1004				properties1005					.into_iter()1006					.map(|(key, value)| Property {1007						key,1008						value,1009					})1010					.collect()1011			});10121013		Ok(properties)1014	}10151016	pub fn filter_property_permissions(1017		collection_id: CollectionId,1018		keys: Option<Vec<PropertyKey>>,1019	) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1020		let permissions = Self::property_permissions(collection_id);10211022		let key_permissions = keys1023			.map(|keys| {1024				keys.into_iter()1025					.filter_map(|key| {1026						permissions1027							.get(&key)1028							.map(|permission| PropertyKeyPermission {1029								key,1030								permission: permission.clone(),1031							})1032					})1033					.collect()1034			})1035			.unwrap_or_else(|| {1036				permissions1037					.into_iter()1038					.map(|(key, permission)| PropertyKeyPermission {1039						key,1040						permission,1041					})1042					.collect()1043			});10441045		Ok(key_permissions)1046	}10471048	pub fn toggle_allowlist(1049		collection: &CollectionHandle<T>,1050		sender: &T::CrossAccountId,1051		user: &T::CrossAccountId,1052		allowed: bool,1053	) -> DispatchResult {1054		collection.check_is_owner_or_admin(sender)?;10551056		// =========10571058		if allowed {1059			<Allowlist<T>>::insert((collection.id, user), true);1060		} else {1061			<Allowlist<T>>::remove((collection.id, user));1062		}10631064		Ok(())1065	}10661067	pub fn toggle_admin(1068		collection: &CollectionHandle<T>,1069		sender: &T::CrossAccountId,1070		user: &T::CrossAccountId,1071		admin: bool,1072	) -> DispatchResult {1073		collection.check_is_owner_or_admin(sender)?;10741075		let was_admin = <IsAdmin<T>>::get((collection.id, user));1076		if was_admin == admin {1077			return Ok(());1078		}1079		let amount = <AdminAmount<T>>::get(collection.id);10801081		if admin {1082			let amount = amount1083				.checked_add(1)1084				.ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1085			ensure!(1086				amount <= Self::collection_admins_limit(),1087				<Error<T>>::CollectionAdminCountExceeded,1088			);10891090			// =========10911092			<AdminAmount<T>>::insert(collection.id, amount);1093			<IsAdmin<T>>::insert((collection.id, user), true);1094		} else {1095			<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1096			<IsAdmin<T>>::remove((collection.id, user));1097		}10981099		Ok(())1100	}11011102	pub fn clamp_limits(1103		mode: CollectionMode,1104		old_limit: &CollectionLimits,1105		mut new_limit: CollectionLimits,1106	) -> Result<CollectionLimits, DispatchError> {1107		limit_default!(old_limit, new_limit,1108			account_token_ownership_limit => ensure!(1109				new_limit <= MAX_TOKEN_OWNERSHIP,1110				<Error<T>>::CollectionLimitBoundsExceeded,1111			),1112			sponsor_transfer_timeout(match mode {1113				CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1114				CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1115				CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1116			}) => ensure!(1117				new_limit <= MAX_SPONSOR_TIMEOUT,1118				<Error<T>>::CollectionLimitBoundsExceeded,1119			),1120			sponsored_data_size => ensure!(1121				new_limit <= CUSTOM_DATA_LIMIT,1122				<Error<T>>::CollectionLimitBoundsExceeded,1123			),1124			token_limit => ensure!(1125				old_limit >= new_limit && new_limit > 0,1126				<Error<T>>::CollectionTokenLimitExceeded1127			),1128			owner_can_transfer => ensure!(1129				old_limit || !new_limit,1130				<Error<T>>::OwnerPermissionsCantBeReverted,1131			),1132			owner_can_destroy => ensure!(1133				old_limit || !new_limit,1134				<Error<T>>::OwnerPermissionsCantBeReverted,1135			),1136			sponsored_data_rate_limit => {},1137			transfers_enabled => {},1138		);1139		Ok(new_limit)1140	}1141	pub fn clamp_permissions(1142		mode: CollectionMode,1143		old_limit: &CollectionPermissions,1144		mut new_limit: CollectionPermissions,1145	) -> Result<CollectionPermissions, DispatchError> {1146		limit_default_clone!(old_limit, new_limit,1147		);1148		Ok(new_limit)1149	}1150}11511152#[macro_export]1153macro_rules! unsupported {1154	() => {1155		Err(<Error<T>>::UnsupportedOperation.into())1156	};1157}11581159/// Worst cases1160pub trait CommonWeightInfo<CrossAccountId> {1161	fn create_item() -> Weight;1162	fn create_multiple_items(amount: &[CreateItemData]) -> Weight;1163	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;1164	fn burn_item() -> Weight;1165	fn set_collection_properties(amount: u32) -> Weight;1166	fn delete_collection_properties(amount: u32) -> Weight;1167	fn set_token_properties(amount: u32) -> Weight;1168	fn delete_token_properties(amount: u32) -> Weight;1169	fn set_property_permissions(amount: u32) -> Weight;1170	fn transfer() -> Weight;1171	fn approve() -> Weight;1172	fn transfer_from() -> Weight;1173	fn burn_from() -> Weight;1174}11751176pub trait CommonCollectionOperations<T: Config> {1177	fn create_item(1178		&self,1179		sender: T::CrossAccountId,1180		to: T::CrossAccountId,1181		data: CreateItemData,1182		nesting_budget: &dyn Budget,1183	) -> DispatchResultWithPostInfo;1184	fn create_multiple_items(1185		&self,1186		sender: T::CrossAccountId,1187		to: T::CrossAccountId,1188		data: Vec<CreateItemData>,1189		nesting_budget: &dyn Budget,1190	) -> DispatchResultWithPostInfo;1191	fn create_multiple_items_ex(1192		&self,1193		sender: T::CrossAccountId,1194		data: CreateItemExData<T::CrossAccountId>,1195		nesting_budget: &dyn Budget,1196	) -> DispatchResultWithPostInfo;1197	fn burn_item(1198		&self,1199		sender: T::CrossAccountId,1200		token: TokenId,1201		amount: u128,1202	) -> DispatchResultWithPostInfo;1203	fn set_collection_properties(1204		&self,1205		sender: T::CrossAccountId,1206		properties: Vec<Property>,1207	) -> DispatchResultWithPostInfo;1208	fn delete_collection_properties(1209		&self,1210		sender: &T::CrossAccountId,1211		property_keys: Vec<PropertyKey>,1212	) -> DispatchResultWithPostInfo;1213	fn set_token_properties(1214		&self,1215		sender: T::CrossAccountId,1216		token_id: TokenId,1217		property: Vec<Property>,1218	) -> DispatchResultWithPostInfo;1219	fn delete_token_properties(1220		&self,1221		sender: T::CrossAccountId,1222		token_id: TokenId,1223		property_keys: Vec<PropertyKey>,1224	) -> DispatchResultWithPostInfo;1225	fn set_property_permissions(1226		&self,1227		sender: &T::CrossAccountId,1228		property_permissions: Vec<PropertyKeyPermission>,1229	) -> DispatchResultWithPostInfo;1230	fn transfer(1231		&self,1232		sender: T::CrossAccountId,1233		to: T::CrossAccountId,1234		token: TokenId,1235		amount: u128,1236		nesting_budget: &dyn Budget,1237	) -> DispatchResultWithPostInfo;1238	fn approve(1239		&self,1240		sender: T::CrossAccountId,1241		spender: T::CrossAccountId,1242		token: TokenId,1243		amount: u128,1244	) -> DispatchResultWithPostInfo;1245	fn transfer_from(1246		&self,1247		sender: T::CrossAccountId,1248		from: T::CrossAccountId,1249		to: T::CrossAccountId,1250		token: TokenId,1251		amount: u128,1252		nesting_budget: &dyn Budget,1253	) -> DispatchResultWithPostInfo;1254	fn burn_from(1255		&self,1256		sender: T::CrossAccountId,1257		from: T::CrossAccountId,1258		token: TokenId,1259		amount: u128,1260		nesting_budget: &dyn Budget,1261	) -> DispatchResultWithPostInfo;12621263	fn check_nesting(1264		&self,1265		sender: T::CrossAccountId,1266		from: (CollectionId, TokenId),1267		under: TokenId,1268		budget: &dyn Budget,1269	) -> DispatchResult;12701271	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;1272	fn collection_tokens(&self) -> Vec<TokenId>;1273	fn token_exists(&self, token: TokenId) -> bool;1274	fn last_token_id(&self) -> TokenId;12751276	fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;1277	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;1278	fn token_properties(&self, token_id: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;1279	/// Amount of unique collection tokens1280	fn total_supply(&self) -> u32;1281	/// Amount of different tokens account has (Applicable to nonfungible/refungible)1282	fn account_balance(&self, account: T::CrossAccountId) -> u32;1283	/// Amount of specific token account have (Applicable to fungible/refungible)1284	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;1285	fn allowance(1286		&self,1287		sender: T::CrossAccountId,1288		spender: T::CrossAccountId,1289		token: TokenId,1290	) -> u128;1291}12921293// Flexible enough for implementing CommonCollectionOperations1294pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {1295	let post_info = PostDispatchInfo {1296		actual_weight: Some(weight),1297		pays_fee: Pays::Yes,1298	};1299	match res {1300		Ok(()) => Ok(post_info),1301		Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),1302	}1303}13041305impl<T: Config> From<PropertiesError> for Error<T> {1306	fn from(error: PropertiesError) -> Self {1307		match error {1308			PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,1309			PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,1310			PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,1311			PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,1312			PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,1313		}1314	}1315}
after · pallets/common/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#![cfg_attr(not(feature = "std"), no_std)]1819extern crate alloc;2021use core::ops::{Deref, DerefMut};22use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};23use sp_std::vec::Vec;24use pallet_evm::account::CrossAccountId;25use frame_support::{26	dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},27	ensure,28	traits::{Imbalance, Get, Currency, WithdrawReasons, ExistenceRequirement},29	BoundedVec,30	weights::Pays,31	transactional,32};33use pallet_evm::GasWeightMapping;34use up_data_structs::{35	COLLECTION_NUMBER_LIMIT,36	Collection,37	RpcCollection,38	CollectionId,39	CreateItemData,40	MAX_TOKEN_PREFIX_LENGTH,41	COLLECTION_ADMINS_LIMIT,42	TokenId,43	CollectionStats,44	MAX_TOKEN_OWNERSHIP,45	CollectionMode,46	NFT_SPONSOR_TRANSFER_TIMEOUT,47	FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,48	REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,49	MAX_SPONSOR_TIMEOUT,50	CUSTOM_DATA_LIMIT,51	CollectionLimits,52	CreateCollectionData,53	SponsorshipState,54	CreateItemExData,55	SponsoringRateLimit,56	budget::Budget,57	COLLECTION_FIELD_LIMIT,58	PhantomType,59	Property,60	Properties,61	PropertiesPermissionMap,62	PropertyKey,63	PropertyValue,64	PropertyPermission,65	PropertiesError,66	PropertyKeyPermission,67	TokenData,68	TrySetProperty,69	PropertyScope,70	// RMRK71	RmrkCollectionInfo,72	RmrkInstanceInfo,73	RmrkResourceInfo,74	RmrkPropertyInfo,75	RmrkBaseInfo,76	RmrkPartType,77	RmrkTheme,78	RmrkNftChild,79	CollectionPermissions,80	SchemaVersion,81};8283pub use pallet::*;84use sp_core::H160;85use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};86#[cfg(feature = "runtime-benchmarks")]87pub mod benchmarking;88pub mod dispatch;89pub mod erc;90pub mod eth;91pub mod weights;9293pub type SelfWeightOf<T> = <T as Config>::WeightInfo;9495#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]96pub struct CollectionHandle<T: Config> {97	pub id: CollectionId,98	collection: Collection<T::AccountId>,99	pub recorder: SubstrateRecorder<T>,100}101impl<T: Config> WithRecorder<T> for CollectionHandle<T> {102	fn recorder(&self) -> &SubstrateRecorder<T> {103		&self.recorder104	}105	fn into_recorder(self) -> SubstrateRecorder<T> {106		self.recorder107	}108}109impl<T: Config> CollectionHandle<T> {110	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {111		<CollectionById<T>>::get(id).map(|collection| Self {112			id,113			collection,114			recorder: SubstrateRecorder::new(gas_limit),115		})116	}117118	pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {119		<CollectionById<T>>::get(id).map(|collection| Self {120			id,121			collection,122			recorder,123		})124	}125126	pub fn new(id: CollectionId) -> Option<Self> {127		Self::new_with_gas_limit(id, u64::MAX)128	}129	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {130		Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)131	}132	pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {133		self.recorder134			.consume_gas(T::GasWeightMapping::weight_to_gas(135				<T as frame_system::Config>::DbWeight::get()136					.read137					.saturating_mul(reads),138			))139	}140	pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {141		self.recorder142			.consume_gas(T::GasWeightMapping::weight_to_gas(143				<T as frame_system::Config>::DbWeight::get()144					.write145					.saturating_mul(writes),146			))147	}148	pub fn save(self) -> DispatchResult {149		<CollectionById<T>>::insert(self.id, self.collection);150		Ok(())151	}152153	pub fn set_sponsor(&mut self, sponsor: T::AccountId) {154		self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);155	}156157	pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> bool {158		if self.collection.sponsorship.pending_sponsor() != Some(sender) {159			return false;160		};161162		self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());163		true164	}165}166impl<T: Config> Deref for CollectionHandle<T> {167	type Target = Collection<T::AccountId>;168169	fn deref(&self) -> &Self::Target {170		&self.collection171	}172}173174impl<T: Config> DerefMut for CollectionHandle<T> {175	fn deref_mut(&mut self) -> &mut Self::Target {176		&mut self.collection177	}178}179180impl<T: Config> CollectionHandle<T> {181	pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {182		ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);183		Ok(())184	}185	pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {186		*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))187	}188	pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {189		ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);190		Ok(())191	}192	pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {193		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)194	}195	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {196		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)197	}198	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {199		ensure!(200			<Allowlist<T>>::get((self.id, user)),201			<Error<T>>::AddressNotInAllowlist202		);203		Ok(())204	}205}206207#[frame_support::pallet]208pub mod pallet {209	use super::*;210	use pallet_evm::account;211	use dispatch::CollectionDispatch;212	use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};213	use frame_system::pallet_prelude::*;214	use frame_support::traits::Currency;215	use up_data_structs::{TokenId, mapping::TokenAddressMapping};216	use scale_info::TypeInfo;217	use weights::WeightInfo;218219	#[pallet::config]220	pub trait Config:221		frame_system::Config + pallet_evm_coder_substrate::Config + TypeInfo + account::Config222	{223		type WeightInfo: WeightInfo;224		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;225226		type Currency: Currency<Self::AccountId>;227228		#[pallet::constant]229		type CollectionCreationPrice: Get<230			<<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,231		>;232		type CollectionDispatch: CollectionDispatch<Self>;233234		type TreasuryAccountId: Get<Self::AccountId>;235236		type EvmTokenAddressMapping: TokenAddressMapping<H160>;237		type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;238	}239240	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);241242	#[pallet::pallet]243	#[pallet::storage_version(STORAGE_VERSION)]244	#[pallet::generate_store(pub(super) trait Store)]245	pub struct Pallet<T>(_);246247	#[pallet::extra_constants]248	impl<T: Config> Pallet<T> {249		pub fn collection_admins_limit() -> u32 {250			COLLECTION_ADMINS_LIMIT251		}252	}253254	#[pallet::event]255	#[pallet::generate_deposit(pub fn deposit_event)]256	pub enum Event<T: Config> {257		/// New collection was created258		///259		/// # Arguments260		///261		/// * collection_id: Globally unique identifier of newly created collection.262		///263		/// * mode: [CollectionMode] converted into u8.264		///265		/// * account_id: Collection owner.266		CollectionCreated(CollectionId, u8, T::AccountId),267268		/// New collection was destroyed269		///270		/// # Arguments271		///272		/// * collection_id: Globally unique identifier of collection.273		CollectionDestroyed(CollectionId),274275		/// New item was created.276		///277		/// # Arguments278		///279		/// * collection_id: Id of the collection where item was created.280		///281		/// * item_id: Id of an item. Unique within the collection.282		///283		/// * recipient: Owner of newly created item284		///285		/// * amount: Always 1 for NFT286		ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),287288		/// Collection item was burned.289		///290		/// # Arguments291		///292		/// * collection_id.293		///294		/// * item_id: Identifier of burned NFT.295		///296		/// * owner: which user has destroyed its tokens297		///298		/// * amount: Always 1 for NFT299		ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),300301		/// Item was transferred302		///303		/// * collection_id: Id of collection to which item is belong304		///305		/// * item_id: Id of an item306		///307		/// * sender: Original owner of item308		///309		/// * recipient: New owner of item310		///311		/// * amount: Always 1 for NFT312		Transfer(313			CollectionId,314			TokenId,315			T::CrossAccountId,316			T::CrossAccountId,317			u128,318		),319320		/// * collection_id321		///322		/// * item_id323		///324		/// * sender325		///326		/// * spender327		///328		/// * amount329		Approved(330			CollectionId,331			TokenId,332			T::CrossAccountId,333			T::CrossAccountId,334			u128,335		),336337		CollectionPropertySet(CollectionId, PropertyKey),338339		CollectionPropertyDeleted(CollectionId, PropertyKey),340341		TokenPropertySet(CollectionId, TokenId, PropertyKey),342343		TokenPropertyDeleted(CollectionId, TokenId, PropertyKey),344345		PropertyPermissionSet(CollectionId, PropertyKey),346	}347348	#[pallet::error]349	pub enum Error<T> {350		/// This collection does not exist.351		CollectionNotFound,352		/// Sender parameter and item owner must be equal.353		MustBeTokenOwner,354		/// No permission to perform action355		NoPermission,356		/// Destroying only empty collections is allowed357		CantDestroyNotEmptyCollection,358		/// Collection is not in mint mode.359		PublicMintingNotAllowed,360		/// Address is not in allow list.361		AddressNotInAllowlist,362363		/// Collection name can not be longer than 63 char.364		CollectionNameLimitExceeded,365		/// Collection description can not be longer than 255 char.366		CollectionDescriptionLimitExceeded,367		/// Token prefix can not be longer than 15 char.368		CollectionTokenPrefixLimitExceeded,369		/// Total collections bound exceeded.370		TotalCollectionsLimitExceeded,371		/// Exceeded max admin count372		CollectionAdminCountExceeded,373		/// Collection limit bounds per collection exceeded374		CollectionLimitBoundsExceeded,375		/// Tried to enable permissions which are only permitted to be disabled376		OwnerPermissionsCantBeReverted,377		/// Collection settings not allowing items transferring378		TransferNotAllowed,379		/// Account token limit exceeded per collection380		AccountTokenLimitExceeded,381		/// Collection token limit exceeded382		CollectionTokenLimitExceeded,383		/// Metadata flag frozen384		MetadataFlagFrozen,385386		/// Item not exists.387		TokenNotFound,388		/// Item balance not enough.389		TokenValueTooLow,390		/// Requested value more than approved.391		ApprovedValueTooLow,392		/// Tried to approve more than owned393		CantApproveMoreThanOwned,394395		/// Can't transfer tokens to ethereum zero address396		AddressIsZero,397		/// Target collection doesn't supports this operation398		UnsupportedOperation,399400		/// Not sufficient founds to perform action401		NotSufficientFounds,402403		/// Collection has nesting disabled404		NestingIsDisabled,405		/// Only owner may nest tokens under this collection406		OnlyOwnerAllowedToNest,407		/// Only tokens from specific collections may nest tokens under this408		SourceCollectionIsNotAllowedToNest,409410		/// Tried to store more data than allowed in collection field411		CollectionFieldSizeExceeded,412413		/// Tried to store more property data than allowed414		NoSpaceForProperty,415416		/// Tried to store more property keys than allowed417		PropertyLimitReached,418419		/// Property key is too long420		PropertyKeyIsTooLong,421422		/// Only ASCII letters, digits, and '_', '-' are allowed423		InvalidCharacterInPropertyKey,424425		/// Empty property keys are forbidden426		EmptyPropertyKey,427	}428429	#[pallet::storage]430	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;431	#[pallet::storage]432	pub type DestroyedCollectionCount<T> =433		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;434435	/// Collection info436	#[pallet::storage]437	pub type CollectionById<T> = StorageMap<438		Hasher = Blake2_128Concat,439		Key = CollectionId,440		Value = Collection<<T as frame_system::Config>::AccountId>,441		QueryKind = OptionQuery,442	>;443444	/// Collection properties445	#[pallet::storage]446	#[pallet::getter(fn collection_properties)]447	pub type CollectionProperties<T> = StorageMap<448		Hasher = Blake2_128Concat,449		Key = CollectionId,450		Value = Properties,451		QueryKind = ValueQuery,452		OnEmpty = up_data_structs::CollectionProperties,453	>;454455	#[pallet::storage]456	#[pallet::getter(fn property_permissions)]457	pub type CollectionPropertyPermissions<T> = StorageMap<458		Hasher = Blake2_128Concat,459		Key = CollectionId,460		Value = PropertiesPermissionMap,461		QueryKind = ValueQuery,462	>;463464	#[pallet::storage]465	pub type AdminAmount<T> = StorageMap<466		Hasher = Blake2_128Concat,467		Key = CollectionId,468		Value = u32,469		QueryKind = ValueQuery,470	>;471472	/// List of collection admins473	#[pallet::storage]474	pub type IsAdmin<T: Config> = StorageNMap<475		Key = (476			Key<Blake2_128Concat, CollectionId>,477			Key<Blake2_128Concat, T::CrossAccountId>,478		),479		Value = bool,480		QueryKind = ValueQuery,481	>;482483	/// Allowlisted collection users484	#[pallet::storage]485	pub type Allowlist<T: Config> = StorageNMap<486		Key = (487			Key<Blake2_128Concat, CollectionId>,488			Key<Blake2_128Concat, T::CrossAccountId>,489		),490		Value = bool,491		QueryKind = ValueQuery,492	>;493494	/// Not used by code, exists only to provide some types to metadata495	#[pallet::storage]496	pub type DummyStorageValue<T: Config> = StorageValue<497		Value = (498			CollectionStats,499			CollectionId,500			TokenId,501			PhantomType<(502				TokenData<T::CrossAccountId>,503				RpcCollection<T::AccountId>,504505				// RMRK506				RmrkCollectionInfo<T::AccountId>,507				RmrkInstanceInfo<T::AccountId>,508				RmrkResourceInfo,509				RmrkPropertyInfo,510				RmrkBaseInfo<T::AccountId>,511				RmrkPartType,512				RmrkTheme,513				RmrkNftChild,514			)>,515		),516		QueryKind = OptionQuery,517	>;518519	#[pallet::hooks]520	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {521		fn on_runtime_upgrade() -> Weight {522			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {523				use up_data_structs::{CollectionVersion1, CollectionVersion2};524				<CollectionById<T>>::translate::<CollectionVersion1<T::AccountId>, _>(|id, v| {525					let mut props = Vec::new();526					if !v.offchain_schema.is_empty() {527						props.push(Property {528							key: b"_old_offchainSchema".to_vec().try_into().unwrap(),529							value: v.offchain_schema.clone().into_inner().try_into().expect("offchain schema too big"),530						});531					}532					if !v.variable_on_chain_schema.is_empty() {533						props.push(Property {534							key: b"_old_variableOnChainSchema".to_vec().try_into().unwrap(),535							value: v.variable_on_chain_schema.clone().into_inner().try_into().expect("offchain schema too big"),536						});537					}538					if !v.const_on_chain_schema.is_empty() {539						props.push(Property {540							key: b"_old_constOnChainSchema".to_vec().try_into().unwrap(),541							value: v.const_on_chain_schema.clone().into_inner().try_into().expect("offchain schema too big"),542						});543					}544					props.push(Property {545						key: b"_old_schemaVersion".to_vec().try_into().unwrap(),546						value: match v.schema_version {547							SchemaVersion::ImageURL => b"ImageUrl".as_slice(),548							SchemaVersion::Unique => b"Unique".as_slice(),549						}.to_vec().try_into().unwrap(),550					});551					Self::set_scoped_collection_properties(552						id,553						PropertyScope::None,554						props.into_iter(),555					).expect("existing data larger than properties");556					let mut new = CollectionVersion2::from(v.clone());557					new.permissions.access = Some(v.access);558					new.permissions.mint_mode = Some(v.mint_mode);559					Some(new)560				});561			}562563			0564		}565	}566}567568impl<T: Config> Pallet<T> {569	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens570	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {571		ensure!(572			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,573			<Error<T>>::AddressIsZero574		);575		Ok(())576	}577	pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {578		<IsAdmin<T>>::iter_prefix((collection,))579			.map(|(a, _)| a)580			.collect()581	}582	pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {583		<Allowlist<T>>::iter_prefix((collection,))584			.map(|(a, _)| a)585			.collect()586	}587	pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {588		<Allowlist<T>>::get((collection, user))589	}590	pub fn collection_stats() -> CollectionStats {591		let created = <CreatedCollectionCount<T>>::get();592		let destroyed = <DestroyedCollectionCount<T>>::get();593		CollectionStats {594			created: created.0,595			destroyed: destroyed.0,596			alive: created.0 - destroyed.0,597		}598	}599600	pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {601		let collection = <CollectionById<T>>::get(collection);602		if collection.is_none() {603			return None;604		}605606		let collection = collection.unwrap();607		let limits = collection.limits;608		let effective_limits = CollectionLimits {609			account_token_ownership_limit: Some(limits.account_token_ownership_limit()),610			sponsored_data_size: Some(limits.sponsored_data_size()),611			sponsored_data_rate_limit: Some(612				limits613					.sponsored_data_rate_limit614					.unwrap_or(SponsoringRateLimit::SponsoringDisabled),615			),616			token_limit: Some(limits.token_limit()),617			sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(618				match collection.mode {619					CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,620					CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,621					CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,622				},623			)),624			sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),625			owner_can_transfer: Some(limits.owner_can_transfer()),626			owner_can_destroy: Some(limits.owner_can_destroy()),627			transfers_enabled: Some(limits.transfers_enabled()),628		};629630		Some(effective_limits)631	}632633	pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {634		let Collection {635			name,636			description,637			owner,638			mode,639			token_prefix,640			sponsorship,641			limits,642			permissions,643		} = <CollectionById<T>>::get(collection)?;644645		let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)646			.into_iter()647			.map(|(key, permission)| PropertyKeyPermission {648				key,649				permission,650			})651			.collect();652653		let properties = <CollectionProperties<T>>::get(collection)654			.into_iter()655			.map(|(key, value)| Property {656				key,657				value,658			})659			.collect();660661		let permissions = CollectionPermissions {662			access: Some(permissions.access()),663			mint_mode: Some(permissions.mint_mode()),664			nesting: Some(permissions.nesting().clone()),665		};666667		Some(RpcCollection {668			name: name.into_inner(),669			description: description.into_inner(),670			owner,671			mode,672			token_prefix: token_prefix.into_inner(),673			sponsorship,674			limits,675			permissions,676			token_property_permissions,677			properties,678		})679	}680}681682macro_rules! limit_default {683	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{684		$(685			if let Some($new) = $new.$field {686				let $old = $old.$field($($arg)?);687				let _ = $new;688				let _ = $old;689				$check690			} else {691				$new.$field = $old.$field692			}693		)*694	}};695}696macro_rules! limit_default_clone {697	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{698		$(699			if let Some($new) = $new.$field.clone() {700				let $old = $old.$field($($arg)?);701				let _ = $new;702				let _ = $old;703				$check704			} else {705				$new.$field = $old.$field.clone()706			}707		)*708	}};709}710711impl<T: Config> Pallet<T> {712	pub fn init_collection(713		owner: T::AccountId,714		data: CreateCollectionData<T::AccountId>,715	) -> Result<CollectionId, DispatchError> {716		{717			ensure!(718				data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,719				Error::<T>::CollectionTokenPrefixLimitExceeded720			);721		}722723		let created_count = <CreatedCollectionCount<T>>::get()724			.0725			.checked_add(1)726			.ok_or(ArithmeticError::Overflow)?;727		let destroyed_count = <DestroyedCollectionCount<T>>::get().0;728		let id = CollectionId(created_count);729730		// bound Total number of collections731		ensure!(732			created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,733			<Error<T>>::TotalCollectionsLimitExceeded734		);735736		// =========737738		let collection = Collection {739			owner: owner.clone(),740			name: data.name,741			mode: data.mode.clone(),742			description: data.description,743			token_prefix: data.token_prefix,744			sponsorship: data745				.pending_sponsor746				.map(SponsorshipState::Unconfirmed)747				.unwrap_or_default(),748			limits: data749				.limits750				.map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))751				.unwrap_or_else(|| Ok(CollectionLimits::default()))?,752			permissions: data753				.permissions754				.map(|permissions| Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions))755				.unwrap_or_else(|| Ok(CollectionPermissions::default()))?,756		};757758		let mut collection_properties = up_data_structs::CollectionProperties::get();759		collection_properties760			.try_set_from_iter(data.properties.into_iter())761			.map_err(<Error<T>>::from)?;762763		CollectionProperties::<T>::insert(id, collection_properties);764765		let mut token_props_permissions = PropertiesPermissionMap::new();766		token_props_permissions767			.try_set_from_iter(data.token_property_permissions.into_iter())768			.map_err(<Error<T>>::from)?;769770		CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);771772		// Take a (non-refundable) deposit of collection creation773		{774			let mut imbalance =775				<<<T as Config>::Currency as Currency<T::AccountId>>::PositiveImbalance>::zero();776			imbalance.subsume(777				<<T as Config>::Currency as Currency<T::AccountId>>::deposit_creating(778					&T::TreasuryAccountId::get(),779					T::CollectionCreationPrice::get(),780				),781			);782			<T as Config>::Currency::settle(783				&owner,784				imbalance,785				WithdrawReasons::TRANSFER,786				ExistenceRequirement::KeepAlive,787			)788			.map_err(|_| Error::<T>::NotSufficientFounds)?;789		}790791		<CreatedCollectionCount<T>>::put(created_count);792		<Pallet<T>>::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone()));793		<CollectionById<T>>::insert(id, collection);794		Ok(id)795	}796797	pub fn destroy_collection(798		collection: CollectionHandle<T>,799		sender: &T::CrossAccountId,800	) -> DispatchResult {801		ensure!(802			collection.limits.owner_can_destroy(),803			<Error<T>>::NoPermission,804		);805		collection.check_is_owner(sender)?;806807		let destroyed_collections = <DestroyedCollectionCount<T>>::get()808			.0809			.checked_add(1)810			.ok_or(ArithmeticError::Overflow)?;811812		// =========813814		<DestroyedCollectionCount<T>>::put(destroyed_collections);815		<CollectionById<T>>::remove(collection.id);816		<AdminAmount<T>>::remove(collection.id);817		<IsAdmin<T>>::remove_prefix((collection.id,), None);818		<Allowlist<T>>::remove_prefix((collection.id,), None);819		<CollectionProperties<T>>::remove(collection.id);820821		<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));822		Ok(())823	}824825	pub fn set_collection_property(826		collection: &CollectionHandle<T>,827		sender: &T::CrossAccountId,828		property: Property,829	) -> DispatchResult {830		collection.check_is_owner_or_admin(sender)?;831832		CollectionProperties::<T>::try_mutate(collection.id, |properties| {833			let property = property.clone();834			properties.try_set(property.key, property.value)835		})836		.map_err(<Error<T>>::from)?;837838		Self::deposit_event(Event::CollectionPropertySet(collection.id, property.key));839840		Ok(())841	}842843	pub fn set_scoped_collection_property(844		collection_id: CollectionId,845		scope: PropertyScope,846		property: Property,847	) -> DispatchResult {848		CollectionProperties::<T>::try_mutate(collection_id, |properties| {849			properties.try_scoped_set(scope, property.key, property.value)850		})851		.map_err(<Error<T>>::from)?;852853		Ok(())854	}855856	pub fn set_scoped_collection_properties(857		collection_id: CollectionId,858		scope: PropertyScope,859		properties: impl Iterator<Item = Property>,860	) -> DispatchResult {861		CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {862			stored_properties.try_scoped_set_from_iter(scope, properties)863		})864		.map_err(<Error<T>>::from)?;865866		Ok(())867	}868869	#[transactional]870	pub fn set_collection_properties(871		collection: &CollectionHandle<T>,872		sender: &T::CrossAccountId,873		properties: Vec<Property>,874	) -> DispatchResult {875		for property in properties {876			Self::set_collection_property(collection, sender, property)?;877		}878879		Ok(())880	}881882	pub fn delete_collection_property(883		collection: &CollectionHandle<T>,884		sender: &T::CrossAccountId,885		property_key: PropertyKey,886	) -> DispatchResult {887		collection.check_is_owner_or_admin(sender)?;888889		CollectionProperties::<T>::try_mutate(collection.id, |properties| {890			properties.remove(&property_key)891		})892		.map_err(<Error<T>>::from)?;893894		Self::deposit_event(Event::CollectionPropertyDeleted(895			collection.id,896			property_key,897		));898899		Ok(())900	}901902	#[transactional]903	pub fn delete_collection_properties(904		collection: &CollectionHandle<T>,905		sender: &T::CrossAccountId,906		property_keys: Vec<PropertyKey>,907	) -> DispatchResult {908		for key in property_keys {909			Self::delete_collection_property(collection, sender, key)?;910		}911912		Ok(())913	}914915	// For migrations916	pub fn set_property_permission_unchecked(917		collection: CollectionId,918		property_permission: PropertyKeyPermission,919	) -> DispatchResult {920		<CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {921			permissions.try_set(property_permission.key, property_permission.permission)922		})923		.map_err(<Error<T>>::from)?;924		Ok(())925	}926927	pub fn set_property_permission(928		collection: &CollectionHandle<T>,929		sender: &T::CrossAccountId,930		property_permission: PropertyKeyPermission,931	) -> DispatchResult {932		collection.check_is_owner_or_admin(sender)?;933934		let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);935		let current_permission = all_permissions.get(&property_permission.key);936		if matches![937			current_permission,938			Some(PropertyPermission { mutable: false, .. })939		] {940			return Err(<Error<T>>::NoPermission.into());941		}942943		CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {944			let property_permission = property_permission.clone();945			permissions.try_set(property_permission.key, property_permission.permission)946		})947		.map_err(<Error<T>>::from)?;948949		Self::deposit_event(Event::PropertyPermissionSet(950			collection.id,951			property_permission.key,952		));953954		Ok(())955	}956957	#[transactional]958	pub fn set_property_permissions(959		collection: &CollectionHandle<T>,960		sender: &T::CrossAccountId,961		property_permissions: Vec<PropertyKeyPermission>,962	) -> DispatchResult {963		for prop_pemission in property_permissions {964			Self::set_property_permission(collection, sender, prop_pemission)?;965		}966967		Ok(())968	}969970	pub fn get_collection_property(971		collection_id: CollectionId,972		key: &PropertyKey,973	) -> Option<PropertyValue> {974		Self::collection_properties(collection_id).get(key).cloned()975	}976977	pub fn bytes_keys_to_property_keys(978		keys: Vec<Vec<u8>>,979	) -> Result<Vec<PropertyKey>, DispatchError> {980		keys.into_iter()981			.map(|key| -> Result<PropertyKey, DispatchError> {982				key.try_into()983					.map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())984			})985			.collect::<Result<Vec<PropertyKey>, DispatchError>>()986	}987988	pub fn filter_collection_properties(989		collection_id: CollectionId,990		keys: Option<Vec<PropertyKey>>,991	) -> Result<Vec<Property>, DispatchError> {992		let properties = Self::collection_properties(collection_id);993994		let properties = keys995			.map(|keys| {996				keys.into_iter()997					.filter_map(|key| {998						properties.get(&key).map(|value| Property {999							key,1000							value: value.clone(),1001						})1002					})1003					.collect()1004			})1005			.unwrap_or_else(|| {1006				properties1007					.into_iter()1008					.map(|(key, value)| Property {1009						key,1010						value,1011					})1012					.collect()1013			});10141015		Ok(properties)1016	}10171018	pub fn filter_property_permissions(1019		collection_id: CollectionId,1020		keys: Option<Vec<PropertyKey>>,1021	) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1022		let permissions = Self::property_permissions(collection_id);10231024		let key_permissions = keys1025			.map(|keys| {1026				keys.into_iter()1027					.filter_map(|key| {1028						permissions1029							.get(&key)1030							.map(|permission| PropertyKeyPermission {1031								key,1032								permission: permission.clone(),1033							})1034					})1035					.collect()1036			})1037			.unwrap_or_else(|| {1038				permissions1039					.into_iter()1040					.map(|(key, permission)| PropertyKeyPermission {1041						key,1042						permission,1043					})1044					.collect()1045			});10461047		Ok(key_permissions)1048	}10491050	pub fn toggle_allowlist(1051		collection: &CollectionHandle<T>,1052		sender: &T::CrossAccountId,1053		user: &T::CrossAccountId,1054		allowed: bool,1055	) -> DispatchResult {1056		collection.check_is_owner_or_admin(sender)?;10571058		// =========10591060		if allowed {1061			<Allowlist<T>>::insert((collection.id, user), true);1062		} else {1063			<Allowlist<T>>::remove((collection.id, user));1064		}10651066		Ok(())1067	}10681069	pub fn toggle_admin(1070		collection: &CollectionHandle<T>,1071		sender: &T::CrossAccountId,1072		user: &T::CrossAccountId,1073		admin: bool,1074	) -> DispatchResult {1075		collection.check_is_owner_or_admin(sender)?;10761077		let was_admin = <IsAdmin<T>>::get((collection.id, user));1078		if was_admin == admin {1079			return Ok(());1080		}1081		let amount = <AdminAmount<T>>::get(collection.id);10821083		if admin {1084			let amount = amount1085				.checked_add(1)1086				.ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1087			ensure!(1088				amount <= Self::collection_admins_limit(),1089				<Error<T>>::CollectionAdminCountExceeded,1090			);10911092			// =========10931094			<AdminAmount<T>>::insert(collection.id, amount);1095			<IsAdmin<T>>::insert((collection.id, user), true);1096		} else {1097			<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1098			<IsAdmin<T>>::remove((collection.id, user));1099		}11001101		Ok(())1102	}11031104	pub fn clamp_limits(1105		mode: CollectionMode,1106		old_limit: &CollectionLimits,1107		mut new_limit: CollectionLimits,1108	) -> Result<CollectionLimits, DispatchError> {1109		limit_default!(old_limit, new_limit,1110			account_token_ownership_limit => ensure!(1111				new_limit <= MAX_TOKEN_OWNERSHIP,1112				<Error<T>>::CollectionLimitBoundsExceeded,1113			),1114			sponsor_transfer_timeout(match mode {1115				CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1116				CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1117				CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1118			}) => ensure!(1119				new_limit <= MAX_SPONSOR_TIMEOUT,1120				<Error<T>>::CollectionLimitBoundsExceeded,1121			),1122			sponsored_data_size => ensure!(1123				new_limit <= CUSTOM_DATA_LIMIT,1124				<Error<T>>::CollectionLimitBoundsExceeded,1125			),1126			token_limit => ensure!(1127				old_limit >= new_limit && new_limit > 0,1128				<Error<T>>::CollectionTokenLimitExceeded1129			),1130			owner_can_transfer => ensure!(1131				old_limit || !new_limit,1132				<Error<T>>::OwnerPermissionsCantBeReverted,1133			),1134			owner_can_destroy => ensure!(1135				old_limit || !new_limit,1136				<Error<T>>::OwnerPermissionsCantBeReverted,1137			),1138			sponsored_data_rate_limit => {},1139			transfers_enabled => {},1140		);1141		Ok(new_limit)1142	}1143	pub fn clamp_permissions(1144		mode: CollectionMode,1145		old_limit: &CollectionPermissions,1146		mut new_limit: CollectionPermissions,1147	) -> Result<CollectionPermissions, DispatchError> {1148		limit_default_clone!(old_limit, new_limit,1149		);1150		Ok(new_limit)1151	}1152}11531154#[macro_export]1155macro_rules! unsupported {1156	() => {1157		Err(<Error<T>>::UnsupportedOperation.into())1158	};1159}11601161/// Worst cases1162pub trait CommonWeightInfo<CrossAccountId> {1163	fn create_item() -> Weight;1164	fn create_multiple_items(amount: &[CreateItemData]) -> Weight;1165	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;1166	fn burn_item() -> Weight;1167	fn set_collection_properties(amount: u32) -> Weight;1168	fn delete_collection_properties(amount: u32) -> Weight;1169	fn set_token_properties(amount: u32) -> Weight;1170	fn delete_token_properties(amount: u32) -> Weight;1171	fn set_property_permissions(amount: u32) -> Weight;1172	fn transfer() -> Weight;1173	fn approve() -> Weight;1174	fn transfer_from() -> Weight;1175	fn burn_from() -> Weight;1176}11771178pub trait CommonCollectionOperations<T: Config> {1179	fn create_item(1180		&self,1181		sender: T::CrossAccountId,1182		to: T::CrossAccountId,1183		data: CreateItemData,1184		nesting_budget: &dyn Budget,1185	) -> DispatchResultWithPostInfo;1186	fn create_multiple_items(1187		&self,1188		sender: T::CrossAccountId,1189		to: T::CrossAccountId,1190		data: Vec<CreateItemData>,1191		nesting_budget: &dyn Budget,1192	) -> DispatchResultWithPostInfo;1193	fn create_multiple_items_ex(1194		&self,1195		sender: T::CrossAccountId,1196		data: CreateItemExData<T::CrossAccountId>,1197		nesting_budget: &dyn Budget,1198	) -> DispatchResultWithPostInfo;1199	fn burn_item(1200		&self,1201		sender: T::CrossAccountId,1202		token: TokenId,1203		amount: u128,1204	) -> DispatchResultWithPostInfo;1205	fn set_collection_properties(1206		&self,1207		sender: T::CrossAccountId,1208		properties: Vec<Property>,1209	) -> DispatchResultWithPostInfo;1210	fn delete_collection_properties(1211		&self,1212		sender: &T::CrossAccountId,1213		property_keys: Vec<PropertyKey>,1214	) -> DispatchResultWithPostInfo;1215	fn set_token_properties(1216		&self,1217		sender: T::CrossAccountId,1218		token_id: TokenId,1219		property: Vec<Property>,1220	) -> DispatchResultWithPostInfo;1221	fn delete_token_properties(1222		&self,1223		sender: T::CrossAccountId,1224		token_id: TokenId,1225		property_keys: Vec<PropertyKey>,1226	) -> DispatchResultWithPostInfo;1227	fn set_property_permissions(1228		&self,1229		sender: &T::CrossAccountId,1230		property_permissions: Vec<PropertyKeyPermission>,1231	) -> DispatchResultWithPostInfo;1232	fn transfer(1233		&self,1234		sender: T::CrossAccountId,1235		to: T::CrossAccountId,1236		token: TokenId,1237		amount: u128,1238		nesting_budget: &dyn Budget,1239	) -> DispatchResultWithPostInfo;1240	fn approve(1241		&self,1242		sender: T::CrossAccountId,1243		spender: T::CrossAccountId,1244		token: TokenId,1245		amount: u128,1246	) -> DispatchResultWithPostInfo;1247	fn transfer_from(1248		&self,1249		sender: T::CrossAccountId,1250		from: T::CrossAccountId,1251		to: T::CrossAccountId,1252		token: TokenId,1253		amount: u128,1254		nesting_budget: &dyn Budget,1255	) -> DispatchResultWithPostInfo;1256	fn burn_from(1257		&self,1258		sender: T::CrossAccountId,1259		from: T::CrossAccountId,1260		token: TokenId,1261		amount: u128,1262		nesting_budget: &dyn Budget,1263	) -> DispatchResultWithPostInfo;12641265	fn check_nesting(1266		&self,1267		sender: T::CrossAccountId,1268		from: (CollectionId, TokenId),1269		under: TokenId,1270		budget: &dyn Budget,1271	) -> DispatchResult;12721273	fn nest(1274		&self,1275		under: TokenId,1276		to_nest: (CollectionId, TokenId)1277	);12781279	fn unnest(1280		&self,1281		under: TokenId,1282		to_nest: (CollectionId, TokenId)1283	);12841285	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;1286	fn collection_tokens(&self) -> Vec<TokenId>;1287	fn token_exists(&self, token: TokenId) -> bool;1288	fn last_token_id(&self) -> TokenId;12891290	fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;1291	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;1292	fn token_properties(&self, token_id: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;1293	/// Amount of unique collection tokens1294	fn total_supply(&self) -> u32;1295	/// Amount of different tokens account has (Applicable to nonfungible/refungible)1296	fn account_balance(&self, account: T::CrossAccountId) -> u32;1297	/// Amount of specific token account have (Applicable to fungible/refungible)1298	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;1299	fn allowance(1300		&self,1301		sender: T::CrossAccountId,1302		spender: T::CrossAccountId,1303		token: TokenId,1304	) -> u128;1305}13061307// Flexible enough for implementing CommonCollectionOperations1308pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {1309	let post_info = PostDispatchInfo {1310		actual_weight: Some(weight),1311		pays_fee: Pays::Yes,1312	};1313	match res {1314		Ok(()) => Ok(post_info),1315		Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),1316	}1317}13181319impl<T: Config> From<PropertiesError> for Error<T> {1320	fn from(error: PropertiesError) -> Self {1321		match error {1322			PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,1323			PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,1324			PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,1325			PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,1326			PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,1327		}1328	}1329}
modifiedpallets/fungible/src/common.rsdiffbeforeafterboth
--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -298,6 +298,18 @@
 		fail!(<Error<T>>::FungibleDisallowsNesting)
 	}
 
+	fn nest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
+	fn unnest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
 	fn collection_tokens(&self) -> Vec<TokenId> {
 		vec![TokenId::default()]
 	}
modifiedpallets/fungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -25,8 +25,8 @@
 	budget::Budget,
 };
 use pallet_common::{
-	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
-	dispatch::CollectionDispatch, eth::collection_id_to_address,
+	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
+	eth::collection_id_to_address,
 };
 use pallet_evm::Pallet as PalletEvm;
 use pallet_structure::Pallet as PalletStructure;
@@ -145,6 +145,10 @@
 	) -> DispatchResult {
 		let id = collection.id;
 
+		if Self::collection_has_tokens(id) {
+			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+		}
+
 		// =========
 
 		PalletCommon::destroy_collection(collection.0, sender)?;
@@ -155,6 +159,10 @@
 		Ok(())
 	}
 
+	fn collection_has_tokens(collection_id: CollectionId) -> bool {
+		<TotalSupply<T>>::get(collection_id) != 0
+	}
+
 	pub fn burn(
 		collection: &FungibleHandle<T>,
 		owner: &T::CrossAccountId,
@@ -176,6 +184,11 @@
 
 		if balance == 0 {
 			<Balance<T>>::remove((collection.id, owner));
+			<PalletStructure<T>>::unnest_if_nested(
+				owner,
+				collection.id,
+				TokenId::default()
+			);
 		} else {
 			<Balance<T>>::insert((collection.id, owner), balance);
 		}
@@ -229,25 +242,25 @@
 			None
 		};
 
-		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-			let handle = <CollectionHandle<T>>::try_get(target.0)?;
-			let dispatch = T::CollectionDispatch::dispatch(handle);
-			let dispatch = dispatch.as_dyn();
+		// =========
 
-			dispatch.check_nesting(
-				from.clone(),
-				(collection.id, TokenId::default()),
-				target.1,
-				nesting_budget,
-			)?;
-		}
+		<PalletStructure<T>>::nest_if_sent_to_token(
+			from.clone(),
+			to,
+			collection.id,
+			TokenId::default(),
+			nesting_budget
+		)?;
 
-		// =========
-
 		if let Some(balance_to) = balance_to {
 			// from != to
 			if balance_from == 0 {
 				<Balance<T>>::remove((collection.id, from));
+				<PalletStructure<T>>::unnest_if_nested(
+					from,
+					collection.id,
+					TokenId::default()
+				);
 			} else {
 				<Balance<T>>::insert((collection.id, from), balance_from);
 			}
@@ -306,18 +319,13 @@
 		}
 
 		for (to, _) in balances.iter() {
-			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-				let handle = <CollectionHandle<T>>::try_get(target.0)?;
-				let dispatch = T::CollectionDispatch::dispatch(handle);
-				let dispatch = dispatch.as_dyn();
-
-				dispatch.check_nesting(
-					sender.clone(),
-					(collection.id, TokenId::default()),
-					target.1,
-					nesting_budget,
-				)?;
-			}
+			<PalletStructure<T>>::check_nesting(
+				sender.clone(),
+				to,
+				collection.id,
+				TokenId::default(),
+				nesting_budget,
+			)?;
 		}
 
 		// =========
@@ -325,7 +333,7 @@
 		<TotalSupply<T>>::insert(collection.id, total_supply);
 		for (user, amount) in balances {
 			<Balance<T>>::insert((collection.id, &user), amount);
-
+			<PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId::default());
 			<PalletEvm<T>>::deposit_log(
 				ERC20Events::Transfer {
 					from: H160::default(),
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -353,6 +353,22 @@
 		<Pallet<T>>::check_nesting(self, sender, from, under, budget)
 	}
 
+	fn nest(
+		&self,
+		under: TokenId,
+		to_nest: (CollectionId, TokenId)
+	) {
+		<Pallet<T>>::nest((self.id, under), to_nest);
+	}
+
+	fn unnest(
+		&self,
+		under: TokenId,
+		to_unnest: (CollectionId, TokenId)
+	) {
+		<Pallet<T>>::unnest((self.id, under), to_unnest);
+	}
+
 	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
 		<Owned<T>>::iter_prefix((self.id, account))
 			.map(|(id, _)| id)
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -27,7 +27,7 @@
 use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
 use pallet_common::{
 	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,
-	dispatch::CollectionDispatch, eth::collection_id_to_address,
+	eth::collection_id_to_address,
 };
 use pallet_structure::Pallet as PalletStructure;
 use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
@@ -76,6 +76,8 @@
 		NotNonfungibleDataUsedToMintFungibleCollectionToken,
 		/// Used amount > 1 with NFT
 		NonfungibleItemsHaveNoAmount,
+		/// Unable to burn NFT with children
+		CantBurnNftWithChildren,
 	}
 
 	#[pallet::config]
@@ -127,7 +129,20 @@
 		QueryKind = ValueQuery,
 	>;
 
+	/// Used to enumerate token's children
 	#[pallet::storage]
+	#[pallet::getter(fn token_children)]
+	pub type TokenChildren<T: Config> = StorageNMap<
+		Key = (
+			Key<Twox64Concat, CollectionId>,
+			Key<Twox64Concat, TokenId>,
+			Key<Twox64Concat, (CollectionId, TokenId)>,
+		),
+		Value = bool,
+		QueryKind = ValueQuery,
+	>;
+
+	#[pallet::storage]
 	pub type AccountBalance<T: Config> = StorageNMap<
 		Key = (
 			Key<Twox64Concat, CollectionId>,
@@ -277,11 +292,16 @@
 	) -> DispatchResult {
 		let id = collection.id;
 
+		if Self::collection_has_tokens(id) {
+			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+		}
+
 		// =========
 
 		PalletCommon::destroy_collection(collection.0, sender)?;
 
 		<TokenData<T>>::remove_prefix((id,), None);
+		<TokenChildren<T>>::remove_prefix((id,), None);
 		<Owned<T>>::remove_prefix((id,), None);
 		<TokensMinted<T>>::remove(id);
 		<TokensBurnt<T>>::remove(id);
@@ -307,6 +327,10 @@
 			collection.check_allowlist(sender)?;
 		}
 
+		if Self::token_has_children(collection.id, token) {
+			return Err(<Error<T>>::CantBurnNftWithChildren.into());
+		}
+
 		let burnt = <TokensBurnt<T>>::get(collection.id)
 			.checked_add(1)
 			.ok_or(ArithmeticError::Overflow)?;
@@ -315,13 +339,20 @@
 			.checked_sub(1)
 			.ok_or(ArithmeticError::Overflow)?;
 
+		// =========
+
 		if balance == 0 {
 			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));
 		} else {
 			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);
 		}
-		// =========
 
+		<PalletStructure<T>>::unnest_if_nested(
+			&token_data.owner,
+			collection.id,
+			token
+		);
+
 		<Owned<T>>::remove((collection.id, &token_data.owner, token));
 		<TokensBurnt<T>>::insert(collection.id, burnt);
 		<TokenData<T>>::remove((collection.id, token));
@@ -553,20 +584,21 @@
 			None
 		};
 
-		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-			let handle = <CollectionHandle<T>>::try_get(target.0)?;
-			let dispatch = T::CollectionDispatch::dispatch(handle);
-			let dispatch = dispatch.as_dyn();
+		<PalletStructure<T>>::nest_if_sent_to_token(
+			from.clone(),
+			to,
+			collection.id,
+			token,
+			nesting_budget
+		)?;
 
-			dispatch.check_nesting(
-				from.clone(),
-				(collection.id, token),
-				target.1,
-				nesting_budget,
-			)?;
-		}
+		// =========
 
-		// =========
+		<PalletStructure<T>>::unnest_if_nested(
+			from,
+			collection.id,
+			token
+		);
 
 		<TokenData<T>>::insert(
 			(collection.id, token),
@@ -653,17 +685,14 @@
 
 		for (i, data) in data.iter().enumerate() {
 			let token = TokenId(first_token + i as u32 + 1);
-			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {
-				let handle = <CollectionHandle<T>>::try_get(target.0)?;
-				let dispatch = T::CollectionDispatch::dispatch(handle);
-				let dispatch = dispatch.as_dyn();
-				dispatch.check_nesting(
-					sender.clone(),
-					(collection.id, token),
-					target.1,
-					nesting_budget,
-				)?;
-			}
+
+			<PalletStructure<T>>::check_nesting(
+				sender.clone(),
+				&data.owner,
+				collection.id,
+				token,
+				nesting_budget,
+			)?;
 		}
 
 		// =========
@@ -680,6 +709,8 @@
 					},
 				);
 
+				<PalletStructure<T>>::nest_if_sent_to_token_unchecked(&data.owner, collection.id, TokenId(token));
+
 				if let Err(e) = Self::set_token_properties(
 					collection,
 					sender,
@@ -927,6 +958,33 @@
 		Ok(())
 	}
 
+	fn nest(
+		under: (CollectionId, TokenId),
+		to_nest: (CollectionId, TokenId),
+	) {
+		<TokenChildren<T>>::insert(
+			(under.0, under.1, (to_nest.0, to_nest.1)),
+			true
+		);
+	}
+
+	fn unnest(
+		under: (CollectionId, TokenId),
+		to_unnest: (CollectionId, TokenId),
+	) {
+		<TokenChildren<T>>::remove(
+			(under.0, under.1, to_unnest)
+		);
+	}
+
+	fn collection_has_tokens(collection_id: CollectionId) -> bool {
+		<TokenData<T>>::iter_prefix((collection_id,)).next().is_some()
+	}
+
+	fn token_has_children(collection_id: CollectionId, token_id: TokenId) -> bool {
+		<TokenChildren<T>>::iter_prefix((collection_id, token_id)).next().is_some()
+	}
+
 	/// Delegated to `create_multiple_items`
 	pub fn create_item(
 		collection: &NonfungibleHandle<T>,
modifiedpallets/proxy-rmrk-core/src/misc.rsdiffbeforeafterboth
--- a/pallets/proxy-rmrk-core/src/misc.rs
+++ b/pallets/proxy-rmrk-core/src/misc.rs
@@ -26,6 +26,18 @@
     }
 }
 
+pub trait RmrkRebind<T, S> {
+    fn rebind(&self) -> BoundedVec<u8, S>;
+}
+
+impl<T, S> RmrkRebind<T, S> for BoundedVec<u8, T> where BoundedVec<u8, S>: TryFrom<Vec<u8>> {
+    fn rebind(&self) -> BoundedVec<u8, S> {
+        BoundedVec::<u8, S>::try_from(
+            self.clone().into_inner()
+        ).unwrap_or_default()
+    }
+}
+
 #[derive(Encode, Decode, PartialEq, Eq)]
 pub enum CollectionType {
     Regular,
modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -313,6 +313,18 @@
 		fail!(<Error<T>>::RefungibleDisallowsNesting)
 	}
 
+	fn nest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
+	fn unnest(
+		&self,
+		_under: TokenId,
+		_to_nest: (CollectionId, TokenId)
+	) {}
+
 	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId> {
 		<Owned<T>>::iter_prefix((self.id, account))
 			.map(|(id, _)| id)
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -23,8 +23,7 @@
 };
 use pallet_evm::account::CrossAccountId;
 use pallet_common::{
-	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CollectionHandle,
-	dispatch::CollectionDispatch,
+	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
 };
 use pallet_structure::Pallet as PalletStructure;
 use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
@@ -211,6 +210,10 @@
 	) -> DispatchResult {
 		let id = collection.id;
 
+		if Self::collection_has_tokens(id) {
+			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());
+		}
+
 		// =========
 
 		PalletCommon::destroy_collection(collection.0, sender)?;
@@ -226,6 +229,10 @@
 		Ok(())
 	}
 
+	fn collection_has_tokens(collection_id: CollectionId) -> bool {
+		<TokenData<T>>::iter_prefix((collection_id,)).next().is_some()
+	}
+
 	pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {
 		let burnt = <TokensBurnt<T>>::get(collection.id)
 			.checked_add(1)
@@ -265,6 +272,7 @@
 			// =========
 
 			<Owned<T>>::remove((collection.id, owner, token));
+			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
 			Self::burn_token(collection, token)?;
 			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
@@ -292,6 +300,7 @@
 
 		if balance == 0 {
 			<Owned<T>>::remove((collection.id, owner, token));
+			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<Balance<T>>::remove((collection.id, token, owner));
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
 		} else {
@@ -372,25 +381,25 @@
 			None
 		};
 
-		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-			let handle = <CollectionHandle<T>>::try_get(target.0)?;
-			let dispatch = T::CollectionDispatch::dispatch(handle);
-			let dispatch = dispatch.as_dyn();
+		// =========
 
-			dispatch.check_nesting(
-				from.clone(),
-				(collection.id, token),
-				target.1,
-				nesting_budget,
-			)?;
-		}
-
-		// =========
+		<PalletStructure<T>>::nest_if_sent_to_token(
+			from.clone(),
+			to,
+			collection.id,
+			token,
+			nesting_budget
+		)?;
 
 		if let Some(balance_to) = balance_to {
 			// from != to
 			if balance_from == 0 {
 				<Balance<T>>::remove((collection.id, token, from));
+				<PalletStructure<T>>::unnest_if_nested(
+					from,
+					collection.id,
+					token
+				);
 			} else {
 				<Balance<T>>::insert((collection.id, token, from), balance_from);
 			}
@@ -488,18 +497,14 @@
 		for (i, token) in data.iter().enumerate() {
 			let token_id = TokenId(first_token_id + i as u32 + 1);
 			for (to, _) in token.users.iter() {
-				if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {
-					let handle = <CollectionHandle<T>>::try_get(target.0)?;
-					let dispatch = T::CollectionDispatch::dispatch(handle);
-					let dispatch = dispatch.as_dyn();
 
-					dispatch.check_nesting(
-						sender.clone(),
-						(collection.id, token_id),
-						target.1,
-						nesting_budget,
-					)?;
-				}
+				<PalletStructure<T>>::check_nesting(
+					sender.clone(),
+					to,
+					collection.id,
+					token_id,
+					nesting_budget,
+				)?;
 			}
 		}
 
@@ -519,12 +524,15 @@
 					const_data: token.const_data,
 				},
 			);
+
 			for (user, amount) in token.users.into_iter() {
 				if amount == 0 {
 					continue;
 				}
 				<Balance<T>>::insert((collection.id, token_id, &user), amount);
 				<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);
+				<PalletStructure<T>>::nest_if_sent_to_token_unchecked(&user, collection.id, TokenId(token_id));
+
 				// TODO: ERC20 transfer event
 				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(
 					collection.id,
modifiedpallets/structure/src/lib.rsdiffbeforeafterboth
--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -1,8 +1,9 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
+use pallet_common::CommonCollectionOperations;
 use sp_std::collections::btree_set::BTreeSet;
 
-use frame_support::dispatch::DispatchError;
+use frame_support::dispatch::{DispatchError, DispatchResult};
 use frame_support::fail;
 pub use pallet::*;
 use pallet_common::{dispatch::CollectionDispatch, CollectionHandle};
@@ -155,8 +156,8 @@
 		budget: &dyn Budget,
 	) -> Result<bool, DispatchError> {
 		let target_parent = match T::CrossTokenAddressMapping::address_to_token(&user) {
-			Some((collection, token)) => Parent::Token(collection, token),
-			None => Parent::User(user),
+			Some((collection, token)) => Self::find_topmost_owner(collection, token, budget)?,
+			None => user,
 		};
 
 		// Tried to nest token in itself
@@ -171,10 +172,10 @@
 					return Err(<Error<T>>::OuroborosDetected.into())
 				}
 				// Found needed parent, token is indirecty owned
-				v if v == target_parent => return Ok(true),
+				Parent::User(user) if user == target_parent => return Ok(true),
 				// Token is owned by other user
 				Parent::User(_) => return Ok(false),
-				Parent::TokenNotFound => return Ok(false),
+				Parent::TokenNotFound => return Err(<Error<T>>::TokenNotFound.into()),
 				// Continue parent chain
 				Parent::Token(_, _) => {}
 			}
@@ -182,4 +183,113 @@
 
 		Err(<Error<T>>::DepthLimit.into())
 	}
+
+	pub fn check_nesting(
+		from: T::CrossAccountId,
+		under: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId,
+		nesting_budget: &dyn Budget
+	) -> DispatchResult {
+		Self::try_exec_if_owner_is_valid_nft(
+			under,
+			|d, parent_id| d.check_nesting(
+				from,
+				(collection_id, token_id),
+				parent_id,
+				nesting_budget
+			)
+		)
+	}
+
+	pub fn nest_if_sent_to_token(
+		from: T::CrossAccountId,
+		under: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId,
+		nesting_budget: &dyn Budget
+	) -> DispatchResult {
+		Self::try_exec_if_owner_is_valid_nft(
+			under,
+			|d, parent_id| {
+				d.check_nesting(
+					from,
+					(collection_id, token_id),
+					parent_id,
+					nesting_budget
+				)?;
+
+				d.nest(parent_id, (collection_id, token_id));
+
+				Ok(())
+			}
+		)
+	}
+
+	pub fn nest_if_sent_to_token_unchecked(
+		owner: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId
+	) {
+		Self::exec_if_owner_is_valid_nft(
+			owner,
+			|d, parent_id| d.nest(
+				parent_id,
+				(collection_id, token_id)
+			)
+		);
+	}
+
+	pub fn unnest_if_nested(
+		owner: &T::CrossAccountId,
+		collection_id: CollectionId,
+		token_id: TokenId
+	) {
+		Self::exec_if_owner_is_valid_nft(
+			owner,
+			|d, parent_id| d.unnest(
+			parent_id,
+			(collection_id, token_id)
+			)
+		);
+	}
+
+	fn exec_if_owner_is_valid_nft(
+		account: &T::CrossAccountId,
+		action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId)
+	) {
+		Self::try_exec_if_owner_is_valid_nft(
+			account,
+			|d, id| {
+				action(d, id);
+				Ok(())
+			}
+		).unwrap();
+	}
+
+	fn try_exec_if_owner_is_valid_nft(
+		account: &T::CrossAccountId,
+		action: impl FnOnce(&dyn CommonCollectionOperations<T>, TokenId) -> DispatchResult
+	) -> DispatchResult {
+		let account = T::CrossTokenAddressMapping::address_to_token(account);
+
+		if account.is_none() {
+			return Ok(());
+		}
+
+		let account = account.unwrap();
+
+		let handle = <CollectionHandle<T>>::try_get(account.0);
+
+		if handle.is_err() {
+			return Ok(());
+		}
+
+		let handle = handle.unwrap();
+
+		let dispatch = T::CollectionDispatch::dispatch(handle);
+		let dispatch = dispatch.as_dyn();
+
+		action(dispatch, account.1)
+	}
 }
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -55,6 +55,8 @@
 pub mod weights;
 use weights::WeightInfo;
 
+const NESTING_BUDGET: u32 = 5;
+
 decl_error! {
 	/// Error for non-fungible-token module.
 	pub enum Error for Module<T: Config> {
@@ -569,7 +571,7 @@
 		#[transactional]
 		pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))
 		}
@@ -597,7 +599,7 @@
 		pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {
 			ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))
 		}
@@ -678,7 +680,7 @@
 		#[transactional]
 		pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))
 		}
@@ -758,7 +760,7 @@
 		#[transactional]
 		pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))
 		}
@@ -790,7 +792,7 @@
 		#[transactional]
 		pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))
 		}
@@ -841,7 +843,7 @@
 		#[transactional]
 		pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
-			let budget = budget::Value::new(2);
+			let budget = budget::Value::new(NESTING_BUDGET);
 
 			dispatch_call::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))
 		}
modifiedruntime/common/src/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -25,7 +25,7 @@
                     dispatch_unique_runtime!(collection.token_owner(token))
                 }
                 fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
-                    let budget = up_data_structs::budget::Value::new(5);
+                    let budget = up_data_structs::budget::Value::new(10);
 
                     Ok(Some(<pallet_structure::Pallet<Runtime>>::find_topmost_owner(collection, token, &budget)?))
                 }
@@ -142,7 +142,7 @@
                 }
 
                 fn collection_by_id(collection_id: RmrkCollectionId) -> Result<Option<RmrkCollectionInfo<AccountId>>, DispatchError> {
-                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode}};
+                    use pallet_proxy_rmrk_core::{RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind}};
 
                     let collection_id = CollectionId(collection_id);
                     let collection = match RmrkCore::get_typed_nft_collection(collection_id, CollectionType::Regular) {
@@ -156,7 +156,7 @@
                         issuer: collection.owner.clone(),
                         metadata: RmrkCore::get_collection_property(collection_id, RmrkProperty::Metadata)?.decode_or_default(),
                         max: collection.limits.token_limit,
-                        symbol: collection.token_prefix.decode_or_default(),
+                        symbol: collection.token_prefix.rebind(),
                         nfts_count
                     }))
                 }
@@ -204,22 +204,21 @@
                 }
 
                 fn nft_children(collection_id: RmrkCollectionId, nft_id: RmrkNftId) -> Result<Vec<RmrkNftChild>, DispatchError> {
-                    use up_data_structs::mapping::TokenAddressMapping;
-
                     let collection_id = CollectionId(collection_id);
                     let nft_id = TokenId(nft_id);
                     if !RmrkCore::nft_exists(collection_id, nft_id) { return Ok(Vec::new()); }
 
-                    let cross_account_id = CrossAccountId::from_eth(
-                        EvmTokenAddressMapping::token_to_address(collection_id, nft_id)
-                    );
-
                     Ok(
-                        pallet_nonfungible::Owned::<Runtime>::iter_prefix((collection_id, cross_account_id))
-                            .map(|(child_id, _)| RmrkNftChild {
-                                collection_id: collection_id.0, // todo make sure they're always from this collection // spoiler: they're not
-                                nft_id: child_id.0,
-                            }).collect()
+                        pallet_nonfungible::TokenChildren::<Runtime>::iter_prefix((collection_id, nft_id))
+                            .filter_map(|(child_id, is_child)|
+                                match is_child {
+                                    true => Some(RmrkNftChild {
+                                        collection_id: child_id.0.0,
+                                        nft_id: child_id.1.0,
+                                    }),
+                                    false => None,
+                                }
+                            ).collect()
                     )
                 }
 
@@ -332,7 +331,7 @@
 
                 fn base(base_id: RmrkBaseId) -> Result<Option<RmrkBaseInfo<AccountId>>, DispatchError> {
                     use pallet_proxy_rmrk_core::{
-                        RmrkProperty, misc::{CollectionType, RmrkDecode},
+                        RmrkProperty, misc::{CollectionType, RmrkDecode, RmrkRebind},
                     };
 
                     let collection_id = CollectionId(base_id);
@@ -344,7 +343,7 @@
                     Ok(Some(RmrkBaseInfo {
                         issuer: collection.owner.clone(),
                         base_type: RmrkCore::get_collection_property(collection_id, RmrkProperty::BaseType)?.decode_or_default(),
-                        symbol: collection.token_prefix.decode_or_default(),
+                        symbol: collection.token_prefix.rebind(),
                     }))
                 }
 
modifiedtests/src/nesting/nest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/nest.test.ts
+++ b/tests/src/nesting/nest.test.ts
@@ -41,7 +41,7 @@
 
       // Create a token to be nested
       const newToken = await createItemExpectSuccess(alice, collection, 'NFT');
-      
+
       // Nest
       await transferExpectSuccess(collection, newToken, alice, {Ethereum: tokenIdToAddress(collection, targetToken)});
       expect(await getTopmostTokenOwner(api, collection, newToken)).to.be.deep.equal({Substrate: alice.address});
@@ -111,8 +111,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       ))).to.not.be.rejected;
 
@@ -134,8 +134,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       ))).to.not.be.rejected;
 
@@ -158,8 +158,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       ))).to.not.be.rejected;
 
@@ -181,7 +181,7 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
+        collectionRFT,
         targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       ))).to.not.be.rejected;
@@ -207,17 +207,29 @@
       await setCollectionPermissionsExceptSuccess(alice, collection, {nesting: 'Owner'});
       const targetToken = await createItemExpectSuccess(alice, collection, 'NFT');
 
+      const maxNestingLevel = 5;
+      let prevToken = targetToken;
+
       // Create a nested-token matryoshka
-      const nestedToken1 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, targetToken)});
-      const nestedToken2 = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, nestedToken1)});
-      // The nesting depth is limited by 2
+      for (let i = 0; i < maxNestingLevel; i++) {
+        const nestedToken = await createItemExpectSuccess(
+          alice,
+          collection,
+          'NFT',
+          {Ethereum: tokenIdToAddress(collection, prevToken)},
+        );
+
+        prevToken = nestedToken;
+      }
+
+      // The nesting depth is limited by `maxNestingLevel`
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, nestedToken2)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, prevToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/^structure\.DepthLimit$/);
 
-      expect(await getTopmostTokenOwner(api, collection, nestedToken2)).to.be.deep.equal({Substrate: alice.address});
+      expect(await getTopmostTokenOwner(api, collection, prevToken)).to.be.deep.equal({Substrate: alice.address});
     });
   });
 
@@ -231,8 +243,8 @@
 
       // Try to create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
 
@@ -259,8 +271,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -285,8 +297,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -307,8 +319,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collection, 
-        {Ethereum: tokenIdToAddress(collection, targetToken)}, 
+        collection,
+        {Ethereum: tokenIdToAddress(collection, targetToken)},
           {nft: {const_data: [], variable_data: []}} as any,
       )), 'while creating nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
@@ -332,11 +344,11 @@
 
       // Try to create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
-      
+
       // Create a token to be nested
       const newToken = await createItemExpectSuccess(alice, collectionFT, 'Fungible');
       // Try to nest
@@ -366,8 +378,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -393,8 +405,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -417,8 +429,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionFT, 
-        targetAddress, 
+        collectionFT,
+        targetAddress,
         {Fungible: {Value: 10}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
@@ -441,8 +453,8 @@
 
       // Create a nested token
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/^common\.NestingIsDisabled$/);
 
@@ -477,8 +489,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -504,8 +516,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.OnlyOwnerAllowedToNest/);
 
@@ -528,8 +540,8 @@
 
       // Try to create a nested token in the wrong collection
       await expect(executeTransaction(api, alice, api.tx.unique.createItem(
-        collectionRFT, 
-        targetAddress, 
+        collectionRFT,
+        targetAddress,
         {ReFungible: {const_data: [], pieces: 100}},
       )), 'while creating a nested token').to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);