git.delta.rocks / unique-network / refs/commits / 690118c8eab3

difftreelog

add EVM event for `destoyCollection`, refactor `Unique` pallet code, add test for events

PraetorP2022-10-24parent: #73c74cb.patch.diff
in: master

14 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5825,7 +5825,7 @@
 
 [[package]]
 name = "pallet-common"
-version = "0.1.8"
+version = "0.1.9"
 dependencies = [
  "ethereum",
  "evm-coder",
modifiedpallets/common/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/common/CHANGELOG.md
+++ b/pallets/common/CHANGELOG.md
@@ -2,29 +2,37 @@
 
 All notable changes to this project will be documented in this file.
 
+## [0.1.9] - 2022-10-13
+
+## Added
+
+- EVM event for `destroy_collection`.
+
 ## [0.1.8] - 2022-08-24
 
 ## Added
- - Eth methods for collection
-    + set_collection_sponsor_substrate
-    + has_collection_pending_sponsor
-    + remove_collection_sponsor
-    + get_collection_sponsor
+
+- Eth methods for collection
+  - set_collection_sponsor_substrate
+  - has_collection_pending_sponsor
+  - remove_collection_sponsor
+  - get_collection_sponsor
 - Add convert function from `uint256` to `CrossAccountId`.
 
 ## [0.1.7] - 2022-08-19
 
 ### Added
 
- - Add convert funtion from `CrossAccountId` to eth `uint256`.
+- Add convert funtion from `CrossAccountId` to eth `uint256`.
 
- 
 ## [0.1.6] - 2022-08-16
 
 ### Added
--   New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate).
 
+- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate).
+
 <!-- bureaucrate goes here -->
+
 ## [v0.1.5] 2022-08-16
 
 ### Other changes
@@ -45,19 +53,21 @@
 - build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b
 
 ## [0.1.3] - 2022-07-25
+
 ### Add
--   Some static property keys and values.
 
+- Some static property keys and values.
+
 ## [0.1.2] - 2022-07-20
 
 ### Fixed
 
--   Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid
-    mutability modifiers, causing invalid stub/abi generation.
+- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid
+  mutability modifiers, causing invalid stub/abi generation.
 
 ## [0.1.1] - 2022-07-14
 
 ### Added
 
- - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
-    This was an internal request to improve the web interface and support fractionalization event.
+- Implementation of RPC method `token_owners` returning 10 owners in no particular order.
+  This was an internal request to improve the web interface and support fractionalization event.
modifiedpallets/common/Cargo.tomldiffbeforeafterboth
--- a/pallets/common/Cargo.toml
+++ b/pallets/common/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-common"
-version = "0.1.8"
+version = "0.1.9"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/common/src/erc.rsdiffbeforeafterboth
--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -53,6 +53,12 @@
 		#[indexed]
 		collection_id: address,
 	},
+	/// The collection has been destroyed.
+	CollectionDestroyed {
+		/// Collection ID.
+		#[indexed]
+		collection_id: address,
+	},
 }
 
 /// Does not always represent a full collection, for RFT it is either
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -999,6 +999,13 @@
 		<CollectionProperties<T>>::remove(collection.id);
 
 		<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));
+
+		<PalletEvm<T>>::deposit_log(
+			erc::CollectionHelpersEvents::CollectionDestroyed {
+				collection_id: eth::collection_id_to_address(collection.id),
+			}
+			.to_log(T::ContractAddress::get()),
+		);
 		Ok(())
 	}
 
modifiedpallets/unique/src/eth/mod.rsdiffbeforeafterboth
--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -19,9 +19,10 @@
 use core::marker::PhantomData;
 use ethereum as _;
 use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*};
-use frame_support::{traits::Get, storage::StorageNMap};
+use frame_support::traits::Get;
+
+use crate::Pallet;
 
-use crate::sp_api_hidden_includes_decl_storage::hidden_include::StorageDoubleMap;
 use pallet_common::{
 	CollectionById,
 	dispatch::CollectionDispatch,
@@ -39,10 +40,7 @@
 	CollectionMode, PropertyValue, CollectionFlags,
 };
 
-use crate::{
-	Config, SelfWeightOf, weights::WeightInfo, NftTransferBasket, FungibleTransferBasket,
-	ReFungibleTransferBasket, NftApproveBasket, FungibleApproveBasket, RefungibleApproveBasket,
-};
+use crate::{Config, SelfWeightOf, weights::WeightInfo};
 
 use sp_std::vec::Vec;
 use alloc::format;
@@ -302,30 +300,13 @@
 	}
 
 	#[weight(<SelfWeightOf<T>>::destroy_collection())]
-	#[solidity(rename_selector = "destroyCollection")]
 	fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
-		let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
-			.ok_or("Invalid collection address format".into())
-			.map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
-		let collection = <pallet_common::CollectionHandle<T>>::try_get(collection_id)
-			.map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
-		collection
-			.check_is_internal()
-			.map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
 
-		T::CollectionDispatch::destroy(caller, collection)
-			.map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
-
-		let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
-		let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
-		let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
-		let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
-		let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
-		let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
-		Ok(())
+		let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
+			.ok_or("Invalid collection address format")?;
+		<Pallet<T>>::destroy_collection_internal(caller, collection_id)
+			.map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)
 	}
 
 	/// Check if a collection exists
modifiedpallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth
--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -20,6 +20,7 @@
 /// @dev inlined interface
 contract CollectionHelpersEvents {
 	event CollectionCreated(address indexed owner, address indexed collectionId);
+	event CollectionDestroyed(address indexed collectionId);
 }
 
 /// @title Contract, which allows users to operate with collections
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
before · pallets/unique/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//! # Unique Pallet18//!19//! A pallet governing Unique transactions.20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The Unique pallet's purpose is to be the primary interface between28//! external users and the inner structure of the Unique chains.29//!30//! It also contains an implementation of [`CollectionHelpers`][`eth`],31//! an Ethereum contract dealing with collection operations.32//!33//! ## Interface34//!35//! ### Dispatchables36//!37//! - `create_collection` - Create a collection of tokens. **Deprecated**, use `create_collection_ex`.38//! - `create_collection_ex` - Create a collection of tokens with explicit parameters.39//! - `destroy_collection` - Destroy a collection if no tokens exist within.40//! - `add_to_allow_list` - Add an address to allow list.41//! - `remove_from_allow_list` - Remove an address from allow list.42//! - `change_collection_owner` - Change the owner of the collection.43//! - `add_collection_admin` - Add an admin to a collection.44//! - `remove_collection_admin` - Remove admin of a collection.45//! - `set_collection_sponsor` - Invite a new collection sponsor.46//! - `confirm_sponsorship` - Confirm own sponsorship of a collection, becoming the sponsor.47//! - `remove_collection_sponsor` - Remove a sponsor from a collection.48//! - `create_item` - Create an item within a collection.49//! - `create_multiple_items` - Create multiple items within a collection.50//! - `set_collection_properties` - Add or change collection properties.51//! - `delete_collection_properties` - Delete specified collection properties.52//! - `set_token_properties` - Add or change token properties.53//! - `delete_token_properties` - Delete token properties.54//! - `set_token_property_permissions` - Add or change token property permissions of a collection.55//! - `create_multiple_items_ex` - Create multiple items within a collection with explicitly specified initial parameters.56//! - `set_transfers_enabled_flag` - Completely allow or disallow transfers for a particular collection.57//! - `burn_item` - Destroy an item.58//! - `burn_from` - Destroy an item on behalf of the owner as a non-owner account.59//! - `transfer` - Change ownership of the token.60//! - `transfer_from` - Change ownership of the token on behalf of the owner as a non-owner account.61//! - `approve` - Allow a non-permissioned address to transfer or burn an item.62//! - `set_collection_limits` - Set specific limits of a collection.63//! - `set_collection_permissions` - Set specific permissions of a collection.64//! - `repartition` - Re-partition a refungible token, while owning all of its parts.6566#![recursion_limit = "1024"]67#![cfg_attr(not(feature = "std"), no_std)]68#![allow(69	clippy::too_many_arguments,70	clippy::unnecessary_mut_passed,71	clippy::unused_unit72)]7374extern crate alloc;7576use frame_support::{77	decl_module, decl_storage, decl_error, decl_event,78	dispatch::DispatchResult,79	ensure, fail,80	weights::{Weight},81	pallet_prelude::{DispatchResultWithPostInfo, ConstU32},82	BoundedVec,83};84use scale_info::TypeInfo;85use frame_system::{self as system, ensure_signed};86use sp_std::{vec, vec::Vec};87use up_data_structs::{88	MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH,89	CreateItemData, CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId,90	SponsorshipState, CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,91	PropertyKeyPermission,92};93use pallet_evm::account::CrossAccountId;94use pallet_common::{95	CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_tx,96	dispatch::CollectionDispatch, RefungibleExtensionsWeightInfo,97};98pub mod eth;99100#[cfg(feature = "runtime-benchmarks")]101pub mod benchmarking;102pub mod weights;103use weights::WeightInfo;104105/// Maximum number of levels of depth in the token nesting tree.106pub const NESTING_BUDGET: u32 = 5;107108decl_error! {109	/// Errors for the common Unique transactions.110	pub enum Error for Module<T: Config> {111		/// Decimal_points parameter must be lower than [`up_data_structs::MAX_DECIMAL_POINTS`].112		CollectionDecimalPointLimitExceeded,113		/// This address is not set as sponsor, use setCollectionSponsor first.114		ConfirmUnsetSponsorFail,115		/// Length of items properties must be greater than 0.116		EmptyArgument,117		/// Repertition is only supported by refungible collection.118		RepartitionCalledOnNonRefungibleCollection,119	}120}121122/// Configuration trait of this pallet.123pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {124	/// Overarching event type.125	type RuntimeEvent: From<Event<Self>> + Into<<Self as frame_system::Config>::RuntimeEvent>;126127	/// Weight information for extrinsics in this pallet.128	type WeightInfo: WeightInfo;129130	/// Weight information for common pallet operations.131	type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;132133	/// Weight info information for extra refungible pallet operations.134	type RefungibleExtensionsWeightInfo: RefungibleExtensionsWeightInfo;135}136137decl_event! {138	pub enum Event<T>139	where140		<T as frame_system::Config>::AccountId,141		<T as pallet_evm::account::Config>::CrossAccountId,142	{143		/// Collection sponsor was removed144		///145		/// # Arguments146		/// * collection_id: ID of the affected collection.147		CollectionSponsorRemoved(CollectionId),148149		/// Collection admin was added150		///151		/// # Arguments152		/// * collection_id: ID of the affected collection.153		/// * admin: Admin address.154		CollectionAdminAdded(CollectionId, CrossAccountId),155156		/// Collection owned was changed157		///158		/// # Arguments159		/// * collection_id: ID of the affected collection.160		/// * owner: New owner address.161		CollectionOwnedChanged(CollectionId, AccountId),162163		/// Collection sponsor was set164		///165		/// # Arguments166		/// * collection_id: ID of the affected collection.167		/// * owner: New sponsor address.168		CollectionSponsorSet(CollectionId, AccountId),169170		/// New sponsor was confirm171		///172		/// # Arguments173		/// * collection_id: ID of the affected collection.174		/// * sponsor: New sponsor address.175		SponsorshipConfirmed(CollectionId, AccountId),176177		/// Collection admin was removed178		///179		/// # Arguments180		/// * collection_id: ID of the affected collection.181		/// * admin: Removed admin address.182		CollectionAdminRemoved(CollectionId, CrossAccountId),183184		/// Address was removed from the allow list185		///186		/// # Arguments187		/// * collection_id: ID of the affected collection.188		/// * user: Address of the removed account.189		AllowListAddressRemoved(CollectionId, CrossAccountId),190191		/// Address was added to the allow list192		///193		/// # Arguments194		/// * collection_id: ID of the affected collection.195		/// * user: Address of the added account.196		AllowListAddressAdded(CollectionId, CrossAccountId),197198		/// Collection limits were set199		///200		/// # Arguments201		/// * collection_id: ID of the affected collection.202		CollectionLimitSet(CollectionId),203204		/// Collection permissions were set205		///206		/// # Arguments207		/// * collection_id: ID of the affected collection.208		CollectionPermissionSet(CollectionId),209	}210}211212type SelfWeightOf<T> = <T as Config>::WeightInfo;213214// # Used definitions215//216// ## User control levels217//218// chain-controlled - key is uncontrolled by user219//                    i.e autoincrementing index220//                    can use non-cryptographic hash221// real - key is controlled by user222//        but it is hard to generate enough colliding values, i.e owner of signed txs223//        can use non-cryptographic hash224// controlled - key is completly controlled by users225//              i.e maps with mutable keys226//              should use cryptographic hash227//228// ## User control level downgrade reasons229//230// ?1 - chain-controlled -> controlled231//      collections/tokens can be destroyed, resulting in massive holes232// ?2 - chain-controlled -> controlled233//      same as ?1, but can be only added, resulting in easier exploitation234// ?3 - real -> controlled235//      no confirmation required, so addresses can be easily generated236decl_storage! {237	trait Store for Module<T: Config> as Unique {238239		//#region Private members240		/// Used for migrations241		ChainVersion: u64;242		//#endregion243244		//#region Tokens transfer sponosoring rate limit baskets245		/// (Collection id (controlled?2), who created (real))246		/// TODO: Off chain worker should remove from this map when collection gets removed247		pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;248		/// Collection id (controlled?2), token id (controlled?2)249		pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;250		/// Collection id (controlled?2), owning user (real)251		pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;252		/// Collection id (controlled?2), token id (controlled?2)253		pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;254		//#endregion255256		/// Variable metadata sponsoring257		/// Collection id (controlled?2), token id (controlled?2)258		#[deprecated]259		pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;260		/// Last sponsoring of token property setting // todo:doc rephrase this and the following261		pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;262263		/// Last sponsoring of NFT approval in a collection264		pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;265		/// Last sponsoring of fungible tokens approval in a collection266		pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;267		/// Last sponsoring of RFT approval in a collection268		pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;269	}270}271272decl_module! {273	/// Type alias to Pallet, to be used by construct_runtime.274	pub struct Module<T: Config> for enum Call275	where276		origin: T::RuntimeOrigin277	{278		type Error = Error<T>;279280		pub fn deposit_event() = default;281282		fn on_initialize(_now: T::BlockNumber) -> Weight {283			Weight::zero()284		}285286		fn on_runtime_upgrade() -> Weight {287			Weight::zero()288		}289290		/// Create a collection of tokens.291		///292		/// Each Token may have multiple properties encoded as an array of bytes293		/// of certain length. The initial owner of the collection is set294		/// to the address that signed the transaction and can be changed later.295		///296		/// Prefer the more advanced [`create_collection_ex`][`Pallet::create_collection_ex`] instead.297		///298		/// # Permissions299		///300		/// * Anyone - becomes the owner of the new collection.301		///302		/// # Arguments303		///304		/// * `collection_name`: Wide-character string with collection name305		/// (limit [`MAX_COLLECTION_NAME_LENGTH`]).306		/// * `collection_description`: Wide-character string with collection description307		/// (limit [`MAX_COLLECTION_DESCRIPTION_LENGTH`]).308		/// * `token_prefix`: Byte string containing the token prefix to mark a collection309		/// to which a token belongs (limit [`MAX_TOKEN_PREFIX_LENGTH`]).310		/// * `mode`: Type of items stored in the collection and type dependent data.311		// returns collection ID312		#[weight = <SelfWeightOf<T>>::create_collection()]313		#[deprecated(note = "`create_collection_ex` is more up-to-date and advanced, prefer it instead")]314		pub fn create_collection(315			origin,316			collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,317			collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,318			token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,319			mode: CollectionMode320		) -> DispatchResult {321			let data: CreateCollectionData<T::AccountId> = CreateCollectionData {322				name: collection_name,323				description: collection_description,324				token_prefix,325				mode,326				..Default::default()327			};328			Self::create_collection_ex(origin, data)329		}330331		/// Create a collection with explicit parameters.332		///333		/// Prefer it to the deprecated [`create_collection`][`Pallet::create_collection`] method.334		///335		/// # Permissions336		///337		/// * Anyone - becomes the owner of the new collection.338		///339		/// # Arguments340		///341		/// * `data`: Explicit data of a collection used for its creation.342		#[weight = <SelfWeightOf<T>>::create_collection()]343		pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {344			let sender = ensure_signed(origin)?;345346			// =========347			let sender = T::CrossAccountId::from_sub(sender);348			let _id = T::CollectionDispatch::create(sender.clone(), sender, data, Default::default())?;349350			Ok(())351		}352353		/// Destroy a collection if no tokens exist within.354		///355		/// # Permissions356		///357		/// * Collection owner358		///359		/// # Arguments360		///361		/// * `collection_id`: Collection to destroy.362		#[weight = <SelfWeightOf<T>>::destroy_collection()]363		pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {364			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);365			let collection = <CollectionHandle<T>>::try_get(collection_id)?;366			collection.check_is_internal()?;367368			// =========369370			T::CollectionDispatch::destroy(sender, collection)?;371372			// TODO: basket cleanup should be moved elsewhere373			// Maybe runtime dispatch.rs should perform it?374375			let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);376			let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);377			let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);378379			let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);380			let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);381			let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);382383			Ok(())384		}385386		/// Add an address to allow list.387		///388		/// # Permissions389		///390		/// * Collection owner391		/// * Collection admin392		///393		/// # Arguments394		///395		/// * `collection_id`: ID of the modified collection.396		/// * `address`: ID of the address to be added to the allowlist.397		#[weight = <SelfWeightOf<T>>::add_to_allow_list()]398		pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{399400			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);401			let collection = <CollectionHandle<T>>::try_get(collection_id)?;402			collection.check_is_internal()?;403404			<PalletCommon<T>>::toggle_allowlist(405				&collection,406				&sender,407				&address,408				true,409			)?;410411			Self::deposit_event(Event::<T>::AllowListAddressAdded(412				collection_id,413				address414			));415416			Ok(())417		}418419		/// Remove an address from allow list.420		///421		/// # Permissions422		///423		/// * Collection owner424		/// * Collection admin425		///426		/// # Arguments427		///428		/// * `collection_id`: ID of the modified collection.429		/// * `address`: ID of the address to be removed from the allowlist.430		#[weight = <SelfWeightOf<T>>::remove_from_allow_list()]431		pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{432433			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);434			let collection = <CollectionHandle<T>>::try_get(collection_id)?;435			collection.check_is_internal()?;436437			<PalletCommon<T>>::toggle_allowlist(438				&collection,439				&sender,440				&address,441				false,442			)?;443444			<Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(445				collection_id,446				address447			));448449			Ok(())450		}451452		/// Change the owner of the collection.453		///454		/// # Permissions455		///456		/// * Collection owner457		///458		/// # Arguments459		///460		/// * `collection_id`: ID of the modified collection.461		/// * `new_owner`: ID of the account that will become the owner.462		#[weight = <SelfWeightOf<T>>::change_collection_owner()]463		pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {464465			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);466467			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;468			target_collection.check_is_internal()?;469			target_collection.check_is_owner(&sender)?;470471			target_collection.owner = new_owner.clone();472			<Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(473				collection_id,474				new_owner475			));476477			target_collection.save()478		}479480		/// Add an admin to a collection.481		///482		/// NFT Collection can be controlled by multiple admin addresses483		/// (some which can also be servers, for example). Admins can issue484		/// and burn NFTs, as well as add and remove other admins,485		/// but cannot change NFT or Collection ownership.486		///487		/// # Permissions488		///489		/// * Collection owner490		/// * Collection admin491		///492		/// # Arguments493		///494		/// * `collection_id`: ID of the Collection to add an admin for.495		/// * `new_admin`: Address of new admin to add.496		#[weight = <SelfWeightOf<T>>::add_collection_admin()]497		pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {498			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);499			let collection = <CollectionHandle<T>>::try_get(collection_id)?;500			collection.check_is_internal()?;501502			<Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(503				collection_id,504				new_admin_id.clone()505			));506507			<PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)508		}509510		/// Remove admin of a collection.511		///512		/// An admin address can remove itself. List of admins may become empty,513		/// in which case only Collection Owner will be able to add an Admin.514		///515		/// # Permissions516		///517		/// * Collection owner518		/// * Collection admin519		///520		/// # Arguments521		///522		/// * `collection_id`: ID of the collection to remove the admin for.523		/// * `account_id`: Address of the admin to remove.524		#[weight = <SelfWeightOf<T>>::remove_collection_admin()]525		pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {526			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);527			let collection = <CollectionHandle<T>>::try_get(collection_id)?;528			collection.check_is_internal()?;529530			<Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(531				collection_id,532				account_id.clone()533			));534535			<PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)536		}537538		/// Set (invite) a new collection sponsor.539		///540		/// If successful, confirmation from the sponsor-to-be will be pending.541		///542		/// # Permissions543		///544		/// * Collection owner545		/// * Collection admin546		///547		/// # Arguments548		///549		/// * `collection_id`: ID of the modified collection.550		/// * `new_sponsor`: ID of the account of the sponsor-to-be.551		#[weight = <SelfWeightOf<T>>::set_collection_sponsor()]552		pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {553			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);554555			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;556			target_collection.check_is_owner_or_admin(&sender)?;557			target_collection.check_is_internal()?;558559			target_collection.set_sponsor(new_sponsor.clone())?;560561			<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(562				collection_id,563				new_sponsor564			));565566			target_collection.save()567		}568569		/// Confirm own sponsorship of a collection, becoming the sponsor.570		///571		/// An invitation must be pending, see [`set_collection_sponsor`][`Pallet::set_collection_sponsor`].572		/// Sponsor can pay the fees of a transaction instead of the sender,573		/// but only within specified limits.574		///575		/// # Permissions576		///577		/// * Sponsor-to-be578		///579		/// # Arguments580		///581		/// * `collection_id`: ID of the collection with the pending sponsor.582		#[weight = <SelfWeightOf<T>>::confirm_sponsorship()]583		pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {584			let sender = ensure_signed(origin)?;585586			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;587			target_collection.check_is_internal()?;588			ensure!(589				target_collection.confirm_sponsorship(&sender)?,590				Error::<T>::ConfirmUnsetSponsorFail591			);592593			<Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(594				collection_id,595				sender596			));597598			target_collection.save()599		}600601		/// Remove a collection's a sponsor, making everyone pay for their own transactions.602		///603		/// # Permissions604		///605		/// * Collection owner606		///607		/// # Arguments608		///609		/// * `collection_id`: ID of the collection with the sponsor to remove.610		#[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]611		pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {612			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);613614			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;615			target_collection.check_is_internal()?;616			target_collection.check_is_owner(&sender)?;617618			target_collection.sponsorship = SponsorshipState::Disabled;619620			<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(621				collection_id622			));623			target_collection.save()624		}625626		/// Mint an item within a collection.627		///628		/// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].629		///630		/// # Permissions631		///632		/// * Collection owner633		/// * Collection admin634		/// * Anyone if635		///     * Allow List is enabled, and636		///     * Address is added to allow list, and637		///     * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])638		///639		/// # Arguments640		///641		/// * `collection_id`: ID of the collection to which an item would belong.642		/// * `owner`: Address of the initial owner of the item.643		/// * `data`: Token data describing the item to store on chain.644		#[weight = T::CommonWeightInfo::create_item()]645		pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {646			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);647			let budget = budget::Value::new(NESTING_BUDGET);648649			dispatch_tx::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))650		}651652		/// Create multiple items within a collection.653		///654		/// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].655		///656		/// # Permissions657		///658		/// * Collection owner659		/// * Collection admin660		/// * Anyone if661		///     * Allow List is enabled, and662		///     * Address is added to the allow list, and663		///     * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])664		///665		/// # Arguments666		///667		/// * `collection_id`: ID of the collection to which the tokens would belong.668		/// * `owner`: Address of the initial owner of the tokens.669		/// * `items_data`: Vector of data describing each item to be created.670		#[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]671		pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {672			ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);673			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);674			let budget = budget::Value::new(NESTING_BUDGET);675676			dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))677		}678679		/// Add or change collection properties.680		///681		/// # Permissions682		///683		/// * Collection owner684		/// * Collection admin685		///686		/// # Arguments687		///688		/// * `collection_id`: ID of the modified collection.689		/// * `properties`: Vector of key-value pairs stored as the collection's metadata.690		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.691		#[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]692		pub fn set_collection_properties(693			origin,694			collection_id: CollectionId,695			properties: Vec<Property>696		) -> DispatchResultWithPostInfo {697			ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);698699			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);700701			dispatch_tx::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))702		}703704		/// Delete specified collection properties.705		///706		/// # Permissions707		///708		/// * Collection Owner709		/// * Collection Admin710		///711		/// # Arguments712		///713		/// * `collection_id`: ID of the modified collection.714		/// * `property_keys`: Vector of keys of the properties to be deleted.715		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.716		#[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]717		pub fn delete_collection_properties(718			origin,719			collection_id: CollectionId,720			property_keys: Vec<PropertyKey>,721		) -> DispatchResultWithPostInfo {722			ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);723724			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);725726			dispatch_tx::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))727		}728729		/// Add or change token properties according to collection's permissions.730		/// Currently properties only work with NFTs.731		///732		/// # Permissions733		///734		/// * Depends on collection's token property permissions and specified property mutability:735		/// 	* Collection owner736		/// 	* Collection admin737		/// 	* Token owner738		///739		/// See [`set_token_property_permissions`][`Pallet::set_token_property_permissions`].740		///741		/// # Arguments742		///743		/// * `collection_id: ID of the collection to which the token belongs.744		/// * `token_id`: ID of the modified token.745		/// * `properties`: Vector of key-value pairs stored as the token's metadata.746		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.747		#[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]748		pub fn set_token_properties(749			origin,750			collection_id: CollectionId,751			token_id: TokenId,752			properties: Vec<Property>753		) -> DispatchResultWithPostInfo {754			ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);755756			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);757			let budget = budget::Value::new(NESTING_BUDGET);758759			dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties, &budget))760		}761762		/// Delete specified token properties. Currently properties only work with NFTs.763		///764		/// # Permissions765		///766		/// * Depends on collection's token property permissions and specified property mutability:767		/// 	* Collection owner768		/// 	* Collection admin769		/// 	* Token owner770		///771		/// # Arguments772		///773		/// * `collection_id`: ID of the collection to which the token belongs.774		/// * `token_id`: ID of the modified token.775		/// * `property_keys`: Vector of keys of the properties to be deleted.776		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.777		#[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]778		pub fn delete_token_properties(779			origin,780			collection_id: CollectionId,781			token_id: TokenId,782			property_keys: Vec<PropertyKey>783		) -> DispatchResultWithPostInfo {784			ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);785786			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);787			let budget = budget::Value::new(NESTING_BUDGET);788789			dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys, &budget))790		}791792		/// Add or change token property permissions of a collection.793		///794		/// Without a permission for a particular key, a property with that key795		/// cannot be created in a token.796		///797		/// # Permissions798		///799		/// * Collection owner800		/// * Collection admin801		///802		/// # Arguments803		///804		/// * `collection_id`: ID of the modified collection.805		/// * `property_permissions`: Vector of permissions for property keys.806		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.807		#[weight = T::CommonWeightInfo::set_token_property_permissions(property_permissions.len() as u32)]808		pub fn set_token_property_permissions(809			origin,810			collection_id: CollectionId,811			property_permissions: Vec<PropertyKeyPermission>,812		) -> DispatchResultWithPostInfo {813			ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);814815			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);816817			dispatch_tx::<T, _>(collection_id, |d| d.set_token_property_permissions(&sender, property_permissions))818		}819820		/// Create multiple items within a collection with explicitly specified initial parameters.821		///822		/// # Permissions823		///824		/// * Collection owner825		/// * Collection admin826		/// * Anyone if827		///     * Allow List is enabled, and828		///     * Address is added to allow list, and829		///     * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])830		///831		/// # Arguments832		///833		/// * `collection_id`: ID of the collection to which the tokens would belong.834		/// * `data`: Explicit item creation data.835		#[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]836		pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {837			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);838			let budget = budget::Value::new(NESTING_BUDGET);839840			dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))841		}842843		/// Completely allow or disallow transfers for a particular collection.844		///845		/// # Permissions846		///847		/// * Collection owner848		///849		/// # Arguments850		///851		/// * `collection_id`: ID of the collection.852		/// * `value`: New value of the flag, are transfers allowed?853		#[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]854		pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {855			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);856			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;857			target_collection.check_is_internal()?;858			target_collection.check_is_owner(&sender)?;859860			// =========861862			target_collection.limits.transfers_enabled = Some(value);863			target_collection.save()864		}865866		/// Destroy an item.867		///868		/// # Permissions869		///870		/// * Collection owner871		/// * Collection admin872		/// * Current item owner873		///874		/// # Arguments875		///876		/// * `collection_id`: ID of the collection to which the item belongs.877		/// * `item_id`: ID of item to burn.878		/// * `value`: Number of pieces of the item to destroy.879		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.880		///     * Fungible Mode: The desired number of pieces to burn.881		///     * Re-Fungible Mode: The desired number of pieces to burn.882		#[weight = T::CommonWeightInfo::burn_item()]883		pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {884			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);885886			let post_info = dispatch_tx::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;887			if value == 1 {888				<NftTransferBasket<T>>::remove(collection_id, item_id);889				<NftApproveBasket<T>>::remove(collection_id, item_id);890			}891			// Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?892			// <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());893			// <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));894			Ok(post_info)895		}896897		/// Destroy a token on behalf of the owner as a non-owner account.898		///899		/// See also: [`approve`][`Pallet::approve`].900		///901		/// After this method executes, one approval is removed from the total so that902		/// the approved address will not be able to transfer this item again from this owner.903		///904		/// # Permissions905		///906		/// * Collection owner907		/// * Collection admin908		/// * Current token owner909		/// * Address approved by current item owner910		///911		/// # Arguments912		///913		/// * `from`: The owner of the burning item.914		/// * `collection_id`: ID of the collection to which the item belongs.915		/// * `item_id`: ID of item to burn.916		/// * `value`: Number of pieces to burn.917		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.918		///     * Fungible Mode: The desired number of pieces to burn.919		///     * Re-Fungible Mode: The desired number of pieces to burn.920		#[weight = T::CommonWeightInfo::burn_from()]921		pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {922			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);923			let budget = budget::Value::new(NESTING_BUDGET);924925			dispatch_tx::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))926		}927928		/// Change ownership of the token.929		///930		/// # Permissions931		///932		/// * Collection owner933		/// * Collection admin934		/// * Current token owner935		///936		/// # Arguments937		///938		/// * `recipient`: Address of token recipient.939		/// * `collection_id`: ID of the collection the item belongs to.940		/// * `item_id`: ID of the item.941		///     * Non-Fungible Mode: Required.942		///     * Fungible Mode: Ignored.943		///     * Re-Fungible Mode: Required.944		///945		/// * `value`: Amount to transfer.946		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.947		///     * Fungible Mode: The desired number of pieces to transfer.948		///     * Re-Fungible Mode: The desired number of pieces to transfer.949		#[weight = T::CommonWeightInfo::transfer()]950		pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {951			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);952			let budget = budget::Value::new(NESTING_BUDGET);953954			dispatch_tx::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))955		}956957		/// Allow a non-permissioned address to transfer or burn an item.958		///959		/// # Permissions960		///961		/// * Collection owner962		/// * Collection admin963		/// * Current item owner964		///965		/// # Arguments966		///967		/// * `spender`: Account to be approved to make specific transactions on non-owned tokens.968		/// * `collection_id`: ID of the collection the item belongs to.969		/// * `item_id`: ID of the item transactions on which are now approved.970		/// * `amount`: Number of pieces of the item approved for a transaction (maximum of 1 for NFTs).971		/// Set to 0 to revoke the approval.972		#[weight = T::CommonWeightInfo::approve()]973		pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {974			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);975976			dispatch_tx::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))977		}978979		/// Change ownership of an item on behalf of the owner as a non-owner account.980		///981		/// See the [`approve`][`Pallet::approve`] method for additional information.982		///983		/// After this method executes, one approval is removed from the total so that984		/// the approved address will not be able to transfer this item again from this owner.985		///986		/// # Permissions987		///988		/// * Collection owner989		/// * Collection admin990		/// * Current item owner991		/// * Address approved by current item owner992		///993		/// # Arguments994		///995		/// * `from`: Address that currently owns the token.996		/// * `recipient`: Address of the new token-owner-to-be.997		/// * `collection_id`: ID of the collection the item.998		/// * `item_id`: ID of the item to be transferred.999		/// * `value`: Amount to transfer.1000		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.1001		///     * Fungible Mode: The desired number of pieces to transfer.1002		///     * Re-Fungible Mode: The desired number of pieces to transfer.1003		#[weight = T::CommonWeightInfo::transfer_from()]1004		pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {1005			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1006			let budget = budget::Value::new(NESTING_BUDGET);10071008			dispatch_tx::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))1009		}10101011		/// Set specific limits of a collection. Empty, or None fields mean chain default.1012		///1013		/// # Permissions1014		///1015		/// * Collection owner1016		/// * Collection admin1017		///1018		/// # Arguments1019		///1020		/// * `collection_id`: ID of the modified collection.1021		/// * `new_limit`: New limits of the collection. Fields that are not set (None)1022		/// will not overwrite the old ones.1023		#[weight = <SelfWeightOf<T>>::set_collection_limits()]1024		pub fn set_collection_limits(1025			origin,1026			collection_id: CollectionId,1027			new_limit: CollectionLimits,1028		) -> DispatchResult {1029			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1030			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1031			target_collection.check_is_internal()?;1032			target_collection.check_is_owner_or_admin(&sender)?;1033			let old_limit = &target_collection.limits;10341035			target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;10361037			<Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(1038				collection_id1039			));10401041			target_collection.save()1042		}10431044		/// Set specific permissions of a collection. Empty, or None fields mean chain default.1045		///1046		/// # Permissions1047		///1048		/// * Collection owner1049		/// * Collection admin1050		///1051		/// # Arguments1052		///1053		/// * `collection_id`: ID of the modified collection.1054		/// * `new_permission`: New permissions of the collection. Fields that are not set (None)1055		/// will not overwrite the old ones.1056		#[weight = <SelfWeightOf<T>>::set_collection_limits()]1057		pub fn set_collection_permissions(1058			origin,1059			collection_id: CollectionId,1060			new_permission: CollectionPermissions,1061		) -> DispatchResult {1062			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1063			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1064			target_collection.check_is_internal()?;1065			target_collection.check_is_owner_or_admin(&sender)?;1066			let old_limit = &target_collection.permissions;10671068			target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_permission)?;10691070			<Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(1071				collection_id1072			));10731074			target_collection.save()1075		}10761077		/// Re-partition a refungible token, while owning all of its parts/pieces.1078		///1079		/// # Permissions1080		///1081		/// * Token owner (must own every part)1082		///1083		/// # Arguments1084		///1085		/// * `collection_id`: ID of the collection the RFT belongs to.1086		/// * `token_id`: ID of the RFT.1087		/// * `amount`: New number of parts/pieces into which the token shall be partitioned.1088		#[weight = T::RefungibleExtensionsWeightInfo::repartition()]1089		pub fn repartition(1090			origin,1091			collection_id: CollectionId,1092			token_id: TokenId,1093			amount: u128,1094		) -> DispatchResultWithPostInfo {1095			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1096			dispatch_tx::<T, _>(collection_id, |d| {1097				if let Some(refungible_extensions) = d.refungible_extensions() {1098					refungible_extensions.repartition(&sender, token_id, amount)1099				} else {1100					fail!(<Error<T>>::RepartitionCalledOnNonRefungibleCollection)1101				}1102			})1103		}1104	}1105}11061107impl<T: Config> Pallet<T> {1108	/// Force set `sponsor` for `collection`.1109	///1110	/// Differs from [`set_collection_sponsor`][`Pallet::set_collection_sponsor`] in that confirmation1111	/// from the `sponsor` is not required.1112	///1113	/// # Arguments1114	///1115	/// * `sponsor`: ID of the account of the sponsor-to-be.1116	/// * `collection_id`: ID of the modified collection.1117	pub fn force_set_sponsor(sponsor: T::AccountId, collection_id: CollectionId) -> DispatchResult {1118		let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1119		target_collection.check_is_internal()?;1120		target_collection.set_sponsor(sponsor.clone())?;11211122		Self::deposit_event(Event::<T>::CollectionSponsorSet(1123			collection_id,1124			sponsor.clone(),1125		));11261127		ensure!(1128			target_collection.confirm_sponsorship(&sponsor)?,1129			Error::<T>::ConfirmUnsetSponsorFail1130		);11311132		Self::deposit_event(Event::<T>::SponsorshipConfirmed(collection_id, sponsor));11331134		target_collection.save()1135	}11361137	/// Force remove `sponsor` for `collection`.1138	///1139	/// Differs from `remove_sponsor` in that1140	/// it doesn't require consent from the `owner` of the collection.1141	///1142	/// # Arguments1143	///1144	/// * `collection_id`: ID of the modified collection.1145	pub fn force_remove_collection_sponsor(collection_id: CollectionId) -> DispatchResult {1146		let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1147		target_collection.check_is_internal()?;1148		target_collection.sponsorship = SponsorshipState::Disabled;11491150		Self::deposit_event(Event::<T>::CollectionSponsorRemoved(collection_id));11511152		target_collection.save()1153	}1154}
after · pallets/unique/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//! # Unique Pallet18//!19//! A pallet governing Unique transactions.20//!21//! - [`Config`]22//! - [`Call`]23//! - [`Pallet`]24//!25//! ## Overview26//!27//! The Unique pallet's purpose is to be the primary interface between28//! external users and the inner structure of the Unique chains.29//!30//! It also contains an implementation of [`CollectionHelpers`][`eth`],31//! an Ethereum contract dealing with collection operations.32//!33//! ## Interface34//!35//! ### Dispatchables36//!37//! - `create_collection` - Create a collection of tokens. **Deprecated**, use `create_collection_ex`.38//! - `create_collection_ex` - Create a collection of tokens with explicit parameters.39//! - `destroy_collection` - Destroy a collection if no tokens exist within.40//! - `add_to_allow_list` - Add an address to allow list.41//! - `remove_from_allow_list` - Remove an address from allow list.42//! - `change_collection_owner` - Change the owner of the collection.43//! - `add_collection_admin` - Add an admin to a collection.44//! - `remove_collection_admin` - Remove admin of a collection.45//! - `set_collection_sponsor` - Invite a new collection sponsor.46//! - `confirm_sponsorship` - Confirm own sponsorship of a collection, becoming the sponsor.47//! - `remove_collection_sponsor` - Remove a sponsor from a collection.48//! - `create_item` - Create an item within a collection.49//! - `create_multiple_items` - Create multiple items within a collection.50//! - `set_collection_properties` - Add or change collection properties.51//! - `delete_collection_properties` - Delete specified collection properties.52//! - `set_token_properties` - Add or change token properties.53//! - `delete_token_properties` - Delete token properties.54//! - `set_token_property_permissions` - Add or change token property permissions of a collection.55//! - `create_multiple_items_ex` - Create multiple items within a collection with explicitly specified initial parameters.56//! - `set_transfers_enabled_flag` - Completely allow or disallow transfers for a particular collection.57//! - `burn_item` - Destroy an item.58//! - `burn_from` - Destroy an item on behalf of the owner as a non-owner account.59//! - `transfer` - Change ownership of the token.60//! - `transfer_from` - Change ownership of the token on behalf of the owner as a non-owner account.61//! - `approve` - Allow a non-permissioned address to transfer or burn an item.62//! - `set_collection_limits` - Set specific limits of a collection.63//! - `set_collection_permissions` - Set specific permissions of a collection.64//! - `repartition` - Re-partition a refungible token, while owning all of its parts.6566#![recursion_limit = "1024"]67#![cfg_attr(not(feature = "std"), no_std)]68#![allow(69	clippy::too_many_arguments,70	clippy::unnecessary_mut_passed,71	clippy::unused_unit72)]7374extern crate alloc;7576use frame_support::{77	decl_module, decl_storage, decl_error, decl_event,78	dispatch::DispatchResult,79	ensure, fail,80	weights::{Weight},81	pallet_prelude::{DispatchResultWithPostInfo, ConstU32},82	BoundedVec,83};84use scale_info::TypeInfo;85use frame_system::{self as system, ensure_signed};86use sp_std::{vec, vec::Vec};87use up_data_structs::{88	MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_TOKEN_PREFIX_LENGTH,89	CreateItemData, CollectionLimits, CollectionPermissions, CollectionId, CollectionMode, TokenId,90	SponsorshipState, CreateCollectionData, CreateItemExData, budget, Property, PropertyKey,91	PropertyKeyPermission,92};93use pallet_evm::account::CrossAccountId;94use pallet_common::{95	CollectionHandle, Pallet as PalletCommon, CommonWeightInfo, dispatch::dispatch_tx,96	dispatch::CollectionDispatch, RefungibleExtensionsWeightInfo,97};98pub mod eth;99100#[cfg(feature = "runtime-benchmarks")]101pub mod benchmarking;102pub mod weights;103use weights::WeightInfo;104105/// Maximum number of levels of depth in the token nesting tree.106pub const NESTING_BUDGET: u32 = 5;107108decl_error! {109	/// Errors for the common Unique transactions.110	pub enum Error for Module<T: Config> {111		/// Decimal_points parameter must be lower than [`up_data_structs::MAX_DECIMAL_POINTS`].112		CollectionDecimalPointLimitExceeded,113		/// This address is not set as sponsor, use setCollectionSponsor first.114		ConfirmUnsetSponsorFail,115		/// Length of items properties must be greater than 0.116		EmptyArgument,117		/// Repertition is only supported by refungible collection.118		RepartitionCalledOnNonRefungibleCollection,119	}120}121122/// Configuration trait of this pallet.123pub trait Config: system::Config + pallet_common::Config + Sized + TypeInfo {124	/// Overarching event type.125	type RuntimeEvent: From<Event<Self>> + Into<<Self as frame_system::Config>::RuntimeEvent>;126127	/// Weight information for extrinsics in this pallet.128	type WeightInfo: WeightInfo;129130	/// Weight information for common pallet operations.131	type CommonWeightInfo: CommonWeightInfo<Self::CrossAccountId>;132133	/// Weight info information for extra refungible pallet operations.134	type RefungibleExtensionsWeightInfo: RefungibleExtensionsWeightInfo;135}136137decl_event! {138	pub enum Event<T>139	where140		<T as frame_system::Config>::AccountId,141		<T as pallet_evm::account::Config>::CrossAccountId,142	{143		/// Collection sponsor was removed144		///145		/// # Arguments146		/// * collection_id: ID of the affected collection.147		CollectionSponsorRemoved(CollectionId),148149		/// Collection admin was added150		///151		/// # Arguments152		/// * collection_id: ID of the affected collection.153		/// * admin: Admin address.154		CollectionAdminAdded(CollectionId, CrossAccountId),155156		/// Collection owned was changed157		///158		/// # Arguments159		/// * collection_id: ID of the affected collection.160		/// * owner: New owner address.161		CollectionOwnedChanged(CollectionId, AccountId),162163		/// Collection sponsor was set164		///165		/// # Arguments166		/// * collection_id: ID of the affected collection.167		/// * owner: New sponsor address.168		CollectionSponsorSet(CollectionId, AccountId),169170		/// New sponsor was confirm171		///172		/// # Arguments173		/// * collection_id: ID of the affected collection.174		/// * sponsor: New sponsor address.175		SponsorshipConfirmed(CollectionId, AccountId),176177		/// Collection admin was removed178		///179		/// # Arguments180		/// * collection_id: ID of the affected collection.181		/// * admin: Removed admin address.182		CollectionAdminRemoved(CollectionId, CrossAccountId),183184		/// Address was removed from the allow list185		///186		/// # Arguments187		/// * collection_id: ID of the affected collection.188		/// * user: Address of the removed account.189		AllowListAddressRemoved(CollectionId, CrossAccountId),190191		/// Address was added to the allow list192		///193		/// # Arguments194		/// * collection_id: ID of the affected collection.195		/// * user: Address of the added account.196		AllowListAddressAdded(CollectionId, CrossAccountId),197198		/// Collection limits were set199		///200		/// # Arguments201		/// * collection_id: ID of the affected collection.202		CollectionLimitSet(CollectionId),203204		/// Collection permissions were set205		///206		/// # Arguments207		/// * collection_id: ID of the affected collection.208		CollectionPermissionSet(CollectionId),209	}210}211212type SelfWeightOf<T> = <T as Config>::WeightInfo;213214// # Used definitions215//216// ## User control levels217//218// chain-controlled - key is uncontrolled by user219//                    i.e autoincrementing index220//                    can use non-cryptographic hash221// real - key is controlled by user222//        but it is hard to generate enough colliding values, i.e owner of signed txs223//        can use non-cryptographic hash224// controlled - key is completly controlled by users225//              i.e maps with mutable keys226//              should use cryptographic hash227//228// ## User control level downgrade reasons229//230// ?1 - chain-controlled -> controlled231//      collections/tokens can be destroyed, resulting in massive holes232// ?2 - chain-controlled -> controlled233//      same as ?1, but can be only added, resulting in easier exploitation234// ?3 - real -> controlled235//      no confirmation required, so addresses can be easily generated236decl_storage! {237	trait Store for Module<T: Config> as Unique {238239		//#region Private members240		/// Used for migrations241		ChainVersion: u64;242		//#endregion243244		//#region Tokens transfer sponosoring rate limit baskets245		/// (Collection id (controlled?2), who created (real))246		/// TODO: Off chain worker should remove from this map when collection gets removed247		pub CreateItemBasket get(fn create_item_basket): map hasher(blake2_128_concat) (CollectionId, T::AccountId) => Option<T::BlockNumber>;248		/// Collection id (controlled?2), token id (controlled?2)249		pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;250		/// Collection id (controlled?2), owning user (real)251		pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;252		/// Collection id (controlled?2), token id (controlled?2)253		pub ReFungibleTransferBasket get(fn refungible_transfer_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;254		//#endregion255256		/// Variable metadata sponsoring257		/// Collection id (controlled?2), token id (controlled?2)258		#[deprecated]259		pub VariableMetaDataBasket get(fn variable_meta_data_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;260		/// Last sponsoring of token property setting // todo:doc rephrase this and the following261		pub TokenPropertyBasket get(fn token_property_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;262263		/// Last sponsoring of NFT approval in a collection264		pub NftApproveBasket get(fn nft_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId => Option<T::BlockNumber>;265		/// Last sponsoring of fungible tokens approval in a collection266		pub FungibleApproveBasket get(fn fungible_approve_basket): double_map hasher(blake2_128_concat) CollectionId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;267		/// Last sponsoring of RFT approval in a collection268		pub RefungibleApproveBasket get(fn refungible_approve_basket): nmap hasher(blake2_128_concat) CollectionId, hasher(blake2_128_concat) TokenId, hasher(twox_64_concat) T::AccountId => Option<T::BlockNumber>;269	}270}271272decl_module! {273	/// Type alias to Pallet, to be used by construct_runtime.274	pub struct Module<T: Config> for enum Call275	where276		origin: T::RuntimeOrigin277	{278		type Error = Error<T>;279280		pub fn deposit_event() = default;281282		fn on_initialize(_now: T::BlockNumber) -> Weight {283			Weight::zero()284		}285286		fn on_runtime_upgrade() -> Weight {287			Weight::zero()288		}289290		/// Create a collection of tokens.291		///292		/// Each Token may have multiple properties encoded as an array of bytes293		/// of certain length. The initial owner of the collection is set294		/// to the address that signed the transaction and can be changed later.295		///296		/// Prefer the more advanced [`create_collection_ex`][`Pallet::create_collection_ex`] instead.297		///298		/// # Permissions299		///300		/// * Anyone - becomes the owner of the new collection.301		///302		/// # Arguments303		///304		/// * `collection_name`: Wide-character string with collection name305		/// (limit [`MAX_COLLECTION_NAME_LENGTH`]).306		/// * `collection_description`: Wide-character string with collection description307		/// (limit [`MAX_COLLECTION_DESCRIPTION_LENGTH`]).308		/// * `token_prefix`: Byte string containing the token prefix to mark a collection309		/// to which a token belongs (limit [`MAX_TOKEN_PREFIX_LENGTH`]).310		/// * `mode`: Type of items stored in the collection and type dependent data.311		// returns collection ID312		#[weight = <SelfWeightOf<T>>::create_collection()]313		#[deprecated(note = "`create_collection_ex` is more up-to-date and advanced, prefer it instead")]314		pub fn create_collection(315			origin,316			collection_name: BoundedVec<u16, ConstU32<MAX_COLLECTION_NAME_LENGTH>>,317			collection_description: BoundedVec<u16, ConstU32<MAX_COLLECTION_DESCRIPTION_LENGTH>>,318			token_prefix: BoundedVec<u8, ConstU32<MAX_TOKEN_PREFIX_LENGTH>>,319			mode: CollectionMode320		) -> DispatchResult {321			let data: CreateCollectionData<T::AccountId> = CreateCollectionData {322				name: collection_name,323				description: collection_description,324				token_prefix,325				mode,326				..Default::default()327			};328			Self::create_collection_ex(origin, data)329		}330331		/// Create a collection with explicit parameters.332		///333		/// Prefer it to the deprecated [`create_collection`][`Pallet::create_collection`] method.334		///335		/// # Permissions336		///337		/// * Anyone - becomes the owner of the new collection.338		///339		/// # Arguments340		///341		/// * `data`: Explicit data of a collection used for its creation.342		#[weight = <SelfWeightOf<T>>::create_collection()]343		pub fn create_collection_ex(origin, data: CreateCollectionData<T::AccountId>) -> DispatchResult {344			let sender = ensure_signed(origin)?;345346			// =========347			let sender = T::CrossAccountId::from_sub(sender);348			let _id = T::CollectionDispatch::create(sender.clone(), sender, data, Default::default())?;349350			Ok(())351		}352353		/// Destroy a collection if no tokens exist within.354		///355		/// # Permissions356		///357		/// * Collection owner358		///359		/// # Arguments360		///361		/// * `collection_id`: Collection to destroy.362		#[weight = <SelfWeightOf<T>>::destroy_collection()]363		pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {364			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);365366			Self::destroy_collection_internal(sender, collection_id)367		}368369		/// Add an address to allow list.370		///371		/// # Permissions372		///373		/// * Collection owner374		/// * Collection admin375		///376		/// # Arguments377		///378		/// * `collection_id`: ID of the modified collection.379		/// * `address`: ID of the address to be added to the allowlist.380		#[weight = <SelfWeightOf<T>>::add_to_allow_list()]381		pub fn add_to_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{382383			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);384			let collection = <CollectionHandle<T>>::try_get(collection_id)?;385			collection.check_is_internal()?;386387			<PalletCommon<T>>::toggle_allowlist(388				&collection,389				&sender,390				&address,391				true,392			)?;393394			Self::deposit_event(Event::<T>::AllowListAddressAdded(395				collection_id,396				address397			));398399			Ok(())400		}401402		/// Remove an address from allow list.403		///404		/// # Permissions405		///406		/// * Collection owner407		/// * Collection admin408		///409		/// # Arguments410		///411		/// * `collection_id`: ID of the modified collection.412		/// * `address`: ID of the address to be removed from the allowlist.413		#[weight = <SelfWeightOf<T>>::remove_from_allow_list()]414		pub fn remove_from_allow_list(origin, collection_id: CollectionId, address: T::CrossAccountId) -> DispatchResult{415416			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);417			let collection = <CollectionHandle<T>>::try_get(collection_id)?;418			collection.check_is_internal()?;419420			<PalletCommon<T>>::toggle_allowlist(421				&collection,422				&sender,423				&address,424				false,425			)?;426427			<Pallet<T>>::deposit_event(Event::<T>::AllowListAddressRemoved(428				collection_id,429				address430			));431432			Ok(())433		}434435		/// Change the owner of the collection.436		///437		/// # Permissions438		///439		/// * Collection owner440		///441		/// # Arguments442		///443		/// * `collection_id`: ID of the modified collection.444		/// * `new_owner`: ID of the account that will become the owner.445		#[weight = <SelfWeightOf<T>>::change_collection_owner()]446		pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {447448			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);449450			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;451			target_collection.check_is_internal()?;452			target_collection.check_is_owner(&sender)?;453454			target_collection.owner = new_owner.clone();455			<Pallet<T>>::deposit_event(Event::<T>::CollectionOwnedChanged(456				collection_id,457				new_owner458			));459460			target_collection.save()461		}462463		/// Add an admin to a collection.464		///465		/// NFT Collection can be controlled by multiple admin addresses466		/// (some which can also be servers, for example). Admins can issue467		/// and burn NFTs, as well as add and remove other admins,468		/// but cannot change NFT or Collection ownership.469		///470		/// # Permissions471		///472		/// * Collection owner473		/// * Collection admin474		///475		/// # Arguments476		///477		/// * `collection_id`: ID of the Collection to add an admin for.478		/// * `new_admin`: Address of new admin to add.479		#[weight = <SelfWeightOf<T>>::add_collection_admin()]480		pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::CrossAccountId) -> DispatchResult {481			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);482			let collection = <CollectionHandle<T>>::try_get(collection_id)?;483			collection.check_is_internal()?;484485			<Pallet<T>>::deposit_event(Event::<T>::CollectionAdminAdded(486				collection_id,487				new_admin_id.clone()488			));489490			<PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)491		}492493		/// Remove admin of a collection.494		///495		/// An admin address can remove itself. List of admins may become empty,496		/// in which case only Collection Owner will be able to add an Admin.497		///498		/// # Permissions499		///500		/// * Collection owner501		/// * Collection admin502		///503		/// # Arguments504		///505		/// * `collection_id`: ID of the collection to remove the admin for.506		/// * `account_id`: Address of the admin to remove.507		#[weight = <SelfWeightOf<T>>::remove_collection_admin()]508		pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::CrossAccountId) -> DispatchResult {509			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);510			let collection = <CollectionHandle<T>>::try_get(collection_id)?;511			collection.check_is_internal()?;512513			<Pallet<T>>::deposit_event(Event::<T>::CollectionAdminRemoved(514				collection_id,515				account_id.clone()516			));517518			<PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)519		}520521		/// Set (invite) a new collection sponsor.522		///523		/// If successful, confirmation from the sponsor-to-be will be pending.524		///525		/// # Permissions526		///527		/// * Collection owner528		/// * Collection admin529		///530		/// # Arguments531		///532		/// * `collection_id`: ID of the modified collection.533		/// * `new_sponsor`: ID of the account of the sponsor-to-be.534		#[weight = <SelfWeightOf<T>>::set_collection_sponsor()]535		pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {536			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);537538			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;539			target_collection.check_is_owner_or_admin(&sender)?;540			target_collection.check_is_internal()?;541542			target_collection.set_sponsor(new_sponsor.clone())?;543544			<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(545				collection_id,546				new_sponsor547			));548549			target_collection.save()550		}551552		/// Confirm own sponsorship of a collection, becoming the sponsor.553		///554		/// An invitation must be pending, see [`set_collection_sponsor`][`Pallet::set_collection_sponsor`].555		/// Sponsor can pay the fees of a transaction instead of the sender,556		/// but only within specified limits.557		///558		/// # Permissions559		///560		/// * Sponsor-to-be561		///562		/// # Arguments563		///564		/// * `collection_id`: ID of the collection with the pending sponsor.565		#[weight = <SelfWeightOf<T>>::confirm_sponsorship()]566		pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {567			let sender = ensure_signed(origin)?;568569			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;570			target_collection.check_is_internal()?;571			ensure!(572				target_collection.confirm_sponsorship(&sender)?,573				Error::<T>::ConfirmUnsetSponsorFail574			);575576			<Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(577				collection_id,578				sender579			));580581			target_collection.save()582		}583584		/// Remove a collection's a sponsor, making everyone pay for their own transactions.585		///586		/// # Permissions587		///588		/// * Collection owner589		///590		/// # Arguments591		///592		/// * `collection_id`: ID of the collection with the sponsor to remove.593		#[weight = <SelfWeightOf<T>>::remove_collection_sponsor()]594		pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {595			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);596597			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;598			target_collection.check_is_internal()?;599			target_collection.check_is_owner(&sender)?;600601			target_collection.sponsorship = SponsorshipState::Disabled;602603			<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(604				collection_id605			));606			target_collection.save()607		}608609		/// Mint an item within a collection.610		///611		/// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].612		///613		/// # Permissions614		///615		/// * Collection owner616		/// * Collection admin617		/// * Anyone if618		///     * Allow List is enabled, and619		///     * Address is added to allow list, and620		///     * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])621		///622		/// # Arguments623		///624		/// * `collection_id`: ID of the collection to which an item would belong.625		/// * `owner`: Address of the initial owner of the item.626		/// * `data`: Token data describing the item to store on chain.627		#[weight = T::CommonWeightInfo::create_item()]628		pub fn create_item(origin, collection_id: CollectionId, owner: T::CrossAccountId, data: CreateItemData) -> DispatchResultWithPostInfo {629			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);630			let budget = budget::Value::new(NESTING_BUDGET);631632			dispatch_tx::<T, _>(collection_id, |d| d.create_item(sender, owner, data, &budget))633		}634635		/// Create multiple items within a collection.636		///637		/// A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].638		///639		/// # Permissions640		///641		/// * Collection owner642		/// * Collection admin643		/// * Anyone if644		///     * Allow List is enabled, and645		///     * Address is added to the allow list, and646		///     * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])647		///648		/// # Arguments649		///650		/// * `collection_id`: ID of the collection to which the tokens would belong.651		/// * `owner`: Address of the initial owner of the tokens.652		/// * `items_data`: Vector of data describing each item to be created.653		#[weight = T::CommonWeightInfo::create_multiple_items(&items_data)]654		pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::CrossAccountId, items_data: Vec<CreateItemData>) -> DispatchResultWithPostInfo {655			ensure!(!items_data.is_empty(), Error::<T>::EmptyArgument);656			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);657			let budget = budget::Value::new(NESTING_BUDGET);658659			dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items(sender, owner, items_data, &budget))660		}661662		/// Add or change collection properties.663		///664		/// # Permissions665		///666		/// * Collection owner667		/// * Collection admin668		///669		/// # Arguments670		///671		/// * `collection_id`: ID of the modified collection.672		/// * `properties`: Vector of key-value pairs stored as the collection's metadata.673		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.674		#[weight = T::CommonWeightInfo::set_collection_properties(properties.len() as u32)]675		pub fn set_collection_properties(676			origin,677			collection_id: CollectionId,678			properties: Vec<Property>679		) -> DispatchResultWithPostInfo {680			ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);681682			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);683684			dispatch_tx::<T, _>(collection_id, |d| d.set_collection_properties(sender, properties))685		}686687		/// Delete specified collection properties.688		///689		/// # Permissions690		///691		/// * Collection Owner692		/// * Collection Admin693		///694		/// # Arguments695		///696		/// * `collection_id`: ID of the modified collection.697		/// * `property_keys`: Vector of keys of the properties to be deleted.698		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.699		#[weight = T::CommonWeightInfo::delete_collection_properties(property_keys.len() as u32)]700		pub fn delete_collection_properties(701			origin,702			collection_id: CollectionId,703			property_keys: Vec<PropertyKey>,704		) -> DispatchResultWithPostInfo {705			ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);706707			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);708709			dispatch_tx::<T, _>(collection_id, |d| d.delete_collection_properties(&sender, property_keys))710		}711712		/// Add or change token properties according to collection's permissions.713		/// Currently properties only work with NFTs.714		///715		/// # Permissions716		///717		/// * Depends on collection's token property permissions and specified property mutability:718		/// 	* Collection owner719		/// 	* Collection admin720		/// 	* Token owner721		///722		/// See [`set_token_property_permissions`][`Pallet::set_token_property_permissions`].723		///724		/// # Arguments725		///726		/// * `collection_id: ID of the collection to which the token belongs.727		/// * `token_id`: ID of the modified token.728		/// * `properties`: Vector of key-value pairs stored as the token's metadata.729		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.730		#[weight = T::CommonWeightInfo::set_token_properties(properties.len() as u32)]731		pub fn set_token_properties(732			origin,733			collection_id: CollectionId,734			token_id: TokenId,735			properties: Vec<Property>736		) -> DispatchResultWithPostInfo {737			ensure!(!properties.is_empty(), Error::<T>::EmptyArgument);738739			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);740			let budget = budget::Value::new(NESTING_BUDGET);741742			dispatch_tx::<T, _>(collection_id, |d| d.set_token_properties(sender, token_id, properties, &budget))743		}744745		/// Delete specified token properties. Currently properties only work with NFTs.746		///747		/// # Permissions748		///749		/// * Depends on collection's token property permissions and specified property mutability:750		/// 	* Collection owner751		/// 	* Collection admin752		/// 	* Token owner753		///754		/// # Arguments755		///756		/// * `collection_id`: ID of the collection to which the token belongs.757		/// * `token_id`: ID of the modified token.758		/// * `property_keys`: Vector of keys of the properties to be deleted.759		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.760		#[weight = T::CommonWeightInfo::delete_token_properties(property_keys.len() as u32)]761		pub fn delete_token_properties(762			origin,763			collection_id: CollectionId,764			token_id: TokenId,765			property_keys: Vec<PropertyKey>766		) -> DispatchResultWithPostInfo {767			ensure!(!property_keys.is_empty(), Error::<T>::EmptyArgument);768769			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);770			let budget = budget::Value::new(NESTING_BUDGET);771772			dispatch_tx::<T, _>(collection_id, |d| d.delete_token_properties(sender, token_id, property_keys, &budget))773		}774775		/// Add or change token property permissions of a collection.776		///777		/// Without a permission for a particular key, a property with that key778		/// cannot be created in a token.779		///780		/// # Permissions781		///782		/// * Collection owner783		/// * Collection admin784		///785		/// # Arguments786		///787		/// * `collection_id`: ID of the modified collection.788		/// * `property_permissions`: Vector of permissions for property keys.789		/// Keys support Latin letters, `-`, `_`, and `.` as symbols.790		#[weight = T::CommonWeightInfo::set_token_property_permissions(property_permissions.len() as u32)]791		pub fn set_token_property_permissions(792			origin,793			collection_id: CollectionId,794			property_permissions: Vec<PropertyKeyPermission>,795		) -> DispatchResultWithPostInfo {796			ensure!(!property_permissions.is_empty(), Error::<T>::EmptyArgument);797798			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);799800			dispatch_tx::<T, _>(collection_id, |d| d.set_token_property_permissions(&sender, property_permissions))801		}802803		/// Create multiple items within a collection with explicitly specified initial parameters.804		///805		/// # Permissions806		///807		/// * Collection owner808		/// * Collection admin809		/// * Anyone if810		///     * Allow List is enabled, and811		///     * Address is added to allow list, and812		///     * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])813		///814		/// # Arguments815		///816		/// * `collection_id`: ID of the collection to which the tokens would belong.817		/// * `data`: Explicit item creation data.818		#[weight = T::CommonWeightInfo::create_multiple_items_ex(&data)]819		pub fn create_multiple_items_ex(origin, collection_id: CollectionId, data: CreateItemExData<T::CrossAccountId>) -> DispatchResultWithPostInfo {820			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);821			let budget = budget::Value::new(NESTING_BUDGET);822823			dispatch_tx::<T, _>(collection_id, |d| d.create_multiple_items_ex(sender, data, &budget))824		}825826		/// Completely allow or disallow transfers for a particular collection.827		///828		/// # Permissions829		///830		/// * Collection owner831		///832		/// # Arguments833		///834		/// * `collection_id`: ID of the collection.835		/// * `value`: New value of the flag, are transfers allowed?836		#[weight = <SelfWeightOf<T>>::set_transfers_enabled_flag()]837		pub fn set_transfers_enabled_flag(origin, collection_id: CollectionId, value: bool) -> DispatchResult {838			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);839			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;840			target_collection.check_is_internal()?;841			target_collection.check_is_owner(&sender)?;842843			// =========844845			target_collection.limits.transfers_enabled = Some(value);846			target_collection.save()847		}848849		/// Destroy an item.850		///851		/// # Permissions852		///853		/// * Collection owner854		/// * Collection admin855		/// * Current item owner856		///857		/// # Arguments858		///859		/// * `collection_id`: ID of the collection to which the item belongs.860		/// * `item_id`: ID of item to burn.861		/// * `value`: Number of pieces of the item to destroy.862		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.863		///     * Fungible Mode: The desired number of pieces to burn.864		///     * Re-Fungible Mode: The desired number of pieces to burn.865		#[weight = T::CommonWeightInfo::burn_item()]866		pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {867			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);868869			let post_info = dispatch_tx::<T, _>(collection_id, |d| d.burn_item(sender, item_id, value))?;870			if value == 1 {871				<NftTransferBasket<T>>::remove(collection_id, item_id);872				<NftApproveBasket<T>>::remove(collection_id, item_id);873			}874			// Those maps should be cleared only if token disappears completly, need to move this part of logic to pallets?875			// <FungibleApproveBasket<T>>::remove(collection_id, sender.as_sub());876			// <RefungibleApproveBasket<T>>::remove((collection_id, item_id, sender.as_sub()));877			Ok(post_info)878		}879880		/// Destroy a token on behalf of the owner as a non-owner account.881		///882		/// See also: [`approve`][`Pallet::approve`].883		///884		/// After this method executes, one approval is removed from the total so that885		/// the approved address will not be able to transfer this item again from this owner.886		///887		/// # Permissions888		///889		/// * Collection owner890		/// * Collection admin891		/// * Current token owner892		/// * Address approved by current item owner893		///894		/// # Arguments895		///896		/// * `from`: The owner of the burning item.897		/// * `collection_id`: ID of the collection to which the item belongs.898		/// * `item_id`: ID of item to burn.899		/// * `value`: Number of pieces to burn.900		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.901		///     * Fungible Mode: The desired number of pieces to burn.902		///     * Re-Fungible Mode: The desired number of pieces to burn.903		#[weight = T::CommonWeightInfo::burn_from()]904		pub fn burn_from(origin, collection_id: CollectionId, from: T::CrossAccountId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {905			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);906			let budget = budget::Value::new(NESTING_BUDGET);907908			dispatch_tx::<T, _>(collection_id, |d| d.burn_from(sender, from, item_id, value, &budget))909		}910911		/// Change ownership of the token.912		///913		/// # Permissions914		///915		/// * Collection owner916		/// * Collection admin917		/// * Current token owner918		///919		/// # Arguments920		///921		/// * `recipient`: Address of token recipient.922		/// * `collection_id`: ID of the collection the item belongs to.923		/// * `item_id`: ID of the item.924		///     * Non-Fungible Mode: Required.925		///     * Fungible Mode: Ignored.926		///     * Re-Fungible Mode: Required.927		///928		/// * `value`: Amount to transfer.929		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.930		///     * Fungible Mode: The desired number of pieces to transfer.931		///     * Re-Fungible Mode: The desired number of pieces to transfer.932		#[weight = T::CommonWeightInfo::transfer()]933		pub fn transfer(origin, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResultWithPostInfo {934			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);935			let budget = budget::Value::new(NESTING_BUDGET);936937			dispatch_tx::<T, _>(collection_id, |d| d.transfer(sender, recipient, item_id, value, &budget))938		}939940		/// Allow a non-permissioned address to transfer or burn an item.941		///942		/// # Permissions943		///944		/// * Collection owner945		/// * Collection admin946		/// * Current item owner947		///948		/// # Arguments949		///950		/// * `spender`: Account to be approved to make specific transactions on non-owned tokens.951		/// * `collection_id`: ID of the collection the item belongs to.952		/// * `item_id`: ID of the item transactions on which are now approved.953		/// * `amount`: Number of pieces of the item approved for a transaction (maximum of 1 for NFTs).954		/// Set to 0 to revoke the approval.955		#[weight = T::CommonWeightInfo::approve()]956		pub fn approve(origin, spender: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResultWithPostInfo {957			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);958959			dispatch_tx::<T, _>(collection_id, |d| d.approve(sender, spender, item_id, amount))960		}961962		/// Change ownership of an item on behalf of the owner as a non-owner account.963		///964		/// See the [`approve`][`Pallet::approve`] method for additional information.965		///966		/// After this method executes, one approval is removed from the total so that967		/// the approved address will not be able to transfer this item again from this owner.968		///969		/// # Permissions970		///971		/// * Collection owner972		/// * Collection admin973		/// * Current item owner974		/// * Address approved by current item owner975		///976		/// # Arguments977		///978		/// * `from`: Address that currently owns the token.979		/// * `recipient`: Address of the new token-owner-to-be.980		/// * `collection_id`: ID of the collection the item.981		/// * `item_id`: ID of the item to be transferred.982		/// * `value`: Amount to transfer.983		/// 	* Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.984		///     * Fungible Mode: The desired number of pieces to transfer.985		///     * Re-Fungible Mode: The desired number of pieces to transfer.986		#[weight = T::CommonWeightInfo::transfer_from()]987		pub fn transfer_from(origin, from: T::CrossAccountId, recipient: T::CrossAccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResultWithPostInfo {988			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);989			let budget = budget::Value::new(NESTING_BUDGET);990991			dispatch_tx::<T, _>(collection_id, |d| d.transfer_from(sender, from, recipient, item_id, value, &budget))992		}993994		/// Set specific limits of a collection. Empty, or None fields mean chain default.995		///996		/// # Permissions997		///998		/// * Collection owner999		/// * Collection admin1000		///1001		/// # Arguments1002		///1003		/// * `collection_id`: ID of the modified collection.1004		/// * `new_limit`: New limits of the collection. Fields that are not set (None)1005		/// will not overwrite the old ones.1006		#[weight = <SelfWeightOf<T>>::set_collection_limits()]1007		pub fn set_collection_limits(1008			origin,1009			collection_id: CollectionId,1010			new_limit: CollectionLimits,1011		) -> DispatchResult {1012			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1013			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1014			target_collection.check_is_internal()?;1015			target_collection.check_is_owner_or_admin(&sender)?;1016			let old_limit = &target_collection.limits;10171018			target_collection.limits = <PalletCommon<T>>::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?;10191020			<Pallet<T>>::deposit_event(Event::<T>::CollectionLimitSet(1021				collection_id1022			));10231024			target_collection.save()1025		}10261027		/// Set specific permissions of a collection. Empty, or None fields mean chain default.1028		///1029		/// # Permissions1030		///1031		/// * Collection owner1032		/// * Collection admin1033		///1034		/// # Arguments1035		///1036		/// * `collection_id`: ID of the modified collection.1037		/// * `new_permission`: New permissions of the collection. Fields that are not set (None)1038		/// will not overwrite the old ones.1039		#[weight = <SelfWeightOf<T>>::set_collection_limits()]1040		pub fn set_collection_permissions(1041			origin,1042			collection_id: CollectionId,1043			new_permission: CollectionPermissions,1044		) -> DispatchResult {1045			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1046			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1047			target_collection.check_is_internal()?;1048			target_collection.check_is_owner_or_admin(&sender)?;1049			let old_limit = &target_collection.permissions;10501051			target_collection.permissions = <PalletCommon<T>>::clamp_permissions(target_collection.mode.clone(), &old_limit, new_permission)?;10521053			<Pallet<T>>::deposit_event(Event::<T>::CollectionPermissionSet(1054				collection_id1055			));10561057			target_collection.save()1058		}10591060		/// Re-partition a refungible token, while owning all of its parts/pieces.1061		///1062		/// # Permissions1063		///1064		/// * Token owner (must own every part)1065		///1066		/// # Arguments1067		///1068		/// * `collection_id`: ID of the collection the RFT belongs to.1069		/// * `token_id`: ID of the RFT.1070		/// * `amount`: New number of parts/pieces into which the token shall be partitioned.1071		#[weight = T::RefungibleExtensionsWeightInfo::repartition()]1072		pub fn repartition(1073			origin,1074			collection_id: CollectionId,1075			token_id: TokenId,1076			amount: u128,1077		) -> DispatchResultWithPostInfo {1078			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);1079			dispatch_tx::<T, _>(collection_id, |d| {1080				if let Some(refungible_extensions) = d.refungible_extensions() {1081					refungible_extensions.repartition(&sender, token_id, amount)1082				} else {1083					fail!(<Error<T>>::RepartitionCalledOnNonRefungibleCollection)1084				}1085			})1086		}1087	}1088}10891090impl<T: Config> Pallet<T> {1091	/// Force set `sponsor` for `collection`.1092	///1093	/// Differs from [`set_collection_sponsor`][`Pallet::set_collection_sponsor`] in that confirmation1094	/// from the `sponsor` is not required.1095	///1096	/// # Arguments1097	///1098	/// * `sponsor`: ID of the account of the sponsor-to-be.1099	/// * `collection_id`: ID of the modified collection.1100	pub fn force_set_sponsor(sponsor: T::AccountId, collection_id: CollectionId) -> DispatchResult {1101		let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1102		target_collection.check_is_internal()?;1103		target_collection.set_sponsor(sponsor.clone())?;11041105		Self::deposit_event(Event::<T>::CollectionSponsorSet(1106			collection_id,1107			sponsor.clone(),1108		));11091110		ensure!(1111			target_collection.confirm_sponsorship(&sponsor)?,1112			Error::<T>::ConfirmUnsetSponsorFail1113		);11141115		Self::deposit_event(Event::<T>::SponsorshipConfirmed(collection_id, sponsor));11161117		target_collection.save()1118	}11191120	/// Force remove `sponsor` for `collection`.1121	///1122	/// Differs from `remove_sponsor` in that1123	/// it doesn't require consent from the `owner` of the collection.1124	///1125	/// # Arguments1126	///1127	/// * `collection_id`: ID of the modified collection.1128	pub fn force_remove_collection_sponsor(collection_id: CollectionId) -> DispatchResult {1129		let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;1130		target_collection.check_is_internal()?;1131		target_collection.sponsorship = SponsorshipState::Disabled;11321133		Self::deposit_event(Event::<T>::CollectionSponsorRemoved(collection_id));11341135		target_collection.save()1136	}11371138	#[inline(always)]1139	pub(crate) fn destroy_collection_internal(1140		sender: T::CrossAccountId,1141		collection_id: CollectionId,1142	) -> DispatchResult {1143		let collection = <CollectionHandle<T>>::try_get(collection_id)?;1144		collection.check_is_internal()?;11451146		T::CollectionDispatch::destroy(sender, collection)?;11471148		// TODO: basket cleanup should be moved elsewhere1149		// Maybe runtime dispatch.rs should perform it?11501151		let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1152		let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1153		let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);11541155		let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1156		let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);1157		let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);11581159		Ok(())1160	}1161}
modifiedtests/.vscode/settings.jsondiffbeforeafterboth
--- a/tests/.vscode/settings.json
+++ b/tests/.vscode/settings.json
@@ -1,5 +1,12 @@
 {
-    "mocha.enabled": true,
-    "mochaExplorer.files": "**/*.test.ts",
-    "mochaExplorer.require": "ts-node/register"
+	"mocha.enabled": true,
+	"mochaExplorer.files": "**/*.test.ts",
+	"mochaExplorer.require": "ts-node/register",
+	"eslint.format.enable": true,
+	"[javascript]": {
+		"editor.defaultFormatter": "dbaeumer.vscode-eslint"
+	},
+	"[typescript]": {
+		"editor.defaultFormatter": "dbaeumer.vscode-eslint"
+	}
 }
modifiedtests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth
--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -15,6 +15,7 @@
 /// @dev inlined interface
 interface CollectionHelpersEvents {
 	event CollectionCreated(address indexed owner, address indexed collectionId);
+	event CollectionDestroyed(address indexed collectionId);
 }
 
 /// @title Contract, which allows users to operate with collections
modifiedtests/src/eth/collectionHelpersAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/collectionHelpersAbi.json
+++ b/tests/src/eth/collectionHelpersAbi.json
@@ -19,6 +19,19 @@
     "type": "event"
   },
   {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "collectionId",
+        "type": "address"
+      }
+    ],
+    "name": "CollectionDestroyed",
+    "type": "event"
+  },
+  {
     "inputs": [],
     "name": "collectionCreationFee",
     "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
modifiedtests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth
--- a/tests/src/eth/createNFTCollection.test.ts
+++ b/tests/src/eth/createNFTCollection.test.ts
@@ -22,7 +22,7 @@
 describe('Create NFT collection from EVM', () => {
   let donor: IKeyringPair;
 
-  before(async function() {
+  before(async function () {
     await usingEthPlaygrounds(async (_helper, privateKey) => {
       donor = await privateKey({filename: __filename});
     });
@@ -35,10 +35,28 @@
     const description = 'Some description';
     const prefix = 'token prefix';
 
-    const {collectionId} = await helper.eth.createNFTCollection(owner, name, description, prefix);
-    const data = (await helper.rft.getData(collectionId))!;
+    // todo:playgrounds this might fail when in async environment.
+    const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
+    const {collectionId, collectionAddress, events} = await helper.eth.createNFTCollection(owner, name, description, prefix);
+    
+    expect(events).to.be.deep.equal([
+      {
+        address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',
+        event: 'CollectionCreated',
+        args: {
+          owner: owner,
+          collectionId: collectionAddress,
+        },
+      },
+    ]);
+    
+    const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
+
     const collection = helper.nft.getCollectionObject(collectionId);
-    
+    const data = (await collection.getData())!;
+
+    expect(collectionCountAfter - collectionCountBefore).to.be.eq(1);
+    expect(collectionId).to.be.eq(collectionCountAfter);
     expect(data.name).to.be.eq(name);
     expect(data.description).to.be.eq(description);
     expect(data.raw.tokenPrefix).to.be.eq(prefix);
@@ -57,8 +75,19 @@
     const prefix = 'token prefix';
     const baseUri = 'BaseURI';
 
-    const {collectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);
+    const {collectionId, collectionAddress, events} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);
 
+    expect(events).to.be.deep.equal([
+      {
+        address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',
+        event: 'CollectionCreated',
+        args: {
+          owner: owner,
+          collectionId: collectionAddress,
+        },
+      },
+    ]);
+
     const collection = helper.nft.getCollectionObject(collectionId);
     const data = (await collection.getData())!;
     
@@ -95,12 +124,12 @@
     await collectionHelpers.methods
       .createNFTCollection('A', 'A', 'A')
       .send({value: Number(2n * helper.balance.getOneTokenNominal())});
-    
+
     expect(await collectionHelpers.methods
       .isCollectionExist(expectedCollectionAddress)
       .call()).to.be.true;
   });
-  
+
   itEth('Set sponsorship', async ({helper}) => {
     const owner = await helper.eth.createAccountWithBalance(donor);
     const sponsor = await helper.eth.createAccountWithBalance(donor);
@@ -147,7 +176,7 @@
     await collection.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send();
     await collection.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send();
     await collection.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send();
-    
+
     const data = (await helper.nft.getData(collectionId))!;
     expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit);
     expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize);
@@ -166,7 +195,7 @@
     expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection)
       .methods.isCollectionExist(collectionAddressForNonexistentCollection).call())
       .to.be.false;
-    
+
     const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC');
     expect(await helper.ethNativeContract.collectionHelpers(collectionAddress)
       .methods.isCollectionExist(collectionAddress).call())
@@ -178,7 +207,7 @@
   let donor: IKeyringPair;
   let nominal: bigint;
 
-  before(async function() {
+  before(async function () {
     await usingEthPlaygrounds(async (helper, privateKey) => {
       donor = await privateKey({filename: __filename});
       nominal = helper.balance.getOneTokenNominal();
@@ -197,7 +226,7 @@
       await expect(collectionHelper.methods
         .createNFTCollection(collectionName, description, tokenPrefix)
         .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);
-      
+
     }
     {
       const MAX_DESCRIPTION_LENGTH = 256;
@@ -218,7 +247,7 @@
         .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);
     }
   });
-  
+
   itEth('(!negative test!) Create collection (no funds)', async ({helper}) => {
     const owner = await helper.eth.createAccountWithBalance(donor);
     const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
@@ -238,7 +267,7 @@
       await expect(malfeasantCollection.methods
         .setCollectionSponsor(sponsor)
         .call()).to.be.rejectedWith(EXPECTED_ERROR);
-      
+
       const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);
       await expect(sponsorCollection.methods
         .confirmCollectionSponsorship()
@@ -259,4 +288,31 @@
       .setCollectionLimit('badLimit', 'true')
       .call()).to.be.rejectedWith('unknown boolean limit "badLimit"');
   });
-});
+
+  itEth('destroyCollection', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF');
+    const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+
+
+    const result = await collectionHelper.methods
+      .destroyCollection(collectionAddress)
+      .send({from: owner});
+
+    const events = helper.eth.normalizeEvents(result.events);
+    
+    expect(events).to.be.deep.equal([
+      {
+        address: collectionHelper.options.address,
+        event: 'CollectionDestroyed',
+        args: {
+          collectionId: collectionAddress,
+        },
+      },
+    ]);
+
+    expect(await collectionHelper.methods
+      .isCollectionExist(collectionAddress)
+      .call()).to.be.false;
+  });
+});
\ No newline at end of file
modifiedtests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -173,49 +173,46 @@
   async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {
     return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);
   }
-
-  async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
+  
+  async createCollecion(functionName: string, signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
     const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
     const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
-
-    const result = await collectionHelper.methods.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
+        
+    const result = await collectionHelper.methods[functionName](name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
 
     const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
     const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
-
-    return {collectionId, collectionAddress};
+    const events = this.helper.eth.normalizeEvents(result.events);
+    
+    return {collectionId, collectionAddress, events};
+  }
+  
+  async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
+    return this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);
   }
 
-  async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+  async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
     const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
 
-    const {collectionId, collectionAddress} = await this.createNFTCollection(signer, name, description, tokenPrefix);
+    const {collectionId, collectionAddress, events} = await this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);
 
     await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();
 
-    return {collectionId, collectionAddress};
+    return {collectionId, collectionAddress, events};
   }
 
   async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
-    const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
-    const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
-
-    const result = await collectionHelper.methods.createRFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
-
-    const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
-    const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
-
-    return {collectionId, collectionAddress};
+    return this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);
   }
 
-  async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+  async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
     const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
 
-    const {collectionId, collectionAddress} = await this.createRFTCollection(signer, name, description, tokenPrefix);
+    const {collectionId, collectionAddress, events} = await this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);
 
     await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();
 
-    return {collectionId, collectionAddress};
+    return {collectionId, collectionAddress, events};
   }
 
   async deployCollectorContract(signer: string): Promise<Contract> {