git.delta.rocks / unique-network / refs/commits / 73d9a3eafd0e

difftreelog

CORE-410 Set collection properties for exist collection

Trubnikov Sergey2022-06-28parent: #c4e0a12.patch.diff
in: master

3 files changed

modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -298,46 +298,72 @@
 
 	fn set_collection_properties(
 		&self,
-		_sender: T::CrossAccountId,
-		_property: Vec<Property>,
+		sender: T::CrossAccountId,
+		properties: Vec<Property>,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight = <CommonWeights<T>>::set_collection_properties(properties.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::set_collection_properties(self, &sender, properties),
+			weight,
+		)
 	}
 
 	fn delete_collection_properties(
 		&self,
-		_sender: &T::CrossAccountId,
-		_property_keys: Vec<PropertyKey>,
+		sender: &T::CrossAccountId,
+		property_keys: Vec<PropertyKey>,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		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,
-		_token_id: TokenId,
-		_property: Vec<Property>,
+		sender: T::CrossAccountId,
+		token_id: TokenId,
+		properties: Vec<Property>,
 		_nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight = <CommonWeights<T>>::set_token_properties(properties.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::set_token_properties(self, &sender, token_id, properties, false),
+			weight,
+		)
 	}
 
 	fn set_token_property_permissions(
 		&self,
-		_sender: &T::CrossAccountId,
-		_property_permissions: Vec<PropertyKeyPermission>,
+		sender: &T::CrossAccountId,
+		property_permissions: Vec<PropertyKeyPermission>,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight =
+			<CommonWeights<T>>::set_token_property_permissions(property_permissions.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::set_token_property_permissions(self, sender, property_permissions),
+			weight,
+		)
 	}
 
 	fn delete_token_properties(
 		&self,
-		_sender: T::CrossAccountId,
-		_token_id: TokenId,
-		_property_keys: Vec<PropertyKey>,
+		sender: T::CrossAccountId,
+		token_id: TokenId,
+		property_keys: Vec<PropertyKey>,
 		_nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		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),
+			weight,
+		)
 	}
 
 	fn check_nesting(
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, PropertyPermission95};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}
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, 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}
modifiedtests/src/refungible.test.tsdiffbeforeafterboth
--- a/tests/src/refungible.test.ts
+++ b/tests/src/refungible.test.ts
@@ -212,17 +212,20 @@
     });
   });
 
-  it.only('Set properties for exist collection', async () => {
+  it('Set properties for exist collection', async () => {
     await usingApi(async api => {
       const collectionId = await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'ReFungible'},
       });
 
-      const properties = [{key: 'key1', value: 'val1'}];
-      // await expect(executeTransaction(
-      //   api, 
-      //   alice, 
-      //   api.tx.unique.setCollectionProperties(collectionId, properties), 
-      // )).to.not.be.rejected;
+      const properties = [
+        {key: 'key1', value: 'val1'},
+        {key: 'key2', value: 'val2'},
+      ];
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setCollectionProperties(collectionId, properties), 
+      )).to.not.be.rejected;
 
       const propertyPermissions = [
         {key: 'key1', permission: {collectionAdmin: true, mutable: false, tokenOwner: true}},