git.delta.rocks / unique-network / refs/commits / 0427b9aac8a2

difftreelog

CORE-410 Adapt to prop check root owner

Trubnikov Sergey2022-07-07parent: #e574f04.patch.diff
in: master

3 files changed

modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -327,12 +327,19 @@
 		sender: T::CrossAccountId,
 		token_id: TokenId,
 		properties: Vec<Property>,
-		_nesting_budget: &dyn Budget,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		let weight = <CommonWeights<T>>::set_token_properties(properties.len() as u32);
 
 		with_weight(
-			<Pallet<T>>::set_token_properties(self, &sender, token_id, properties, false),
+			<Pallet<T>>::set_token_properties(
+				self,
+				&sender,
+				token_id,
+				properties.into_iter(),
+				false,
+				nesting_budget,
+			),
 			weight,
 		)
 	}
@@ -356,12 +363,18 @@
 		sender: T::CrossAccountId,
 		token_id: TokenId,
 		property_keys: Vec<PropertyKey>,
-		_nesting_budget: &dyn Budget,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
 		let weight = <CommonWeights<T>>::delete_token_properties(property_keys.len() as u32);
 
 		with_weight(
-			<Pallet<T>>::delete_token_properties(self, &sender, token_id, property_keys),
+			<Pallet<T>>::delete_token_properties(
+				self,
+				&sender,
+				token_id,
+				property_keys.into_iter(),
+				nesting_budget,
+			),
 			weight,
 		)
 	}
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
before · pallets/refungible/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//! # Refungible Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Refungible pallet provides functions for:29//!30//! - RFT collection creation and removal31//! - Minting and burning of RFT tokens32//! - Partition and repartition of RFT tokens33//! - Retrieving number of pieces of RFT token34//! - Retrieving account balances35//! - Transfering RFT token pieces36//! - Burning RFT token pieces37//! - Setting and checking allowance for RFT tokens38//!39//! ### Terminology40//!41//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all42//!   of the RFT token pieces than it owns the RFT token and can repartition it.43//!44//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.45//!   Each collection has its own settings and set of permissions.46//!47//! - **RFT token piece:** A fungible part of an RFT token.48//!49//! - **Balance:** RFT token pieces owned by an account50//!51//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to52//!   transfer from the balance of another account53//!54//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from55//!   an account balance.56//!57//! ### Implementations58//!59//! The Refungible pallet provides implementations for the following traits. If these traits provide60//! the functionality that you need, then you can avoid coupling with the Refungible pallet.61//!62//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight63//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing64//!   with collections65//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible66//!   collection67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for73//!   some accounts.74//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.75//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.76//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.77//!   Nests the RFT token if RFT token pieces are sent to another token.78//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.80//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.81//!82//! ## Assumptions83//!84//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.85//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.86//! * Sender should be in collection's allow list to perform operations on tokens.8788#![cfg_attr(not(feature = "std"), no_std)]8990use frame_support::{ensure, BoundedVec, transactional, storage::with_transaction};91use up_data_structs::{92	AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,93	CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget,94	Property, PropertyScope, TrySetProperty, PropertyKey, PropertyPermission, PropertyKeyPermission95};96use pallet_evm::account::CrossAccountId;97use pallet_common::{Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, CommonCollectionOperations as _};98use pallet_structure::Pallet as PalletStructure;99use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};100use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};101use core::ops::Deref;102use codec::{Encode, Decode, MaxEncodedLen};103use scale_info::TypeInfo;104105pub use pallet::*;106#[cfg(feature = "runtime-benchmarks")]107pub mod benchmarking;108pub mod common;109pub mod erc;110pub mod weights;111pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;112113#[struct_versioning::versioned(version = 2, upper)]114#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]115pub struct ItemData {116	pub const_data: BoundedVec<u8, CustomDataLimit>,117118	#[version(..2)]119	pub variable_data: BoundedVec<u8, CustomDataLimit>,120}121122#[frame_support::pallet]123pub mod pallet {124	use super::*;125	use frame_support::{126		Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,127		traits::StorageVersion,128	};129	use frame_system::pallet_prelude::*;130	use up_data_structs::{CollectionId, TokenId};131	use super::weights::WeightInfo;132133	#[pallet::error]134	pub enum Error<T> {135		/// Not Refungible item data used to mint in Refungible collection.136		NotRefungibleDataUsedToMintFungibleCollectionToken,137		/// Maximum refungibility exceeded138		WrongRefungiblePieces,139		/// Refungible token can't be repartitioned by user who isn't owns all pieces140		RepartitionWhileNotOwningAllPieces,141		/// Refungible token can't nest other tokens142		RefungibleDisallowsNesting,143		/// Setting item properties is not allowed144		SettingPropertiesNotAllowed,145	}146147	#[pallet::config]148	pub trait Config:149		frame_system::Config + pallet_common::Config + pallet_structure::Config150	{151		type WeightInfo: WeightInfo;152	}153154	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);155156	#[pallet::pallet]157	#[pallet::storage_version(STORAGE_VERSION)]158	#[pallet::generate_store(pub(super) trait Store)]159	pub struct Pallet<T>(_);160161	/// Amount of tokens minted for collection162	#[pallet::storage]163	pub type TokensMinted<T: Config> =164		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;165166	/// Amount of burnt tokens for collection167	#[pallet::storage]168	pub type TokensBurnt<T: Config> =169		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;170171	/// Custom data serialized to bytes for token172	#[pallet::storage]173	pub type TokenData<T: Config> = StorageNMap<174		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),175		Value = ItemData,176		QueryKind = ValueQuery,177	>;178179	#[pallet::storage]180	#[pallet::getter(fn token_properties)]181	pub type TokenProperties<T: Config> = StorageNMap<182		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),183		Value = up_data_structs::Properties,184		QueryKind = ValueQuery,185		OnEmpty = up_data_structs::TokenProperties,186	>;187188	/// Total amount of pieces for token189	#[pallet::storage]190	pub type TotalSupply<T: Config> = StorageNMap<191		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),192		Value = u128,193		QueryKind = ValueQuery,194	>;195196	/// Used to enumerate tokens owned by account197	#[pallet::storage]198	pub type Owned<T: Config> = StorageNMap<199		Key = (200			Key<Twox64Concat, CollectionId>,201			Key<Blake2_128Concat, T::CrossAccountId>,202			Key<Twox64Concat, TokenId>,203		),204		Value = bool,205		QueryKind = ValueQuery,206	>;207208	/// Amount of tokens owned by account209	#[pallet::storage]210	pub type AccountBalance<T: Config> = StorageNMap<211		Key = (212			Key<Twox64Concat, CollectionId>,213			// Owner214			Key<Blake2_128Concat, T::CrossAccountId>,215		),216		Value = u32,217		QueryKind = ValueQuery,218	>;219220	/// Amount of token pieces owned by account221	#[pallet::storage]222	pub type Balance<T: Config> = StorageNMap<223		Key = (224			Key<Twox64Concat, CollectionId>,225			Key<Twox64Concat, TokenId>,226			// Owner227			Key<Blake2_128Concat, T::CrossAccountId>,228		),229		Value = u128,230		QueryKind = ValueQuery,231	>;232233	/// Allowance set by an owner for a spender for a token234	#[pallet::storage]235	pub type Allowance<T: Config> = StorageNMap<236		Key = (237			Key<Twox64Concat, CollectionId>,238			Key<Twox64Concat, TokenId>,239			// Owner240			Key<Blake2_128, T::CrossAccountId>,241			// Spender242			Key<Blake2_128Concat, T::CrossAccountId>,243		),244		Value = u128,245		QueryKind = ValueQuery,246	>;247248	#[pallet::hooks]249	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {250		fn on_runtime_upgrade() -> Weight {251			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {252				<TokenData<T>>::translate_values::<ItemDataVersion1, _>(|v| {253					Some(<ItemDataVersion2>::from(v))254				})255			}256257			0258		}259	}260}261262pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);263impl<T: Config> RefungibleHandle<T> {264	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {265		Self(inner)266	}267	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {268		self.0269	}270}271impl<T: Config> Deref for RefungibleHandle<T> {272	type Target = pallet_common::CollectionHandle<T>;273274	fn deref(&self) -> &Self::Target {275		&self.0276	}277}278279impl<T: Config> Pallet<T> {280	/// Get number of RFT tokens in collection281	pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {282		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)283	}284285	/// Check that RFT token exists286	///287	/// - `token`: Token ID.288	pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {289		<TotalSupply<T>>::contains_key((collection.id, token))290	}291292	pub fn set_scoped_token_property(293		collection_id: CollectionId,294		token_id: TokenId,295		scope: PropertyScope,296		property: Property,297	) -> DispatchResult {298		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {299			properties.try_scoped_set(scope, property.key, property.value)300		})301		.map_err(<CommonError<T>>::from)?;302303		Ok(())304	}305306	pub fn set_scoped_token_properties(307		collection_id: CollectionId,308		token_id: TokenId,309		scope: PropertyScope,310		properties: impl Iterator<Item = Property>,311	) -> DispatchResult {312		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {313			stored_properties.try_scoped_set_from_iter(scope, properties)314		})315		.map_err(<CommonError<T>>::from)?;316317		Ok(())318	}319}320321// unchecked calls skips any permission checks322impl<T: Config> Pallet<T> {323	/// Create RFT collection324	///325	/// `init_collection` will take non-refundable deposit for collection creation.326	///327	/// - `data`: Contains settings for collection limits and permissions.328	pub fn init_collection(329		owner: T::CrossAccountId,330		data: CreateCollectionData<T::AccountId>,331	) -> Result<CollectionId, DispatchError> {332		<PalletCommon<T>>::init_collection(owner, data, false)333	}334335	/// Destroy RFT collection336	///337	/// `destroy_collection` will throw error if collection contains any tokens.338	/// Only owner can destroy collection.339	pub fn destroy_collection(340		collection: RefungibleHandle<T>,341		sender: &T::CrossAccountId,342	) -> DispatchResult {343		let id = collection.id;344345		if Self::collection_has_tokens(id) {346			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());347		}348349		// =========350351		PalletCommon::destroy_collection(collection.0, sender)?;352353		<TokensMinted<T>>::remove(id);354		<TokensBurnt<T>>::remove(id);355		<TokenData<T>>::remove_prefix((id,), None);356		<TotalSupply<T>>::remove_prefix((id,), None);357		<Balance<T>>::remove_prefix((id,), None);358		<Allowance<T>>::remove_prefix((id,), None);359		<Owned<T>>::remove_prefix((id,), None);360		<AccountBalance<T>>::remove_prefix((id,), None);361		Ok(())362	}363364	fn collection_has_tokens(collection_id: CollectionId) -> bool {365		<TokenData<T>>::iter_prefix((collection_id,))366			.next()367			.is_some()368	}369370	pub fn burn_token_unchecked(371		collection: &RefungibleHandle<T>,372		token_id: TokenId,373	) -> DispatchResult {374		let burnt = <TokensBurnt<T>>::get(collection.id)375			.checked_add(1)376			.ok_or(ArithmeticError::Overflow)?;377378		<TokensBurnt<T>>::insert(collection.id, burnt);379		<TokenData<T>>::remove((collection.id, token_id));380		<TokenProperties<T>>::remove((collection.id, token_id));381		<TotalSupply<T>>::remove((collection.id, token_id));382		<Balance<T>>::remove_prefix((collection.id, token_id), None);383		<Allowance<T>>::remove_prefix((collection.id, token_id), None);384		// TODO: ERC721 transfer event385		Ok(())386	}387388	/// Burn RFT token pieces389	///390	/// `burn` will decrease total amount of token pieces and amount owned by sender.391	/// `burn` can be called even if there are multiple owners of the RFT token.392	/// If sender wouldn't have any pieces left after `burn` than she will stop being393	/// one of the owners of the token. If there is no account that owns any pieces of394	/// the token than token will be burned too.395	///396	/// - `amount`: Amount of token pieces to burn.397	/// - `token`: Token who's pieces should be burned398	/// - `collection`: Collection that contains the token399	pub fn burn(400		collection: &RefungibleHandle<T>,401		owner: &T::CrossAccountId,402		token: TokenId,403		amount: u128,404	) -> DispatchResult {405		let total_supply = <TotalSupply<T>>::get((collection.id, token))406			.checked_sub(amount)407			.ok_or(<CommonError<T>>::TokenValueTooLow)?;408409		// This was probally last owner of this token?410		if total_supply == 0 {411			// Ensure user actually owns this amount412			ensure!(413				<Balance<T>>::get((collection.id, token, owner)) == amount,414				<CommonError<T>>::TokenValueTooLow415			);416			let account_balance = <AccountBalance<T>>::get((collection.id, owner))417				.checked_sub(1)418				// Should not occur419				.ok_or(ArithmeticError::Underflow)?;420421			// =========422423			<Owned<T>>::remove((collection.id, owner, token));424			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);425			<AccountBalance<T>>::insert((collection.id, owner), account_balance);426			Self::burn_token_unchecked(collection, token)?;427			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(428				collection.id,429				token,430				owner.clone(),431				amount,432			));433			return Ok(());434		}435436		let balance = <Balance<T>>::get((collection.id, token, owner))437			.checked_sub(amount)438			.ok_or(<CommonError<T>>::TokenValueTooLow)?;439		let account_balance = if balance == 0 {440			<AccountBalance<T>>::get((collection.id, owner))441				.checked_sub(1)442				// Should not occur443				.ok_or(ArithmeticError::Underflow)?444		} else {445			0446		};447448		// =========449450		if balance == 0 {451			<Owned<T>>::remove((collection.id, owner, token));452			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);453			<Balance<T>>::remove((collection.id, token, owner));454			<AccountBalance<T>>::insert((collection.id, owner), account_balance);455		} else {456			<Balance<T>>::insert((collection.id, token, owner), balance);457		}458		<TotalSupply<T>>::insert((collection.id, token), total_supply);459		// TODO: ERC20 transfer event460		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(461			collection.id,462			token,463			owner.clone(),464			amount,465		));466		Ok(())467	}468469	pub fn set_token_property(470		collection: &RefungibleHandle<T>,471		sender: &T::CrossAccountId,472		token_id: TokenId,473		property: Property,474		is_token_create: bool,475	) -> DispatchResult {476		Self::check_token_change_permission(477			collection,478			sender,479			token_id,480			&property.key,481			is_token_create,482		)?;483484		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {485			let property = property.clone();486			properties.try_set(property.key, property.value)487		})488		.map_err(<CommonError<T>>::from)?;489490		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(491			collection.id,492			token_id,493			property.key,494		));495496		Ok(())497	}498499	#[transactional]500	pub fn set_token_properties(501		collection: &RefungibleHandle<T>,502		sender: &T::CrossAccountId,503		token_id: TokenId,504		properties: Vec<Property>,505		is_token_create: bool,506	) -> DispatchResult {507		for property in properties {508			Self::set_token_property(collection, sender, token_id, property, is_token_create)?;509		}510511		Ok(())512	}513514	pub fn delete_token_property(515		collection: &RefungibleHandle<T>,516		sender: &T::CrossAccountId,517		token_id: TokenId,518		property_key: PropertyKey,519	) -> DispatchResult {520		Self::check_token_change_permission(collection, sender, token_id, &property_key, false)?;521522		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {523			properties.remove(&property_key)524		})525		.map_err(<CommonError<T>>::from)?;526527		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(528			collection.id,529			token_id,530			property_key,531		));532533		Ok(())534	}535536	fn check_token_change_permission(537		collection: &RefungibleHandle<T>,538		sender: &T::CrossAccountId,539		token_id: TokenId,540		property_key: &PropertyKey,541		is_token_create: bool,542	) -> DispatchResult {543		let permission = <PalletCommon<T>>::property_permissions(collection.id)544			.get(property_key)545			.cloned()546			.unwrap_or_else(PropertyPermission::none);547548		// Not "try_fold" because total count of pieces is limited by 'MAX_REFUNGIBLE_PIECES'.549		let total_pieces: u128 = <Balance<T>>::iter_prefix((collection.id, token_id,)).fold(0, |total, piece| total + piece.1);550		let balance = collection.balance(sender.clone(), token_id);551552		let check_token_owner = || -> DispatchResult {553			ensure!(balance == total_pieces, <CommonError<T>>::NoPermission);554			Ok(())555		};556557		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))558			.get(property_key)559			.is_some();560561		match permission {562			PropertyPermission { mutable: false, .. } if is_property_exists => {563				Err(<CommonError<T>>::NoPermission.into())564			}565566			PropertyPermission {567				collection_admin,568				token_owner,569				..570			} => {571				//TODO: investigate threats during public minting.572				if is_token_create && (collection_admin || token_owner) {573					return Ok(());574				}575576				let mut check_result = Err(<CommonError<T>>::NoPermission.into());577578				if collection_admin {579					check_result = collection.check_is_owner_or_admin(sender);580				}581582				if token_owner {583					check_result.or_else(|_| check_token_owner())584				} else {585					check_result586				}587			}588		}589	}590591	#[transactional]592	pub fn delete_token_properties(593		collection: &RefungibleHandle<T>,594		sender: &T::CrossAccountId,595		token_id: TokenId,596		property_keys: Vec<PropertyKey>,597	) -> DispatchResult {598		for key in property_keys {599			Self::delete_token_property(collection, sender, token_id, key)?;600		}601602		Ok(())603	}604605	/// Transfer RFT token pieces from one account to another.606	///607	/// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.608	///609	/// - `from`: Owner of token pieces to transfer.610	/// - `to`: Recepient of transfered token pieces.611	/// - `amount`: Amount of token pieces to transfer.612	/// - `token`: Token whos pieces should be transfered613	/// - `collection`: Collection that contains the token614	pub fn transfer(615		collection: &RefungibleHandle<T>,616		from: &T::CrossAccountId,617		to: &T::CrossAccountId,618		token: TokenId,619		amount: u128,620		nesting_budget: &dyn Budget,621	) -> DispatchResult {622		ensure!(623			collection.limits.transfers_enabled(),624			<CommonError<T>>::TransferNotAllowed625		);626627		if collection.permissions.access() == AccessMode::AllowList {628			collection.check_allowlist(from)?;629			collection.check_allowlist(to)?;630		}631		<PalletCommon<T>>::ensure_correct_receiver(to)?;632633		let balance_from = <Balance<T>>::get((collection.id, token, from))634			.checked_sub(amount)635			.ok_or(<CommonError<T>>::TokenValueTooLow)?;636		let mut create_target = false;637		let from_to_differ = from != to;638		let balance_to = if from != to {639			let old_balance = <Balance<T>>::get((collection.id, token, to));640			if old_balance == 0 {641				create_target = true;642			}643			Some(644				old_balance645					.checked_add(amount)646					.ok_or(ArithmeticError::Overflow)?,647			)648		} else {649			None650		};651652		let account_balance_from = if balance_from == 0 {653			Some(654				<AccountBalance<T>>::get((collection.id, from))655					.checked_sub(1)656					// Should not occur657					.ok_or(ArithmeticError::Underflow)?,658			)659		} else {660			None661		};662		// Account data is created in token, AccountBalance should be increased663		// But only if from != to as we shouldn't check overflow in this case664		let account_balance_to = if create_target && from_to_differ {665			let account_balance_to = <AccountBalance<T>>::get((collection.id, to))666				.checked_add(1)667				.ok_or(ArithmeticError::Overflow)?;668			ensure!(669				account_balance_to < collection.limits.account_token_ownership_limit(),670				<CommonError<T>>::AccountTokenLimitExceeded,671			);672673			Some(account_balance_to)674		} else {675			None676		};677678		// =========679680		<PalletStructure<T>>::nest_if_sent_to_token(681			from.clone(),682			to,683			collection.id,684			token,685			nesting_budget,686		)?;687688		if let Some(balance_to) = balance_to {689			// from != to690			if balance_from == 0 {691				<Balance<T>>::remove((collection.id, token, from));692				<PalletStructure<T>>::unnest_if_nested(from, collection.id, token);693			} else {694				<Balance<T>>::insert((collection.id, token, from), balance_from);695			}696			<Balance<T>>::insert((collection.id, token, to), balance_to);697			if let Some(account_balance_from) = account_balance_from {698				<AccountBalance<T>>::insert((collection.id, from), account_balance_from);699				<Owned<T>>::remove((collection.id, from, token));700			}701			if let Some(account_balance_to) = account_balance_to {702				<AccountBalance<T>>::insert((collection.id, to), account_balance_to);703				<Owned<T>>::insert((collection.id, to, token), true);704			}705		}706707		// TODO: ERC20 transfer event708		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(709			collection.id,710			token,711			from.clone(),712			to.clone(),713			amount,714		));715		Ok(())716	}717718	/// Batched operation to create multiple RFT tokens.719	///720	/// Same as `create_item` but creates multiple tokens.721	///722	/// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.723	pub fn create_multiple_items(724		collection: &RefungibleHandle<T>,725		sender: &T::CrossAccountId,726		data: Vec<CreateRefungibleExData<T::CrossAccountId>>,727		nesting_budget: &dyn Budget,728	) -> DispatchResult {729		if !collection.is_owner_or_admin(sender) {730			ensure!(731				collection.permissions.mint_mode(),732				<CommonError<T>>::PublicMintingNotAllowed733			);734			collection.check_allowlist(sender)?;735736			for item in data.iter() {737				for user in item.users.keys() {738					collection.check_allowlist(user)?;739				}740			}741		}742743		for item in data.iter() {744			for (owner, _) in item.users.iter() {745				<PalletCommon<T>>::ensure_correct_receiver(owner)?;746			}747		}748749		// Total pieces per tokens750		let totals = data751			.iter()752			.map(|data| {753				Ok(data754					.users755					.iter()756					.map(|u| u.1)757					.try_fold(0u128, |acc, v| acc.checked_add(*v))758					.ok_or(ArithmeticError::Overflow)?)759			})760			.collect::<Result<Vec<_>, DispatchError>>()?;761		for total in &totals {762			ensure!(763				*total <= MAX_REFUNGIBLE_PIECES,764				<Error<T>>::WrongRefungiblePieces765			);766		}767768		let first_token_id = <TokensMinted<T>>::get(collection.id);769		let tokens_minted = first_token_id770			.checked_add(data.len() as u32)771			.ok_or(ArithmeticError::Overflow)?;772		ensure!(773			tokens_minted < collection.limits.token_limit(),774			<CommonError<T>>::CollectionTokenLimitExceeded775		);776777		let mut balances = BTreeMap::new();778		for data in &data {779			for owner in data.users.keys() {780				let balance = balances781					.entry(owner)782					.or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));783				*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;784785				ensure!(786					*balance <= collection.limits.account_token_ownership_limit(),787					<CommonError<T>>::AccountTokenLimitExceeded,788				);789			}790		}791792		for (i, token) in data.iter().enumerate() {793			let token_id = TokenId(first_token_id + i as u32 + 1);794			for (to, _) in token.users.iter() {795				<PalletStructure<T>>::check_nesting(796					sender.clone(),797					to,798					collection.id,799					token_id,800					nesting_budget,801				)?;802			}803		}804805		// =========806807		with_transaction(|| {808			for (i, data) in data.iter().enumerate() {809				let token_id = first_token_id + i as u32 + 1;810				<TotalSupply<T>>::insert((collection.id, token_id), totals[i]);811812				<TokenData<T>>::insert(813					(collection.id, token_id),814					ItemData {815						const_data: data.const_data.clone(),816					},817				);818819				for (user, amount) in data.users.iter() {820					if *amount == 0 {821						continue;822					}823					<Balance<T>>::insert((collection.id, token_id, &user), amount);824					<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);825					<PalletStructure<T>>::nest_if_sent_to_token_unchecked(826						user,827						collection.id,828						TokenId(token_id),829					);830				}831832				if let Err(e) = Self::set_token_properties(833					collection,834					sender,835					TokenId(token_id),836					data.properties.clone().into_inner(),837					true,838				) {839					return TransactionOutcome::Rollback(Err(e));840				}841			}842			TransactionOutcome::Commit(Ok(()))843		})?;844845		<TokensMinted<T>>::insert(collection.id, tokens_minted);846847		for (account, balance) in balances {848			<AccountBalance<T>>::insert((collection.id, account), balance);849		}850851		for (i, token) in data.into_iter().enumerate() {852			let token_id = first_token_id + i as u32 + 1;853854			for (user, amount) in token.users.into_iter() {855				if amount == 0 {856					continue;857				}			858859				// TODO: ERC20 transfer event860				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(861					collection.id,862					TokenId(token_id),863					user,864					amount,865				));866			}867		}868		Ok(())869	}870871	pub fn set_allowance_unchecked(872		collection: &RefungibleHandle<T>,873		sender: &T::CrossAccountId,874		spender: &T::CrossAccountId,875		token: TokenId,876		amount: u128,877	) {878		if amount == 0 {879			<Allowance<T>>::remove((collection.id, token, sender, spender));880		} else {881			<Allowance<T>>::insert((collection.id, token, sender, spender), amount);882		}883		// TODO: ERC20 approval event884		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(885			collection.id,886			token,887			sender.clone(),888			spender.clone(),889			amount,890		))891	}892893	/// Set allowance for the spender to `transfer` or `burn` sender's token pieces.894	///895	/// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.896	pub fn set_allowance(897		collection: &RefungibleHandle<T>,898		sender: &T::CrossAccountId,899		spender: &T::CrossAccountId,900		token: TokenId,901		amount: u128,902	) -> DispatchResult {903		if collection.permissions.access() == AccessMode::AllowList {904			collection.check_allowlist(sender)?;905			collection.check_allowlist(spender)?;906		}907908		<PalletCommon<T>>::ensure_correct_receiver(spender)?;909910		if <Balance<T>>::get((collection.id, token, sender)) < amount {911			ensure!(912				collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),913				<CommonError<T>>::CantApproveMoreThanOwned914			);915		}916917		// =========918919		Self::set_allowance_unchecked(collection, sender, spender, token, amount);920		Ok(())921	}922923	/// Returns allowance, which should be set after transaction924	fn check_allowed(925		collection: &RefungibleHandle<T>,926		spender: &T::CrossAccountId,927		from: &T::CrossAccountId,928		token: TokenId,929		amount: u128,930		nesting_budget: &dyn Budget,931	) -> Result<Option<u128>, DispatchError> {932		if spender.conv_eq(from) {933			return Ok(None);934		}935		if collection.permissions.access() == AccessMode::AllowList {936			// `from`, `to` checked in [`transfer`]937			collection.check_allowlist(spender)?;938		}939		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {940			// TODO: should collection owner be allowed to perform this transfer?941			ensure!(942				<PalletStructure<T>>::check_indirectly_owned(943					spender.clone(),944					source.0,945					source.1,946					None,947					nesting_budget948				)?,949				<CommonError<T>>::ApprovedValueTooLow,950			);951			return Ok(None);952		}953		let allowance =954			<Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);955		if allowance.is_none() {956			ensure!(957				collection.ignores_allowance(spender),958				<CommonError<T>>::ApprovedValueTooLow959			);960		}961		Ok(allowance)962	}963964	/// Transfer RFT token pieces from one account to another.965	///966	/// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.967	/// The owner should set allowance for the spender to transfer pieces.968	///969	/// [`transfer`]: struct.Pallet.html#method.transfer970	pub fn transfer_from(971		collection: &RefungibleHandle<T>,972		spender: &T::CrossAccountId,973		from: &T::CrossAccountId,974		to: &T::CrossAccountId,975		token: TokenId,976		amount: u128,977		nesting_budget: &dyn Budget,978	) -> DispatchResult {979		let allowance =980			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;981982		// =========983984		Self::transfer(collection, from, to, token, amount, nesting_budget)?;985		if let Some(allowance) = allowance {986			Self::set_allowance_unchecked(collection, from, spender, token, allowance);987		}988		Ok(())989	}990991	/// Burn RFT token pieces from the account.992	///993	/// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should994	/// set allowance for the spender to burn pieces995	///996	/// [`burn`]: struct.Pallet.html#method.burn997	pub fn burn_from(998		collection: &RefungibleHandle<T>,999		spender: &T::CrossAccountId,1000		from: &T::CrossAccountId,1001		token: TokenId,1002		amount: u128,1003		nesting_budget: &dyn Budget,1004	) -> DispatchResult {1005		let allowance =1006			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10071008		// =========10091010		Self::burn(collection, from, token, amount)?;1011		if let Some(allowance) = allowance {1012			Self::set_allowance_unchecked(collection, from, spender, token, allowance);1013		}1014		Ok(())1015	}10161017	/// Create RFT token.1018	///1019	/// The sender should be the owner/admin of the collection or collection should be configured1020	/// to allow public minting.1021	///1022	/// - `data`: Contains list of users who will become the owners of the token pieces and amount1023	///   of token pieces they will receive.1024	pub fn create_item(1025		collection: &RefungibleHandle<T>,1026		sender: &T::CrossAccountId,1027		data: CreateRefungibleExData<T::CrossAccountId>,1028		nesting_budget: &dyn Budget,1029	) -> DispatchResult {1030		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1031	}10321033	/// Repartition RFT token.1034	///1035	/// `repartition` will set token balance of the sender and total amount of token pieces.1036	/// Sender should own all of the token pieces. `repartition' could be done even if some1037	/// token pieces were burned before.1038	///1039	/// - `amount`: Total amount of token pieces that the token will have after `repartition`.1040	pub fn repartition(1041		collection: &RefungibleHandle<T>,1042		owner: &T::CrossAccountId,1043		token: TokenId,1044		amount: u128,1045	) -> DispatchResult {1046		ensure!(1047			amount <= MAX_REFUNGIBLE_PIECES,1048			<Error<T>>::WrongRefungiblePieces1049		);1050		ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1051		// Ensure user owns all pieces1052		let total_supply = <TotalSupply<T>>::get((collection.id, token));1053		let balance = <Balance<T>>::get((collection.id, token, owner));1054		ensure!(1055			total_supply == balance,1056			<Error<T>>::RepartitionWhileNotOwningAllPieces1057		);10581059		<Balance<T>>::insert((collection.id, token, owner), amount);1060		<TotalSupply<T>>::insert((collection.id, token), amount);1061		Ok(())1062	}10631064	fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1065		<TotalSupply<T>>::try_get((collection_id, token_id)).ok()1066	}1067	1068	pub fn set_collection_properties(1069		collection: &RefungibleHandle<T>,1070		sender: &T::CrossAccountId,1071		properties: Vec<Property>,1072	) -> DispatchResult {1073		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)1074	}10751076	pub fn delete_collection_properties(1077		collection: &RefungibleHandle<T>,1078		sender: &T::CrossAccountId,1079		property_keys: Vec<PropertyKey>,1080	) -> DispatchResult {1081		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1082	}10831084	pub fn set_token_property_permissions(1085		collection: &RefungibleHandle<T>,1086		sender: &T::CrossAccountId,1087		property_permissions: Vec<PropertyKeyPermission>,1088	) -> DispatchResult {1089		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1090	}1091}
after · pallets/refungible/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//! # Refungible Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Refungible pallet provides functions for:29//!30//! - RFT collection creation and removal31//! - Minting and burning of RFT tokens32//! - Partition and repartition of RFT tokens33//! - Retrieving number of pieces of RFT token34//! - Retrieving account balances35//! - Transfering RFT token pieces36//! - Burning RFT token pieces37//! - Setting and checking allowance for RFT tokens38//!39//! ### Terminology40//!41//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all42//!   of the RFT token pieces than it owns the RFT token and can repartition it.43//!44//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.45//!   Each collection has its own settings and set of permissions.46//!47//! - **RFT token piece:** A fungible part of an RFT token.48//!49//! - **Balance:** RFT token pieces owned by an account50//!51//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to52//!   transfer from the balance of another account53//!54//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from55//!   an account balance.56//!57//! ### Implementations58//!59//! The Refungible pallet provides implementations for the following traits. If these traits provide60//! the functionality that you need, then you can avoid coupling with the Refungible pallet.61//!62//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight63//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing64//!   with collections65//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible66//!   collection67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for73//!   some accounts.74//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.75//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.76//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.77//!   Nests the RFT token if RFT token pieces are sent to another token.78//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.80//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.81//!82//! ## Assumptions83//!84//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.85//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.86//! * Sender should be in collection's allow list to perform operations on tokens.8788#![cfg_attr(not(feature = "std"), no_std)]8990use frame_support::{ensure, fail, BoundedVec, transactional, storage::with_transaction};91use up_data_structs::{92	AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,93	CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget,94	Property, PropertyScope, TrySetProperty, PropertyKey, PropertyValue, PropertyPermission,95	PropertyKeyPermission,96};97use pallet_evm::account::CrossAccountId;98use pallet_common::{99	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,100	CommonCollectionOperations as _,101};102use pallet_structure::Pallet as PalletStructure;103use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};104use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};105use core::ops::Deref;106use codec::{Encode, Decode, MaxEncodedLen};107use scale_info::TypeInfo;108109pub use pallet::*;110#[cfg(feature = "runtime-benchmarks")]111pub mod benchmarking;112pub mod common;113pub mod erc;114pub mod weights;115pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;116117#[struct_versioning::versioned(version = 2, upper)]118#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]119pub struct ItemData {120	pub const_data: BoundedVec<u8, CustomDataLimit>,121122	#[version(..2)]123	pub variable_data: BoundedVec<u8, CustomDataLimit>,124}125126#[frame_support::pallet]127pub mod pallet {128	use super::*;129	use frame_support::{130		Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,131		traits::StorageVersion,132	};133	use frame_system::pallet_prelude::*;134	use up_data_structs::{CollectionId, TokenId};135	use super::weights::WeightInfo;136137	#[pallet::error]138	pub enum Error<T> {139		/// Not Refungible item data used to mint in Refungible collection.140		NotRefungibleDataUsedToMintFungibleCollectionToken,141		/// Maximum refungibility exceeded142		WrongRefungiblePieces,143		/// Refungible token can't be repartitioned by user who isn't owns all pieces144		RepartitionWhileNotOwningAllPieces,145		/// Refungible token can't nest other tokens146		RefungibleDisallowsNesting,147		/// Setting item properties is not allowed148		SettingPropertiesNotAllowed,149	}150151	#[pallet::config]152	pub trait Config:153		frame_system::Config + pallet_common::Config + pallet_structure::Config154	{155		type WeightInfo: WeightInfo;156	}157158	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);159160	#[pallet::pallet]161	#[pallet::storage_version(STORAGE_VERSION)]162	#[pallet::generate_store(pub(super) trait Store)]163	pub struct Pallet<T>(_);164165	/// Amount of tokens minted for collection166	#[pallet::storage]167	pub type TokensMinted<T: Config> =168		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;169170	/// Amount of burnt tokens for collection171	#[pallet::storage]172	pub type TokensBurnt<T: Config> =173		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;174175	/// Custom data serialized to bytes for token176	#[pallet::storage]177	pub type TokenData<T: Config> = StorageNMap<178		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),179		Value = ItemData,180		QueryKind = ValueQuery,181	>;182183	#[pallet::storage]184	#[pallet::getter(fn token_properties)]185	pub type TokenProperties<T: Config> = StorageNMap<186		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),187		Value = up_data_structs::Properties,188		QueryKind = ValueQuery,189		OnEmpty = up_data_structs::TokenProperties,190	>;191192	/// Total amount of pieces for token193	#[pallet::storage]194	pub type TotalSupply<T: Config> = StorageNMap<195		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),196		Value = u128,197		QueryKind = ValueQuery,198	>;199200	/// Used to enumerate tokens owned by account201	#[pallet::storage]202	pub type Owned<T: Config> = StorageNMap<203		Key = (204			Key<Twox64Concat, CollectionId>,205			Key<Blake2_128Concat, T::CrossAccountId>,206			Key<Twox64Concat, TokenId>,207		),208		Value = bool,209		QueryKind = ValueQuery,210	>;211212	/// Amount of tokens owned by account213	#[pallet::storage]214	pub type AccountBalance<T: Config> = StorageNMap<215		Key = (216			Key<Twox64Concat, CollectionId>,217			// Owner218			Key<Blake2_128Concat, T::CrossAccountId>,219		),220		Value = u32,221		QueryKind = ValueQuery,222	>;223224	/// Amount of token pieces owned by account225	#[pallet::storage]226	pub type Balance<T: Config> = StorageNMap<227		Key = (228			Key<Twox64Concat, CollectionId>,229			Key<Twox64Concat, TokenId>,230			// Owner231			Key<Blake2_128Concat, T::CrossAccountId>,232		),233		Value = u128,234		QueryKind = ValueQuery,235	>;236237	/// Allowance set by an owner for a spender for a token238	#[pallet::storage]239	pub type Allowance<T: Config> = StorageNMap<240		Key = (241			Key<Twox64Concat, CollectionId>,242			Key<Twox64Concat, TokenId>,243			// Owner244			Key<Blake2_128, T::CrossAccountId>,245			// Spender246			Key<Blake2_128Concat, T::CrossAccountId>,247		),248		Value = u128,249		QueryKind = ValueQuery,250	>;251252	#[pallet::hooks]253	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {254		fn on_runtime_upgrade() -> Weight {255			if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {256				<TokenData<T>>::translate_values::<ItemDataVersion1, _>(|v| {257					Some(<ItemDataVersion2>::from(v))258				})259			}260261			0262		}263	}264}265266pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);267impl<T: Config> RefungibleHandle<T> {268	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {269		Self(inner)270	}271	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {272		self.0273	}274}275impl<T: Config> Deref for RefungibleHandle<T> {276	type Target = pallet_common::CollectionHandle<T>;277278	fn deref(&self) -> &Self::Target {279		&self.0280	}281}282283impl<T: Config> Pallet<T> {284	/// Get number of RFT tokens in collection285	pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {286		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)287	}288289	/// Check that RFT token exists290	///291	/// - `token`: Token ID.292	pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {293		<TotalSupply<T>>::contains_key((collection.id, token))294	}295296	pub fn set_scoped_token_property(297		collection_id: CollectionId,298		token_id: TokenId,299		scope: PropertyScope,300		property: Property,301	) -> DispatchResult {302		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {303			properties.try_scoped_set(scope, property.key, property.value)304		})305		.map_err(<CommonError<T>>::from)?;306307		Ok(())308	}309310	pub fn set_scoped_token_properties(311		collection_id: CollectionId,312		token_id: TokenId,313		scope: PropertyScope,314		properties: impl Iterator<Item = Property>,315	) -> DispatchResult {316		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {317			stored_properties.try_scoped_set_from_iter(scope, properties)318		})319		.map_err(<CommonError<T>>::from)?;320321		Ok(())322	}323}324325// unchecked calls skips any permission checks326impl<T: Config> Pallet<T> {327	/// Create RFT collection328	///329	/// `init_collection` will take non-refundable deposit for collection creation.330	///331	/// - `data`: Contains settings for collection limits and permissions.332	pub fn init_collection(333		owner: T::CrossAccountId,334		data: CreateCollectionData<T::AccountId>,335	) -> Result<CollectionId, DispatchError> {336		<PalletCommon<T>>::init_collection(owner, data, false)337	}338339	/// Destroy RFT collection340	///341	/// `destroy_collection` will throw error if collection contains any tokens.342	/// Only owner can destroy collection.343	pub fn destroy_collection(344		collection: RefungibleHandle<T>,345		sender: &T::CrossAccountId,346	) -> DispatchResult {347		let id = collection.id;348349		if Self::collection_has_tokens(id) {350			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());351		}352353		// =========354355		PalletCommon::destroy_collection(collection.0, sender)?;356357		<TokensMinted<T>>::remove(id);358		<TokensBurnt<T>>::remove(id);359		<TokenData<T>>::remove_prefix((id,), None);360		<TotalSupply<T>>::remove_prefix((id,), None);361		<Balance<T>>::remove_prefix((id,), None);362		<Allowance<T>>::remove_prefix((id,), None);363		<Owned<T>>::remove_prefix((id,), None);364		<AccountBalance<T>>::remove_prefix((id,), None);365		Ok(())366	}367368	fn collection_has_tokens(collection_id: CollectionId) -> bool {369		<TokenData<T>>::iter_prefix((collection_id,))370			.next()371			.is_some()372	}373374	pub fn burn_token_unchecked(375		collection: &RefungibleHandle<T>,376		token_id: TokenId,377	) -> DispatchResult {378		let burnt = <TokensBurnt<T>>::get(collection.id)379			.checked_add(1)380			.ok_or(ArithmeticError::Overflow)?;381382		<TokensBurnt<T>>::insert(collection.id, burnt);383		<TokenData<T>>::remove((collection.id, token_id));384		<TokenProperties<T>>::remove((collection.id, token_id));385		<TotalSupply<T>>::remove((collection.id, token_id));386		<Balance<T>>::remove_prefix((collection.id, token_id), None);387		<Allowance<T>>::remove_prefix((collection.id, token_id), None);388		// TODO: ERC721 transfer event389		Ok(())390	}391392	/// Burn RFT token pieces393	///394	/// `burn` will decrease total amount of token pieces and amount owned by sender.395	/// `burn` can be called even if there are multiple owners of the RFT token.396	/// If sender wouldn't have any pieces left after `burn` than she will stop being397	/// one of the owners of the token. If there is no account that owns any pieces of398	/// the token than token will be burned too.399	///400	/// - `amount`: Amount of token pieces to burn.401	/// - `token`: Token who's pieces should be burned402	/// - `collection`: Collection that contains the token403	pub fn burn(404		collection: &RefungibleHandle<T>,405		owner: &T::CrossAccountId,406		token: TokenId,407		amount: u128,408	) -> DispatchResult {409		let total_supply = <TotalSupply<T>>::get((collection.id, token))410			.checked_sub(amount)411			.ok_or(<CommonError<T>>::TokenValueTooLow)?;412413		// This was probally last owner of this token?414		if total_supply == 0 {415			// Ensure user actually owns this amount416			ensure!(417				<Balance<T>>::get((collection.id, token, owner)) == amount,418				<CommonError<T>>::TokenValueTooLow419			);420			let account_balance = <AccountBalance<T>>::get((collection.id, owner))421				.checked_sub(1)422				// Should not occur423				.ok_or(ArithmeticError::Underflow)?;424425			// =========426427			<Owned<T>>::remove((collection.id, owner, token));428			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);429			<AccountBalance<T>>::insert((collection.id, owner), account_balance);430			Self::burn_token_unchecked(collection, token)?;431			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(432				collection.id,433				token,434				owner.clone(),435				amount,436			));437			return Ok(());438		}439440		let balance = <Balance<T>>::get((collection.id, token, owner))441			.checked_sub(amount)442			.ok_or(<CommonError<T>>::TokenValueTooLow)?;443		let account_balance = if balance == 0 {444			<AccountBalance<T>>::get((collection.id, owner))445				.checked_sub(1)446				// Should not occur447				.ok_or(ArithmeticError::Underflow)?448		} else {449			0450		};451452		// =========453454		if balance == 0 {455			<Owned<T>>::remove((collection.id, owner, token));456			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);457			<Balance<T>>::remove((collection.id, token, owner));458			<AccountBalance<T>>::insert((collection.id, owner), account_balance);459		} else {460			<Balance<T>>::insert((collection.id, token, owner), balance);461		}462		<TotalSupply<T>>::insert((collection.id, token), total_supply);463		// TODO: ERC20 transfer event464		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(465			collection.id,466			token,467			owner.clone(),468			amount,469		));470		Ok(())471	}472473	#[transactional]474	fn modify_token_properties(475		collection: &RefungibleHandle<T>,476		sender: &T::CrossAccountId,477		token_id: TokenId,478		properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,479		is_token_create: bool,480		nesting_budget: &dyn Budget,481	) -> DispatchResult {482		let is_collection_admin = || collection.is_owner_or_admin(sender);483		let is_token_owner = || -> Result<bool, DispatchError> {484			let balance = collection.balance(sender.clone(), token_id);485			let total_pieces: u128 =486				Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);487			if balance != total_pieces {488				return Ok(false);489			}490491			let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(492				sender.clone(),493				collection.id,494				token_id,495				None,496				nesting_budget,497			)?;498499			Ok(is_bundle_owner)500		};501502		for (key, value) in properties {503			let permission = <PalletCommon<T>>::property_permissions(collection.id)504				.get(&key)505				.cloned()506				.unwrap_or_else(PropertyPermission::none);507508			let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))509				.get(&key)510				.is_some();511512			match permission {513				PropertyPermission { mutable: false, .. } if is_property_exists => {514					return Err(<CommonError<T>>::NoPermission.into());515				}516517				PropertyPermission {518					collection_admin,519					token_owner,520					..521				} => {522					//TODO: investigate threats during public minting.523					let is_token_create =524						is_token_create && (collection_admin || token_owner) && value.is_some();525					if !(is_token_create526						|| (collection_admin && is_collection_admin())527						|| (token_owner && is_token_owner()?))528					{529						fail!(<CommonError<T>>::NoPermission);530					}531				}532			}533534			match value {535				Some(value) => {536					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {537						properties.try_set(key.clone(), value)538					})539					.map_err(<CommonError<T>>::from)?;540541					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(542						collection.id,543						token_id,544						key,545					));546				}547				None => {548					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {549						properties.remove(&key)550					})551					.map_err(<CommonError<T>>::from)?;552553					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(554						collection.id,555						token_id,556						key,557					));558				}559			}560		}561562		Ok(())563	}564565	pub fn set_token_properties(566		collection: &RefungibleHandle<T>,567		sender: &T::CrossAccountId,568		token_id: TokenId,569		properties: impl Iterator<Item = Property>,570		is_token_create: bool,571		nesting_budget: &dyn Budget,572	) -> DispatchResult {573		Self::modify_token_properties(574			collection,575			sender,576			token_id,577			properties.map(|p| (p.key, Some(p.value))),578			is_token_create,579			nesting_budget,580		)581	}582583	pub fn set_token_property(584		collection: &RefungibleHandle<T>,585		sender: &T::CrossAccountId,586		token_id: TokenId,587		property: Property,588		nesting_budget: &dyn Budget,589	) -> DispatchResult {590		let is_token_create = false;591592		Self::set_token_properties(593			collection,594			sender,595			token_id,596			[property].into_iter(),597			is_token_create,598			nesting_budget,599		)600	}601602	pub fn delete_token_properties(603		collection: &RefungibleHandle<T>,604		sender: &T::CrossAccountId,605		token_id: TokenId,606		property_keys: impl Iterator<Item = PropertyKey>,607		nesting_budget: &dyn Budget,608	) -> DispatchResult {609		let is_token_create = false;610611		Self::modify_token_properties(612			collection,613			sender,614			token_id,615			property_keys.into_iter().map(|key| (key, None)),616			is_token_create,617			nesting_budget,618		)619	}620621	pub fn delete_token_property(622		collection: &RefungibleHandle<T>,623		sender: &T::CrossAccountId,624		token_id: TokenId,625		property_key: PropertyKey,626		nesting_budget: &dyn Budget,627	) -> DispatchResult {628		Self::delete_token_properties(629			collection,630			sender,631			token_id,632			[property_key].into_iter(),633			nesting_budget,634		)635	}636637	/// Transfer RFT token pieces from one account to another.638	///639	/// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.640	///641	/// - `from`: Owner of token pieces to transfer.642	/// - `to`: Recepient of transfered token pieces.643	/// - `amount`: Amount of token pieces to transfer.644	/// - `token`: Token whos pieces should be transfered645	/// - `collection`: Collection that contains the token646	pub fn transfer(647		collection: &RefungibleHandle<T>,648		from: &T::CrossAccountId,649		to: &T::CrossAccountId,650		token: TokenId,651		amount: u128,652		nesting_budget: &dyn Budget,653	) -> DispatchResult {654		ensure!(655			collection.limits.transfers_enabled(),656			<CommonError<T>>::TransferNotAllowed657		);658659		if collection.permissions.access() == AccessMode::AllowList {660			collection.check_allowlist(from)?;661			collection.check_allowlist(to)?;662		}663		<PalletCommon<T>>::ensure_correct_receiver(to)?;664665		let balance_from = <Balance<T>>::get((collection.id, token, from))666			.checked_sub(amount)667			.ok_or(<CommonError<T>>::TokenValueTooLow)?;668		let mut create_target = false;669		let from_to_differ = from != to;670		let balance_to = if from != to {671			let old_balance = <Balance<T>>::get((collection.id, token, to));672			if old_balance == 0 {673				create_target = true;674			}675			Some(676				old_balance677					.checked_add(amount)678					.ok_or(ArithmeticError::Overflow)?,679			)680		} else {681			None682		};683684		let account_balance_from = if balance_from == 0 {685			Some(686				<AccountBalance<T>>::get((collection.id, from))687					.checked_sub(1)688					// Should not occur689					.ok_or(ArithmeticError::Underflow)?,690			)691		} else {692			None693		};694		// Account data is created in token, AccountBalance should be increased695		// But only if from != to as we shouldn't check overflow in this case696		let account_balance_to = if create_target && from_to_differ {697			let account_balance_to = <AccountBalance<T>>::get((collection.id, to))698				.checked_add(1)699				.ok_or(ArithmeticError::Overflow)?;700			ensure!(701				account_balance_to < collection.limits.account_token_ownership_limit(),702				<CommonError<T>>::AccountTokenLimitExceeded,703			);704705			Some(account_balance_to)706		} else {707			None708		};709710		// =========711712		<PalletStructure<T>>::nest_if_sent_to_token(713			from.clone(),714			to,715			collection.id,716			token,717			nesting_budget,718		)?;719720		if let Some(balance_to) = balance_to {721			// from != to722			if balance_from == 0 {723				<Balance<T>>::remove((collection.id, token, from));724				<PalletStructure<T>>::unnest_if_nested(from, collection.id, token);725			} else {726				<Balance<T>>::insert((collection.id, token, from), balance_from);727			}728			<Balance<T>>::insert((collection.id, token, to), balance_to);729			if let Some(account_balance_from) = account_balance_from {730				<AccountBalance<T>>::insert((collection.id, from), account_balance_from);731				<Owned<T>>::remove((collection.id, from, token));732			}733			if let Some(account_balance_to) = account_balance_to {734				<AccountBalance<T>>::insert((collection.id, to), account_balance_to);735				<Owned<T>>::insert((collection.id, to, token), true);736			}737		}738739		// TODO: ERC20 transfer event740		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(741			collection.id,742			token,743			from.clone(),744			to.clone(),745			amount,746		));747		Ok(())748	}749750	/// Batched operation to create multiple RFT tokens.751	///752	/// Same as `create_item` but creates multiple tokens.753	///754	/// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.755	pub fn create_multiple_items(756		collection: &RefungibleHandle<T>,757		sender: &T::CrossAccountId,758		data: Vec<CreateRefungibleExData<T::CrossAccountId>>,759		nesting_budget: &dyn Budget,760	) -> DispatchResult {761		if !collection.is_owner_or_admin(sender) {762			ensure!(763				collection.permissions.mint_mode(),764				<CommonError<T>>::PublicMintingNotAllowed765			);766			collection.check_allowlist(sender)?;767768			for item in data.iter() {769				for user in item.users.keys() {770					collection.check_allowlist(user)?;771				}772			}773		}774775		for item in data.iter() {776			for (owner, _) in item.users.iter() {777				<PalletCommon<T>>::ensure_correct_receiver(owner)?;778			}779		}780781		// Total pieces per tokens782		let totals = data783			.iter()784			.map(|data| {785				Ok(data786					.users787					.iter()788					.map(|u| u.1)789					.try_fold(0u128, |acc, v| acc.checked_add(*v))790					.ok_or(ArithmeticError::Overflow)?)791			})792			.collect::<Result<Vec<_>, DispatchError>>()?;793		for total in &totals {794			ensure!(795				*total <= MAX_REFUNGIBLE_PIECES,796				<Error<T>>::WrongRefungiblePieces797			);798		}799800		let first_token_id = <TokensMinted<T>>::get(collection.id);801		let tokens_minted = first_token_id802			.checked_add(data.len() as u32)803			.ok_or(ArithmeticError::Overflow)?;804		ensure!(805			tokens_minted < collection.limits.token_limit(),806			<CommonError<T>>::CollectionTokenLimitExceeded807		);808809		let mut balances = BTreeMap::new();810		for data in &data {811			for owner in data.users.keys() {812				let balance = balances813					.entry(owner)814					.or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));815				*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;816817				ensure!(818					*balance <= collection.limits.account_token_ownership_limit(),819					<CommonError<T>>::AccountTokenLimitExceeded,820				);821			}822		}823824		for (i, token) in data.iter().enumerate() {825			let token_id = TokenId(first_token_id + i as u32 + 1);826			for (to, _) in token.users.iter() {827				<PalletStructure<T>>::check_nesting(828					sender.clone(),829					to,830					collection.id,831					token_id,832					nesting_budget,833				)?;834			}835		}836837		// =========838839		with_transaction(|| {840			for (i, data) in data.iter().enumerate() {841				let token_id = first_token_id + i as u32 + 1;842				<TotalSupply<T>>::insert((collection.id, token_id), totals[i]);843844				<TokenData<T>>::insert(845					(collection.id, token_id),846					ItemData {847						const_data: data.const_data.clone(),848					},849				);850851				for (user, amount) in data.users.iter() {852					if *amount == 0 {853						continue;854					}855					<Balance<T>>::insert((collection.id, token_id, &user), amount);856					<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);857					<PalletStructure<T>>::nest_if_sent_to_token_unchecked(858						user,859						collection.id,860						TokenId(token_id),861					);862				}863864				if let Err(e) = Self::set_token_properties(865					collection,866					sender,867					TokenId(token_id),868					data.properties.clone().into_iter(),869					true,870					nesting_budget,871				) {872					return TransactionOutcome::Rollback(Err(e));873				}874			}875			TransactionOutcome::Commit(Ok(()))876		})?;877878		<TokensMinted<T>>::insert(collection.id, tokens_minted);879880		for (account, balance) in balances {881			<AccountBalance<T>>::insert((collection.id, account), balance);882		}883884		for (i, token) in data.into_iter().enumerate() {885			let token_id = first_token_id + i as u32 + 1;886887			for (user, amount) in token.users.into_iter() {888				if amount == 0 {889					continue;890				}891892				// TODO: ERC20 transfer event893				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(894					collection.id,895					TokenId(token_id),896					user,897					amount,898				));899			}900		}901		Ok(())902	}903904	pub fn set_allowance_unchecked(905		collection: &RefungibleHandle<T>,906		sender: &T::CrossAccountId,907		spender: &T::CrossAccountId,908		token: TokenId,909		amount: u128,910	) {911		if amount == 0 {912			<Allowance<T>>::remove((collection.id, token, sender, spender));913		} else {914			<Allowance<T>>::insert((collection.id, token, sender, spender), amount);915		}916		// TODO: ERC20 approval event917		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(918			collection.id,919			token,920			sender.clone(),921			spender.clone(),922			amount,923		))924	}925926	/// Set allowance for the spender to `transfer` or `burn` sender's token pieces.927	///928	/// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.929	pub fn set_allowance(930		collection: &RefungibleHandle<T>,931		sender: &T::CrossAccountId,932		spender: &T::CrossAccountId,933		token: TokenId,934		amount: u128,935	) -> DispatchResult {936		if collection.permissions.access() == AccessMode::AllowList {937			collection.check_allowlist(sender)?;938			collection.check_allowlist(spender)?;939		}940941		<PalletCommon<T>>::ensure_correct_receiver(spender)?;942943		if <Balance<T>>::get((collection.id, token, sender)) < amount {944			ensure!(945				collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),946				<CommonError<T>>::CantApproveMoreThanOwned947			);948		}949950		// =========951952		Self::set_allowance_unchecked(collection, sender, spender, token, amount);953		Ok(())954	}955956	/// Returns allowance, which should be set after transaction957	fn check_allowed(958		collection: &RefungibleHandle<T>,959		spender: &T::CrossAccountId,960		from: &T::CrossAccountId,961		token: TokenId,962		amount: u128,963		nesting_budget: &dyn Budget,964	) -> Result<Option<u128>, DispatchError> {965		if spender.conv_eq(from) {966			return Ok(None);967		}968		if collection.permissions.access() == AccessMode::AllowList {969			// `from`, `to` checked in [`transfer`]970			collection.check_allowlist(spender)?;971		}972		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {973			// TODO: should collection owner be allowed to perform this transfer?974			ensure!(975				<PalletStructure<T>>::check_indirectly_owned(976					spender.clone(),977					source.0,978					source.1,979					None,980					nesting_budget981				)?,982				<CommonError<T>>::ApprovedValueTooLow,983			);984			return Ok(None);985		}986		let allowance =987			<Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);988		if allowance.is_none() {989			ensure!(990				collection.ignores_allowance(spender),991				<CommonError<T>>::ApprovedValueTooLow992			);993		}994		Ok(allowance)995	}996997	/// Transfer RFT token pieces from one account to another.998	///999	/// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1000	/// The owner should set allowance for the spender to transfer pieces.1001	///1002	/// [`transfer`]: struct.Pallet.html#method.transfer1003	pub fn transfer_from(1004		collection: &RefungibleHandle<T>,1005		spender: &T::CrossAccountId,1006		from: &T::CrossAccountId,1007		to: &T::CrossAccountId,1008		token: TokenId,1009		amount: u128,1010		nesting_budget: &dyn Budget,1011	) -> DispatchResult {1012		let allowance =1013			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10141015		// =========10161017		Self::transfer(collection, from, to, token, amount, nesting_budget)?;1018		if let Some(allowance) = allowance {1019			Self::set_allowance_unchecked(collection, from, spender, token, allowance);1020		}1021		Ok(())1022	}10231024	/// Burn RFT token pieces from the account.1025	///1026	/// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1027	/// set allowance for the spender to burn pieces1028	///1029	/// [`burn`]: struct.Pallet.html#method.burn1030	pub fn burn_from(1031		collection: &RefungibleHandle<T>,1032		spender: &T::CrossAccountId,1033		from: &T::CrossAccountId,1034		token: TokenId,1035		amount: u128,1036		nesting_budget: &dyn Budget,1037	) -> DispatchResult {1038		let allowance =1039			Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10401041		// =========10421043		Self::burn(collection, from, token, amount)?;1044		if let Some(allowance) = allowance {1045			Self::set_allowance_unchecked(collection, from, spender, token, allowance);1046		}1047		Ok(())1048	}10491050	/// Create RFT token.1051	///1052	/// The sender should be the owner/admin of the collection or collection should be configured1053	/// to allow public minting.1054	///1055	/// - `data`: Contains list of users who will become the owners of the token pieces and amount1056	///   of token pieces they will receive.1057	pub fn create_item(1058		collection: &RefungibleHandle<T>,1059		sender: &T::CrossAccountId,1060		data: CreateRefungibleExData<T::CrossAccountId>,1061		nesting_budget: &dyn Budget,1062	) -> DispatchResult {1063		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1064	}10651066	/// Repartition RFT token.1067	///1068	/// `repartition` will set token balance of the sender and total amount of token pieces.1069	/// Sender should own all of the token pieces. `repartition' could be done even if some1070	/// token pieces were burned before.1071	///1072	/// - `amount`: Total amount of token pieces that the token will have after `repartition`.1073	pub fn repartition(1074		collection: &RefungibleHandle<T>,1075		owner: &T::CrossAccountId,1076		token: TokenId,1077		amount: u128,1078	) -> DispatchResult {1079		ensure!(1080			amount <= MAX_REFUNGIBLE_PIECES,1081			<Error<T>>::WrongRefungiblePieces1082		);1083		ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1084		// Ensure user owns all pieces1085		let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1086		let balance = <Balance<T>>::get((collection.id, token, owner));1087		ensure!(1088			total_pieces == balance,1089			<Error<T>>::RepartitionWhileNotOwningAllPieces1090		);10911092		<Balance<T>>::insert((collection.id, token, owner), amount);1093		<TotalSupply<T>>::insert((collection.id, token), amount);1094		Ok(())1095	}10961097	fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1098		<TotalSupply<T>>::try_get((collection_id, token_id)).ok()1099	}11001101	pub fn set_collection_properties(1102		collection: &RefungibleHandle<T>,1103		sender: &T::CrossAccountId,1104		properties: Vec<Property>,1105	) -> DispatchResult {1106		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)1107	}11081109	pub fn delete_collection_properties(1110		collection: &RefungibleHandle<T>,1111		sender: &T::CrossAccountId,1112		property_keys: Vec<PropertyKey>,1113	) -> DispatchResult {1114		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1115	}11161117	pub fn set_token_property_permissions(1118		collection: &RefungibleHandle<T>,1119		sender: &T::CrossAccountId,1120		property_permissions: Vec<PropertyKeyPermission>,1121	) -> DispatchResult {1122		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1123	}1124}
modifiedprimitives/data-structs/src/lib.rsdiffbeforeafterboth
--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -534,9 +534,9 @@
 	#[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))]
 	#[derivative(Debug(format_with = "bounded::vec_debug"))]
 	pub const_data: BoundedVec<u8, CustomDataLimit>,
-	
+
 	pub pieces: u128,
-	
+
 	#[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))]
 	#[derivative(Debug(format_with = "bounded::vec_debug"))]
 	pub properties: CollectionPropertiesVec,