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

difftreelog

Merge branch 'develop' into doc/architectural-changes

Farhad Hakimov2022-07-22parents: #5b3cd06 #26e74f7.patch.diff
in: master

80 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5731,7 +5731,7 @@
 
 [[package]]
 name = "pallet-common"
-version = "0.1.0"
+version = "0.1.2"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -5953,7 +5953,7 @@
 
 [[package]]
 name = "pallet-fungible"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -6198,7 +6198,7 @@
 
 [[package]]
 name = "pallet-nonfungible"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -6320,13 +6320,16 @@
 
 [[package]]
 name = "pallet-refungible"
-version = "0.1.0"
+version = "0.1.2"
 dependencies = [
+ "ethereum",
+ "evm-coder",
  "frame-benchmarking",
  "frame-support",
  "frame-system",
  "pallet-common",
  "pallet-evm",
+ "pallet-evm-coder-substrate",
  "pallet-structure",
  "parity-scale-codec 3.1.5",
  "scale-info",
@@ -12374,7 +12377,7 @@
 
 [[package]]
 name = "uc-rpc"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "anyhow",
  "jsonrpsee",
@@ -12733,7 +12736,7 @@
 
 [[package]]
 name = "up-data-structs"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "derivative",
  "frame-support",
@@ -12751,7 +12754,7 @@
 
 [[package]]
 name = "up-rpc"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "pallet-common",
  "pallet-evm",
modifiedMakefilediffbeforeafterboth
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,10 @@
 NONFUNGIBLE_EVM_STUBS=./pallets/nonfungible/src/stubs
 NONFUNGIBLE_EVM_ABI=./tests/src/eth/nonFungibleAbi.json
 
+REFUNGIBLE_EVM_STUBS=./pallets/refungible/src/stubs
+RENFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json
+RENFUNGIBLE_TOKEN_EVM_ABI=./tests/src/eth/reFungibleTokenAbi.json
+
 CONTRACT_HELPERS_STUBS=./pallets/evm-contract-helpers/src/stubs/
 CONTRACT_HELPERS_ABI=./tests/src/eth/util/contractHelpersAbi.json
 
@@ -21,7 +25,7 @@
 TESTS_API=./tests/src/eth/api/
 
 .PHONY: regenerate_solidity
-regenerate_solidity: UniqueFungible.sol UniqueNFT.sol ContractHelpers.sol CollectionHelpers.sol
+regenerate_solidity: UniqueFungible.sol UniqueNFT.sol UniqueRefungibleToken.sol ContractHelpers.sol CollectionHelpers.sol
 
 UniqueFungible.sol:
 	PACKAGE=pallet-fungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -30,6 +34,10 @@
 UniqueNFT.sol:
 	PACKAGE=pallet-nonfungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
 	PACKAGE=pallet-nonfungible NAME=erc::gen_impl OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
+	
+UniqueRefungibleToken.sol:
+	PACKAGE=pallet-refungible NAME=erc_token::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
+	PACKAGE=pallet-refungible NAME=erc_token::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
 
 ContractHelpers.sol:
 	PACKAGE=pallet-evm-contract-helpers NAME=eth::contract_helpers_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -47,6 +55,10 @@
 	INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/UniqueNFT.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
 
+UniqueRefungibleToken: UniqueRefungibleToken.sol
+	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungibleToken.raw ./.maintain/scripts/compile_stub.sh
+	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(RENFUNGIBLE_TOKEN_EVM_ABI) ./.maintain/scripts/generate_abi.sh
+
 ContractHelpers: ContractHelpers.sol
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_STUBS)/ContractHelpers.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_ABI) ./.maintain/scripts/generate_abi.sh
@@ -55,7 +67,7 @@
 	INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_STUBS)/CollectionHelpers.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_ABI) ./.maintain/scripts/generate_abi.sh
 
-evm_stubs: UniqueFungible UniqueNFT ContractHelpers CollectionHelpers
+evm_stubs: UniqueFungible UniqueNFT UniqueRefungibleToken ContractHelpers CollectionHelpers
 
 .PHONY: _bench
 _bench:
addedclient/rpc/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/client/rpc/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [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. 
+ 
\ No newline at end of file
modifiedclient/rpc/Cargo.tomldiffbeforeafterboth
--- a/client/rpc/Cargo.toml
+++ b/client/rpc/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "uc-rpc"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedclient/rpc/src/lib.rsdiffbeforeafterboth
--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -77,6 +77,15 @@
 		at: Option<BlockHash>,
 	) -> Result<Option<CrossAccountId>>;
 
+	/// Returns 10 tokens owners in no particular order.
+	#[method(name = "unique_tokenOwners")]
+	fn token_owners(
+		&self,
+		collection: CollectionId,
+		token: TokenId,
+		at: Option<BlockHash>,
+	) -> Result<Vec<CrossAccountId>>;
+
 	/// Get the topmost token owner in the hierarchy of a possibly nested token.
 	#[method(name = "unique_topmostTokenOwner")]
 	fn topmost_token_owner(
@@ -510,6 +519,7 @@
 	pass_method!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option<u64>, unique_api);
 	pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option<CollectionLimits>, unique_api);
 	pass_method!(total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128>, unique_api);
+	pass_method!(token_owners(collection: CollectionId, token: TokenId) -> Vec<CrossAccountId>, unique_api);
 }
 
 #[allow(deprecated)]
addedpallets/common/CHANGELOG.MDdiffbeforeafterboth
--- /dev/null
+++ b/pallets/common/CHANGELOG.MD
@@ -0,0 +1,10 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.2] - 2022-07-20
+
+### Fixed
+
+-   Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid
+    mutability modifiers, causing invalid stub/abi generation.
addedpallets/common/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/common/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [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. 
+
+ 
\ No newline at end of file
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.0"
+version = "0.1.2"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/common/src/dispatch.rsdiffbeforeafterboth
--- a/pallets/common/src/dispatch.rs
+++ b/pallets/common/src/dispatch.rs
@@ -1,3 +1,5 @@
+//! Module with interfaces for dispatching collections.
+
 use frame_support::{
 	dispatch::{
 		DispatchResultWithPostInfo, PostDispatchInfo, Weight, DispatchErrorWithPostInfo,
@@ -20,7 +22,10 @@
 	// submit_logs is measured as part of collection pallets
 }
 
-/// Helper function to implement substrate calls for common collection methods
+/// Helper function to implement substrate calls for common collection methods.
+///
+/// * `collection` - The collection on which to call the method.
+/// * `call` - The function in which to call the corresponding method from [`CommonCollectionOperations`].
 pub fn dispatch_tx<
 	T: Config,
 	C: FnOnce(&dyn CommonCollectionOperations<T>) -> DispatchResultWithPostInfo,
@@ -64,15 +69,31 @@
 	result
 }
 
+/// Interface for working with different collections through the dispatcher.
 pub trait CollectionDispatch<T: Config> {
+	/// Create a collection. The collection will be created according to the value of [`data.mode`](CreateCollectionData::mode).
+	///
+	/// * `sender` - The user who will become the owner of the collection.
+	/// * `data` - Description of the created collection.
 	fn create(
 		sender: T::CrossAccountId,
 		data: CreateCollectionData<T::AccountId>,
 	) -> DispatchResult;
+
+	/// Delete the collection.
+	///
+	/// * `sender` - The owner of the collection.
+	/// * `handle` - Collection handle.
 	fn destroy(sender: T::CrossAccountId, handle: CollectionHandle<T>) -> DispatchResult;
 
+	/// Get a specialized collection from the handle.
+	///
+	/// * `handle` - Collection handle.
 	fn dispatch(handle: CollectionHandle<T>) -> Self;
+
+	/// Get the collection handle for the corresponding implementation.
 	fn into_inner(self) -> CollectionHandle<T>;
 
+	/// Get the implementation of [`CommonCollectionOperations`].
 	fn as_dyn(&self) -> &dyn CommonCollectionOperations<T>;
 }
modifiedpallets/common/src/erc.rsdiffbeforeafterboth
--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -14,6 +14,8 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! This module contains the implementation of pallet methods for evm.
+
 use evm_coder::{
 	solidity_interface, solidity, ToLog,
 	types::*,
@@ -29,29 +31,40 @@
 
 use crate::{Pallet, CollectionHandle, Config, CollectionProperties};
 
+/// Events for ethereum collection helper.
 #[derive(ToLog)]
 pub enum CollectionHelpersEvents {
+	/// The collection has been created.
 	CollectionCreated {
+		/// Collection owner.
 		#[indexed]
 		owner: address,
+
+		/// Collection ID.
 		#[indexed]
 		collection_id: address,
 	},
 }
 
 /// Does not always represent a full collection, for RFT it is either
-/// collection (Implementing ERC721), or specific collection token (Implementing ERC20)
+/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).
 pub trait CommonEvmHandler {
 	const CODE: &'static [u8];
 
+	/// Call precompiled handle.
 	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;
 }
 
+/// @title A contract that allows you to work with collections.
 #[solidity_interface(name = "Collection")]
 impl<T: Config> CollectionHandle<T>
 where
 	T::AccountId: From<[u8; 32]>,
 {
+	/// Set collection property.
+	///
+	/// @param key Property key.
+	/// @param value Propery value.
 	fn set_collection_property(
 		&mut self,
 		caller: caller,
@@ -68,6 +81,9 @@
 			.map_err(dispatch_to_evm::<T>)
 	}
 
+	/// Delete collection property.
+	///
+	/// @param key Property key.
 	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		let key = <Vec<u8>>::from(key)
@@ -77,7 +93,12 @@
 		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)
 	}
 
-	/// Throws error if key not found
+	/// Get collection property.
+	///
+	/// @dev Throws error if key not found.
+	///
+	/// @param key Property key.
+	/// @return bytes The property corresponding to the key.
 	fn collection_property(&self, key: string) -> Result<bytes> {
 		let key = <Vec<u8>>::from(key)
 			.try_into()
@@ -89,6 +110,11 @@
 		Ok(prop.to_vec())
 	}
 
+	/// Set the sponsor of the collection.
+	///
+	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
+	///
+	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.
 	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {
 		check_is_owner_or_admin(caller, self)?;
 
@@ -98,6 +124,9 @@
 		save(self)
 	}
 
+	/// Collection sponsorship confirmation.
+	///
+	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.
 	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		if !self
@@ -109,6 +138,16 @@
 		save(self)
 	}
 
+	/// Set limits for the collection.
+	/// @dev Throws error if limit not found.
+	/// @param limit Name of the limit. Valid names:
+	/// 	"accountTokenOwnershipLimit",
+	/// 	"sponsoredDataSize",
+	/// 	"sponsoredDataRateLimit",
+	/// 	"tokenLimit",
+	/// 	"sponsorTransferTimeout",
+	/// 	"sponsorApproveTimeout"
+	/// @param value Value of the limit.
 	#[solidity(rename_selector = "setCollectionLimit")]
 	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {
 		check_is_owner_or_admin(caller, self)?;
@@ -145,6 +184,13 @@
 		save(self)
 	}
 
+	/// Set limits for the collection.
+	/// @dev Throws error if limit not found.
+	/// @param limit Name of the limit. Valid names:
+	/// 	"ownerCanTransfer",
+	/// 	"ownerCanDestroy",
+	/// 	"transfersEnabled"
+	/// @param value Value of the limit.
 	#[solidity(rename_selector = "setCollectionLimit")]
 	fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {
 		check_is_owner_or_admin(caller, self)?;
@@ -172,11 +218,18 @@
 		save(self)
 	}
 
+	/// Get contract address.
 	fn contract_address(&self, _caller: caller) -> Result<address> {
 		Ok(crate::eth::collection_id_to_address(self.id))
 	}
 
-	fn add_collection_admin_substrate(&self, caller: caller, new_admin: uint256) -> Result<void> {
+	/// Add collection admin by substrate address.
+	/// @param new_admin Substrate administrator address.
+	fn add_collection_admin_substrate(
+		&mut self,
+		caller: caller,
+		new_admin: uint256,
+	) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		let mut new_admin_arr: [u8; 32] = Default::default();
 		new_admin.to_big_endian(&mut new_admin_arr);
@@ -186,35 +239,44 @@
 		Ok(())
 	}
 
+	/// Remove collection admin by substrate address.
+	/// @param admin Substrate administrator address.
 	fn remove_collection_admin_substrate(
-		&self,
+		&mut self,
 		caller: caller,
-		new_admin: uint256,
+		admin: uint256,
 	) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
-		let mut new_admin_arr: [u8; 32] = Default::default();
-		new_admin.to_big_endian(&mut new_admin_arr);
-		let account_id = T::AccountId::from(new_admin_arr);
-		let new_admin = T::CrossAccountId::from_sub(account_id);
-		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, false)
-			.map_err(dispatch_to_evm::<T>)?;
+		let mut admin_arr: [u8; 32] = Default::default();
+		admin.to_big_endian(&mut admin_arr);
+		let account_id = T::AccountId::from(admin_arr);
+		let admin = T::CrossAccountId::from_sub(account_id);
+		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;
 		Ok(())
 	}
 
-	fn add_collection_admin(&self, caller: caller, new_admin: address) -> Result<void> {
+	/// Add collection admin.
+	/// @param new_admin Address of the added administrator.
+	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		let new_admin = T::CrossAccountId::from_eth(new_admin);
 		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;
 		Ok(())
 	}
 
-	fn remove_collection_admin(&self, caller: caller, admin: address) -> Result<void> {
+	/// Remove collection admin.
+	///
+	/// @param new_admin Address of the removed administrator.
+	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		let admin = T::CrossAccountId::from_eth(admin);
 		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;
 		Ok(())
 	}
 
+	/// Toggle accessibility of collection nesting.
+	///
+	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'
 	#[solidity(rename_selector = "setCollectionNesting")]
 	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {
 		check_is_owner_or_admin(caller, self)?;
@@ -235,6 +297,10 @@
 		save(self)
 	}
 
+	/// Toggle accessibility of collection nesting.
+	///
+	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'
+	/// @param collections Addresses of collections that will be available for nesting.
 	#[solidity(rename_selector = "setCollectionNesting")]
 	fn set_nesting(
 		&mut self,
@@ -280,6 +346,10 @@
 		save(self)
 	}
 
+	/// Set the collection access method.
+	/// @param mode Access mode
+	/// 	0 for Normal
+	/// 	1 for AllowList
 	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {
 		check_is_owner_or_admin(caller, self)?;
 		let permissions = CollectionPermissions {
@@ -300,20 +370,29 @@
 		save(self)
 	}
 
-	fn add_to_collection_allow_list(&self, caller: caller, user: address) -> Result<void> {
+	/// Add the user to the allowed list.
+	///
+	/// @param user Address of a trusted user.
+	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		let user = T::CrossAccountId::from_eth(user);
 		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;
 		Ok(())
 	}
 
-	fn remove_from_collection_allow_list(&self, caller: caller, user: address) -> Result<void> {
+	/// Remove the user from the allowed list.
+	///
+	/// @param user Address of a removed user.
+	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		let user = T::CrossAccountId::from_eth(user);
 		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;
 		Ok(())
 	}
 
+	/// Switch permission for minting.
+	///
+	/// @param mode Enable if "true".
 	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {
 		check_is_owner_or_admin(caller, self)?;
 		let permissions = CollectionPermissions {
@@ -351,6 +430,7 @@
 	Ok(())
 }
 
+/// Get the "tokenURI" key as [PropertyKey](up_data_structs::PropertyKey).
 pub fn token_uri_key() -> up_data_structs::PropertyKey {
 	b"tokenURI"
 		.to_vec()
modifiedpallets/common/src/eth.rsdiffbeforeafterboth
--- a/pallets/common/src/eth.rs
+++ b/pallets/common/src/eth.rs
@@ -14,6 +14,8 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! The module contains a number of functions for converting and checking ethereum identifiers.
+
 use up_data_structs::CollectionId;
 use sp_core::H160;
 
@@ -23,6 +25,7 @@
 	0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,
 ];
 
+/// Maps the ethereum address of the collection in substrate.
 pub fn map_eth_to_id(eth: &H160) -> Option<CollectionId> {
 	if eth[0..16] != ETH_COLLECTION_PREFIX {
 		return None;
@@ -31,6 +34,8 @@
 	id_bytes.copy_from_slice(&eth[16..20]);
 	Some(CollectionId(u32::from_be_bytes(id_bytes)))
 }
+
+/// Maps the substrate collection id in ethereum.
 pub fn collection_id_to_address(id: CollectionId) -> H160 {
 	let mut out = [0; 20];
 	out[0..16].copy_from_slice(&ETH_COLLECTION_PREFIX);
@@ -38,6 +43,7 @@
 	H160(out)
 }
 
+/// Check if the ethereum address is a collection.
 pub fn is_collection(address: &H160) -> bool {
 	address[0..16] == ETH_COLLECTION_PREFIX
 }
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -14,8 +14,43 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Common pallet
+//!
+//! The Common pallet provides functionality for handling collections.
+//!
+//! ## Overview
+//!
+//! The Common pallet provides an interface for common collection operations for different collection types
+//! (see [CommonCollectionOperations]), as well as a generic dispatcher for these, see [dispatch] module.
+//! It also provides this functionality to EVM, see [erc] and [eth] modules.
+//!
+//! The Common pallet provides functions for:
+//!
+//! - Setting and approving collection sponsor.
+//! - Get\set\delete allow list.
+//! - Get\set\delete collection properties.
+//! - Get\set\delete collection property permissions.
+//! - Get\set\delete token property permissions.
+//! - Get\set\delete collection administrators.
+//! - Checking access permissions.
+//!
+//! ### Terminology
+//! **Collection sponsor** - For the collection, you can set a sponsor, at whose expense it will
+//! be possible to mint tokens.
+//!
+//! **Allow list** - List of users who have the right to minting tokens.
+//!
+//! **Collection properties** - Collection properties are simply key-value stores where various
+//! metadata can be placed.
+//!
+//! **Permissions on token properties** - For each property in the token can be set permission
+//! to change, see [`PropertyPermission`].
+//!
+//! **Collection administrator** - For a collection, you can set administrators who have the right
+//! to most actions on the collection.
+
+#![warn(missing_docs)]
 #![cfg_attr(not(feature = "std"), no_std)]
-
 extern crate alloc;
 
 use core::ops::{Deref, DerefMut};
@@ -90,14 +125,24 @@
 pub mod eth;
 pub mod weights;
 
+/// Weight info.
 pub type SelfWeightOf<T> = <T as Config>::WeightInfo;
 
+/// Collection handle contains information about collection data and id.
+/// Also provides functionality to count consumed gas.
+///
+/// CollectionHandle is used as a generic wrapper for collections of all types.
+/// It allows to perform common operations and queries on any collection type,
+/// both completely general for all, as well as their respective implementations of [`CommonCollectionOperations`].
 #[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]
 pub struct CollectionHandle<T: Config> {
+	/// Collection id
 	pub id: CollectionId,
 	collection: Collection<T::AccountId>,
+	/// Substrate recorder for counting consumed gas
 	pub recorder: SubstrateRecorder<T>,
 }
+
 impl<T: Config> WithRecorder<T> for CollectionHandle<T> {
 	fn recorder(&self) -> &SubstrateRecorder<T> {
 		&self.recorder
@@ -106,7 +151,9 @@
 		self.recorder
 	}
 }
+
 impl<T: Config> CollectionHandle<T> {
+	/// Same as [CollectionHandle::new] but with an explicit gas limit.
 	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {
 		<CollectionById<T>>::get(id).map(|collection| Self {
 			id,
@@ -115,6 +162,7 @@
 		})
 	}
 
+	/// Same as [CollectionHandle::new] but with an existed [`SubstrateRecorder`].
 	pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {
 		<CollectionById<T>>::get(id).map(|collection| Self {
 			id,
@@ -123,14 +171,18 @@
 		})
 	}
 
+	/// Retrives collection data from storage and creates collection handle with default parameters.
+	/// If collection not found return `None`
 	pub fn new(id: CollectionId) -> Option<Self> {
 		Self::new_with_gas_limit(id, u64::MAX)
 	}
 
+	/// Same as [`CollectionHandle::new`] but if collection not found [CollectionNotFound](Error::CollectionNotFound) returned.
 	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {
 		Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)
 	}
 
+	/// Consume gas for reading.
 	pub fn consume_store_reads(&self, reads: u64) -> evm_coder::execution::Result<()> {
 		self.recorder
 			.consume_gas(T::GasWeightMapping::weight_to_gas(
@@ -140,6 +192,7 @@
 			))
 	}
 
+	/// Consume gas for writing.
 	pub fn consume_store_writes(&self, writes: u64) -> evm_coder::execution::Result<()> {
 		self.recorder
 			.consume_gas(T::GasWeightMapping::weight_to_gas(
@@ -148,16 +201,27 @@
 					.saturating_mul(writes),
 			))
 	}
+
+	/// Save collection to storage.
 	pub fn save(self) -> DispatchResult {
 		<CollectionById<T>>::insert(self.id, self.collection);
 		Ok(())
 	}
 
+	/// Set collection sponsor.
+	///
+	/// Unique collections allows sponsoring for certain actions.
+	/// This method allows you to set the sponsor of the collection.
+	/// In order for sponsorship to become active, it must be confirmed through [`Self::confirm_sponsorship`].
 	pub fn set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {
 		self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor);
 		Ok(())
 	}
 
+	/// Confirm sponsorship
+	///
+	/// In order for the sponsorship to become active, the user set as the sponsor must confirm their participation.
+	/// Before confirming sponsorship, the user must be specified as the sponsor of the collection via [`Self::set_sponsor`].
 	pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> Result<bool, DispatchError> {
 		if self.collection.sponsorship.pending_sponsor() != Some(sender) {
 			return Ok(false);
@@ -168,7 +232,7 @@
 	}
 
 	/// Checks that the collection was created with, and must be operated upon through **Unique API**.
-	/// Now check only the `external_collection` flag and if it's **true**, then return `CollectionIsExternal` error.
+	/// Now check only the `external_collection` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error.
 	pub fn check_is_internal(&self) -> DispatchResult {
 		if self.external_collection {
 			return Err(<Error<T>>::CollectionIsExternal)?;
@@ -178,7 +242,7 @@
 	}
 
 	/// Checks that the collection was created with, and must be operated upon through an **assimilated API**.
-	/// Now check only the `external_collection` flag and if it's **false**, then return `CollectionIsInternal` error.
+	/// Now check only the `external_collection` flag and if it's **false**, then return [`Error::CollectionIsInternal`] error.
 	pub fn check_is_external(&self) -> DispatchResult {
 		if !self.external_collection {
 			return Err(<Error<T>>::CollectionIsInternal)?;
@@ -203,23 +267,34 @@
 }
 
 impl<T: Config> CollectionHandle<T> {
-	pub fn check_is_owner(&self, subject: &T::CrossAccountId) -> DispatchResult {
-		ensure!(*subject.as_sub() == self.owner, <Error<T>>::NoPermission);
+	/// Checks if the `user` is the owner of the collection.
+	pub fn check_is_owner(&self, user: &T::CrossAccountId) -> DispatchResult {
+		ensure!(*user.as_sub() == self.owner, <Error<T>>::NoPermission);
 		Ok(())
 	}
-	pub fn is_owner_or_admin(&self, subject: &T::CrossAccountId) -> bool {
-		*subject.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, subject))
+
+	/// Returns **true** if the `user` is the owner or administrator of the collection.
+	pub fn is_owner_or_admin(&self, user: &T::CrossAccountId) -> bool {
+		*user.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, user))
 	}
-	pub fn check_is_owner_or_admin(&self, subject: &T::CrossAccountId) -> DispatchResult {
-		ensure!(self.is_owner_or_admin(subject), <Error<T>>::NoPermission);
+
+	/// Checks if the `user` is the owner or administrator of the collection.
+	pub fn check_is_owner_or_admin(&self, user: &T::CrossAccountId) -> DispatchResult {
+		ensure!(self.is_owner_or_admin(user), <Error<T>>::NoPermission);
 		Ok(())
 	}
+
+	/// Return **true** if `user` was not allowed to have tokens, and he can ignore such restrictions.
 	pub fn ignores_allowance(&self, user: &T::CrossAccountId) -> bool {
 		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)
 	}
+
+	/// Return **true** if `user` does not have enough token parts, and he can ignore such restrictions.
 	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {
 		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)
 	}
+
+	/// Checks if the user is in the allow list. If not [Error::AddressNotInAllowlist] returns.
 	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {
 		ensure!(
 			<Allowlist<T>>::get((self.id, user)),
@@ -249,21 +324,34 @@
 		+ TypeInfo
 		+ account::Config
 	{
+		/// Weight information for functions of this pallet.
 		type WeightInfo: WeightInfo;
+
+		/// Events compatible with [`frame_system::Config::Event`].
 		type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;
 
+		/// Handler of accounts and payment.
 		type Currency: Currency<Self::AccountId>;
 
+		/// Set price to create a collection.
 		#[pallet::constant]
 		type CollectionCreationPrice: Get<
 			<<Self as Config>::Currency as Currency<Self::AccountId>>::Balance,
 		>;
+
+		/// Dispatcher of operations on collections.
 		type CollectionDispatch: CollectionDispatch<Self>;
 
+		/// Account which holds the chain's treasury.
 		type TreasuryAccountId: Get<Self::AccountId>;
+
+		/// Address under which the CollectionHelper contract would be available.
 		type ContractAddress: Get<H160>;
 
+		/// Mapper for token addresses to Ethereum addresses.
 		type EvmTokenAddressMapping: TokenAddressMapping<H160>;
+
+		/// Mapper for token addresses to [`CrossAccountId`].
 		type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;
 	}
 
@@ -276,6 +364,7 @@
 
 	#[pallet::extra_constants]
 	impl<T: Config> Pallet<T> {
+		/// Maximum admins per collection.
 		pub fn collection_admins_limit() -> u32 {
 			COLLECTION_ADMINS_LIMIT
 		}
@@ -285,116 +374,116 @@
 	#[pallet::generate_deposit(pub fn deposit_event)]
 	pub enum Event<T: Config> {
 		/// New collection was created
-		///
-		/// # Arguments
-		///
-		/// * collection_id - Globally unique identifier of newly created collection.
-		/// * mode - [CollectionMode] converted into u8.
-		/// * account_id - Collection owner.
-		CollectionCreated(CollectionId, u8, T::AccountId),
+		CollectionCreated(
+			/// Globally unique identifier of newly created collection.
+			CollectionId,
+			/// [`CollectionMode`] converted into _u8_.
+			u8,
+			/// Collection owner.
+			T::AccountId,
+		),
 
 		/// New collection was destroyed
-		///
-		/// # Arguments
-		///
-		/// * collection_id - Globally unique identifier of collection that has been destroyed.
-		CollectionDestroyed(CollectionId),
+		CollectionDestroyed(
+			/// Globally unique identifier of collection.
+			CollectionId,
+		),
 
 		/// New item was created.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - ID of the collection where the item was created.
-		/// * item_id - ID of the item. Unique within the collection.
-		/// * recipient - Owner of the newly created item.
-		/// * amount - The amount of tokens that were created (always 1 for NFT).
-		ItemCreated(CollectionId, TokenId, T::CrossAccountId, u128),
+		ItemCreated(
+			/// Id of the collection where item was created.
+			CollectionId,
+			/// Id of an item. Unique within the collection.
+			TokenId,
+			/// Owner of newly created item
+			T::CrossAccountId,
+			/// Always 1 for NFT
+			u128,
+		),
 
 		/// Collection item was burned.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - Identifier of the collection to which the burned NFT belonged.
-		/// * item_id - Identifier of burned NFT.
-		/// * owner - Which user has destroyed their tokens.
-		/// * amount - Amount of tokens that were destroyed (always 1 for NFT).
-		ItemDestroyed(CollectionId, TokenId, T::CrossAccountId, u128),
+		ItemDestroyed(
+			/// Id of the collection where item was destroyed.
+			CollectionId,
+			/// Identifier of burned NFT.
+			TokenId,
+			/// Which user has destroyed its tokens.
+			T::CrossAccountId,
+			/// Amount of token pieces destroed. Always 1 for NFT.
+			u128,
+		),
 
-		/// Item was transferred.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - ID of the collection to which the item belongs.
-		/// * item_id - ID of the item transferred.
-		/// * sender - Original owner of the item.
-		/// * recipient - New owner of the item.
-		/// * amount - Amount of tokens that were transferred (always 1 for NFT).
+		/// Item was transferred
 		Transfer(
+			/// Id of collection to which item is belong.
 			CollectionId,
+			/// Id of an item.
 			TokenId,
+			/// Original owner of item.
 			T::CrossAccountId,
+			/// New owner of item.
 			T::CrossAccountId,
+			/// Amount of token pieces transfered. Always 1 for NFT.
 			u128,
 		),
 
-		/// Sponsoring allowance was approved.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - todo:doc flesh out
-		/// * item_id
-		/// * sender
-		/// * spender
-		/// * amount
+		/// Amount pieces of token owned by `sender` was approved for `spender`.
 		Approved(
+			/// Id of collection to which item is belong.
 			CollectionId,
+			/// Id of an item.
 			TokenId,
+			/// Original owner of item.
 			T::CrossAccountId,
+			/// Id for which the approval was granted.
 			T::CrossAccountId,
+			/// Amount of token pieces transfered. Always 1 for NFT.
 			u128,
 		),
 
-		/// Collection property was added or edited.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - ID of the collection, whose property was just set.
-		/// * property_key - Key of the property that was just set.
-		CollectionPropertySet(CollectionId, PropertyKey),
+		/// The colletion property has been added or edited.
+		CollectionPropertySet(
+			/// Id of collection to which property has been set.
+			CollectionId,
+			/// The property that was set.
+			PropertyKey,
+		),
 
-		/// Collection property was deleted.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - ID of the collection, whose property was just deleted.
-		/// * property_key - Key of the property that was just deleted.
-		CollectionPropertyDeleted(CollectionId, PropertyKey),
+		/// The property has been deleted.
+		CollectionPropertyDeleted(
+			/// Id of collection to which property has been deleted.
+			CollectionId,
+			/// The property that was deleted.
+			PropertyKey,
+		),
 
-		/// Item property was added or edited.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - ID of the collection, whose token's property was just set.
-		/// * item_id - ID of the item, whose property was just set.
-		/// * property_key - Key of the property that was just set.
-		TokenPropertySet(CollectionId, TokenId, PropertyKey),
+		/// The token property has been added or edited.
+		TokenPropertySet(
+			/// Identifier of the collection whose token has the property set.
+			CollectionId,
+			/// The token for which the property was set.
+			TokenId,
+			/// The property that was set.
+			PropertyKey,
+		),
 
-		/// Item property was deleted.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - ID of the collection, whose token's property was just deleted.
-		/// * item_id - ID of the item, whose property was just deleted.
-		/// * property_key - Key of the property that was just deleted.
-		TokenPropertyDeleted(CollectionId, TokenId, PropertyKey),
+		/// The token property has been deleted.
+		TokenPropertyDeleted(
+			/// Identifier of the collection whose token has the property deleted.
+			CollectionId,
+			/// The token for which the property was deleted.
+			TokenId,
+			/// The property that was deleted.
+			PropertyKey,
+		),
 
-		/// Token property permission was added or updated for a collection.
-		///
-		/// # Arguments
-		///
-		/// * collection_id - ID of the collection, whose permissions were just set/updated.
-		/// * property_key - Key of the property of the set/updated permission.
-		PropertyPermissionSet(CollectionId, PropertyKey),
+		/// The token property permission of a collection has been set.
+		PropertyPermissionSet(
+			/// ID of collection to which property permission has been set.
+			CollectionId,
+			/// The property permission that was set.
+			PropertyKey,
+		),
 	}
 
 	#[pallet::error]
@@ -482,16 +571,16 @@
 		CollectionIsInternal,
 	}
 
-	/// The number of created collections. Essentially contains the last collection ID.
+	/// Storage of the count of created collections. Essentially contains the last collection ID.
 	#[pallet::storage]
 	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;
 
-	/// The number of destroyed collections
+	/// Storage of the count of deleted collections.
 	#[pallet::storage]
 	pub type DestroyedCollectionCount<T> =
 		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;
 
-	/// Collection info
+	/// Storage of collection info.
 	#[pallet::storage]
 	pub type CollectionById<T> = StorageMap<
 		Hasher = Blake2_128Concat,
@@ -500,7 +589,7 @@
 		QueryKind = OptionQuery,
 	>;
 
-	/// Collection properties
+	/// Storage of collection properties.
 	#[pallet::storage]
 	#[pallet::getter(fn collection_properties)]
 	pub type CollectionProperties<T> = StorageMap<
@@ -511,7 +600,7 @@
 		OnEmpty = up_data_structs::CollectionProperties,
 	>;
 
-	/// Token permissions of a collection
+	/// Storage of token property permissions of a collection.
 	#[pallet::storage]
 	#[pallet::getter(fn property_permissions)]
 	pub type CollectionPropertyPermissions<T> = StorageMap<
@@ -521,7 +610,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// Amount of collection admins
+	/// Storage of the amount of collection admins.
 	#[pallet::storage]
 	pub type AdminAmount<T> = StorageMap<
 		Hasher = Blake2_128Concat,
@@ -530,7 +619,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// List of collection admins
+	/// List of collection admins.
 	#[pallet::storage]
 	pub type IsAdmin<T: Config> = StorageNMap<
 		Key = (
@@ -541,7 +630,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// Allowlisted collection users
+	/// Allowlisted collection users.
 	#[pallet::storage]
 	pub type Allowlist<T: Config> = StorageNMap<
 		Key = (
@@ -552,7 +641,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// Not used by code, exists only to provide some types to metadata
+	/// Not used by code, exists only to provide some types to metadata.
 	#[pallet::storage]
 	pub type DummyStorageValue<T: Config> = StorageValue<
 		Value = (
@@ -646,7 +735,9 @@
 }
 
 impl<T: Config> Pallet<T> {
-	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens
+	/// Enshure that receiver address is correct.
+	///
+	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens.
 	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {
 		ensure!(
 			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,
@@ -654,19 +745,27 @@
 		);
 		Ok(())
 	}
+
+	/// Get a vector of collection admins.
 	pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {
 		<IsAdmin<T>>::iter_prefix((collection,))
 			.map(|(a, _)| a)
 			.collect()
 	}
+
+	/// Get a vector of users allowed to mint tokens.
 	pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {
 		<Allowlist<T>>::iter_prefix((collection,))
 			.map(|(a, _)| a)
 			.collect()
 	}
+
+	/// Is `user` allowed to mint token in `collection`.
 	pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {
 		<Allowlist<T>>::get((collection, user))
 	}
+
+	/// Get statistics of collections.
 	pub fn collection_stats() -> CollectionStats {
 		let created = <CreatedCollectionCount<T>>::get();
 		let destroyed = <DestroyedCollectionCount<T>>::get();
@@ -677,6 +776,7 @@
 		}
 	}
 
+	/// Get the effective limits for the collection.
 	pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {
 		let collection = <CollectionById<T>>::get(collection);
 		if collection.is_none() {
@@ -710,6 +810,7 @@
 		Some(effective_limits)
 	}
 
+	/// Returns information about the `collection` adapted for rpc.
 	pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {
 		let Collection {
 			name,
@@ -785,6 +886,11 @@
 }
 
 impl<T: Config> Pallet<T> {
+	/// Create new collection.
+	///
+	/// * `owner` - The owner of the collection.
+	/// * `data` - Description of the created collection.
+	/// * `is_external` - Marks that collection managet by not "Unique network".
 	pub fn init_collection(
 		owner: T::CrossAccountId,
 		data: CreateCollectionData<T::AccountId>,
@@ -860,7 +966,7 @@
 				),
 			);
 			<T as Config>::Currency::settle(
-				&owner.as_sub(),
+				owner.as_sub(),
 				imbalance,
 				WithdrawReasons::TRANSFER,
 				ExistenceRequirement::KeepAlive,
@@ -885,6 +991,10 @@
 		Ok(id)
 	}
 
+	/// Destroy collection.
+	///
+	/// * `collection` - Collection handler.
+	/// * `sender` - The owner or administrator of the collection.
 	pub fn destroy_collection(
 		collection: CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -913,6 +1023,11 @@
 		Ok(())
 	}
 
+	/// Set collection property.
+	///
+	/// * `collection` - Collection handler.
+	/// * `sender` - The owner or administrator of the collection.
+	/// * `property` - The property to set.
 	pub fn set_collection_property(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -931,6 +1046,11 @@
 		Ok(())
 	}
 
+	/// Set scouped collection property.
+	///
+	/// * `collection_id` - ID of the collection for which the property is being set.
+	/// * `scope` - Property scope.
+	/// * `property` - The property to set.
 	pub fn set_scoped_collection_property(
 		collection_id: CollectionId,
 		scope: PropertyScope,
@@ -944,6 +1064,11 @@
 		Ok(())
 	}
 
+	/// Set scouped collection properties.
+	///
+	/// * `collection_id` - ID of the collection for which the properties is being set.
+	/// * `scope` - Property scope.
+	/// * `properties` - The properties to set.
 	pub fn set_scoped_collection_properties(
 		collection_id: CollectionId,
 		scope: PropertyScope,
@@ -957,6 +1082,11 @@
 		Ok(())
 	}
 
+	/// Set collection properties.
+	///
+	/// * `collection` - Collection handler.
+	/// * `sender` - The owner or administrator of the collection.
+	/// * `properties` - The properties to set.
 	#[transactional]
 	pub fn set_collection_properties(
 		collection: &CollectionHandle<T>,
@@ -970,6 +1100,11 @@
 		Ok(())
 	}
 
+	/// Delete collection property.
+	///
+	/// * `collection` - Collection handler.
+	/// * `sender` - The owner or administrator of the collection.
+	/// * `property` - The property to delete.
 	pub fn delete_collection_property(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -990,6 +1125,11 @@
 		Ok(())
 	}
 
+	/// Delete collection properties.
+	///
+	/// * `collection` - Collection handler.
+	/// * `sender` - The owner or administrator of the collection.
+	/// * `properties` - The properties to delete.
 	#[transactional]
 	pub fn delete_collection_properties(
 		collection: &CollectionHandle<T>,
@@ -1003,7 +1143,12 @@
 		Ok(())
 	}
 
-	// For migrations
+	/// Set collection propetry permission without any checks.
+	///
+	/// Used for migrations.
+	///
+	/// * `collection` - Collection handler.
+	/// * `property_permissions` - Property permissions.
 	pub fn set_property_permission_unchecked(
 		collection: CollectionId,
 		property_permission: PropertyKeyPermission,
@@ -1015,6 +1160,11 @@
 		Ok(())
 	}
 
+	/// Set collection property permission.
+	///
+	/// * `collection` - Collection handler.
+	/// * `sender` - The owner or administrator of the collection.
+	/// * `property_permission` - Property permission.
 	pub fn set_property_permission(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -1045,6 +1195,11 @@
 		Ok(())
 	}
 
+	/// Set token property permission.
+	///
+	/// * `collection` - Collection handler.
+	/// * `sender` - The owner or administrator of the collection.
+	/// * `property_permissions` - Property permissions.
 	#[transactional]
 	pub fn set_token_property_permissions(
 		collection: &CollectionHandle<T>,
@@ -1058,6 +1213,7 @@
 		Ok(())
 	}
 
+	/// Get collection property.
 	pub fn get_collection_property(
 		collection_id: CollectionId,
 		key: &PropertyKey,
@@ -1065,6 +1221,7 @@
 		Self::collection_properties(collection_id).get(key).cloned()
 	}
 
+	/// Convert byte vector to property key vector.
 	pub fn bytes_keys_to_property_keys(
 		keys: Vec<Vec<u8>>,
 	) -> Result<Vec<PropertyKey>, DispatchError> {
@@ -1076,6 +1233,7 @@
 			.collect::<Result<Vec<PropertyKey>, DispatchError>>()
 	}
 
+	/// Get properties according to given keys.
 	pub fn filter_collection_properties(
 		collection_id: CollectionId,
 		keys: Option<Vec<PropertyKey>>,
@@ -1103,6 +1261,7 @@
 		Ok(properties)
 	}
 
+	/// Get property permissions according to given keys.
 	pub fn filter_property_permissions(
 		collection_id: CollectionId,
 		keys: Option<Vec<PropertyKey>>,
@@ -1132,6 +1291,7 @@
 		Ok(key_permissions)
 	}
 
+	/// Toggle `user` participation in the `collection`'s allow list.
 	pub fn toggle_allowlist(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -1151,6 +1311,7 @@
 		Ok(())
 	}
 
+	/// Toggle `user` participation in the `collection`'s admin list.
 	pub fn toggle_admin(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -1186,6 +1347,7 @@
 		Ok(())
 	}
 
+	/// Merge set fields from `new_limit` to `old_limit`.
 	pub fn clamp_limits(
 		mode: CollectionMode,
 		old_limit: &CollectionLimits,
@@ -1231,20 +1393,22 @@
 		Ok(new_limit)
 	}
 
+	/// Merge set fields from `new_permission` to `old_permission`.
 	pub fn clamp_permissions(
 		_mode: CollectionMode,
-		old_limit: &CollectionPermissions,
-		mut new_limit: CollectionPermissions,
+		old_permission: &CollectionPermissions,
+		mut new_permission: CollectionPermissions,
 	) -> Result<CollectionPermissions, DispatchError> {
-		limit_default_clone!(old_limit, new_limit,
+		limit_default_clone!(old_permission, new_permission,
 			access => {},
 			mint_mode => {},
 			nesting => { /* todo check for permissive, if only it gets out of benchmarks */ },
 		);
-		Ok(new_limit)
+		Ok(new_permission)
 	}
 }
 
+/// Indicates unsupported methods by returning [Error::UnsupportedOperation].
 #[macro_export]
 macro_rules! unsupported {
 	() => {
@@ -1252,32 +1416,72 @@
 	};
 }
 
-/// Worst cases
+/// Return weights for various worst-case operations.
 pub trait CommonWeightInfo<CrossAccountId> {
+	/// Weight of item creation.
 	fn create_item() -> Weight;
+
+	/// Weight of items creation.
 	fn create_multiple_items(amount: &[CreateItemData]) -> Weight;
+
+	/// Weight of items creation.
 	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;
+
+	/// The weight of the burning item.
 	fn burn_item() -> Weight;
+
+	/// Property setting weight.
+	///
+	/// * `amount`- The number of properties to set.
 	fn set_collection_properties(amount: u32) -> Weight;
+
+	/// Collection property deletion weight.
+	///
+	/// * `amount`- The number of properties to set.
 	fn delete_collection_properties(amount: u32) -> Weight;
+
+	/// Token property setting weight.
+	///
+	/// * `amount`- The number of properties to set.
 	fn set_token_properties(amount: u32) -> Weight;
+
+	/// Token property deletion weight.
+	///
+	/// * `amount`- The number of properties to delete.
 	fn delete_token_properties(amount: u32) -> Weight;
+
+	/// Token property permissions set weight.
+	///
+	/// * `amount`- The number of property permissions to set.
 	fn set_token_property_permissions(amount: u32) -> Weight;
+
+	/// Transfer price of the token or its parts.
 	fn transfer() -> Weight;
+
+	/// The price of setting the permission of the operation from another user.
 	fn approve() -> Weight;
+
+	/// Transfer price from another user.
 	fn transfer_from() -> Weight;
+
+	/// The price of burning a token from another user.
 	fn burn_from() -> Weight;
 
 	/// Differs from burn_item in case of Fungible and Refungible, as it should burn
-	/// whole users's balance
+	/// whole users's balance.
 	///
-	/// This method shouldn't be used directly, as it doesn't count breadth price, use `burn_recursively` instead
+	/// This method shouldn't be used directly, as it doesn't count breadth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead
 	fn burn_recursively_self_raw() -> Weight;
-	/// Cost of iterating over `amount` children while burning, without counting child burning itself
+
+	/// Cost of iterating over `amount` children while burning, without counting child burning itself.
 	///
-	/// This method shouldn't be used directly, as it doesn't count depth price, use `burn_recursively` instead
+	/// This method shouldn't be used directly, as it doesn't count depth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead
 	fn burn_recursively_breadth_raw(amount: u32) -> Weight;
 
+	/// The price of recursive burning a token.
+	///
+	/// `max_selfs` - The maximum burning weight of the token itself.
+	/// `max_breadth` - The maximum number of nested tokens to burn.
 	fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight {
 		Self::burn_recursively_self_raw()
 			.saturating_mul(max_selfs.max(1) as u64)
@@ -1285,11 +1489,23 @@
 	}
 }
 
+/// Weight info extension trait for refungible pallet.
 pub trait RefungibleExtensionsWeightInfo {
+	/// Weight of token repartition.
 	fn repartition() -> Weight;
 }
 
+/// Common collection operations.
+///
+/// It wraps methods in Fungible, Nonfungible and Refungible pallets
+/// and adds weight info.
 pub trait CommonCollectionOperations<T: Config> {
+	/// Create token.
+	///
+	/// * `sender` - The user who mint the token and pays for the transaction.
+	/// * `to` - The user who will own the token.
+	/// * `data` - Token data.
+	/// * `nesting_budget` - A budget that can be spent on nesting tokens.
 	fn create_item(
 		&self,
 		sender: T::CrossAccountId,
@@ -1297,6 +1513,13 @@
 		data: CreateItemData,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Create multiple tokens.
+	///
+	/// * `sender` - The user who mint the token and pays for the transaction.
+	/// * `to` - The user who will own the token.
+	/// * `data` - Token data.
+	/// * `nesting_budget` - A budget that can be spent on nesting tokens.
 	fn create_multiple_items(
 		&self,
 		sender: T::CrossAccountId,
@@ -1304,18 +1527,38 @@
 		data: Vec<CreateItemData>,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Create multiple tokens.
+	///
+	/// * `sender` - The user who mint the token and pays for the transaction.
+	/// * `to` - The user who will own the token.
+	/// * `data` - Token data.
+	/// * `nesting_budget` - A budget that can be spent on nesting tokens.
 	fn create_multiple_items_ex(
 		&self,
 		sender: T::CrossAccountId,
 		data: CreateItemExData<T::CrossAccountId>,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Burn token.
+	///
+	/// * `sender` - The user who owns the token.
+	/// * `token` - Token id that will burned.
+	/// * `amount` - The number of parts of the token that will be burned.
 	fn burn_item(
 		&self,
 		sender: T::CrossAccountId,
 		token: TokenId,
 		amount: u128,
 	) -> DispatchResultWithPostInfo;
+
+	/// Burn token and all nested tokens recursievly.
+	///
+	/// * `sender` - The user who owns the token.
+	/// * `token` - Token id that will burned.
+	/// * `self_budget` - The budget that can be spent on burning tokens.
+	/// * `breadth_budget` - The budget that can be spent on burning nested tokens.
 	fn burn_item_recursively(
 		&self,
 		sender: T::CrossAccountId,
@@ -1323,43 +1566,95 @@
 		self_budget: &dyn Budget,
 		breadth_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Set collection properties.
+	///
+	/// * `sender` - Must be either the owner of the collection or its admin.
+	/// * `properties` - Properties to be set.
 	fn set_collection_properties(
 		&self,
 		sender: T::CrossAccountId,
 		properties: Vec<Property>,
 	) -> DispatchResultWithPostInfo;
+
+	/// Delete collection properties.
+	///
+	/// * `sender` - Must be either the owner of the collection or its admin.
+	/// * `properties` - The properties to be removed.
 	fn delete_collection_properties(
 		&self,
 		sender: &T::CrossAccountId,
 		property_keys: Vec<PropertyKey>,
 	) -> DispatchResultWithPostInfo;
+
+	/// Set token properties.
+	///
+	/// The appropriate [`PropertyPermission`] for the token property
+	/// must be set with [`Self::set_token_property_permissions`].
+	///
+	/// * `sender` - Must be either the owner of the token or its admin.
+	/// * `token_id` - The token for which the properties are being set.
+	/// * `properties` - Properties to be set.
+	/// * `budget` - Budget for setting properties.
 	fn set_token_properties(
 		&self,
 		sender: T::CrossAccountId,
 		token_id: TokenId,
-		property: Vec<Property>,
-		nesting_budget: &dyn Budget,
+		properties: Vec<Property>,
+		budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Remove token properties.
+	///
+	/// The appropriate [`PropertyPermission`] for the token property
+	/// must be set with [`Self::set_token_property_permissions`].
+	///
+	/// * `sender` - Must be either the owner of the token or its admin.
+	/// * `token_id` - The token for which the properties are being remove.
+	/// * `property_keys` - Keys to remove corresponding properties.
+	/// * `budget` - Budget for removing properties.
 	fn delete_token_properties(
 		&self,
 		sender: T::CrossAccountId,
 		token_id: TokenId,
 		property_keys: Vec<PropertyKey>,
-		nesting_budget: &dyn Budget,
+		budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Set token property permissions.
+	///
+	/// * `sender` - Must be either the owner of the token or its admin.
+	/// * `token_id` - The token for which the properties are being set.
+	/// * `properties` - Properties to be set.
+	/// * `budget` - Budget for setting properties.
 	fn set_token_property_permissions(
 		&self,
 		sender: &T::CrossAccountId,
 		property_permissions: Vec<PropertyKeyPermission>,
 	) -> DispatchResultWithPostInfo;
+
+	/// Transfer amount of token pieces.
+	///
+	/// * `sender` - Donor user.
+	/// * `to` - Recepient user.
+	/// * `token` - The token of which parts are being sent.
+	/// * `amount` - The number of parts of the token that will be transferred.
+	/// * `budget` - The maximum budget that can be spent on the transfer.
 	fn transfer(
 		&self,
 		sender: T::CrossAccountId,
 		to: T::CrossAccountId,
 		token: TokenId,
 		amount: u128,
-		nesting_budget: &dyn Budget,
+		budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Grant access to another account to transfer parts of the token owned by the calling user via [Self::transfer_from].
+	///
+	/// * `sender` - The user who grants access to the token.
+	/// * `spender` - The user to whom the rights are granted.
+	/// * `token` - The token to which access is granted.
+	/// * `amount` - The amount of pieces that another user can dispose of.
 	fn approve(
 		&self,
 		sender: T::CrossAccountId,
@@ -1367,6 +1662,17 @@
 		token: TokenId,
 		amount: u128,
 	) -> DispatchResultWithPostInfo;
+
+	/// Send parts of a token owned by another user.
+	///
+	/// Before calling this method, you must grant rights to the calling user via [`Self::approve`].
+	///
+	/// * `sender` - The user who must have access to the token (see [`Self::approve`]).
+	/// * `from` - The user who owns the token.
+	/// * `to` - Recepient user.
+	/// * `token` - The token of which parts are being sent.
+	/// * `amount` - The number of parts of the token that will be transferred.
+	/// * `budget` - The maximum budget that can be spent on the transfer.
 	fn transfer_from(
 		&self,
 		sender: T::CrossAccountId,
@@ -1374,67 +1680,145 @@
 		to: T::CrossAccountId,
 		token: TokenId,
 		amount: u128,
-		nesting_budget: &dyn Budget,
+		budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
+
+	/// Burn parts of a token owned by another user.
+	///
+	/// Before calling this method, you must grant rights to the calling user via [`Self::approve`].
+	///
+	/// * `sender` - The user who must have access to the token (see [`Self::approve`]).
+	/// * `from` - The user who owns the token.
+	/// * `token` - The token of which parts are being sent.
+	/// * `amount` - The number of parts of the token that will be transferred.
+	/// * `budget` - The maximum budget that can be spent on the burn.
 	fn burn_from(
 		&self,
 		sender: T::CrossAccountId,
 		from: T::CrossAccountId,
 		token: TokenId,
 		amount: u128,
-		nesting_budget: &dyn Budget,
+		budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo;
 
+	/// Check permission to nest token.
+	///
+	/// * `sender` - The user who initiated the check.
+	/// * `from` - The token that is checked for embedding.
+	/// * `under` - Token under which to check.
+	/// * `budget` - The maximum budget that can be spent on the check.
 	fn check_nesting(
 		&self,
 		sender: T::CrossAccountId,
 		from: (CollectionId, TokenId),
 		under: TokenId,
-		nesting_budget: &dyn Budget,
+		budget: &dyn Budget,
 	) -> DispatchResult;
 
+	/// Nest one token into another.
+	///
+	/// * `under` - Token holder.
+	/// * `to_nest` - Nested token.
 	fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));
 
+	/// Unnest token.
+	///
+	/// * `under` - Token holder.
+	/// * `to_nest` - Token to unnest.
 	fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId));
 
+	/// Get all user tokens.
+	///
+	/// * `account` - Account for which you need to get tokens.
 	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;
+
+	/// Get all the tokens in the collection.
 	fn collection_tokens(&self) -> Vec<TokenId>;
+
+	/// Check if the token exists.
+	///
+	/// * `token` - Id token to check.
 	fn token_exists(&self, token: TokenId) -> bool;
+
+	/// Get the id of the last minted token.
 	fn last_token_id(&self) -> TokenId;
 
+	/// Get the owner of the token.
+	///
+	/// * `token` - The token for which you need to find out the owner.
 	fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;
+
+	/// Returns 10 tokens owners in no particular order.
+	///
+	/// * `token` - The token for which you need to find out the owners.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId>;
+
+	/// Get the value of the token property by key.
+	///
+	/// * `token` - Token with the property to get.
+	/// * `key` - Property name.
 	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;
-	fn token_properties(&self, token_id: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;
+
+	/// Get a set of token properties by key vector.
+	///
+	/// * `token` - Token with the property to get.
+	/// * `keys` - Vector of property keys. If this parameter is [None](sp_std::result::Result),
+	/// then all properties are returned.
+	fn token_properties(&self, token: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;
+
 	/// Amount of unique collection tokens
 	fn total_supply(&self) -> u32;
-	/// Amount of different tokens account has (Applicable to nonfungible/refungible)
+
+	/// Amount of different tokens account has.
+	///
+	/// * `account` - The account for which need to get the balance.
 	fn account_balance(&self, account: T::CrossAccountId) -> u32;
-	/// Amount of specific token account have (Applicable to fungible/refungible)
+
+	/// Amount of specific token account have.
 	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;
+
 	/// Amount of token pieces
 	fn total_pieces(&self, token: TokenId) -> Option<u128>;
+
+	/// Get the number of parts of the token that a trusted user can manage.
+	///
+	/// * `sender` - Trusted user.
+	/// * `spender` - Owner of the token.
+	/// * `token` - The token for which to get the value.
 	fn allowance(
 		&self,
 		sender: T::CrossAccountId,
 		spender: T::CrossAccountId,
 		token: TokenId,
 	) -> u128;
+
+	/// Get extension for RFT collection.
 	fn refungible_extensions(&self) -> Option<&dyn RefungibleExtensions<T>>;
 }
 
+/// Extension for RFT collection.
 pub trait RefungibleExtensions<T>
 where
 	T: Config,
 {
+	/// Change the number of parts of the token.
+	///
+	/// When the value changes down, this function is equivalent to burning parts of the token.
+	///
+	/// * `sender` - The user calling the repartition operation. Must be the owner of the token.
+	/// * `token` - The token for which you want to change the number of parts.
+	/// * `amount` - The new value of the parts of the token.
 	fn repartition(
 		&self,
-		owner: &T::CrossAccountId,
+		sender: &T::CrossAccountId,
 		token: TokenId,
 		amount: u128,
 	) -> DispatchResultWithPostInfo;
 }
 
-// Flexible enough for implementing CommonCollectionOperations
+/// Merge [`DispatchResult`] with [`Weight`] into [`DispatchResultWithPostInfo`].
+///
+/// Used for [`CommonCollectionOperations`] implementations and flexible enough to do so.
 pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {
 	let post_info = PostDispatchInfo {
 		actual_weight: Some(weight),
modifiedpallets/evm-contract-helpers/src/stubs/ContractHelpers.rawdiffbeforeafterboth

binary blob — no preview

addedpallets/fungible/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/fungible/CHANGELOG.md
@@ -0,0 +1,13 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [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. 
+    
+ 
+ 
\ No newline at end of file
modifiedpallets/fungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/fungible/Cargo.toml
+++ b/pallets/fungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-fungible"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/fungible/src/common.rsdiffbeforeafterboth
--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -105,6 +105,8 @@
 	}
 }
 
+/// Implementation of `CommonCollectionOperations` for `FungibleHandle`. It wraps FungibleHandle Pallete
+/// methods and adds weight info.
 impl<T: Config> CommonCollectionOperations<T> for FungibleHandle<T> {
 	fn create_item(
 		&self,
@@ -360,6 +362,11 @@
 		None
 	}
 
+	/// Returns 10 tokens owners in no particular order.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId> {
+		<Pallet<T>>::token_owners(self.id, token).unwrap_or_default()
+	}
+
 	fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option<PropertyValue> {
 		None
 	}
modifiedpallets/fungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/fungible/src/erc.rs
+++ b/pallets/fungible/src/erc.rs
@@ -14,6 +14,8 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! ERC-20 standart support implementation.
+
 use core::char::{REPLACEMENT_CHARACTER, decode_utf16};
 use core::convert::TryInto;
 use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*, weight};
modifiedpallets/fungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/fungible/src/lib.rs
+++ b/pallets/fungible/src/lib.rs
@@ -14,6 +14,68 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Fungible Pallet
+//!
+//! The Fungible pallet provides functionality for dealing with fungible assets.
+//!
+//! - [`CreateItemData`]
+//! - [`Config`]
+//! - [`FungibleHandle`]
+//! - [`Pallet`]
+//! - [`TotalSupply`]
+//! - [`Balance`]
+//! - [`Allowance`]
+//! - [`Error`]
+//!
+//! ## Fungible tokens
+//!
+//! Fungible tokens or assets are divisible and non-unique. For instance,
+//! fiat currencies like the dollar are fungible: A $1 bill
+//! in New York City has the same value as a $1 bill in Miami.
+//! A fungible token can also be a cryptocurrency like Bitcoin: 1 BTC is worth 1 BTC,
+//! no matter where it is issued. Thus, the fungibility refers to a specific currency’s
+//! ability to maintain one standard value. As well, it needs to have uniform acceptance.
+//! This means that a currency’s history should not be able to affect its value,
+//! and this is due to the fact that each piece that is a part of the currency is equal
+//! in value when compared to every other piece of that exact same currency.
+//! In the world of cryptocurrencies, this is essentially a coin or a token
+//! that can be replaced by another identical coin or token, and they are
+//! both mutually interchangeable. A popular implementation of fungible tokens is
+//! the ERC-20 token standard.
+//!
+//! ### ERC-20
+//!
+//! The [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) (Ethereum Request for Comments 20), proposed by Fabian Vogelsteller in November 2015,
+//! is a Token Standard that implements an API for tokens within Smart Contracts.
+//!
+//! Example functionalities ERC-20 provides:
+//!
+//! * transfer tokens from one account to another
+//! * get the current token balance of an account
+//! * get the total supply of the token available on the network
+//! * approve whether an amount of token from an account can be spent by a third-party account
+//!
+//! ## Overview
+//!
+//! The module provides functionality for asset management of fungible asset, supports ERC-20 standart, includes:
+//!
+//! * Asset Issuance
+//! * Asset Transferal
+//! * Asset Destruction
+//! * Delegated Asset Transfers
+//!
+//! **NOTE:** The created fungible asset always has `token_id` = 0.
+//! So `tokenA` and `tokenB` will have different `collection_id`.
+//!
+//! ### Implementations
+//!
+//! The Fungible pallet provides implementations for the following traits.
+//!
+//! - [`WithRecorder`](pallet_evm_coder_substrate::WithRecorder): Trait for EVM support
+//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing with collections
+//!	- [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight
+//! - [`CommonEvmHandler`](pallet_common::erc::CommonEvmHandler): Function for handling EVM runtime calls
+
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use core::ops::Deref;
@@ -33,7 +95,7 @@
 use pallet_evm_coder_substrate::WithRecorder;
 use sp_core::H160;
 use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
-use sp_std::{collections::btree_map::BTreeMap};
+use sp_std::{collections::btree_map::BTreeMap, vec::Vec};
 
 pub use pallet::*;
 
@@ -44,7 +106,6 @@
 pub mod erc;
 pub mod weights;
 
-/// todo:doc?
 pub type CreateItemData<T> = (<T as pallet_evm::account::Config>::CrossAccountId, u128);
 pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;
 
@@ -58,7 +119,7 @@
 	pub enum Error<T> {
 		/// Not Fungible item data used to mint in Fungible collection.
 		NotFungibleDataUsedToMintFungibleCollectionToken,
-		/// Not default id passed as TokenId argument.
+		/// Fungible tokens hold no ID, and the default value of TokenId for Fungible collection is 0.
 		FungibleItemsHaveNoId,
 		/// Tried to set data for fungible item.
 		FungibleItemsDontHaveData,
@@ -95,7 +156,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// todo:doc
+	/// Storage for assets delegated to a limited extent to other users.
 	#[pallet::storage]
 	pub type Allowance<T: Config> = StorageNMap<
 		Key = (
@@ -108,14 +169,22 @@
 	>;
 }
 
+/// Wrapper around untyped collection handle, asserting inner collection is of fungible type.
+/// Required for interaction with Fungible collections, type safety and implementation [`solidity_interface`][`evm_coder::solidity_interface`].
 pub struct FungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);
+
+/// Implementation of methods required for dispatching during runtime.
 impl<T: Config> FungibleHandle<T> {
+	/// Casts [`CollectionHandle`][`pallet_common::CollectionHandle`] into [`FungibleHandle`].
 	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {
 		Self(inner)
 	}
+
+	/// Casts [`FungibleHandle`] into [`CollectionHandle`][`pallet_common::CollectionHandle`].
 	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {
 		self.0
 	}
+	/// Returns a mutable reference to the internal [`CollectionHandle`][`pallet_common::CollectionHandle`].
 	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {
 		&mut self.0
 	}
@@ -136,13 +205,17 @@
 	}
 }
 
+/// Pallet implementation for fungible assets
 impl<T: Config> Pallet<T> {
+	/// Initializes the collection. Returns [CollectionId] on success, [DispatchError] otherwise.
 	pub fn init_collection(
 		owner: T::CrossAccountId,
 		data: CreateCollectionData<T::AccountId>,
 	) -> Result<CollectionId, DispatchError> {
 		<PalletCommon<T>>::init_collection(owner, data, false)
 	}
+
+	/// Destroys a collection.
 	pub fn destroy_collection(
 		collection: FungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -163,10 +236,14 @@
 		Ok(())
 	}
 
+	///Checks if collection has tokens. Return `true` if it has.
 	fn collection_has_tokens(collection_id: CollectionId) -> bool {
 		<TotalSupply<T>>::get(collection_id) != 0
 	}
 
+	/// Burns the specified amount of the token. If the token balance
+	/// or total supply is less than the given value,
+	/// it will return [DispatchError].
 	pub fn burn(
 		collection: &FungibleHandle<T>,
 		owner: &T::CrossAccountId,
@@ -211,6 +288,13 @@
 		Ok(())
 	}
 
+	/// Transfers the specified amount of tokens. Will check that
+	/// the transfer is allowed for the token.
+	///
+	/// - `from`: Owner of tokens to transfer.
+	/// - `to`: Recepient of transfered tokens.
+	/// - `amount`: Amount of tokens to transfer.
+	/// - `collection`: Collection that contains the token
 	pub fn transfer(
 		collection: &FungibleHandle<T>,
 		from: &T::CrossAccountId,
@@ -281,6 +365,8 @@
 		Ok(())
 	}
 
+	/// Minting tokens for multiple IDs.
+	/// See [`create_item`][`Pallet::create_item`] for more details.
 	pub fn create_multiple_items(
 		collection: &FungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -382,6 +468,12 @@
 		));
 	}
 
+	/// Set allowance for the spender to `transfer` or `burn` owner's tokens.
+	///
+	/// - `collection`: Collection that contains the token
+	/// - `owner`: Owner of tokens that sets the allowance.
+	/// - `spender`: Recipient of the allowance rights.
+	/// - `amount`: Amount of tokens the spender is allowed to `transfer` or `burn`.
 	pub fn set_allowance(
 		collection: &FungibleHandle<T>,
 		owner: &T::CrossAccountId,
@@ -406,6 +498,13 @@
 		Ok(())
 	}
 
+	/// Checks if a non-owner has (enough) allowance from the owner to perform operations on the tokens.
+	/// Returns the expected remaining allowance - it should be set manually if the transaction proceeds.
+	///
+	/// - `collection`: Collection that contains the token.
+	/// - `spender`: CrossAccountId who has the allowance rights.
+	/// - `from`: The owner of the tokens who sets the allowance.
+	/// - `amount`: Amount of tokens by which the allowance sholud be reduced.
 	fn check_allowed(
 		collection: &FungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -445,6 +544,11 @@
 		Ok(allowance)
 	}
 
+	/// Transfer fungible tokens from one account to another.
+	/// Same as the [`transfer`][`Pallet::transfer`] but spender doesn't needs to be an owner of the token pieces.
+	/// The owner should set allowance for the spender to transfer pieces.
+	///	See [`set_allowance`][`Pallet::set_allowance`] for more details.
+
 	pub fn transfer_from(
 		collection: &FungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -464,6 +568,11 @@
 		Ok(())
 	}
 
+	/// Burn fungible tokens from the account.
+	///
+	/// Same as the [`burn`][`Pallet::burn`] but spender doesn't need to be an owner of the tokens. The `from` should
+	/// set allowance for the spender to burn tokens.
+	/// See [`set_allowance`][`Pallet::set_allowance`] for more details.
 	pub fn burn_from(
 		collection: &FungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -482,7 +591,13 @@
 		Ok(())
 	}
 
-	/// Delegated to `create_multiple_items`
+	///	Creates fungible token.
+	///
+	/// The sender should be the owner/admin of the collection or collection should be configured
+	/// to allow public minting.
+	///
+	/// - `data`: Contains user who will become the owners of the tokens and amount
+	///   of tokens he will receive.
 	pub fn create_item(
 		collection: &FungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -496,4 +611,26 @@
 			nesting_budget,
 		)
 	}
+
+	/// Returns 10 tokens owners in no particular order
+	///
+	/// There is no direct way to get token holders in ascending order,
+	/// since `iter_prefix` returns values in no particular order.
+	/// Therefore, getting the 10 largest holders with a large value of holders
+	/// can lead to impact memory allocation + sorting with  `n * log (n)`.
+	pub fn token_owners(
+		collection: CollectionId,
+		_token: TokenId,
+	) -> Option<Vec<T::CrossAccountId>> {
+		let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection,))
+			.map(|(owner, _amount)| owner)
+			.take(10)
+			.collect();
+
+		if res.is_empty() {
+			None
+		} else {
+			Some(res)
+		}
+	}
 }
modifiedpallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth
--- a/pallets/fungible/src/stubs/UniqueFungible.sol
+++ b/pallets/fungible/src/stubs/UniqueFungible.sol
@@ -43,6 +43,233 @@
 	}
 }
 
+// Selector: 7d9262e6
+contract Collection is Dummy, ERC165 {
+	// Set collection property.
+	//
+	// @param key Property key.
+	// @param value Propery value.
+	//
+	// Selector: setCollectionProperty(string,bytes) 2f073f66
+	function setCollectionProperty(string memory key, bytes memory value)
+		public
+	{
+		require(false, stub_error);
+		key;
+		value;
+		dummy = 0;
+	}
+
+	// Delete collection property.
+	//
+	// @param key Property key.
+	//
+	// Selector: deleteCollectionProperty(string) 7b7debce
+	function deleteCollectionProperty(string memory key) public {
+		require(false, stub_error);
+		key;
+		dummy = 0;
+	}
+
+	// Get collection property.
+	//
+	// @dev Throws error if key not found.
+	//
+	// @param key Property key.
+	// @return bytes The property corresponding to the key.
+	//
+	// Selector: collectionProperty(string) cf24fd6d
+	function collectionProperty(string memory key)
+		public
+		view
+		returns (bytes memory)
+	{
+		require(false, stub_error);
+		key;
+		dummy;
+		return hex"";
+	}
+
+	// Set the sponsor of the collection.
+	//
+	// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
+	//
+	// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.
+	//
+	// Selector: setCollectionSponsor(address) 7623402e
+	function setCollectionSponsor(address sponsor) public {
+		require(false, stub_error);
+		sponsor;
+		dummy = 0;
+	}
+
+	// Collection sponsorship confirmation.
+	//
+	// @dev After setting the sponsor for the collection, it must be confirmed with this function.
+	//
+	// Selector: confirmCollectionSponsorship() 3c50e97a
+	function confirmCollectionSponsorship() public {
+		require(false, stub_error);
+		dummy = 0;
+	}
+
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"accountTokenOwnershipLimit",
+	// 	"sponsoredDataSize",
+	// 	"sponsoredDataRateLimit",
+	// 	"tokenLimit",
+	// 	"sponsorTransferTimeout",
+	// 	"sponsorApproveTimeout"
+	// @param value Value of the limit.
+	//
+	// Selector: setCollectionLimit(string,uint32) 6a3841db
+	function setCollectionLimit(string memory limit, uint32 value) public {
+		require(false, stub_error);
+		limit;
+		value;
+		dummy = 0;
+	}
+
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"ownerCanTransfer",
+	// 	"ownerCanDestroy",
+	// 	"transfersEnabled"
+	// @param value Value of the limit.
+	//
+	// Selector: setCollectionLimit(string,bool) 993b7fba
+	function setCollectionLimit(string memory limit, bool value) public {
+		require(false, stub_error);
+		limit;
+		value;
+		dummy = 0;
+	}
+
+	// Get contract address.
+	//
+	// Selector: contractAddress() f6b4dfb4
+	function contractAddress() public view returns (address) {
+		require(false, stub_error);
+		dummy;
+		return 0x0000000000000000000000000000000000000000;
+	}
+
+	// Add collection admin by substrate address.
+	// @param new_admin Substrate administrator address.
+	//
+	// Selector: addCollectionAdminSubstrate(uint256) 5730062b
+	function addCollectionAdminSubstrate(uint256 newAdmin) public {
+		require(false, stub_error);
+		newAdmin;
+		dummy = 0;
+	}
+
+	// Remove collection admin by substrate address.
+	// @param admin Substrate administrator address.
+	//
+	// Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9
+	function removeCollectionAdminSubstrate(uint256 admin) public {
+		require(false, stub_error);
+		admin;
+		dummy = 0;
+	}
+
+	// Add collection admin.
+	// @param new_admin Address of the added administrator.
+	//
+	// Selector: addCollectionAdmin(address) 92e462c7
+	function addCollectionAdmin(address newAdmin) public {
+		require(false, stub_error);
+		newAdmin;
+		dummy = 0;
+	}
+
+	// Remove collection admin.
+	//
+	// @param new_admin Address of the removed administrator.
+	//
+	// Selector: removeCollectionAdmin(address) fafd7b42
+	function removeCollectionAdmin(address admin) public {
+		require(false, stub_error);
+		admin;
+		dummy = 0;
+	}
+
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'
+	//
+	// Selector: setCollectionNesting(bool) 112d4586
+	function setCollectionNesting(bool enable) public {
+		require(false, stub_error);
+		enable;
+		dummy = 0;
+	}
+
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'
+	// @param collections Addresses of collections that will be available for nesting.
+	//
+	// Selector: setCollectionNesting(bool,address[]) 64872396
+	function setCollectionNesting(bool enable, address[] memory collections)
+		public
+	{
+		require(false, stub_error);
+		enable;
+		collections;
+		dummy = 0;
+	}
+
+	// Set the collection access method.
+	// @param mode Access mode
+	// 	0 for Normal
+	// 	1 for AllowList
+	//
+	// Selector: setCollectionAccess(uint8) 41835d4c
+	function setCollectionAccess(uint8 mode) public {
+		require(false, stub_error);
+		mode;
+		dummy = 0;
+	}
+
+	// Add the user to the allowed list.
+	//
+	// @param user Address of a trusted user.
+	//
+	// Selector: addToCollectionAllowList(address) 67844fe6
+	function addToCollectionAllowList(address user) public {
+		require(false, stub_error);
+		user;
+		dummy = 0;
+	}
+
+	// Remove the user from the allowed list.
+	//
+	// @param user Address of a removed user.
+	//
+	// Selector: removeFromCollectionAllowList(address) 85c51acb
+	function removeFromCollectionAllowList(address user) public {
+		require(false, stub_error);
+		user;
+		dummy = 0;
+	}
+
+	// Switch permission for minting.
+	//
+	// @param mode Enable if "true".
+	//
+	// Selector: setCollectionMintMode(bool) 00018e84
+	function setCollectionMintMode(bool mode) public {
+		require(false, stub_error);
+		mode;
+		dummy = 0;
+	}
+}
+
 // Selector: 942e8b22
 contract ERC20 is Dummy, ERC165, ERC20Events {
 	// Selector: name() 06fdde03
@@ -124,76 +351,6 @@
 		spender;
 		dummy;
 		return 0;
-	}
-}
-
-// Selector: c894dc35
-contract Collection is Dummy, ERC165 {
-	// Selector: setCollectionProperty(string,bytes) 2f073f66
-	function setCollectionProperty(string memory key, bytes memory value)
-		public
-	{
-		require(false, stub_error);
-		key;
-		value;
-		dummy = 0;
-	}
-
-	// Selector: deleteCollectionProperty(string) 7b7debce
-	function deleteCollectionProperty(string memory key) public {
-		require(false, stub_error);
-		key;
-		dummy = 0;
-	}
-
-	// Throws error if key not found
-	//
-	// Selector: collectionProperty(string) cf24fd6d
-	function collectionProperty(string memory key)
-		public
-		view
-		returns (bytes memory)
-	{
-		require(false, stub_error);
-		key;
-		dummy;
-		return hex"";
-	}
-
-	// Selector: ethSetSponsor(address) 8f9af356
-	function ethSetSponsor(address sponsor) public {
-		require(false, stub_error);
-		sponsor;
-		dummy = 0;
-	}
-
-	// Selector: ethConfirmSponsorship() a8580d1a
-	function ethConfirmSponsorship() public {
-		require(false, stub_error);
-		dummy = 0;
-	}
-
-	// Selector: setLimit(string,uint32) 68db30ca
-	function setLimit(string memory limit, uint32 value) public {
-		require(false, stub_error);
-		limit;
-		value;
-		dummy = 0;
-	}
-
-	// Selector: setLimit(string,bool) ea67e4c2
-	function setLimit(string memory limit, bool value) public {
-		require(false, stub_error);
-		limit;
-		value;
-		dummy = 0;
-	}
-
-	// Selector: contractAddress() f6b4dfb4
-	function contractAddress() public view returns (address) {
-		require(false, stub_error);
-		dummy;
-		return 0x0000000000000000000000000000000000000000;
 	}
 }
 
modifiedpallets/inflation/src/lib.rsdiffbeforeafterboth
--- a/pallets/inflation/src/lib.rs
+++ b/pallets/inflation/src/lib.rs
@@ -14,6 +14,20 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Inflation
+//!
+//! The inflation pallet is designed to increase the number of tokens at certain intervals.
+//! With each iteration, increases the `total_issuance` value for the native token.
+//! Executing an `on_initialize` hook at the beginning of each block, causing inflation to begin.
+//!
+//! ## Interface
+//!
+//! ### Dispatchable Functions
+//!
+//! * `start_inflation` - This method sets the inflation start date. Can be only called once.
+//! Inflation start block can be backdated and will catch up. The method will create Treasury
+//!	account if it does not exist and perform the first inflation deposit.
+
 // #![recursion_limit = "1024"]
 #![cfg_attr(not(feature = "std"), no_std)]
 
addedpallets/nonfungible/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/nonfungible/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.1] - 2022-07-14
+
+### Added
+
+- Implementation of RPC method `token_owners`.
+   For reasons of compatibility with this pallet, returns only one owner if token exists.
+   This was an internal request to improve the web interface and support fractionalization event. 
\ No newline at end of file
modifiedpallets/nonfungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/nonfungible/Cargo.toml
+++ b/pallets/nonfungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-nonfungible"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/nonfungible/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/benchmarking.rs
+++ b/pallets/nonfungible/src/benchmarking.rs
@@ -183,7 +183,7 @@
 			value: property_value(),
 		}).collect::<Vec<_>>();
 		let item = create_max_item(&collection, &owner, owner.clone())?;
-	}: {<Pallet<T>>::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?}
+	}: {<Pallet<T>>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?}
 
 	delete_token_properties {
 		let b in 0..MAX_PROPERTIES_PER_ITEM;
@@ -205,7 +205,7 @@
 			value: property_value(),
 		}).collect::<Vec<_>>();
 		let item = create_max_item(&collection, &owner, owner.clone())?;
-		<Pallet<T>>::set_token_properties(&collection, &owner, item, props, false, &Unlimited)?;
+		<Pallet<T>>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?;
 		let to_delete = (0..b).map(|k| property_key(k as usize)).collect::<Vec<_>>();
-	}: {<Pallet<T>>::delete_token_properties(&collection, &owner, item, to_delete, &Unlimited)?}
+	}: {<Pallet<T>>::delete_token_properties(&collection, &owner, item, to_delete.into_iter(), &Unlimited)?}
 }
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -26,7 +26,7 @@
 	weights::WeightInfo as _,
 };
 use sp_runtime::DispatchError;
-use sp_std::vec::Vec;
+use sp_std::{vec::Vec, vec};
 
 use crate::{
 	AccountBalance, Allowance, Config, CreateItemData, Error, NonfungibleHandle, Owned, Pallet,
@@ -133,6 +133,8 @@
 	}
 }
 
+/// Implementation of `CommonCollectionOperations` for `NonfungibleHandle`. It wraps Nonfungible Pallete
+/// methods and adds weight info.
 impl<T: Config> CommonCollectionOperations<T> for NonfungibleHandle<T> {
 	fn create_item(
 		&self,
@@ -420,6 +422,11 @@
 		<TokenData<T>>::get((self.id, token)).map(|t| t.owner)
 	}
 
+	/// Returns token owners.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId> {
+		self.token_owner(token).map_or_else(|| vec![], |t| vec![t])
+	}
+
 	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue> {
 		<Pallet<T>>::token_properties((self.id, token_id))
 			.get(key)
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -14,6 +14,11 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Nonfungible Pallet EVM API
+//!
+//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Nonfungible Pallet.
+//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.
+
 extern crate alloc;
 use core::{
 	char::{REPLACEMENT_CHARACTER, decode_utf16},
@@ -40,8 +45,15 @@
 	SelfWeightOf, weights::WeightInfo, TokenProperties,
 };
 
+/// @title A contract that allows to set and delete token properties and change token property permissions.
 #[solidity_interface(name = "TokenProperties")]
 impl<T: Config> NonfungibleHandle<T> {
+	/// @notice Set permissions for token property.
+	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	/// @param key Property key.
+	/// @param is_mutable Permission to mutate property.
+	/// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	/// @param token_owner Permission to mutate property by token owner if property is mutable.
 	fn set_token_property_permission(
 		&mut self,
 		caller: caller,
@@ -68,6 +80,11 @@
 		.map_err(dispatch_to_evm::<T>)
 	}
 
+	/// @notice Set token property value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param key Property key.
+	/// @param value Property value.
 	fn set_property(
 		&mut self,
 		caller: caller,
@@ -96,6 +113,10 @@
 		.map_err(dispatch_to_evm::<T>)
 	}
 
+	/// @notice Delete token property value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param key Property key.
 	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {
 		let caller = T::CrossAccountId::from_eth(caller);
 		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
@@ -111,7 +132,11 @@
 			.map_err(dispatch_to_evm::<T>)
 	}
 
-	/// Throws error if key not found
+	/// @notice Get token property value.
+	/// @dev Throws error if key not found
+	/// @param tokenId ID of the token.
+	/// @param key Property key.
+	/// @return Property value bytes
 	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {
 		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
 		let key = <Vec<u8>>::from(key)
@@ -127,6 +152,11 @@
 
 #[derive(ToLog)]
 pub enum ERC721Events {
+	/// @dev This emits when ownership of any NFT changes by any mechanism.
+	///  This event emits when NFTs are created (`from` == 0) and destroyed
+	///  (`to` == 0). Exception: during contract creation, any number of NFTs
+	///  may be created and assigned without emitting Transfer. At the time of
+	///  any transfer, the approved address for that NFT (if any) is reset to none.
 	Transfer {
 		#[indexed]
 		from: address,
@@ -135,6 +165,10 @@
 		#[indexed]
 		token_id: uint256,
 	},
+	/// @dev This emits when the approved address for an NFT is changed or
+	///  reaffirmed. The zero address indicates there is no approved address.
+	///  When a Transfer event emits, this also indicates that the approved
+	///  address for that NFT (if any) is reset to none.
 	Approval {
 		#[indexed]
 		owner: address,
@@ -143,6 +177,8 @@
 		#[indexed]
 		token_id: uint256,
 	},
+	/// @dev This emits when an operator is enabled or disabled for an owner.
+	///  The operator can manage all NFTs of the owner.
 	#[allow(dead_code)]
 	ApprovalForAll {
 		#[indexed]
@@ -159,19 +195,27 @@
 	MintingFinished {},
 }
 
+/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
+/// @dev See https://eips.ethereum.org/EIPS/eip-721
 #[solidity_interface(name = "ERC721Metadata")]
 impl<T: Config> NonfungibleHandle<T> {
+	/// @notice A descriptive name for a collection of NFTs in this contract
 	fn name(&self) -> Result<string> {
 		Ok(decode_utf16(self.name.iter().copied())
 			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
 			.collect::<string>())
 	}
 
+	/// @notice An abbreviated name for NFTs in this contract
 	fn symbol(&self) -> Result<string> {
 		Ok(string::from_utf8_lossy(&self.token_prefix).into())
 	}
 
-	/// Returns token's const_metadata
+	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	/// @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC
+	///  3986. The URI may point to a JSON file that conforms to the "ERC721
+	///  Metadata JSON Schema".
+	/// @return token's const_metadata
 	#[solidity(rename_selector = "tokenURI")]
 	fn token_uri(&self, token_id: uint256) -> Result<string> {
 		let key = token_uri_key();
@@ -192,32 +236,53 @@
 	}
 }
 
+/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
+/// @dev See https://eips.ethereum.org/EIPS/eip-721
 #[solidity_interface(name = "ERC721Enumerable")]
 impl<T: Config> NonfungibleHandle<T> {
+	/// @notice Enumerate valid NFTs
+	/// @param index A counter less than `totalSupply()`
+	/// @return The token identifier for the `index`th NFT,
+	///  (sort order not specified)
 	fn token_by_index(&self, index: uint256) -> Result<uint256> {
 		Ok(index)
 	}
 
-	/// Not implemented
+	/// @dev Not implemented
 	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {
 		// TODO: Not implemetable
 		Err("not implemented".into())
 	}
 
+	/// @notice Count NFTs tracked by this contract
+	/// @return A count of valid NFTs tracked by this contract, where each one of
+	///  them has an assigned and queryable owner not equal to the zero address
 	fn total_supply(&self) -> Result<uint256> {
 		self.consume_store_reads(1)?;
 		Ok(<Pallet<T>>::total_supply(self).into())
 	}
 }
 
+/// @title ERC-721 Non-Fungible Token Standard
+/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
 #[solidity_interface(name = "ERC721", events(ERC721Events))]
 impl<T: Config> NonfungibleHandle<T> {
+	/// @notice Count all NFTs assigned to an owner
+	/// @dev NFTs assigned to the zero address are considered invalid, and this
+	///  function throws for queries about the zero address.
+	/// @param owner An address for whom to query the balance
+	/// @return The number of NFTs owned by `owner`, possibly zero
 	fn balance_of(&self, owner: address) -> Result<uint256> {
 		self.consume_store_reads(1)?;
 		let owner = T::CrossAccountId::from_eth(owner);
 		let balance = <AccountBalance<T>>::get((self.id, owner));
 		Ok(balance.into())
 	}
+	/// @notice Find the owner of an NFT
+	/// @dev NFTs assigned to zero address are considered invalid, and queries
+	///  about them do throw.
+	/// @param tokenId The identifier for an NFT
+	/// @return The address of the owner of the NFT
 	fn owner_of(&self, token_id: uint256) -> Result<address> {
 		self.consume_store_reads(1)?;
 		let token: TokenId = token_id.try_into()?;
@@ -226,7 +291,7 @@
 			.owner
 			.as_eth())
 	}
-	/// Not implemented
+	/// @dev Not implemented
 	fn safe_transfer_from_with_data(
 		&mut self,
 		_from: address,
@@ -238,7 +303,7 @@
 		// TODO: Not implemetable
 		Err("not implemented".into())
 	}
-	/// Not implemented
+	/// @dev Not implemented
 	fn safe_transfer_from(
 		&mut self,
 		_from: address,
@@ -250,6 +315,16 @@
 		Err("not implemented".into())
 	}
 
+	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
+	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	///  THEY MAY BE PERMANENTLY LOST
+	/// @dev Throws unless `msg.sender` is the current owner or an authorized
+	///  operator for this NFT. Throws if `from` is not the current owner. Throws
+	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.
+	/// @param from The current owner of the NFT
+	/// @param to The new owner
+	/// @param tokenId The NFT to transfer
+	/// @param _value Not used for an NFT
 	#[weight(<SelfWeightOf<T>>::transfer_from())]
 	fn transfer_from(
 		&mut self,
@@ -272,6 +347,12 @@
 		Ok(())
 	}
 
+	/// @notice Set or reaffirm the approved address for an NFT
+	/// @dev The zero address indicates there is no approved address.
+	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
+	///  operator of the current owner.
+	/// @param approved The new approved NFT controller
+	/// @param tokenId The NFT to approve
 	#[weight(<SelfWeightOf<T>>::approve())]
 	fn approve(
 		&mut self,
@@ -289,7 +370,7 @@
 		Ok(())
 	}
 
-	/// Not implemented
+	/// @dev Not implemented
 	fn set_approval_for_all(
 		&mut self,
 		_caller: caller,
@@ -300,21 +381,26 @@
 		Err("not implemented".into())
 	}
 
-	/// Not implemented
+	/// @dev Not implemented
 	fn get_approved(&self, _token_id: uint256) -> Result<address> {
 		// TODO: Not implemetable
 		Err("not implemented".into())
 	}
 
-	/// Not implemented
+	/// @dev Not implemented
 	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {
 		// TODO: Not implemetable
 		Err("not implemented".into())
 	}
 }
 
+/// @title ERC721 Token that can be irreversibly burned (destroyed).
 #[solidity_interface(name = "ERC721Burnable")]
 impl<T: Config> NonfungibleHandle<T> {
+	/// @notice Burns a specific ERC721 token.
+	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
+	///  operator of the current owner.
+	/// @param tokenId The NFT to approve
 	#[weight(<SelfWeightOf<T>>::burn_item())]
 	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {
 		let caller = T::CrossAccountId::from_eth(caller);
@@ -325,14 +411,18 @@
 	}
 }
 
+/// @title ERC721 minting logic.
 #[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]
 impl<T: Config> NonfungibleHandle<T> {
 	fn minting_finished(&self) -> Result<bool> {
 		Ok(false)
 	}
 
-	/// `token_id` should be obtained with `next_token_id` method,
-	/// unlike standard, you can't specify it manually
+	/// @notice Function to mint token.
+	/// @dev `tokenId` should be obtained with `nextTokenId` method,
+	///  unlike standard, you can't specify it manually
+	/// @param to The new owner
+	/// @param tokenId ID of the minted NFT
 	#[weight(<SelfWeightOf<T>>::create_item())]
 	fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {
 		let caller = T::CrossAccountId::from_eth(caller);
@@ -364,8 +454,12 @@
 		Ok(true)
 	}
 
-	/// `token_id` should be obtained with `next_token_id` method,
-	/// unlike standard, you can't specify it manually
+	/// @notice Function to mint token with the given tokenUri.
+	/// @dev `tokenId` should be obtained with `nextTokenId` method,
+	///  unlike standard, you can't specify it manually
+	/// @param to The new owner
+	/// @param tokenId ID of the minted NFT
+	/// @param tokenUri Token URI that would be stored in the NFT properties
 	#[solidity(rename_selector = "mintWithTokenURI")]
 	#[weight(<SelfWeightOf<T>>::create_item())]
 	fn mint_with_token_uri(
@@ -420,7 +514,7 @@
 		Ok(true)
 	}
 
-	/// Not implemented
+	/// @dev Not implemented
 	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {
 		Err("not implementable".into())
 	}
@@ -449,8 +543,15 @@
 	false
 }
 
+/// @title Unique extensions for ERC721.
 #[solidity_interface(name = "ERC721UniqueExtensions")]
 impl<T: Config> NonfungibleHandle<T> {
+	/// @notice Transfer ownership of an NFT
+	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	///  is the zero address. Throws if `tokenId` is not a valid NFT.
+	/// @param to The new owner
+	/// @param tokenId The NFT to transfer
+	/// @param _value Not used for an NFT
 	#[weight(<SelfWeightOf<T>>::transfer())]
 	fn transfer(
 		&mut self,
@@ -470,6 +571,13 @@
 		Ok(())
 	}
 
+	/// @notice Burns a specific ERC721 token.
+	/// @dev Throws unless `msg.sender` is the current owner or an authorized
+	///  operator for this NFT. Throws if `from` is not the current owner. Throws
+	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.
+	/// @param from The current owner of the NFT
+	/// @param tokenId The NFT to transfer
+	/// @param _value Not used for an NFT
 	#[weight(<SelfWeightOf<T>>::burn_from())]
 	fn burn_from(
 		&mut self,
@@ -490,6 +598,7 @@
 		Ok(())
 	}
 
+	/// @notice Returns next free NFT ID.
 	fn next_token_id(&self) -> Result<uint256> {
 		self.consume_store_reads(1)?;
 		Ok(<TokensMinted<T>>::get(self.id)
@@ -498,6 +607,11 @@
 			.into())
 	}
 
+	/// @notice Function to mint multiple tokens.
+	/// @dev `tokenIds` should be an array of consecutive numbers and first number
+	///  should be obtained with `nextTokenId` method
+	/// @param to The new owner
+	/// @param tokenIds IDs of the minted NFTs
 	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]
 	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {
 		let caller = T::CrossAccountId::from_eth(caller);
@@ -529,6 +643,11 @@
 		Ok(true)
 	}
 
+	/// @notice Function to mint multiple tokens with the given tokenUris.
+	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	///  numbers and first number should be obtained with `nextTokenId` method
+	/// @param to The new owner
+	/// @param tokens array of pairs of token ID and token URI for minted tokens
 	#[solidity(rename_selector = "mintBulkWithTokenURI")]
 	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]
 	fn mint_bulk_with_token_uri(
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -14,6 +14,80 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Nonfungible Pallet
+//!
+//! The Nonfungible pallet provides functionality for handling nonfungible collections and tokens.
+//!
+//! - [`Config`]
+//! - [`NonfungibleHandle`]
+//! - [`Pallet`]
+//! - [`CommonWeights`](common::CommonWeights)
+//!
+//! ## Overview
+//!
+//! The Nonfungible pallet provides functions for:
+//!
+//! - NFT collection creation and removal
+//! - Minting and burning of NFT tokens
+//! - Retrieving account balances
+//! - Transfering NFT tokens
+//! - Setting and checking allowance for NFT tokens
+//! - Setting properties and permissions for NFT collections and tokens
+//! - Nesting and unnesting tokens
+//!
+//! ### Terminology
+//!
+//! - **NFT token:** Non fungible token.
+//!
+//! - **NFT Collection:** A collection of NFT tokens. All NFT tokens are part of a collection.
+//!   Each collection can define it's own properties, properties for it's tokens and set of permissions.
+//!
+//! - **Balance:** Number of NFT tokens owned by an account
+//!
+//! - **Allowance:** NFT tokens owned by one account that another account is allowed to make operations on
+//!
+//! - **Burning:** The process of “deleting” a token from a collection and from
+//!   an account balance of the owner.
+//!
+//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting
+//!   owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in
+//!   it's child token i.e. parent-child relationship graph shouldn't have cycles.
+//!
+//! - **Properties:** Key-Values pairs. Token properties are attached to a token. Collection properties are
+//!   attached to a collection. Set of permissions could be defined for each property.
+//!
+//! ### Implementations
+//!
+//! The Nonfungible pallet provides implementations for the following traits. If these traits provide
+//! the functionality that you need, then you can avoid coupling with the Nonfungible pallet.
+//!
+//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight
+//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing
+//!   with collections
+//!
+//! ## Interface
+//!
+//! ### Dispatchable Functions
+//!
+//! - `init_collection` - Create NFT collection. NFT collection can be configured to allow or deny access for
+//!   some accounts.
+//! - `destroy_collection` - Destroy exising NFT collection. There should be no tokens in the collection.
+//! - `burn` - Burn NFT token owned by account.
+//! - `transfer` - Transfer NFT token. Transfers should be enabled for NFT collection.
+//!   Nests the NFT token if it is sent to another token.
+//! - `create_item` - Mint NFT token in collection. Sender should have permission to mint tokens.
+//! - `set_allowance` - Set allowance for another account.
+//! - `set_token_property` - Set token property value.
+//! - `delete_token_property` - Remove property from the token.
+//! - `set_collection_properties` - Set collection properties.
+//! - `delete_collection_properties` - Remove properties from the collection.
+//! - `set_property_permission` - Set collection property permission.
+//! - `set_token_property_permissions` - Set token property permissions.
+//!
+//! ## Assumptions
+//!
+//! * To perform operations on tokens sender should be in collection's allow list if collection access mode is `AllowList`.
+
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use erc::ERC721Events;
@@ -56,8 +130,8 @@
 pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;
 pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;
 
-/// Token data, stored independently from other data used to describe it.
-/// Notably contains the owner account address.
+/// Token data, stored independently from other data used to describe it
+/// for the convenience of database access. Notably contains the owner account address.
 #[struct_versioning::versioned(version = 2, upper)]
 #[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
 pub struct ItemData<CrossAccountId> {
@@ -122,7 +196,7 @@
 		QueryKind = OptionQuery,
 	>;
 
-	/// Key-value pairs, describing the metadata of a token.
+	/// Map of key-value pairs, describing the metadata of a token.
 	#[pallet::storage]
 	#[pallet::getter(fn token_properties)]
 	pub type TokenProperties<T: Config> = StorageNMap<
@@ -132,7 +206,15 @@
 		OnEmpty = up_data_structs::TokenProperties,
 	>;
 
-	/// Scoped, auxiliary properties of a token, primarily used for on-chain operations.
+	/// Custom data of a token that is serialized to bytes, 
+	/// primarily reserved for on-chain operations, 
+	/// normally obscured from the external users.
+	/// 
+	/// Auxiliary properties are slightly different from 
+	/// usual [`TokenProperties`] due to an unlimited number 
+	/// and separately stored and written-to key-value pairs.
+	/// 
+	/// Currently used to store RMRK data.
 	#[pallet::storage]
 	#[pallet::getter(fn token_aux_property)]
 	pub type TokenAuxProperties<T: Config> = StorageNMap<
@@ -171,7 +253,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// Amount of tokens owned in a collection.
+	/// Amount of tokens owned by an account in a collection.
 	#[pallet::storage]
 	pub type AccountBalance<T: Config> = StorageNMap<
 		Key = (
@@ -182,7 +264,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// todo:doc
+	/// Allowance set by a token owner for another user to perform one of certain transactions on a token.
 	#[pallet::storage]
 	pub type Allowance<T: Config> = StorageNMap<
 		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),
@@ -284,13 +366,21 @@
 }
 
 impl<T: Config> Pallet<T> {
+	/// Get number of NFT tokens in collection.
 	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {
 		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)
 	}
+
+	/// Check that NFT token exists.
+	///
+	/// - `token`: Token ID.
 	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {
 		<TokenData<T>>::contains_key((collection.id, token))
 	}
 
+	/// Set the token property with the scope.
+	///
+	/// - `property`: Contains key-value pair.
 	pub fn set_scoped_token_property(
 		collection_id: CollectionId,
 		token_id: TokenId,
@@ -305,6 +395,7 @@
 		Ok(())
 	}
 
+	/// Batch operation to set multiple properties with the same scope.
 	pub fn set_scoped_token_properties(
 		collection_id: CollectionId,
 		token_id: TokenId,
@@ -319,6 +410,9 @@
 		Ok(())
 	}
 
+	/// Add or edit auxiliary data for the property.
+	///
+	/// - `f`: function that adds or edits auxiliary data.
 	pub fn try_mutate_token_aux_property<R, E>(
 		collection_id: CollectionId,
 		token_id: TokenId,
@@ -329,6 +423,7 @@
 		<TokenAuxProperties<T>>::try_mutate((collection_id, token_id, scope, key), f)
 	}
 
+	/// Remove auxiliary data for the property.
 	pub fn remove_token_aux_property(
 		collection_id: CollectionId,
 		token_id: TokenId,
@@ -338,6 +433,9 @@
 		<TokenAuxProperties<T>>::remove((collection_id, token_id, scope, key));
 	}
 
+	/// Get all auxiliary data in a given scope.
+	///
+	/// Returns iterator over Property Key - Data pairs.
 	pub fn iterate_token_aux_properties(
 		collection_id: CollectionId,
 		token_id: TokenId,
@@ -346,6 +444,7 @@
 		<TokenAuxProperties<T>>::iter_prefix((collection_id, token_id, scope))
 	}
 
+	/// Get ID of the last minted token
 	pub fn current_token_id(collection_id: CollectionId) -> TokenId {
 		TokenId(<TokensMinted<T>>::get(collection_id))
 	}
@@ -353,6 +452,11 @@
 
 // unchecked calls skips any permission checks
 impl<T: Config> Pallet<T> {
+	/// Create NFT collection
+	///
+	/// `init_collection` will take non-refundable deposit for collection creation.
+	///
+	/// - `data`: Contains settings for collection limits and permissions.
 	pub fn init_collection(
 		owner: T::CrossAccountId,
 		data: CreateCollectionData<T::AccountId>,
@@ -360,6 +464,11 @@
 	) -> Result<CollectionId, DispatchError> {
 		<PalletCommon<T>>::init_collection(owner, data, is_external)
 	}
+
+	/// Destroy NFT collection
+	///
+	/// `destroy_collection` will throw error if collection contains any tokens.
+	/// Only owner can destroy collection.
 	pub fn destroy_collection(
 		collection: NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -384,6 +493,15 @@
 		Ok(())
 	}
 
+	/// Burn NFT token
+	///
+	/// `burn` removes `token` from the `collection`, from it's owner and from the parent token
+	/// if the token is nested.
+	/// Only the owner can `burn` the token. The `token` shouldn't have any nested tokens.
+	/// Also removes all corresponding properties and auxiliary properties.
+	///
+	/// - `token`: Token that should be burned
+	/// - `collection`: Collection that contains the token
 	pub fn burn(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -453,6 +571,12 @@
 		Ok(())
 	}
 
+	/// Same as [`burn`] but burns all the tokens that are nested in the token first
+	///
+	/// - `self_budget`: Limit for searching children in depth.
+	/// - `breadth_budget`: Limit of breadth of searching children.
+	///
+	/// [`burn`]: struct.Pallet.html#method.burn
 	#[transactional]
 	pub fn burn_recursively(
 		collection: &NonfungibleHandle<T>,
@@ -492,6 +616,14 @@
 		})
 	}
 
+	/// Batch operation to add, edit or remove properties for the token
+	///
+	/// All affected properties should have mutable permission and sender should have
+	/// permission to edit those properties.
+	///
+	/// - `nesting_budget`: Limit for searching parents in depth to check ownership.
+	/// - `is_token_create`: Indicates that method is called during token initialization.
+	///   Allows to bypass ownership check.
 	#[transactional]
 	fn modify_token_properties(
 		collection: &NonfungibleHandle<T>,
@@ -585,6 +717,11 @@
 		Ok(())
 	}
 
+	/// Batch operation to add or edit properties for the token
+	///
+	/// Same as [`modify_token_properties`] but doesn't allow to remove properties
+	///
+	/// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties
 	pub fn set_token_properties(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -603,6 +740,11 @@
 		)
 	}
 
+	/// Add or edit single property for the token
+	///
+	/// Calls [`set_token_properties`] internally
+	///
+	/// [`set_token_properties`]: struct.Pallet.html#method.set_token_properties
 	pub fn set_token_property(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -622,6 +764,11 @@
 		)
 	}
 
+	/// Batch operation to remove properties from the token
+	///
+	/// Same as [`modify_token_properties`] but doesn't allow to add or edit properties
+	///
+	/// [`modify_token_properties`]: struct.Pallet.html#method.modify_token_properties
 	pub fn delete_token_properties(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -641,6 +788,11 @@
 		)
 	}
 
+	/// Remove single property from the token
+	///
+	/// Calls [`delete_token_properties`] internally
+	///
+	/// [`delete_token_properties`]: struct.Pallet.html#method.delete_token_properties
 	pub fn delete_token_property(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -657,6 +809,7 @@
 		)
 	}
 
+	/// Add or edit properties for the collection
 	pub fn set_collection_properties(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -665,6 +818,7 @@
 		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)
 	}
 
+	/// Remove properties from the collection
 	pub fn delete_collection_properties(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -673,6 +827,9 @@
 		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)
 	}
 
+	/// Set property permissions for the token.
+	///
+	/// Sender should be the owner or admin of token's collection.
 	pub fn set_token_property_permissions(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -681,6 +838,9 @@
 		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)
 	}
 
+	/// Set property permissions for the collection.
+	///
+	/// Sender should be the owner or admin of the collection.
 	pub fn set_property_permission(
 		collection: &CollectionHandle<T>,
 		sender: &T::CrossAccountId,
@@ -689,6 +849,15 @@
 		<PalletCommon<T>>::set_property_permission(collection, sender, permission)
 	}
 
+	/// Transfer NFT token from one account to another.
+	///
+	/// `from` account stops being the owner and `to` account becomes the owner of the token.
+	/// If `to` is token than `to` becomes owner of the token and the token become nested.
+	/// Unnests token from previous parent if it was nested before.
+	/// Removes allowance for the token if there was any.
+	/// Throws if transfers aren't allowed for collection or if receiver reached token ownership limit.
+	///
+	/// - `nesting_budget`: Limit for token nesting depth
 	pub fn transfer(
 		collection: &NonfungibleHandle<T>,
 		from: &T::CrossAccountId,
@@ -780,6 +949,16 @@
 		Ok(())
 	}
 
+	/// Batch operation to mint multiple NFT tokens.
+	///
+	/// The sender should be the owner/admin of the collection or collection should be configured
+	/// to allow public minting.
+	/// Throws if amount of tokens reached it's limit for the collection or if caller reached
+	/// token ownership limit.
+	///
+	/// - `data`: Contains list of token properties and users who will become the owners of the
+	///   corresponging tokens.
+	/// - `nesting_budget`: Limit for token nesting depth
 	pub fn create_multiple_items(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -964,6 +1143,9 @@
 		}
 	}
 
+	/// Set allowance for the spender to `transfer` or `burn` sender's token.
+	///
+	/// - `token`: Token the spender is allowed to `transfer` or `burn`.
 	pub fn set_allowance(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -996,6 +1178,7 @@
 		Ok(())
 	}
 
+	/// Checks allowance for the spender to use the token.
 	fn check_allowed(
 		collection: &NonfungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -1038,6 +1221,12 @@
 		Ok(())
 	}
 
+	/// Transfer NFT token from one account to another.
+	///
+	/// Same as the [`transfer`] but spender doesn't needs to be the owner of the token.
+	/// The owner should set allowance for the spender to transfer token.
+	///
+	/// [`transfer`]: struct.Pallet.html#method.transfer
 	pub fn transfer_from(
 		collection: &NonfungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -1054,6 +1243,12 @@
 		Self::transfer(collection, from, to, token, nesting_budget)
 	}
 
+	/// Burn NFT token for `from` account.
+	///
+	/// Same as the [`burn`] but spender doesn't need to be an owner of the token. The owner should
+	/// set allowance for the spender to burn token.
+	///
+	/// [`burn`]: struct.Pallet.html#method.burn
 	pub fn burn_from(
 		collection: &NonfungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -1068,6 +1263,8 @@
 		Self::burn(collection, from, token)
 	}
 
+	/// Check that `from` token could be nested in `under` token.
+	///
 	pub fn check_nesting(
 		handle: &NonfungibleHandle<T>,
 		sender: T::CrossAccountId,
@@ -1137,7 +1334,11 @@
 			.collect()
 	}
 
-	/// Delegated to `create_multiple_items`
+	/// Mint single NFT token.
+	///
+	/// Delegated to [`create_multiple_items`]
+	///
+	/// [`create_multiple_items`]: struct.Pallet.html#method.create_multiple_items
 	pub fn create_item(
 		collection: &NonfungibleHandle<T>,
 		sender: &T::CrossAccountId,
modifiedpallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth
--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -53,6 +53,13 @@
 
 // Selector: 41369377
 contract TokenProperties is Dummy, ERC165 {
+	// @notice Set permissions for token property.
+	// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// @param key Property key.
+	// @param is_mutable Permission to mutate property.
+	// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	// @param token_owner Permission to mutate property by token owner if property is mutable.
+	//
 	// Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa
 	function setTokenPropertyPermission(
 		string memory key,
@@ -68,6 +75,12 @@
 		dummy = 0;
 	}
 
+	// @notice Set token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @param value Property value.
+	//
 	// Selector: setProperty(uint256,string,bytes) 1752d67b
 	function setProperty(
 		uint256 tokenId,
@@ -81,6 +94,11 @@
 		dummy = 0;
 	}
 
+	// @notice Delete token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	//
 	// Selector: deleteProperty(uint256,string) 066111d1
 	function deleteProperty(uint256 tokenId, string memory key) public {
 		require(false, stub_error);
@@ -89,7 +107,11 @@
 		dummy = 0;
 	}
 
-	// Throws error if key not found
+	// @notice Get token property value.
+	// @dev Throws error if key not found
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @return Property value bytes
 	//
 	// Selector: property(uint256,string) 7228c327
 	function property(uint256 tokenId, string memory key)
@@ -107,6 +129,11 @@
 
 // Selector: 42966c68
 contract ERC721Burnable is Dummy, ERC165 {
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
+	//  operator of the current owner.
+	// @param tokenId The NFT to approve
+	//
 	// Selector: burn(uint256) 42966c68
 	function burn(uint256 tokenId) public {
 		require(false, stub_error);
@@ -117,6 +144,12 @@
 
 // Selector: 58800161
 contract ERC721 is Dummy, ERC165, ERC721Events {
+	// @notice Count all NFTs assigned to an owner
+	// @dev NFTs assigned to the zero address are considered invalid, and this
+	//  function throws for queries about the zero address.
+	// @param owner An address for whom to query the balance
+	// @return The number of NFTs owned by `owner`, possibly zero
+	//
 	// Selector: balanceOf(address) 70a08231
 	function balanceOf(address owner) public view returns (uint256) {
 		require(false, stub_error);
@@ -125,6 +158,12 @@
 		return 0;
 	}
 
+	// @notice Find the owner of an NFT
+	// @dev NFTs assigned to zero address are considered invalid, and queries
+	//  about them do throw.
+	// @param tokenId The identifier for an NFT
+	// @return The address of the owner of the NFT
+	//
 	// Selector: ownerOf(uint256) 6352211e
 	function ownerOf(uint256 tokenId) public view returns (address) {
 		require(false, stub_error);
@@ -133,7 +172,7 @@
 		return 0x0000000000000000000000000000000000000000;
 	}
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
 	function safeTransferFromWithData(
@@ -150,7 +189,7 @@
 		dummy = 0;
 	}
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: safeTransferFrom(address,address,uint256) 42842e0e
 	function safeTransferFrom(
@@ -165,6 +204,17 @@
 		dummy = 0;
 	}
 
+	// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
+	//  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	//  THEY MAY BE PERMANENTLY LOST
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this NFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.
+	// @param from The current owner of the NFT
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
 	// Selector: transferFrom(address,address,uint256) 23b872dd
 	function transferFrom(
 		address from,
@@ -178,6 +228,13 @@
 		dummy = 0;
 	}
 
+	// @notice Set or reaffirm the approved address for an NFT
+	// @dev The zero address indicates there is no approved address.
+	// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
+	//  operator of the current owner.
+	// @param approved The new approved NFT controller
+	// @param tokenId The NFT to approve
+	//
 	// Selector: approve(address,uint256) 095ea7b3
 	function approve(address approved, uint256 tokenId) public {
 		require(false, stub_error);
@@ -186,7 +243,7 @@
 		dummy = 0;
 	}
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: setApprovalForAll(address,bool) a22cb465
 	function setApprovalForAll(address operator, bool approved) public {
@@ -196,7 +253,7 @@
 		dummy = 0;
 	}
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: getApproved(uint256) 081812fc
 	function getApproved(uint256 tokenId) public view returns (address) {
@@ -206,7 +263,7 @@
 		return 0x0000000000000000000000000000000000000000;
 	}
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: isApprovedForAll(address,address) e985e9c5
 	function isApprovedForAll(address owner, address operator)
@@ -224,6 +281,8 @@
 
 // Selector: 5b5e139f
 contract ERC721Metadata is Dummy, ERC165 {
+	// @notice A descriptive name for a collection of NFTs in this contract
+	//
 	// Selector: name() 06fdde03
 	function name() public view returns (string memory) {
 		require(false, stub_error);
@@ -231,6 +290,8 @@
 		return "";
 	}
 
+	// @notice An abbreviated name for NFTs in this contract
+	//
 	// Selector: symbol() 95d89b41
 	function symbol() public view returns (string memory) {
 		require(false, stub_error);
@@ -238,7 +299,11 @@
 		return "";
 	}
 
-	// Returns token's const_metadata
+	// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	// @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC
+	//  3986. The URI may point to a JSON file that conforms to the "ERC721
+	//  Metadata JSON Schema".
+	// @return token's const_metadata
 	//
 	// Selector: tokenURI(uint256) c87b56dd
 	function tokenURI(uint256 tokenId) public view returns (string memory) {
@@ -258,8 +323,11 @@
 		return false;
 	}
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted NFT
 	//
 	// Selector: mint(address,uint256) 40c10f19
 	function mint(address to, uint256 tokenId) public returns (bool) {
@@ -270,8 +338,12 @@
 		return false;
 	}
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token with the given tokenUri.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted NFT
+	// @param tokenUri Token URI that would be stored in the NFT properties
 	//
 	// Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
 	function mintWithTokenURI(
@@ -287,7 +359,7 @@
 		return false;
 	}
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: finishMinting() 7d64bcb4
 	function finishMinting() public returns (bool) {
@@ -299,6 +371,11 @@
 
 // Selector: 780e9d63
 contract ERC721Enumerable is Dummy, ERC165 {
+	// @notice Enumerate valid NFTs
+	// @param index A counter less than `totalSupply()`
+	// @return The token identifier for the `index`th NFT,
+	//  (sort order not specified)
+	//
 	// Selector: tokenByIndex(uint256) 4f6ccce7
 	function tokenByIndex(uint256 index) public view returns (uint256) {
 		require(false, stub_error);
@@ -307,7 +384,7 @@
 		return 0;
 	}
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
 	function tokenOfOwnerByIndex(address owner, uint256 index)
@@ -322,6 +399,10 @@
 		return 0;
 	}
 
+	// @notice Count NFTs tracked by this contract
+	// @return A count of valid NFTs tracked by this contract, where each one of
+	//  them has an assigned and queryable owner not equal to the zero address
+	//
 	// Selector: totalSupply() 18160ddd
 	function totalSupply() public view returns (uint256) {
 		require(false, stub_error);
@@ -332,6 +413,11 @@
 
 // Selector: 7d9262e6
 contract Collection is Dummy, ERC165 {
+	// Set collection property.
+	//
+	// @param key Property key.
+	// @param value Propery value.
+	//
 	// Selector: setCollectionProperty(string,bytes) 2f073f66
 	function setCollectionProperty(string memory key, bytes memory value)
 		public
@@ -342,6 +428,10 @@
 		dummy = 0;
 	}
 
+	// Delete collection property.
+	//
+	// @param key Property key.
+	//
 	// Selector: deleteCollectionProperty(string) 7b7debce
 	function deleteCollectionProperty(string memory key) public {
 		require(false, stub_error);
@@ -349,8 +439,13 @@
 		dummy = 0;
 	}
 
-	// Throws error if key not found
+	// Get collection property.
+	//
+	// @dev Throws error if key not found.
 	//
+	// @param key Property key.
+	// @return bytes The property corresponding to the key.
+	//
 	// Selector: collectionProperty(string) cf24fd6d
 	function collectionProperty(string memory key)
 		public
@@ -363,6 +458,12 @@
 		return hex"";
 	}
 
+	// Set the sponsor of the collection.
+	//
+	// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
+	//
+	// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.
+	//
 	// Selector: setCollectionSponsor(address) 7623402e
 	function setCollectionSponsor(address sponsor) public {
 		require(false, stub_error);
@@ -370,12 +471,27 @@
 		dummy = 0;
 	}
 
+	// Collection sponsorship confirmation.
+	//
+	// @dev After setting the sponsor for the collection, it must be confirmed with this function.
+	//
 	// Selector: confirmCollectionSponsorship() 3c50e97a
 	function confirmCollectionSponsorship() public {
 		require(false, stub_error);
 		dummy = 0;
 	}
 
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"accountTokenOwnershipLimit",
+	// 	"sponsoredDataSize",
+	// 	"sponsoredDataRateLimit",
+	// 	"tokenLimit",
+	// 	"sponsorTransferTimeout",
+	// 	"sponsorApproveTimeout"
+	// @param value Value of the limit.
+	//
 	// Selector: setCollectionLimit(string,uint32) 6a3841db
 	function setCollectionLimit(string memory limit, uint32 value) public {
 		require(false, stub_error);
@@ -384,6 +500,14 @@
 		dummy = 0;
 	}
 
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"ownerCanTransfer",
+	// 	"ownerCanDestroy",
+	// 	"transfersEnabled"
+	// @param value Value of the limit.
+	//
 	// Selector: setCollectionLimit(string,bool) 993b7fba
 	function setCollectionLimit(string memory limit, bool value) public {
 		require(false, stub_error);
@@ -392,6 +516,8 @@
 		dummy = 0;
 	}
 
+	// Get contract address.
+	//
 	// Selector: contractAddress() f6b4dfb4
 	function contractAddress() public view returns (address) {
 		require(false, stub_error);
@@ -399,34 +525,51 @@
 		return 0x0000000000000000000000000000000000000000;
 	}
 
+	// Add collection admin by substrate address.
+	// @param new_admin Substrate administrator address.
+	//
 	// Selector: addCollectionAdminSubstrate(uint256) 5730062b
-	function addCollectionAdminSubstrate(uint256 newAdmin) public view {
+	function addCollectionAdminSubstrate(uint256 newAdmin) public {
 		require(false, stub_error);
 		newAdmin;
-		dummy;
+		dummy = 0;
 	}
 
+	// Remove collection admin by substrate address.
+	// @param admin Substrate administrator address.
+	//
 	// Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9
-	function removeCollectionAdminSubstrate(uint256 newAdmin) public view {
+	function removeCollectionAdminSubstrate(uint256 admin) public {
 		require(false, stub_error);
-		newAdmin;
-		dummy;
+		admin;
+		dummy = 0;
 	}
 
+	// Add collection admin.
+	// @param new_admin Address of the added administrator.
+	//
 	// Selector: addCollectionAdmin(address) 92e462c7
-	function addCollectionAdmin(address newAdmin) public view {
+	function addCollectionAdmin(address newAdmin) public {
 		require(false, stub_error);
 		newAdmin;
-		dummy;
+		dummy = 0;
 	}
 
+	// Remove collection admin.
+	//
+	// @param new_admin Address of the removed administrator.
+	//
 	// Selector: removeCollectionAdmin(address) fafd7b42
-	function removeCollectionAdmin(address admin) public view {
+	function removeCollectionAdmin(address admin) public {
 		require(false, stub_error);
 		admin;
-		dummy;
+		dummy = 0;
 	}
 
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'
+	//
 	// Selector: setCollectionNesting(bool) 112d4586
 	function setCollectionNesting(bool enable) public {
 		require(false, stub_error);
@@ -434,6 +577,11 @@
 		dummy = 0;
 	}
 
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'
+	// @param collections Addresses of collections that will be available for nesting.
+	//
 	// Selector: setCollectionNesting(bool,address[]) 64872396
 	function setCollectionNesting(bool enable, address[] memory collections)
 		public
@@ -444,6 +592,11 @@
 		dummy = 0;
 	}
 
+	// Set the collection access method.
+	// @param mode Access mode
+	// 	0 for Normal
+	// 	1 for AllowList
+	//
 	// Selector: setCollectionAccess(uint8) 41835d4c
 	function setCollectionAccess(uint8 mode) public {
 		require(false, stub_error);
@@ -451,20 +604,32 @@
 		dummy = 0;
 	}
 
+	// Add the user to the allowed list.
+	//
+	// @param user Address of a trusted user.
+	//
 	// Selector: addToCollectionAllowList(address) 67844fe6
-	function addToCollectionAllowList(address user) public view {
+	function addToCollectionAllowList(address user) public {
 		require(false, stub_error);
 		user;
-		dummy;
+		dummy = 0;
 	}
 
+	// Remove the user from the allowed list.
+	//
+	// @param user Address of a removed user.
+	//
 	// Selector: removeFromCollectionAllowList(address) 85c51acb
-	function removeFromCollectionAllowList(address user) public view {
+	function removeFromCollectionAllowList(address user) public {
 		require(false, stub_error);
 		user;
-		dummy;
+		dummy = 0;
 	}
 
+	// Switch permission for minting.
+	//
+	// @param mode Enable if "true".
+	//
 	// Selector: setCollectionMintMode(bool) 00018e84
 	function setCollectionMintMode(bool mode) public {
 		require(false, stub_error);
@@ -475,6 +640,13 @@
 
 // Selector: d74d154f
 contract ERC721UniqueExtensions is Dummy, ERC165 {
+	// @notice Transfer ownership of an NFT
+	// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	//  is the zero address. Throws if `tokenId` is not a valid NFT.
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
 	// Selector: transfer(address,uint256) a9059cbb
 	function transfer(address to, uint256 tokenId) public {
 		require(false, stub_error);
@@ -483,6 +655,14 @@
 		dummy = 0;
 	}
 
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this NFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.
+	// @param from The current owner of the NFT
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
 	// Selector: burnFrom(address,uint256) 79cc6790
 	function burnFrom(address from, uint256 tokenId) public {
 		require(false, stub_error);
@@ -491,6 +671,8 @@
 		dummy = 0;
 	}
 
+	// @notice Returns next free NFT ID.
+	//
 	// Selector: nextTokenId() 75794a3c
 	function nextTokenId() public view returns (uint256) {
 		require(false, stub_error);
@@ -498,6 +680,12 @@
 		return 0;
 	}
 
+	// @notice Function to mint multiple tokens.
+	// @dev `tokenIds` should be an array of consecutive numbers and first number
+	//  should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokenIds IDs of the minted NFTs
+	//
 	// Selector: mintBulk(address,uint256[]) 44a9945e
 	function mintBulk(address to, uint256[] memory tokenIds)
 		public
@@ -510,6 +698,12 @@
 		return false;
 	}
 
+	// @notice Function to mint multiple tokens with the given tokenUris.
+	// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	//  numbers and first number should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokens array of pairs of token ID and token URI for minted tokens
+	//
 	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
 	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
 		public
modifiedpallets/nonfungible/src/weights.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/weights.rs
+++ b/pallets/nonfungible/src/weights.rs
@@ -3,7 +3,7 @@
 //! Autogenerated weights for pallet_nonfungible
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2022-06-15, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2022-07-20, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
 
 // Executed Command:
@@ -56,7 +56,7 @@
 	// Storage: Nonfungible TokenData (r:0 w:1)
 	// Storage: Nonfungible Owned (r:0 w:1)
 	fn create_item() -> Weight {
-		(24_135_000 as Weight)
+		(20_328_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(4 as Weight))
 	}
@@ -65,9 +65,9 @@
 	// Storage: Nonfungible TokenData (r:0 w:4)
 	// Storage: Nonfungible Owned (r:0 w:4)
 	fn create_multiple_items(b: u32, ) -> Weight {
-		(21_952_000 as Weight)
-			// Standard Error: 5_000
-			.saturating_add((4_727_000 as Weight).saturating_mul(b as Weight))
+		(10_134_000 as Weight)
+			// Standard Error: 3_000
+			.saturating_add((4_927_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(b as Weight)))
@@ -77,9 +77,9 @@
 	// Storage: Nonfungible TokenData (r:0 w:4)
 	// Storage: Nonfungible Owned (r:0 w:4)
 	fn create_multiple_items_ex(b: u32, ) -> Weight {
-		(10_432_000 as Weight)
-			// Standard Error: 6_000
-			.saturating_add((7_383_000 as Weight).saturating_mul(b as Weight))
+		(5_710_000 as Weight)
+			// Standard Error: 4_000
+			.saturating_add((7_578_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(T::DbWeight::get().reads(1 as Weight))
 			.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
@@ -93,7 +93,7 @@
 	// Storage: Nonfungible Owned (r:0 w:1)
 	// Storage: Nonfungible TokenProperties (r:0 w:1)
 	fn burn_item() -> Weight {
-		(29_798_000 as Weight)
+		(28_433_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(5 as Weight))
 			.saturating_add(T::DbWeight::get().writes(5 as Weight))
 	}
@@ -105,7 +105,7 @@
 	// Storage: Nonfungible Owned (r:0 w:1)
 	// Storage: Nonfungible TokenProperties (r:0 w:1)
 	fn burn_recursively_self_raw() -> Weight {
-		(37_955_000 as Weight)
+		(34_435_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(5 as Weight))
 			.saturating_add(T::DbWeight::get().writes(5 as Weight))
 	}
@@ -119,8 +119,8 @@
 	// Storage: Common CollectionById (r:1 w:0)
 	fn burn_recursively_breadth_plus_self_plus_self_per_each_raw(b: u32, ) -> Weight {
 		(0 as Weight)
-			// Standard Error: 1_349_000
-			.saturating_add((275_145_000 as Weight).saturating_mul(b as Weight))
+			// Standard Error: 1_539_000
+			.saturating_add((304_456_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(T::DbWeight::get().reads(7 as Weight))
 			.saturating_add(T::DbWeight::get().reads((4 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(T::DbWeight::get().writes(6 as Weight))
@@ -131,14 +131,14 @@
 	// Storage: Nonfungible Allowance (r:1 w:0)
 	// Storage: Nonfungible Owned (r:0 w:2)
 	fn transfer() -> Weight {
-		(27_867_000 as Weight)
+		(24_376_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(4 as Weight))
 			.saturating_add(T::DbWeight::get().writes(5 as Weight))
 	}
 	// Storage: Nonfungible TokenData (r:1 w:0)
 	// Storage: Nonfungible Allowance (r:1 w:1)
 	fn approve() -> Weight {
-		(18_824_000 as Weight)
+		(15_890_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
 	}
@@ -147,7 +147,7 @@
 	// Storage: Nonfungible AccountBalance (r:2 w:2)
 	// Storage: Nonfungible Owned (r:0 w:2)
 	fn transfer_from() -> Weight {
-		(32_879_000 as Weight)
+		(28_634_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(4 as Weight))
 			.saturating_add(T::DbWeight::get().writes(6 as Weight))
 	}
@@ -159,7 +159,7 @@
 	// Storage: Nonfungible Owned (r:0 w:1)
 	// Storage: Nonfungible TokenProperties (r:0 w:1)
 	fn burn_from() -> Weight {
-		(37_061_000 as Weight)
+		(32_201_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(5 as Weight))
 			.saturating_add(T::DbWeight::get().writes(6 as Weight))
 	}
@@ -167,28 +167,26 @@
 	fn set_token_property_permissions(b: u32, ) -> Weight {
 		(0 as Weight)
 			// Standard Error: 57_000
-			.saturating_add((15_149_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add((15_232_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(T::DbWeight::get().reads(1 as Weight))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
 	}
 	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
-	// Storage: Nonfungible TokenData (r:1 w:0)
 	// Storage: Nonfungible TokenProperties (r:1 w:1)
 	fn set_token_properties(b: u32, ) -> Weight {
 		(0 as Weight)
-			// Standard Error: 2_278_000
-			.saturating_add((409_613_000 as Weight).saturating_mul(b as Weight))
-			.saturating_add(T::DbWeight::get().reads(3 as Weight))
+			// Standard Error: 1_648_000
+			.saturating_add((288_654_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
 	}
 	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
-	// Storage: Nonfungible TokenData (r:1 w:0)
 	// Storage: Nonfungible TokenProperties (r:1 w:1)
 	fn delete_token_properties(b: u32, ) -> Weight {
 		(0 as Weight)
-			// Standard Error: 2_234_000
-			.saturating_add((408_185_000 as Weight).saturating_mul(b as Weight))
-			.saturating_add(T::DbWeight::get().reads(3 as Weight))
+			// Standard Error: 1_632_000
+			.saturating_add((289_190_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
 	}
 }
@@ -200,7 +198,7 @@
 	// Storage: Nonfungible TokenData (r:0 w:1)
 	// Storage: Nonfungible Owned (r:0 w:1)
 	fn create_item() -> Weight {
-		(24_135_000 as Weight)
+		(20_328_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(4 as Weight))
 	}
@@ -209,9 +207,9 @@
 	// Storage: Nonfungible TokenData (r:0 w:4)
 	// Storage: Nonfungible Owned (r:0 w:4)
 	fn create_multiple_items(b: u32, ) -> Weight {
-		(21_952_000 as Weight)
-			// Standard Error: 5_000
-			.saturating_add((4_727_000 as Weight).saturating_mul(b as Weight))
+		(10_134_000 as Weight)
+			// Standard Error: 3_000
+			.saturating_add((4_927_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(b as Weight)))
@@ -221,9 +219,9 @@
 	// Storage: Nonfungible TokenData (r:0 w:4)
 	// Storage: Nonfungible Owned (r:0 w:4)
 	fn create_multiple_items_ex(b: u32, ) -> Weight {
-		(10_432_000 as Weight)
-			// Standard Error: 6_000
-			.saturating_add((7_383_000 as Weight).saturating_mul(b as Weight))
+		(5_710_000 as Weight)
+			// Standard Error: 4_000
+			.saturating_add((7_578_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(RocksDbWeight::get().reads(1 as Weight))
 			.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
@@ -237,7 +235,7 @@
 	// Storage: Nonfungible Owned (r:0 w:1)
 	// Storage: Nonfungible TokenProperties (r:0 w:1)
 	fn burn_item() -> Weight {
-		(29_798_000 as Weight)
+		(28_433_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(5 as Weight))
 	}
@@ -249,7 +247,7 @@
 	// Storage: Nonfungible Owned (r:0 w:1)
 	// Storage: Nonfungible TokenProperties (r:0 w:1)
 	fn burn_recursively_self_raw() -> Weight {
-		(37_955_000 as Weight)
+		(34_435_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(5 as Weight))
 	}
@@ -263,8 +261,8 @@
 	// Storage: Common CollectionById (r:1 w:0)
 	fn burn_recursively_breadth_plus_self_plus_self_per_each_raw(b: u32, ) -> Weight {
 		(0 as Weight)
-			// Standard Error: 1_349_000
-			.saturating_add((275_145_000 as Weight).saturating_mul(b as Weight))
+			// Standard Error: 1_539_000
+			.saturating_add((304_456_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(RocksDbWeight::get().reads(7 as Weight))
 			.saturating_add(RocksDbWeight::get().reads((4 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(RocksDbWeight::get().writes(6 as Weight))
@@ -275,14 +273,14 @@
 	// Storage: Nonfungible Allowance (r:1 w:0)
 	// Storage: Nonfungible Owned (r:0 w:2)
 	fn transfer() -> Weight {
-		(27_867_000 as Weight)
+		(24_376_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(4 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(5 as Weight))
 	}
 	// Storage: Nonfungible TokenData (r:1 w:0)
 	// Storage: Nonfungible Allowance (r:1 w:1)
 	fn approve() -> Weight {
-		(18_824_000 as Weight)
+		(15_890_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
 	}
@@ -291,7 +289,7 @@
 	// Storage: Nonfungible AccountBalance (r:2 w:2)
 	// Storage: Nonfungible Owned (r:0 w:2)
 	fn transfer_from() -> Weight {
-		(32_879_000 as Weight)
+		(28_634_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(4 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(6 as Weight))
 	}
@@ -303,7 +301,7 @@
 	// Storage: Nonfungible Owned (r:0 w:1)
 	// Storage: Nonfungible TokenProperties (r:0 w:1)
 	fn burn_from() -> Weight {
-		(37_061_000 as Weight)
+		(32_201_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(6 as Weight))
 	}
@@ -311,28 +309,26 @@
 	fn set_token_property_permissions(b: u32, ) -> Weight {
 		(0 as Weight)
 			// Standard Error: 57_000
-			.saturating_add((15_149_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add((15_232_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(RocksDbWeight::get().reads(1 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
 	}
 	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
-	// Storage: Nonfungible TokenData (r:1 w:0)
 	// Storage: Nonfungible TokenProperties (r:1 w:1)
 	fn set_token_properties(b: u32, ) -> Weight {
 		(0 as Weight)
-			// Standard Error: 2_278_000
-			.saturating_add((409_613_000 as Weight).saturating_mul(b as Weight))
-			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
+			// Standard Error: 1_648_000
+			.saturating_add((288_654_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
 	}
 	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
-	// Storage: Nonfungible TokenData (r:1 w:0)
 	// Storage: Nonfungible TokenProperties (r:1 w:1)
 	fn delete_token_properties(b: u32, ) -> Weight {
 		(0 as Weight)
-			// Standard Error: 2_234_000
-			.saturating_add((408_185_000 as Weight).saturating_mul(b as Weight))
-			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
+			// Standard Error: 1_632_000
+			.saturating_add((289_190_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
 	}
 }
addedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/refungible/CHANGELOG.md
@@ -0,0 +1,18 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [v0.1.2] - 2022-07-14
+
+### Other changes
+
+feat(refungible-pallet): add ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
+test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
+
+## [v0.1.1] - 2022-07-14
+
+### Other changes
+
+- feat: 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/refungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/refungible/Cargo.toml
+++ b/pallets/refungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-refungible"
-version = "0.1.0"
+version = "0.1.2"
 license = "GPLv3"
 edition = "2021"
 
@@ -11,36 +11,43 @@
 version = '3.1.2'
 
 [dependencies]
-frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.24" }
+evm-coder = { default-features = false, path = '../../crates/evm-coder' }
+pallet-evm-coder-substrate = { default-features = false, path = '../../pallets/evm-coder-substrate' }
 pallet-common = { default-features = false, path = '../common' }
 pallet-structure = { default-features = false, path = '../structure' }
+struct-versioning = { path = "../../crates/struct-versioning" }
 up-data-structs = { default-features = false, path = '../../primitives/data-structs' }
+
+ethereum = { version = "0.12.0", default-features = false }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive",] }
+
 frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-scale-info = { version = "2.0.1", default-features = false, features = [
-    "derive",
-] }
-struct-versioning = { path = "../../crates/struct-versioning" }
+frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.24" }
+sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
 
 [features]
 default = ["std"]
 std = [
+    "ethereum/std",
+    "evm-coder/std",
+    'frame-benchmarking/std',
     "frame-support/std",
     "frame-system/std",
+    "pallet-common/std",
+    "pallet-evm/std",
+    "pallet-evm-coder-substrate/std",
+    "pallet-structure/std",
     "sp-runtime/std",
     "sp-std/std",
     "up-data-structs/std",
-    "pallet-common/std",
-    "pallet-structure/std",
-    'frame-benchmarking/std',
-    "pallet-evm/std",
 ]
 runtime-benchmarks = [
     'frame-benchmarking',
     'frame-support/runtime-benchmarks',
     'frame-system/runtime-benchmarks',
+    'up-data-structs/runtime-benchmarks',
 ]
addedpallets/refungible/Changelog.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/refungible/Changelog.md
@@ -0,0 +1,3 @@
+### 0.1.1
+---
+* Added support for properties for RFT collections and tokens.
modifiedpallets/refungible/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/refungible/src/benchmarking.rs
+++ b/pallets/refungible/src/benchmarking.rs
@@ -18,9 +18,12 @@
 use crate::{Pallet, Config, RefungibleHandle};
 
 use sp_std::prelude::*;
-use pallet_common::benchmarking::{create_collection_raw, create_data};
+use pallet_common::benchmarking::{create_collection_raw, property_key, property_value, create_data};
 use frame_benchmarking::{benchmarks, account};
-use up_data_structs::{CollectionMode, MAX_ITEMS_PER_BATCH, CUSTOM_DATA_LIMIT, budget::Unlimited};
+use up_data_structs::{
+	CollectionMode, MAX_ITEMS_PER_BATCH, MAX_PROPERTIES_PER_ITEM, CUSTOM_DATA_LIMIT,
+	budget::Unlimited,
+};
 use pallet_common::bench_init;
 use core::convert::TryInto;
 use core::iter::IntoIterator;
@@ -38,6 +41,7 @@
 			.collect::<BTreeMap<_, _>>()
 			.try_into()
 			.unwrap(),
+		properties: Default::default(),
 	}
 }
 fn create_max_item<T: Config>(
@@ -204,6 +208,68 @@
 		<Pallet<T>>::set_allowance(&collection, &sender, &burner, item, 200)?;
 	}: {<Pallet<T>>::burn_from(&collection, &burner, &sender, item, 200, &Unlimited)?}
 
+	set_token_property_permissions {
+		let b in 0..MAX_PROPERTIES_PER_ITEM;
+		bench_init!{
+			owner: sub; collection: collection(owner);
+			owner: cross_from_sub;
+		};
+		let perms = (0..b).map(|k| PropertyKeyPermission {
+			key: property_key(k as usize),
+			permission: PropertyPermission {
+				mutable: false,
+				collection_admin: false,
+				token_owner: false,
+			},
+		}).collect::<Vec<_>>();
+	}: {<Pallet<T>>::set_token_property_permissions(&collection, &owner, perms)?}
+
+	set_token_properties {
+		let b in 0..MAX_PROPERTIES_PER_ITEM;
+		bench_init!{
+			owner: sub; collection: collection(owner);
+			owner: cross_from_sub;
+		};
+		let perms = (0..b).map(|k| PropertyKeyPermission {
+			key: property_key(k as usize),
+			permission: PropertyPermission {
+				mutable: false,
+				collection_admin: true,
+				token_owner: true,
+			},
+		}).collect::<Vec<_>>();
+		<Pallet<T>>::set_token_property_permissions(&collection, &owner, perms)?;
+		let props = (0..b).map(|k| Property {
+			key: property_key(k as usize),
+			value: property_value(),
+		}).collect::<Vec<_>>();
+		let item = create_max_item(&collection, &owner, [(owner.clone(), 200)])?;
+	}: {<Pallet<T>>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?}
+
+	delete_token_properties {
+		let b in 0..MAX_PROPERTIES_PER_ITEM;
+		bench_init!{
+			owner: sub; collection: collection(owner);
+			owner: cross_from_sub;
+		};
+		let perms = (0..b).map(|k| PropertyKeyPermission {
+			key: property_key(k as usize),
+			permission: PropertyPermission {
+				mutable: true,
+				collection_admin: true,
+				token_owner: true,
+			},
+		}).collect::<Vec<_>>();
+		<Pallet<T>>::set_token_property_permissions(&collection, &owner, perms)?;
+		let props = (0..b).map(|k| Property {
+			key: property_key(k as usize),
+			value: property_value(),
+		}).collect::<Vec<_>>();
+		let item = create_max_item(&collection, &owner, [(owner.clone(), 200)])?;
+		<Pallet<T>>::set_token_properties(&collection, &owner, item, props.into_iter(), false, &Unlimited)?;
+		let to_delete = (0..b).map(|k| property_key(k as usize)).collect::<Vec<_>>();
+	}: {<Pallet<T>>::delete_token_properties(&collection, &owner, item, to_delete.into_iter(), &Unlimited)?}
+
 	repartition_item {
 		bench_init!{
 			owner: sub; collection: collection(owner);
modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -20,9 +20,12 @@
 use frame_support::{dispatch::DispatchResultWithPostInfo, ensure, fail, weights::Weight, traits::Get};
 use up_data_structs::{
 	CollectionId, TokenId, CreateItemExData, CreateRefungibleExData, budget::Budget, Property,
-	PropertyKey, PropertyValue, PropertyKeyPermission, CreateItemData,
+	PropertyKey, PropertyValue, PropertyKeyPermission, CreateItemData, CollectionPropertiesVec,
+};
+use pallet_common::{
+	CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight,
+	weights::WeightInfo as _,
 };
-use pallet_common::{CommonCollectionOperations, CommonWeightInfo, RefungibleExtensions, with_weight};
 use pallet_structure::Error as StructureError;
 use sp_runtime::{DispatchError};
 use sp_std::{vec::Vec, vec};
@@ -41,6 +44,14 @@
 	};
 }
 
+fn properties_weight<T: Config>(properties: &CollectionPropertiesVec) -> u64 {
+	if properties.len() > 0 {
+		<CommonWeights<T>>::set_token_properties(properties.len() as u32)
+	} else {
+		0
+	}
+}
+
 pub struct CommonWeights<T: Config>(PhantomData<T>);
 impl<T: Config> CommonWeightInfo<T::CrossAccountId> for CommonWeights<T> {
 	fn create_item() -> Weight {
@@ -48,16 +59,31 @@
 	}
 
 	fn create_multiple_items(data: &[CreateItemData]) -> Weight {
-		<SelfWeightOf<T>>::create_multiple_items(data.len() as u32)
+		<SelfWeightOf<T>>::create_multiple_items(data.len() as u32).saturating_add(
+			data.iter()
+				.map(|data| match data {
+					CreateItemData::ReFungible(rft_data) => {
+						properties_weight::<T>(&rft_data.properties)
+					}
+					_ => 0,
+				})
+				.fold(0, |a, b| a.saturating_add(b)),
+		)
 	}
 
 	fn create_multiple_items_ex(call: &CreateItemExData<T::CrossAccountId>) -> Weight {
 		match call {
 			CreateItemExData::RefungibleMultipleOwners(i) => {
 				<SelfWeightOf<T>>::create_multiple_items_ex_multiple_owners(i.users.len() as u32)
+					.saturating_add(properties_weight::<T>(&i.properties))
 			}
 			CreateItemExData::RefungibleMultipleItems(i) => {
 				<SelfWeightOf<T>>::create_multiple_items_ex_multiple_items(i.len() as u32)
+					.saturating_add(
+						i.iter()
+							.map(|d| properties_weight::<T>(&d.properties))
+							.fold(0, |a, b| a.saturating_add(b)),
+					)
 			}
 			_ => 0,
 		}
@@ -67,29 +93,24 @@
 		max_weight_of!(burn_item_partial(), burn_item_fully())
 	}
 
-	fn set_collection_properties(_amount: u32) -> Weight {
-		// Error
-		0
+	fn set_collection_properties(amount: u32) -> Weight {
+		<pallet_common::SelfWeightOf<T>>::set_collection_properties(amount)
 	}
 
-	fn delete_collection_properties(_amount: u32) -> Weight {
-		// Error
-		0
+	fn delete_collection_properties(amount: u32) -> Weight {
+		<pallet_common::SelfWeightOf<T>>::delete_collection_properties(amount)
 	}
 
-	fn set_token_properties(_amount: u32) -> Weight {
-		// Error
-		0
+	fn set_token_properties(amount: u32) -> Weight {
+		<SelfWeightOf<T>>::set_token_properties(amount)
 	}
 
-	fn delete_token_properties(_amount: u32) -> Weight {
-		// Error
-		0
+	fn delete_token_properties(amount: u32) -> Weight {
+		<SelfWeightOf<T>>::delete_token_properties(amount)
 	}
 
-	fn set_token_property_permissions(_amount: u32) -> Weight {
-		// Error
-		0
+	fn set_token_property_permissions(amount: u32) -> Weight {
+		<SelfWeightOf<T>>::set_token_property_permissions(amount)
 	}
 
 	fn transfer() -> Weight {
@@ -140,11 +161,14 @@
 				out.insert(to.clone(), data.pieces);
 				out.try_into().expect("limit > 0")
 			},
+			properties: data.properties,
 		}),
 		_ => fail!(<Error<T>>::NotRefungibleDataUsedToMintFungibleCollectionToken),
 	}
 }
 
+/// Implementation of `CommonCollectionOperations` for `RefungibleHandle`. It wraps Refungible Pallete
+/// methods and adds weight info.
 impl<T: Config> CommonCollectionOperations<T> for RefungibleHandle<T> {
 	fn create_item(
 		&self,
@@ -295,46 +319,85 @@
 
 	fn set_collection_properties(
 		&self,
-		_sender: T::CrossAccountId,
-		_property: Vec<Property>,
+		sender: T::CrossAccountId,
+		properties: Vec<Property>,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight = <CommonWeights<T>>::set_collection_properties(properties.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::set_collection_properties(self, &sender, properties),
+			weight,
+		)
 	}
 
 	fn delete_collection_properties(
 		&self,
-		_sender: &T::CrossAccountId,
-		_property_keys: Vec<PropertyKey>,
+		sender: &T::CrossAccountId,
+		property_keys: Vec<PropertyKey>,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight = <CommonWeights<T>>::delete_collection_properties(property_keys.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::delete_collection_properties(self, sender, property_keys),
+			weight,
+		)
 	}
 
 	fn set_token_properties(
 		&self,
-		_sender: T::CrossAccountId,
-		_token_id: TokenId,
-		_property: Vec<Property>,
-		_nesting_budget: &dyn Budget,
+		sender: T::CrossAccountId,
+		token_id: TokenId,
+		properties: Vec<Property>,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight = <CommonWeights<T>>::set_token_properties(properties.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::set_token_properties(
+				self,
+				&sender,
+				token_id,
+				properties.into_iter(),
+				false,
+				nesting_budget,
+			),
+			weight,
+		)
 	}
 
 	fn set_token_property_permissions(
 		&self,
-		_sender: &T::CrossAccountId,
-		_property_permissions: Vec<PropertyKeyPermission>,
+		sender: &T::CrossAccountId,
+		property_permissions: Vec<PropertyKeyPermission>,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight =
+			<CommonWeights<T>>::set_token_property_permissions(property_permissions.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::set_token_property_permissions(self, sender, property_permissions),
+			weight,
+		)
 	}
 
 	fn delete_token_properties(
 		&self,
-		_sender: T::CrossAccountId,
-		_token_id: TokenId,
-		_property_keys: Vec<PropertyKey>,
-		_nesting_budget: &dyn Budget,
+		sender: T::CrossAccountId,
+		token_id: TokenId,
+		property_keys: Vec<PropertyKey>,
+		nesting_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
-		fail!(<Error<T>>::SettingPropertiesNotAllowed)
+		let weight = <CommonWeights<T>>::delete_token_properties(property_keys.len() as u32);
+
+		with_weight(
+			<Pallet<T>>::delete_token_properties(
+				self,
+				&sender,
+				token_id,
+				property_keys.into_iter(),
+				nesting_budget,
+			),
+			weight,
+		)
 	}
 
 	fn check_nesting(
@@ -371,8 +434,13 @@
 		TokenId(<TokensMinted<T>>::get(self.id))
 	}
 
-	fn token_owner(&self, _token: TokenId) -> Option<T::CrossAccountId> {
-		None
+	fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId> {
+		<Pallet<T>>::token_owner(self.id, token)
+	}
+
+	/// Returns 10 token in no particular order.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId> {
+		<Pallet<T>>::token_owners(self.id, token).unwrap_or_default()
 	}
 
 	fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option<PropertyValue> {
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -14,34 +14,35 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-use up_data_structs::TokenId;
-use pallet_common::erc::CommonEvmHandler;
-use pallet_evm::PrecompileHandle;
+extern crate alloc;
+use evm_coder::{generate_stubgen, solidity_interface, types::*};
 
-use crate::{Config, RefungibleHandle};
+use pallet_common::{CollectionHandle, erc::CollectionCall, erc::CommonEvmHandler};
 
-impl<T: Config> CommonEvmHandler for RefungibleHandle<T> {
-	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");
+use pallet_evm::PrecompileHandle;
+use pallet_evm_coder_substrate::call;
 
-	fn call(
-		self,
-		_handle: &mut impl PrecompileHandle,
-	) -> Option<pallet_common::erc::PrecompileResult> {
-		// TODO: Implement RFT variant of ERC721
-		None
-	}
-}
+use crate::{Config, RefungibleHandle};
 
-pub struct RefungibleTokenHandle<T: Config>(pub RefungibleHandle<T>, pub TokenId);
+#[solidity_interface(
+	name = "UniqueRFT",
+	is(via("CollectionHandle<T>", common_mut, Collection),)
+)]
+impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}
 
-impl<T: Config> CommonEvmHandler for RefungibleTokenHandle<T> {
-	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw");
+// Not a tests, but code generators
+generate_stubgen!(gen_impl, UniqueRFTCall<()>, true);
+generate_stubgen!(gen_iface, UniqueRFTCall<()>, false);
 
+impl<T: Config> CommonEvmHandler for RefungibleHandle<T>
+where
+	T::AccountId: From<[u8; 32]>,
+{
+	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");
 	fn call(
 		self,
-		_handle: &mut impl PrecompileHandle,
+		handle: &mut impl PrecompileHandle,
 	) -> Option<pallet_common::erc::PrecompileResult> {
-		// TODO: Implement RFT variant of ERC20
-		None
+		call::<T, UniqueRFTCall<T>, _, _>(handle, self)
 	}
 }
addedpallets/refungible/src/erc_token.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/refungible/src/erc_token.rs
@@ -0,0 +1,257 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+//! # Refungible Pallet EVM API for token pieces
+//!
+//! Provides ERC-20 standart support implementation and EVM API for unique extensions for Refungible Pallet.
+//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.
+
+extern crate alloc;
+use core::{
+	char::{REPLACEMENT_CHARACTER, decode_utf16},
+	convert::TryInto,
+	ops::Deref,
+};
+use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*, weight};
+use pallet_common::{
+	CommonWeightInfo,
+	erc::{CommonEvmHandler, PrecompileResult},
+};
+use pallet_evm::{account::CrossAccountId, PrecompileHandle};
+use pallet_evm_coder_substrate::{call, dispatch_to_evm, WithRecorder};
+use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
+use sp_std::vec::Vec;
+use up_data_structs::TokenId;
+
+use crate::{
+	Allowance, Balance, common::CommonWeights, Config, Pallet, RefungibleHandle, SelfWeightOf,
+	weights::WeightInfo, TotalSupply,
+};
+
+pub struct RefungibleTokenHandle<T: Config>(pub RefungibleHandle<T>, pub TokenId);
+
+#[derive(ToLog)]
+pub enum ERC20Events {
+	/// @dev This event is emitted when the amount of tokens (value) is sent
+	/// from the from address to the to address. In the case of minting new
+	/// tokens, the transfer is usually from the 0 address while in the case
+	/// of burning tokens the transfer is to 0.
+	Transfer {
+		#[indexed]
+		from: address,
+		#[indexed]
+		to: address,
+		value: uint256,
+	},
+	/// @dev This event is emitted when the amount of tokens (value) is approved
+	/// by the owner to be used by the spender.
+	Approval {
+		#[indexed]
+		owner: address,
+		#[indexed]
+		spender: address,
+		value: uint256,
+	},
+}
+
+/// @title Standard ERC20 token
+///
+/// @dev Implementation of the basic standard token.
+/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
+#[solidity_interface(name = "ERC20", events(ERC20Events))]
+impl<T: Config> RefungibleTokenHandle<T> {
+	/// @return the name of the token.
+	fn name(&self) -> Result<string> {
+		Ok(decode_utf16(self.name.iter().copied())
+			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
+			.collect::<string>())
+	}
+
+	/// @return the symbol of the token.
+	fn symbol(&self) -> Result<string> {
+		Ok(string::from_utf8_lossy(&self.token_prefix).into())
+	}
+
+	/// @dev Total number of tokens in existence
+	fn total_supply(&self) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		Ok(<TotalSupply<T>>::get((self.id, self.1)).into())
+	}
+
+	/// @dev Not supported
+	fn decimals(&self) -> Result<uint8> {
+		// Decimals aren't supported for refungible tokens
+		Ok(0)
+	}
+
+	/// @dev Gets the balance of the specified address.
+	/// @param owner The address to query the balance of.
+	/// @return An uint256 representing the amount owned by the passed address.
+	fn balance_of(&self, owner: address) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		let owner = T::CrossAccountId::from_eth(owner);
+		let balance = <Balance<T>>::get((self.id, self.1, owner));
+		Ok(balance.into())
+	}
+
+	/// @dev Transfer token for a specified address
+	/// @param to The address to transfer to.
+	/// @param amount The amount to be transferred.
+	#[weight(<CommonWeights<T>>::transfer())]
+	fn transfer(&mut self, caller: caller, to: address, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::transfer(self, &caller, &to, self.1, amount, &budget)
+			.map_err(|_| "transfer error")?;
+		Ok(true)
+	}
+
+	/// @dev Transfer tokens from one address to another
+	/// @param from address The address which you want to send tokens from
+	/// @param to address The address which you want to transfer to
+	/// @param amount uint256 the amount of tokens to be transferred
+	#[weight(<CommonWeights<T>>::transfer_from())]
+	fn transfer_from(
+		&mut self,
+		caller: caller,
+		from: address,
+		to: address,
+		amount: uint256,
+	) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let to = T::CrossAccountId::from_eth(to);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::transfer_from(self, &caller, &from, &to, self.1, amount, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`.
+	/// Beware that changing an allowance with this method brings the risk that someone may use both the old
+	/// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
+	/// race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
+	/// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
+	/// @param spender The address which will spend the funds.
+	/// @param amount The amount of tokens to be spent.
+	#[weight(<SelfWeightOf<T>>::approve())]
+	fn approve(&mut self, caller: caller, spender: address, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let spender = T::CrossAccountId::from_eth(spender);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+		<Pallet<T>>::set_allowance(self, &caller, &spender, self.1, amount)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Function to check the amount of tokens that an owner allowed to a spender.
+	/// @param owner address The address which owns the funds.
+	/// @param spender address The address which will spend the funds.
+	/// @return A uint256 specifying the amount of tokens still available for the spender.
+	fn allowance(&self, owner: address, spender: address) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		let owner = T::CrossAccountId::from_eth(owner);
+		let spender = T::CrossAccountId::from_eth(spender);
+
+		Ok(<Allowance<T>>::get((self.id, self.1, owner, spender)).into())
+	}
+}
+
+#[solidity_interface(name = "ERC20UniqueExtensions")]
+impl<T: Config> RefungibleTokenHandle<T> {
+	/// @dev Function that burns an amount of the token of a given account,
+	/// deducting from the sender's allowance for said account.
+	/// @param from The account whose tokens will be burnt.
+	/// @param amount The amount that will be burnt.
+	#[weight(<SelfWeightOf<T>>::burn_from())]
+	fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::burn_from(self, &caller, &from, self.1, amount, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Function that changes total amount of the tokens.
+	///  Throws if `msg.sender` doesn't owns all of the tokens.
+	/// @param amount New total amount of the tokens.
+	#[weight(<SelfWeightOf<T>>::repartition_item())]
+	fn repartition(&mut self, caller: caller, amount: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+		<Pallet<T>>::repartition(self, &caller, self.1, amount).map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+}
+
+impl<T: Config> RefungibleTokenHandle<T> {
+	pub fn into_inner(self) -> RefungibleHandle<T> {
+		self.0
+	}
+	pub fn common_mut(&mut self) -> &mut RefungibleHandle<T> {
+		&mut self.0
+	}
+}
+
+impl<T: Config> WithRecorder<T> for RefungibleTokenHandle<T> {
+	fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		self.0.recorder()
+	}
+	fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		self.0.into_recorder()
+	}
+}
+
+impl<T: Config> Deref for RefungibleTokenHandle<T> {
+	type Target = RefungibleHandle<T>;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+
+#[solidity_interface(name = "UniqueRefungibleToken", is(ERC20, ERC20UniqueExtensions,))]
+impl<T: Config> RefungibleTokenHandle<T> where T::AccountId: From<[u8; 32]> {}
+
+generate_stubgen!(gen_impl, UniqueRefungibleTokenCall<()>, true);
+generate_stubgen!(gen_iface, UniqueRefungibleTokenCall<()>, false);
+
+impl<T: Config> CommonEvmHandler for RefungibleTokenHandle<T>
+where
+	T::AccountId: From<[u8; 32]>,
+{
+	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw");
+
+	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
+		call::<T, UniqueRefungibleTokenCall<T>, _, _>(handle, self)
+	}
+}
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -14,32 +14,113 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Refungible Pallet
+//!
+//! The Refungible pallet provides functionality for handling refungible collections and tokens.
+//!
+//! - [`Config`]
+//! - [`RefungibleHandle`]
+//! - [`Pallet`]
+//! - [`CommonWeights`](common::CommonWeights)
+//!
+//! ## Overview
+//!
+//! The Refungible pallet provides functions for:
+//!
+//! - RFT collection creation and removal
+//! - Minting and burning of RFT tokens
+//! - Partition and repartition of RFT tokens
+//! - Retrieving number of pieces of RFT token
+//! - Retrieving account balances
+//! - Transfering RFT token pieces
+//! - Burning RFT token pieces
+//! - Setting and checking allowance for RFT tokens
+//!
+//! ### Terminology
+//!
+//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all
+//!   of the RFT token pieces than it owns the RFT token and can repartition it.
+//!
+//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.
+//!   Each collection has its own settings and set of permissions.
+//!
+//! - **RFT token piece:** A fungible part of an RFT token.
+//!
+//! - **Balance:** RFT token pieces owned by an account
+//!
+//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to
+//!   transfer from the balance of another account
+//!
+//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from
+//!   an account balance.
+//!
+//! ### Implementations
+//!
+//! The Refungible pallet provides implementations for the following traits. If these traits provide
+//! the functionality that you need, then you can avoid coupling with the Refungible pallet.
+//!
+//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight
+//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing
+//!   with collections
+//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible
+//!   collection
+//!
+//! ## Interface
+//!
+//! ### Dispatchable Functions
+//!
+//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for
+//!   some accounts.
+//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.
+//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.
+//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.
+//!   Nests the RFT token if RFT token pieces are sent to another token.
+//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.
+//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.
+//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.
+//!
+//! ## Assumptions
+//!
+//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.
+//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.
+//! * Sender should be in collection's allow list to perform operations on tokens.
+
 #![cfg_attr(not(feature = "std"), no_std)]
 
-use frame_support::{ensure, BoundedVec};
-use up_data_structs::{
-	AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,
-	CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget,
+use crate::erc_token::ERC20Events;
+
+use codec::{Encode, Decode, MaxEncodedLen};
+use core::ops::Deref;
+use evm_coder::ToLog;
+use frame_support::{BoundedVec, ensure, fail, storage::with_transaction, transactional};
+use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
+use pallet_evm_coder_substrate::WithRecorder;
+use pallet_common::{
+	CommonCollectionOperations, Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
 };
-use pallet_evm::account::CrossAccountId;
-use pallet_common::{Error as CommonError, Event as CommonEvent, Pallet as PalletCommon};
 use pallet_structure::Pallet as PalletStructure;
-use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};
+use scale_info::TypeInfo;
+use sp_core::H160;
+use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};
 use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};
-use core::ops::Deref;
-use codec::{Encode, Decode, MaxEncodedLen};
-use scale_info::TypeInfo;
+use up_data_structs::{
+	AccessMode, budget::Budget, CollectionId, CreateCollectionData, CreateRefungibleExData,
+	CustomDataLimit, mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, TokenId, Property,
+	PropertyKey, PropertyKeyPermission, PropertyPermission, PropertyScope, PropertyValue,
+	TrySetProperty,
+};
 
 pub use pallet::*;
 #[cfg(feature = "runtime-benchmarks")]
 pub mod benchmarking;
 pub mod common;
 pub mod erc;
+pub mod erc_token;
 pub mod weights;
 pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;
 
-/// Token data, stored independently from other data used to describe it.
-/// Notably contains the token metadata.
+/// Token data, stored independently from other data used to describe it
+/// for the convenience of database access. Notably contains the token metadata.
 #[struct_versioning::versioned(version = 2, upper)]
 #[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]
 pub struct ItemData {
@@ -108,6 +189,16 @@
 
 	/// Amount of pieces a refungible token is split into.
 	#[pallet::storage]
+	#[pallet::getter(fn token_properties)]
+	pub type TokenProperties<T: Config> = StorageNMap<
+		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),
+		Value = up_data_structs::Properties,
+		QueryKind = ValueQuery,
+		OnEmpty = up_data_structs::TokenProperties,
+	>;
+
+	/// Total amount of pieces for token
+	#[pallet::storage]
 	pub type TotalSupply<T: Config> = StorageNMap<
 		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),
 		Value = u128,
@@ -138,7 +229,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// Amount of pieces of a token owned by an account.
+	/// Amount of token pieces owned by account.
 	#[pallet::storage]
 	pub type Balance<T: Config> = StorageNMap<
 		Key = (
@@ -151,7 +242,7 @@
 		QueryKind = ValueQuery,
 	>;
 
-	/// todo:doc
+	/// Allowance set by a token owner for another user to perform one of certain transactions on a number of pieces of a token.
 	#[pallet::storage]
 	pub type Allowance<T: Config> = StorageNMap<
 		Key = (
@@ -188,7 +279,11 @@
 	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {
 		self.0
 	}
+	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {
+		&mut self.0
+	}
 }
+
 impl<T: Config> Deref for RefungibleHandle<T> {
 	type Target = pallet_common::CollectionHandle<T>;
 
@@ -197,23 +292,75 @@
 	}
 }
 
+impl<T: Config> WithRecorder<T> for RefungibleHandle<T> {
+	fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		self.0.recorder()
+	}
+	fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		self.0.into_recorder()
+	}
+}
+
 impl<T: Config> Pallet<T> {
+	/// Get number of RFT tokens in collection
 	pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {
 		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)
 	}
+
+	/// Check that RFT token exists
+	///
+	/// - `token`: Token ID.
 	pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {
 		<TotalSupply<T>>::contains_key((collection.id, token))
 	}
+
+	pub fn set_scoped_token_property(
+		collection_id: CollectionId,
+		token_id: TokenId,
+		scope: PropertyScope,
+		property: Property,
+	) -> DispatchResult {
+		TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {
+			properties.try_scoped_set(scope, property.key, property.value)
+		})
+		.map_err(<CommonError<T>>::from)?;
+
+		Ok(())
+	}
+
+	pub fn set_scoped_token_properties(
+		collection_id: CollectionId,
+		token_id: TokenId,
+		scope: PropertyScope,
+		properties: impl Iterator<Item = Property>,
+	) -> DispatchResult {
+		TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {
+			stored_properties.try_scoped_set_from_iter(scope, properties)
+		})
+		.map_err(<CommonError<T>>::from)?;
+
+		Ok(())
+	}
 }
 
 // unchecked calls skips any permission checks
 impl<T: Config> Pallet<T> {
+	/// Create RFT collection
+	///
+	/// `init_collection` will take non-refundable deposit for collection creation.
+	///
+	/// - `data`: Contains settings for collection limits and permissions.
 	pub fn init_collection(
 		owner: T::CrossAccountId,
 		data: CreateCollectionData<T::AccountId>,
 	) -> Result<CollectionId, DispatchError> {
 		<PalletCommon<T>>::init_collection(owner, data, false)
 	}
+
+	/// Destroy RFT collection
+	///
+	/// `destroy_collection` will throw error if collection contains any tokens.
+	/// Only owner can destroy collection.
 	pub fn destroy_collection(
 		collection: RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -245,13 +392,17 @@
 			.is_some()
 	}
 
-	pub fn burn_token(collection: &RefungibleHandle<T>, token_id: TokenId) -> DispatchResult {
+	pub fn burn_token_unchecked(
+		collection: &RefungibleHandle<T>,
+		token_id: TokenId,
+	) -> DispatchResult {
 		let burnt = <TokensBurnt<T>>::get(collection.id)
 			.checked_add(1)
 			.ok_or(ArithmeticError::Overflow)?;
 
 		<TokensBurnt<T>>::insert(collection.id, burnt);
 		<TokenData<T>>::remove((collection.id, token_id));
+		<TokenProperties<T>>::remove((collection.id, token_id));
 		<TotalSupply<T>>::remove((collection.id, token_id));
 		<Balance<T>>::remove_prefix((collection.id, token_id), None);
 		<Allowance<T>>::remove_prefix((collection.id, token_id), None);
@@ -259,6 +410,17 @@
 		Ok(())
 	}
 
+	/// Burn RFT token pieces
+	///
+	/// `burn` will decrease total amount of token pieces and amount owned by sender.
+	/// `burn` can be called even if there are multiple owners of the RFT token.
+	/// If sender wouldn't have any pieces left after `burn` than she will stop being
+	/// one of the owners of the token. If there is no account that owns any pieces of
+	/// the token than token will be burned too.
+	///
+	/// - `amount`: Amount of token pieces to burn.
+	/// - `token`: Token who's pieces should be burned
+	/// - `collection`: Collection that contains the token
 	pub fn burn(
 		collection: &RefungibleHandle<T>,
 		owner: &T::CrossAccountId,
@@ -286,7 +448,7 @@
 			<Owned<T>>::remove((collection.id, owner, token));
 			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
-			Self::burn_token(collection, token)?;
+			Self::burn_token_unchecked(collection, token)?;
 			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
 				collection.id,
 				token,
@@ -319,7 +481,18 @@
 			<Balance<T>>::insert((collection.id, token, owner), balance);
 		}
 		<TotalSupply<T>>::insert((collection.id, token), total_supply);
-		// TODO: ERC20 transfer event
+
+		<PalletEvm<T>>::deposit_log(
+			ERC20Events::Transfer {
+				from: *owner.as_eth(),
+				to: H160::default(),
+				value: amount.into(),
+			}
+			.to_log(T::EvmTokenAddressMapping::token_to_address(
+				collection.id,
+				token,
+			)),
+		);
 		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
 			collection.id,
 			token,
@@ -329,6 +502,179 @@
 		Ok(())
 	}
 
+	#[transactional]
+	fn modify_token_properties(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		token_id: TokenId,
+		properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,
+		is_token_create: bool,
+		nesting_budget: &dyn Budget,
+	) -> DispatchResult {
+		let is_collection_admin = || collection.is_owner_or_admin(sender);
+		let is_token_owner = || -> Result<bool, DispatchError> {
+			let balance = collection.balance(sender.clone(), token_id);
+			let total_pieces: u128 =
+				Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);
+			if balance != total_pieces {
+				return Ok(false);
+			}
+
+			let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(
+				sender.clone(),
+				collection.id,
+				token_id,
+				None,
+				nesting_budget,
+			)?;
+
+			Ok(is_bundle_owner)
+		};
+
+		for (key, value) in properties {
+			let permission = <PalletCommon<T>>::property_permissions(collection.id)
+				.get(&key)
+				.cloned()
+				.unwrap_or_else(PropertyPermission::none);
+
+			let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))
+				.get(&key)
+				.is_some();
+
+			match permission {
+				PropertyPermission { mutable: false, .. } if is_property_exists => {
+					return Err(<CommonError<T>>::NoPermission.into());
+				}
+
+				PropertyPermission {
+					collection_admin,
+					token_owner,
+					..
+				} => {
+					//TODO: investigate threats during public minting.
+					let is_token_create =
+						is_token_create && (collection_admin || token_owner) && value.is_some();
+					if !(is_token_create
+						|| (collection_admin && is_collection_admin())
+						|| (token_owner && is_token_owner()?))
+					{
+						fail!(<CommonError<T>>::NoPermission);
+					}
+				}
+			}
+
+			match value {
+				Some(value) => {
+					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
+						properties.try_set(key.clone(), value)
+					})
+					.map_err(<CommonError<T>>::from)?;
+
+					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(
+						collection.id,
+						token_id,
+						key,
+					));
+				}
+				None => {
+					<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {
+						properties.remove(&key)
+					})
+					.map_err(<CommonError<T>>::from)?;
+
+					<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(
+						collection.id,
+						token_id,
+						key,
+					));
+				}
+			}
+		}
+
+		Ok(())
+	}
+
+	pub fn set_token_properties(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		token_id: TokenId,
+		properties: impl Iterator<Item = Property>,
+		is_token_create: bool,
+		nesting_budget: &dyn Budget,
+	) -> DispatchResult {
+		Self::modify_token_properties(
+			collection,
+			sender,
+			token_id,
+			properties.map(|p| (p.key, Some(p.value))),
+			is_token_create,
+			nesting_budget,
+		)
+	}
+
+	pub fn set_token_property(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		token_id: TokenId,
+		property: Property,
+		nesting_budget: &dyn Budget,
+	) -> DispatchResult {
+		let is_token_create = false;
+
+		Self::set_token_properties(
+			collection,
+			sender,
+			token_id,
+			[property].into_iter(),
+			is_token_create,
+			nesting_budget,
+		)
+	}
+
+	pub fn delete_token_properties(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		token_id: TokenId,
+		property_keys: impl Iterator<Item = PropertyKey>,
+		nesting_budget: &dyn Budget,
+	) -> DispatchResult {
+		let is_token_create = false;
+
+		Self::modify_token_properties(
+			collection,
+			sender,
+			token_id,
+			property_keys.into_iter().map(|key| (key, None)),
+			is_token_create,
+			nesting_budget,
+		)
+	}
+
+	pub fn delete_token_property(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		token_id: TokenId,
+		property_key: PropertyKey,
+		nesting_budget: &dyn Budget,
+	) -> DispatchResult {
+		Self::delete_token_properties(
+			collection,
+			sender,
+			token_id,
+			[property_key].into_iter(),
+			nesting_budget,
+		)
+	}
+
+	/// Transfer RFT token pieces from one account to another.
+	///
+	/// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.
+	///
+	/// - `from`: Owner of token pieces to transfer.
+	/// - `to`: Recepient of transfered token pieces.
+	/// - `amount`: Amount of token pieces to transfer.
+	/// - `token`: Token whos pieces should be transfered
+	/// - `collection`: Collection that contains the token
 	pub fn transfer(
 		collection: &RefungibleHandle<T>,
 		from: &T::CrossAccountId,
@@ -422,7 +768,17 @@
 			}
 		}
 
-		// TODO: ERC20 transfer event
+		<PalletEvm<T>>::deposit_log(
+			ERC20Events::Transfer {
+				from: *from.as_eth(),
+				to: *to.as_eth(),
+				value: amount.into(),
+			}
+			.to_log(T::EvmTokenAddressMapping::token_to_address(
+				collection.id,
+				token,
+			)),
+		);
 		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(
 			collection.id,
 			token,
@@ -433,6 +789,11 @@
 		Ok(())
 	}
 
+	/// Batched operation to create multiple RFT tokens.
+	///
+	/// Same as `create_item` but creates multiple tokens.
+	///
+	/// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.
 	pub fn create_multiple_items(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -517,34 +878,70 @@
 
 		// =========
 
+		with_transaction(|| {
+			for (i, data) in data.iter().enumerate() {
+				let token_id = first_token_id + i as u32 + 1;
+				<TotalSupply<T>>::insert((collection.id, token_id), totals[i]);
+
+				<TokenData<T>>::insert(
+					(collection.id, token_id),
+					ItemData {
+						const_data: data.const_data.clone(),
+					},
+				);
+
+				for (user, amount) in data.users.iter() {
+					if *amount == 0 {
+						continue;
+					}
+					<Balance<T>>::insert((collection.id, token_id, &user), amount);
+					<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);
+					<PalletStructure<T>>::nest_if_sent_to_token_unchecked(
+						user,
+						collection.id,
+						TokenId(token_id),
+					);
+				}
+
+				if let Err(e) = Self::set_token_properties(
+					collection,
+					sender,
+					TokenId(token_id),
+					data.properties.clone().into_iter(),
+					true,
+					nesting_budget,
+				) {
+					return TransactionOutcome::Rollback(Err(e));
+				}
+			}
+			TransactionOutcome::Commit(Ok(()))
+		})?;
+
 		<TokensMinted<T>>::insert(collection.id, tokens_minted);
+
 		for (account, balance) in balances {
 			<AccountBalance<T>>::insert((collection.id, account), balance);
 		}
+
 		for (i, token) in data.into_iter().enumerate() {
 			let token_id = first_token_id + i as u32 + 1;
-			<TotalSupply<T>>::insert((collection.id, token_id), totals[i]);
-
-			<TokenData<T>>::insert(
-				(collection.id, token_id),
-				ItemData {
-					const_data: token.const_data,
-				},
-			);
 
 			for (user, amount) in token.users.into_iter() {
 				if amount == 0 {
 					continue;
 				}
-				<Balance<T>>::insert((collection.id, token_id, &user), amount);
-				<Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);
-				<PalletStructure<T>>::nest_if_sent_to_token_unchecked(
-					&user,
-					collection.id,
-					TokenId(token_id),
+
+				<PalletEvm<T>>::deposit_log(
+					ERC20Events::Transfer {
+						from: H160::default(),
+						to: *user.as_eth(),
+						value: amount.into(),
+					}
+					.to_log(T::EvmTokenAddressMapping::token_to_address(
+						collection.id,
+						TokenId(token_id),
+					)),
 				);
-
-				// TODO: ERC20 transfer event
 				<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(
 					collection.id,
 					TokenId(token_id),
@@ -568,7 +965,18 @@
 		} else {
 			<Allowance<T>>::insert((collection.id, token, sender, spender), amount);
 		}
-		// TODO: ERC20 approval event
+
+		<PalletEvm<T>>::deposit_log(
+			ERC20Events::Approval {
+				owner: *sender.as_eth(),
+				spender: *spender.as_eth(),
+				value: amount.into(),
+			}
+			.to_log(T::EvmTokenAddressMapping::token_to_address(
+				collection.id,
+				token,
+			)),
+		);
 		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(
 			collection.id,
 			token,
@@ -578,6 +986,9 @@
 		))
 	}
 
+	/// Set allowance for the spender to `transfer` or `burn` sender's token pieces.
+	///
+	/// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.
 	pub fn set_allowance(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -646,6 +1057,12 @@
 		Ok(allowance)
 	}
 
+	/// Transfer RFT token pieces from one account to another.
+	///
+	/// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.
+	/// The owner should set allowance for the spender to transfer pieces.
+	///
+	/// [`transfer`]: struct.Pallet.html#method.transfer
 	pub fn transfer_from(
 		collection: &RefungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -667,6 +1084,12 @@
 		Ok(())
 	}
 
+	/// Burn RFT token pieces from the account.
+	///
+	/// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should
+	/// set allowance for the spender to burn pieces
+	///
+	/// [`burn`]: struct.Pallet.html#method.burn
 	pub fn burn_from(
 		collection: &RefungibleHandle<T>,
 		spender: &T::CrossAccountId,
@@ -687,7 +1110,13 @@
 		Ok(())
 	}
 
-	/// Delegated to `create_multiple_items`
+	/// Create RFT token.
+	///
+	/// The sender should be the owner/admin of the collection or collection should be configured
+	/// to allow public minting.
+	///
+	/// - `data`: Contains list of users who will become the owners of the token pieces and amount
+	///   of token pieces they will receive.
 	pub fn create_item(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
@@ -697,6 +1126,13 @@
 		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)
 	}
 
+	/// Repartition RFT token.
+	///
+	/// `repartition` will set token balance of the sender and total amount of token pieces.
+	/// Sender should own all of the token pieces. `repartition' could be done even if some
+	/// token pieces were burned before.
+	///
+	/// - `amount`: Total amount of token pieces that the token will have after `repartition`.
 	pub fn repartition(
 		collection: &RefungibleHandle<T>,
 		owner: &T::CrossAccountId,
@@ -709,19 +1145,119 @@
 		);
 		ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);
 		// Ensure user owns all pieces
-		let total_supply = <TotalSupply<T>>::get((collection.id, token));
+		let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);
 		let balance = <Balance<T>>::get((collection.id, token, owner));
 		ensure!(
-			total_supply == balance,
+			total_pieces == balance,
 			<Error<T>>::RepartitionWhileNotOwningAllPieces
 		);
 
 		<Balance<T>>::insert((collection.id, token, owner), amount);
 		<TotalSupply<T>>::insert((collection.id, token), amount);
+
+		if amount > total_pieces {
+			let mint_amount = amount - total_pieces;
+			<PalletEvm<T>>::deposit_log(
+				ERC20Events::Transfer {
+					from: H160::default(),
+					to: *owner.as_eth(),
+					value: mint_amount.into(),
+				}
+				.to_log(T::EvmTokenAddressMapping::token_to_address(
+					collection.id,
+					token,
+				)),
+			);
+			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(
+				collection.id,
+				token,
+				owner.clone(),
+				mint_amount,
+			));
+		} else if total_pieces > amount {
+			let burn_amount = total_pieces - amount;
+			<PalletEvm<T>>::deposit_log(
+				ERC20Events::Transfer {
+					from: *owner.as_eth(),
+					to: H160::default(),
+					value: burn_amount.into(),
+				}
+				.to_log(T::EvmTokenAddressMapping::token_to_address(
+					collection.id,
+					token,
+				)),
+			);
+			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
+				collection.id,
+				token,
+				owner.clone(),
+				burn_amount,
+			));
+		}
+
 		Ok(())
 	}
 
+	fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {
+		let mut owner = None;
+		let mut count = 0;
+		for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {
+			count += 1;
+			if count > 1 {
+				return None;
+			}
+			owner = Some(key);
+		}
+		owner
+	}
+
 	fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {
 		<TotalSupply<T>>::try_get((collection_id, token_id)).ok()
 	}
+
+	pub fn set_collection_properties(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		properties: Vec<Property>,
+	) -> DispatchResult {
+		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)
+	}
+
+	pub fn delete_collection_properties(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		property_keys: Vec<PropertyKey>,
+	) -> DispatchResult {
+		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)
+	}
+
+	pub fn set_token_property_permissions(
+		collection: &RefungibleHandle<T>,
+		sender: &T::CrossAccountId,
+		property_permissions: Vec<PropertyKeyPermission>,
+	) -> DispatchResult {
+		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)
+	}
+
+	/// Returns 10 token in no particular order.
+	///
+	/// There is no direct way to get token holders in ascending order,
+	/// since `iter_prefix` returns values in no particular order.
+	/// Therefore, getting the 10 largest holders with a large value of holders
+	/// can lead to impact memory allocation + sorting with  `n * log (n)`.
+	pub fn token_owners(
+		collection_id: CollectionId,
+		token: TokenId,
+	) -> Option<Vec<T::CrossAccountId>> {
+		let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))
+			.map(|(owner, _amount)| owner)
+			.take(10)
+			.collect();
+
+		if res.is_empty() {
+			None
+		} else {
+			Some(res)
+		}
+	}
 }
modifiedpallets/refungible/src/stubs/UniqueRefungibleToken.rawdiffbeforeafterboth

binary blob — no preview

addedpallets/refungible/src/stubs/UniqueRefungibleToken.soldiffbeforeafterboth
--- /dev/null
+++ b/pallets/refungible/src/stubs/UniqueRefungibleToken.sol
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+// Common stubs holder
+contract Dummy {
+	uint8 dummy;
+	string stub_error = "this contract is implemented in native";
+}
+
+contract ERC165 is Dummy {
+	function supportsInterface(bytes4 interfaceID)
+		external
+		view
+		returns (bool)
+	{
+		require(false, stub_error);
+		interfaceID;
+		return true;
+	}
+}
+
+// Inline
+contract ERC20Events {
+	event Transfer(address indexed from, address indexed to, uint256 value);
+	event Approval(
+		address indexed owner,
+		address indexed spender,
+		uint256 value
+	);
+}
+
+// Selector: 942e8b22
+contract ERC20 is Dummy, ERC165, ERC20Events {
+	// Selector: name() 06fdde03
+	function name() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	// Selector: symbol() 95d89b41
+	function symbol() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	// Selector: totalSupply() 18160ddd
+	function totalSupply() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	// Selector: decimals() 313ce567
+	function decimals() public view returns (uint8) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	// Selector: balanceOf(address) 70a08231
+	function balanceOf(address owner) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		dummy;
+		return 0;
+	}
+
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: transferFrom(address,address,uint256) 23b872dd
+	function transferFrom(
+		address from,
+		address to,
+		uint256 amount
+	) public returns (bool) {
+		require(false, stub_error);
+		from;
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: approve(address,uint256) 095ea7b3
+	function approve(address spender, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		spender;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: allowance(address,address) dd62ed3e
+	function allowance(address owner, address spender)
+		public
+		view
+		returns (uint256)
+	{
+		require(false, stub_error);
+		owner;
+		spender;
+		dummy;
+		return 0;
+	}
+}
+
+// Selector: ab8deb37
+contract ERC20UniqueExtensions is Dummy, ERC165 {
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		from;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	// Selector: repartition(uint256) d2418ca7
+	function repartition(uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		amount;
+		dummy = 0;
+		return false;
+	}
+}
+
+contract UniqueRefungibleToken is Dummy, ERC165, ERC20, ERC20UniqueExtensions {}
modifiedpallets/refungible/src/weights.rsdiffbeforeafterboth
--- a/pallets/refungible/src/weights.rs
+++ b/pallets/refungible/src/weights.rs
@@ -3,7 +3,7 @@
 //! Autogenerated weights for pallet_refungible
 //!
 //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
-//! DATE: 2022-06-27, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! DATE: 2022-07-20, STEPS: `50`, REPEAT: 80, LOW RANGE: `[]`, HIGH RANGE: `[]`
 //! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: None, DB CACHE: 1024
 
 // Executed Command:
@@ -49,6 +49,9 @@
 	fn transfer_from_removing() -> Weight;
 	fn transfer_from_creating_removing() -> Weight;
 	fn burn_from() -> Weight;
+	fn set_token_property_permissions(b: u32, ) -> Weight;
+	fn set_token_properties(b: u32, ) -> Weight;
+	fn delete_token_properties(b: u32, ) -> Weight;
 	fn repartition_item() -> Weight;
 }
 
@@ -62,7 +65,7 @@
 	// Storage: Refungible TokenData (r:0 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn create_item() -> Weight {
-		(17_553_000 as Weight)
+		(21_310_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(6 as Weight))
 	}
@@ -73,9 +76,9 @@
 	// Storage: Refungible TokenData (r:0 w:4)
 	// Storage: Refungible Owned (r:0 w:4)
 	fn create_multiple_items(b: u32, ) -> Weight {
-		(10_654_000 as Weight)
-			// Standard Error: 1_000
-			.saturating_add((5_114_000 as Weight).saturating_mul(b as Weight))
+		(9_552_000 as Weight)
+			// Standard Error: 2_000
+			.saturating_add((7_056_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes((4 as Weight).saturating_mul(b as Weight)))
@@ -87,9 +90,9 @@
 	// Storage: Refungible TokenData (r:0 w:4)
 	// Storage: Refungible Owned (r:0 w:4)
 	fn create_multiple_items_ex_multiple_items(b: u32, ) -> Weight {
-		(3_587_000 as Weight)
+		(4_857_000 as Weight)
 			// Standard Error: 2_000
-			.saturating_add((7_931_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add((9_838_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(T::DbWeight::get().reads(1 as Weight))
 			.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
@@ -102,9 +105,9 @@
 	// Storage: Refungible Balance (r:0 w:4)
 	// Storage: Refungible Owned (r:0 w:4)
 	fn create_multiple_items_ex_multiple_owners(b: u32, ) -> Weight {
-		(1_980_000 as Weight)
+		(11_335_000 as Weight)
 			// Standard Error: 2_000
-			.saturating_add((6_305_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add((6_784_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(T::DbWeight::get().reads(1 as Weight))
 			.saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
@@ -115,7 +118,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn burn_item_partial() -> Weight {
-		(21_010_000 as Weight)
+		(21_239_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(4 as Weight))
 	}
@@ -125,14 +128,15 @@
 	// Storage: Refungible TokensBurnt (r:1 w:1)
 	// Storage: Refungible TokenData (r:0 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
+	// Storage: Refungible TokenProperties (r:0 w:1)
 	fn burn_item_fully() -> Weight {
-		(28_413_000 as Weight)
+		(29_426_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(4 as Weight))
-			.saturating_add(T::DbWeight::get().writes(6 as Weight))
+			.saturating_add(T::DbWeight::get().writes(7 as Weight))
 	}
 	// Storage: Refungible Balance (r:2 w:2)
 	fn transfer_normal() -> Weight {
-		(17_513_000 as Weight)
+		(17_743_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(2 as Weight))
 	}
@@ -140,7 +144,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_creating() -> Weight {
-		(20_469_000 as Weight)
+		(20_699_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(4 as Weight))
 	}
@@ -148,7 +152,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_removing() -> Weight {
-		(22_472_000 as Weight)
+		(22_833_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(4 as Weight))
 	}
@@ -156,21 +160,21 @@
 	// Storage: Refungible AccountBalance (r:2 w:2)
 	// Storage: Refungible Owned (r:0 w:2)
 	fn transfer_creating_removing() -> Weight {
-		(24_866_000 as Weight)
+		(24_936_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(4 as Weight))
 			.saturating_add(T::DbWeight::get().writes(6 as Weight))
 	}
 	// Storage: Refungible Balance (r:1 w:0)
 	// Storage: Refungible Allowance (r:0 w:1)
 	fn approve() -> Weight {
-		(13_475_000 as Weight)
+		(13_446_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(1 as Weight))
 			.saturating_add(T::DbWeight::get().writes(1 as Weight))
 	}
 	// Storage: Refungible Allowance (r:1 w:1)
 	// Storage: Refungible Balance (r:2 w:2)
 	fn transfer_from_normal() -> Weight {
-		(24_707_000 as Weight)
+		(24_777_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(3 as Weight))
 			.saturating_add(T::DbWeight::get().writes(3 as Weight))
 	}
@@ -179,7 +183,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_from_creating() -> Weight {
-		(27_812_000 as Weight)
+		(28_483_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(4 as Weight))
 			.saturating_add(T::DbWeight::get().writes(5 as Weight))
 	}
@@ -188,7 +192,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_from_removing() -> Weight {
-		(29_966_000 as Weight)
+		(29_896_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(4 as Weight))
 			.saturating_add(T::DbWeight::get().writes(5 as Weight))
 	}
@@ -197,7 +201,7 @@
 	// Storage: Refungible AccountBalance (r:2 w:2)
 	// Storage: Refungible Owned (r:0 w:2)
 	fn transfer_from_creating_removing() -> Weight {
-		(31_660_000 as Weight)
+		(32_070_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(5 as Weight))
 			.saturating_add(T::DbWeight::get().writes(7 as Weight))
 	}
@@ -208,15 +212,42 @@
 	// Storage: Refungible TokensBurnt (r:1 w:1)
 	// Storage: Refungible TokenData (r:0 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
+	// Storage: Refungible TokenProperties (r:0 w:1)
 	fn burn_from() -> Weight {
-		(36_248_000 as Weight)
+		(36_789_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(5 as Weight))
-			.saturating_add(T::DbWeight::get().writes(7 as Weight))
+			.saturating_add(T::DbWeight::get().writes(8 as Weight))
+	}
+	// Storage: Common CollectionPropertyPermissions (r:1 w:1)
+	fn set_token_property_permissions(b: u32, ) -> Weight {
+		(0 as Weight)
+			// Standard Error: 62_000
+			.saturating_add((15_803_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(T::DbWeight::get().reads(1 as Weight))
+			.saturating_add(T::DbWeight::get().writes(1 as Weight))
+	}
+	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
+	// Storage: Refungible TokenProperties (r:1 w:1)
+	fn set_token_properties(b: u32, ) -> Weight {
+		(0 as Weight)
+			// Standard Error: 1_668_000
+			.saturating_add((302_308_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(T::DbWeight::get().reads(2 as Weight))
+			.saturating_add(T::DbWeight::get().writes(1 as Weight))
+	}
+	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
+	// Storage: Refungible TokenProperties (r:1 w:1)
+	fn delete_token_properties(b: u32, ) -> Weight {
+		(0 as Weight)
+			// Standard Error: 1_619_000
+			.saturating_add((294_574_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(T::DbWeight::get().reads(2 as Weight))
+			.saturating_add(T::DbWeight::get().writes(1 as Weight))
 	}
 	// Storage: Refungible TotalSupply (r:1 w:1)
 	// Storage: Refungible Balance (r:1 w:1)
 	fn repartition_item() -> Weight {
-		(8_226_000 as Weight)
+		(8_325_000 as Weight)
 			.saturating_add(T::DbWeight::get().reads(2 as Weight))
 			.saturating_add(T::DbWeight::get().writes(2 as Weight))
 	}
@@ -231,7 +262,7 @@
 	// Storage: Refungible TokenData (r:0 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn create_item() -> Weight {
-		(17_553_000 as Weight)
+		(21_310_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(6 as Weight))
 	}
@@ -242,9 +273,9 @@
 	// Storage: Refungible TokenData (r:0 w:4)
 	// Storage: Refungible Owned (r:0 w:4)
 	fn create_multiple_items(b: u32, ) -> Weight {
-		(10_654_000 as Weight)
-			// Standard Error: 1_000
-			.saturating_add((5_114_000 as Weight).saturating_mul(b as Weight))
+		(9_552_000 as Weight)
+			// Standard Error: 2_000
+			.saturating_add((7_056_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes((4 as Weight).saturating_mul(b as Weight)))
@@ -256,9 +287,9 @@
 	// Storage: Refungible TokenData (r:0 w:4)
 	// Storage: Refungible Owned (r:0 w:4)
 	fn create_multiple_items_ex_multiple_items(b: u32, ) -> Weight {
-		(3_587_000 as Weight)
+		(4_857_000 as Weight)
 			// Standard Error: 2_000
-			.saturating_add((7_931_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add((9_838_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(RocksDbWeight::get().reads(1 as Weight))
 			.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
@@ -271,9 +302,9 @@
 	// Storage: Refungible Balance (r:0 w:4)
 	// Storage: Refungible Owned (r:0 w:4)
 	fn create_multiple_items_ex_multiple_owners(b: u32, ) -> Weight {
-		(1_980_000 as Weight)
+		(11_335_000 as Weight)
 			// Standard Error: 2_000
-			.saturating_add((6_305_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add((6_784_000 as Weight).saturating_mul(b as Weight))
 			.saturating_add(RocksDbWeight::get().reads(1 as Weight))
 			.saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(b as Weight)))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
@@ -284,7 +315,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn burn_item_partial() -> Weight {
-		(21_010_000 as Weight)
+		(21_239_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(4 as Weight))
 	}
@@ -294,14 +325,15 @@
 	// Storage: Refungible TokensBurnt (r:1 w:1)
 	// Storage: Refungible TokenData (r:0 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
+	// Storage: Refungible TokenProperties (r:0 w:1)
 	fn burn_item_fully() -> Weight {
-		(28_413_000 as Weight)
+		(29_426_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(4 as Weight))
-			.saturating_add(RocksDbWeight::get().writes(6 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(7 as Weight))
 	}
 	// Storage: Refungible Balance (r:2 w:2)
 	fn transfer_normal() -> Weight {
-		(17_513_000 as Weight)
+		(17_743_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(2 as Weight))
 	}
@@ -309,7 +341,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_creating() -> Weight {
-		(20_469_000 as Weight)
+		(20_699_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(4 as Weight))
 	}
@@ -317,7 +349,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_removing() -> Weight {
-		(22_472_000 as Weight)
+		(22_833_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(4 as Weight))
 	}
@@ -325,21 +357,21 @@
 	// Storage: Refungible AccountBalance (r:2 w:2)
 	// Storage: Refungible Owned (r:0 w:2)
 	fn transfer_creating_removing() -> Weight {
-		(24_866_000 as Weight)
+		(24_936_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(4 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(6 as Weight))
 	}
 	// Storage: Refungible Balance (r:1 w:0)
 	// Storage: Refungible Allowance (r:0 w:1)
 	fn approve() -> Weight {
-		(13_475_000 as Weight)
+		(13_446_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(1 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
 	}
 	// Storage: Refungible Allowance (r:1 w:1)
 	// Storage: Refungible Balance (r:2 w:2)
 	fn transfer_from_normal() -> Weight {
-		(24_707_000 as Weight)
+		(24_777_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(3 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(3 as Weight))
 	}
@@ -348,7 +380,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_from_creating() -> Weight {
-		(27_812_000 as Weight)
+		(28_483_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(4 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(5 as Weight))
 	}
@@ -357,7 +389,7 @@
 	// Storage: Refungible AccountBalance (r:1 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
 	fn transfer_from_removing() -> Weight {
-		(29_966_000 as Weight)
+		(29_896_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(4 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(5 as Weight))
 	}
@@ -366,7 +398,7 @@
 	// Storage: Refungible AccountBalance (r:2 w:2)
 	// Storage: Refungible Owned (r:0 w:2)
 	fn transfer_from_creating_removing() -> Weight {
-		(31_660_000 as Weight)
+		(32_070_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(7 as Weight))
 	}
@@ -377,15 +409,42 @@
 	// Storage: Refungible TokensBurnt (r:1 w:1)
 	// Storage: Refungible TokenData (r:0 w:1)
 	// Storage: Refungible Owned (r:0 w:1)
+	// Storage: Refungible TokenProperties (r:0 w:1)
 	fn burn_from() -> Weight {
-		(36_248_000 as Weight)
+		(36_789_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(5 as Weight))
-			.saturating_add(RocksDbWeight::get().writes(7 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(8 as Weight))
+	}
+	// Storage: Common CollectionPropertyPermissions (r:1 w:1)
+	fn set_token_property_permissions(b: u32, ) -> Weight {
+		(0 as Weight)
+			// Standard Error: 62_000
+			.saturating_add((15_803_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(RocksDbWeight::get().reads(1 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
+	}
+	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
+	// Storage: Refungible TokenProperties (r:1 w:1)
+	fn set_token_properties(b: u32, ) -> Weight {
+		(0 as Weight)
+			// Standard Error: 1_668_000
+			.saturating_add((302_308_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
 	}
+	// Storage: Common CollectionPropertyPermissions (r:1 w:0)
+	// Storage: Refungible TokenProperties (r:1 w:1)
+	fn delete_token_properties(b: u32, ) -> Weight {
+		(0 as Weight)
+			// Standard Error: 1_619_000
+			.saturating_add((294_574_000 as Weight).saturating_mul(b as Weight))
+			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
+			.saturating_add(RocksDbWeight::get().writes(1 as Weight))
+	}
 	// Storage: Refungible TotalSupply (r:1 w:1)
 	// Storage: Refungible Balance (r:1 w:1)
 	fn repartition_item() -> Weight {
-		(8_226_000 as Weight)
+		(8_325_000 as Weight)
 			.saturating_add(RocksDbWeight::get().reads(2 as Weight))
 			.saturating_add(RocksDbWeight::get().writes(2 as Weight))
 	}
modifiedpallets/scheduler/src/lib.rsdiffbeforeafterboth
--- a/pallets/scheduler/src/lib.rs
+++ b/pallets/scheduler/src/lib.rs
@@ -24,7 +24,7 @@
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
 //
-// 	http://www.apache.org/licenses/LICENSE-2.0
+// 	<http://www.apache.org/licenses/LICENSE-2.0>
 //
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "AS IS" BASIS,
@@ -32,27 +32,36 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! # Schedulerdo_reschedule
+//! # Unique scheduler
+//! A Pallet for scheduling dispatches.
+//!
+//! - [`Config`]
+//! - [`Call`]
+//! - [`Pallet`]
+//!
+//! ## Overview
 //!
 //! This Pallet exposes capabilities for scheduling dispatches to occur at a
 //! specified block number or at a specified period. These scheduled dispatches
-//! may be named or anonymous and may be canceled.
+//! should be named and may be canceled.
 //!
-//! **NOTE:** The scheduled calls will be dispatched with the default filter
-//! for the origin: namely `frame_system::Config::BaseCallFilter` for all origin
-//! except root which will get no filter. And not the filter contained in origin
-//! use to call `fn schedule`.
+//! **NOTE:** The unique scheduler is designed for deferred transaction calls by block number.
+//! Any user can book a call of a certain transaction to a specific block number.
+//! Also possible to book a call with a certain frequency.
 //!
-//! If a call is scheduled using proxy or whatever mecanism which adds filter,
-//! then those filter will not be used when dispatching the schedule call.
+//! Key differences from the original pallet:
+//! <https://crates.io/crates/pallet-scheduler>
+//! Schedule Id restricted by 16 bytes. Identificator for booked call.
+//! Priority limited by HARD DEADLINE (<= 63). Calls over maximum weight don't include to block.
+//! The maximum weight that may be scheduled per block for any dispatchables of less priority than `schedule::HARD_DEADLINE`.
+//! Maybe_periodic limit is 100 calls. Reserved for future sponsored transaction support.
+//! At 100 calls reserved amount is not so much and this is avoid potential problems with balance locks.
+//! Any account allowed to schedule any calls. Account withdraw implemented through default transaction logic.
 //!
 //! ## Interface
 //!
 //! ### Dispatchable Functions
 //!
-//! * `schedule` - schedule a dispatch, which may be periodic, to occur at a specified block and
-//!   with a specified priority.
-//! * `cancel` - cancel a scheduled dispatch, specified by block number and index.
 //! * `schedule_named` - augments the `schedule` interface with an additional `Vec<u8>` parameter
 //!   that can be used for identification.
 //! * `cancel_named` - the named complement to the cancel function.
@@ -258,7 +267,7 @@
 
 	/// A Scheduler-Runtime interface for finer payment handling.
 	pub trait DispatchCall<T: frame_system::Config + Config, SelfContainedSignedInfo> {
-		/// Reserve the maximum spendings on a call.
+		/// Reserve (lock) the maximum spendings on a call, calculated from its weight and the repetition count.
 		fn reserve_balance(
 			id: ScheduledId,
 			sponsor: <T as frame_system::Config>::AccountId,
@@ -266,7 +275,7 @@
 			count: u32,
 		) -> Result<(), DispatchError>;
 
-		/// Pay for call dispatch (un-reserve) from the reserved funds, returning the change.
+		/// Unreserve (unlock) a certain amount from the payer's reserved funds, returning the change.
 		fn pay_for_call(
 			id: ScheduledId,
 			sponsor: <T as frame_system::Config>::AccountId,
@@ -282,7 +291,7 @@
 			TransactionValidityError,
 		>;
 
-		/// Release reserved funds.
+		/// Release unspent reserved funds in case of a schedule cancel.
 		fn cancel_reserve(
 			id: ScheduledId,
 			sponsor: <T as frame_system::Config>::AccountId,
@@ -441,6 +450,8 @@
 				// 	);
 				// }
 
+				// Execute transaction via chain default pipeline
+				// That means dispatch will be processed like any user's extrinsic e.g. transaction fees will be taken
 				let r = T::CallExecutor::dispatch_call(sender, call.clone());
 
 				let mut actual_call_weight: Weight = item_weight;
@@ -485,8 +496,8 @@
 					Agenda::<T>::append(wake, Some(s));
 				}
 			}
+			// Total weight should be 0, because the transaction is already paid for
 			0
-			//total_weight
 		}
 	}
 
modifiedpallets/structure/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/structure/src/benchmarking.rs
+++ b/pallets/structure/src/benchmarking.rs
@@ -1,3 +1,19 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
 use super::*;
 
 use frame_benchmarking::{benchmarks, account};
modifiedpallets/structure/src/lib.rsdiffbeforeafterboth
--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -1,3 +1,56 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+//! # Structure Pallet
+//!
+//! The Structure pallet provides functionality for handling tokens nesting an unnesting.
+//!
+//! - [`Config`]
+//! - [`Pallet`]
+//!
+//! ## Overview
+//!
+//! The Structure pallet provides functions for:
+//!
+//! - Searching for token parents, children and owners. Actual implementation of searching for
+//!   parent/child is done by pallets corresponding to token's collection type.
+//! - Nesting and unnesting tokens. Actual implementation of nesting is done by pallets corresponding
+//!   to token's collection type.
+//!
+//! ### Terminology
+//!
+//! - **Nesting:** Setting up parent-child relationship between tokens. Nested tokens are inhereting
+//!   owner from their parent. There could be multiple levels of nesting. Token couldn't be nested in
+//!   it's child token i.e. parent-child relationship graph shouldn't have
+//!
+//! - **Parent:** Token that current token is nested in.
+//!
+//! - **Owner:** Account that owns the token and all nested tokens.
+//!
+//! ## Interface
+//!
+//! ### Available Functions
+//!
+//! - `find_parent` - Find parent of the token. It could be an account or another token.
+//! - `parent_chain` - Find chain of parents of the token.
+//! - `find_topmost_owner` - Find account or token in the end of the chain of parents.
+//! - `check_nesting` - Check if the token could be nested in the other token
+//! - `nest_if_sent_to_token` - Nest the token in the other token
+//! - `unnest_if_nested` - Unnest the token from the other token
+
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use pallet_common::CommonCollectionOperations;
@@ -82,6 +135,12 @@
 }
 
 impl<T: Config> Pallet<T> {
+	/// Find account owning the `token` or a token that the `token` is nested in.
+	///
+	/// Returns the enum that have three variants:
+	/// - [`User`](crate::Parent<T>::User): Contains account.
+	/// - [`Token`](crate::Parent<T>::Token): Contains token id and collection id.
+	/// - [`TokenNotFound`](crate::Parent<T>::TokenNotFound): Indicates that parent was not found
 	pub fn find_parent(
 		collection: CollectionId,
 		token: TokenId,
@@ -103,6 +162,11 @@
 		})
 	}
 
+	/// Get the chain of parents of a token in the nesting hierarchy
+	///
+	/// Returns an iterator of addresses of the owning tokens and the owning account,
+	/// starting from the immediate parent token, ending with the account.
+	/// Returns error if cycle is detected.
 	pub fn parent_chain(
 		mut collection: CollectionId,
 		mut token: TokenId,
@@ -133,6 +197,8 @@
 	/// Try to dereference address, until finding top level owner
 	///
 	/// May return token address if parent token not yet exists
+	///
+	/// - `budget`: Limit for searching parents in depth.
 	pub fn find_topmost_owner(
 		collection: CollectionId,
 		token: TokenId,
@@ -149,6 +215,10 @@
 		})
 	}
 
+	/// Find the topmost parent and check that assigning `for_nest` token as a child for
+	/// `token` wouldn't create a cycle.
+	///
+	/// - `budget`: Limit for searching parents in depth.
 	pub fn get_checked_topmost_owner(
 		collection: CollectionId,
 		token: TokenId,
@@ -177,6 +247,10 @@
 		Err(<Error<T>>::DepthLimit.into())
 	}
 
+	/// Burn token and all of it's nested tokens
+	///
+	/// - `self_budget`: Limit for searching children in depth.
+	/// - `breadth_budget`: Limit of breadth of searching children.
 	pub fn burn_item_recursively(
 		from: T::CrossAccountId,
 		collection: CollectionId,
@@ -190,7 +264,13 @@
 		dispatch.burn_item_recursively(from.clone(), token, self_budget, breadth_budget)
 	}
 
-	/// Check if token indirectly owned by specified user
+	/// Check if `token` indirectly owned by `user`
+	///
+	/// Returns `true` if `user` is `token`'s owner. Or If token is provided as `user` then
+	/// check that `user` and `token` have same owner.
+	/// Checks that assigning `for_nest` token as a child for `token` wouldn't create a cycle.
+	///
+	/// - `budget`: Limit for searching parents in depth.
 	pub fn check_indirectly_owned(
 		user: T::CrossAccountId,
 		collection: CollectionId,
@@ -207,6 +287,12 @@
 			.map(|indirect_owner| indirect_owner == target_parent)
 	}
 
+	/// Checks that `under` is valid token and that `token_id` could be nested under it
+	/// and that `from` is `under`'s owner
+	///
+	/// Returns OK if `under` is not a token
+	///
+	/// - `nesting_budget`: Limit for searching parents in depth.
 	pub fn check_nesting(
 		from: T::CrossAccountId,
 		under: &T::CrossAccountId,
@@ -219,6 +305,11 @@
 		})
 	}
 
+	/// Nests `token_id` under `under` token
+	///
+	/// Returns OK if `under` is not a token. Checks that nesting is possible.
+	///
+	/// - `nesting_budget`: Limit for searching parents in depth.
 	pub fn nest_if_sent_to_token(
 		from: T::CrossAccountId,
 		under: &T::CrossAccountId,
@@ -235,6 +326,9 @@
 		})
 	}
 
+	/// Nests `token_id` under `owner` token
+	///
+	/// Caller should check that nesting wouldn't cause recursion in nesting
 	pub fn nest_if_sent_to_token_unchecked(
 		owner: &T::CrossAccountId,
 		collection_id: CollectionId,
@@ -245,6 +339,7 @@
 		});
 	}
 
+	/// Unnests `token_id` from `owner`.
 	pub fn unnest_if_nested(
 		owner: &T::CrossAccountId,
 		collection_id: CollectionId,
modifiedpallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterboth

binary blob — no preview

modifiedprimitives/data-structs/Cargo.tomldiffbeforeafterboth
--- a/primitives/data-structs/Cargo.toml
+++ b/primitives/data-structs/Cargo.toml
@@ -6,7 +6,7 @@
 license = 'GPLv3'
 homepage = "https://unique.network"
 repository = 'https://github.com/UniqueNetwork/unique-chain'
-version = '0.1.0'
+version = '0.1.1'
 
 [dependencies]
 scale-info = { version = "2.0.1", default-features = false, features = [
addedprimitives/data-structs/Changelog.mddiffbeforeafterboth
--- /dev/null
+++ b/primitives/data-structs/Changelog.md
@@ -0,0 +1,3 @@
+### 0.1.1
+---
+* Added fields with properties to `CreateReFungibleData` and `CreateRefungibleExData`.
\ No newline at end of file
modifiedprimitives/data-structs/src/lib.rsdiffbeforeafterboth
--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -563,8 +563,14 @@
 	#[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))]
 	#[derivative(Debug(format_with = "bounded::vec_debug"))]
 	pub const_data: BoundedVec<u8, CustomDataLimit>,
+
 	/// Number of pieces the RFT is split into
 	pub pieces: u128,
+
+	/// Key-value pairs used to describe the token as metadata
+	#[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))]
+	#[derivative(Debug(format_with = "bounded::vec_debug"))]
+	pub properties: CollectionPropertiesVec,
 }
 
 #[derive(Encode, Decode, Debug, Clone, PartialEq, TypeInfo, MaxEncodedLen)]
@@ -601,6 +607,8 @@
 	pub const_data: BoundedVec<u8, CustomDataLimit>,
 	#[derivative(Debug(format_with = "bounded::map_debug"))]
 	pub users: BoundedBTreeMap<CrossAccountId, u128, ConstU32<MAX_ITEMS_PER_BATCH>>,
+	#[derivative(Debug(format_with = "bounded::vec_debug"))]
+	pub properties: CollectionPropertiesVec,
 }
 
 /// Explicit item creation data with meta parameters, namely the owner.
addedprimitives/rpc/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/primitives/rpc/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [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. 
\ No newline at end of file
modifiedprimitives/rpc/Cargo.tomldiffbeforeafterboth
--- a/primitives/rpc/Cargo.toml
+++ b/primitives/rpc/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "up-rpc"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedprimitives/rpc/src/lib.rsdiffbeforeafterboth
--- a/primitives/rpc/src/lib.rs
+++ b/primitives/rpc/src/lib.rs
@@ -81,5 +81,6 @@
 		fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result<Option<u64>>;
 		fn effective_collection_limits(collection_id: CollectionId) -> Result<Option<CollectionLimits>>;
 		fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Result<Option<u128>>;
+		fn token_owners(collection: CollectionId, token: TokenId) -> Result<Vec<CrossAccountId>>;
 	}
 }
addedruntime/common/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/runtime/common/CHANGELOG.md
@@ -0,0 +1,13 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.9.25] - 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. 
+
+
+ 
\ No newline at end of file
modifiedruntime/common/src/dispatch.rsdiffbeforeafterboth
--- a/runtime/common/src/dispatch.rs
+++ b/runtime/common/src/dispatch.rs
@@ -25,7 +25,9 @@
 pub use pallet_common::dispatch::CollectionDispatch;
 use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};
 use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};
-use pallet_refungible::{Pallet as PalletRefungible, RefungibleHandle, erc::RefungibleTokenHandle};
+use pallet_refungible::{
+	Pallet as PalletRefungible, RefungibleHandle, erc_token::RefungibleTokenHandle,
+};
 use up_data_structs::{
 	CollectionMode, CreateCollectionData, MAX_DECIMAL_POINTS, mapping::TokenAddressMapping,
 };
modifiedruntime/common/src/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -40,6 +40,11 @@
                 fn token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
                     dispatch_unique_runtime!(collection.token_owner(token))
                 }
+
+                fn token_owners(collection: CollectionId, token: TokenId) -> Result<Vec::<CrossAccountId>, DispatchError>  {
+                   dispatch_unique_runtime!(collection.token_owners(token))
+                }
+
                 fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
                     let budget = up_data_structs::budget::Value::new(10);
 
modifiedruntime/tests/src/tests.rsdiffbeforeafterboth
--- a/runtime/tests/src/tests.rs
+++ b/runtime/tests/src/tests.rs
@@ -64,6 +64,12 @@
 	CreateReFungibleData {
 		const_data: vec![1, 2, 3].try_into().unwrap(),
 		pieces: 1023,
+		properties: vec![Property {
+			key: b"test-prop".to_vec().try_into().unwrap(),
+			value: b"test-nft-prop".to_vec().try_into().unwrap(),
+		}]
+		.try_into()
+		.unwrap(),
 	}
 }
 
addedtests/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/tests/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## 2022-07-14
+
+### Added
+ - Integrintegration tests of RPC method `token_owners`.
+ - Integrintegration tests of Fungible Pallet.
+  
+ 
\ No newline at end of file
modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -80,6 +80,8 @@
     "testLimits": "mocha --timeout 9999999 -r ts-node/register ./**/limits.test.ts",
     "testEthCreateCollection": "mocha --timeout 9999999 -r ts-node/register ./**/eth/createCollection.test.ts",
     "testRFT": "mocha --timeout 9999999 -r ts-node/register ./**/refungible.test.ts",
+    "testFT": "mocha --timeout 9999999 -r ts-node/register ./**/fungible.test.ts",
+    "testRPC": "mocha --timeout 9999999 -r ts-node/register ./**/rpc.test.ts",
     "polkadot-types-fetch-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/interfaces/metadata.json",
     "polkadot-types-from-defs": "ts-node ./node_modules/.bin/polkadot-types-from-defs --endpoint src/interfaces/metadata.json --input src/interfaces/ --package .",
     "polkadot-types-from-chain": "ts-node ./node_modules/.bin/polkadot-types-from-chain --endpoint src/interfaces/metadata.json --output src/interfaces/ --package .",
modifiedtests/src/createMultipleItems.test.tsdiffbeforeafterboth
--- a/tests/src/createMultipleItems.test.ts
+++ b/tests/src/createMultipleItems.test.ts
@@ -376,14 +376,26 @@
       // ReFungible
       const collectionIdReFungible =
         await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
-      const argsReFungible = [
-        {ReFungible: ['1'.repeat(2049), 10]},
-        {ReFungible: ['2'.repeat(2049), 10]},
-        {ReFungible: ['3'.repeat(2049), 10]},
-      ];
-      const createMultipleItemsTxFungible = api.tx.unique
-        .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible);
-      await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected;
+      {
+        const argsReFungible = [
+          {ReFungible: ['1'.repeat(2049), 10, []]},
+          {ReFungible: ['2'.repeat(2049), 10, []]},
+          {ReFungible: ['3'.repeat(2049), 10, []]},
+        ];
+        const createMultipleItemsTxFungible = api.tx.unique
+          .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible);
+        await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected;
+      }
+      {
+        const argsReFungible = [
+          {ReFungible: {properties: [{key: 'key', value: 'A'.repeat(32769)}]}},
+          {ReFungible: {properties: [{key: 'key', value: 'B'.repeat(32769)}]}},
+          {ReFungible: {properties: [{key: 'key', value: 'C'.repeat(32769)}]}},
+        ];
+        const createMultipleItemsTxFungible = api.tx.unique
+          .createMultipleItems(collectionIdReFungible, normalizeAccountId(alice.address), argsReFungible);
+        await expect(submitTransactionExpectFailAsync(alice, createMultipleItemsTxFungible)).to.be.rejected;
+      }
     });
   });
 
modifiedtests/src/eth/api/UniqueFungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueFungible.sol
+++ b/tests/src/eth/api/UniqueFungible.sol
@@ -28,6 +28,154 @@
 	function burnFrom(address from, uint256 amount) external returns (bool);
 }
 
+// Selector: 7d9262e6
+interface Collection is Dummy, ERC165 {
+	// Set collection property.
+	//
+	// @param key Property key.
+	// @param value Propery value.
+	//
+	// Selector: setCollectionProperty(string,bytes) 2f073f66
+	function setCollectionProperty(string memory key, bytes memory value)
+		external;
+
+	// Delete collection property.
+	//
+	// @param key Property key.
+	//
+	// Selector: deleteCollectionProperty(string) 7b7debce
+	function deleteCollectionProperty(string memory key) external;
+
+	// Get collection property.
+	//
+	// @dev Throws error if key not found.
+	//
+	// @param key Property key.
+	// @return bytes The property corresponding to the key.
+	//
+	// Selector: collectionProperty(string) cf24fd6d
+	function collectionProperty(string memory key)
+		external
+		view
+		returns (bytes memory);
+
+	// Set the sponsor of the collection.
+	//
+	// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
+	//
+	// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.
+	//
+	// Selector: setCollectionSponsor(address) 7623402e
+	function setCollectionSponsor(address sponsor) external;
+
+	// Collection sponsorship confirmation.
+	//
+	// @dev After setting the sponsor for the collection, it must be confirmed with this function.
+	//
+	// Selector: confirmCollectionSponsorship() 3c50e97a
+	function confirmCollectionSponsorship() external;
+
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"accountTokenOwnershipLimit",
+	// 	"sponsoredDataSize",
+	// 	"sponsoredDataRateLimit",
+	// 	"tokenLimit",
+	// 	"sponsorTransferTimeout",
+	// 	"sponsorApproveTimeout"
+	// @param value Value of the limit.
+	//
+	// Selector: setCollectionLimit(string,uint32) 6a3841db
+	function setCollectionLimit(string memory limit, uint32 value) external;
+
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"ownerCanTransfer",
+	// 	"ownerCanDestroy",
+	// 	"transfersEnabled"
+	// @param value Value of the limit.
+	//
+	// Selector: setCollectionLimit(string,bool) 993b7fba
+	function setCollectionLimit(string memory limit, bool value) external;
+
+	// Get contract address.
+	//
+	// Selector: contractAddress() f6b4dfb4
+	function contractAddress() external view returns (address);
+
+	// Add collection admin by substrate address.
+	// @param new_admin Substrate administrator address.
+	//
+	// Selector: addCollectionAdminSubstrate(uint256) 5730062b
+	function addCollectionAdminSubstrate(uint256 newAdmin) external;
+
+	// Remove collection admin by substrate address.
+	// @param admin Substrate administrator address.
+	//
+	// Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9
+	function removeCollectionAdminSubstrate(uint256 admin) external;
+
+	// Add collection admin.
+	// @param new_admin Address of the added administrator.
+	//
+	// Selector: addCollectionAdmin(address) 92e462c7
+	function addCollectionAdmin(address newAdmin) external;
+
+	// Remove collection admin.
+	//
+	// @param new_admin Address of the removed administrator.
+	//
+	// Selector: removeCollectionAdmin(address) fafd7b42
+	function removeCollectionAdmin(address admin) external;
+
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'
+	//
+	// Selector: setCollectionNesting(bool) 112d4586
+	function setCollectionNesting(bool enable) external;
+
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'
+	// @param collections Addresses of collections that will be available for nesting.
+	//
+	// Selector: setCollectionNesting(bool,address[]) 64872396
+	function setCollectionNesting(bool enable, address[] memory collections)
+		external;
+
+	// Set the collection access method.
+	// @param mode Access mode
+	// 	0 for Normal
+	// 	1 for AllowList
+	//
+	// Selector: setCollectionAccess(uint8) 41835d4c
+	function setCollectionAccess(uint8 mode) external;
+
+	// Add the user to the allowed list.
+	//
+	// @param user Address of a trusted user.
+	//
+	// Selector: addToCollectionAllowList(address) 67844fe6
+	function addToCollectionAllowList(address user) external;
+
+	// Remove the user from the allowed list.
+	//
+	// @param user Address of a removed user.
+	//
+	// Selector: removeFromCollectionAllowList(address) 85c51acb
+	function removeFromCollectionAllowList(address user) external;
+
+	// Switch permission for minting.
+	//
+	// @param mode Enable if "true".
+	//
+	// Selector: setCollectionMintMode(bool) 00018e84
+	function setCollectionMintMode(bool mode) external;
+}
+
 // Selector: 942e8b22
 interface ERC20 is Dummy, ERC165, ERC20Events {
 	// Selector: name() 06fdde03
@@ -63,39 +211,6 @@
 		external
 		view
 		returns (uint256);
-}
-
-// Selector: c894dc35
-interface Collection is Dummy, ERC165 {
-	// Selector: setCollectionProperty(string,bytes) 2f073f66
-	function setCollectionProperty(string memory key, bytes memory value)
-		external;
-
-	// Selector: deleteCollectionProperty(string) 7b7debce
-	function deleteCollectionProperty(string memory key) external;
-
-	// Throws error if key not found
-	//
-	// Selector: collectionProperty(string) cf24fd6d
-	function collectionProperty(string memory key)
-		external
-		view
-		returns (bytes memory);
-
-	// Selector: ethSetSponsor(address) 8f9af356
-	function ethSetSponsor(address sponsor) external;
-
-	// Selector: ethConfirmSponsorship() a8580d1a
-	function ethConfirmSponsorship() external;
-
-	// Selector: setLimit(string,uint32) 68db30ca
-	function setLimit(string memory limit, uint32 value) external;
-
-	// Selector: setLimit(string,bool) ea67e4c2
-	function setLimit(string memory limit, bool value) external;
-
-	// Selector: contractAddress() f6b4dfb4
-	function contractAddress() external view returns (address);
 }
 
 interface UniqueFungible is
modifiedtests/src/eth/api/UniqueNFT.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -44,6 +44,13 @@
 
 // Selector: 41369377
 interface TokenProperties is Dummy, ERC165 {
+	// @notice Set permissions for token property.
+	// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// @param key Property key.
+	// @param is_mutable Permission to mutate property.
+	// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	// @param token_owner Permission to mutate property by token owner if property is mutable.
+	//
 	// Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa
 	function setTokenPropertyPermission(
 		string memory key,
@@ -52,6 +59,12 @@
 		bool tokenOwner
 	) external;
 
+	// @notice Set token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @param value Property value.
+	//
 	// Selector: setProperty(uint256,string,bytes) 1752d67b
 	function setProperty(
 		uint256 tokenId,
@@ -59,10 +72,19 @@
 		bytes memory value
 	) external;
 
+	// @notice Delete token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	//
 	// Selector: deleteProperty(uint256,string) 066111d1
 	function deleteProperty(uint256 tokenId, string memory key) external;
 
-	// Throws error if key not found
+	// @notice Get token property value.
+	// @dev Throws error if key not found
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @return Property value bytes
 	//
 	// Selector: property(uint256,string) 7228c327
 	function property(uint256 tokenId, string memory key)
@@ -73,19 +95,36 @@
 
 // Selector: 42966c68
 interface ERC721Burnable is Dummy, ERC165 {
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
+	//  operator of the current owner.
+	// @param tokenId The NFT to approve
+	//
 	// Selector: burn(uint256) 42966c68
 	function burn(uint256 tokenId) external;
 }
 
 // Selector: 58800161
 interface ERC721 is Dummy, ERC165, ERC721Events {
+	// @notice Count all NFTs assigned to an owner
+	// @dev NFTs assigned to the zero address are considered invalid, and this
+	//  function throws for queries about the zero address.
+	// @param owner An address for whom to query the balance
+	// @return The number of NFTs owned by `owner`, possibly zero
+	//
 	// Selector: balanceOf(address) 70a08231
 	function balanceOf(address owner) external view returns (uint256);
 
+	// @notice Find the owner of an NFT
+	// @dev NFTs assigned to zero address are considered invalid, and queries
+	//  about them do throw.
+	// @param tokenId The identifier for an NFT
+	// @return The address of the owner of the NFT
+	//
 	// Selector: ownerOf(uint256) 6352211e
 	function ownerOf(uint256 tokenId) external view returns (address);
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
 	function safeTransferFromWithData(
@@ -95,7 +134,7 @@
 		bytes memory data
 	) external;
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: safeTransferFrom(address,address,uint256) 42842e0e
 	function safeTransferFrom(
@@ -104,6 +143,17 @@
 		uint256 tokenId
 	) external;
 
+	// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
+	//  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	//  THEY MAY BE PERMANENTLY LOST
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this NFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.
+	// @param from The current owner of the NFT
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
 	// Selector: transferFrom(address,address,uint256) 23b872dd
 	function transferFrom(
 		address from,
@@ -111,20 +161,27 @@
 		uint256 tokenId
 	) external;
 
+	// @notice Set or reaffirm the approved address for an NFT
+	// @dev The zero address indicates there is no approved address.
+	// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized
+	//  operator of the current owner.
+	// @param approved The new approved NFT controller
+	// @param tokenId The NFT to approve
+	//
 	// Selector: approve(address,uint256) 095ea7b3
 	function approve(address approved, uint256 tokenId) external;
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: setApprovalForAll(address,bool) a22cb465
 	function setApprovalForAll(address operator, bool approved) external;
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: getApproved(uint256) 081812fc
 	function getApproved(uint256 tokenId) external view returns (address);
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: isApprovedForAll(address,address) e985e9c5
 	function isApprovedForAll(address owner, address operator)
@@ -135,13 +192,21 @@
 
 // Selector: 5b5e139f
 interface ERC721Metadata is Dummy, ERC165 {
+	// @notice A descriptive name for a collection of NFTs in this contract
+	//
 	// Selector: name() 06fdde03
 	function name() external view returns (string memory);
 
+	// @notice An abbreviated name for NFTs in this contract
+	//
 	// Selector: symbol() 95d89b41
 	function symbol() external view returns (string memory);
 
-	// Returns token's const_metadata
+	// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	// @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC
+	//  3986. The URI may point to a JSON file that conforms to the "ERC721
+	//  Metadata JSON Schema".
+	// @return token's const_metadata
 	//
 	// Selector: tokenURI(uint256) c87b56dd
 	function tokenURI(uint256 tokenId) external view returns (string memory);
@@ -152,14 +217,21 @@
 	// Selector: mintingFinished() 05d2035b
 	function mintingFinished() external view returns (bool);
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted NFT
 	//
 	// Selector: mint(address,uint256) 40c10f19
 	function mint(address to, uint256 tokenId) external returns (bool);
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token with the given tokenUri.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted NFT
+	// @param tokenUri Token URI that would be stored in the NFT properties
 	//
 	// Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
 	function mintWithTokenURI(
@@ -168,7 +240,7 @@
 		string memory tokenUri
 	) external returns (bool);
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: finishMinting() 7d64bcb4
 	function finishMinting() external returns (bool);
@@ -176,10 +248,15 @@
 
 // Selector: 780e9d63
 interface ERC721Enumerable is Dummy, ERC165 {
+	// @notice Enumerate valid NFTs
+	// @param index A counter less than `totalSupply()`
+	// @return The token identifier for the `index`th NFT,
+	//  (sort order not specified)
+	//
 	// Selector: tokenByIndex(uint256) 4f6ccce7
 	function tokenByIndex(uint256 index) external view returns (uint256);
 
-	// Not implemented
+	// @dev Not implemented
 	//
 	// Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
 	function tokenOfOwnerByIndex(address owner, uint256 index)
@@ -187,20 +264,38 @@
 		view
 		returns (uint256);
 
+	// @notice Count NFTs tracked by this contract
+	// @return A count of valid NFTs tracked by this contract, where each one of
+	//  them has an assigned and queryable owner not equal to the zero address
+	//
 	// Selector: totalSupply() 18160ddd
 	function totalSupply() external view returns (uint256);
 }
 
 // Selector: 7d9262e6
 interface Collection is Dummy, ERC165 {
+	// Set collection property.
+	//
+	// @param key Property key.
+	// @param value Propery value.
+	//
 	// Selector: setCollectionProperty(string,bytes) 2f073f66
 	function setCollectionProperty(string memory key, bytes memory value)
 		external;
 
+	// Delete collection property.
+	//
+	// @param key Property key.
+	//
 	// Selector: deleteCollectionProperty(string) 7b7debce
 	function deleteCollectionProperty(string memory key) external;
 
-	// Throws error if key not found
+	// Get collection property.
+	//
+	// @dev Throws error if key not found.
+	//
+	// @param key Property key.
+	// @return bytes The property corresponding to the key.
 	//
 	// Selector: collectionProperty(string) cf24fd6d
 	function collectionProperty(string memory key)
@@ -208,69 +303,168 @@
 		view
 		returns (bytes memory);
 
+	// Set the sponsor of the collection.
+	//
+	// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
+	//
+	// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.
+	//
 	// Selector: setCollectionSponsor(address) 7623402e
 	function setCollectionSponsor(address sponsor) external;
 
+	// Collection sponsorship confirmation.
+	//
+	// @dev After setting the sponsor for the collection, it must be confirmed with this function.
+	//
 	// Selector: confirmCollectionSponsorship() 3c50e97a
 	function confirmCollectionSponsorship() external;
 
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"accountTokenOwnershipLimit",
+	// 	"sponsoredDataSize",
+	// 	"sponsoredDataRateLimit",
+	// 	"tokenLimit",
+	// 	"sponsorTransferTimeout",
+	// 	"sponsorApproveTimeout"
+	// @param value Value of the limit.
+	//
 	// Selector: setCollectionLimit(string,uint32) 6a3841db
 	function setCollectionLimit(string memory limit, uint32 value) external;
 
+	// Set limits for the collection.
+	// @dev Throws error if limit not found.
+	// @param limit Name of the limit. Valid names:
+	// 	"ownerCanTransfer",
+	// 	"ownerCanDestroy",
+	// 	"transfersEnabled"
+	// @param value Value of the limit.
+	//
 	// Selector: setCollectionLimit(string,bool) 993b7fba
 	function setCollectionLimit(string memory limit, bool value) external;
 
+	// Get contract address.
+	//
 	// Selector: contractAddress() f6b4dfb4
 	function contractAddress() external view returns (address);
 
+	// Add collection admin by substrate address.
+	// @param new_admin Substrate administrator address.
+	//
 	// Selector: addCollectionAdminSubstrate(uint256) 5730062b
-	function addCollectionAdminSubstrate(uint256 newAdmin) external view;
+	function addCollectionAdminSubstrate(uint256 newAdmin) external;
 
+	// Remove collection admin by substrate address.
+	// @param admin Substrate administrator address.
+	//
 	// Selector: removeCollectionAdminSubstrate(uint256) 4048fcf9
-	function removeCollectionAdminSubstrate(uint256 newAdmin) external view;
+	function removeCollectionAdminSubstrate(uint256 admin) external;
 
+	// Add collection admin.
+	// @param new_admin Address of the added administrator.
+	//
 	// Selector: addCollectionAdmin(address) 92e462c7
-	function addCollectionAdmin(address newAdmin) external view;
+	function addCollectionAdmin(address newAdmin) external;
 
+	// Remove collection admin.
+	//
+	// @param new_admin Address of the removed administrator.
+	//
 	// Selector: removeCollectionAdmin(address) fafd7b42
-	function removeCollectionAdmin(address admin) external view;
+	function removeCollectionAdmin(address admin) external;
 
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'
+	//
 	// Selector: setCollectionNesting(bool) 112d4586
 	function setCollectionNesting(bool enable) external;
 
+	// Toggle accessibility of collection nesting.
+	//
+	// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'
+	// @param collections Addresses of collections that will be available for nesting.
+	//
 	// Selector: setCollectionNesting(bool,address[]) 64872396
 	function setCollectionNesting(bool enable, address[] memory collections)
 		external;
 
+	// Set the collection access method.
+	// @param mode Access mode
+	// 	0 for Normal
+	// 	1 for AllowList
+	//
 	// Selector: setCollectionAccess(uint8) 41835d4c
 	function setCollectionAccess(uint8 mode) external;
 
+	// Add the user to the allowed list.
+	//
+	// @param user Address of a trusted user.
+	//
 	// Selector: addToCollectionAllowList(address) 67844fe6
-	function addToCollectionAllowList(address user) external view;
+	function addToCollectionAllowList(address user) external;
 
+	// Remove the user from the allowed list.
+	//
+	// @param user Address of a removed user.
+	//
 	// Selector: removeFromCollectionAllowList(address) 85c51acb
-	function removeFromCollectionAllowList(address user) external view;
+	function removeFromCollectionAllowList(address user) external;
 
+	// Switch permission for minting.
+	//
+	// @param mode Enable if "true".
+	//
 	// Selector: setCollectionMintMode(bool) 00018e84
 	function setCollectionMintMode(bool mode) external;
 }
 
 // Selector: d74d154f
 interface ERC721UniqueExtensions is Dummy, ERC165 {
+	// @notice Transfer ownership of an NFT
+	// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	//  is the zero address. Throws if `tokenId` is not a valid NFT.
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
 	// Selector: transfer(address,uint256) a9059cbb
 	function transfer(address to, uint256 tokenId) external;
 
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this NFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.
+	// @param from The current owner of the NFT
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
+	//
 	// Selector: burnFrom(address,uint256) 79cc6790
 	function burnFrom(address from, uint256 tokenId) external;
 
+	// @notice Returns next free NFT ID.
+	//
 	// Selector: nextTokenId() 75794a3c
 	function nextTokenId() external view returns (uint256);
 
+	// @notice Function to mint multiple tokens.
+	// @dev `tokenIds` should be an array of consecutive numbers and first number
+	//  should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokenIds IDs of the minted NFTs
+	//
 	// Selector: mintBulk(address,uint256[]) 44a9945e
 	function mintBulk(address to, uint256[] memory tokenIds)
 		external
 		returns (bool);
 
+	// @notice Function to mint multiple tokens with the given tokenUris.
+	// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	//  numbers and first number should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokens array of pairs of token ID and token URI for minted tokens
+	//
 	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
 	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
 		external
addedtests/src/eth/api/UniqueRefungibleToken.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/api/UniqueRefungibleToken.sol
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+// Common stubs holder
+interface Dummy {
+
+}
+
+interface ERC165 is Dummy {
+	function supportsInterface(bytes4 interfaceID) external view returns (bool);
+}
+
+// Inline
+interface ERC20Events {
+	event Transfer(address indexed from, address indexed to, uint256 value);
+	event Approval(
+		address indexed owner,
+		address indexed spender,
+		uint256 value
+	);
+}
+
+// Selector: 942e8b22
+interface ERC20 is Dummy, ERC165, ERC20Events {
+	// Selector: name() 06fdde03
+	function name() external view returns (string memory);
+
+	// Selector: symbol() 95d89b41
+	function symbol() external view returns (string memory);
+
+	// Selector: totalSupply() 18160ddd
+	function totalSupply() external view returns (uint256);
+
+	// Selector: decimals() 313ce567
+	function decimals() external view returns (uint8);
+
+	// Selector: balanceOf(address) 70a08231
+	function balanceOf(address owner) external view returns (uint256);
+
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 amount) external returns (bool);
+
+	// Selector: transferFrom(address,address,uint256) 23b872dd
+	function transferFrom(
+		address from,
+		address to,
+		uint256 amount
+	) external returns (bool);
+
+	// Selector: approve(address,uint256) 095ea7b3
+	function approve(address spender, uint256 amount) external returns (bool);
+
+	// Selector: allowance(address,address) dd62ed3e
+	function allowance(address owner, address spender)
+		external
+		view
+		returns (uint256);
+}
+
+// Selector: ab8deb37
+interface ERC20UniqueExtensions is Dummy, ERC165 {
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 amount) external returns (bool);
+
+	// Selector: repartition(uint256) d2418ca7
+	function repartition(uint256 amount) external returns (bool);
+}
+
+interface UniqueRefungibleToken is
+	Dummy,
+	ERC165,
+	ERC20,
+	ERC20UniqueExtensions
+{}
modifiedtests/src/eth/fungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/fungibleAbi.json
+++ b/tests/src/eth/fungibleAbi.json
@@ -51,6 +51,33 @@
   },
   {
     "inputs": [
+      { "internalType": "address", "name": "newAdmin", "type": "address" }
+    ],
+    "name": "addCollectionAdmin",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "newAdmin", "type": "uint256" }
+    ],
+    "name": "addCollectionAdminSubstrate",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "user", "type": "address" }
+    ],
+    "name": "addToCollectionAllowList",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
       { "internalType": "address", "name": "owner", "type": "address" },
       { "internalType": "address", "name": "spender", "type": "address" }
     ],
@@ -97,6 +124,13 @@
   },
   {
     "inputs": [],
+    "name": "confirmCollectionSponsorship",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
     "name": "contractAddress",
     "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
     "stateMutability": "view",
@@ -118,43 +152,51 @@
   },
   {
     "inputs": [],
-    "name": "ethConfirmSponsorship",
-    "outputs": [],
-    "stateMutability": "nonpayable",
+    "name": "name",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
     "type": "function"
   },
   {
     "inputs": [
-      { "internalType": "address", "name": "sponsor", "type": "address" }
+      { "internalType": "address", "name": "admin", "type": "address" }
     ],
-    "name": "ethSetSponsor",
+    "name": "removeCollectionAdmin",
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
   },
   {
-    "inputs": [],
-    "name": "name",
-    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
-    "stateMutability": "view",
+    "inputs": [
+      { "internalType": "uint256", "name": "admin", "type": "uint256" }
+    ],
+    "name": "removeCollectionAdminSubstrate",
+    "outputs": [],
+    "stateMutability": "nonpayable",
     "type": "function"
   },
   {
     "inputs": [
-      { "internalType": "string", "name": "key", "type": "string" },
-      { "internalType": "bytes", "name": "value", "type": "bytes" }
+      { "internalType": "address", "name": "user", "type": "address" }
     ],
-    "name": "setCollectionProperty",
+    "name": "removeFromCollectionAllowList",
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
   },
   {
+    "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }],
+    "name": "setCollectionAccess",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
     "inputs": [
       { "internalType": "string", "name": "limit", "type": "string" },
       { "internalType": "uint32", "name": "value", "type": "uint32" }
     ],
-    "name": "setLimit",
+    "name": "setCollectionLimit",
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
@@ -164,7 +206,54 @@
       { "internalType": "string", "name": "limit", "type": "string" },
       { "internalType": "bool", "name": "value", "type": "bool" }
     ],
-    "name": "setLimit",
+    "name": "setCollectionLimit",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }],
+    "name": "setCollectionMintMode",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }],
+    "name": "setCollectionNesting",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "bool", "name": "enable", "type": "bool" },
+      {
+        "internalType": "address[]",
+        "name": "collections",
+        "type": "address[]"
+      }
+    ],
+    "name": "setCollectionNesting",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "key", "type": "string" },
+      { "internalType": "bytes", "name": "value", "type": "bytes" }
+    ],
+    "name": "setCollectionProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "sponsor", "type": "address" }
+    ],
+    "name": "setCollectionSponsor",
     "outputs": [],
     "stateMutability": "nonpayable",
     "type": "function"
modifiedtests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/nonFungibleAbi.json
+++ b/tests/src/eth/nonFungibleAbi.json
@@ -86,7 +86,7 @@
     ],
     "name": "addCollectionAdmin",
     "outputs": [],
-    "stateMutability": "view",
+    "stateMutability": "nonpayable",
     "type": "function"
   },
   {
@@ -95,7 +95,7 @@
     ],
     "name": "addCollectionAdminSubstrate",
     "outputs": [],
-    "stateMutability": "view",
+    "stateMutability": "nonpayable",
     "type": "function"
   },
   {
@@ -104,7 +104,7 @@
     ],
     "name": "addToCollectionAllowList",
     "outputs": [],
-    "stateMutability": "view",
+    "stateMutability": "nonpayable",
     "type": "function"
   },
   {
@@ -304,16 +304,16 @@
     ],
     "name": "removeCollectionAdmin",
     "outputs": [],
-    "stateMutability": "view",
+    "stateMutability": "nonpayable",
     "type": "function"
   },
   {
     "inputs": [
-      { "internalType": "uint256", "name": "newAdmin", "type": "uint256" }
+      { "internalType": "uint256", "name": "admin", "type": "uint256" }
     ],
     "name": "removeCollectionAdminSubstrate",
     "outputs": [],
-    "stateMutability": "view",
+    "stateMutability": "nonpayable",
     "type": "function"
   },
   {
@@ -322,7 +322,7 @@
     ],
     "name": "removeFromCollectionAllowList",
     "outputs": [],
-    "stateMutability": "view",
+    "stateMutability": "nonpayable",
     "type": "function"
   },
   {
addedtests/src/eth/reFungibleToken.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungibleToken.test.ts
@@ -0,0 +1,458 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {approve, createCollection, createRefungibleToken, transfer, transferFrom, UNIQUE} from '../util/helpers';
+import {createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, tokenIdToAddress, transferBalanceToEth} from './util/helpers';
+import reFungibleTokenAbi from './reFungibleTokenAbi.json';
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+describe('Refungible token: Information getting', () => {
+  itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    const totalSupply = await contract.methods.totalSupply().call();
+
+    expect(totalSupply).to.equal('200');
+  });
+
+  itWeb3('balanceOf', async ({api, web3, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    const balance = await contract.methods.balanceOf(caller).call();
+
+    expect(balance).to.equal('200');
+  });
+
+  itWeb3('decimals', async ({api, web3, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    const decimals = await contract.methods.decimals().call();
+
+    expect(decimals).to.equal('0');
+  });
+});
+
+describe('Refungible: Plain calls', () => {
+  itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+
+    const spender = createEthAccount(web3);
+
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    {
+      const result = await contract.methods.approve(spender, 100).send({from: owner});
+      const events = normalizeEvents(result.events);
+
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Approval',
+          args: {
+            owner,
+            spender,
+            value: '100',
+          },
+        },
+      ]);
+    }
+
+    {
+      const allowance = await contract.methods.allowance(owner, spender).call();
+      expect(+allowance).to.equal(100);
+    }
+  });
+
+  itWeb3('Can perform transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const spender = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, spender);
+
+    const receiver = createEthAccount(web3);
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    await contract.methods.approve(spender, 100).send();
+
+    {
+      const result = await contract.methods.transferFrom(owner, receiver, 49).send({from: spender});
+      const events = normalizeEvents(result.events);
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: owner,
+            to: receiver,
+            value: '49',
+          },
+        },
+        {
+          address,
+          event: 'Approval',
+          args: {
+            owner,
+            spender,
+            value: '51',
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(49);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(owner).call();
+      expect(+balance).to.equal(151);
+    }
+  });
+
+  itWeb3('Can perform transfer()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const receiver = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, receiver);
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    {
+      const result = await contract.methods.transfer(receiver, 50).send({from: owner});
+      const events = normalizeEvents(result.events);
+      expect(events).to.be.deep.equal([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: owner,
+            to: receiver,
+            value: '50',
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(owner).call();
+      expect(+balance).to.equal(150);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(50);
+    }
+  });
+
+  itWeb3('Can perform repartition()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const receiver = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, receiver);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    await contract.methods.repartition(200).send({from: owner});
+    expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(200);
+    await contract.methods.transfer(receiver, 110).send({from: owner});
+    expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(90);
+    expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(110);
+    
+    await expect(contract.methods.repartition(80).send({from: owner})).to.eventually.be.rejected;
+
+    await contract.methods.transfer(receiver, 90).send({from: owner});
+    expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(0);
+    expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(200);
+
+    await contract.methods.repartition(150).send({from: receiver});
+    await expect(contract.methods.transfer(owner, 160).send({from: receiver})).to.eventually.be.rejected;
+    expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(150);
+  });
+
+  itWeb3('Can repartition with increased amount', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const result = await contract.methods.repartition(200).send();
+    const events = normalizeEvents(result.events);
+
+    expect(events).to.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: '0x0000000000000000000000000000000000000000',
+          to: owner,
+          value: '100',
+        },
+      },
+    ]);
+  });
+
+  itWeb3('Can repartition with decreased amount', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = createEthAccount(web3);
+    await transferBalanceToEth(api, alice, owner);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const result = await contract.methods.repartition(50).send();
+    const events = normalizeEvents(result.events);
+    expect(events).to.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: owner,
+          to: '0x0000000000000000000000000000000000000000',
+          value: '50',
+        },
+      },
+    ]);
+  });
+});
+
+describe('Refungible: Fees', () => {
+  itWeb3('approve() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const spender = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const cost = await recordEthFee(api, owner, () => contract.methods.approve(spender, 100).send({from: owner}));
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+  });
+
+  itWeb3('transferFrom() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const spender = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    await contract.methods.approve(spender, 100).send({from: owner});
+
+    const cost = await recordEthFee(api, spender, () => contract.methods.transferFrom(owner, spender, 100).send({from: spender}));
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+  });
+
+  itWeb3('transfer() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+    const cost = await recordEthFee(api, owner, () => contract.methods.transfer(receiver, 100).send({from: owner}));
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+  });
+});
+
+describe('Refungible: Substrate calls', () => {
+  itWeb3('Events emitted for approve()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+    const events = await recordEvents(contract, async () => {
+      expect(await approve(api, collectionId, tokenId, alice, {Ethereum: receiver}, 100n)).to.be.true;
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Approval',
+        args: {
+          owner: subToEth(alice.address),
+          spender: receiver,
+          value: '100',
+        },
+      },
+    ]);
+  });
+
+  itWeb3('Events emitted for transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+    const bob = privateKeyWrapper('//Bob');
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+    expect(await approve(api, collectionId, tokenId, alice, bob.address, 100n)).to.be.true;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+    const events = await recordEvents(contract, async () => {
+      expect(await transferFrom(api, collectionId, tokenId, bob, alice, {Ethereum: receiver},  51n)).to.be.true;
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: subToEth(alice.address),
+          to: receiver,
+          value: '51',
+        },
+      },
+      {
+        address,
+        event: 'Approval',
+        args: {
+          owner: subToEth(alice.address),
+          spender: subToEth(bob.address),
+          value: '49',
+        },
+      },
+    ]);
+  });
+
+  itWeb3('Events emitted for transfer()', async ({web3, api, privateKeyWrapper}) => {
+    const alice = privateKeyWrapper('//Alice');
+
+    const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+    const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+    const events = await recordEvents(contract, async () => {
+      expect(await transfer(api, collectionId, tokenId, alice, {Ethereum: receiver},  51n)).to.be.true;
+    });
+
+    expect(events).to.be.deep.equal([
+      {
+        address,
+        event: 'Transfer',
+        args: {
+          from: subToEth(alice.address),
+          to: receiver,
+          value: '51',
+        },
+      },
+    ]);
+  });
+});
addedtests/src/eth/reFungibleTokenAbi.jsondiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungibleTokenAbi.json
@@ -0,0 +1,158 @@
+[
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "owner",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "spender",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "value",
+        "type": "uint256"
+      }
+    ],
+    "name": "Approval",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "from",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "to",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "value",
+        "type": "uint256"
+      }
+    ],
+    "name": "Transfer",
+    "type": "event"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" },
+      { "internalType": "address", "name": "spender", "type": "address" }
+    ],
+    "name": "allowance",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "spender", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "approve",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" }
+    ],
+    "name": "balanceOf",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "burnFrom",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "decimals",
+    "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "name",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "repartition",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }
+    ],
+    "name": "supportsInterface",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "symbol",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "totalSupply",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "transfer",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "transferFrom",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]
addedtests/src/fungible.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/fungible.test.ts
@@ -0,0 +1,184 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {default as usingApi} from './substrate/substrate-api';
+import {IKeyringPair} from '@polkadot/types/types';
+import {
+  getBalance,
+  createMultipleItemsExpectSuccess,
+  isTokenExists,
+  getLastTokenId,
+  getAllowance,
+  approve,
+  transferFrom,
+  createCollection,
+  transfer,
+  burnItem,
+  normalizeAccountId,
+  CrossAccountId,
+  createFungibleItemExpectSuccess,
+  U128_MAX,
+  burnFromExpectSuccess,
+} from './util/helpers';
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+let alice: IKeyringPair;
+let bob: IKeyringPair;
+
+describe('integration test: Fungible functionality:', () => {
+  before(async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      alice = privateKeyWrapper('//Alice');
+      bob = privateKeyWrapper('//Bob');
+    });
+  });
+
+  it('Create fungible collection and token', async () => {
+    await usingApi(async api => {
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
+      expect(createCollectionResult.success).to.be.true;
+      const collectionId  = createCollectionResult.collectionId;
+      const defaultTokenId = await getLastTokenId(api, collectionId);
+      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
+      const aliceBalance = await getBalance(api, collectionId, alice, aliceTokenId); 
+      const itemCountAfter = await getLastTokenId(api, collectionId);
+      
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(itemCountAfter).to.be.equal(defaultTokenId);
+      expect(aliceBalance).to.be.equal(U128_MAX);
+    });
+  });
+  
+  it('RPC method tokenOnewrs for fungible collection and token', async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
+      
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
+      const collectionId = createCollectionResult.collectionId;
+      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
+     
+      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
+      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
+            
+      for (let i = 0; i < 7; i++) {
+        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 1);
+      } 
+      
+      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
+      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
+      const aliceID = normalizeAccountId(alice);
+      const bobId = normalizeAccountId(bob);
+
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
+      expect(owners.length == 10).to.be.true;
+      
+      const eleven = privateKeyWrapper('11');
+      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
+      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+    });
+  });
+  
+  it('Transfer token', async () => {
+    await usingApi(async api => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const tokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address);
+
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n);
+      expect(await transfer(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
+      expect(await transfer(api, collectionId, tokenId, alice, ethAcc, 140n)).to.be.true;
+
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(300n);
+      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(60n);
+      expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(140n);
+      await expect(transfer(api, collectionId, tokenId, alice, bob, 350n)).to.eventually.be.rejected;
+    });
+  });
+
+  it('Tokens multiple creation', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      
+      const args = [
+        {Fungible: {Value: 500n}},
+        {Fungible: {Value: 400n}},
+        {Fungible: {Value: 300n}},
+      ];
+      
+      await createMultipleItemsExpectSuccess(alice, collectionId, args);
+      expect(await getBalance(api, collectionId, alice, 0)).to.be.equal(1200n);
+    });   
+  });
+
+  it('Burn some tokens ', async () => {
+    await usingApi(async api => {   
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address));
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n);
+      expect(await burnItem(api, alice, collectionId, tokenId, 499n)).to.be.true;
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(1n);
+    });
+  });
+  
+  it('Burn all tokens ', async () => {
+    await usingApi(async api => {   
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address));
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      expect(await burnItem(api, alice, collectionId, tokenId, 500n)).to.be.true;
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(0n);
+      expect((await api.rpc.unique.totalPieces(collectionId, tokenId)).value.toBigInt()).to.be.equal(0n);
+    });
+  });
+
+  it('Set allowance for token', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      
+      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 100n}, alice.address));
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
+
+      expect(await approve(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
+      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(60n);
+      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(0n);
+      
+      expect(await transferFrom(api, collectionId, tokenId, bob, alice, bob,  20n)).to.be.true;
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(80n);
+      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(20n);
+      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(40n);
+      
+      await burnFromExpectSuccess(bob, alice, collectionId, tokenId, 10n);
+     
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(70n);
+      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(30n);
+      expect(await transferFrom(api, collectionId, tokenId, bob, alice, ethAcc,  10n)).to.be.true;
+      expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(10n);
+    });
+  });
+});
modifiedtests/src/interfaces/augment-api-consts.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-consts.ts
+++ b/tests/src/interfaces/augment-api-consts.ts
@@ -29,7 +29,13 @@
       [key: string]: Codec;
     };
     common: {
+      /**
+       * Maximum admins per collection.
+       **/
       collectionAdminsLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Set price to create a collection.
+       **/
       collectionCreationPrice: u128 & AugmentedConst<ApiType>;
       /**
        * Generic const
modifiedtests/src/interfaces/augment-api-errors.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-errors.ts
+++ b/tests/src/interfaces/augment-api-errors.ts
@@ -285,7 +285,7 @@
        **/
       FungibleItemsDontHaveData: AugmentedError<ApiType>;
       /**
-       * Not default id passed as TokenId argument.
+       * Fungible tokens hold no ID, and the default value of TokenId for Fungible collection is 0.
        **/
       FungibleItemsHaveNoId: AugmentedError<ApiType>;
       /**
@@ -596,7 +596,7 @@
     };
     unique: {
       /**
-       * Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.
+       * Decimal_points parameter must be lower than [`up_data_structs::MAX_DECIMAL_POINTS`].
        **/
       CollectionDecimalPointLimitExceeded: AugmentedError<ApiType>;
       /**
@@ -608,7 +608,7 @@
        **/
       EmptyArgument: AugmentedError<ApiType>;
       /**
-       * Repertition is only supported by refungible collection
+       * Repertition is only supported by refungible collection.
        **/
       RepartitionCalledOnNonRefungibleCollection: AugmentedError<ApiType>;
       /**
modifiedtests/src/interfaces/augment-api-events.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-events.ts
+++ b/tests/src/interfaces/augment-api-events.ts
@@ -59,114 +59,47 @@
     };
     common: {
       /**
-       * Sponsoring allowance was approved.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - todo:doc flesh out
-       * * item_id
-       * * sender
-       * * spender
-       * * amount
+       * Amount pieces of token owned by `sender` was approved for `spender`.
        **/
       Approved: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
       /**
        * New collection was created
-       * 
-       * # Arguments
-       * 
-       * * collection_id - Globally unique identifier of newly created collection.
-       * * mode - [CollectionMode] converted into u8.
-       * * account_id - Collection owner.
        **/
       CollectionCreated: AugmentedEvent<ApiType, [u32, u8, AccountId32]>;
       /**
        * New collection was destroyed
-       * 
-       * # Arguments
-       * 
-       * * collection_id - Globally unique identifier of collection that has been destroyed.
        **/
       CollectionDestroyed: AugmentedEvent<ApiType, [u32]>;
       /**
-       * Collection property was deleted.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - ID of the collection, whose property was just deleted.
-       * * property_key - Key of the property that was just deleted.
+       * The property has been deleted.
        **/
       CollectionPropertyDeleted: AugmentedEvent<ApiType, [u32, Bytes]>;
       /**
-       * Collection property was added or edited.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - ID of the collection, whose property was just set.
-       * * property_key - Key of the property that was just set.
+       * The colletion property has been added or edited.
        **/
       CollectionPropertySet: AugmentedEvent<ApiType, [u32, Bytes]>;
       /**
        * New item was created.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - ID of the collection where the item was created.
-       * * item_id - ID of the item. Unique within the collection.
-       * * recipient - Owner of the newly created item.
-       * * amount - The amount of tokens that were created (always 1 for NFT).
        **/
       ItemCreated: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
       /**
        * Collection item was burned.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - Identifier of the collection to which the burned NFT belonged.
-       * * item_id - Identifier of burned NFT.
-       * * owner - Which user has destroyed their tokens.
-       * * amount - Amount of tokens that were destroyed (always 1 for NFT).
        **/
       ItemDestroyed: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
       /**
-       * Token property permission was added or updated for a collection.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - ID of the collection, whose permissions were just set/updated.
-       * * property_key - Key of the property of the set/updated permission.
+       * The token property permission of a collection has been set.
        **/
       PropertyPermissionSet: AugmentedEvent<ApiType, [u32, Bytes]>;
       /**
-       * Item property was deleted.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - ID of the collection, whose token's property was just deleted.
-       * * item_id - ID of the item, whose property was just deleted.
-       * * property_key - Key of the property that was just deleted.
+       * The token property has been deleted.
        **/
       TokenPropertyDeleted: AugmentedEvent<ApiType, [u32, u32, Bytes]>;
       /**
-       * Item property was added or edited.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - ID of the collection, whose token's property was just set.
-       * * item_id - ID of the item, whose property was just set.
-       * * property_key - Key of the property that was just set.
+       * The token property has been added or edited.
        **/
       TokenPropertySet: AugmentedEvent<ApiType, [u32, u32, Bytes]>;
       /**
-       * Item was transferred.
-       * 
-       * # Arguments
-       * 
-       * * collection_id - ID of the collection to which the item belongs.
-       * * item_id - ID of the item transferred.
-       * * sender - Original owner of the item.
-       * * recipient - New owner of the item.
-       * * amount - Amount of tokens that were transferred (always 1 for NFT).
+       * Item was transferred
        **/
       Transfer: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
       /**
@@ -569,94 +502,77 @@
        * Address was added to the allow list
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
-       * 
-       * * user:  Address.
+       * * collection_id: ID of the affected collection.
+       * * user: Address of the added account.
        **/
       AllowListAddressAdded: AugmentedEvent<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
        * Address was removed from the allow list
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
-       * 
-       * * user:  Address.
+       * * collection_id: ID of the affected collection.
+       * * user: Address of the removed account.
        **/
       AllowListAddressRemoved: AugmentedEvent<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
        * Collection admin was added
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
-       * 
-       * * admin:  Admin address.
+       * * collection_id: ID of the affected collection.
+       * * admin: Admin address.
        **/
       CollectionAdminAdded: AugmentedEvent<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
        * Collection admin was removed
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
-       * 
-       * * admin:  Admin address.
+       * * collection_id: ID of the affected collection.
+       * * admin: Removed admin address.
        **/
       CollectionAdminRemoved: AugmentedEvent<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
        * Collection limits were set
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
+       * * collection_id: ID of the affected collection.
        **/
       CollectionLimitSet: AugmentedEvent<ApiType, [u32]>;
       /**
        * Collection owned was changed
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
-       * 
-       * * owner:  New owner address.
+       * * collection_id: ID of the affected collection.
+       * * owner: New owner address.
        **/
       CollectionOwnedChanged: AugmentedEvent<ApiType, [u32, AccountId32]>;
       /**
        * Collection permissions were set
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
+       * * collection_id: ID of the affected collection.
        **/
       CollectionPermissionSet: AugmentedEvent<ApiType, [u32]>;
       /**
        * Collection sponsor was removed
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
+       * * collection_id: ID of the affected collection.
        **/
       CollectionSponsorRemoved: AugmentedEvent<ApiType, [u32]>;
       /**
        * Collection sponsor was set
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
-       * 
-       * * owner:  New sponsor address.
+       * * collection_id: ID of the affected collection.
+       * * owner: New sponsor address.
        **/
       CollectionSponsorSet: AugmentedEvent<ApiType, [u32, AccountId32]>;
       /**
        * New sponsor was confirm
        * 
        * # Arguments
-       * 
-       * * collection_id: Globally unique collection identifier.
-       * 
-       * * sponsor:  New sponsor address.
+       * * collection_id: ID of the affected collection.
+       * * sponsor: New sponsor address.
        **/
       SponsorshipConfirmed: AugmentedEvent<ApiType, [u32, AccountId32]>;
       /**
modifiedtests/src/interfaces/augment-api-query.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-query.ts
+++ b/tests/src/interfaces/augment-api-query.ts
@@ -70,39 +70,39 @@
     };
     common: {
       /**
-       * Amount of collection admins
+       * Storage of the amount of collection admins.
        **/
       adminAmount: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
-       * Allowlisted collection users
+       * Allowlisted collection users.
        **/
       allowlist: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<bool>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * Collection info
+       * Storage of collection info.
        **/
       collectionById: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<Option<UpDataStructsCollection>>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
-       * Collection properties
+       * Storage of collection properties.
        **/
       collectionProperties: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<UpDataStructsProperties>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
-       * Token permissions of a collection
+       * Storage of token property permissions of a collection.
        **/
       collectionPropertyPermissions: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<BTreeMap<Bytes, UpDataStructsPropertyPermission>>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
-       * The number of created collections. Essentially contains the last collection ID.
+       * Storage of the count of created collections. Essentially contains the last collection ID.
        **/
       createdCollectionCount: AugmentedQuery<ApiType, () => Observable<u32>, []> & QueryableStorageEntry<ApiType, []>;
       /**
-       * The number of destroyed collections
+       * Storage of the count of deleted collections.
        **/
       destroyedCollectionCount: AugmentedQuery<ApiType, () => Observable<u32>, []> & QueryableStorageEntry<ApiType, []>;
       /**
-       * Not used by code, exists only to provide some types to metadata
+       * Not used by code, exists only to provide some types to metadata.
        **/
       dummyStorageValue: AugmentedQuery<ApiType, () => Observable<Option<ITuple<[UpDataStructsCollectionStats, u32, u32, UpDataStructsTokenChild, PhantomTypeUpDataStructs]>>>, []> & QueryableStorageEntry<ApiType, []>;
       /**
-       * List of collection admins
+       * List of collection admins.
        **/
       isAdmin: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<bool>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
@@ -200,7 +200,7 @@
     };
     fungible: {
       /**
-       * todo:doc
+       * Storage for assets delegated to a limited extent to other users.
        **/
       allowance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, arg3: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<u128>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
@@ -244,11 +244,11 @@
     };
     nonfungible: {
       /**
-       * Amount of tokens owned in a collection.
+       * Amount of tokens owned by an account in a collection.
        **/
       accountBalance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<u32>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * todo:doc
+       * Allowance set by a token owner for another user to perform one of certain transactions on a token.
        **/
       allowance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<Option<PalletEvmAccountBasicCrossAccountIdRepr>>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
       /**
@@ -256,7 +256,15 @@
        **/
       owned: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, arg3: u32 | AnyNumber | Uint8Array) => Observable<bool>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32]>;
       /**
-       * Scoped, auxiliary properties of a token, primarily used for on-chain operations.
+       * Custom data of a token that is serialized to bytes,
+       * primarily reserved for on-chain operations,
+       * normally obscured from the external users.
+       * 
+       * Auxiliary properties are slightly different from
+       * usual [`TokenProperties`] due to an unlimited number
+       * and separately stored and written-to key-value pairs.
+       * 
+       * Currently used to store RMRK data.
        **/
       tokenAuxProperties: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array, arg3: UpDataStructsPropertyScope | 'None' | 'Rmrk' | number | Uint8Array, arg4: Bytes | string | Uint8Array) => Observable<Option<Bytes>>, [u32, u32, UpDataStructsPropertyScope, Bytes]> & QueryableStorageEntry<ApiType, [u32, u32, UpDataStructsPropertyScope, Bytes]>;
       /**
@@ -268,7 +276,7 @@
        **/
       tokenData: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<Option<PalletNonfungibleItemData>>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
       /**
-       * Key-value pairs, describing the metadata of a token.
+       * Map of key-value pairs, describing the metadata of a token.
        **/
       tokenProperties: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<UpDataStructsProperties>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
       /**
@@ -447,11 +455,11 @@
        **/
       accountBalance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<u32>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * todo:doc
+       * Allowance set by a token owner for another user to perform one of certain transactions on a number of pieces of a token.
        **/
       allowance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array, arg3: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, arg4: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<u128>, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * Amount of pieces of a token owned by an account.
+       * Amount of token pieces owned by account.
        **/
       balance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array, arg3: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<u128>, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
@@ -463,6 +471,10 @@
        **/
       tokenData: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<PalletRefungibleItemData>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
       /**
+       * Amount of pieces a refungible token is split into.
+       **/
+      tokenProperties: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<UpDataStructsProperties>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
+      /**
        * Amount of tokens burnt in a collection.
        **/
       tokensBurnt: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
@@ -471,7 +483,7 @@
        **/
       tokensMinted: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
-       * Amount of pieces a refungible token is split into.
+       * Total amount of pieces for token
        **/
       totalSupply: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<u128>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
       /**
modifiedtests/src/interfaces/augment-api-rpc.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-rpc.ts
+++ b/tests/src/interfaces/augment-api-rpc.ts
@@ -673,6 +673,10 @@
        **/
       collectionTokens: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Vec<u32>>>;
       /**
+       * Get token constant metadata
+       **/
+      constMetadata: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Bytes>>;
+      /**
        * Get effective collection limits
        **/
       effectiveCollectionLimits: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Option<UpDataStructsCollectionLimits>>>;
@@ -705,6 +709,10 @@
        **/
       tokenOwner: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Option<PalletEvmAccountBasicCrossAccountIdRepr>>>;
       /**
+       * Returns 10 tokens owners in no particular order
+       **/
+      tokenOwners: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Vec<PalletEvmAccountBasicCrossAccountIdRepr>>>;
+      /**
        * Get token properties, optionally limited to the provided keys
        **/
       tokenProperties: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, propertyKeys?: Vec<Text> | (Text | string)[], at?: Hash | string | Uint8Array) => Observable<Vec<UpDataStructsProperty>>>;
@@ -720,6 +728,10 @@
        * Get the amount of distinctive tokens present in a collection
        **/
       totalSupply: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<u32>>;
+      /**
+       * Get token variable metadata
+       **/
+      variableMetadata: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Bytes>>;
     };
     web3: {
       /**
modifiedtests/src/interfaces/augment-api-tx.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-tx.ts
+++ b/tests/src/interfaces/augment-api-tx.ts
@@ -730,18 +730,22 @@
     };
     unique: {
       /**
-       * Adds an admin of the collection.
-       * NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership.
+       * Add an admin to a collection.
+       * 
+       * NFT Collection can be controlled by multiple admin addresses
+       * (some which can also be servers, for example). Admins can issue
+       * and burn NFTs, as well as add and remove other admins,
+       * but cannot change NFT or Collection ownership.
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the Collection to add admin for.
-       * * new_admin - Address of new admin to add.
+       * * `collection_id`: ID of the Collection to add an admin for.
+       * * `new_admin`: Address of new admin to add.
        **/
       addCollectionAdmin: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, newAdmin: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
@@ -749,61 +753,76 @@
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * address.
+       * * `collection_id`: ID of the modified collection.
+       * * `address`: ID of the address to be added to the allowlist.
        **/
       addToAllowList: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, address: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * Set, change, or remove approved address to transfer the ownership of the NFT.
+       * Allow a non-permissioned address to transfer or burn an item.
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
-       * * Current NFT owner
+       * * Collection owner
+       * * Collection admin
+       * * Current item owner
        * 
        * # Arguments
        * 
-       * * approved - Address that is approved to transfer this NFT or zero (if needed to remove approval).
-       * * collection_id.
-       * * item_id - ID of the item.
+       * * `spender`: Account to be approved to make specific transactions on non-owned tokens.
+       * * `collection_id`: ID of the collection the item belongs to.
+       * * `item_id`: ID of the item transactions on which are now approved.
+       * * `amount`: Number of pieces of the item approved for a transaction (maximum of 1 for NFTs).
+       * Set to 0 to revoke the approval.
        **/
       approve: AugmentedSubmittable<(spender: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, collectionId: u32 | AnyNumber | Uint8Array, itemId: u32 | AnyNumber | Uint8Array, amount: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [PalletEvmAccountBasicCrossAccountIdRepr, u32, u32, u128]>;
       /**
-       * Destroy a concrete instance of NFT on behalf of the owner.
-       * See also: [`approve`]
+       * Destroy a token on behalf of the owner as a non-owner account.
+       * 
+       * See also: [`approve`][`Pallet::approve`].
+       * 
+       * After this method executes, one approval is removed from the total so that
+       * the approved address will not be able to transfer this item again from this owner.
        * 
        * # Permissions
        * 
-       * * Collection Owner.
-       * * Collection Admin.
-       * * Current NFT Owner.
+       * * Collection owner
+       * * Collection admin
+       * * Current token owner
+       * * Address approved by current item owner
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the collection.
-       * * item_id - ID of NFT to burn.
-       * * from - The owner of the item from whom it is taken away.
+       * * `from`: The owner of the burning item.
+       * * `collection_id`: ID of the collection to which the item belongs.
+       * * `item_id`: ID of item to burn.
+       * * `value`: Number of pieces to burn.
+       * * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.
+       * * Fungible Mode: The desired number of pieces to burn.
+       * * Re-Fungible Mode: The desired number of pieces to burn.
        **/
       burnFrom: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, from: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, itemId: u32 | AnyNumber | Uint8Array, value: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32, u128]>;
       /**
-       * Destroy a concrete instance of NFT.
+       * Destroy an item.
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
-       * * Current NFT Owner
+       * * Collection owner
+       * * Collection admin
+       * * Current item owner
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the collection.
-       * * item_id - ID of NFT to burn.
+       * * `collection_id`: ID of the collection to which the item belongs.
+       * * `item_id`: ID of item to burn.
+       * * `value`: Number of pieces of the item to destroy.
+       * * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.
+       * * Fungible Mode: The desired number of pieces to burn.
+       * * Re-Fungible Mode: The desired number of pieces to burn.
        **/
       burnItem: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, itemId: u32 | AnyNumber | Uint8Array, value: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, u32, u128]>;
       /**
@@ -811,108 +830,126 @@
        * 
        * # Permissions
        * 
-       * * Collection Owner
+       * * Collection owner
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * new_owner.
+       * * `collection_id`: ID of the modified collection.
+       * * `new_owner`: ID of the account that will become the owner.
        **/
       changeCollectionOwner: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, newOwner: AccountId32 | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, AccountId32]>;
       /**
-       * Confirm own sponsorship of a collection.
+       * Confirm own sponsorship of a collection, becoming the sponsor.
        * 
+       * An invitation must be pending, see [`set_collection_sponsor`][`Pallet::set_collection_sponsor`].
+       * Sponsor can pay the fees of a transaction instead of the sender,
+       * but only within specified limits.
+       * 
        * # Permissions
        * 
        * * Sponsor-to-be
        * 
        * # Arguments
        * 
-       * * collection_id.
+       * * `collection_id`: ID of the collection with the pending sponsor.
        **/
       confirmSponsorship: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32]>;
       /**
-       * This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner of the collection is set to the address that signed the transaction and can be changed later.
+       * Create a collection of tokens.
+       * 
+       * Each Token may have multiple properties encoded as an array of bytes
+       * of certain length. The initial owner of the collection is set
+       * to the address that signed the transaction and can be changed later.
        * 
+       * Prefer the more advanced [`create_collection_ex`][`Pallet::create_collection_ex`] instead.
+       * 
        * # Permissions
        * 
-       * * Anyone.
+       * * Anyone - becomes the owner of the new collection.
        * 
        * # Arguments
        * 
-       * * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.
-       * * collection_description - UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.
-       * * token_prefix - UTF-8 string with token prefix.
-       * * mode - [CollectionMode] collection type and type dependent data.
+       * * `collection_name`: Wide-character string with collection name
+       * (limit [`MAX_COLLECTION_NAME_LENGTH`]).
+       * * `collection_description`: Wide-character string with collection description
+       * (limit [`MAX_COLLECTION_DESCRIPTION_LENGTH`]).
+       * * `token_prefix`: Byte string containing the token prefix to mark a collection
+       * to which a token belongs (limit [`MAX_TOKEN_PREFIX_LENGTH`]).
+       * * `mode`: Type of items stored in the collection and type dependent data.
        **/
       createCollection: AugmentedSubmittable<(collectionName: Vec<u16> | (u16 | AnyNumber | Uint8Array)[], collectionDescription: Vec<u16> | (u16 | AnyNumber | Uint8Array)[], tokenPrefix: Bytes | string | Uint8Array, mode: UpDataStructsCollectionMode | { NFT: any } | { Fungible: any } | { ReFungible: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [Vec<u16>, Vec<u16>, Bytes, UpDataStructsCollectionMode]>;
       /**
        * Create a collection with explicit parameters.
-       * Prefer it to the deprecated [`created_collection`] method.
        * 
+       * Prefer it to the deprecated [`create_collection`][`Pallet::create_collection`] method.
+       * 
        * # Permissions
        * 
-       * * Anyone.
+       * * Anyone - becomes the owner of the new collection.
        * 
        * # Arguments
        * 
-       * * data: explicit create-collection data.
+       * * `data`: Explicit data of a collection used for its creation.
        **/
       createCollectionEx: AugmentedSubmittable<(data: UpDataStructsCreateCollectionData | { mode?: any; access?: any; name?: any; description?: any; tokenPrefix?: any; pendingSponsor?: any; limits?: any; permissions?: any; tokenPropertyPermissions?: any; properties?: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [UpDataStructsCreateCollectionData]>;
       /**
-       * Create a concrete instance of NFT Collection created with CreateCollection method.
+       * Mint an item within a collection.
+       * 
+       * A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * * Anyone if
        * * Allow List is enabled, and
        * * Address is added to allow list, and
-       * * MintPermission is enabled (see SetMintPermission method)
+       * * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the collection.
-       * * owner - Address, initial owner of the NFT.
-       * * data - Token data to store on chain.
+       * * `collection_id`: ID of the collection to which an item would belong.
+       * * `owner`: Address of the initial owner of the item.
+       * * `data`: Token data describing the item to store on chain.
        **/
       createItem: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, owner: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, data: UpDataStructsCreateItemData | { NFT: any } | { Fungible: any } | { ReFungible: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, UpDataStructsCreateItemData]>;
       /**
-       * Create multiple items in a collection created with CreateCollection method.
+       * Create multiple items within a collection.
+       * 
+       * A collection must exist first, see [`create_collection_ex`][`Pallet::create_collection_ex`].
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * * Anyone if
        * * Allow List is enabled, and
-       * * Address is added to allow list, and
-       * * MintPermission is enabled (see SetMintPermission method)
+       * * Address is added to the allow list, and
+       * * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the collection.
-       * * owner - Address, initial owner of the NFT.
-       * * items_data - Array items properties. Each property is an array of bytes itself, see [`create_item`].
+       * * `collection_id`: ID of the collection to which the tokens would belong.
+       * * `owner`: Address of the initial owner of the tokens.
+       * * `items_data`: Vector of data describing each item to be created.
        **/
       createMultipleItems: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, owner: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, itemsData: Vec<UpDataStructsCreateItemData> | (UpDataStructsCreateItemData | { NFT: any } | { Fungible: any } | { ReFungible: any } | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, Vec<UpDataStructsCreateItemData>]>;
       /**
-       * Create multiple items inside a collection with explicitly specified initial parameters.
+       * Create multiple items within a collection with explicitly specified initial parameters.
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * * Anyone if
        * * Allow List is enabled, and
        * * Address is added to allow list, and
-       * * MintPermission is enabled (see SetMintPermission method)
+       * * MintPermission is enabled (see [`set_collection_permissions`][`Pallet::set_collection_permissions`])
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the collection.
-       * * data - Explicit item creation data.
+       * * `collection_id`: ID of the collection to which the tokens would belong.
+       * * `data`: Explicit item creation data.
        **/
       createMultipleItemsEx: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, data: UpDataStructsCreateItemExData | { NFT: any } | { Fungible: any } | { RefungibleMultipleItems: any } | { RefungibleMultipleOwners: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, UpDataStructsCreateItemExData]>;
       /**
@@ -925,63 +962,68 @@
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * property_keys - Vector of keys of the properties to be deleted.
+       * * `collection_id`: ID of the modified collection.
+       * * `property_keys`: Vector of keys of the properties to be deleted.
+       * Keys support Latin letters, `-`, `_`, and `.` as symbols.
        **/
       deleteCollectionProperties: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, propertyKeys: Vec<Bytes> | (Bytes | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [u32, Vec<Bytes>]>;
       /**
-       * Delete specified token properties.
+       * Delete specified token properties. Currently properties only work with NFTs.
        * 
        * # Permissions
        * 
        * * Depends on collection's token property permissions and specified property mutability:
-       * * Collection Owner
-       * * Collection Admin
-       * * Token Owner
+       * * Collection owner
+       * * Collection admin
+       * * Token owner
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * token_id.
-       * * property_keys - Vector of keys of the properties to be deleted.
+       * * `collection_id`: ID of the collection to which the token belongs.
+       * * `token_id`: ID of the modified token.
+       * * `property_keys`: Vector of keys of the properties to be deleted.
+       * Keys support Latin letters, `-`, `_`, and `.` as symbols.
        **/
       deleteTokenProperties: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, propertyKeys: Vec<Bytes> | (Bytes | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [u32, u32, Vec<Bytes>]>;
       /**
-       * Destroy the collection if no tokens exist within.
+       * Destroy a collection if no tokens exist within.
        * 
        * # Permissions
        * 
-       * * Collection Owner
+       * * Collection owner
        * 
        * # Arguments
        * 
-       * * collection_id - collection to destroy.
+       * * `collection_id`: Collection to destroy.
        **/
       destroyCollection: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32]>;
       /**
-       * Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.
+       * Remove admin of a collection.
+       * 
+       * An admin address can remove itself. List of admins may become empty,
+       * in which case only Collection Owner will be able to add an Admin.
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the Collection to remove admin for.
-       * * account_id - Address of admin to remove.
+       * * `collection_id`: ID of the collection to remove the admin for.
+       * * `account_id`: Address of the admin to remove.
        **/
       removeCollectionAdmin: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, accountId: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * Switch back to pay-per-own-transaction model.
+       * Remove a collection's a sponsor, making everyone pay for their own transactions.
        * 
        * # Permissions
        * 
-       * * Collection Owner
+       * * Collection owner
        * 
        * # Arguments
        * 
-       * * collection_id.
+       * * `collection_id`: ID of the collection with the sponsor to remove.
        **/
       removeCollectionSponsor: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32]>;
       /**
@@ -989,27 +1031,27 @@
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * address.
+       * * `collection_id`: ID of the modified collection.
+       * * `address`: ID of the address to be removed from the allowlist.
        **/
       removeFromAllowList: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, address: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * Re-partition a refungible token, while owning all of its parts.
+       * Re-partition a refungible token, while owning all of its parts/pieces.
        * 
        * # Permissions
        * 
-       * * Token Owner (must own every part)
+       * * Token owner (must own every part)
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * token_id - ID of the RFT.
-       * * amount - New number of parts into which the token shall be partitioned.
+       * * `collection_id`: ID of the collection the RFT belongs to.
+       * * `token_id`: ID of the RFT.
+       * * `amount`: New number of parts/pieces into which the token shall be partitioned.
        **/
       repartition: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, amount: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, u32, u128]>;
       /**
@@ -1017,13 +1059,14 @@
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * new_limit - New limits of the collection. They will overwrite the current ones.
+       * * `collection_id`: ID of the modified collection.
+       * * `new_limit`: New limits of the collection. Fields that are not set (None)
+       * will not overwrite the old ones.
        **/
       setCollectionLimits: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, newLimit: UpDataStructsCollectionLimits | { accountTokenOwnershipLimit?: any; sponsoredDataSize?: any; sponsoredDataRateLimit?: any; tokenLimit?: any; sponsorTransferTimeout?: any; sponsorApproveTimeout?: any; ownerCanTransfer?: any; ownerCanDestroy?: any; transfersEnabled?: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, UpDataStructsCollectionLimits]>;
       /**
@@ -1031,13 +1074,14 @@
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * new_permission - New permissions of the collection. They will overwrite the current ones.
+       * * `collection_id`: ID of the modified collection.
+       * * `new_permission`: New permissions of the collection. Fields that are not set (None)
+       * will not overwrite the old ones.
        **/
       setCollectionPermissions: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, newPermission: UpDataStructsCollectionPermissions | { access?: any; mintMode?: any; nesting?: any } | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, UpDataStructsCollectionPermissions]>;
       /**
@@ -1045,71 +1089,82 @@
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * properties - Vector of key-value pairs stored as the collection's metadata. Keys support Latin letters, `-`, `_`, and `.` as symbols.
+       * * `collection_id`: ID of the modified collection.
+       * * `properties`: Vector of key-value pairs stored as the collection's metadata.
+       * Keys support Latin letters, `-`, `_`, and `.` as symbols.
        **/
       setCollectionProperties: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, properties: Vec<UpDataStructsProperty> | (UpDataStructsProperty | { key?: any; value?: any } | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [u32, Vec<UpDataStructsProperty>]>;
       /**
-       * Set (invite) a new collection sponsor. If successful, confirmation from the sponsor-to-be will be pending.
+       * Set (invite) a new collection sponsor.
+       * 
+       * If successful, confirmation from the sponsor-to-be will be pending.
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * new_sponsor.
+       * * `collection_id`: ID of the modified collection.
+       * * `new_sponsor`: ID of the account of the sponsor-to-be.
        **/
       setCollectionSponsor: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, newSponsor: AccountId32 | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, AccountId32]>;
       /**
        * Add or change token properties according to collection's permissions.
+       * Currently properties only work with NFTs.
        * 
        * # Permissions
        * 
        * * Depends on collection's token property permissions and specified property mutability:
-       * * Collection Owner
-       * * Collection Admin
-       * * Token Owner
+       * * Collection owner
+       * * Collection admin
+       * * Token owner
+       * 
+       * See [`set_token_property_permissions`][`Pallet::set_token_property_permissions`].
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * token_id.
-       * * properties - Vector of key-value pairs stored as the token's metadata. Keys support Latin letters, `-`, `_`, and `.` as symbols.
+       * * `collection_id: ID of the collection to which the token belongs.
+       * * `token_id`: ID of the modified token.
+       * * `properties`: Vector of key-value pairs stored as the token's metadata.
+       * Keys support Latin letters, `-`, `_`, and `.` as symbols.
        **/
       setTokenProperties: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, properties: Vec<UpDataStructsProperty> | (UpDataStructsProperty | { key?: any; value?: any } | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [u32, u32, Vec<UpDataStructsProperty>]>;
       /**
        * Add or change token property permissions of a collection.
        * 
+       * Without a permission for a particular key, a property with that key
+       * cannot be created in a token.
+       * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
+       * * Collection owner
+       * * Collection admin
        * 
        * # Arguments
        * 
-       * * collection_id.
-       * * property_permissions - Vector of permissions for property keys. Keys support Latin letters, `-`, `_`, and `.` as symbols.
+       * * `collection_id`: ID of the modified collection.
+       * * `property_permissions`: Vector of permissions for property keys.
+       * Keys support Latin letters, `-`, `_`, and `.` as symbols.
        **/
       setTokenPropertyPermissions: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, propertyPermissions: Vec<UpDataStructsPropertyKeyPermission> | (UpDataStructsPropertyKeyPermission | { key?: any; permission?: any } | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [u32, Vec<UpDataStructsPropertyKeyPermission>]>;
       /**
-       * Set transfers_enabled value for particular collection.
+       * Completely allow or disallow transfers for a particular collection.
        * 
        * # Permissions
        * 
-       * * Collection Owner
+       * * Collection owner
        * 
        * # Arguments
        * 
-       * * collection_id - ID of the collection.
-       * * value - New flag value.
+       * * `collection_id`: ID of the collection.
+       * * `value`: New value of the flag, are transfers allowed?
        **/
       setTransfersEnabledFlag: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array, value: bool | boolean | Uint8Array) => SubmittableExtrinsic<ApiType>, [u32, bool]>;
       /**
@@ -1117,44 +1172,50 @@
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
-       * * Current NFT owner
+       * * Collection owner
+       * * Collection admin
+       * * Current token owner
        * 
        * # Arguments
        * 
-       * * recipient - Address of token recipient.
-       * 
-       * * collection_id.
-       * 
-       * * item_id - ID of the item
+       * * `recipient`: Address of token recipient.
+       * * `collection_id`: ID of the collection the item belongs to.
+       * * `item_id`: ID of the item.
        * * Non-Fungible Mode: Required.
        * * Fungible Mode: Ignored.
        * * Re-Fungible Mode: Required.
        * 
-       * * value - Amount to transfer.
-       * * Non-Fungible Mode: Ignored
-       * * Fungible Mode: Must specify transferred amount
-       * * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)
+       * * `value`: Amount to transfer.
+       * * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.
+       * * Fungible Mode: The desired number of pieces to transfer.
+       * * Re-Fungible Mode: The desired number of pieces to transfer.
        **/
       transfer: AugmentedSubmittable<(recipient: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, collectionId: u32 | AnyNumber | Uint8Array, itemId: u32 | AnyNumber | Uint8Array, value: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [PalletEvmAccountBasicCrossAccountIdRepr, u32, u32, u128]>;
       /**
-       * Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.
+       * Change ownership of an item on behalf of the owner as a non-owner account.
+       * 
+       * See the [`approve`][`Pallet::approve`] method for additional information.
+       * 
+       * After this method executes, one approval is removed from the total so that
+       * the approved address will not be able to transfer this item again from this owner.
        * 
        * # Permissions
        * 
-       * * Collection Owner
-       * * Collection Admin
-       * * Current NFT owner
-       * * Address approved by current NFT owner
+       * * Collection owner
+       * * Collection admin
+       * * Current item owner
+       * * Address approved by current item owner
        * 
        * # Arguments
        * 
-       * * from - Address that currently owns the token.
-       * * recipient - Address of the new token-owner-to-be.
-       * * collection_id.
-       * * item_id - ID of the item to be transferred.
-       * * value - Amount to transfer.
+       * * `from`: Address that currently owns the token.
+       * * `recipient`: Address of the new token-owner-to-be.
+       * * `collection_id`: ID of the collection the item.
+       * * `item_id`: ID of the item to be transferred.
+       * * `value`: Amount to transfer.
+       * * Non-Fungible Mode: An NFT is indivisible, there is always 1 corresponding to an ID.
+       * * Fungible Mode: The desired number of pieces to transfer.
+       * * Re-Fungible Mode: The desired number of pieces to transfer.
        **/
       transferFrom: AugmentedSubmittable<(from: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, recipient: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, collectionId: u32 | AnyNumber | Uint8Array, itemId: u32 | AnyNumber | Uint8Array, value: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic<ApiType>, [PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u32, u32, u128]>;
       /**
modifiedtests/src/interfaces/default/types.tsdiffbeforeafterboth
--- a/tests/src/interfaces/default/types.ts
+++ b/tests/src/interfaces/default/types.ts
@@ -2461,12 +2461,14 @@
 export interface UpDataStructsCreateReFungibleData extends Struct {
   readonly constData: Bytes;
   readonly pieces: u128;
+  readonly properties: Vec<UpDataStructsProperty>;
 }
 
 /** @name UpDataStructsCreateRefungibleExData */
 export interface UpDataStructsCreateRefungibleExData extends Struct {
   readonly constData: Bytes;
   readonly users: BTreeMap<PalletEvmAccountBasicCrossAccountIdRepr, u128>;
+  readonly properties: Vec<UpDataStructsProperty>;
 }
 
 /** @name UpDataStructsNestingPermissions */
modifiedtests/src/interfaces/lookup.tsdiffbeforeafterboth
--- a/tests/src/interfaces/lookup.ts
+++ b/tests/src/interfaces/lookup.ts
@@ -1502,7 +1502,8 @@
    **/
   UpDataStructsCreateReFungibleData: {
     constData: 'Bytes',
-    pieces: 'u128'
+    pieces: 'u128',
+    properties: 'Vec<UpDataStructsProperty>'
   },
   /**
    * Lookup193: up_data_structs::CreateItemExData<pallet_evm::account::BasicCrossAccountIdRepr<sp_core::crypto::AccountId32>>
@@ -1527,7 +1528,8 @@
    **/
   UpDataStructsCreateRefungibleExData: {
     constData: 'Bytes',
-    users: 'BTreeMap<PalletEvmAccountBasicCrossAccountIdRepr, u128>'
+    users: 'BTreeMap<PalletEvmAccountBasicCrossAccountIdRepr, u128>',
+    properties: 'Vec<UpDataStructsProperty>'
   },
   /**
    * Lookup204: pallet_unique_scheduler::pallet::Call<T>
modifiedtests/src/interfaces/types-lookup.tsdiffbeforeafterboth
--- a/tests/src/interfaces/types-lookup.ts
+++ b/tests/src/interfaces/types-lookup.ts
@@ -1629,6 +1629,7 @@
   export interface UpDataStructsCreateReFungibleData extends Struct {
     readonly constData: Bytes;
     readonly pieces: u128;
+    readonly properties: Vec<UpDataStructsProperty>;
   }
 
   /** @name UpDataStructsCreateItemExData (193) */
@@ -1654,6 +1655,7 @@
   export interface UpDataStructsCreateRefungibleExData extends Struct {
     readonly constData: Bytes;
     readonly users: BTreeMap<PalletEvmAccountBasicCrossAccountIdRepr, u128>;
+    readonly properties: Vec<UpDataStructsProperty>;
   }
 
   /** @name PalletUniqueSchedulerCall (204) */
modifiedtests/src/interfaces/unique/definitions.tsdiffbeforeafterboth
--- a/tests/src/interfaces/unique/definitions.ts
+++ b/tests/src/interfaces/unique/definitions.ts
@@ -63,6 +63,11 @@
       [collectionParam, tokenParam], 
       `Option<${CROSS_ACCOUNT_ID_TYPE}>`,
     ),
+    tokenOwners: fun(
+      'Returns 10 tokens owners in no particular order', 
+      [collectionParam, tokenParam], 
+      `Vec<${CROSS_ACCOUNT_ID_TYPE}>`
+    ),
     tokenChildren: fun(
       'Get tokens nested directly into the token', 
       [collectionParam, tokenParam], 
@@ -85,6 +90,17 @@
       'Vec<UpDataStructsPropertyKeyPermission>',
     ),
 
+    constMetadata: fun(
+      'Get token constant metadata', 
+      [collectionParam, tokenParam], 
+      'Vec<u8>',
+    ),
+    variableMetadata: fun(
+      'Get token variable metadata', 
+      [collectionParam, tokenParam], 
+      'Vec<u8>',
+    ),
+
     tokenData: fun(
       'Get token data, including properties, optionally limited to the provided keys, and total pieces for an RFT',
       [collectionParam, tokenParam, propertyKeysParam],
@@ -128,7 +144,11 @@
       'bool',
     ),
 
-    lastTokenId: fun('Get the last token ID created in a collection', [collectionParam], 'u32'),
+    lastTokenId: fun(
+      'Get the last token ID created in a collection', 
+      [collectionParam], 
+      'u32',
+    ),
     collectionById: fun(
       'Get a collection by the specified ID', 
       [collectionParam], 
modifiedtests/src/nesting/properties.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/properties.test.ts
+++ b/tests/src/nesting/properties.test.ts
@@ -2,6 +2,7 @@
 import usingApi, {executeTransaction} from '../substrate/substrate-api';
 import {
   addCollectionAdminExpectSuccess,
+  CollectionMode,
   createCollectionExpectSuccess,
   setCollectionPermissionsExpectSuccess,
   createItemExpectSuccess,
@@ -23,9 +24,9 @@
     });
   });
 
-  it('Makes sure collectionById supplies required fields', async () => {
+  async function testMakeSureSuppliesRequired(mode: CollectionMode) {
     await usingApi(async api => {
-      const collectionId = await createCollectionExpectSuccess();
+      const collectionId = await createCollectionExpectSuccess({mode: mode});
 
       const collectionOption = await api.rpc.unique.collectionById(collectionId);
       expect(collectionOption.isSome).to.be.true;
@@ -57,7 +58,15 @@
       expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions);
       expect(collection.properties.toHuman()).to.be.deep.equal(collectionProperties);
     });
+  }
+
+  it('Makes sure collectionById supplies required fields for NFT', async () => {
+    await testMakeSureSuppliesRequired({type: 'NFT'});
   });
+
+  it('Makes sure collectionById supplies required fields for ReFungible', async () => {
+    await testMakeSureSuppliesRequired({type: 'ReFungible'});
+  });
 });
 
 // ---------- COLLECTION PROPERTIES
@@ -79,9 +88,10 @@
     });
   });
 
-  it('Sets properties for a collection', async () => {
+
+  async function testSetsPropertiesForCollection(mode: string) {
     await usingApi(async api => {
-      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));
+      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode}));
       const {collectionId} = getCreateCollectionResult(events);
 
       // As owner
@@ -106,48 +116,54 @@
         {key: 'black_hole', value: ''},
       ]);
     });
+  }
+  it('Sets properties for a NFT collection', async () => {
+    await testSetsPropertiesForCollection('NFT');
   });
+  it('Sets properties for a ReFungible collection', async () => {
+    await testSetsPropertiesForCollection('ReFungible');
+  });
 
-  it('Check valid names for collection properties keys', async () => {
+  async function testCheckValidNames(mode: string) {
     await usingApi(async api => {
-      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: 'NFT'}));
+      const events = await executeTransaction(api, bob, api.tx.unique.createCollectionEx({mode: mode}));
       const {collectionId} = getCreateCollectionResult(events);
-
+  
       // alpha symbols
       await expect(executeTransaction(
         api, 
         bob, 
         api.tx.unique.setCollectionProperties(collectionId, [{key: 'alpha'}]), 
       )).to.not.be.rejected;
-
+  
       // numeric symbols
       await expect(executeTransaction(
         api, 
         bob, 
         api.tx.unique.setCollectionProperties(collectionId, [{key: '123'}]), 
       )).to.not.be.rejected;
-
+  
       // underscore symbol
       await expect(executeTransaction(
         api, 
         bob, 
         api.tx.unique.setCollectionProperties(collectionId, [{key: 'black_hole'}]), 
       )).to.not.be.rejected;
-
+  
       // dash symbol
       await expect(executeTransaction(
         api, 
         bob, 
         api.tx.unique.setCollectionProperties(collectionId, [{key: 'semi-automatic'}]), 
       )).to.not.be.rejected;
-
+  
       // underscore symbol
       await expect(executeTransaction(
         api, 
         bob, 
         api.tx.unique.setCollectionProperties(collectionId, [{key: 'build.rs'}]), 
       )).to.not.be.rejected;
-
+  
       const propertyKeys = ['alpha', '123', 'black_hole', 'semi-automatic', 'build.rs'];
       const properties = (await api.rpc.unique.collectionProperties(collectionId, propertyKeys)).toHuman();
       expect(properties).to.be.deep.equal([
@@ -158,54 +174,72 @@
         {key: 'build.rs', value: ''},
       ]);
     });
+  }
+  it('Check valid names for NFT collection properties keys', async () => {
+    await testCheckValidNames('NFT');
+  });
+  it('Check valid names for ReFungible collection properties keys', async () => {
+    await testCheckValidNames('ReFungible');
   });
 
-  it('Changes properties of a collection', async () => {
+  async function testChangesProperties(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole'}]), 
       )).to.not.be.rejected;
-
+  
       // Mutate the properties
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'bonded'}, {key: 'black_hole', value: 'LIGO'}]), 
       )).to.not.be.rejected;
-
+  
       const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();
       expect(properties).to.be.deep.equal([
         {key: 'electron', value: 'bonded'},
         {key: 'black_hole', value: 'LIGO'},
       ]);
     });
+  }
+  it('Changes properties of a NFT collection', async () => {
+    await testChangesProperties({type: 'NFT'});
+  });
+  it('Changes properties of a ReFungible collection', async () => {
+    await testChangesProperties({type: 'ReFungible'});
   });
 
-  it('Deletes properties of a collection', async () => {
+  async function testDeleteProperties(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]), 
       )).to.not.be.rejected;
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.deleteCollectionProperties(collection, ['electron']), 
       )).to.not.be.rejected;
-
+  
       const properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black_hole'])).toHuman();
       expect(properties).to.be.deep.equal([
         {key: 'black_hole', value: 'LIGO'},
       ]);
-    });
+    });  
+  }
+  it('Deletes properties of a NFT collection', async () => {
+    await testDeleteProperties({type: 'NFT'});
+  });
+  it('Deletes properties of a ReFungible collection', async () => {
+    await testDeleteProperties({type: 'ReFungible'});
   });
 });
 
@@ -217,10 +251,10 @@
     });
   });
   
-  it('Fails to set properties in a collection if not its onwer/administrator', async () => {
+  async function testFailsSetPropertiesIfNotOwnerOrAdmin(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       await expect(executeTransaction(
         api, 
         bob, 
@@ -230,14 +264,20 @@
       const properties = (await api.query.common.collectionProperties(collection)).toJSON();
       expect(properties.map).to.be.empty;
       expect(properties.consumedSpace).to.equal(0);
-    });
+    });  
+  }
+  it('Fails to set properties in a NFT collection if not its onwer/administrator', async () => {
+    await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'NFT'});
   });
+  it('Fails to set properties in a ReFungible collection if not its onwer/administrator', async () => {
+    await testFailsSetPropertiesIfNotOwnerOrAdmin({type: 'ReFungible'});
+  });
   
-  it('Fails to set properties that exceed the limits', async () => {
+  async function testFailsSetPropertiesThatExeedLimits(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
+      const collection = await createCollectionExpectSuccess({mode: mode});
       const spaceLimit = (await api.query.common.collectionProperties(collection)).toJSON().spaceLimit as number; 
-
+  
       // Mute the general tx parsing error, too many bytes to process
       {
         console.error = () => {};
@@ -247,10 +287,10 @@
           api.tx.unique.setCollectionProperties(collection, [{key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))}]), 
         )).to.be.rejected;
       }
-
+  
       let properties = (await api.rpc.unique.collectionProperties(collection, ['electron'])).toJSON();
       expect(properties).to.be.empty;
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
@@ -259,16 +299,22 @@
           {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))}, 
         ]), 
       )).to.be.rejectedWith(/common\.NoSpaceForProperty/);
-
+  
       properties = (await api.rpc.unique.collectionProperties(collection, ['electron', 'black hole'])).toJSON();
       expect(properties).to.be.empty;
-    });
+    });  
+  }
+  it('Fails to set properties that exceed the limits (NFT)', async () => {
+    await testFailsSetPropertiesThatExeedLimits({type: 'NFT'});
+  });
+  it('Fails to set properties that exceed the limits (ReFungible)', async () => {
+    await testFailsSetPropertiesThatExeedLimits({type: 'ReFungible'});
   });
   
-  it('Fails to set more properties than it is allowed', async () => {
+  async function testFailsSetMorePropertiesThanAllowed(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       const propertiesToBeSet = [];
       for (let i = 0; i < 65; i++) {
         propertiesToBeSet.push({
@@ -276,29 +322,35 @@
           value: Math.random() > 0.5 ? 'high' : 'low',
         });
       }
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setCollectionProperties(collection, propertiesToBeSet), 
       )).to.be.rejectedWith(/common\.PropertyLimitReached/);
-
+  
       const properties = (await api.query.common.collectionProperties(collection)).toJSON();
       expect(properties.map).to.be.empty;
       expect(properties.consumedSpace).to.equal(0);
-    });
+    });  
+  }
+  it('Fails to set more properties than it is allowed (NFT)', async () => {
+    await testFailsSetMorePropertiesThanAllowed({type: 'NFT'});
   });
-
-  it('Fails to set properties with invalid names', async () => {
+  it('Fails to set more properties than it is allowed (ReFungible)', async () => {
+    await testFailsSetMorePropertiesThanAllowed({type: 'ReFungible'});
+  });
+  
+  async function testFailsSetPropertiesWithInvalidNames(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       const invalidProperties = [
         [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],
         [{key: 'Mr/Sandman', value: 'Bring me a gene'}],
         [{key: 'déjà vu', value: 'hmm...'}],
       ];
-
+  
       for (let i = 0; i < invalidProperties.length; i++) {
         await expect(executeTransaction(
           api, 
@@ -306,13 +358,13 @@
           api.tx.unique.setCollectionProperties(collection, invalidProperties[i]), 
         ), `on rejecting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
       }
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setCollectionProperties(collection, [{key: '', value: 'nothing must not exist'}]), 
       ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
@@ -320,14 +372,14 @@
           {key: 'CRISPR-Cas9', value: 'rewriting nature!'},
         ]), 
       ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;
-
+  
       const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');
-
+  
       const properties = (await api.rpc.unique.collectionProperties(collection, keys)).toHuman();
       expect(properties).to.be.deep.equal([
         {key: 'CRISPR-Cas9', value: 'rewriting nature!'},
       ]);
-
+  
       for (let i = 0; i < invalidProperties.length; i++) {
         await expect(executeTransaction(
           api, 
@@ -336,7 +388,13 @@
         ), `on trying to delete the non-existent badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
       }
     });
+  }
+  it('Fails to set properties with invalid names (NFT)', async () => {
+    await testFailsSetPropertiesWithInvalidNames({type: 'NFT'});
   });
+  it('Fails to set properties with invalid names (ReFungible)', async () => {
+    await testFailsSetPropertiesWithInvalidNames({type: 'ReFungible'});
+  });
 });
 
 // ---------- ACCESS RIGHTS
@@ -357,54 +415,66 @@
     });
   });
   
-  it('Sets access rights to properties of a collection', async () => {
+  async function testSetsAccessRightsToProperties(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true}}]), 
       )).to.not.be.rejected;
-
+  
       await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]), 
       )).to.not.be.rejected;
-
+  
       const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery', 'mindgame'])).toHuman();
       expect(propertyRights).to.be.deep.equal([
         {key: 'skullduggery', permission: {'mutable': true, 'collectionAdmin': false, 'tokenOwner': false}},
         {key: 'mindgame', permission: {'mutable': false, 'collectionAdmin': true, 'tokenOwner': false}},
       ]);
-    });
+    });  
+  }
+  it('Sets access rights to properties of a collection (NFT)', async () => {
+    await testSetsAccessRightsToProperties({type: 'NFT'});
   });
+  it('Sets access rights to properties of a collection (ReFungible)', async () => {
+    await testSetsAccessRightsToProperties({type: 'ReFungible'});
+  });
   
-  it('Changes access rights to properties of a collection', async () => {
+  async function testChangesAccessRightsToProperty(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]), 
       )).to.not.be.rejected;
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 
       )).to.not.be.rejected;
-
+  
       const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();
       expect(propertyRights).to.be.deep.equal([
         {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
       ]);
     });
+  }
+  it('Changes access rights to properties of a NFT collection', async () => {
+    await testChangesAccessRightsToProperty({type: 'NFT'});
   });
+  it('Changes access rights to properties of a ReFungible collection', async () => {
+    await testChangesAccessRightsToProperty({type: 'ReFungible'});
+  });
 });
 
 describe('Negative Integration Test: Access Rights to Token Properties', () => {
@@ -415,25 +485,31 @@
     });
   });
 
-  it('Prevents from setting access rights to properties of a collection if not an onwer/admin', async () => {
+  async function testPreventsFromSettingAccessRightsNotAdminOrOwner(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       await expect(executeTransaction(
         api, 
         bob, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]), 
       )).to.be.rejectedWith(/common\.NoPermission/);
-
+  
       const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toJSON();
       expect(propertyRights).to.be.empty;
     });
+  }
+  it('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async () => {
+    await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'NFT'});
   });
+  it('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', async () => {
+    await testPreventsFromSettingAccessRightsNotAdminOrOwner({type: 'ReFungible'});
+  });
 
-  it('Prevents from adding too many possible properties', async () => {
+  async function testPreventFromAddingTooManyPossibleProperties(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       const constitution = [];
       for (let i = 0; i < 65; i++) {
         constitution.push({
@@ -441,51 +517,63 @@
           permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},
         });
       }
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, constitution), 
       )).to.be.rejectedWith(/common\.PropertyLimitReached/);
-
+  
       const propertyRights = (await api.query.common.collectionPropertyPermissions(collection)).toJSON();
       expect(propertyRights).to.be.empty;
-    });
+    });  
+  }
+  it('Prevents from adding too many possible properties (NFT)', async () => {
+    await testPreventFromAddingTooManyPossibleProperties({type: 'NFT'});
   });
+  it('Prevents from adding too many possible properties (ReFungible)', async () => {
+    await testPreventFromAddingTooManyPossibleProperties({type: 'ReFungible'});
+  });
 
-  it('Prevents access rights to be modified if constant', async () => {
+  async function testPreventAccessRightsModifiedIfConstant(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]), 
       )).to.not.be.rejected;
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'skullduggery', permission: {}}]), 
       )).to.be.rejectedWith(/common\.NoPermission/);
-
+  
       const propertyRights = (await api.rpc.unique.propertyPermissions(collection, ['skullduggery'])).toHuman();
       expect(propertyRights).to.deep.equal([
         {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
       ]);
-    });
+    });  
+  }
+  it('Prevents access rights to be modified if constant (NFT)', async () => {
+    await testPreventAccessRightsModifiedIfConstant({type: 'NFT'});
+  });
+  it('Prevents access rights to be modified if constant (ReFungible)', async () => {
+    await testPreventAccessRightsModifiedIfConstant({type: 'ReFungible'});
   });
 
-  it('Prevents adding properties with invalid names', async () => {
+  async function testPreventsAddingPropertiesWithInvalidNames(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-
+      const collection = await createCollectionExpectSuccess({mode: mode});
+  
       const invalidProperties = [
         [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],
         [{key: 'G#4', permission: {tokenOwner: true}}],
         [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],
       ];
-
+  
       for (let i = 0; i < invalidProperties.length; i++) {
         await expect(executeTransaction(
           api, 
@@ -493,13 +581,13 @@
           api.tx.unique.setTokenPropertyPermissions(collection, invalidProperties[i]), 
         ), `on setting the new badly-named property #${i}`).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
       }
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: '', permission: {}}]), 
       ), 'on rejecting an unnamed property').to.be.rejectedWith(/common\.EmptyPropertyKey/);
-
+  
       const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string
       await expect(executeTransaction(
         api, 
@@ -508,30 +596,33 @@
           {key: correctKey, permission: {collectionAdmin: true}},
         ]), 
       ), 'on setting the correctly-but-still-badly-named property').to.not.be.rejected;
-
+  
       const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');
-
+  
       const propertyRights = (await api.rpc.unique.propertyPermissions(collection, keys)).toHuman();
       expect(propertyRights).to.be.deep.equal([
         {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},
       ]);
     });
+  }
+  it('Prevents adding properties with invalid names (NFT)', async () => {
+    await testPreventsAddingPropertiesWithInvalidNames({type: 'NFT'});
+  });
+  it('Prevents adding properties with invalid names (ReFungible)', async () => {
+    await testPreventsAddingPropertiesWithInvalidNames({type: 'ReFungible'});
   });
 });
 
 // ---------- TOKEN PROPERTIES
 
 describe('Integration Test: Token Properties', () => {
-  let collection: number;
-  let token: number;
-  let nestedToken: number;
   let permissions: {permission: any, signers: IKeyringPair[]}[];
 
   before(async () => {
     await usingApi(async (api, privateKeyWrapper) => {
-      alice = privateKeyWrapper('//Alice');
-      bob = privateKeyWrapper('//Bob');
-      charlie = privateKeyWrapper('//Charlie');
+      alice = privateKeyWrapper('//Alice'); // collection owner
+      bob = privateKeyWrapper('//Bob'); // collection admin
+      charlie = privateKeyWrapper('//Charlie'); // token owner
 
       permissions = [
         {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},
@@ -543,36 +634,34 @@
       ];
     });
   });
-
-  beforeEach(async () => {
-    await usingApi(async () => {
-      collection = await createCollectionExpectSuccess();
-      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
-
-      token = await createItemExpectSuccess(alice, collection, 'NFT');
-      nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});
-
-      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-      await transferExpectSuccess(collection, token, alice, charlie);
-    });
-  });
   
-  it('Reads yet empty properties of a token', async () => {
+  async function testReadsYetEmptyProperties(mode: CollectionMode) {
     await usingApi(async api => {
-      const collection = await createCollectionExpectSuccess();
-      const token = await createItemExpectSuccess(alice, collection, 'NFT');
+      const collection = await createCollectionExpectSuccess({mode: mode});
+      const token = await createItemExpectSuccess(alice, collection, mode.type);
   
       const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(properties.map).to.be.empty;
       expect(properties.consumedSpace).to.be.equal(0);
-
+  
       const tokenData = (await api.rpc.unique.tokenData(collection, token, ['anything'])).toJSON().properties;
       expect(tokenData).to.be.empty;
     });
+  }
+  it('Reads yet empty properties of a token (NFT)', async () => {
+    await testReadsYetEmptyProperties({type: 'NFT'});
   });
+  it('Reads yet empty properties of a token (ReFungible)', async () => {
+    await testReadsYetEmptyProperties({type: 'ReFungible'});
+  });
 
-  it('Assigns properties to a token according to permissions', async () => {
+  async function testAssignPropertiesAccordingToPermissions(mode: CollectionMode, pieces: number) {
     await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: mode});
+      const token = await createItemExpectSuccess(alice, collection, mode.type);
+      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
+      await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);
+
       const propertyKeys: string[] = [];
       let i = 0;
       for (const permission of permissions) {
@@ -603,10 +692,21 @@
         expect(tokensData[i].value).to.be.equal('Serotonin increase');
       }
     });
+  }
+  it('Assigns properties to a token according to permissions (NFT)', async () => {
+    await testAssignPropertiesAccordingToPermissions({type: 'NFT'}, 1);
+  });
+  it('Assigns properties to a token according to permissions (ReFungible)', async () => {
+    await testAssignPropertiesAccordingToPermissions({type: 'ReFungible'}, 100);
   });
 
-  it('Changes properties of a token according to permissions', async () => {
+  async function testChangesPropertiesAccordingPermission(mode: CollectionMode, pieces: number) {
     await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: mode});
+      const token = await createItemExpectSuccess(alice, collection, mode.type);
+      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
+      await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);
+
       const propertyKeys: string[] = [];
       let i = 0;
       for (const permission of permissions) {
@@ -615,29 +715,29 @@
         for (const signer of permission.signers) {
           const key = i + '_' + signer.address;
           propertyKeys.push(key);
-
+  
           await expect(executeTransaction(
             api, 
             alice, 
             api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 
           ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;
-
+  
           await expect(executeTransaction(
             api, 
             signer, 
             api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 
           ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;
-
+  
           await expect(executeTransaction(
             api, 
             signer, 
             api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin stable'}]), 
           ), `on changing property ${i} by ${signer.address}`).to.not.be.rejected;
         }
-
+  
         i++;
       }
-
+  
       const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toHuman() as any[];
       const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toHuman().properties as any[];
       for (let i = 0; i < properties.length; i++) {
@@ -645,32 +745,43 @@
         expect(tokensData[i].value).to.be.equal('Serotonin stable');
       }
     });
+  }
+  it('Changes properties of a token according to permissions (NFT)', async () => {
+    await testChangesPropertiesAccordingPermission({type: 'NFT'}, 1);
   });
+  it('Changes properties of a token according to permissions (ReFungible)', async () => {
+    await testChangesPropertiesAccordingPermission({type: 'ReFungible'}, 100);
+  });
 
-  it('Deletes properties of a token according to permissions', async () => {
+  async function testDeletePropertiesAccordingPermission(mode: CollectionMode, pieces: number) {
     await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: mode});
+      const token = await createItemExpectSuccess(alice, collection, mode.type);
+      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
+      await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);
+
       const propertyKeys: string[] = [];
       let i = 0;
-
+  
       for (const permission of permissions) {
         if (!permission.permission.mutable) continue;
         
         for (const signer of permission.signers) {
           const key = i + '_' + signer.address;
           propertyKeys.push(key);
-
+  
           await expect(executeTransaction(
             api, 
             alice, 
             api.tx.unique.setTokenPropertyPermissions(collection, [{key: key, permission: permission.permission}]), 
           ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;
-
+  
           await expect(executeTransaction(
             api, 
             signer, 
             api.tx.unique.setTokenProperties(collection, token, [{key: key, value: 'Serotonin increase'}]), 
           ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;
-
+  
           await expect(executeTransaction(
             api, 
             signer, 
@@ -680,17 +791,30 @@
         
         i++;
       }
-
+  
       const properties = (await api.rpc.unique.tokenProperties(collection, token, propertyKeys)).toJSON() as any[];
       expect(properties).to.be.empty;
       const tokensData = (await api.rpc.unique.tokenData(collection, token, propertyKeys)).toJSON().properties as any[];
       expect(tokensData).to.be.empty;
       expect((await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace).to.be.equal(0);
     });
+  }
+  it('Deletes properties of a token according to permissions (NFT)', async () => {
+    await testDeletePropertiesAccordingPermission({type: 'NFT'}, 1);
   });
+  it('Deletes properties of a token according to permissions (ReFungible)', async () => {
+    await testDeletePropertiesAccordingPermission({type: 'ReFungible'}, 100);
+  });
 
   it('Assigns properties to a nested token according to permissions', async () => {
     await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
+      const token = await createItemExpectSuccess(alice, collection, 'NFT');
+      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});
+      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
+      await transferExpectSuccess(collection, token, alice, charlie);
+
       const propertyKeys: string[] = [];
       let i = 0;
       for (const permission of permissions) {
@@ -725,6 +849,13 @@
 
   it('Changes properties of a nested token according to permissions', async () => {
     await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
+      const token = await createItemExpectSuccess(alice, collection, 'NFT');
+      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});
+      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
+      await transferExpectSuccess(collection, token, alice, charlie);
+
       const propertyKeys: string[] = [];
       let i = 0;
       for (const permission of permissions) {
@@ -767,6 +898,13 @@
 
   it('Deletes properties of a nested token according to permissions', async () => {
     await usingApi(async api => {
+      const collection = await createCollectionExpectSuccess({mode: {type: 'NFT'}});
+      await setCollectionPermissionsExpectSuccess(alice, collection, {nesting: {tokenOwner: true}});
+      const token = await createItemExpectSuccess(alice, collection, 'NFT');
+      const nestedToken = await createItemExpectSuccess(alice, collection, 'NFT', {Ethereum: tokenIdToAddress(collection, token)});
+      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
+      await transferExpectSuccess(collection, token, alice, charlie);
+
       const propertyKeys: string[] = [];
       let i = 0;
 
@@ -832,11 +970,11 @@
     });
   });
 
-  beforeEach(async () => {
-    collection = await createCollectionExpectSuccess();
-    token = await createItemExpectSuccess(alice, collection, 'NFT');
+  async function prepare(mode: CollectionMode, pieces: number) {
+    collection = await createCollectionExpectSuccess({mode: mode});
+    token = await createItemExpectSuccess(alice, collection, mode.type);
     await addCollectionAdminExpectSuccess(alice, collection, bob.address);
-    await transferExpectSuccess(collection, token, alice, charlie);
+    await transferExpectSuccess(collection, token, alice, charlie, pieces, mode.type);
         
     await usingApi(async api => {
       let i = 0;
@@ -848,71 +986,89 @@
           alice, 
           api.tx.unique.setTokenPropertyPermissions(collection, [{key: `${i}`, permission: passage.permission}]), 
         ), `on setting permission ${i} by ${signer.address}`).to.not.be.rejected;
-
+  
         await expect(executeTransaction(
           api, 
           signer, 
           api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin increase'}]), 
         ), `on adding property ${i} by ${signer.address}`).to.not.be.rejected;
-
+  
         i++;
       }
-
+  
       originalSpace = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON().consumedSpace as number;
-    });
-  });
+    }); 
+  }
 
-  it('Forbids changing/deleting properties of a token if the user is outside of permissions', async () => {
+  async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(mode: CollectionMode, pieces: number) {
+    await prepare(mode, pieces);
+  
     await usingApi(async api => {
       let i = -1;
       for (const forbiddance of constitution) {
         i++;
         if (!forbiddance.permission.mutable) continue;
-
+  
         await expect(executeTransaction(
           api, 
           forbiddance.sinner, 
           api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 
         ), `on failing to change property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);
-
+  
         await expect(executeTransaction(
           api, 
           forbiddance.sinner, 
           api.tx.unique.deleteTokenProperties(collection, token, [`${i}`]), 
         ), `on failing to delete property ${i} by ${forbiddance.sinner.address}`).to.be.rejectedWith(/common\.NoPermission/);
       }
-
+  
       const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(properties.consumedSpace).to.be.equal(originalSpace);
     });
+  }
+  it('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async () => {
+    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'NFT'}, 1);
   });
+  it('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', async () => {
+    await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions({type: 'ReFungible'}, 100);
+  });
 
-  it('Forbids changing/deleting properties of a token if the property is permanent (immutable)', async () => {
+  async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(mode: CollectionMode, pieces: number) {
+    await prepare(mode, pieces);
+    
     await usingApi(async api => {
       let i = -1;
       for (const permission of constitution) {
         i++;
         if (permission.permission.mutable) continue;
-
+  
         await expect(executeTransaction(
           api, 
           permission.signers[0], 
           api.tx.unique.setTokenProperties(collection, token, [{key: `${i}`, value: 'Serotonin down'}]), 
         ), `on failing to change property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);
-
+  
         await expect(executeTransaction(
           api, 
           permission.signers[0], 
           api.tx.unique.deleteTokenProperties(collection, token, [i.toString()]), 
         ), `on failing to delete property ${i} by ${permission.signers[0].address}`).to.be.rejectedWith(/common\.NoPermission/);
       }
-
+  
       const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(properties.consumedSpace).to.be.equal(originalSpace);
-    });
+    });  
+  }
+  it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async () => {
+    await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'NFT'}, 1);
   });
+  it('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', async () => {
+    await testForbidsChangingDeletingPropertiesIfPropertyImmutable({type: 'ReFungible'}, 100);
+  });
+
+  async function testForbidsAddingPropertiesIfPropertyNotDeclared(mode: CollectionMode, pieces: number) {
+    await prepare(mode, pieces);
 
-  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission', async () => {
     await usingApi(async api => {
       await expect(executeTransaction(
         api, 
@@ -925,20 +1081,28 @@
         alice, 
         api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'now-existent', permission: {}}]), 
       ), 'on setting a new non-permitted property').to.not.be.rejected;
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
         api.tx.unique.setTokenProperties(collection, token, [{key: 'now-existent', value: 'I exist!'}]), 
       ), 'on failing to add a property forbidden by the \'None\' permission').to.be.rejectedWith(/common\.NoPermission/);
-
+  
       expect((await api.rpc.unique.tokenProperties(collection, token, ['non-existent', 'now-existent'])).toJSON()).to.be.empty;
       const properties = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(properties.consumedSpace).to.be.equal(originalSpace);
     });
+  }
+  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async () => {
+    await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'NFT'}, 1);
+  });
+  it('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', async () => {
+    await testForbidsAddingPropertiesIfPropertyNotDeclared({type: 'ReFungible'}, 100);
   });
 
-  it('Forbids adding too many properties to a token', async () => {
+  async function testForbidsAddingTooManyProperties(mode: CollectionMode, pieces: number) {
+    await prepare(mode, pieces);
+
     await usingApi(async api => {
       await expect(executeTransaction(
         api, 
@@ -948,7 +1112,7 @@
           {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},
         ]), 
       ), 'on setting a new non-permitted property').to.not.be.rejected;
-
+  
       // Mute the general tx parsing error
       {
         console.error = () => {};
@@ -958,7 +1122,7 @@
           api.tx.unique.setCollectionProperties(collection, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]), 
         )).to.be.rejected;
       }
-
+  
       await expect(executeTransaction(
         api, 
         alice, 
@@ -967,10 +1131,107 @@
           {key: 'young_years', value: 'neverending'.repeat(1490)},
         ]), 
       )).to.be.rejectedWith(/common\.NoSpaceForProperty/);
-
+  
       expect((await api.rpc.unique.tokenProperties(collection, token, ['a_holy_book', 'young_years'])).toJSON()).to.be.empty;
       const propertiesMap = (await api.query.nonfungible.tokenProperties(collection, token)).toJSON();
       expect(propertiesMap.consumedSpace).to.be.equal(originalSpace);
     });
+  }
+  it('Forbids adding too many properties to a token (NFT)', async () => {
+    await testForbidsAddingTooManyProperties({type: 'NFT'}, 1);
+  });
+  it('Forbids adding too many properties to a token (ReFungible)', async () => {
+    await testForbidsAddingTooManyProperties({type: 'ReFungible'}, 100);
+  });
+});
+
+describe('ReFungible token properties permissions tests', () => {
+  let collection: number;
+  let token: number;
+
+  before(async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      alice = privateKeyWrapper('//Alice');
+      bob = privateKeyWrapper('//Bob');
+      charlie = privateKeyWrapper('//Charlie');
+    });
+  });
+
+  beforeEach(async () => {
+    await usingApi(async api => {
+      collection = await createCollectionExpectSuccess({mode: {type: 'ReFungible'}});
+      token = await createItemExpectSuccess(alice, collection, 'ReFungible');
+      await addCollectionAdminExpectSuccess(alice, collection, bob.address);
+
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), 
+      )).to.not.be.rejected;
+    });
+  });
+
+  it('Forbids add token property with tokenOwher==true but signer have\'t all pieces', async () => {
+    await usingApi(async api => {
+      await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible');
+  
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setTokenProperties(collection, token, [
+          {key: 'key', value: 'word'}, 
+        ]), 
+      )).to.be.rejectedWith(/common\.NoPermission/);
+    });
+  });
+
+  it('Forbids mutate token property with tokenOwher==true but signer have\'t all pieces', async () => {
+    await usingApi(async api => {
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setTokenPropertyPermissions(collection, [{key: 'key', permission: {mutable:true, tokenOwner: true}}]), 
+      )).to.not.be.rejected;
+  
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setTokenProperties(collection, token, [
+          {key: 'key', value: 'word'}, 
+        ]), 
+      )).to.be.not.rejected;
+
+      await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible');
+  
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setTokenProperties(collection, token, [
+          {key: 'key', value: 'bad word'}, 
+        ]), 
+      )).to.be.rejectedWith(/common\.NoPermission/);
+    });
+  });
+
+  it('Forbids delete token property with tokenOwher==true but signer have\'t all pieces', async () => {
+    await usingApi(async api => {
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.setTokenProperties(collection, token, [
+          {key: 'key', value: 'word'}, 
+        ]), 
+      )).to.be.not.rejected;
+
+      await transferExpectSuccess(collection, token, alice, charlie, 33, 'ReFungible');
+  
+      await expect(executeTransaction(
+        api, 
+        alice, 
+        api.tx.unique.deleteTokenProperties(collection, token, [
+          'key',
+        ]), 
+      )).to.be.rejectedWith(/common\.NoPermission/);
+    });
   });
 });
modifiedtests/src/refungible.test.tsdiffbeforeafterboth
--- a/tests/src/refungible.test.ts
+++ b/tests/src/refungible.test.ts
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {default as usingApi} from './substrate/substrate-api';
+import {default as usingApi, submitTransactionAsync} from './substrate/substrate-api';
 import {IKeyringPair} from '@polkadot/types/types';
 import {
   createCollectionExpectSuccess,
@@ -30,6 +30,12 @@
   transfer,
   burnItem,
   repartitionRFT,
+  createCollectionWithPropsExpectSuccess,
+  getDetailedCollectionInfo,
+  normalizeAccountId,
+  CrossAccountId,
+  getCreateItemsResult,
+  getDestroyItemsResult,
 } from './util/helpers';
 
 import chai from 'chai';
@@ -51,14 +57,14 @@
   it('Create refungible collection and token', async () => {
     await usingApi(async api => {
       const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}});
-      expect(createCollectionResult.success).to.be.true;    
+      expect(createCollectionResult.success).to.be.true;
       const collectionId  = createCollectionResult.collectionId;
-
+      
       const itemCountBefore = await getLastTokenId(api, collectionId);
       const result = await createRefungibleToken(api, alice, collectionId, 100n);
-
+      
       const itemCountAfter = await getLastTokenId(api, collectionId);
-
+      
       // What to expect
       // tslint:disable-next-line:no-unused-expression
       expect(result.success).to.be.true;
@@ -67,7 +73,43 @@
       expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());
     });
   });
-
+  
+  it('RPC method tokenOnewrs for refungible collection and token', async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
+      
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}});
+      const collectionId = createCollectionResult.collectionId;
+      
+      const result = await createRefungibleToken(api, alice, collectionId, 10_000n);
+      const aliceTokenId = result.itemId;
+      
+      
+      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
+      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
+      
+      for (let i = 0; i < 7; i++) {
+        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 50*(i+1));
+      } 
+      
+      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
+      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
+      
+      const aliceID = normalizeAccountId(alice);
+      const bobId = normalizeAccountId(bob);
+      
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
+      expect(owners.length).to.be.equal(10);
+      
+      const eleven = privateKeyWrapper('11');
+      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
+      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+    });
+  });
+  
   it('Transfer token pieces', async () => {
     await usingApi(async api => {
       const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
@@ -186,4 +228,67 @@
       await expect(transfer(api, collectionId, tokenId, bob, alice, 160n)).to.eventually.be.rejected;
     });
   });
+
+  it('Repartition with increased amount', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+
+      const tx = api.tx.unique.repartition(collectionId, tokenId, 200n);
+      const events = await submitTransactionAsync(alice, tx);
+      const substrateEvents = getCreateItemsResult(events);
+      expect(substrateEvents).to.include.deep.members([
+        {
+          success: true,
+          collectionId,
+          itemId: tokenId,
+          recipient: {Substrate: alice.address},
+          amount: 100,
+        },
+      ]);
+    });
+  });
+
+  it('Repartition with decreased amount', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+      const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+
+      const tx = api.tx.unique.repartition(collectionId, tokenId, 50n);
+      const events = await submitTransactionAsync(alice, tx);
+      const substrateEvents = getDestroyItemsResult(events);
+      expect(substrateEvents).to.include.deep.members([
+        {
+          success: true,
+          collectionId,
+          itemId: tokenId,
+          owner: {Substrate: alice.address},
+          amount: 50,
+        },
+      ]);
+    });
+  });
+});
+
+describe('Test Refungible properties:', () => {
+  before(async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      alice = privateKeyWrapper('//Alice');
+      bob = privateKeyWrapper('//Bob');
+    });
+  });
+  
+  it('Сreate new collection with properties', async () => {
+    await usingApi(async api => {
+      const properties = [{key: 'key1', value: 'val1'}];
+      const propertyPermissions = [{key: 'key1', permission: {tokenOwner: true, mutable: false, collectionAdmin: true}}];
+      const collectionId = await createCollectionWithPropsExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'ReFungible'},
+        properties: properties,
+        propPerm: propertyPermissions, 
+      });
+      const collection = (await getDetailedCollectionInfo(api, collectionId))!;
+      expect(collection.properties.toHuman()).to.be.deep.equal(properties);
+      expect(collection.tokenPropertyPermissions.toHuman()).to.be.deep.equal(propertyPermissions);
+    });
+  });
 });
modifiedtests/src/rpc.test.tsdiffbeforeafterboth
--- a/tests/src/rpc.test.ts
+++ b/tests/src/rpc.test.ts
@@ -1,12 +1,57 @@
+import {IKeyringPair} from '@polkadot/types/types';
 import {expect} from 'chai';
 import usingApi from './substrate/substrate-api';
-import {createCollectionExpectSuccess, getTokenOwner} from './util/helpers';
+import {createCollection, createCollectionExpectSuccess, createFungibleItemExpectSuccess, CrossAccountId, getTokenOwner, normalizeAccountId, transfer, U128_MAX} from './util/helpers';
+
+let alice: IKeyringPair;
+let bob: IKeyringPair;
 
-describe('getTokenOwner', () => {
+
+describe('integration test: RPC methods', () => {
+  before(async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      alice = privateKeyWrapper('//Alice');
+      bob = privateKeyWrapper('//Bob');
+    });
+  });
+
+  
   it('returns None for fungible collection', async () => {
     await usingApi(async api => {
       const collection = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
       await expect(getTokenOwner(api, collection, 0)).to.be.rejectedWith(/^owner == null$/);
     });
   });
+  
+  it('RPC method tokenOwners for fungible collection and token', async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
+      
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
+      const collectionId = createCollectionResult.collectionId;
+      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
+     
+      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
+      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
+            
+      for (let i = 0; i < 7; i++) {
+        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 1);
+      } 
+      
+      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
+      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
+      const aliceID = normalizeAccountId(alice);
+      const bobId = normalizeAccountId(bob);
+
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
+      expect(owners.length == 10).to.be.true;
+      
+      const eleven = privateKeyWrapper('11');
+      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
+      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+    });
+  });
 });
\ No newline at end of file
modifiedtests/src/util/helpers.tsdiffbeforeafterboth
before · tests/src/util/helpers.ts
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/>.1617import '../interfaces/augment-api-rpc';18import '../interfaces/augment-api-query';19import {ApiPromise} from '@polkadot/api';20import type {AccountId, EventRecord, Event} from '@polkadot/types/interfaces';21import type {GenericEventData} from '@polkadot/types';22import {AnyTuple, IEvent, IKeyringPair} from '@polkadot/types/types';23import {evmToAddress} from '@polkadot/util-crypto';24import BN from 'bn.js';25import chai from 'chai';26import chaiAsPromised from 'chai-as-promised';27import {default as usingApi, executeTransaction, submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api';28import {hexToStr, strToUTF16, utf16ToStr} from './util';29import {UpDataStructsRpcCollection, UpDataStructsCreateItemData, UpDataStructsProperty} from '@polkadot/types/lookup';30import {UpDataStructsTokenChild} from '../interfaces';3132chai.use(chaiAsPromised);33const expect = chai.expect;3435export type CrossAccountId = {36  Substrate: string,37} | {38  Ethereum: string,39};4041export function normalizeAccountId(input: string | AccountId | CrossAccountId | IKeyringPair): CrossAccountId {42  if (typeof input === 'string') {43    if (input.length >= 47) {44      return {Substrate: input};45    } else if (input.length === 42 && input.startsWith('0x')) {46      return {Ethereum: input.toLowerCase()};47    } else if (input.length === 40 && !input.startsWith('0x')) {48      return {Ethereum: '0x' + input.toLowerCase()};49    } else {50      throw new Error(`Unknown address format: "${input}"`);51    }52  }53  if ('address' in input) {54    return {Substrate: input.address};55  }56  if ('Ethereum' in input) {57    return {58      Ethereum: input.Ethereum.toLowerCase(),59    };60  } else if ('ethereum' in input) {61    return {62      Ethereum: (input as any).ethereum.toLowerCase(),63    };64  } else if ('Substrate' in input) {65    return input;66  } else if ('substrate' in input) {67    return {68      Substrate: (input as any).substrate,69    };70  }7172  // AccountId73  return {Substrate: input.toString()};74}75export function toSubstrateAddress(input: string | CrossAccountId | IKeyringPair): string {76  input = normalizeAccountId(input);77  if ('Substrate' in input) {78    return input.Substrate;79  } else {80    return evmToAddress(input.Ethereum);81  }82}8384export const U128_MAX = (1n << 128n) - 1n;8586const MICROUNIQUE = 1_000_000_000_000n;87const MILLIUNIQUE = 1_000n * MICROUNIQUE;88const CENTIUNIQUE = 10n * MILLIUNIQUE;89export const UNIQUE = 100n * CENTIUNIQUE;9091interface GenericResult<T> {92  success: boolean;93  data: T | null;94}9596interface CreateCollectionResult {97  success: boolean;98  collectionId: number;99}100101interface CreateItemResult {102  success: boolean;103  collectionId: number;104  itemId: number;105  recipient?: CrossAccountId;106}107108interface TransferResult {109  collectionId: number;110  itemId: number;111  sender?: CrossAccountId;112  recipient?: CrossAccountId;113  value: bigint;114}115116interface IReFungibleOwner {117  fraction: BN;118  owner: number[];119}120121interface IGetMessage {122  checkMsgUnqMethod: string;123  checkMsgTrsMethod: string;124  checkMsgSysMethod: string;125}126127export interface IFungibleTokenDataType {128  value: number;129}130131export interface IChainLimits {132  collectionNumbersLimit: number;133  accountTokenOwnershipLimit: number;134  collectionsAdminsLimit: number;135  customDataLimit: number;136  nftSponsorTransferTimeout: number;137  fungibleSponsorTransferTimeout: number;138  refungibleSponsorTransferTimeout: number;139  //offchainSchemaLimit: number;140  //constOnChainSchemaLimit: number;141}142143export interface IReFungibleTokenDataType {144  owner: IReFungibleOwner[];145}146147export function uniqueEventMessage(events: EventRecord[]): IGetMessage {148  let checkMsgUnqMethod = '';149  let checkMsgTrsMethod = '';150  let checkMsgSysMethod = '';151  events.forEach(({event: {method, section}}) => {152    if (section === 'common') {153      checkMsgUnqMethod = method;154    } else if (section === 'treasury') {155      checkMsgTrsMethod = method;156    } else if (section === 'system') {157      checkMsgSysMethod = method;158    } else { return null; }159  });160  const result: IGetMessage = {161    checkMsgUnqMethod,162    checkMsgTrsMethod,163    checkMsgSysMethod,164  };165  return result;166}167168export function getEvent<T extends Event>(events: EventRecord[], check: (event: IEvent<AnyTuple>) => event is T): T | undefined {169  const event = events.find(r => check(r.event));170  if (!event) return;171  return event.event as T;172}173174export function getGenericResult<T>(events: EventRecord[]): GenericResult<T>;175export function getGenericResult<T>(176  events: EventRecord[],177  expectSection: string,178  expectMethod: string,179  extractAction: (data: GenericEventData) => T180): GenericResult<T>;181182export function getGenericResult<T>(183  events: EventRecord[],184  expectSection?: string,185  expectMethod?: string,186  extractAction?: (data: GenericEventData) => T,187): GenericResult<T> {188  let success = false;189  let successData = null;190191  events.forEach(({event: {data, method, section}}) => {192    // console.log(`    ${phase}: ${section}.${method}:: ${data}`);193    if (method === 'ExtrinsicSuccess') {194      success = true;195    } else if ((expectSection == section) && (expectMethod == method)) {196      successData = extractAction!(data as any);197    }198  });199200  const result: GenericResult<T> = {201    success,202    data: successData,203  };204  return result;205}206207export function getCreateCollectionResult(events: EventRecord[]): CreateCollectionResult {208  const genericResult = getGenericResult(events, 'common', 'CollectionCreated', (data) => parseInt(data[0].toString(), 10));209  const result: CreateCollectionResult = {210    success: genericResult.success,211    collectionId: genericResult.data ?? 0,212  };213  return result;214}215216export function getCreateItemsResult(events: EventRecord[]): CreateItemResult[] {217  const results: CreateItemResult[] = [];218  219  const genericResult = getGenericResult<CreateItemResult[]>(events, 'common', 'ItemCreated', (data) => {220    const collectionId = parseInt(data[0].toString(), 10);221    const itemId = parseInt(data[1].toString(), 10);222    const recipient = normalizeAccountId(data[2].toJSON() as any);223224    const itemRes: CreateItemResult = {225      success: true,226      collectionId,227      itemId,228      recipient,229    };230231    results.push(itemRes);232    return results;233  });234235  if (!genericResult.success) return [];236  return results;237}238239export function getCreateItemResult(events: EventRecord[]): CreateItemResult {240  const genericResult = getGenericResult<[number, number, CrossAccountId?]>(events, 'common', 'ItemCreated', (data) => [241    parseInt(data[0].toString(), 10),242    parseInt(data[1].toString(), 10),243    normalizeAccountId(data[2].toJSON() as any),244  ]);245246  if (genericResult.data == null) genericResult.data = [0, 0];247248  const result: CreateItemResult = {249    success: genericResult.success,250    collectionId: genericResult.data[0],251    itemId: genericResult.data[1],252    recipient: genericResult.data![2],253  };254  255  return result;256}257258export function getTransferResult(api: ApiPromise, events: EventRecord[]): TransferResult {259  for (const {event} of events) {260    if (api.events.common.Transfer.is(event)) {261      const [collection, token, sender, recipient, value] = event.data;262      return {263        collectionId: collection.toNumber(),264        itemId: token.toNumber(),265        sender: normalizeAccountId(sender.toJSON() as any),266        recipient: normalizeAccountId(recipient.toJSON() as any),267        value: value.toBigInt(),268      };269    }270  }271  throw new Error('no transfer event');272}273274interface Nft {275  type: 'NFT';276}277278interface Fungible {279  type: 'Fungible';280  decimalPoints: number;281}282283interface ReFungible {284  type: 'ReFungible';285}286287type CollectionMode = Nft | Fungible | ReFungible;288289export type Property = {290  key: any,291  value: any,292};293294type Permission = {295  mutable: boolean;296  collectionAdmin: boolean;297  tokenOwner: boolean;298}299300type PropertyPermission = {301  key: any;302  permission: Permission;303}304305export type CreateCollectionParams = {306  mode: CollectionMode,307  name: string,308  description: string,309  tokenPrefix: string,310  properties?: Array<Property>,311  propPerm?: Array<PropertyPermission>312};313314const defaultCreateCollectionParams: CreateCollectionParams = {315  description: 'description',316  mode: {type: 'NFT'},317  name: 'name',318  tokenPrefix: 'prefix',319};320321export async function322createCollection(323  api: ApiPromise,324  sender: IKeyringPair,325  params: Partial<CreateCollectionParams> = {},326): Promise<CreateCollectionResult> {327  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};328329  let modeprm = {};330  if (mode.type === 'NFT') {331    modeprm = {nft: null};332  } else if (mode.type === 'Fungible') {333    modeprm = {fungible: mode.decimalPoints};334  } else if (mode.type === 'ReFungible') {335    modeprm = {refungible: null};336  }337338  const tx = api.tx.unique.createCollectionEx({339    name: strToUTF16(name),340    description: strToUTF16(description),341    tokenPrefix: strToUTF16(tokenPrefix),342    mode: modeprm as any,343  });344  const events = await submitTransactionAsync(sender, tx);345  return getCreateCollectionResult(events);346}347348export async function createCollectionExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {349  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};350351  let collectionId = 0;352  await usingApi(async (api, privateKeyWrapper) => {353    // Get number of collections before the transaction354    const collectionCountBefore = await getCreatedCollectionCount(api);355356    // Run the CreateCollection transaction357    const alicePrivateKey = privateKeyWrapper('//Alice');358359    const result = await createCollection(api, alicePrivateKey, params);360361    // Get number of collections after the transaction362    const collectionCountAfter = await getCreatedCollectionCount(api);363364    // Get the collection365    const collection = await queryCollectionExpectSuccess(api, result.collectionId);366367    // What to expect368    // tslint:disable-next-line:no-unused-expression369    expect(result.success).to.be.true;370    expect(result.collectionId).to.be.equal(collectionCountAfter);371    // tslint:disable-next-line:no-unused-expression372    expect(collection).to.be.not.null;373    expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.');374    expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey));375    expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name);376    expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description);377    expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix);378379    collectionId = result.collectionId;380  });381382  return collectionId;383}384385export async function createCollectionWithPropsExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {386  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};387388  let collectionId = 0;389  await usingApi(async (api, privateKeyWrapper) => {390    // Get number of collections before the transaction391    const collectionCountBefore = await getCreatedCollectionCount(api);392393    // Run the CreateCollection transaction394    const alicePrivateKey = privateKeyWrapper('//Alice');395396    let modeprm = {};397    if (mode.type === 'NFT') {398      modeprm = {nft: null};399    } else if (mode.type === 'Fungible') {400      modeprm = {fungible: mode.decimalPoints};401    } else if (mode.type === 'ReFungible') {402      modeprm = {refungible: null};403    }404405    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm});406    const events = await submitTransactionAsync(alicePrivateKey, tx);407    const result = getCreateCollectionResult(events);408409    // Get number of collections after the transaction410    const collectionCountAfter = await getCreatedCollectionCount(api);411412    // Get the collection413    const collection = await queryCollectionExpectSuccess(api, result.collectionId);414415    // What to expect416    // tslint:disable-next-line:no-unused-expression417    expect(result.success).to.be.true;418    expect(result.collectionId).to.be.equal(collectionCountAfter);419    // tslint:disable-next-line:no-unused-expression420    expect(collection).to.be.not.null;421    expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.');422    expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey));423    expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name);424    expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description);425    expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix);426427428    collectionId = result.collectionId;429  });430431  return collectionId;432}433434export async function createCollectionWithPropsExpectFailure(params: Partial<CreateCollectionParams> = {}) {435  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};436437  await usingApi(async (api, privateKeyWrapper) => {438    // Get number of collections before the transaction439    const collectionCountBefore = await getCreatedCollectionCount(api);440441    // Run the CreateCollection transaction442    const alicePrivateKey = privateKeyWrapper('//Alice');443444    let modeprm = {};445    if (mode.type === 'NFT') {446      modeprm = {nft: null};447    } else if (mode.type === 'Fungible') {448      modeprm = {fungible: mode.decimalPoints};449    } else if (mode.type === 'ReFungible') {450      modeprm = {refungible: null};451    }452453    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm});454    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;455456457    // Get number of collections after the transaction458    const collectionCountAfter = await getCreatedCollectionCount(api);459460    expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.');461  });462}463464export async function createCollectionExpectFailure(params: Partial<CreateCollectionParams> = {}) {465  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};466467  let modeprm = {};468  if (mode.type === 'NFT') {469    modeprm = {nft: null};470  } else if (mode.type === 'Fungible') {471    modeprm = {fungible: mode.decimalPoints};472  } else if (mode.type === 'ReFungible') {473    modeprm = {refungible: null};474  }475476  await usingApi(async (api, privateKeyWrapper) => {477    // Get number of collections before the transaction478    const collectionCountBefore = await getCreatedCollectionCount(api);479480    // Run the CreateCollection transaction481    const alicePrivateKey = privateKeyWrapper('//Alice');482    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any});483    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;484485    // Get number of collections after the transaction486    const collectionCountAfter = await getCreatedCollectionCount(api);487488    // What to expect489    expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.');490  });491}492493export async function findUnusedAddress(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, seedAddition = ''): Promise<IKeyringPair> {494  let bal = 0n;495  let unused;496  do {497    const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)) + seedAddition;498    unused = privateKeyWrapper(`//${randomSeed}`);499    bal = (await api.query.system.account(unused.address)).data.free.toBigInt();500  } while (bal !== 0n);501  return unused;502}503504export async function getAllowance(api: ApiPromise, collectionId: number, owner: CrossAccountId | string | IKeyringPair, approved: CrossAccountId | string | IKeyringPair, tokenId: number) {505  return (await api.rpc.unique.allowance(collectionId, normalizeAccountId(owner), normalizeAccountId(approved), tokenId)).toBigInt();506}507508export function findUnusedAddresses(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, amount: number): Promise<IKeyringPair[]> {509  return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, privateKeyWrapper, '_' + Date.now())));510}511512export async function findNotExistingCollection(api: ApiPromise): Promise<number> {513  const totalNumber = await getCreatedCollectionCount(api);514  const newCollection: number = totalNumber + 1;515  return newCollection;516}517518function getDestroyResult(events: EventRecord[]): boolean {519  let success = false;520  events.forEach(({event: {method}}) => {521    if (method == 'ExtrinsicSuccess') {522      success = true;523    }524  });525  return success;526}527528export async function destroyCollectionExpectFailure(collectionId: number, senderSeed = '//Alice') {529  await usingApi(async (api, privateKeyWrapper) => {530    // Run the DestroyCollection transaction531    const alicePrivateKey = privateKeyWrapper(senderSeed);532    const tx = api.tx.unique.destroyCollection(collectionId);533    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;534  });535}536537export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed = '//Alice') {538  await usingApi(async (api, privateKeyWrapper) => {539    // Run the DestroyCollection transaction540    const alicePrivateKey = privateKeyWrapper(senderSeed);541    const tx = api.tx.unique.destroyCollection(collectionId);542    const events = await submitTransactionAsync(alicePrivateKey, tx);543    const result = getDestroyResult(events);544    expect(result).to.be.true;545546    // What to expect547    expect(await getDetailedCollectionInfo(api, collectionId)).to.be.null;548  });549}550551export async function setCollectionLimitsExpectSuccess(sender: IKeyringPair, collectionId: number, limits: any) {552  await usingApi(async (api) => {553    const tx = api.tx.unique.setCollectionLimits(collectionId, limits);554    const events = await submitTransactionAsync(sender, tx);555    const result = getGenericResult(events);556557    expect(result.success).to.be.true;558  });559}560561export const setCollectionPermissionsExpectSuccess = async (sender: IKeyringPair, collectionId: number, permissions: any) => {562  await usingApi(async(api) => {563    const tx = api.tx.unique.setCollectionPermissions(collectionId, permissions);564    const events = await submitTransactionAsync(sender, tx);565    const result = getGenericResult(events);566567    expect(result.success).to.be.true;568  });569};570571export async function setCollectionLimitsExpectFailure(sender: IKeyringPair, collectionId: number, limits: any) {572  await usingApi(async (api) => {573    const tx = api.tx.unique.setCollectionLimits(collectionId, limits);574    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;575    const result = getGenericResult(events);576577    expect(result.success).to.be.false;578  });579}580581export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string, sender = '//Alice') {582  await usingApi(async (api, privateKeyWrapper) => {583584    // Run the transaction585    const senderPrivateKey = privateKeyWrapper(sender);586    const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor);587    const events = await submitTransactionAsync(senderPrivateKey, tx);588    const result = getGenericResult(events);589590    // Get the collection591    const collection = await queryCollectionExpectSuccess(api, collectionId);592593    // What to expect594    expect(result.success).to.be.true;595    expect(collection.sponsorship.toJSON()).to.deep.equal({596      unconfirmed: sponsor,597    });598  });599}600601export async function removeCollectionSponsorExpectSuccess(collectionId: number, sender = '//Alice') {602  await usingApi(async (api, privateKeyWrapper) => {603604    // Run the transaction605    const alicePrivateKey = privateKeyWrapper(sender);606    const tx = api.tx.unique.removeCollectionSponsor(collectionId);607    const events = await submitTransactionAsync(alicePrivateKey, tx);608    const result = getGenericResult(events);609610    // Get the collection611    const collection = await queryCollectionExpectSuccess(api, collectionId);612613    // What to expect614    expect(result.success).to.be.true;615    expect(collection.sponsorship.toJSON()).to.be.deep.equal({disabled: null});616  });617}618619export async function removeCollectionSponsorExpectFailure(collectionId: number, senderSeed = '//Alice') {620  await usingApi(async (api, privateKeyWrapper) => {621622    // Run the transaction623    const alicePrivateKey = privateKeyWrapper(senderSeed);624    const tx = api.tx.unique.removeCollectionSponsor(collectionId);625    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;626  });627}628629export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed = '//Alice') {630  await usingApi(async (api, privateKeyWrapper) => {631632    // Run the transaction633    const alicePrivateKey = privateKeyWrapper(senderSeed);634    const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor);635    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;636  });637}638639export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed = '//Alice') {640  await usingApi(async (api, privateKeyWrapper) => {641642    // Run the transaction643    const sender = privateKeyWrapper(senderSeed);644    await confirmSponsorshipByKeyExpectSuccess(collectionId, sender);645  });646}647648export async function confirmSponsorshipByKeyExpectSuccess(collectionId: number, sender: IKeyringPair) {649  await usingApi(async (api, privateKeyWrapper) => {650651    // Run the transaction652    const tx = api.tx.unique.confirmSponsorship(collectionId);653    const events = await submitTransactionAsync(sender, tx);654    const result = getGenericResult(events);655656    // Get the collection657    const collection = await queryCollectionExpectSuccess(api, collectionId);658659    // What to expect660    expect(result.success).to.be.true;661    expect(collection.sponsorship.toJSON()).to.be.deep.equal({662      confirmed: sender.address,663    });664  });665}666667668export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed = '//Alice') {669  await usingApi(async (api, privateKeyWrapper) => {670671    // Run the transaction672    const sender = privateKeyWrapper(senderSeed);673    const tx = api.tx.unique.confirmSponsorship(collectionId);674    await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;675  });676}677678export async function enableContractSponsoringExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {679  await usingApi(async (api) => {680    const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable);681    const events = await submitTransactionAsync(sender, tx);682    const result = getGenericResult(events);683684    expect(result.success).to.be.true;685  });686}687688export async function enableContractSponsoringExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {689  await usingApi(async (api) => {690    const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable);691    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;692    const result = getGenericResult(events);693694    expect(result.success).to.be.false;695  });696}697698export async function setTransferFlagExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) {699700  await usingApi(async (api) => {701702    const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled);703    const events = await submitTransactionAsync(sender, tx);704    const result = getGenericResult(events);705706    expect(result.success).to.be.true;707  });708}709710export async function setTransferFlagExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) {711712  await usingApi(async (api) => {713714    const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled);715    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;716    const result = getGenericResult(events);717718    expect(result.success).to.be.false;719  });720}721722export async function setContractSponsoringRateLimitExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {723  await usingApi(async (api) => {724    const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit);725    const events = await submitTransactionAsync(sender, tx);726    const result = getGenericResult(events);727728    expect(result.success).to.be.true;729  });730}731732export async function setContractSponsoringRateLimitExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {733  await usingApi(async (api) => {734    const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit);735    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;736    const result = getGenericResult(events);737738    expect(result.success).to.be.false;739  });740}741742export async function getNextSponsored(743  api: ApiPromise,744  collectionId: number,745  account: string | CrossAccountId,746  tokenId: number,747): Promise<number> {748  return Number((await api.rpc.unique.nextSponsored(collectionId, account, tokenId)).unwrapOr(-1));749}750751export async function toggleContractAllowlistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, value = true) {752  await usingApi(async (api) => {753    const tx = api.tx.unique.toggleContractAllowList(contractAddress, value);754    const events = await submitTransactionAsync(sender, tx);755    const result = getGenericResult(events);756757    expect(result.success).to.be.true;758  });759}760761export async function isAllowlistedInContract(contractAddress: AccountId | string, user: string) {762  let allowlisted = false;763  await usingApi(async (api) => {764    allowlisted = (await api.query.unique.contractAllowList(contractAddress, user)).toJSON() as boolean;765  });766  return allowlisted;767}768769export async function addToContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {770  await usingApi(async (api) => {771    const tx = api.tx.unique.addToContractAllowList(contractAddress.toString(), user.toString());772    const events = await submitTransactionAsync(sender, tx);773    const result = getGenericResult(events);774775    expect(result.success).to.be.true;776  });777}778779export async function removeFromContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {780  await usingApi(async (api) => {781    const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString());782    const events = await submitTransactionAsync(sender, tx);783    const result = getGenericResult(events);784785    expect(result.success).to.be.true;786  });787}788789export async function removeFromContractAllowListExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {790  await usingApi(async (api) => {791    const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString());792    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;793    const result = getGenericResult(events);794795    expect(result.success).to.be.false;796  });797}798799export interface CreateFungibleData {800  readonly Value: bigint;801}802803export interface CreateReFungibleData { }804export interface CreateNftData { }805806export type CreateItemData = {807  NFT: CreateNftData;808} | {809  Fungible: CreateFungibleData;810} | {811  ReFungible: CreateReFungibleData;812};813814export async function burnItem(api: ApiPromise, sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint) : Promise<boolean> {815  const tx = api.tx.unique.burnItem(collectionId, tokenId, value);816  const events = await submitTransactionAsync(sender, tx);817  return getGenericResult(events).success;818}819820export async function burnItemExpectSuccess(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) {821  await usingApi(async (api) => {822    const balanceBefore = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId);823    // if burning token by admin - use adminButnItemExpectSuccess824    expect(balanceBefore >= BigInt(value)).to.be.true;825826    expect(await burnItem(api, sender, collectionId, tokenId, value)).to.be.true;827828    const balanceAfter = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId);829    expect(balanceAfter + BigInt(value)).to.be.equal(balanceBefore);830  });831}832833export async function burnItemExpectFailure(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) {834  await usingApi(async (api) => {835    const tx = api.tx.unique.burnItem(collectionId, tokenId, value);836837    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;838    const result = getCreateCollectionResult(events);839    // tslint:disable-next-line:no-unused-expression840    expect(result.success).to.be.false;841  });842}843844export async function burnFromExpectSuccess(sender: IKeyringPair, from: IKeyringPair | CrossAccountId, collectionId: number, tokenId: number, value: number | bigint = 1) {845  await usingApi(async (api) => {846    const tx = api.tx.unique.burnFrom(collectionId, normalizeAccountId(from), tokenId, value);847    const events = await submitTransactionAsync(sender, tx);848    return getGenericResult(events).success;849  });850}851852export async function853approve(854  api: ApiPromise,855  collectionId: number,856  tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string | IKeyringPair, amount: number | bigint,857) {858  const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);859  const events = await submitTransactionAsync(owner, approveUniqueTx);860  return getGenericResult(events).success;861}862863export async function864approveExpectSuccess(865  collectionId: number,866  tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string, amount: number | bigint = 1,867) {868  await usingApi(async (api: ApiPromise) => {869    const result = await approve(api, collectionId, tokenId, owner, approved, amount);870    expect(result).to.be.true;871872    expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount));873  });874}875876export async function adminApproveFromExpectSuccess(877  collectionId: number,878  tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1,879) {880  await usingApi(async (api: ApiPromise) => {881    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);882    const events = await submitTransactionAsync(admin, approveUniqueTx);883    const result = getGenericResult(events);884    expect(result.success).to.be.true;885886    expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount));887  });888}889890export async function891transferFrom(892  api: ApiPromise,893  collectionId: number,894  tokenId: number,895  accountApproved: IKeyringPair,896  accountFrom: IKeyringPair | CrossAccountId,897  accountTo: IKeyringPair | CrossAccountId,898  value: number | bigint,899) {900  const from = normalizeAccountId(accountFrom);901  const to = normalizeAccountId(accountTo);902  const transferFromTx = api.tx.unique.transferFrom(from, to, collectionId, tokenId, value);903  const events = await submitTransactionAsync(accountApproved, transferFromTx);904  return getGenericResult(events).success;905}906907export async function908transferFromExpectSuccess(909  collectionId: number,910  tokenId: number,911  accountApproved: IKeyringPair,912  accountFrom: IKeyringPair | CrossAccountId,913  accountTo: IKeyringPair | CrossAccountId,914  value: number | bigint = 1,915  type = 'NFT',916) {917  await usingApi(async (api: ApiPromise) => {918    const from = normalizeAccountId(accountFrom);919    const to = normalizeAccountId(accountTo);920    let balanceBefore = 0n;921    if (type === 'Fungible' || type === 'ReFungible') {922      balanceBefore = await getBalance(api, collectionId, to, tokenId);923    }924    expect(await transferFrom(api, collectionId, tokenId, accountApproved, accountFrom, accountTo, value)).to.be.true;925    if (type === 'NFT') {926      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to);927    }928    if (type === 'Fungible') {929      const balanceAfter = await getBalance(api, collectionId, to, tokenId);930      if (JSON.stringify(to) !== JSON.stringify(from)) {931        expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value));932      } else {933        expect(balanceAfter).to.be.equal(balanceBefore);934      }935    }936    if (type === 'ReFungible') {937      expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(balanceBefore + BigInt(value));938    }939  });940}941942export async function943transferFromExpectFail(944  collectionId: number,945  tokenId: number,946  accountApproved: IKeyringPair,947  accountFrom: IKeyringPair,948  accountTo: IKeyringPair,949  value: number | bigint = 1,950) {951  await usingApi(async (api: ApiPromise) => {952    const transferFromTx = api.tx.unique.transferFrom(normalizeAccountId(accountFrom.address), normalizeAccountId(accountTo.address), collectionId, tokenId, value);953    const events = await expect(submitTransactionExpectFailAsync(accountApproved, transferFromTx)).to.be.rejected;954    const result = getCreateCollectionResult(events);955    // tslint:disable-next-line:no-unused-expression956    expect(result.success).to.be.false;957  });958}959960/* eslint no-async-promise-executor: "off" */961export async function getBlockNumber(api: ApiPromise): Promise<number> {962  return new Promise<number>(async (resolve) => {963    const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => {964      unsubscribe();965      resolve(head.number.toNumber());966    });967  });968}969970export async function addCollectionAdminExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | CrossAccountId) {971  await usingApi(async (api) => {972    const changeAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(address));973    const events = await submitTransactionAsync(sender, changeAdminTx);974    const result = getCreateCollectionResult(events);975    expect(result.success).to.be.true;976  });977}978979export async function adminApproveFromExpectFail(980  collectionId: number,981  tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1,982) {983  await usingApi(async (api: ApiPromise) => {984    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);985    const events = await expect(submitTransactionAsync(admin, approveUniqueTx)).to.be.rejected;986    const result = getGenericResult(events);987    expect(result.success).to.be.false;988  });989}990991export async function992getFreeBalance(account: IKeyringPair): Promise<bigint> {993  let balance = 0n;994  await usingApi(async (api) => {995    balance = BigInt((await api.query.system.account(account.address)).data.free.toString());996  });997998  return balance;999}10001001export async function transferBalanceTo(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {1002  const tx = api.tx.balances.transfer(target, amount);1003  const events = await submitTransactionAsync(source, tx);1004  const result = getGenericResult(events);1005  expect(result.success).to.be.true;1006}10071008export async function1009scheduleExpectSuccess(1010  operationTx: any,1011  sender: IKeyringPair,1012  blockSchedule: number,1013  scheduledId: string,1014  period = 1,1015  repetitions = 1,1016) {1017  await usingApi(async (api: ApiPromise) => {1018    const blockNumber: number | undefined = await getBlockNumber(api);1019    const expectedBlockNumber = blockNumber + blockSchedule;10201021    expect(blockNumber).to.be.greaterThan(0);1022    const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule1023      scheduledId,1024      expectedBlockNumber, 1025      repetitions > 1 ? [period, repetitions] : null, 1026      0, 1027      {Value: operationTx as any},1028    );10291030    const events = await submitTransactionAsync(sender, scheduleTx);1031    expect(getGenericResult(events).success).to.be.true;1032  });1033}10341035export async function1036scheduleExpectFailure(1037  operationTx: any,1038  sender: IKeyringPair,1039  blockSchedule: number,1040  scheduledId: string,1041  period = 1,1042  repetitions = 1,1043) {1044  await usingApi(async (api: ApiPromise) => {1045    const blockNumber: number | undefined = await getBlockNumber(api);1046    const expectedBlockNumber = blockNumber + blockSchedule;10471048    expect(blockNumber).to.be.greaterThan(0);1049    const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule1050      scheduledId,1051      expectedBlockNumber, 1052      repetitions <= 1 ? null : [period, repetitions], 1053      0, 1054      {Value: operationTx as any},1055    );10561057    //const events = 1058    await expect(submitTransactionExpectFailAsync(sender, scheduleTx)).to.be.rejected;1059    //expect(getGenericResult(events).success).to.be.false;1060  });1061}10621063export async function1064scheduleTransferAndWaitExpectSuccess(1065  collectionId: number,1066  tokenId: number,1067  sender: IKeyringPair,1068  recipient: IKeyringPair,1069  value: number | bigint = 1,1070  blockSchedule: number,1071  scheduledId: string,1072) {1073  await usingApi(async (api: ApiPromise) => {1074    await scheduleTransferExpectSuccess(collectionId, tokenId, sender, recipient, value, blockSchedule, scheduledId);10751076    const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt();10771078    // sleep for n + 1 blocks1079    await waitNewBlocks(blockSchedule + 1);10801081    const recipientBalanceAfter = (await api.query.system.account(recipient.address)).data.free.toBigInt();10821083    expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(recipient.address));1084    expect(recipientBalanceAfter).to.be.equal(recipientBalanceBefore);1085  });1086}10871088export async function1089scheduleTransferExpectSuccess(1090  collectionId: number,1091  tokenId: number,1092  sender: IKeyringPair,1093  recipient: IKeyringPair,1094  value: number | bigint = 1,1095  blockSchedule: number,1096  scheduledId: string,1097) {1098  await usingApi(async (api: ApiPromise) => {1099    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, value);11001101    await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId);11021103    expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address));1104  });1105}11061107export async function1108scheduleTransferFundsPeriodicExpectSuccess(1109  amount: bigint,1110  sender: IKeyringPair,1111  recipient: IKeyringPair,1112  blockSchedule: number,1113  scheduledId: string,1114  period: number,1115  repetitions: number,1116) {1117  await usingApi(async (api: ApiPromise) => {1118    const transferTx = api.tx.balances.transfer(recipient.address, amount);11191120    const balanceBefore = await getFreeBalance(recipient);1121    1122    await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions);11231124    expect(await getFreeBalance(recipient)).to.be.equal(balanceBefore);1125  });1126}11271128export async function1129transfer(1130  api: ApiPromise,1131  collectionId: number,1132  tokenId: number,1133  sender: IKeyringPair,1134  recipient: IKeyringPair | CrossAccountId,1135  value: number | bigint,1136) : Promise<boolean> {1137  const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);1138  const events = await executeTransaction(api, sender, transferTx);1139  return getGenericResult(events).success;1140}11411142export async function1143transferExpectSuccess(1144  collectionId: number,1145  tokenId: number,1146  sender: IKeyringPair,1147  recipient: IKeyringPair | CrossAccountId,1148  value: number | bigint = 1,1149  type = 'NFT',1150) {1151  await usingApi(async (api: ApiPromise) => {1152    const from = normalizeAccountId(sender);1153    const to = normalizeAccountId(recipient);11541155    let balanceBefore = 0n;1156    if (type === 'Fungible' || type === 'ReFungible') {1157      balanceBefore = await getBalance(api, collectionId, to, tokenId);1158    }11591160    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);1161    const events = await executeTransaction(api, sender, transferTx);1162    const result = getTransferResult(api, events);11631164    expect(result.collectionId).to.be.equal(collectionId);1165    expect(result.itemId).to.be.equal(tokenId);1166    expect(result.sender).to.be.deep.equal(normalizeAccountId(sender.address));1167    expect(result.recipient).to.be.deep.equal(to);1168    expect(result.value).to.be.equal(BigInt(value));11691170    if (type === 'NFT') {1171      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to);1172    }1173    if (type === 'Fungible' || type === 'ReFungible') {1174      const balanceAfter = await getBalance(api, collectionId, to, tokenId);1175      if (JSON.stringify(to) !== JSON.stringify(from)) {1176        expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value));1177      } else {1178        expect(balanceAfter).to.be.equal(balanceBefore);1179      }1180    }1181  });1182}11831184export async function1185transferExpectFailure(1186  collectionId: number,1187  tokenId: number,1188  sender: IKeyringPair,1189  recipient: IKeyringPair | CrossAccountId,1190  value: number | bigint = 1,1191) {1192  await usingApi(async (api: ApiPromise) => {1193    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);1194    const events = await expect(submitTransactionExpectFailAsync(sender, transferTx)).to.be.rejected;1195    const result = getGenericResult(events);1196    // if (events && Array.isArray(events)) {1197    //   const result = getCreateCollectionResult(events);1198    // tslint:disable-next-line:no-unused-expression1199    expect(result.success).to.be.false;1200    //}1201  });1202}12031204export async function1205approveExpectFail(1206  collectionId: number,1207  tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1,1208) {1209  await usingApi(async (api: ApiPromise) => {1210    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved.address), collectionId, tokenId, amount);1211    const events = await expect(submitTransactionExpectFailAsync(owner, approveUniqueTx)).to.be.rejected;1212    const result = getCreateCollectionResult(events);1213    // tslint:disable-next-line:no-unused-expression1214    expect(result.success).to.be.false;1215  });1216}12171218export async function getBalance(1219  api: ApiPromise,1220  collectionId: number,1221  owner: string | CrossAccountId | IKeyringPair,1222  token: number,1223): Promise<bigint> {1224  return (await api.rpc.unique.balance(collectionId, normalizeAccountId(owner), token)).toBigInt();1225}1226export async function getTokenOwner(1227  api: ApiPromise,1228  collectionId: number,1229  token: number,1230): Promise<CrossAccountId> {1231  const owner = (await api.rpc.unique.tokenOwner(collectionId, token)).toJSON() as any;1232  if (owner == null) throw new Error('owner == null');1233  return normalizeAccountId(owner);1234}1235export async function getTopmostTokenOwner(1236  api: ApiPromise,1237  collectionId: number,1238  token: number,1239): Promise<CrossAccountId> {1240  const owner = (await api.rpc.unique.topmostTokenOwner(collectionId, token)).toJSON() as any;1241  if (owner == null) throw new Error('owner == null');1242  return normalizeAccountId(owner);1243}1244export async function getTokenChildren(1245  api: ApiPromise,1246  collectionId: number,1247  tokenId: number,1248): Promise<UpDataStructsTokenChild[]> {1249  return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any;1250}1251export async function isTokenExists(1252  api: ApiPromise,1253  collectionId: number,1254  token: number,1255): Promise<boolean> {1256  return (await api.rpc.unique.tokenExists(collectionId, token)).toJSON();1257}1258export async function getLastTokenId(1259  api: ApiPromise,1260  collectionId: number,1261): Promise<number> {1262  return (await api.rpc.unique.lastTokenId(collectionId)).toJSON();1263}1264export async function getAdminList(1265  api: ApiPromise,1266  collectionId: number,1267): Promise<string[]> {1268  return (await api.rpc.unique.adminlist(collectionId)).toHuman() as any;1269}1270export async function getTokenProperties(1271  api: ApiPromise,1272  collectionId: number,1273  tokenId: number,1274  propertyKeys: string[],1275): Promise<UpDataStructsProperty[]> {1276  return (await api.rpc.unique.tokenProperties(collectionId, tokenId, propertyKeys)).toHuman() as any;1277}12781279export async function createFungibleItemExpectSuccess(1280  sender: IKeyringPair,1281  collectionId: number,1282  data: CreateFungibleData,1283  owner: CrossAccountId | string = sender.address,1284) {1285  return await usingApi(async (api) => {1286    const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), {Fungible: data});12871288    const events = await submitTransactionAsync(sender, tx);1289    const result = getCreateItemResult(events);12901291    expect(result.success).to.be.true;1292    return result.itemId;1293  });1294}12951296export async function createMultipleItemsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) {1297  await usingApi(async (api) => {1298    const to = normalizeAccountId(owner);1299    const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData);13001301    const events = await submitTransactionAsync(sender, tx);1302    expect(getGenericResult(events).success).to.be.true;1303  });1304}13051306export async function createMultipleItemsWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) {1307  await usingApi(async (api) => {1308    const to = normalizeAccountId(owner);1309    const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData);13101311    const events = await submitTransactionAsync(sender, tx);1312    const result = getCreateItemsResult(events);13131314    for (const res of result) {1315      expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty;1316    }1317  });1318}13191320export async function createMultipleItemsExWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any) {1321  await usingApi(async (api) => {1322    const tx = api.tx.unique.createMultipleItemsEx(collectionId, itemsData);13231324    const events = await submitTransactionAsync(sender, tx);1325    const result = getCreateItemsResult(events);13261327    for (const res of result) {1328      expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty;1329    }1330  });1331}13321333export async function createItemWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, props:  Array<Property>, owner: CrossAccountId | string = sender.address) {1334  let newItemId = 0;1335  await usingApi(async (api) => {1336    const to = normalizeAccountId(owner);1337    const itemCountBefore = await getLastTokenId(api, collectionId);1338    const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId);13391340    let tx;1341    if (createMode === 'Fungible') {1342      const createData = {fungible: {value: 10}};1343      tx = api.tx.unique.createItem(collectionId, to, createData as any);1344    } else if (createMode === 'ReFungible') {1345      const createData = {refungible: {pieces: 100}};1346      tx = api.tx.unique.createItem(collectionId, to, createData as any);1347    } else {1348      const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}});1349      tx = api.tx.unique.createItem(collectionId, to, data as UpDataStructsCreateItemData);1350    }13511352    const events = await submitTransactionAsync(sender, tx);1353    const result = getCreateItemResult(events);13541355    const itemCountAfter = await getLastTokenId(api, collectionId);1356    const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId);13571358    if (createMode === 'NFT') {1359      expect(await api.rpc.unique.tokenProperties(collectionId, result.itemId)).not.to.be.empty;1360    }13611362    // What to expect1363    // tslint:disable-next-line:no-unused-expression1364    expect(result.success).to.be.true;1365    if (createMode === 'Fungible') {1366      expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n);1367    } else {1368      expect(itemCountAfter).to.be.equal(itemCountBefore + 1);1369    }1370    expect(collectionId).to.be.equal(result.collectionId);1371    expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());1372    expect(to).to.be.deep.equal(result.recipient);1373    newItemId = result.itemId;1374  });1375  return newItemId;1376}13771378export async function createItemWithPropsExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, props: Array<Property>, owner: CrossAccountId | string = sender.address) {1379  await usingApi(async (api) => {13801381    let tx;1382    if (createMode === 'NFT') {1383      const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}}) as UpDataStructsCreateItemData;1384      tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), data);1385    } else {1386      tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode);1387    }138813891390    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1391    if(events.message && events.message.toString().indexOf('1002: Verification Error') > -1) return;1392    const result = getCreateItemResult(events);13931394    expect(result.success).to.be.false;1395  });1396}13971398export async function createItemExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) {1399  let newItemId = 0;1400  await usingApi(async (api) => {1401    const to = normalizeAccountId(owner);1402    const itemCountBefore = await getLastTokenId(api, collectionId);1403    const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId);14041405    let tx;1406    if (createMode === 'Fungible') {1407      const createData = {fungible: {value: 10}};1408      tx = api.tx.unique.createItem(collectionId, to, createData as any);1409    } else if (createMode === 'ReFungible') {1410      const createData = {refungible: {pieces: 100}};1411      tx = api.tx.unique.createItem(collectionId, to, createData as any);1412    } else {1413      const createData = {nft: {}};1414      tx = api.tx.unique.createItem(collectionId, to, createData as any);1415    }14161417    const events = await submitTransactionAsync(sender, tx);1418    const result = getCreateItemResult(events);14191420    const itemCountAfter = await getLastTokenId(api, collectionId);1421    const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId);14221423    // What to expect1424    // tslint:disable-next-line:no-unused-expression1425    expect(result.success).to.be.true;1426    if (createMode === 'Fungible') {1427      expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n);1428    } else {1429      expect(itemCountAfter).to.be.equal(itemCountBefore + 1);1430    }1431    expect(collectionId).to.be.equal(result.collectionId);1432    expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());1433    expect(to).to.be.deep.equal(result.recipient);1434    newItemId = result.itemId;1435  });1436  return newItemId;1437}14381439export async function createRefungibleToken(api: ApiPromise, sender: IKeyringPair, collectionId: number, amount: bigint, owner: CrossAccountId | IKeyringPair | string = sender.address) : Promise<CreateItemResult> {1440  const createData = {refungible: {pieces: amount}};1441  const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createData as any);14421443  const events = await submitTransactionAsync(sender, tx);1444  return  getCreateItemResult(events);1445}14461447export async function createItemExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) {1448  await usingApi(async (api) => {1449    const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode);14501451    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1452    const result = getCreateItemResult(events);14531454    expect(result.success).to.be.false;1455  });1456}14571458export async function setPublicAccessModeExpectSuccess(1459  sender: IKeyringPair, collectionId: number,1460  accessMode: 'Normal' | 'AllowList',1461) {1462  await usingApi(async (api) => {14631464    // Run the transaction1465    const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode});1466    const events = await submitTransactionAsync(sender, tx);1467    const result = getGenericResult(events);14681469    // Get the collection1470    const collection = await queryCollectionExpectSuccess(api, collectionId);14711472    // What to expect1473    // tslint:disable-next-line:no-unused-expression1474    expect(result.success).to.be.true;1475    expect(collection.permissions.access.toHuman()).to.be.equal(accessMode);1476  });1477}14781479export async function setPublicAccessModeExpectFail(1480  sender: IKeyringPair, collectionId: number,1481  accessMode: 'Normal' | 'AllowList',1482) {1483  await usingApi(async (api) => {14841485    // Run the transaction1486    const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode});1487    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1488    const result = getGenericResult(events);14891490    // What to expect1491    // tslint:disable-next-line:no-unused-expression1492    expect(result.success).to.be.false;1493  });1494}14951496export async function enableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) {1497  await setPublicAccessModeExpectSuccess(sender, collectionId, 'AllowList');1498}14991500export async function enableAllowListExpectFail(sender: IKeyringPair, collectionId: number) {1501  await setPublicAccessModeExpectFail(sender, collectionId, 'AllowList');1502}15031504export async function disableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) {1505  await setPublicAccessModeExpectSuccess(sender, collectionId, 'Normal');1506}15071508export async function setMintPermissionExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) {1509  await usingApi(async (api) => {15101511    // Run the transaction1512    const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled});1513    const events = await submitTransactionAsync(sender, tx);1514    const result = getGenericResult(events);1515    expect(result.success).to.be.true;15161517    // Get the collection1518    const collection = await queryCollectionExpectSuccess(api, collectionId);15191520    expect(collection.permissions.mintMode.toHuman()).to.be.equal(enabled);1521  });1522}15231524export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) {1525  await setMintPermissionExpectSuccess(sender, collectionId, true);1526}15271528export async function setMintPermissionExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) {1529  await usingApi(async (api) => {1530    // Run the transaction1531    const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled});1532    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1533    const result = getCreateCollectionResult(events);1534    // tslint:disable-next-line:no-unused-expression1535    expect(result.success).to.be.false;1536  });1537}15381539export async function setChainLimitsExpectFailure(sender: IKeyringPair, limits: IChainLimits) {1540  await usingApi(async (api) => {1541    // Run the transaction1542    const tx = api.tx.unique.setChainLimits(limits);1543    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1544    const result = getCreateCollectionResult(events);1545    // tslint:disable-next-line:no-unused-expression1546    expect(result.success).to.be.false;1547  });1548}15491550export async function isAllowlisted(api: ApiPromise, collectionId: number, address: string | CrossAccountId) {1551  return (await api.rpc.unique.allowed(collectionId, normalizeAccountId(address))).toJSON();1552}15531554export async function addToAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId | CrossAccountId) {1555  await usingApi(async (api) => {1556    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.false;15571558    // Run the transaction1559    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));1560    const events = await submitTransactionAsync(sender, tx);1561    const result = getGenericResult(events);1562    expect(result.success).to.be.true;15631564    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;1565  });1566}15671568export async function addToAllowListAgainExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId) {1569  await usingApi(async (api) => {15701571    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;15721573    // Run the transaction1574    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));1575    const events = await submitTransactionAsync(sender, tx);1576    const result = getGenericResult(events);1577    expect(result.success).to.be.true;15781579    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;1580  });1581}15821583export async function addToAllowListExpectFail(sender: IKeyringPair, collectionId: number, address: string | AccountId) {1584  await usingApi(async (api) => {15851586    // Run the transaction1587    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));1588    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1589    const result = getGenericResult(events);15901591    // What to expect1592    // tslint:disable-next-line:no-unused-expression1593    expect(result.success).to.be.false;1594  });1595}15961597export async function removeFromAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: CrossAccountId) {1598  await usingApi(async (api) => {1599    // Run the transaction1600    const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address));1601    const events = await submitTransactionAsync(sender, tx);1602    const result = getGenericResult(events);16031604    // What to expect1605    // tslint:disable-next-line:no-unused-expression1606    expect(result.success).to.be.true;1607  });1608}16091610export async function removeFromAllowListExpectFailure(sender: IKeyringPair, collectionId: number, address: CrossAccountId) {1611  await usingApi(async (api) => {1612    // Run the transaction1613    const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address));1614    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1615    const result = getGenericResult(events);16161617    // What to expect1618    // tslint:disable-next-line:no-unused-expression1619    expect(result.success).to.be.false;1620  });1621}16221623export const getDetailedCollectionInfo = async (api: ApiPromise, collectionId: number)1624  : Promise<UpDataStructsRpcCollection | null> => {1625  return (await api.rpc.unique.collectionById(collectionId)).unwrapOr(null);1626};16271628export const getCreatedCollectionCount = async (api: ApiPromise): Promise<number> => {1629  // set global object - collectionsCount1630  return (await api.rpc.unique.collectionStats()).created.toNumber();1631};16321633export async function queryCollectionExpectSuccess(api: ApiPromise, collectionId: number): Promise<UpDataStructsRpcCollection> {1634  return (await api.rpc.unique.collectionById(collectionId)).unwrap();1635}16361637export async function waitNewBlocks(blocksCount = 1): Promise<void> {1638  await usingApi(async (api) => {1639    const promise = new Promise<void>(async (resolve) => {1640      const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => {1641        if (blocksCount > 0) {1642          blocksCount--;1643        } else {1644          unsubscribe();1645          resolve();1646        }1647      });1648    });1649    return promise;1650  });1651}16521653export async function repartitionRFT(1654  api: ApiPromise,1655  collectionId: number,1656  sender: IKeyringPair,1657  tokenId: number,1658  amount: bigint,1659): Promise<boolean> {1660  const tx = api.tx.unique.repartition(collectionId, tokenId, amount);1661  const events = await submitTransactionAsync(sender, tx);1662  const result = getGenericResult(events);16631664  return result.success;1665}
after · tests/src/util/helpers.ts
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/>.1617import '../interfaces/augment-api-rpc';18import '../interfaces/augment-api-query';19import {ApiPromise} from '@polkadot/api';20import type {AccountId, EventRecord, Event} from '@polkadot/types/interfaces';21import type {GenericEventData} from '@polkadot/types';22import {AnyTuple, IEvent, IKeyringPair} from '@polkadot/types/types';23import {evmToAddress} from '@polkadot/util-crypto';24import BN from 'bn.js';25import chai from 'chai';26import chaiAsPromised from 'chai-as-promised';27import {default as usingApi, executeTransaction, submitTransactionAsync, submitTransactionExpectFailAsync} from '../substrate/substrate-api';28import {hexToStr, strToUTF16, utf16ToStr} from './util';29import {UpDataStructsRpcCollection, UpDataStructsCreateItemData, UpDataStructsProperty} from '@polkadot/types/lookup';30import {UpDataStructsTokenChild} from '../interfaces';3132chai.use(chaiAsPromised);33const expect = chai.expect;3435export type CrossAccountId = {36  Substrate: string,37} | {38  Ethereum: string,39};4041export function normalizeAccountId(input: string | AccountId | CrossAccountId | IKeyringPair): CrossAccountId {42  if (typeof input === 'string') {43    if (input.length >= 47) {44      return {Substrate: input};45    } else if (input.length === 42 && input.startsWith('0x')) {46      return {Ethereum: input.toLowerCase()};47    } else if (input.length === 40 && !input.startsWith('0x')) {48      return {Ethereum: '0x' + input.toLowerCase()};49    } else {50      throw new Error(`Unknown address format: "${input}"`);51    }52  }53  if ('address' in input) {54    return {Substrate: input.address};55  }56  if ('Ethereum' in input) {57    return {58      Ethereum: input.Ethereum.toLowerCase(),59    };60  } else if ('ethereum' in input) {61    return {62      Ethereum: (input as any).ethereum.toLowerCase(),63    };64  } else if ('Substrate' in input) {65    return input;66  } else if ('substrate' in input) {67    return {68      Substrate: (input as any).substrate,69    };70  }7172  // AccountId73  return {Substrate: input.toString()};74}75export function toSubstrateAddress(input: string | CrossAccountId | IKeyringPair): string {76  input = normalizeAccountId(input);77  if ('Substrate' in input) {78    return input.Substrate;79  } else {80    return evmToAddress(input.Ethereum);81  }82}8384export const U128_MAX = (1n << 128n) - 1n;8586const MICROUNIQUE = 1_000_000_000_000n;87const MILLIUNIQUE = 1_000n * MICROUNIQUE;88const CENTIUNIQUE = 10n * MILLIUNIQUE;89export const UNIQUE = 100n * CENTIUNIQUE;9091interface GenericResult<T> {92  success: boolean;93  data: T | null;94}9596interface CreateCollectionResult {97  success: boolean;98  collectionId: number;99}100101interface CreateItemResult {102  success: boolean;103  collectionId: number;104  itemId: number;105  recipient?: CrossAccountId;106  amount?: number;107}108109interface DestroyItemResult {110  success: boolean;111  collectionId: number;112  itemId: number;113  owner: CrossAccountId;114  amount: number;115}116117interface TransferResult {118  collectionId: number;119  itemId: number;120  sender?: CrossAccountId;121  recipient?: CrossAccountId;122  value: bigint;123}124125interface IReFungibleOwner {126  fraction: BN;127  owner: number[];128}129130interface IGetMessage {131  checkMsgUnqMethod: string;132  checkMsgTrsMethod: string;133  checkMsgSysMethod: string;134}135136export interface IFungibleTokenDataType {137  value: number;138}139140export interface IChainLimits {141  collectionNumbersLimit: number;142  accountTokenOwnershipLimit: number;143  collectionsAdminsLimit: number;144  customDataLimit: number;145  nftSponsorTransferTimeout: number;146  fungibleSponsorTransferTimeout: number;147  refungibleSponsorTransferTimeout: number;148  //offchainSchemaLimit: number;149  //constOnChainSchemaLimit: number;150}151152export interface IReFungibleTokenDataType {153  owner: IReFungibleOwner[];154}155156export function uniqueEventMessage(events: EventRecord[]): IGetMessage {157  let checkMsgUnqMethod = '';158  let checkMsgTrsMethod = '';159  let checkMsgSysMethod = '';160  events.forEach(({event: {method, section}}) => {161    if (section === 'common') {162      checkMsgUnqMethod = method;163    } else if (section === 'treasury') {164      checkMsgTrsMethod = method;165    } else if (section === 'system') {166      checkMsgSysMethod = method;167    } else { return null; }168  });169  const result: IGetMessage = {170    checkMsgUnqMethod,171    checkMsgTrsMethod,172    checkMsgSysMethod,173  };174  return result;175}176177export function getEvent<T extends Event>(events: EventRecord[], check: (event: IEvent<AnyTuple>) => event is T): T | undefined {178  const event = events.find(r => check(r.event));179  if (!event) return;180  return event.event as T;181}182183export function getGenericResult<T>(events: EventRecord[]): GenericResult<T>;184export function getGenericResult<T>(185  events: EventRecord[],186  expectSection: string,187  expectMethod: string,188  extractAction: (data: GenericEventData) => T189): GenericResult<T>;190191export function getGenericResult<T>(192  events: EventRecord[],193  expectSection?: string,194  expectMethod?: string,195  extractAction?: (data: GenericEventData) => T,196): GenericResult<T> {197  let success = false;198  let successData = null;199200  events.forEach(({event: {data, method, section}}) => {201    // console.log(`    ${phase}: ${section}.${method}:: ${data}`);202    if (method === 'ExtrinsicSuccess') {203      success = true;204    } else if ((expectSection == section) && (expectMethod == method)) {205      successData = extractAction!(data as any);206    }207  });208209  const result: GenericResult<T> = {210    success,211    data: successData,212  };213  return result;214}215216export function getCreateCollectionResult(events: EventRecord[]): CreateCollectionResult {217  const genericResult = getGenericResult(events, 'common', 'CollectionCreated', (data) => parseInt(data[0].toString(), 10));218  const result: CreateCollectionResult = {219    success: genericResult.success,220    collectionId: genericResult.data ?? 0,221  };222  return result;223}224225export function getCreateItemsResult(events: EventRecord[]): CreateItemResult[] {226  const results: CreateItemResult[] = [];227  228  const genericResult = getGenericResult<CreateItemResult[]>(events, 'common', 'ItemCreated', (data) => {229    const collectionId = parseInt(data[0].toString(), 10);230    const itemId = parseInt(data[1].toString(), 10);231    const recipient = normalizeAccountId(data[2].toJSON() as any);232    const amount = parseInt(data[3].toString(), 10);233234    const itemRes: CreateItemResult = {235      success: true,236      collectionId,237      itemId,238      recipient,239      amount,240    };241242    results.push(itemRes);243    return results;244  });245246  if (!genericResult.success) return [];247  return results;248}249250export function getCreateItemResult(events: EventRecord[]): CreateItemResult {251  const genericResult = getGenericResult<[number, number, CrossAccountId?]>(events, 'common', 'ItemCreated', (data) => [252    parseInt(data[0].toString(), 10),253    parseInt(data[1].toString(), 10),254    normalizeAccountId(data[2].toJSON() as any),255  ]);256257  if (genericResult.data == null) genericResult.data = [0, 0];258259  const result: CreateItemResult = {260    success: genericResult.success,261    collectionId: genericResult.data[0],262    itemId: genericResult.data[1],263    recipient: genericResult.data![2],264  };265  266  return result;267}268269export function getDestroyItemsResult(events: EventRecord[]): DestroyItemResult[] {270  const results: DestroyItemResult[] = [];271  272  const genericResult = getGenericResult<DestroyItemResult[]>(events, 'common', 'ItemDestroyed', (data) => {273    const collectionId = parseInt(data[0].toString(), 10);274    const itemId = parseInt(data[1].toString(), 10);275    const owner = normalizeAccountId(data[2].toJSON() as any);276    const amount = parseInt(data[3].toString(), 10);277278    const itemRes: DestroyItemResult = {279      success: true,280      collectionId,281      itemId,282      owner,283      amount,284    };285286    results.push(itemRes);287    return results;288  });289290  if (!genericResult.success) return [];291  return results;292}293294export function getTransferResult(api: ApiPromise, events: EventRecord[]): TransferResult {295  for (const {event} of events) {296    if (api.events.common.Transfer.is(event)) {297      const [collection, token, sender, recipient, value] = event.data;298      return {299        collectionId: collection.toNumber(),300        itemId: token.toNumber(),301        sender: normalizeAccountId(sender.toJSON() as any),302        recipient: normalizeAccountId(recipient.toJSON() as any),303        value: value.toBigInt(),304      };305    }306  }307  throw new Error('no transfer event');308}309310interface Nft {311  type: 'NFT';312}313314interface Fungible {315  type: 'Fungible';316  decimalPoints: number;317}318319interface ReFungible {320  type: 'ReFungible';321}322323export type CollectionMode = Nft | Fungible | ReFungible;324325export type Property = {326  key: any,327  value: any,328};329330type Permission = {331  mutable: boolean;332  collectionAdmin: boolean;333  tokenOwner: boolean;334}335336type PropertyPermission = {337  key: any;338  permission: Permission;339}340341export type CreateCollectionParams = {342  mode: CollectionMode,343  name: string,344  description: string,345  tokenPrefix: string,346  properties?: Array<Property>,347  propPerm?: Array<PropertyPermission>348};349350const defaultCreateCollectionParams: CreateCollectionParams = {351  description: 'description',352  mode: {type: 'NFT'},353  name: 'name',354  tokenPrefix: 'prefix',355};356357export async function358createCollection(359  api: ApiPromise,360  sender: IKeyringPair,361  params: Partial<CreateCollectionParams> = {},362): Promise<CreateCollectionResult> {363  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};364365  let modeprm = {};366  if (mode.type === 'NFT') {367    modeprm = {nft: null};368  } else if (mode.type === 'Fungible') {369    modeprm = {fungible: mode.decimalPoints};370  } else if (mode.type === 'ReFungible') {371    modeprm = {refungible: null};372  }373374  const tx = api.tx.unique.createCollectionEx({375    name: strToUTF16(name),376    description: strToUTF16(description),377    tokenPrefix: strToUTF16(tokenPrefix),378    mode: modeprm as any,379  });380  const events = await submitTransactionAsync(sender, tx);381  return getCreateCollectionResult(events);382}383384export async function createCollectionExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {385  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};386387  let collectionId = 0;388  await usingApi(async (api, privateKeyWrapper) => {389    // Get number of collections before the transaction390    const collectionCountBefore = await getCreatedCollectionCount(api);391392    // Run the CreateCollection transaction393    const alicePrivateKey = privateKeyWrapper('//Alice');394395    const result = await createCollection(api, alicePrivateKey, params);396397    // Get number of collections after the transaction398    const collectionCountAfter = await getCreatedCollectionCount(api);399400    // Get the collection401    const collection = await queryCollectionExpectSuccess(api, result.collectionId);402403    // What to expect404    // tslint:disable-next-line:no-unused-expression405    expect(result.success).to.be.true;406    expect(result.collectionId).to.be.equal(collectionCountAfter);407    // tslint:disable-next-line:no-unused-expression408    expect(collection).to.be.not.null;409    expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.');410    expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey));411    expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name);412    expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description);413    expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix);414415    collectionId = result.collectionId;416  });417418  return collectionId;419}420421export async function createCollectionWithPropsExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {422  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};423424  let collectionId = 0;425  await usingApi(async (api, privateKeyWrapper) => {426    // Get number of collections before the transaction427    const collectionCountBefore = await getCreatedCollectionCount(api);428429    // Run the CreateCollection transaction430    const alicePrivateKey = privateKeyWrapper('//Alice');431432    let modeprm = {};433    if (mode.type === 'NFT') {434      modeprm = {nft: null};435    } else if (mode.type === 'Fungible') {436      modeprm = {fungible: mode.decimalPoints};437    } else if (mode.type === 'ReFungible') {438      modeprm = {refungible: null};439    }440441    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm});442    const events = await submitTransactionAsync(alicePrivateKey, tx);443    const result = getCreateCollectionResult(events);444445    // Get number of collections after the transaction446    const collectionCountAfter = await getCreatedCollectionCount(api);447448    // Get the collection449    const collection = await queryCollectionExpectSuccess(api, result.collectionId);450451    // What to expect452    // tslint:disable-next-line:no-unused-expression453    expect(result.success).to.be.true;454    expect(result.collectionId).to.be.equal(collectionCountAfter);455    // tslint:disable-next-line:no-unused-expression456    expect(collection).to.be.not.null;457    expect(collectionCountAfter).to.be.equal(collectionCountBefore + 1, 'Error: NFT collection NOT created.');458    expect(collection.owner.toString()).to.be.equal(toSubstrateAddress(alicePrivateKey));459    expect(utf16ToStr(collection.name.toJSON() as any)).to.be.equal(name);460    expect(utf16ToStr(collection.description.toJSON() as any)).to.be.equal(description);461    expect(hexToStr(collection.tokenPrefix.toJSON())).to.be.equal(tokenPrefix);462463464    collectionId = result.collectionId;465  });466467  return collectionId;468}469470export async function createCollectionWithPropsExpectFailure(params: Partial<CreateCollectionParams> = {}) {471  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};472473  await usingApi(async (api, privateKeyWrapper) => {474    // Get number of collections before the transaction475    const collectionCountBefore = await getCreatedCollectionCount(api);476477    // Run the CreateCollection transaction478    const alicePrivateKey = privateKeyWrapper('//Alice');479480    let modeprm = {};481    if (mode.type === 'NFT') {482      modeprm = {nft: null};483    } else if (mode.type === 'Fungible') {484      modeprm = {fungible: mode.decimalPoints};485    } else if (mode.type === 'ReFungible') {486      modeprm = {refungible: null};487    }488489    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any, properties: params.properties, tokenPropertyPermissions: params.propPerm});490    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;491492493    // Get number of collections after the transaction494    const collectionCountAfter = await getCreatedCollectionCount(api);495496    expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.');497  });498}499500export async function createCollectionExpectFailure(params: Partial<CreateCollectionParams> = {}) {501  const {name, description, mode, tokenPrefix} = {...defaultCreateCollectionParams, ...params};502503  let modeprm = {};504  if (mode.type === 'NFT') {505    modeprm = {nft: null};506  } else if (mode.type === 'Fungible') {507    modeprm = {fungible: mode.decimalPoints};508  } else if (mode.type === 'ReFungible') {509    modeprm = {refungible: null};510  }511512  await usingApi(async (api, privateKeyWrapper) => {513    // Get number of collections before the transaction514    const collectionCountBefore = await getCreatedCollectionCount(api);515516    // Run the CreateCollection transaction517    const alicePrivateKey = privateKeyWrapper('//Alice');518    const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any});519    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;520521    // Get number of collections after the transaction522    const collectionCountAfter = await getCreatedCollectionCount(api);523524    // What to expect525    expect(collectionCountAfter).to.be.equal(collectionCountBefore, 'Error: Collection with incorrect data created.');526  });527}528529export async function findUnusedAddress(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, seedAddition = ''): Promise<IKeyringPair> {530  let bal = 0n;531  let unused;532  do {533    const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)) + seedAddition;534    unused = privateKeyWrapper(`//${randomSeed}`);535    bal = (await api.query.system.account(unused.address)).data.free.toBigInt();536  } while (bal !== 0n);537  return unused;538}539540export async function getAllowance(api: ApiPromise, collectionId: number, owner: CrossAccountId | string | IKeyringPair, approved: CrossAccountId | string | IKeyringPair, tokenId: number) {541  return (await api.rpc.unique.allowance(collectionId, normalizeAccountId(owner), normalizeAccountId(approved), tokenId)).toBigInt();542}543544export function findUnusedAddresses(api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair, amount: number): Promise<IKeyringPair[]> {545  return Promise.all(new Array(amount).fill(null).map(() => findUnusedAddress(api, privateKeyWrapper, '_' + Date.now())));546}547548export async function findNotExistingCollection(api: ApiPromise): Promise<number> {549  const totalNumber = await getCreatedCollectionCount(api);550  const newCollection: number = totalNumber + 1;551  return newCollection;552}553554function getDestroyResult(events: EventRecord[]): boolean {555  let success = false;556  events.forEach(({event: {method}}) => {557    if (method == 'ExtrinsicSuccess') {558      success = true;559    }560  });561  return success;562}563564export async function destroyCollectionExpectFailure(collectionId: number, senderSeed = '//Alice') {565  await usingApi(async (api, privateKeyWrapper) => {566    // Run the DestroyCollection transaction567    const alicePrivateKey = privateKeyWrapper(senderSeed);568    const tx = api.tx.unique.destroyCollection(collectionId);569    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;570  });571}572573export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed = '//Alice') {574  await usingApi(async (api, privateKeyWrapper) => {575    // Run the DestroyCollection transaction576    const alicePrivateKey = privateKeyWrapper(senderSeed);577    const tx = api.tx.unique.destroyCollection(collectionId);578    const events = await submitTransactionAsync(alicePrivateKey, tx);579    const result = getDestroyResult(events);580    expect(result).to.be.true;581582    // What to expect583    expect(await getDetailedCollectionInfo(api, collectionId)).to.be.null;584  });585}586587export async function setCollectionLimitsExpectSuccess(sender: IKeyringPair, collectionId: number, limits: any) {588  await usingApi(async (api) => {589    const tx = api.tx.unique.setCollectionLimits(collectionId, limits);590    const events = await submitTransactionAsync(sender, tx);591    const result = getGenericResult(events);592593    expect(result.success).to.be.true;594  });595}596597export const setCollectionPermissionsExpectSuccess = async (sender: IKeyringPair, collectionId: number, permissions: any) => {598  await usingApi(async(api) => {599    const tx = api.tx.unique.setCollectionPermissions(collectionId, permissions);600    const events = await submitTransactionAsync(sender, tx);601    const result = getGenericResult(events);602603    expect(result.success).to.be.true;604  });605};606607export async function setCollectionLimitsExpectFailure(sender: IKeyringPair, collectionId: number, limits: any) {608  await usingApi(async (api) => {609    const tx = api.tx.unique.setCollectionLimits(collectionId, limits);610    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;611    const result = getGenericResult(events);612613    expect(result.success).to.be.false;614  });615}616617export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string, sender = '//Alice') {618  await usingApi(async (api, privateKeyWrapper) => {619620    // Run the transaction621    const senderPrivateKey = privateKeyWrapper(sender);622    const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor);623    const events = await submitTransactionAsync(senderPrivateKey, tx);624    const result = getGenericResult(events);625626    // Get the collection627    const collection = await queryCollectionExpectSuccess(api, collectionId);628629    // What to expect630    expect(result.success).to.be.true;631    expect(collection.sponsorship.toJSON()).to.deep.equal({632      unconfirmed: sponsor,633    });634  });635}636637export async function removeCollectionSponsorExpectSuccess(collectionId: number, sender = '//Alice') {638  await usingApi(async (api, privateKeyWrapper) => {639640    // Run the transaction641    const alicePrivateKey = privateKeyWrapper(sender);642    const tx = api.tx.unique.removeCollectionSponsor(collectionId);643    const events = await submitTransactionAsync(alicePrivateKey, tx);644    const result = getGenericResult(events);645646    // Get the collection647    const collection = await queryCollectionExpectSuccess(api, collectionId);648649    // What to expect650    expect(result.success).to.be.true;651    expect(collection.sponsorship.toJSON()).to.be.deep.equal({disabled: null});652  });653}654655export async function removeCollectionSponsorExpectFailure(collectionId: number, senderSeed = '//Alice') {656  await usingApi(async (api, privateKeyWrapper) => {657658    // Run the transaction659    const alicePrivateKey = privateKeyWrapper(senderSeed);660    const tx = api.tx.unique.removeCollectionSponsor(collectionId);661    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;662  });663}664665export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed = '//Alice') {666  await usingApi(async (api, privateKeyWrapper) => {667668    // Run the transaction669    const alicePrivateKey = privateKeyWrapper(senderSeed);670    const tx = api.tx.unique.setCollectionSponsor(collectionId, sponsor);671    await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;672  });673}674675export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed = '//Alice') {676  await usingApi(async (api, privateKeyWrapper) => {677678    // Run the transaction679    const sender = privateKeyWrapper(senderSeed);680    await confirmSponsorshipByKeyExpectSuccess(collectionId, sender);681  });682}683684export async function confirmSponsorshipByKeyExpectSuccess(collectionId: number, sender: IKeyringPair) {685  await usingApi(async (api, privateKeyWrapper) => {686687    // Run the transaction688    const tx = api.tx.unique.confirmSponsorship(collectionId);689    const events = await submitTransactionAsync(sender, tx);690    const result = getGenericResult(events);691692    // Get the collection693    const collection = await queryCollectionExpectSuccess(api, collectionId);694695    // What to expect696    expect(result.success).to.be.true;697    expect(collection.sponsorship.toJSON()).to.be.deep.equal({698      confirmed: sender.address,699    });700  });701}702703704export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed = '//Alice') {705  await usingApi(async (api, privateKeyWrapper) => {706707    // Run the transaction708    const sender = privateKeyWrapper(senderSeed);709    const tx = api.tx.unique.confirmSponsorship(collectionId);710    await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;711  });712}713714export async function enableContractSponsoringExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {715  await usingApi(async (api) => {716    const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable);717    const events = await submitTransactionAsync(sender, tx);718    const result = getGenericResult(events);719720    expect(result.success).to.be.true;721  });722}723724export async function enableContractSponsoringExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, enable: boolean) {725  await usingApi(async (api) => {726    const tx = api.tx.unique.enableContractSponsoring(contractAddress, enable);727    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;728    const result = getGenericResult(events);729730    expect(result.success).to.be.false;731  });732}733734export async function setTransferFlagExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) {735736  await usingApi(async (api) => {737738    const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled);739    const events = await submitTransactionAsync(sender, tx);740    const result = getGenericResult(events);741742    expect(result.success).to.be.true;743  });744}745746export async function setTransferFlagExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) {747748  await usingApi(async (api) => {749750    const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled);751    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;752    const result = getGenericResult(events);753754    expect(result.success).to.be.false;755  });756}757758export async function setContractSponsoringRateLimitExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {759  await usingApi(async (api) => {760    const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit);761    const events = await submitTransactionAsync(sender, tx);762    const result = getGenericResult(events);763764    expect(result.success).to.be.true;765  });766}767768export async function setContractSponsoringRateLimitExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, rateLimit: number) {769  await usingApi(async (api) => {770    const tx = api.tx.unique.setContractSponsoringRateLimit(contractAddress, rateLimit);771    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;772    const result = getGenericResult(events);773774    expect(result.success).to.be.false;775  });776}777778export async function getNextSponsored(779  api: ApiPromise,780  collectionId: number,781  account: string | CrossAccountId,782  tokenId: number,783): Promise<number> {784  return Number((await api.rpc.unique.nextSponsored(collectionId, account, tokenId)).unwrapOr(-1));785}786787export async function toggleContractAllowlistExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, value = true) {788  await usingApi(async (api) => {789    const tx = api.tx.unique.toggleContractAllowList(contractAddress, value);790    const events = await submitTransactionAsync(sender, tx);791    const result = getGenericResult(events);792793    expect(result.success).to.be.true;794  });795}796797export async function isAllowlistedInContract(contractAddress: AccountId | string, user: string) {798  let allowlisted = false;799  await usingApi(async (api) => {800    allowlisted = (await api.query.unique.contractAllowList(contractAddress, user)).toJSON() as boolean;801  });802  return allowlisted;803}804805export async function addToContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {806  await usingApi(async (api) => {807    const tx = api.tx.unique.addToContractAllowList(contractAddress.toString(), user.toString());808    const events = await submitTransactionAsync(sender, tx);809    const result = getGenericResult(events);810811    expect(result.success).to.be.true;812  });813}814815export async function removeFromContractAllowListExpectSuccess(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {816  await usingApi(async (api) => {817    const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString());818    const events = await submitTransactionAsync(sender, tx);819    const result = getGenericResult(events);820821    expect(result.success).to.be.true;822  });823}824825export async function removeFromContractAllowListExpectFailure(sender: IKeyringPair, contractAddress: AccountId | string, user: AccountId | string) {826  await usingApi(async (api) => {827    const tx = api.tx.unique.removeFromContractAllowList(contractAddress.toString(), user.toString());828    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;829    const result = getGenericResult(events);830831    expect(result.success).to.be.false;832  });833}834835export interface CreateFungibleData {836  readonly Value: bigint;837}838839export interface CreateReFungibleData { }840export interface CreateNftData { }841842export type CreateItemData = {843  NFT: CreateNftData;844} | {845  Fungible: CreateFungibleData;846} | {847  ReFungible: CreateReFungibleData;848};849850export async function burnItem(api: ApiPromise, sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint) : Promise<boolean> {851  const tx = api.tx.unique.burnItem(collectionId, tokenId, value);852  const events = await submitTransactionAsync(sender, tx);853  return getGenericResult(events).success;854}855856export async function burnItemExpectSuccess(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) {857  await usingApi(async (api) => {858    const balanceBefore = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId);859    // if burning token by admin - use adminButnItemExpectSuccess860    expect(balanceBefore >= BigInt(value)).to.be.true;861862    expect(await burnItem(api, sender, collectionId, tokenId, value)).to.be.true;863864    const balanceAfter = await getBalance(api, collectionId, normalizeAccountId(sender), tokenId);865    expect(balanceAfter + BigInt(value)).to.be.equal(balanceBefore);866  });867}868869export async function burnItemExpectFailure(sender: IKeyringPair, collectionId: number, tokenId: number, value: number | bigint = 1) {870  await usingApi(async (api) => {871    const tx = api.tx.unique.burnItem(collectionId, tokenId, value);872873    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;874    const result = getCreateCollectionResult(events);875    // tslint:disable-next-line:no-unused-expression876    expect(result.success).to.be.false;877  });878}879880export async function burnFromExpectSuccess(sender: IKeyringPair, from: IKeyringPair | CrossAccountId, collectionId: number, tokenId: number, value: number | bigint = 1) {881  await usingApi(async (api) => {882    const tx = api.tx.unique.burnFrom(collectionId, normalizeAccountId(from), tokenId, value);883    const events = await submitTransactionAsync(sender, tx);884    return getGenericResult(events).success;885  });886}887888export async function889approve(890  api: ApiPromise,891  collectionId: number,892  tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string | IKeyringPair, amount: number | bigint,893) {894  const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);895  const events = await submitTransactionAsync(owner, approveUniqueTx);896  return getGenericResult(events).success;897}898899export async function900approveExpectSuccess(901  collectionId: number,902  tokenId: number, owner: IKeyringPair, approved: CrossAccountId | string, amount: number | bigint = 1,903) {904  await usingApi(async (api: ApiPromise) => {905    const result = await approve(api, collectionId, tokenId, owner, approved, amount);906    expect(result).to.be.true;907908    expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount));909  });910}911912export async function adminApproveFromExpectSuccess(913  collectionId: number,914  tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1,915) {916  await usingApi(async (api: ApiPromise) => {917    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);918    const events = await submitTransactionAsync(admin, approveUniqueTx);919    const result = getGenericResult(events);920    expect(result.success).to.be.true;921922    expect(await getAllowance(api, collectionId, owner, approved, tokenId)).to.be.equal(BigInt(amount));923  });924}925926export async function927transferFrom(928  api: ApiPromise,929  collectionId: number,930  tokenId: number,931  accountApproved: IKeyringPair,932  accountFrom: IKeyringPair | CrossAccountId,933  accountTo: IKeyringPair | CrossAccountId,934  value: number | bigint,935) {936  const from = normalizeAccountId(accountFrom);937  const to = normalizeAccountId(accountTo);938  const transferFromTx = api.tx.unique.transferFrom(from, to, collectionId, tokenId, value);939  const events = await submitTransactionAsync(accountApproved, transferFromTx);940  return getGenericResult(events).success;941}942943export async function944transferFromExpectSuccess(945  collectionId: number,946  tokenId: number,947  accountApproved: IKeyringPair,948  accountFrom: IKeyringPair | CrossAccountId,949  accountTo: IKeyringPair | CrossAccountId,950  value: number | bigint = 1,951  type = 'NFT',952) {953  await usingApi(async (api: ApiPromise) => {954    const from = normalizeAccountId(accountFrom);955    const to = normalizeAccountId(accountTo);956    let balanceBefore = 0n;957    if (type === 'Fungible' || type === 'ReFungible') {958      balanceBefore = await getBalance(api, collectionId, to, tokenId);959    }960    expect(await transferFrom(api, collectionId, tokenId, accountApproved, accountFrom, accountTo, value)).to.be.true;961    if (type === 'NFT') {962      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to);963    }964    if (type === 'Fungible') {965      const balanceAfter = await getBalance(api, collectionId, to, tokenId);966      if (JSON.stringify(to) !== JSON.stringify(from)) {967        expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value));968      } else {969        expect(balanceAfter).to.be.equal(balanceBefore);970      }971    }972    if (type === 'ReFungible') {973      expect(await getBalance(api, collectionId, to, tokenId)).to.be.equal(balanceBefore + BigInt(value));974    }975  });976}977978export async function979transferFromExpectFail(980  collectionId: number,981  tokenId: number,982  accountApproved: IKeyringPair,983  accountFrom: IKeyringPair,984  accountTo: IKeyringPair,985  value: number | bigint = 1,986) {987  await usingApi(async (api: ApiPromise) => {988    const transferFromTx = api.tx.unique.transferFrom(normalizeAccountId(accountFrom.address), normalizeAccountId(accountTo.address), collectionId, tokenId, value);989    const events = await expect(submitTransactionExpectFailAsync(accountApproved, transferFromTx)).to.be.rejected;990    const result = getCreateCollectionResult(events);991    // tslint:disable-next-line:no-unused-expression992    expect(result.success).to.be.false;993  });994}995996/* eslint no-async-promise-executor: "off" */997export async function getBlockNumber(api: ApiPromise): Promise<number> {998  return new Promise<number>(async (resolve) => {999    const unsubscribe = await api.rpc.chain.subscribeNewHeads((head) => {1000      unsubscribe();1001      resolve(head.number.toNumber());1002    });1003  });1004}10051006export async function addCollectionAdminExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | CrossAccountId) {1007  await usingApi(async (api) => {1008    const changeAdminTx = api.tx.unique.addCollectionAdmin(collectionId, normalizeAccountId(address));1009    const events = await submitTransactionAsync(sender, changeAdminTx);1010    const result = getCreateCollectionResult(events);1011    expect(result.success).to.be.true;1012  });1013}10141015export async function adminApproveFromExpectFail(1016  collectionId: number,1017  tokenId: number, admin: IKeyringPair, owner: CrossAccountId | string, approved: CrossAccountId | string, amount: number | bigint = 1,1018) {1019  await usingApi(async (api: ApiPromise) => {1020    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved), collectionId, tokenId, amount);1021    const events = await expect(submitTransactionAsync(admin, approveUniqueTx)).to.be.rejected;1022    const result = getGenericResult(events);1023    expect(result.success).to.be.false;1024  });1025}10261027export async function1028getFreeBalance(account: IKeyringPair): Promise<bigint> {1029  let balance = 0n;1030  await usingApi(async (api) => {1031    balance = BigInt((await api.query.system.account(account.address)).data.free.toString());1032  });10331034  return balance;1035}10361037export async function transferBalanceTo(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {1038  const tx = api.tx.balances.transfer(target, amount);1039  const events = await submitTransactionAsync(source, tx);1040  const result = getGenericResult(events);1041  expect(result.success).to.be.true;1042}10431044export async function1045scheduleExpectSuccess(1046  operationTx: any,1047  sender: IKeyringPair,1048  blockSchedule: number,1049  scheduledId: string,1050  period = 1,1051  repetitions = 1,1052) {1053  await usingApi(async (api: ApiPromise) => {1054    const blockNumber: number | undefined = await getBlockNumber(api);1055    const expectedBlockNumber = blockNumber + blockSchedule;10561057    expect(blockNumber).to.be.greaterThan(0);1058    const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule1059      scheduledId,1060      expectedBlockNumber, 1061      repetitions > 1 ? [period, repetitions] : null, 1062      0, 1063      {Value: operationTx as any},1064    );10651066    const events = await submitTransactionAsync(sender, scheduleTx);1067    expect(getGenericResult(events).success).to.be.true;1068  });1069}10701071export async function1072scheduleExpectFailure(1073  operationTx: any,1074  sender: IKeyringPair,1075  blockSchedule: number,1076  scheduledId: string,1077  period = 1,1078  repetitions = 1,1079) {1080  await usingApi(async (api: ApiPromise) => {1081    const blockNumber: number | undefined = await getBlockNumber(api);1082    const expectedBlockNumber = blockNumber + blockSchedule;10831084    expect(blockNumber).to.be.greaterThan(0);1085    const scheduleTx = api.tx.scheduler.scheduleNamed( // schedule1086      scheduledId,1087      expectedBlockNumber, 1088      repetitions <= 1 ? null : [period, repetitions], 1089      0, 1090      {Value: operationTx as any},1091    );10921093    //const events = 1094    await expect(submitTransactionExpectFailAsync(sender, scheduleTx)).to.be.rejected;1095    //expect(getGenericResult(events).success).to.be.false;1096  });1097}10981099export async function1100scheduleTransferAndWaitExpectSuccess(1101  collectionId: number,1102  tokenId: number,1103  sender: IKeyringPair,1104  recipient: IKeyringPair,1105  value: number | bigint = 1,1106  blockSchedule: number,1107  scheduledId: string,1108) {1109  await usingApi(async (api: ApiPromise) => {1110    await scheduleTransferExpectSuccess(collectionId, tokenId, sender, recipient, value, blockSchedule, scheduledId);11111112    const recipientBalanceBefore = (await api.query.system.account(recipient.address)).data.free.toBigInt();11131114    // sleep for n + 1 blocks1115    await waitNewBlocks(blockSchedule + 1);11161117    const recipientBalanceAfter = (await api.query.system.account(recipient.address)).data.free.toBigInt();11181119    expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(recipient.address));1120    expect(recipientBalanceAfter).to.be.equal(recipientBalanceBefore);1121  });1122}11231124export async function1125scheduleTransferExpectSuccess(1126  collectionId: number,1127  tokenId: number,1128  sender: IKeyringPair,1129  recipient: IKeyringPair,1130  value: number | bigint = 1,1131  blockSchedule: number,1132  scheduledId: string,1133) {1134  await usingApi(async (api: ApiPromise) => {1135    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient.address), collectionId, tokenId, value);11361137    await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId);11381139    expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(sender.address));1140  });1141}11421143export async function1144scheduleTransferFundsPeriodicExpectSuccess(1145  amount: bigint,1146  sender: IKeyringPair,1147  recipient: IKeyringPair,1148  blockSchedule: number,1149  scheduledId: string,1150  period: number,1151  repetitions: number,1152) {1153  await usingApi(async (api: ApiPromise) => {1154    const transferTx = api.tx.balances.transfer(recipient.address, amount);11551156    const balanceBefore = await getFreeBalance(recipient);1157    1158    await scheduleExpectSuccess(transferTx, sender, blockSchedule, scheduledId, period, repetitions);11591160    expect(await getFreeBalance(recipient)).to.be.equal(balanceBefore);1161  });1162}11631164export async function1165transfer(1166  api: ApiPromise,1167  collectionId: number,1168  tokenId: number,1169  sender: IKeyringPair,1170  recipient: IKeyringPair | CrossAccountId,1171  value: number | bigint,1172) : Promise<boolean> {1173  const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);1174  const events = await executeTransaction(api, sender, transferTx);1175  return getGenericResult(events).success;1176}11771178export async function1179transferExpectSuccess(1180  collectionId: number,1181  tokenId: number,1182  sender: IKeyringPair,1183  recipient: IKeyringPair | CrossAccountId,1184  value: number | bigint = 1,1185  type = 'NFT',1186) {1187  await usingApi(async (api: ApiPromise) => {1188    const from = normalizeAccountId(sender);1189    const to = normalizeAccountId(recipient);11901191    let balanceBefore = 0n;1192    if (type === 'Fungible' || type === 'ReFungible') {1193      balanceBefore = await getBalance(api, collectionId, to, tokenId);1194    }11951196    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);1197    const events = await executeTransaction(api, sender, transferTx);1198    const result = getTransferResult(api, events);11991200    expect(result.collectionId).to.be.equal(collectionId);1201    expect(result.itemId).to.be.equal(tokenId);1202    expect(result.sender).to.be.deep.equal(normalizeAccountId(sender.address));1203    expect(result.recipient).to.be.deep.equal(to);1204    expect(result.value).to.be.equal(BigInt(value));12051206    if (type === 'NFT') {1207      expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(to);1208    }1209    if (type === 'Fungible' || type === 'ReFungible') {1210      const balanceAfter = await getBalance(api, collectionId, to, tokenId);1211      if (JSON.stringify(to) !== JSON.stringify(from)) {1212        expect(balanceAfter - balanceBefore).to.be.equal(BigInt(value));1213      } else {1214        expect(balanceAfter).to.be.equal(balanceBefore);1215      }1216    }1217  });1218}12191220export async function1221transferExpectFailure(1222  collectionId: number,1223  tokenId: number,1224  sender: IKeyringPair,1225  recipient: IKeyringPair | CrossAccountId,1226  value: number | bigint = 1,1227) {1228  await usingApi(async (api: ApiPromise) => {1229    const transferTx = api.tx.unique.transfer(normalizeAccountId(recipient), collectionId, tokenId, value);1230    const events = await expect(submitTransactionExpectFailAsync(sender, transferTx)).to.be.rejected;1231    const result = getGenericResult(events);1232    // if (events && Array.isArray(events)) {1233    //   const result = getCreateCollectionResult(events);1234    // tslint:disable-next-line:no-unused-expression1235    expect(result.success).to.be.false;1236    //}1237  });1238}12391240export async function1241approveExpectFail(1242  collectionId: number,1243  tokenId: number, owner: IKeyringPair, approved: IKeyringPair, amount: number | bigint = 1,1244) {1245  await usingApi(async (api: ApiPromise) => {1246    const approveUniqueTx = api.tx.unique.approve(normalizeAccountId(approved.address), collectionId, tokenId, amount);1247    const events = await expect(submitTransactionExpectFailAsync(owner, approveUniqueTx)).to.be.rejected;1248    const result = getCreateCollectionResult(events);1249    // tslint:disable-next-line:no-unused-expression1250    expect(result.success).to.be.false;1251  });1252}12531254export async function getBalance(1255  api: ApiPromise,1256  collectionId: number,1257  owner: string | CrossAccountId | IKeyringPair,1258  token: number,1259): Promise<bigint> {1260  return (await api.rpc.unique.balance(collectionId, normalizeAccountId(owner), token)).toBigInt();1261}1262export async function getTokenOwner(1263  api: ApiPromise,1264  collectionId: number,1265  token: number,1266): Promise<CrossAccountId> {1267  const owner = (await api.rpc.unique.tokenOwner(collectionId, token)).toJSON() as any;1268  if (owner == null) throw new Error('owner == null');1269  return normalizeAccountId(owner);1270}1271export async function getTopmostTokenOwner(1272  api: ApiPromise,1273  collectionId: number,1274  token: number,1275): Promise<CrossAccountId> {1276  const owner = (await api.rpc.unique.topmostTokenOwner(collectionId, token)).toJSON() as any;1277  if (owner == null) throw new Error('owner == null');1278  return normalizeAccountId(owner);1279}1280export async function getTokenChildren(1281  api: ApiPromise,1282  collectionId: number,1283  tokenId: number,1284): Promise<UpDataStructsTokenChild[]> {1285  return (await api.rpc.unique.tokenChildren(collectionId, tokenId)).toJSON() as any;1286}1287export async function isTokenExists(1288  api: ApiPromise,1289  collectionId: number,1290  token: number,1291): Promise<boolean> {1292  return (await api.rpc.unique.tokenExists(collectionId, token)).toJSON();1293}1294export async function getLastTokenId(1295  api: ApiPromise,1296  collectionId: number,1297): Promise<number> {1298  return (await api.rpc.unique.lastTokenId(collectionId)).toJSON();1299}1300export async function getAdminList(1301  api: ApiPromise,1302  collectionId: number,1303): Promise<string[]> {1304  return (await api.rpc.unique.adminlist(collectionId)).toHuman() as any;1305}1306export async function getTokenProperties(1307  api: ApiPromise,1308  collectionId: number,1309  tokenId: number,1310  propertyKeys: string[],1311): Promise<UpDataStructsProperty[]> {1312  return (await api.rpc.unique.tokenProperties(collectionId, tokenId, propertyKeys)).toHuman() as any;1313}13141315export async function createFungibleItemExpectSuccess(1316  sender: IKeyringPair,1317  collectionId: number,1318  data: CreateFungibleData,1319  owner: CrossAccountId | string = sender.address,1320) {1321  return await usingApi(async (api) => {1322    const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), {Fungible: data});13231324    const events = await submitTransactionAsync(sender, tx);1325    const result = getCreateItemResult(events);13261327    expect(result.success).to.be.true;1328    return result.itemId;1329  });1330}13311332export async function createMultipleItemsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) {1333  await usingApi(async (api) => {1334    const to = normalizeAccountId(owner);1335    const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData);13361337    const events = await submitTransactionAsync(sender, tx);1338    expect(getGenericResult(events).success).to.be.true;1339  });1340}13411342export async function createMultipleItemsWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any, owner: CrossAccountId | string = sender.address) {1343  await usingApi(async (api) => {1344    const to = normalizeAccountId(owner);1345    const tx = api.tx.unique.createMultipleItems(collectionId, to, itemsData);13461347    const events = await submitTransactionAsync(sender, tx);1348    const result = getCreateItemsResult(events);13491350    for (const res of result) {1351      expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty;1352    }1353  });1354}13551356export async function createMultipleItemsExWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, itemsData: any) {1357  await usingApi(async (api) => {1358    const tx = api.tx.unique.createMultipleItemsEx(collectionId, itemsData);13591360    const events = await submitTransactionAsync(sender, tx);1361    const result = getCreateItemsResult(events);13621363    for (const res of result) {1364      expect(await api.rpc.unique.tokenProperties(collectionId, res.itemId)).not.to.be.empty;1365    }1366  });1367}13681369export async function createItemWithPropsExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, props:  Array<Property>, owner: CrossAccountId | string = sender.address) {1370  let newItemId = 0;1371  await usingApi(async (api) => {1372    const to = normalizeAccountId(owner);1373    const itemCountBefore = await getLastTokenId(api, collectionId);1374    const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId);13751376    let tx;1377    if (createMode === 'Fungible') {1378      const createData = {fungible: {value: 10}};1379      tx = api.tx.unique.createItem(collectionId, to, createData as any);1380    } else if (createMode === 'ReFungible') {1381      const createData = {refungible: {pieces: 100}};1382      tx = api.tx.unique.createItem(collectionId, to, createData as any);1383    } else {1384      const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}});1385      tx = api.tx.unique.createItem(collectionId, to, data as UpDataStructsCreateItemData);1386    }13871388    const events = await submitTransactionAsync(sender, tx);1389    const result = getCreateItemResult(events);13901391    const itemCountAfter = await getLastTokenId(api, collectionId);1392    const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId);13931394    if (createMode === 'NFT') {1395      expect(await api.rpc.unique.tokenProperties(collectionId, result.itemId)).not.to.be.empty;1396    }13971398    // What to expect1399    // tslint:disable-next-line:no-unused-expression1400    expect(result.success).to.be.true;1401    if (createMode === 'Fungible') {1402      expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n);1403    } else {1404      expect(itemCountAfter).to.be.equal(itemCountBefore + 1);1405    }1406    expect(collectionId).to.be.equal(result.collectionId);1407    expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());1408    expect(to).to.be.deep.equal(result.recipient);1409    newItemId = result.itemId;1410  });1411  return newItemId;1412}14131414export async function createItemWithPropsExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, props: Array<Property>, owner: CrossAccountId | string = sender.address) {1415  await usingApi(async (api) => {14161417    let tx;1418    if (createMode === 'NFT') {1419      const data = api.createType('UpDataStructsCreateItemData', {NFT: {properties: props}}) as UpDataStructsCreateItemData;1420      tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), data);1421    } else {1422      tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode);1423    }142414251426    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1427    if(events.message && events.message.toString().indexOf('1002: Verification Error') > -1) return;1428    const result = getCreateItemResult(events);14291430    expect(result.success).to.be.false;1431  });1432}14331434export async function createItemExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) {1435  let newItemId = 0;1436  await usingApi(async (api) => {1437    const to = normalizeAccountId(owner);1438    const itemCountBefore = await getLastTokenId(api, collectionId);1439    const itemBalanceBefore = await getBalance(api, collectionId, to, newItemId);14401441    let tx;1442    if (createMode === 'Fungible') {1443      const createData = {fungible: {value: 10}};1444      tx = api.tx.unique.createItem(collectionId, to, createData as any);1445    } else if (createMode === 'ReFungible') {1446      const createData = {refungible: {pieces: 100}};1447      tx = api.tx.unique.createItem(collectionId, to, createData as any);1448    } else {1449      const createData = {nft: {}};1450      tx = api.tx.unique.createItem(collectionId, to, createData as any);1451    }14521453    const events = await executeTransaction(api, sender, tx);1454    const result = getCreateItemResult(events);14551456    const itemCountAfter = await getLastTokenId(api, collectionId);1457    const itemBalanceAfter = await getBalance(api, collectionId, to, newItemId);14581459    // What to expect1460    // tslint:disable-next-line:no-unused-expression1461    expect(result.success).to.be.true;1462    if (createMode === 'Fungible') {1463      expect(itemBalanceAfter - itemBalanceBefore).to.be.equal(10n);1464    } else {1465      expect(itemCountAfter).to.be.equal(itemCountBefore + 1);1466    }1467    expect(collectionId).to.be.equal(result.collectionId);1468    expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());1469    expect(to).to.be.deep.equal(result.recipient);1470    newItemId = result.itemId;1471  });1472  return newItemId;1473}14741475export async function createRefungibleToken(api: ApiPromise, sender: IKeyringPair, collectionId: number, amount: bigint, owner: CrossAccountId | IKeyringPair | string = sender.address) : Promise<CreateItemResult> {1476  const createData = {refungible: {pieces: amount}};1477  const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createData as any);14781479  const events = await submitTransactionAsync(sender, tx);1480  return  getCreateItemResult(events);1481}14821483export async function createItemExpectFailure(sender: IKeyringPair, collectionId: number, createMode: string, owner: CrossAccountId | string = sender.address) {1484  await usingApi(async (api) => {1485    const tx = api.tx.unique.createItem(collectionId, normalizeAccountId(owner), createMode);14861487    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1488    const result = getCreateItemResult(events);14891490    expect(result.success).to.be.false;1491  });1492}14931494export async function setPublicAccessModeExpectSuccess(1495  sender: IKeyringPair, collectionId: number,1496  accessMode: 'Normal' | 'AllowList',1497) {1498  await usingApi(async (api) => {14991500    // Run the transaction1501    const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode});1502    const events = await submitTransactionAsync(sender, tx);1503    const result = getGenericResult(events);15041505    // Get the collection1506    const collection = await queryCollectionExpectSuccess(api, collectionId);15071508    // What to expect1509    // tslint:disable-next-line:no-unused-expression1510    expect(result.success).to.be.true;1511    expect(collection.permissions.access.toHuman()).to.be.equal(accessMode);1512  });1513}15141515export async function setPublicAccessModeExpectFail(1516  sender: IKeyringPair, collectionId: number,1517  accessMode: 'Normal' | 'AllowList',1518) {1519  await usingApi(async (api) => {15201521    // Run the transaction1522    const tx = api.tx.unique.setCollectionPermissions(collectionId, {access: accessMode});1523    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1524    const result = getGenericResult(events);15251526    // What to expect1527    // tslint:disable-next-line:no-unused-expression1528    expect(result.success).to.be.false;1529  });1530}15311532export async function enableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) {1533  await setPublicAccessModeExpectSuccess(sender, collectionId, 'AllowList');1534}15351536export async function enableAllowListExpectFail(sender: IKeyringPair, collectionId: number) {1537  await setPublicAccessModeExpectFail(sender, collectionId, 'AllowList');1538}15391540export async function disableAllowListExpectSuccess(sender: IKeyringPair, collectionId: number) {1541  await setPublicAccessModeExpectSuccess(sender, collectionId, 'Normal');1542}15431544export async function setMintPermissionExpectSuccess(sender: IKeyringPair, collectionId: number, enabled: boolean) {1545  await usingApi(async (api) => {15461547    // Run the transaction1548    const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled});1549    const events = await submitTransactionAsync(sender, tx);1550    const result = getGenericResult(events);1551    expect(result.success).to.be.true;15521553    // Get the collection1554    const collection = await queryCollectionExpectSuccess(api, collectionId);15551556    expect(collection.permissions.mintMode.toHuman()).to.be.equal(enabled);1557  });1558}15591560export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) {1561  await setMintPermissionExpectSuccess(sender, collectionId, true);1562}15631564export async function setMintPermissionExpectFailure(sender: IKeyringPair, collectionId: number, enabled: boolean) {1565  await usingApi(async (api) => {1566    // Run the transaction1567    const tx = api.tx.unique.setCollectionPermissions(collectionId, {mintMode: enabled});1568    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1569    const result = getCreateCollectionResult(events);1570    // tslint:disable-next-line:no-unused-expression1571    expect(result.success).to.be.false;1572  });1573}15741575export async function setChainLimitsExpectFailure(sender: IKeyringPair, limits: IChainLimits) {1576  await usingApi(async (api) => {1577    // Run the transaction1578    const tx = api.tx.unique.setChainLimits(limits);1579    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1580    const result = getCreateCollectionResult(events);1581    // tslint:disable-next-line:no-unused-expression1582    expect(result.success).to.be.false;1583  });1584}15851586export async function isAllowlisted(api: ApiPromise, collectionId: number, address: string | CrossAccountId) {1587  return (await api.rpc.unique.allowed(collectionId, normalizeAccountId(address))).toJSON();1588}15891590export async function addToAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId | CrossAccountId) {1591  await usingApi(async (api) => {1592    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.false;15931594    // Run the transaction1595    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));1596    const events = await submitTransactionAsync(sender, tx);1597    const result = getGenericResult(events);1598    expect(result.success).to.be.true;15991600    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;1601  });1602}16031604export async function addToAllowListAgainExpectSuccess(sender: IKeyringPair, collectionId: number, address: string | AccountId) {1605  await usingApi(async (api) => {16061607    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;16081609    // Run the transaction1610    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));1611    const events = await submitTransactionAsync(sender, tx);1612    const result = getGenericResult(events);1613    expect(result.success).to.be.true;16141615    expect(await isAllowlisted(api, collectionId, normalizeAccountId(address))).to.be.true;1616  });1617}16181619export async function addToAllowListExpectFail(sender: IKeyringPair, collectionId: number, address: string | AccountId) {1620  await usingApi(async (api) => {16211622    // Run the transaction1623    const tx = api.tx.unique.addToAllowList(collectionId, normalizeAccountId(address));1624    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1625    const result = getGenericResult(events);16261627    // What to expect1628    // tslint:disable-next-line:no-unused-expression1629    expect(result.success).to.be.false;1630  });1631}16321633export async function removeFromAllowListExpectSuccess(sender: IKeyringPair, collectionId: number, address: CrossAccountId) {1634  await usingApi(async (api) => {1635    // Run the transaction1636    const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address));1637    const events = await submitTransactionAsync(sender, tx);1638    const result = getGenericResult(events);16391640    // What to expect1641    // tslint:disable-next-line:no-unused-expression1642    expect(result.success).to.be.true;1643  });1644}16451646export async function removeFromAllowListExpectFailure(sender: IKeyringPair, collectionId: number, address: CrossAccountId) {1647  await usingApi(async (api) => {1648    // Run the transaction1649    const tx = api.tx.unique.removeFromAllowList(collectionId, normalizeAccountId(address));1650    const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;1651    const result = getGenericResult(events);16521653    // What to expect1654    // tslint:disable-next-line:no-unused-expression1655    expect(result.success).to.be.false;1656  });1657}16581659export const getDetailedCollectionInfo = async (api: ApiPromise, collectionId: number)1660  : Promise<UpDataStructsRpcCollection | null> => {1661  return (await api.rpc.unique.collectionById(collectionId)).unwrapOr(null);1662};16631664export const getCreatedCollectionCount = async (api: ApiPromise): Promise<number> => {1665  // set global object - collectionsCount1666  return (await api.rpc.unique.collectionStats()).created.toNumber();1667};16681669export async function queryCollectionExpectSuccess(api: ApiPromise, collectionId: number): Promise<UpDataStructsRpcCollection> {1670  return (await api.rpc.unique.collectionById(collectionId)).unwrap();1671}16721673export async function waitNewBlocks(blocksCount = 1): Promise<void> {1674  await usingApi(async (api) => {1675    const promise = new Promise<void>(async (resolve) => {1676      const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => {1677        if (blocksCount > 0) {1678          blocksCount--;1679        } else {1680          unsubscribe();1681          resolve();1682        }1683      });1684    });1685    return promise;1686  });1687}16881689export async function repartitionRFT(1690  api: ApiPromise,1691  collectionId: number,1692  sender: IKeyringPair,1693  tokenId: number,1694  amount: bigint,1695): Promise<boolean> {1696  const tx = api.tx.unique.repartition(collectionId, tokenId, amount);1697  const events = await submitTransactionAsync(sender, tx);1698  const result = getGenericResult(events);16991700  return result.success;1701}