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

difftreelog

Add extrinsic: delete collection property

Daniel Shiposha2022-05-04parent: #aa3d2ae.patch.diff
in: master

10 files changed

modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -292,6 +292,8 @@
 
 		CollectionPropertySet(CollectionId, Property),
 
+		CollectionPropertyDeleted(CollectionId, PropertyKey),
+
 		TokenPropertySet(CollectionId, TokenId, Property),
 
 		TokenPropertyDeleted(CollectionId, TokenId, PropertyKey),
@@ -761,6 +763,34 @@
 		Ok(())
 	}
 
+	pub fn delete_collection_property(
+		collection: &CollectionHandle<T>,
+		sender: &T::CrossAccountId,
+		property_key: PropertyKey,
+	) -> DispatchResult {
+		collection.check_is_owner_or_admin(sender)?;
+
+		CollectionProperties::<T>::mutate(collection.id, |properties| {
+			properties.remove_property(&property_key);
+		});
+
+		Self::deposit_event(Event::CollectionPropertyDeleted(collection.id, property_key));
+
+		Ok(())
+	}
+
+	pub fn delete_collection_properties(
+		collection: &CollectionHandle<T>,
+		sender: &T::CrossAccountId,
+		property_keys: Vec<PropertyKey>,
+	) -> DispatchResult {
+		for key in property_keys {
+			Self::delete_collection_property(collection, sender, key)?;
+		}
+
+		Ok(())
+	}
+
 	pub fn set_property_permission(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -956,6 +986,7 @@
 	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;
 	fn burn_item() -> Weight;
 	fn set_collection_properties(amount: u32) -> Weight;
+	fn delete_collection_properties(amount: u32) -> Weight;
 	fn set_token_properties(amount: u32) -> Weight;
 	fn delete_token_properties(amount: u32) -> Weight;
 	fn set_property_permissions(amount: u32) -> Weight;
@@ -998,6 +1029,11 @@
 		sender: T::CrossAccountId,
 		properties: Vec<Property>,
 	) -> DispatchResultWithPostInfo;
+	fn delete_collection_properties(
+		&self,
+		sender: &T::CrossAccountId,
+		property_keys: Vec<PropertyKey>,
+	) -> DispatchResultWithPostInfo;
 	fn set_token_properties(
 		&self,
 		sender: T::CrossAccountId,
modifiedpallets/fungible/src/common.rsdiffbeforeafterboth
--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -54,6 +54,10 @@
 		<SelfWeightOf<T>>::set_collection_properties(amount)
 	}
 
+	fn delete_collection_properties(amount: u32) -> Weight {
+		<SelfWeightOf<T>>::delete_collection_properties(amount)
+	}
+
 	fn set_token_properties(amount: u32) -> Weight {
 		<SelfWeightOf<T>>::set_token_properties(amount)
 	}
@@ -249,6 +253,14 @@
 		fail!(<Error<T>>::PropertiesNotAllowed)
 	}
 
+	fn delete_collection_properties(
+		&self,
+		_sender: &T::CrossAccountId,
+		_property_keys: Vec<PropertyKey>,
+	) -> DispatchResultWithPostInfo {
+		fail!(<Error<T>>::PropertiesNotAllowed)
+	}
+
 	fn set_token_properties(
 		&self,
 		_sender: T::CrossAccountId,
modifiedpallets/fungible/src/weights.rsdiffbeforeafterboth
--- a/pallets/fungible/src/weights.rs
+++ b/pallets/fungible/src/weights.rs
@@ -36,6 +36,7 @@
 	fn create_multiple_items_ex(b: u32, ) -> Weight;
 	fn burn_item() -> Weight;
 	fn set_collection_properties(amount: u32) -> Weight;
+	fn delete_collection_properties(amount: u32) -> Weight;
 	fn set_token_properties(amount: u32) -> Weight;
 	fn delete_token_properties(amount: u32) -> Weight;
 	fn set_property_permissions(amount: u32) -> Weight;
@@ -79,6 +80,11 @@
 		0
 	}
 
+	fn delete_collection_properties(_amount: u32) -> Weight {
+		// Error
+		0
+	}
+
 	fn set_token_properties(_amount: u32) -> Weight {
 		// Error
 		0
@@ -157,6 +163,11 @@
 		0
 	}
 
+	fn delete_collection_properties(_amount: u32) -> Weight {
+		// Error
+		0
+	}
+
 	fn set_token_properties(_amount: u32) -> Weight {
 		// Error
 		0
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -55,6 +55,10 @@
 		<SelfWeightOf<T>>::set_collection_properties(amount)
 	}
 
+	fn delete_collection_properties(amount: u32) -> Weight {
+		<SelfWeightOf<T>>::delete_collection_properties(amount)
+	}
+
 	fn set_token_properties(amount: u32) -> Weight {
 		<SelfWeightOf<T>>::set_token_properties(amount)
 	}
@@ -171,6 +175,19 @@
 		)
 	}
 
+	fn delete_collection_properties(
+		&self,
+		sender: &T::CrossAccountId,
+		property_keys: Vec<PropertyKey>,
+	) -> DispatchResultWithPostInfo {
+		let weight = <CommonWeights<T>>::delete_collection_properties(property_keys.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::delete_collection_properties(self, &sender, property_keys),
+			weight
+		)
+	}
+
 	fn set_token_properties(
 		&self,
 		sender: T::CrossAccountId,
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
before · pallets/nonfungible/src/lib.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use frame_support::{BoundedVec, ensure, fail};21use up_data_structs::{22	AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,23	mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,24	PropertyKey, PropertyKeyPermission,25};26use pallet_evm::account::CrossAccountId;27use pallet_common::{28	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,29	dispatch::CollectionDispatch,30};31use pallet_structure::Pallet as PalletStructure;32use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};33use sp_core::H160;34use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};35use sp_std::{vec::Vec, vec};36use core::ops::Deref;37use sp_std::collections::btree_map::BTreeMap;38use codec::{Encode, Decode, MaxEncodedLen};39use scale_info::TypeInfo;4041pub use pallet::*;42#[cfg(feature = "runtime-benchmarks")]43pub mod benchmarking;44pub mod common;45pub mod erc;46pub mod weights;4748pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;49pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5051#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]52pub struct ItemData<CrossAccountId> {53	pub const_data: BoundedVec<u8, CustomDataLimit>,54	pub variable_data: BoundedVec<u8, CustomDataLimit>,55	pub owner: CrossAccountId,56}5758#[frame_support::pallet]59pub mod pallet {60	use super::*;61	use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};62	use up_data_structs::{CollectionId, TokenId};63	use super::weights::WeightInfo;6465	#[pallet::error]66	pub enum Error<T> {67		/// Not Nonfungible item data used to mint in Nonfungible collection.68		NotNonfungibleDataUsedToMintFungibleCollectionToken,69		/// Used amount > 1 with NFT70		NonfungibleItemsHaveNoAmount,71	}7273	#[pallet::config]74	pub trait Config:75		frame_system::Config + pallet_common::Config + pallet_structure::Config76	{77		type WeightInfo: WeightInfo;78	}7980	#[pallet::pallet]81	#[pallet::generate_store(pub(super) trait Store)]82	pub struct Pallet<T>(_);8384	#[pallet::storage]85	pub type TokensMinted<T: Config> =86		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;87	#[pallet::storage]88	pub type TokensBurnt<T: Config> =89		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;9091	#[pallet::storage]92	pub type TokenData<T: Config> = StorageNMap<93		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),94		Value = ItemData<T::CrossAccountId>,95		QueryKind = OptionQuery,96	>;9798	#[pallet::storage]99	pub type TokenProperties<T: Config> = StorageNMap<100		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),101		Value = up_data_structs::Properties,102		QueryKind = ValueQuery,103		OnEmpty = up_data_structs::TokenProperties,104	>;105106	/// Used to enumerate tokens owned by account107	#[pallet::storage]108	pub type Owned<T: Config> = StorageNMap<109		Key = (110			Key<Twox64Concat, CollectionId>,111			Key<Blake2_128Concat, T::CrossAccountId>,112			Key<Twox64Concat, TokenId>,113		),114		Value = bool,115		QueryKind = ValueQuery,116	>;117118	#[pallet::storage]119	pub type AccountBalance<T: Config> = StorageNMap<120		Key = (121			Key<Twox64Concat, CollectionId>,122			Key<Blake2_128Concat, T::CrossAccountId>,123		),124		Value = u32,125		QueryKind = ValueQuery,126	>;127128	#[pallet::storage]129	pub type Allowance<T: Config> = StorageNMap<130		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),131		Value = T::CrossAccountId,132		QueryKind = OptionQuery,133	>;134}135136pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);137impl<T: Config> NonfungibleHandle<T> {138	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {139		Self(inner)140	}141	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {142		self.0143	}144}145impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {146	fn recorder(&self) -> &SubstrateRecorder<T> {147		self.0.recorder()148	}149	fn into_recorder(self) -> SubstrateRecorder<T> {150		self.0.into_recorder()151	}152}153impl<T: Config> Deref for NonfungibleHandle<T> {154	type Target = pallet_common::CollectionHandle<T>;155156	fn deref(&self) -> &Self::Target {157		&self.0158	}159}160161impl<T: Config> Pallet<T> {162	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {163		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)164	}165	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {166		<TokenData<T>>::contains_key((collection.id, token))167	}168}169170// unchecked calls skips any permission checks171impl<T: Config> Pallet<T> {172	pub fn init_collection(173		owner: T::AccountId,174		data: CreateCollectionData<T::AccountId>,175	) -> Result<CollectionId, DispatchError> {176		<PalletCommon<T>>::init_collection(owner, data)177	}178	pub fn destroy_collection(179		collection: NonfungibleHandle<T>,180		sender: &T::CrossAccountId,181	) -> DispatchResult {182		let id = collection.id;183184		// =========185186		PalletCommon::destroy_collection(collection.0, sender)?;187188		<TokenData<T>>::remove_prefix((id,), None);189		<Owned<T>>::remove_prefix((id,), None);190		<TokensMinted<T>>::remove(id);191		<TokensBurnt<T>>::remove(id);192		<Allowance<T>>::remove_prefix((id,), None);193		<AccountBalance<T>>::remove_prefix((id,), None);194		Ok(())195	}196197	pub fn burn(198		collection: &NonfungibleHandle<T>,199		sender: &T::CrossAccountId,200		token: TokenId,201	) -> DispatchResult {202		let token_data =203			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;204		ensure!(205			&token_data.owner == sender206				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),207			<CommonError<T>>::NoPermission208		);209210		if collection.access == AccessMode::AllowList {211			collection.check_allowlist(sender)?;212		}213214		let burnt = <TokensBurnt<T>>::get(collection.id)215			.checked_add(1)216			.ok_or(ArithmeticError::Overflow)?;217218		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))219			.checked_sub(1)220			.ok_or(ArithmeticError::Overflow)?;221222		if balance == 0 {223			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));224		} else {225			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);226		}227		// =========228229		<Owned<T>>::remove((collection.id, &token_data.owner, token));230		<TokensBurnt<T>>::insert(collection.id, burnt);231		<TokenData<T>>::remove((collection.id, token));232		let old_spender = <Allowance<T>>::take((collection.id, token));233234		if let Some(old_spender) = old_spender {235			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(236				collection.id,237				token,238				sender.clone(),239				old_spender,240				0,241			));242		}243244		collection.log_mirrored(ERC721Events::Transfer {245			from: *token_data.owner.as_eth(),246			to: H160::default(),247			token_id: token.into(),248		});249		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(250			collection.id,251			token,252			token_data.owner,253			1,254		));255		Ok(())256	}257258	pub fn set_token_property(259		collection: &NonfungibleHandle<T>,260		sender: &T::CrossAccountId,261		token_id: TokenId,262		property: Property,263	) -> DispatchResult {264		Self::check_token_change_permission(collection, sender, token_id, &property.key)?;265266		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {267			properties.try_set_property(property.clone())268		})?;269270		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(271			collection.id,272			token_id,273			property,274		));275276		Ok(())277	}278279	pub fn set_token_properties(280		collection: &NonfungibleHandle<T>,281		sender: &T::CrossAccountId,282		token_id: TokenId,283		properties: Vec<Property>,284	) -> DispatchResult {285		for property in properties {286			Self::set_token_property(collection, sender, token_id, property)?;287		}288289		Ok(())290	}291292	pub fn delete_token_property(293		collection: &NonfungibleHandle<T>,294		sender: &T::CrossAccountId,295		token_id: TokenId,296		property_key: PropertyKey,297	) -> DispatchResult {298		Self::check_token_change_permission(collection, sender, token_id, &property_key)?;299300		<TokenProperties<T>>::mutate((collection.id, token_id), |properties| {301			properties.remove_property(&property_key);302		});303304		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(305			collection.id,306			token_id,307			property_key,308		));309310		Ok(())311	}312313	fn check_token_change_permission(314		collection: &NonfungibleHandle<T>,315		sender: &T::CrossAccountId,316		token_id: TokenId,317		property_key: &PropertyKey,318	) -> DispatchResult {319		let permission = <PalletCommon<T>>::property_permission(collection.id)320			.get(property_key)321			.map(|p| p.clone())322			.unwrap_or(PropertyPermission::None);323324		let token_data = <TokenData<T>>::get((collection.id, token_id))325			.ok_or(<CommonError<T>>::TokenNotFound)?;326327		let check_token_owner = || -> DispatchResult {328			ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);329			Ok(())330		};331332		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))333			.get_property(property_key)334			.is_some();335336		match (permission, is_property_exists) {337			(PropertyPermission::AdminConst, false) => collection.check_is_owner_or_admin(sender),338			(PropertyPermission::Admin, _) => collection.check_is_owner_or_admin(sender),339			(PropertyPermission::ItemOwnerConst, false) => check_token_owner(),340			(PropertyPermission::ItemOwner, _) => check_token_owner(),341			(PropertyPermission::ItemOwnerOrAdmin, _) => {342				check_token_owner().or(collection.check_is_owner_or_admin(sender))343			}344			_ => Err(<CommonError<T>>::NoPermission.into()),345		}346	}347348	pub fn delete_token_properties(349		collection: &NonfungibleHandle<T>,350		sender: &T::CrossAccountId,351		token_id: TokenId,352		property_keys: Vec<PropertyKey>,353	) -> DispatchResult {354		for key in property_keys {355			Self::delete_token_property(collection, sender, token_id, key)?;356		}357358		Ok(())359	}360361	pub fn set_collection_properties(362		collection: &NonfungibleHandle<T>,363		sender: &T::CrossAccountId,364		properties: Vec<Property>,365	) -> DispatchResult {366		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)367	}368369	pub fn set_property_permissions(370		collection: &CollectionHandle<T>,371		sender: &T::CrossAccountId,372		property_permissions: Vec<PropertyKeyPermission>,373	) -> DispatchResult {374		<PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)375	}376377	pub fn transfer(378		collection: &NonfungibleHandle<T>,379		from: &T::CrossAccountId,380		to: &T::CrossAccountId,381		token: TokenId,382		nesting_budget: &dyn Budget,383	) -> DispatchResult {384		ensure!(385			collection.limits.transfers_enabled(),386			<CommonError<T>>::TransferNotAllowed387		);388389		let token_data =390			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;391		// TODO: require sender to be token, owner, require admins to go through transfer_from392		ensure!(393			&token_data.owner == from394				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),395			<CommonError<T>>::NoPermission396		);397398		if collection.access == AccessMode::AllowList {399			collection.check_allowlist(from)?;400			collection.check_allowlist(to)?;401		}402		<PalletCommon<T>>::ensure_correct_receiver(to)?;403404		let balance_from = <AccountBalance<T>>::get((collection.id, from))405			.checked_sub(1)406			.ok_or(<CommonError<T>>::TokenValueTooLow)?;407		let balance_to = if from != to {408			let balance_to = <AccountBalance<T>>::get((collection.id, to))409				.checked_add(1)410				.ok_or(ArithmeticError::Overflow)?;411412			ensure!(413				balance_to < collection.limits.account_token_ownership_limit(),414				<CommonError<T>>::AccountTokenLimitExceeded,415			);416417			Some(balance_to)418		} else {419			None420		};421422		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {423			let handle = <CollectionHandle<T>>::try_get(target.0)?;424			let dispatch = T::CollectionDispatch::dispatch(handle);425			let dispatch = dispatch.as_dyn();426427			dispatch.check_nesting(428				from.clone(),429				(collection.id, token),430				target.1,431				nesting_budget,432			)?;433		}434435		// =========436437		<TokenData<T>>::insert(438			(collection.id, token),439			ItemData {440				owner: to.clone(),441				..token_data442			},443		);444445		if let Some(balance_to) = balance_to {446			// from != to447			if balance_from == 0 {448				<AccountBalance<T>>::remove((collection.id, from));449			} else {450				<AccountBalance<T>>::insert((collection.id, from), balance_from);451			}452			<AccountBalance<T>>::insert((collection.id, to), balance_to);453			<Owned<T>>::remove((collection.id, from, token));454			<Owned<T>>::insert((collection.id, to, token), true);455		}456		Self::set_allowance_unchecked(collection, from, token, None, true);457458		collection.log_mirrored(ERC721Events::Transfer {459			from: *from.as_eth(),460			to: *to.as_eth(),461			token_id: token.into(),462		});463		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(464			collection.id,465			token,466			from.clone(),467			to.clone(),468			1,469		));470		Ok(())471	}472473	pub fn create_multiple_items(474		collection: &NonfungibleHandle<T>,475		sender: &T::CrossAccountId,476		data: Vec<CreateItemData<T>>,477		nesting_budget: &dyn Budget,478	) -> DispatchResult {479		if !collection.is_owner_or_admin(sender) {480			ensure!(481				collection.mint_mode,482				<CommonError<T>>::PublicMintingNotAllowed483			);484			collection.check_allowlist(sender)?;485486			for item in data.iter() {487				collection.check_allowlist(&item.owner)?;488			}489		}490491		for data in data.iter() {492			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;493		}494495		let first_token = <TokensMinted<T>>::get(collection.id);496		let tokens_minted = first_token497			.checked_add(data.len() as u32)498			.ok_or(ArithmeticError::Overflow)?;499		ensure!(500			tokens_minted <= collection.limits.token_limit(),501			<CommonError<T>>::CollectionTokenLimitExceeded502		);503504		let mut balances = BTreeMap::new();505		for data in &data {506			let balance = balances507				.entry(&data.owner)508				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));509			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;510511			ensure!(512				*balance <= collection.limits.account_token_ownership_limit(),513				<CommonError<T>>::AccountTokenLimitExceeded,514			);515		}516517		for (i, data) in data.iter().enumerate() {518			let token = TokenId(first_token + i as u32 + 1);519			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {520				let handle = <CollectionHandle<T>>::try_get(target.0)?;521				let dispatch = T::CollectionDispatch::dispatch(handle);522				let dispatch = dispatch.as_dyn();523				dispatch.check_nesting(524					sender.clone(),525					(collection.id, token),526					target.1,527					nesting_budget,528				)?;529			}530		}531532		// =========533534		<TokensMinted<T>>::insert(collection.id, tokens_minted);535		for (account, balance) in balances {536			<AccountBalance<T>>::insert((collection.id, account), balance);537		}538		for (i, data) in data.into_iter().enumerate() {539			let token = first_token + i as u32 + 1;540541			<TokenData<T>>::insert(542				(collection.id, token),543				ItemData {544					const_data: data.const_data,545					variable_data: data.variable_data,546					owner: data.owner.clone(),547				},548			);549			<Owned<T>>::insert((collection.id, &data.owner, token), true);550551			collection.log_mirrored(ERC721Events::Transfer {552				from: H160::default(),553				to: *data.owner.as_eth(),554				token_id: token.into(),555			});556			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(557				collection.id,558				TokenId(token),559				data.owner.clone(),560				1,561			));562		}563		Ok(())564	}565566	pub fn set_allowance_unchecked(567		collection: &NonfungibleHandle<T>,568		sender: &T::CrossAccountId,569		token: TokenId,570		spender: Option<&T::CrossAccountId>,571		assume_implicit_eth: bool,572	) {573		if let Some(spender) = spender {574			let old_spender = <Allowance<T>>::get((collection.id, token));575			<Allowance<T>>::insert((collection.id, token), spender);576			// In ERC721 there is only one possible approved user of token, so we set577			// approved user to spender578			collection.log_mirrored(ERC721Events::Approval {579				owner: *sender.as_eth(),580				approved: *spender.as_eth(),581				token_id: token.into(),582			});583			// In Unique chain, any token can have any amount of approved users, so we need to584			// set allowance of old owner to 0, and allowance of new owner to 1585			if old_spender.as_ref() != Some(spender) {586				if let Some(old_owner) = old_spender {587					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(588						collection.id,589						token,590						sender.clone(),591						old_owner,592						0,593					));594				}595				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(596					collection.id,597					token,598					sender.clone(),599					spender.clone(),600					1,601				));602			}603		} else {604			let old_spender = <Allowance<T>>::take((collection.id, token));605			if !assume_implicit_eth {606				// In ERC721 there is only one possible approved user of token, so we set607				// approved user to zero address608				collection.log_mirrored(ERC721Events::Approval {609					owner: *sender.as_eth(),610					approved: H160::default(),611					token_id: token.into(),612				});613			}614			// In Unique chain, any token can have any amount of approved users, so we need to615			// set allowance of old owner to 0616			if let Some(old_spender) = old_spender {617				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(618					collection.id,619					token,620					sender.clone(),621					old_spender,622					0,623				));624			}625		}626	}627628	pub fn set_allowance(629		collection: &NonfungibleHandle<T>,630		sender: &T::CrossAccountId,631		token: TokenId,632		spender: Option<&T::CrossAccountId>,633	) -> DispatchResult {634		if collection.access == AccessMode::AllowList {635			collection.check_allowlist(sender)?;636			if let Some(spender) = spender {637				collection.check_allowlist(spender)?;638			}639		}640641		if let Some(spender) = spender {642			<PalletCommon<T>>::ensure_correct_receiver(spender)?;643		}644		let token_data =645			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;646		if &token_data.owner != sender {647			ensure!(648				collection.ignores_owned_amount(sender),649				<CommonError<T>>::CantApproveMoreThanOwned650			);651		}652653		// =========654655		Self::set_allowance_unchecked(collection, sender, token, spender, false);656		Ok(())657	}658659	fn check_allowed(660		collection: &NonfungibleHandle<T>,661		spender: &T::CrossAccountId,662		from: &T::CrossAccountId,663		token: TokenId,664		nesting_budget: &dyn Budget,665	) -> DispatchResult {666		if spender.conv_eq(from) {667			return Ok(());668		}669		if collection.access == AccessMode::AllowList {670			// `from`, `to` checked in [`transfer`]671			collection.check_allowlist(spender)?;672		}673		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {674			// TODO: should collection owner be allowed to perform this transfer?675			ensure!(676				<PalletStructure<T>>::check_indirectly_owned(677					spender.clone(),678					source.0,679					source.1,680					None,681					nesting_budget682				)?,683				<CommonError<T>>::ApprovedValueTooLow,684			);685			return Ok(());686		}687		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {688			return Ok(());689		}690		ensure!(691			collection.ignores_allowance(spender),692			<CommonError<T>>::ApprovedValueTooLow693		);694		Ok(())695	}696697	pub fn transfer_from(698		collection: &NonfungibleHandle<T>,699		spender: &T::CrossAccountId,700		from: &T::CrossAccountId,701		to: &T::CrossAccountId,702		token: TokenId,703		nesting_budget: &dyn Budget,704	) -> DispatchResult {705		Self::check_allowed(collection, spender, from, token, nesting_budget)?;706707		// =========708709		// Allowance is reset in [`transfer`]710		Self::transfer(collection, from, to, token, nesting_budget)711	}712713	pub fn burn_from(714		collection: &NonfungibleHandle<T>,715		spender: &T::CrossAccountId,716		from: &T::CrossAccountId,717		token: TokenId,718		nesting_budget: &dyn Budget,719	) -> DispatchResult {720		Self::check_allowed(collection, spender, from, token, nesting_budget)?;721722		// =========723724		Self::burn(collection, from, token)725	}726727	pub fn set_variable_metadata(728		collection: &NonfungibleHandle<T>,729		sender: &T::CrossAccountId,730		token: TokenId,731		data: BoundedVec<u8, CustomDataLimit>,732	) -> DispatchResult {733		let token_data =734			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;735		collection.check_can_update_meta(sender, &token_data.owner)?;736737		// =========738739		<TokenData<T>>::insert(740			(collection.id, token),741			ItemData {742				variable_data: data,743				..token_data744			},745		);746		Ok(())747	}748749	pub fn check_nesting(750		handle: &NonfungibleHandle<T>,751		sender: T::CrossAccountId,752		from: (CollectionId, TokenId),753		under: TokenId,754		nesting_budget: &dyn Budget,755	) -> DispatchResult {756		fn ensure_sender_allowed<T: Config>(757			collection: CollectionId,758			token: TokenId,759			for_nest: (CollectionId, TokenId),760			sender: T::CrossAccountId,761			budget: &dyn Budget,762		) -> DispatchResult {763			ensure!(764				<PalletStructure<T>>::check_indirectly_owned(765					sender,766					collection,767					token,768					Some(for_nest),769					budget770				)?,771				<CommonError<T>>::OnlyOwnerAllowedToNest,772			);773			Ok(())774		}775		match handle.limits.nesting_rule() {776			NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),777			NestingRule::Owner => {778				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?779			}780			NestingRule::OwnerRestricted(whitelist) => {781				ensure!(782					whitelist.contains(&from.0),783					<CommonError<T>>::SourceCollectionIsNotAllowedToNest784				);785				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?786			}787		}788		Ok(())789	}790791	/// Delegated to `create_multiple_items`792	pub fn create_item(793		collection: &NonfungibleHandle<T>,794		sender: &T::CrossAccountId,795		data: CreateItemData<T>,796		nesting_budget: &dyn Budget,797	) -> DispatchResult {798		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)799	}800}
after · pallets/nonfungible/src/lib.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use frame_support::{BoundedVec, ensure, fail};21use up_data_structs::{22	AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,23	mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,24	PropertyKey, PropertyKeyPermission,25};26use pallet_evm::account::CrossAccountId;27use pallet_common::{28	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,29	dispatch::CollectionDispatch,30};31use pallet_structure::Pallet as PalletStructure;32use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};33use sp_core::H160;34use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};35use sp_std::{vec::Vec, vec};36use core::ops::Deref;37use sp_std::collections::btree_map::BTreeMap;38use codec::{Encode, Decode, MaxEncodedLen};39use scale_info::TypeInfo;4041pub use pallet::*;42#[cfg(feature = "runtime-benchmarks")]43pub mod benchmarking;44pub mod common;45pub mod erc;46pub mod weights;4748pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;49pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5051#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]52pub struct ItemData<CrossAccountId> {53	pub const_data: BoundedVec<u8, CustomDataLimit>,54	pub variable_data: BoundedVec<u8, CustomDataLimit>,55	pub owner: CrossAccountId,56}5758#[frame_support::pallet]59pub mod pallet {60	use super::*;61	use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};62	use up_data_structs::{CollectionId, TokenId};63	use super::weights::WeightInfo;6465	#[pallet::error]66	pub enum Error<T> {67		/// Not Nonfungible item data used to mint in Nonfungible collection.68		NotNonfungibleDataUsedToMintFungibleCollectionToken,69		/// Used amount > 1 with NFT70		NonfungibleItemsHaveNoAmount,71	}7273	#[pallet::config]74	pub trait Config:75		frame_system::Config + pallet_common::Config + pallet_structure::Config76	{77		type WeightInfo: WeightInfo;78	}7980	#[pallet::pallet]81	#[pallet::generate_store(pub(super) trait Store)]82	pub struct Pallet<T>(_);8384	#[pallet::storage]85	pub type TokensMinted<T: Config> =86		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;87	#[pallet::storage]88	pub type TokensBurnt<T: Config> =89		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;9091	#[pallet::storage]92	pub type TokenData<T: Config> = StorageNMap<93		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),94		Value = ItemData<T::CrossAccountId>,95		QueryKind = OptionQuery,96	>;9798	#[pallet::storage]99	pub type TokenProperties<T: Config> = StorageNMap<100		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),101		Value = up_data_structs::Properties,102		QueryKind = ValueQuery,103		OnEmpty = up_data_structs::TokenProperties,104	>;105106	/// Used to enumerate tokens owned by account107	#[pallet::storage]108	pub type Owned<T: Config> = StorageNMap<109		Key = (110			Key<Twox64Concat, CollectionId>,111			Key<Blake2_128Concat, T::CrossAccountId>,112			Key<Twox64Concat, TokenId>,113		),114		Value = bool,115		QueryKind = ValueQuery,116	>;117118	#[pallet::storage]119	pub type AccountBalance<T: Config> = StorageNMap<120		Key = (121			Key<Twox64Concat, CollectionId>,122			Key<Blake2_128Concat, T::CrossAccountId>,123		),124		Value = u32,125		QueryKind = ValueQuery,126	>;127128	#[pallet::storage]129	pub type Allowance<T: Config> = StorageNMap<130		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),131		Value = T::CrossAccountId,132		QueryKind = OptionQuery,133	>;134}135136pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);137impl<T: Config> NonfungibleHandle<T> {138	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {139		Self(inner)140	}141	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {142		self.0143	}144}145impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {146	fn recorder(&self) -> &SubstrateRecorder<T> {147		self.0.recorder()148	}149	fn into_recorder(self) -> SubstrateRecorder<T> {150		self.0.into_recorder()151	}152}153impl<T: Config> Deref for NonfungibleHandle<T> {154	type Target = pallet_common::CollectionHandle<T>;155156	fn deref(&self) -> &Self::Target {157		&self.0158	}159}160161impl<T: Config> Pallet<T> {162	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {163		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)164	}165	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {166		<TokenData<T>>::contains_key((collection.id, token))167	}168}169170// unchecked calls skips any permission checks171impl<T: Config> Pallet<T> {172	pub fn init_collection(173		owner: T::AccountId,174		data: CreateCollectionData<T::AccountId>,175	) -> Result<CollectionId, DispatchError> {176		<PalletCommon<T>>::init_collection(owner, data)177	}178	pub fn destroy_collection(179		collection: NonfungibleHandle<T>,180		sender: &T::CrossAccountId,181	) -> DispatchResult {182		let id = collection.id;183184		// =========185186		PalletCommon::destroy_collection(collection.0, sender)?;187188		<TokenData<T>>::remove_prefix((id,), None);189		<Owned<T>>::remove_prefix((id,), None);190		<TokensMinted<T>>::remove(id);191		<TokensBurnt<T>>::remove(id);192		<Allowance<T>>::remove_prefix((id,), None);193		<AccountBalance<T>>::remove_prefix((id,), None);194		Ok(())195	}196197	pub fn burn(198		collection: &NonfungibleHandle<T>,199		sender: &T::CrossAccountId,200		token: TokenId,201	) -> DispatchResult {202		let token_data =203			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;204		ensure!(205			&token_data.owner == sender206				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),207			<CommonError<T>>::NoPermission208		);209210		if collection.access == AccessMode::AllowList {211			collection.check_allowlist(sender)?;212		}213214		let burnt = <TokensBurnt<T>>::get(collection.id)215			.checked_add(1)216			.ok_or(ArithmeticError::Overflow)?;217218		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))219			.checked_sub(1)220			.ok_or(ArithmeticError::Overflow)?;221222		if balance == 0 {223			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));224		} else {225			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);226		}227		// =========228229		<Owned<T>>::remove((collection.id, &token_data.owner, token));230		<TokensBurnt<T>>::insert(collection.id, burnt);231		<TokenData<T>>::remove((collection.id, token));232		let old_spender = <Allowance<T>>::take((collection.id, token));233234		if let Some(old_spender) = old_spender {235			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(236				collection.id,237				token,238				sender.clone(),239				old_spender,240				0,241			));242		}243244		collection.log_mirrored(ERC721Events::Transfer {245			from: *token_data.owner.as_eth(),246			to: H160::default(),247			token_id: token.into(),248		});249		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(250			collection.id,251			token,252			token_data.owner,253			1,254		));255		Ok(())256	}257258	pub fn set_token_property(259		collection: &NonfungibleHandle<T>,260		sender: &T::CrossAccountId,261		token_id: TokenId,262		property: Property,263	) -> DispatchResult {264		Self::check_token_change_permission(collection, sender, token_id, &property.key)?;265266		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {267			properties.try_set_property(property.clone())268		})?;269270		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(271			collection.id,272			token_id,273			property,274		));275276		Ok(())277	}278279	pub fn set_token_properties(280		collection: &NonfungibleHandle<T>,281		sender: &T::CrossAccountId,282		token_id: TokenId,283		properties: Vec<Property>,284	) -> DispatchResult {285		for property in properties {286			Self::set_token_property(collection, sender, token_id, property)?;287		}288289		Ok(())290	}291292	pub fn delete_token_property(293		collection: &NonfungibleHandle<T>,294		sender: &T::CrossAccountId,295		token_id: TokenId,296		property_key: PropertyKey,297	) -> DispatchResult {298		Self::check_token_change_permission(collection, sender, token_id, &property_key)?;299300		<TokenProperties<T>>::mutate((collection.id, token_id), |properties| {301			properties.remove_property(&property_key);302		});303304		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(305			collection.id,306			token_id,307			property_key,308		));309310		Ok(())311	}312313	fn check_token_change_permission(314		collection: &NonfungibleHandle<T>,315		sender: &T::CrossAccountId,316		token_id: TokenId,317		property_key: &PropertyKey,318	) -> DispatchResult {319		let permission = <PalletCommon<T>>::property_permission(collection.id)320			.get(property_key)321			.map(|p| p.clone())322			.unwrap_or(PropertyPermission::None);323324		let token_data = <TokenData<T>>::get((collection.id, token_id))325			.ok_or(<CommonError<T>>::TokenNotFound)?;326327		let check_token_owner = || -> DispatchResult {328			ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);329			Ok(())330		};331332		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))333			.get_property(property_key)334			.is_some();335336		match (permission, is_property_exists) {337			(PropertyPermission::AdminConst, false) => collection.check_is_owner_or_admin(sender),338			(PropertyPermission::Admin, _) => collection.check_is_owner_or_admin(sender),339			(PropertyPermission::ItemOwnerConst, false) => check_token_owner(),340			(PropertyPermission::ItemOwner, _) => check_token_owner(),341			(PropertyPermission::ItemOwnerOrAdmin, _) => {342				check_token_owner().or(collection.check_is_owner_or_admin(sender))343			}344			_ => Err(<CommonError<T>>::NoPermission.into()),345		}346	}347348	pub fn delete_token_properties(349		collection: &NonfungibleHandle<T>,350		sender: &T::CrossAccountId,351		token_id: TokenId,352		property_keys: Vec<PropertyKey>,353	) -> DispatchResult {354		for key in property_keys {355			Self::delete_token_property(collection, sender, token_id, key)?;356		}357358		Ok(())359	}360361	pub fn set_collection_properties(362		collection: &NonfungibleHandle<T>,363		sender: &T::CrossAccountId,364		properties: Vec<Property>,365	) -> DispatchResult {366		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)367	}368369	pub fn delete_collection_properties(370		collection: &CollectionHandle<T>,371		sender: &T::CrossAccountId,372		property_keys: Vec<PropertyKey>,373	) -> DispatchResult {374		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)375	}376377	pub fn set_property_permissions(378		collection: &CollectionHandle<T>,379		sender: &T::CrossAccountId,380		property_permissions: Vec<PropertyKeyPermission>,381	) -> DispatchResult {382		<PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)383	}384385	pub fn transfer(386		collection: &NonfungibleHandle<T>,387		from: &T::CrossAccountId,388		to: &T::CrossAccountId,389		token: TokenId,390		nesting_budget: &dyn Budget,391	) -> DispatchResult {392		ensure!(393			collection.limits.transfers_enabled(),394			<CommonError<T>>::TransferNotAllowed395		);396397		let token_data =398			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;399		// TODO: require sender to be token, owner, require admins to go through transfer_from400		ensure!(401			&token_data.owner == from402				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),403			<CommonError<T>>::NoPermission404		);405406		if collection.access == AccessMode::AllowList {407			collection.check_allowlist(from)?;408			collection.check_allowlist(to)?;409		}410		<PalletCommon<T>>::ensure_correct_receiver(to)?;411412		let balance_from = <AccountBalance<T>>::get((collection.id, from))413			.checked_sub(1)414			.ok_or(<CommonError<T>>::TokenValueTooLow)?;415		let balance_to = if from != to {416			let balance_to = <AccountBalance<T>>::get((collection.id, to))417				.checked_add(1)418				.ok_or(ArithmeticError::Overflow)?;419420			ensure!(421				balance_to < collection.limits.account_token_ownership_limit(),422				<CommonError<T>>::AccountTokenLimitExceeded,423			);424425			Some(balance_to)426		} else {427			None428		};429430		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {431			let handle = <CollectionHandle<T>>::try_get(target.0)?;432			let dispatch = T::CollectionDispatch::dispatch(handle);433			let dispatch = dispatch.as_dyn();434435			dispatch.check_nesting(436				from.clone(),437				(collection.id, token),438				target.1,439				nesting_budget,440			)?;441		}442443		// =========444445		<TokenData<T>>::insert(446			(collection.id, token),447			ItemData {448				owner: to.clone(),449				..token_data450			},451		);452453		if let Some(balance_to) = balance_to {454			// from != to455			if balance_from == 0 {456				<AccountBalance<T>>::remove((collection.id, from));457			} else {458				<AccountBalance<T>>::insert((collection.id, from), balance_from);459			}460			<AccountBalance<T>>::insert((collection.id, to), balance_to);461			<Owned<T>>::remove((collection.id, from, token));462			<Owned<T>>::insert((collection.id, to, token), true);463		}464		Self::set_allowance_unchecked(collection, from, token, None, true);465466		collection.log_mirrored(ERC721Events::Transfer {467			from: *from.as_eth(),468			to: *to.as_eth(),469			token_id: token.into(),470		});471		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(472			collection.id,473			token,474			from.clone(),475			to.clone(),476			1,477		));478		Ok(())479	}480481	pub fn create_multiple_items(482		collection: &NonfungibleHandle<T>,483		sender: &T::CrossAccountId,484		data: Vec<CreateItemData<T>>,485		nesting_budget: &dyn Budget,486	) -> DispatchResult {487		if !collection.is_owner_or_admin(sender) {488			ensure!(489				collection.mint_mode,490				<CommonError<T>>::PublicMintingNotAllowed491			);492			collection.check_allowlist(sender)?;493494			for item in data.iter() {495				collection.check_allowlist(&item.owner)?;496			}497		}498499		for data in data.iter() {500			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;501		}502503		let first_token = <TokensMinted<T>>::get(collection.id);504		let tokens_minted = first_token505			.checked_add(data.len() as u32)506			.ok_or(ArithmeticError::Overflow)?;507		ensure!(508			tokens_minted <= collection.limits.token_limit(),509			<CommonError<T>>::CollectionTokenLimitExceeded510		);511512		let mut balances = BTreeMap::new();513		for data in &data {514			let balance = balances515				.entry(&data.owner)516				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));517			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;518519			ensure!(520				*balance <= collection.limits.account_token_ownership_limit(),521				<CommonError<T>>::AccountTokenLimitExceeded,522			);523		}524525		for (i, data) in data.iter().enumerate() {526			let token = TokenId(first_token + i as u32 + 1);527			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {528				let handle = <CollectionHandle<T>>::try_get(target.0)?;529				let dispatch = T::CollectionDispatch::dispatch(handle);530				let dispatch = dispatch.as_dyn();531				dispatch.check_nesting(532					sender.clone(),533					(collection.id, token),534					target.1,535					nesting_budget,536				)?;537			}538		}539540		// =========541542		<TokensMinted<T>>::insert(collection.id, tokens_minted);543		for (account, balance) in balances {544			<AccountBalance<T>>::insert((collection.id, account), balance);545		}546		for (i, data) in data.into_iter().enumerate() {547			let token = first_token + i as u32 + 1;548549			<TokenData<T>>::insert(550				(collection.id, token),551				ItemData {552					const_data: data.const_data,553					variable_data: data.variable_data,554					owner: data.owner.clone(),555				},556			);557			<Owned<T>>::insert((collection.id, &data.owner, token), true);558559			collection.log_mirrored(ERC721Events::Transfer {560				from: H160::default(),561				to: *data.owner.as_eth(),562				token_id: token.into(),563			});564			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(565				collection.id,566				TokenId(token),567				data.owner.clone(),568				1,569			));570		}571		Ok(())572	}573574	pub fn set_allowance_unchecked(575		collection: &NonfungibleHandle<T>,576		sender: &T::CrossAccountId,577		token: TokenId,578		spender: Option<&T::CrossAccountId>,579		assume_implicit_eth: bool,580	) {581		if let Some(spender) = spender {582			let old_spender = <Allowance<T>>::get((collection.id, token));583			<Allowance<T>>::insert((collection.id, token), spender);584			// In ERC721 there is only one possible approved user of token, so we set585			// approved user to spender586			collection.log_mirrored(ERC721Events::Approval {587				owner: *sender.as_eth(),588				approved: *spender.as_eth(),589				token_id: token.into(),590			});591			// In Unique chain, any token can have any amount of approved users, so we need to592			// set allowance of old owner to 0, and allowance of new owner to 1593			if old_spender.as_ref() != Some(spender) {594				if let Some(old_owner) = old_spender {595					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(596						collection.id,597						token,598						sender.clone(),599						old_owner,600						0,601					));602				}603				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(604					collection.id,605					token,606					sender.clone(),607					spender.clone(),608					1,609				));610			}611		} else {612			let old_spender = <Allowance<T>>::take((collection.id, token));613			if !assume_implicit_eth {614				// In ERC721 there is only one possible approved user of token, so we set615				// approved user to zero address616				collection.log_mirrored(ERC721Events::Approval {617					owner: *sender.as_eth(),618					approved: H160::default(),619					token_id: token.into(),620				});621			}622			// In Unique chain, any token can have any amount of approved users, so we need to623			// set allowance of old owner to 0624			if let Some(old_spender) = old_spender {625				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(626					collection.id,627					token,628					sender.clone(),629					old_spender,630					0,631				));632			}633		}634	}635636	pub fn set_allowance(637		collection: &NonfungibleHandle<T>,638		sender: &T::CrossAccountId,639		token: TokenId,640		spender: Option<&T::CrossAccountId>,641	) -> DispatchResult {642		if collection.access == AccessMode::AllowList {643			collection.check_allowlist(sender)?;644			if let Some(spender) = spender {645				collection.check_allowlist(spender)?;646			}647		}648649		if let Some(spender) = spender {650			<PalletCommon<T>>::ensure_correct_receiver(spender)?;651		}652		let token_data =653			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;654		if &token_data.owner != sender {655			ensure!(656				collection.ignores_owned_amount(sender),657				<CommonError<T>>::CantApproveMoreThanOwned658			);659		}660661		// =========662663		Self::set_allowance_unchecked(collection, sender, token, spender, false);664		Ok(())665	}666667	fn check_allowed(668		collection: &NonfungibleHandle<T>,669		spender: &T::CrossAccountId,670		from: &T::CrossAccountId,671		token: TokenId,672		nesting_budget: &dyn Budget,673	) -> DispatchResult {674		if spender.conv_eq(from) {675			return Ok(());676		}677		if collection.access == AccessMode::AllowList {678			// `from`, `to` checked in [`transfer`]679			collection.check_allowlist(spender)?;680		}681		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {682			// TODO: should collection owner be allowed to perform this transfer?683			ensure!(684				<PalletStructure<T>>::check_indirectly_owned(685					spender.clone(),686					source.0,687					source.1,688					None,689					nesting_budget690				)?,691				<CommonError<T>>::ApprovedValueTooLow,692			);693			return Ok(());694		}695		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {696			return Ok(());697		}698		ensure!(699			collection.ignores_allowance(spender),700			<CommonError<T>>::ApprovedValueTooLow701		);702		Ok(())703	}704705	pub fn transfer_from(706		collection: &NonfungibleHandle<T>,707		spender: &T::CrossAccountId,708		from: &T::CrossAccountId,709		to: &T::CrossAccountId,710		token: TokenId,711		nesting_budget: &dyn Budget,712	) -> DispatchResult {713		Self::check_allowed(collection, spender, from, token, nesting_budget)?;714715		// =========716717		// Allowance is reset in [`transfer`]718		Self::transfer(collection, from, to, token, nesting_budget)719	}720721	pub fn burn_from(722		collection: &NonfungibleHandle<T>,723		spender: &T::CrossAccountId,724		from: &T::CrossAccountId,725		token: TokenId,726		nesting_budget: &dyn Budget,727	) -> DispatchResult {728		Self::check_allowed(collection, spender, from, token, nesting_budget)?;729730		// =========731732		Self::burn(collection, from, token)733	}734735	pub fn set_variable_metadata(736		collection: &NonfungibleHandle<T>,737		sender: &T::CrossAccountId,738		token: TokenId,739		data: BoundedVec<u8, CustomDataLimit>,740	) -> DispatchResult {741		let token_data =742			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;743		collection.check_can_update_meta(sender, &token_data.owner)?;744745		// =========746747		<TokenData<T>>::insert(748			(collection.id, token),749			ItemData {750				variable_data: data,751				..token_data752			},753		);754		Ok(())755	}756757	pub fn check_nesting(758		handle: &NonfungibleHandle<T>,759		sender: T::CrossAccountId,760		from: (CollectionId, TokenId),761		under: TokenId,762		nesting_budget: &dyn Budget,763	) -> DispatchResult {764		fn ensure_sender_allowed<T: Config>(765			collection: CollectionId,766			token: TokenId,767			for_nest: (CollectionId, TokenId),768			sender: T::CrossAccountId,769			budget: &dyn Budget,770		) -> DispatchResult {771			ensure!(772				<PalletStructure<T>>::check_indirectly_owned(773					sender,774					collection,775					token,776					Some(for_nest),777					budget778				)?,779				<CommonError<T>>::OnlyOwnerAllowedToNest,780			);781			Ok(())782		}783		match handle.limits.nesting_rule() {784			NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),785			NestingRule::Owner => {786				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?787			}788			NestingRule::OwnerRestricted(whitelist) => {789				ensure!(790					whitelist.contains(&from.0),791					<CommonError<T>>::SourceCollectionIsNotAllowedToNest792				);793				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?794			}795		}796		Ok(())797	}798799	/// Delegated to `create_multiple_items`800	pub fn create_item(801		collection: &NonfungibleHandle<T>,802		sender: &T::CrossAccountId,803		data: CreateItemData<T>,804		nesting_budget: &dyn Budget,805	) -> DispatchResult {806		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)807	}808}
modifiedpallets/nonfungible/src/weights.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/weights.rs
+++ b/pallets/nonfungible/src/weights.rs
@@ -37,6 +37,7 @@
 	fn create_multiple_items_ex(b: u32, ) -> Weight;
 	fn burn_item() -> Weight;
 	fn set_collection_properties(amount: u32) -> Weight;
+	fn delete_collection_properties(amount: u32) -> Weight;
 	fn set_token_properties(amount: u32) -> Weight;
 	fn delete_token_properties(amount: u32) -> Weight;
 	fn set_property_permissions(amount: u32) -> Weight;
@@ -100,6 +101,11 @@
 		(50_000_000 as Weight).saturating_mul(amount as Weight)
 	}
 
+	fn delete_collection_properties(amount: u32) -> Weight {
+		// TODO calculate appropriate weight
+		(50_000_000 as Weight).saturating_mul(amount as Weight)
+	}
+
 	fn set_token_properties(amount: u32) -> Weight {
 		// TODO calculate appropriate weight
 		(50_000_000 as Weight).saturating_mul(amount as Weight)
@@ -210,6 +216,11 @@
 		(50_000_000 as Weight).saturating_mul(amount as Weight)
 	}
 
+	fn delete_collection_properties(amount: u32) -> Weight {
+		// TODO calculate appropriate weight
+		(50_000_000 as Weight).saturating_mul(amount as Weight)
+	}
+
 	fn set_token_properties(amount: u32) -> Weight {
 		// TODO calculate appropriate weight
 		(50_000_000 as Weight).saturating_mul(amount as Weight)
modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -70,6 +70,10 @@
 		<SelfWeightOf<T>>::set_collection_properties(amount)
 	}
 
+	fn delete_collection_properties(amount: u32) -> Weight {
+		<SelfWeightOf<T>>::delete_collection_properties(amount)
+	}
+
 	fn set_token_properties(amount: u32) -> Weight {
 		<SelfWeightOf<T>>::set_token_properties(amount)
 	}
@@ -268,6 +272,14 @@
 		fail!(<Error<T>>::PropertiesNotAllowed)
 	}
 
+	fn delete_collection_properties(
+		&self,
+		_sender: &T::CrossAccountId,
+		_property_keys: Vec<PropertyKey>,
+	) -> DispatchResultWithPostInfo {
+		fail!(<Error<T>>::PropertiesNotAllowed)
+	}
+
 	fn set_token_properties(
 		&self,
 		_sender: T::CrossAccountId,
modifiedpallets/refungible/src/weights.rsdiffbeforeafterboth
--- a/pallets/refungible/src/weights.rs
+++ b/pallets/refungible/src/weights.rs
@@ -39,6 +39,7 @@
 	fn burn_item_partial() -> Weight;
 	fn burn_item_fully() -> Weight;
 	fn set_collection_properties(amount: u32) -> Weight;
+	fn delete_collection_properties(amount: u32) -> Weight;
 	fn set_token_properties(amount: u32) -> Weight;
 	fn delete_token_properties(amount: u32) -> Weight;
 	fn set_property_permissions(amount: u32) -> Weight;
@@ -139,6 +140,11 @@
 		0
 	}
 
+	fn delete_collection_properties(_amount: u32) -> Weight {
+		// Error
+		0
+	}
+
 	fn set_token_properties(_amount: u32) -> Weight {
 		// Error
 		0
@@ -328,6 +334,11 @@
 		0
 	}
 
+	fn delete_collection_properties(_amount: u32) -> Weight {
+		// Error
+		0
+	}
+
 	fn set_token_properties(_amount: u32) -> Weight {
 		// Error
 		0
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -708,6 +708,20 @@
 			dispatch_call::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))
 		}
 
+		#[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]
+		#[transactional]
+		pub fn delete_collection_properties(
+			origin,
+			collection_id: CollectionId,
+			property_keys: Vec<PropertyKey>,
+		) -> DispatchResultWithPostInfo {
+			ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);
+
+			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
+
+			dispatch_call::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))
+		}
+
 		#[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]
 		#[transactional]
 		pub fn set_token_properties(
@@ -723,19 +737,19 @@
 			dispatch_call::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties))
 		}
 
-		#[weight = T::CommonWeightInfo::delete_token_properties(properties.len() as u32)]
+		#[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]
 		#[transactional]
 		pub fn delete_token_properties(
 			origin,
 			collection_id: CollectionId,
 			token_id: TokenId,
-			properties: Vec<PropertyKey>
+			property_keys: Vec<PropertyKey>
 		) -> DispatchResultWithPostInfo {
-			ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);
+			ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);
 
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 
-			dispatch_call::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, properties))
+			dispatch_call::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys))
 		}
 
 		#[weight = T::CommonWeightInfo::set_property_permissions(property_permissions.len() as u32)]
modifiedruntime/common/src/weights.rsdiffbeforeafterboth
--- a/runtime/common/src/weights.rs
+++ b/runtime/common/src/weights.rs
@@ -58,6 +58,10 @@
 		dispatch_weight::<T>() + max_weight_of!(set_collection_properties(amount))
 	}
 
+	fn delete_collection_properties(amount: u32) -> Weight {
+		dispatch_weight::<T>() + max_weight_of!(delete_collection_properties(amount))
+	}
+
 	fn set_token_properties(amount: u32) -> Weight {
 		dispatch_weight::<T>() + max_weight_of!(set_token_properties(amount))
 	}