git.delta.rocks / unique-network / refs/commits / 833ab943fbb5

difftreelog

feat merge native erc20 adapter

Yaroslav Bolyukin2023-05-30parents: #103f351 #c43c309.patch.diff
in: master

78 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5871,6 +5871,7 @@
  "pallet-aura",
  "pallet-authorship",
  "pallet-balances",
+ "pallet-balances-adapter",
  "pallet-base-fee",
  "pallet-collator-selection",
  "pallet-common",
@@ -6252,6 +6253,27 @@
 ]
 
 [[package]]
+name = "pallet-balances-adapter"
+version = "0.1.0"
+dependencies = [
+ "evm-coder",
+ "frame-support",
+ "frame-system",
+ "pallet-balances",
+ "pallet-common",
+ "pallet-evm",
+ "pallet-evm-coder-substrate",
+ "pallet-evm-transaction-payment",
+ "pallet-structure",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-core",
+ "sp-runtime",
+ "sp-std",
+ "up-data-structs",
+]
+
+[[package]]
 name = "pallet-base-fee"
 version = "1.0.0"
 source = "git+https://github.com/uniquenetwork/unique-frontier?branch=unique-polkadot-v0.9.42#75ad5b9a49e1942553c4068228f1546ffb1f7246"
@@ -6947,6 +6969,7 @@
  "frame-benchmarking",
  "frame-support",
  "frame-system",
+ "pallet-balances",
  "pallet-common",
  "pallet-evm",
  "pallet-evm-coder-substrate",
@@ -7411,6 +7434,7 @@
  "frame-benchmarking",
  "frame-support",
  "frame-system",
+ "pallet-balances-adapter",
  "pallet-common",
  "pallet-evm",
  "pallet-evm-coder-substrate",
@@ -9350,6 +9374,7 @@
  "pallet-aura",
  "pallet-authorship",
  "pallet-balances",
+ "pallet-balances-adapter",
  "pallet-base-fee",
  "pallet-collator-selection",
  "pallet-common",
@@ -12902,8 +12927,8 @@
  "frame-support",
  "frame-system",
  "pallet-balances",
+ "pallet-balances-adapter",
  "pallet-common",
- "pallet-configuration",
  "pallet-ethereum",
  "pallet-evm",
  "pallet-evm-coder-substrate",
@@ -12914,7 +12939,6 @@
  "pallet-timestamp",
  "pallet-transaction-payment",
  "pallet-unique",
- "pallet-xcm",
  "parity-scale-codec",
  "scale-info",
  "sp-core",
@@ -13767,6 +13791,7 @@
  "pallet-aura",
  "pallet-authorship",
  "pallet-balances",
+ "pallet-balances-adapter",
  "pallet-base-fee",
  "pallet-collator-selection",
  "pallet-common",
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,9 +19,9 @@
 panic = "unwind"
 
 [profile.production]
+codegen-units = 1
 inherits = "release"
 lto = true
-codegen-units = 1
 opt-level = 3
 
 [workspace.dependencies]
@@ -29,6 +29,7 @@
 app-promotion-rpc = { path = "primitives/app_promotion_rpc", default-features = false }
 evm-coder = { git = "https://github.com/uniquenetwork/evm-coder", tag = "v0.3.1", default-features = false }
 pallet-app-promotion = { path = "pallets/app-promotion", default-features = false }
+pallet-balances-adapter = { default-features = false, path = "pallets/balances-adapter" }
 pallet-charge-transaction = { package = "pallet-template-transaction-payment", default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.42" }
 pallet-collator-selection = { default-features = false, path = "pallets/collator-selection" }
 pallet-common = { default-features = false, path = "pallets/common" }
@@ -85,15 +86,15 @@
 cumulus-client-consensus-common = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
 cumulus-client-network = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
 cumulus-client-service = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-pallet-aura-ext = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-pallet-dmp-queue = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-pallet-parachain-system = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-pallet-xcm = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-pallet-xcmp-queue = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-primitives-core = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-pallet-aura-ext = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-pallet-dmp-queue = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-pallet-parachain-system = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-pallet-xcm = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-pallet-xcmp-queue = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-primitives-core = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
 cumulus-primitives-parachain-inherent = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-primitives-timestamp = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
-cumulus-primitives-utility = { default-features = false , git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-primitives-timestamp = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
+cumulus-primitives-utility = { default-features = false, git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
 cumulus-relay-chain-inprocess-interface = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
 cumulus-relay-chain-interface = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
 cumulus-relay-chain-minimal-node = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.42" }
@@ -123,9 +124,9 @@
 sc-cli = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 sc-client-api = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 sc-consensus = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
+sc-consensus-grandpa = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 sc-consensus-manual-seal = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 sc-executor = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
-sc-consensus-grandpa = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 sc-network = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 sc-network-sync = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 sc-rpc = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
@@ -177,11 +178,11 @@
 try-runtime-cli = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" }
 
 # ORML
-orml-tokens = { default-features = false , git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
-orml-traits = { default-features = false , git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
-orml-vesting = { default-features = false , git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
-orml-xtokens = { default-features = false , git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
-orml-xcm-support = { default-features = false , git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
+orml-tokens = { default-features = false, git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
+orml-traits = { default-features = false, git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
+orml-vesting = { default-features = false, git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
+orml-xcm-support = { default-features = false, git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
+orml-xtokens = { default-features = false, git = "https://github.com/uniquenetwork/open-runtime-module-library", branch = "polkadot-v0.9.42" }
 
 # Other
 derivative = { version = "2.2.0", features = ["use_core"] }
@@ -194,4 +195,3 @@
 num_enum = { version = "0.5.3", default-features = false }
 serde = { default-features = false, features = ['derive'], version = "1.0.136" }
 smallvec = "1.6.1"
-
modifiedMakefilediffbeforeafterboth
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,9 @@
 	@echo "  bench-evm-migration"
 	@echo "  bench-unique"
 
+NATIVE_FUNGIBLE_EVM_STUBS=./pallets/balances-adapter/src/stubs
+NATIVE_FUNGIBLE_EVM_ABI=./tests/src/eth/abi/nativeFungible.json
+
 FUNGIBLE_EVM_STUBS=./pallets/fungible/src/stubs
 FUNGIBLE_EVM_ABI=./tests/src/eth/abi/fungible.json
 
@@ -27,6 +30,10 @@
 .PHONY: regenerate_solidity
 regenerate_solidity: UniqueFungible.sol UniqueNFT.sol UniqueRefungible.sol UniqueRefungibleToken.sol ContractHelpers.sol CollectionHelpers.sol
 
+UniqueNativeFungible.sol:
+	PACKAGE=pallet-balances-adapter NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
+	PACKAGE=pallet-balances-adapter NAME=erc::gen_impl OUTPUT=$(NATIVE_FUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
+	
 UniqueFungible.sol:
 	PACKAGE=pallet-fungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
 	PACKAGE=pallet-fungible NAME=erc::gen_impl OUTPUT=$(FUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
@@ -51,6 +58,10 @@
 	PACKAGE=pallet-unique NAME=eth::collection_helper_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
 	PACKAGE=pallet-unique NAME=eth::collection_helper_impl OUTPUT=$(COLLECTION_HELPER_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
 
+UniqueNativeFungible: UniqueNativeFungible.sol
+	INPUT=$(NATIVE_FUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NATIVE_FUNGIBLE_EVM_STUBS)/UniqueNativeFungible.raw ./.maintain/scripts/compile_stub.sh
+	INPUT=$(NATIVE_FUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NATIVE_FUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
+	
 UniqueFungible: UniqueFungible.sol
 	INPUT=$(FUNGIBLE_EVM_STUBS)/$< OUTPUT=$(FUNGIBLE_EVM_STUBS)/UniqueFungible.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(FUNGIBLE_EVM_STUBS)/$< OUTPUT=$(FUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
modifiednode/cli/src/chain_spec.rsdiffbeforeafterboth
--- a/node/cli/src/chain_spec.rs
+++ b/node/cli/src/chain_spec.rs
@@ -267,7 +267,7 @@
 pub fn development_config() -> DefaultChainSpec {
 	let mut properties = Map::new();
 	properties.insert("tokenSymbol".into(), default_runtime::TOKEN_SYMBOL.into());
-	properties.insert("tokenDecimals".into(), 18.into());
+	properties.insert("tokenDecimals".into(), default_runtime::DECIMALS.into());
 	properties.insert(
 		"ss58Format".into(),
 		default_runtime::SS58Prefix::get().into(),
@@ -341,7 +341,7 @@
 pub fn local_testnet_config() -> DefaultChainSpec {
 	let mut properties = Map::new();
 	properties.insert("tokenSymbol".into(), default_runtime::TOKEN_SYMBOL.into());
-	properties.insert("tokenDecimals".into(), 18.into());
+	properties.insert("tokenDecimals".into(), default_runtime::DECIMALS.into());
 	properties.insert(
 		"ss58Format".into(),
 		default_runtime::SS58Prefix::get().into(),
addedpallets/balances-adapter/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/pallets/balances-adapter/Cargo.toml
@@ -0,0 +1,45 @@
+[package]
+edition = "2021"
+license = "GPLv3"
+name = "pallet-balances-adapter"
+version = "0.1.0"
+
+[dependencies]
+# Substrate
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+pallet-balances = { workspace = true }
+pallet-structure = { workspace = true }
+sp-core = { workspace = true }
+sp-runtime = { workspace = true }
+sp-std = { workspace = true }
+
+#Parity
+codec = { workspace = true, package = "parity-scale-codec" }
+scale-info = { workspace = true }
+
+# Unique
+evm-coder = { workspace = true }
+pallet-evm = { workspace = true }
+
+#Local
+pallet-common = { workspace = true }
+pallet-evm-coder-substrate = { workspace = true }
+pallet-evm-transaction-payment = { workspace = true }
+up-data-structs = { workspace = true, features = ['serde1'] }
+
+[features]
+default = ["std"]
+std = [
+	"evm-coder/std",
+	"frame-support/std",
+	"frame-system/std",
+	"pallet-balances/std",
+	"pallet-common/std",
+	"pallet-evm-coder-substrate/std",
+	"pallet-evm/std",
+	"sp-core/std",
+	"sp-std/std",
+]
+stubgen = ["evm-coder/stubgen", "pallet-common/stubgen"]
+try-runtime = ["frame-support/try-runtime"]
addedpallets/balances-adapter/src/common.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/balances-adapter/src/common.rs
@@ -0,0 +1,359 @@
+use alloc::{vec, vec::Vec};
+use core::marker::PhantomData;
+use crate::{Config, NativeFungibleHandle, Pallet};
+use frame_support::{fail, weights::Weight};
+use pallet_balances::{weights::SubstrateWeight as BalancesWeight, WeightInfo};
+use pallet_common::{CommonCollectionOperations, CommonWeightInfo};
+use up_data_structs::TokenId;
+
+pub struct CommonWeights<T: Config>(PhantomData<T>);
+
+// All implementations with `Weight::default` used in methods that return error `UnsupportedOperation`.
+impl<T: Config> CommonWeightInfo<T::CrossAccountId> for CommonWeights<T> {
+	fn create_multiple_items(_amount: &[up_data_structs::CreateItemData]) -> Weight {
+		Weight::default()
+	}
+
+	fn create_multiple_items_ex(
+		_cost: &up_data_structs::CreateItemExData<T::CrossAccountId>,
+	) -> Weight {
+		Weight::default()
+	}
+
+	fn burn_item() -> Weight {
+		Weight::default()
+	}
+
+	fn set_collection_properties(_amount: u32) -> Weight {
+		Weight::default()
+	}
+
+	fn delete_collection_properties(_amount: u32) -> Weight {
+		Weight::default()
+	}
+
+	fn set_token_properties(_amount: u32) -> Weight {
+		Weight::default()
+	}
+
+	fn delete_token_properties(_amount: u32) -> Weight {
+		Weight::default()
+	}
+
+	fn set_token_property_permissions(_amount: u32) -> Weight {
+		Weight::default()
+	}
+
+	fn transfer() -> Weight {
+		<BalancesWeight<T> as WeightInfo>::transfer_allow_death()
+	}
+
+	fn approve() -> Weight {
+		Weight::default()
+	}
+
+	fn approve_from() -> Weight {
+		Weight::default()
+	}
+
+	fn transfer_from() -> Weight {
+		<BalancesWeight<T> as WeightInfo>::transfer_allow_death()
+	}
+
+	fn burn_from() -> Weight {
+		Weight::default()
+	}
+
+	fn burn_recursively_self_raw() -> Weight {
+		Weight::default()
+	}
+
+	fn burn_recursively_breadth_raw(_amount: u32) -> Weight {
+		Weight::default()
+	}
+
+	fn token_owner() -> Weight {
+		Weight::default()
+	}
+
+	fn set_allowance_for_all() -> Weight {
+		Weight::default()
+	}
+
+	fn force_repair_item() -> Weight {
+		Weight::default()
+	}
+}
+
+/// Implementation of `CommonCollectionOperations` for `FungibleHandle`. It wraps FungibleHandle Pallet
+/// methods and adds weight info.
+impl<T: Config> CommonCollectionOperations<T> for NativeFungibleHandle<T> {
+	fn create_item(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_to: <T>::CrossAccountId,
+		_data: up_data_structs::CreateItemData,
+		_nesting_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn create_multiple_items(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_to: <T>::CrossAccountId,
+		_data: Vec<up_data_structs::CreateItemData>,
+		_nesting_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn create_multiple_items_ex(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_data: up_data_structs::CreateItemExData<<T>::CrossAccountId>,
+		_nesting_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn burn_item(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_token: TokenId,
+		_amount: u128,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn burn_item_recursively(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_token: TokenId,
+		_self_budget: &dyn up_data_structs::budget::Budget,
+		_breadth_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn set_collection_properties(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_properties: Vec<up_data_structs::Property>,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn delete_collection_properties(
+		&self,
+		_sender: &<T>::CrossAccountId,
+		_property_keys: Vec<up_data_structs::PropertyKey>,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn set_token_properties(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_token_id: TokenId,
+		_properties: Vec<up_data_structs::Property>,
+		_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn delete_token_properties(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_token_id: TokenId,
+		_property_keys: Vec<up_data_structs::PropertyKey>,
+		_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn set_token_property_permissions(
+		&self,
+		_sender: &<T>::CrossAccountId,
+		_property_permissions: Vec<up_data_structs::PropertyKeyPermission>,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn transfer(
+		&self,
+		sender: <T>::CrossAccountId,
+		to: <T>::CrossAccountId,
+		_token: TokenId,
+		amount: u128,
+		budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		<Pallet<T>>::transfer(self, &sender, &to, amount, budget)
+	}
+
+	fn approve(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_spender: <T>::CrossAccountId,
+		_token: TokenId,
+		_amount: u128,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn approve_from(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_from: <T>::CrossAccountId,
+		_to: <T>::CrossAccountId,
+		_token: TokenId,
+		_amount: u128,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn transfer_from(
+		&self,
+		sender: <T>::CrossAccountId,
+		from: <T>::CrossAccountId,
+		to: <T>::CrossAccountId,
+		_token: TokenId,
+		amount: u128,
+		budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		<Pallet<T>>::transfer_from(self, &sender, &from, &to, amount, budget)
+	}
+
+	fn burn_from(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_from: <T>::CrossAccountId,
+		_token: TokenId,
+		_amount: u128,
+		_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn check_nesting(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_from: (up_data_structs::CollectionId, TokenId),
+		_under: TokenId,
+		_budget: &dyn up_data_structs::budget::Budget,
+	) -> frame_support::sp_runtime::DispatchResult {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn nest(&self, _under: TokenId, _to_nest: (up_data_structs::CollectionId, TokenId)) {}
+
+	fn unnest(&self, _under: TokenId, _to_nest: (up_data_structs::CollectionId, TokenId)) {}
+
+	fn account_tokens(&self, account: <T>::CrossAccountId) -> Vec<TokenId> {
+		let balance = <Pallet<T>>::total_balance(&account);
+		if balance != 0 {
+			vec![TokenId::default()]
+		} else {
+			vec![]
+		}
+	}
+
+	fn collection_tokens(&self) -> Vec<TokenId> {
+		vec![TokenId::default()]
+	}
+
+	fn token_exists(&self, token: TokenId) -> bool {
+		token == TokenId::default()
+	}
+
+	fn last_token_id(&self) -> TokenId {
+		TokenId::default()
+	}
+
+	fn token_owner(
+		&self,
+		_token: TokenId,
+	) -> Result<<T>::CrossAccountId, up_data_structs::TokenOwnerError> {
+		Err(up_data_structs::TokenOwnerError::MultipleOwners)
+	}
+
+	fn token_owners(&self, _token: TokenId) -> Vec<<T>::CrossAccountId> {
+		vec![]
+	}
+
+	fn token_property(
+		&self,
+		_token_id: TokenId,
+		_key: &up_data_structs::PropertyKey,
+	) -> Option<up_data_structs::PropertyValue> {
+		None
+	}
+
+	fn token_properties(
+		&self,
+		_token: TokenId,
+		_keys: Option<Vec<up_data_structs::PropertyKey>>,
+	) -> Vec<up_data_structs::Property> {
+		vec![]
+	}
+
+	fn total_supply(&self) -> u32 {
+		1
+	}
+
+	fn account_balance(&self, account: T::CrossAccountId) -> u32 {
+		let balance = <Pallet<T>>::balance_of(&account);
+		(balance != 0).into()
+	}
+
+	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128 {
+		if token != TokenId::default() {
+			return 0;
+		}
+		<Pallet<T>>::balance_of(&account)
+	}
+
+	fn total_pieces(&self, token: TokenId) -> Option<u128> {
+		if token != TokenId::default() {
+			return None;
+		}
+		Some(<Pallet<T>>::total_issuance())
+	}
+
+	fn allowance(
+		&self,
+		_sender: <T>::CrossAccountId,
+		_spender: <T>::CrossAccountId,
+		_token: TokenId,
+	) -> u128 {
+		0
+	}
+
+	fn refungible_extensions(&self) -> Option<&dyn pallet_common::RefungibleExtensions<T>> {
+		None
+	}
+
+	fn set_allowance_for_all(
+		&self,
+		_owner: <T>::CrossAccountId,
+		_operator: <T>::CrossAccountId,
+		_approve: bool,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+
+	fn allowance_for_all(
+		&self,
+		_owner: <T>::CrossAccountId,
+		_operator: <T>::CrossAccountId,
+	) -> bool {
+		false
+	}
+
+	fn repair_item(
+		&self,
+		_token: TokenId,
+	) -> frame_support::pallet_prelude::DispatchResultWithPostInfo {
+		fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+	}
+}
addedpallets/balances-adapter/src/erc.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/balances-adapter/src/erc.rs
@@ -0,0 +1,165 @@
+use crate::{Config, NativeFungibleHandle, Pallet, SelfWeightOf};
+use evm_coder::{abi::AbiType, generate_stubgen, solidity_interface, types::*};
+use pallet_balances::WeightInfo;
+use pallet_common::{
+	erc::{CommonEvmHandler, CrossAccountId, PrecompileHandle, PrecompileResult},
+	eth::CrossAddress,
+};
+use pallet_evm_coder_substrate::{
+	call, dispatch_to_evm,
+	execution::{PreDispatch, Result},
+	frontier_contract, WithRecorder,
+};
+use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
+use sp_core::{U256, Get};
+
+frontier_contract! {
+	macro_rules! NativeFungibleHandle_result {...}
+	impl<T: Config> Contract for NativeFungibleHandle<T> {...}
+}
+
+#[solidity_interface(name = ERC20, enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x942e8b22)]
+impl<T: Config> NativeFungibleHandle<T> {
+	fn allowance(&self, _owner: Address, _spender: Address) -> Result<U256> {
+		Ok(U256::zero())
+	}
+
+	fn approve(&mut self, _caller: Caller, _spender: Address, _amount: U256) -> Result<bool> {
+		Err("Approve not supported".into())
+	}
+
+	fn balance_of(&self, owner: Address) -> Result<U256> {
+		self.consume_store_reads(1)?;
+		let owner = T::CrossAccountId::from_eth(owner);
+		let balance = <Pallet<T>>::balance_of(&owner);
+		Ok(balance.into())
+	}
+
+	fn decimals(&self) -> Result<u8> {
+		Ok(T::Decimals::get())
+	}
+
+	fn name(&self) -> Result<String> {
+		Ok(T::Name::get())
+	}
+
+	fn symbol(&self) -> Result<String> {
+		Ok(T::Symbol::get())
+	}
+
+	fn total_supply(&self) -> Result<U256> {
+		self.consume_store_reads(1)?;
+		Ok(<Pallet<T>>::total_issuance().into())
+	}
+
+	#[weight(<SelfWeightOf<T>>::transfer_allow_death())]
+	fn transfer(&mut self, caller: Caller, to: Address, amount: U256) -> 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, amount, &budget)
+			.map_err(|e| dispatch_to_evm::<T>(e.error))?;
+		Ok(true)
+	}
+
+	#[weight(<SelfWeightOf<T>>::transfer_allow_death())]
+	fn transfer_from(
+		&mut self,
+		caller: Caller,
+		from: Address,
+		to: Address,
+		amount: U256,
+	) -> 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, amount, &budget)
+			.map_err(|e| dispatch_to_evm::<T>(e.error))?;
+		Ok(true)
+	}
+}
+
+#[solidity_interface(name = ERC20UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))]
+impl<T: Config> NativeFungibleHandle<T>
+where
+	T::AccountId: From<[u8; 32]>,
+{
+	fn balance_of_cross(&self, owner: CrossAddress) -> Result<U256> {
+		self.consume_store_reads(1)?;
+		let owner = owner.into_sub_cross_account::<T>()?;
+		let balance = <Pallet<T>>::balance_of(&owner);
+		Ok(balance.into())
+	}
+
+	#[weight(<SelfWeightOf<T>>::transfer_allow_death())]
+	fn transfer_cross(&mut self, caller: Caller, to: CrossAddress, amount: U256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = to.into_sub_cross_account::<T>()?;
+		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, amount, &budget)
+			.map_err(|e| dispatch_to_evm::<T>(e.error))?;
+
+		Ok(true)
+	}
+
+	#[weight(<SelfWeightOf<T>>::transfer_allow_death())]
+	fn transfer_from_cross(
+		&mut self,
+		caller: Caller,
+		from: CrossAddress,
+		to: CrossAddress,
+		amount: U256,
+	) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = from.into_sub_cross_account::<T>()?;
+		let to = to.into_sub_cross_account::<T>()?;
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+		if from != caller {
+			return Err("no permission".into());
+		}
+
+		let budget = self
+			.recorder()
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::transfer_from(self, &caller, &from, &to, amount, &budget)
+			.map_err(|e| dispatch_to_evm::<T>(e.error))?;
+
+		Ok(true)
+	}
+}
+
+#[solidity_interface(
+	name = UniqueNativeFungible,
+	is(ERC20, ERC20UniqueExtensions),
+	enum(derive(PreDispatch))
+)]
+impl<T: Config> NativeFungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}
+
+generate_stubgen!(gen_impl, UniqueNativeFungibleCall<()>, true);
+generate_stubgen!(gen_iface, UniqueNativeFungibleCall<()>, false);
+
+impl<T: Config> CommonEvmHandler for NativeFungibleHandle<T>
+where
+	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,
+{
+	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNativeFungible.raw");
+
+	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
+		call::<T, UniqueNativeFungibleCall<T>, _, _>(handle, self)
+	}
+}
addedpallets/balances-adapter/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/pallets/balances-adapter/src/lib.rs
@@ -0,0 +1,206 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+extern crate alloc;
+use core::ops::Deref;
+
+use frame_support::sp_runtime::DispatchResult;
+use pallet_evm_coder_substrate::{WithRecorder, SubstrateRecorder};
+pub use pallet::*;
+
+pub mod common;
+pub mod erc;
+
+pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;
+
+/// Handle for native fungible collection
+pub struct NativeFungibleHandle<T: Config>(SubstrateRecorder<T>);
+impl<T: Config> NativeFungibleHandle<T> {
+	/// Creates a handle
+	pub fn new() -> NativeFungibleHandle<T> {
+		Self(SubstrateRecorder::new(u64::MAX))
+	}
+
+	/// Creates a handle
+	pub fn new_with_gas_limit(gas_limit: u64) -> NativeFungibleHandle<T> {
+		Self(SubstrateRecorder::new(gas_limit))
+	}
+
+	/// Check if the collection is internal
+	pub fn check_is_internal(&self) -> DispatchResult {
+		Ok(())
+	}
+}
+
+impl<T: Config> WithRecorder<T> for NativeFungibleHandle<T> {
+	fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		&self.0
+	}
+	fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {
+		self.0
+	}
+}
+
+impl<T: Config> Deref for NativeFungibleHandle<T> {
+	type Target = SubstrateRecorder<T>;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+#[frame_support::pallet]
+pub mod pallet {
+	use super::*;
+	use alloc::string::String;
+	use frame_support::{
+		dispatch::PostDispatchInfo,
+		ensure,
+		pallet_prelude::{DispatchResultWithPostInfo, Pays},
+		traits::{
+			Get,
+			fungible::{Inspect, Mutate},
+			tokens::Preservation,
+		},
+	};
+	use pallet_balances::WeightInfo;
+	use pallet_common::{erc::CrossAccountId, Error as CommonError, Pallet as PalletCommon};
+	use pallet_structure::Pallet as PalletStructure;
+	use sp_core::U256;
+	use sp_runtime::DispatchError;
+	use up_data_structs::{budget::Budget, mapping::TokenAddressMapping};
+
+	#[pallet::config]
+	pub trait Config:
+		frame_system::Config
+		+ pallet_evm_coder_substrate::Config
+		+ pallet_common::Config
+		+ pallet_structure::Config
+	{
+		/// Inspect from `pallet_balances`
+		type Inspect: frame_support::traits::tokens::fungible::Inspect<
+			Self::AccountId,
+			Balance = Self::CurrencyBalance,
+		>;
+
+		/// Mutate from `pallet_balances`
+		type Mutate: frame_support::traits::tokens::fungible::Mutate<
+			Self::AccountId,
+			Balance = Self::CurrencyBalance,
+		>;
+
+		/// Balance type of chain
+		type CurrencyBalance: Into<U256> + TryFrom<U256> + TryFrom<u128> + Into<u128>;
+
+		/// Decimals of balance
+		type Decimals: Get<u8>;
+		/// Collection name
+		type Name: Get<String>;
+		/// Collection symbol
+		type Symbol: Get<String>;
+
+		/// Weight information
+		type WeightInfo: WeightInfo;
+	}
+	#[pallet::pallet]
+	pub struct Pallet<T>(_);
+
+	impl<T: Config> Pallet<T> {
+		pub fn balance_of(account: &T::CrossAccountId) -> u128 {
+			T::Inspect::balance(account.as_sub()).into()
+		}
+
+		pub fn total_balance(account: &T::CrossAccountId) -> u128 {
+			T::Inspect::total_balance(account.as_sub()).into()
+		}
+
+		pub fn total_issuance() -> u128 {
+			T::Inspect::total_issuance().into()
+		}
+
+		/// 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.
+		///
+		/// - `spender`: CrossAccountId who has the allowance rights.
+		/// - `from`: The owner of the tokens who sets the allowance.
+		/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.
+		fn check_allowed(
+			spender: &T::CrossAccountId,
+			from: &T::CrossAccountId,
+			nesting_budget: &dyn Budget,
+		) -> Result<u128, DispatchError> {
+			if let Some((collection_id, token_id)) =
+				T::CrossTokenAddressMapping::address_to_token(from)
+			{
+				ensure!(
+					<PalletStructure<T>>::check_indirectly_owned(
+						spender.clone(),
+						collection_id,
+						token_id,
+						None,
+						nesting_budget
+					)?,
+					<CommonError<T>>::ApprovedValueTooLow,
+				);
+			} else if !spender.conv_eq(from) {
+				return Ok(0);
+			}
+
+			Ok(Self::balance_of(from))
+		}
+
+		/// Transfers the specified amount of tokens. Will check that
+		/// the transfer is allowed for the token.
+		///
+		/// - `collection`: Collection that contains the token.
+		/// - `from`: Owner of tokens to transfer.
+		/// - `to`: Recepient of transfered tokens.
+		/// - `amount`: Amount of tokens to transfer.
+		/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.
+		pub fn transfer(
+			_collection: &NativeFungibleHandle<T>,
+			from: &T::CrossAccountId,
+			to: &T::CrossAccountId,
+			amount: u128,
+			_nesting_budget: &dyn Budget,
+		) -> DispatchResultWithPostInfo {
+			<PalletCommon<T>>::ensure_correct_receiver(to)?;
+
+			if from != to && amount != 0 {
+				let amount = amount
+					.try_into()
+					.map_err(|_| sp_runtime::ArithmeticError::Overflow)?;
+				T::Mutate::transfer(from.as_sub(), to.as_sub(), amount, Preservation::Expendable)?;
+			};
+
+			Ok(PostDispatchInfo {
+				actual_weight: Some(<SelfWeightOf<T>>::transfer_allow_death()),
+				pays_fee: Pays::Yes,
+			})
+		}
+
+		/// Transfer NFT token from one account to another.
+		///
+		/// Same as the [`Self::transfer`] but spender doesn't needs to be the owner of the token.
+		/// The owner should set allowance for the spender to transfer token.
+		///
+		/// - `collection`: Collection that contains the token.
+		/// - `spender`: Account that spend the money.
+		/// - `from`: Owner of tokens to transfer.
+		/// - `to`: Recepient of transfered tokens.
+		/// - `amount`: Amount of tokens to transfer.
+		/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.
+		pub fn transfer_from(
+			collection: &NativeFungibleHandle<T>,
+			spender: &T::CrossAccountId,
+			from: &T::CrossAccountId,
+			to: &T::CrossAccountId,
+			amount: u128,
+			nesting_budget: &dyn Budget,
+		) -> DispatchResultWithPostInfo {
+			let allowance = Self::check_allowed(spender, from, nesting_budget)?;
+			if allowance < amount {
+				return Err(<CommonError<T>>::ApprovedValueTooLow.into());
+			}
+			Self::transfer(collection, from, to, amount, nesting_budget)
+		}
+	}
+}
addedpallets/balances-adapter/src/stubs/UniqueNativeFungible.rawdiffbeforeafterboth

binary blob — no preview

addedpallets/balances-adapter/src/stubs/UniqueNativeFungible.soldiffbeforeafterboth
--- /dev/null
+++ b/pallets/balances-adapter/src/stubs/UniqueNativeFungible.sol
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+/// @dev 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;
+	}
+}
+
+/// @dev the ERC-165 identifier for this interface is 0x1313556c
+contract ERC20UniqueExtensions is Dummy, ERC165 {
+	/// @dev EVM selector for this function is: 0xec069398,
+	///  or in textual repr: balanceOfCross((address,uint256))
+	function balanceOfCross(CrossAddress memory owner) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		dummy;
+		return 0;
+	}
+
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(CrossAddress memory to, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	/// @dev EVM selector for this function is: 0xd5cf430b,
+	///  or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256)
+	function transferFromCross(
+		CrossAddress memory from,
+		CrossAddress memory to,
+		uint256 amount
+	) public returns (bool) {
+		require(false, stub_error);
+		from;
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+}
+
+/// Cross account struct
+struct CrossAddress {
+	address eth;
+	uint256 sub;
+}
+
+/// @dev inlined interface
+contract ERC20Events {
+	event Transfer(address indexed from, address indexed to, uint256 value);
+	event Approval(address indexed owner, address indexed spender, uint256 value);
+}
+
+/// @dev the ERC-165 identifier for this interface is 0x942e8b22
+contract ERC20 is Dummy, ERC165, ERC20Events {
+	/// @dev EVM selector for this function is: 0xdd62ed3e,
+	///  or in textual repr: allowance(address,address)
+	function allowance(address owner, address spender) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		spender;
+		dummy;
+		return 0;
+	}
+
+	/// @dev EVM selector for this function is: 0x095ea7b3,
+	///  or in textual repr: approve(address,uint256)
+	function approve(address spender, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		spender;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	/// @dev EVM selector for this function is: 0x70a08231,
+	///  or in textual repr: balanceOf(address)
+	function balanceOf(address owner) public view returns (uint256) {
+		require(false, stub_error);
+		owner;
+		dummy;
+		return 0;
+	}
+
+	/// @dev EVM selector for this function is: 0x313ce567,
+	///  or in textual repr: decimals()
+	function decimals() public view returns (uint8) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	/// @dev EVM selector for this function is: 0x06fdde03,
+	///  or in textual repr: name()
+	function name() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	/// @dev EVM selector for this function is: 0x95d89b41,
+	///  or in textual repr: symbol()
+	function symbol() public view returns (string memory) {
+		require(false, stub_error);
+		dummy;
+		return "";
+	}
+
+	/// @dev EVM selector for this function is: 0x18160ddd,
+	///  or in textual repr: totalSupply()
+	function totalSupply() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	/// @dev EVM selector for this function is: 0xa9059cbb,
+	///  or in textual repr: transfer(address,uint256)
+	function transfer(address to, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
+	/// @dev EVM selector for this function is: 0x23b872dd,
+	///  or in textual repr: transferFrom(address,address,uint256)
+	function transferFrom(
+		address from,
+		address to,
+		uint256 amount
+	) public returns (bool) {
+		require(false, stub_error);
+		from;
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+}
+
+contract UniqueNativeFungible is Dummy, ERC165, ERC20, ERC20UniqueExtensions {}
modifiedpallets/common/src/dispatch.rsdiffbeforeafterboth
--- a/pallets/common/src/dispatch.rs
+++ b/pallets/common/src/dispatch.rs
@@ -11,7 +11,7 @@
 use sp_runtime::DispatchError;
 use up_data_structs::{CollectionId, CreateCollectionData, CollectionFlags};
 
-use crate::{pallet::Config, CommonCollectionOperations, CollectionHandle};
+use crate::{pallet::Config, CommonCollectionOperations};
 
 // TODO: move to benchmarking
 /// Price of [`dispatch_tx`] call with noop `call` argument
@@ -34,16 +34,11 @@
 	collection: CollectionId,
 	call: C,
 ) -> DispatchResultWithPostInfo {
-	let handle =
-		CollectionHandle::try_get(collection).map_err(|error| DispatchErrorWithPostInfo {
-			post_info: PostDispatchInfo {
-				actual_weight: Some(dispatch_weight::<T>()),
-				pays_fee: Pays::Yes,
-			},
-			error,
-		})?;
-	handle
-		.check_is_internal()
+	let dispatched = T::CollectionDispatch::dispatch(collection)
+		.and_then(|dispatched| {
+			dispatched.check_is_internal()?;
+			Ok(dispatched)
+		})
 		.map_err(|error| DispatchErrorWithPostInfo {
 			post_info: PostDispatchInfo {
 				actual_weight: Some(dispatch_weight::<T>()),
@@ -51,7 +46,6 @@
 			},
 			error,
 		})?;
-	let dispatched = T::CollectionDispatch::dispatch(handle);
 	let mut result = call(dispatched.as_dyn());
 	match &mut result {
 		Ok(PostDispatchInfo {
@@ -72,6 +66,9 @@
 
 /// Interface for working with different collections through the dispatcher.
 pub trait CollectionDispatch<T: Config> {
+	/// Check if the collection is internal.
+	fn check_is_internal(&self) -> DispatchResult;
+
 	/// 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.
@@ -87,15 +84,14 @@
 	///
 	/// * `sender` - The owner of the collection.
 	/// * `handle` - Collection handle.
-	fn destroy(sender: T::CrossAccountId, handle: CollectionHandle<T>) -> DispatchResult;
+	fn destroy(sender: T::CrossAccountId, collection_id: CollectionId) -> 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>;
+	fn dispatch(collection_id: CollectionId) -> Result<Self, DispatchError>
+	where
+		Self: Sized;
 
 	/// Get the implementation of [`CommonCollectionOperations`].
 	fn as_dyn(&self) -> &dyn CommonCollectionOperations<T>;
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
before · pallets/common/src/lib.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Common pallet18//!19//! The Common pallet provides functionality for handling collections.20//!21//! ## Overview22//!23//! The Common pallet provides an interface for common collection operations for different collection types24//! (see [CommonCollectionOperations]), as well as a generic dispatcher for these, see [dispatch] module.25//! It also provides this functionality to EVM, see [erc] and [eth] modules.26//!27//! The Common pallet provides functions for:28//!29//! - Setting and approving collection sponsor.30//! - Get\set\delete allow list.31//! - Get\set\delete collection properties.32//! - Get\set\delete collection property permissions.33//! - Get\set\delete token property permissions.34//! - Get\set\delete collection administrators.35//! - Checking access permissions.36//!37//! ### Terminology38//! **Collection sponsor** - For the collection, you can set a sponsor, at whose expense it will39//! be possible to mint tokens.40//!41//! **Allow list** - List of users who have the right to minting tokens.42//!43//! **Collection properties** - Collection properties are simply key-value stores where various44//! metadata can be placed.45//!46//! **Permissions on token properties** - For each property in the token can be set permission47//! to change, see [`PropertyPermission`].48//!49//! **Collection administrator** - For a collection, you can set administrators who have the right50//! to most actions on the collection.5152#![warn(missing_docs)]53#![cfg_attr(not(feature = "std"), no_std)]54extern crate alloc;5556use core::{57	ops::{Deref, DerefMut},58	slice::from_ref,59};60use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};61use sp_std::vec::Vec;62use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};63use evm_coder::ToLog;64use frame_support::{65	dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},66	ensure,67	traits::{68		Get,69		fungible::{Balanced, Debt, Inspect},70		tokens::{Imbalance, Precision, Preservation},71	},72	dispatch::Pays,73	transactional, fail,74};75use pallet_evm::GasWeightMapping;76use up_data_structs::{77	AccessMode, COLLECTION_NUMBER_LIMIT, Collection, RpcCollection, CollectionFlags,78	RpcCollectionFlags, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH,79	COLLECTION_ADMINS_LIMIT, TokenId, TokenChild, CollectionStats, MAX_TOKEN_OWNERSHIP,80	CollectionMode, NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,81	REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,82	CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit, budget::Budget,83	PhantomType, Property, CollectionProperties as CollectionPropertiesT, TokenProperties,84	PropertiesPermissionMap, PropertyKey, PropertyValue, PropertyPermission, PropertiesError,85	TokenOwnerError, PropertyKeyPermission, TokenData, TrySetProperty, PropertyScope,86	CollectionPermissions,87};88use up_pov_estimate_rpc::PovInfo;8990pub use pallet::*;91use sp_core::H160;92use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, traits::Zero};9394#[cfg(feature = "runtime-benchmarks")]95pub mod benchmarking;96pub mod dispatch;97pub mod erc;98pub mod eth;99pub mod helpers;100#[allow(missing_docs)]101pub mod weights;102/// Weight info.103pub type SelfWeightOf<T> = <T as Config>::WeightInfo;104105/// Collection handle contains information about collection data and id.106/// Also provides functionality to count consumed gas.107///108/// CollectionHandle is used as a generic wrapper for collections of all types.109/// It allows to perform common operations and queries on any collection type,110/// both completely general for all, as well as their respective implementations of [`CommonCollectionOperations`].111#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]112pub struct CollectionHandle<T: Config> {113	/// Collection id114	pub id: CollectionId,115	collection: Collection<T::AccountId>,116	/// Substrate recorder for counting consumed gas117	pub recorder: SubstrateRecorder<T>,118}119120impl<T: Config> WithRecorder<T> for CollectionHandle<T> {121	fn recorder(&self) -> &SubstrateRecorder<T> {122		&self.recorder123	}124	fn into_recorder(self) -> SubstrateRecorder<T> {125		self.recorder126	}127}128129impl<T: Config> CollectionHandle<T> {130	/// Same as [CollectionHandle::new] but with an explicit gas limit.131	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {132		<CollectionById<T>>::get(id).map(|collection| Self {133			id,134			collection,135			recorder: SubstrateRecorder::new(gas_limit),136		})137	}138139	/// Same as [CollectionHandle::new] but with an existed [`SubstrateRecorder`].140	pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {141		<CollectionById<T>>::get(id).map(|collection| Self {142			id,143			collection,144			recorder,145		})146	}147148	/// Retrives collection data from storage and creates collection handle with default parameters.149	/// If collection not found return `None`150	pub fn new(id: CollectionId) -> Option<Self> {151		Self::new_with_gas_limit(id, u64::MAX)152	}153154	/// Same as [`CollectionHandle::new`] but if collection not found [CollectionNotFound](Error::CollectionNotFound) returned.155	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {156		Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)157	}158159	/// Consume gas for reading.160	pub fn consume_store_reads(161		&self,162		reads: u64,163	) -> pallet_evm_coder_substrate::execution::Result<()> {164		self.recorder165			.consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_parts(166				<T as frame_system::Config>::DbWeight::get()167					.read168					.saturating_mul(reads),169				// TODO: measure proof170				0,171			)))172	}173174	/// Consume gas for writing.175	pub fn consume_store_writes(176		&self,177		writes: u64,178	) -> pallet_evm_coder_substrate::execution::Result<()> {179		self.recorder180			.consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_parts(181				<T as frame_system::Config>::DbWeight::get()182					.write183					.saturating_mul(writes),184				// TODO: measure proof185				0,186			)))187	}188189	/// Consume gas for reading and writing.190	pub fn consume_store_reads_and_writes(191		&self,192		reads: u64,193		writes: u64,194	) -> pallet_evm_coder_substrate::execution::Result<()> {195		let weight = <T as frame_system::Config>::DbWeight::get();196		let reads = weight.read.saturating_mul(reads);197		let writes = weight.read.saturating_mul(writes);198		self.recorder199			.consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_parts(200				reads.saturating_add(writes),201				// TODO: measure proof202				0,203			)))204	}205206	/// Save collection to storage.207	pub fn save(&self) -> DispatchResult {208		<CollectionById<T>>::insert(self.id, &self.collection);209		Ok(())210	}211212	/// Set collection sponsor.213	///214	/// Unique collections allows sponsoring for certain actions.215	/// This method allows you to set the sponsor of the collection.216	/// In order for sponsorship to become active, it must be confirmed through [`Self::confirm_sponsorship`].217	pub fn set_sponsor(218		&mut self,219		sender: &T::CrossAccountId,220		sponsor: T::AccountId,221	) -> DispatchResult {222		self.check_is_internal()?;223		self.check_is_owner_or_admin(sender)?;224225		self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor.clone());226227		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(self.id, sponsor));228		<PalletEvm<T>>::deposit_log(229			erc::CollectionHelpersEvents::CollectionChanged {230				collection_id: eth::collection_id_to_address(self.id),231			}232			.to_log(T::ContractAddress::get()),233		);234235		self.save()236	}237238	/// Force set `sponsor`.239	///240	/// Differs from [`set_sponsor`][`Self::set_sponsor`] in that confirmation241	/// from the `sponsor` is not required.242	///243	/// # Arguments244	///245	/// * `sender`: Caller's account.246	/// * `sponsor`: ID of the account of the sponsor-to-be.247	pub fn force_set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {248		self.check_is_internal()?;249250		self.collection.sponsorship = SponsorshipState::Confirmed(sponsor.clone());251252		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(self.id, sponsor.clone()));253		<Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(self.id, sponsor));254		<PalletEvm<T>>::deposit_log(255			erc::CollectionHelpersEvents::CollectionChanged {256				collection_id: eth::collection_id_to_address(self.id),257			}258			.to_log(T::ContractAddress::get()),259		);260261		self.save()262	}263264	/// Confirm sponsorship265	///266	/// In order for the sponsorship to become active, the user set as the sponsor must confirm their participation.267	/// Before confirming sponsorship, the user must be specified as the sponsor of the collection via [`Self::set_sponsor`].268	pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> DispatchResult {269		self.check_is_internal()?;270		ensure!(271			self.collection.sponsorship.pending_sponsor() == Some(sender),272			Error::<T>::ConfirmSponsorshipFail273		);274275		self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());276277		<Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(self.id, sender.clone()));278		<PalletEvm<T>>::deposit_log(279			erc::CollectionHelpersEvents::CollectionChanged {280				collection_id: eth::collection_id_to_address(self.id),281			}282			.to_log(T::ContractAddress::get()),283		);284285		self.save()286	}287288	/// Remove collection sponsor.289	pub fn remove_sponsor(&mut self, sender: &T::CrossAccountId) -> DispatchResult {290		self.check_is_internal()?;291		self.check_is_owner_or_admin(sender)?;292293		self.collection.sponsorship = SponsorshipState::Disabled;294295		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(self.id));296		<PalletEvm<T>>::deposit_log(297			erc::CollectionHelpersEvents::CollectionChanged {298				collection_id: eth::collection_id_to_address(self.id),299			}300			.to_log(T::ContractAddress::get()),301		);302		self.save()303	}304305	/// Force remove `sponsor`.306	///307	/// Differs from `remove_sponsor` in that308	/// it doesn't require consent from the `owner` of the collection.309	pub fn force_remove_sponsor(&mut self) -> DispatchResult {310		self.check_is_internal()?;311312		self.collection.sponsorship = SponsorshipState::Disabled;313314		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(self.id));315		<PalletEvm<T>>::deposit_log(316			erc::CollectionHelpersEvents::CollectionChanged {317				collection_id: eth::collection_id_to_address(self.id),318			}319			.to_log(T::ContractAddress::get()),320		);321		self.save()322	}323324	/// Checks that the collection was created with, and must be operated upon through **Unique API**.325	/// Now check only the `external` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error.326	pub fn check_is_internal(&self) -> DispatchResult {327		if self.flags.external {328			return Err(<Error<T>>::CollectionIsExternal)?;329		}330331		Ok(())332	}333334	/// Checks that the collection was created with, and must be operated upon through an **assimilated API**.335	/// Now check only the `external` flag and if it's **false**, then return [`Error::CollectionIsInternal`] error.336	pub fn check_is_external(&self) -> DispatchResult {337		if !self.flags.external {338			return Err(<Error<T>>::CollectionIsInternal)?;339		}340341		Ok(())342	}343}344345impl<T: Config> Deref for CollectionHandle<T> {346	type Target = Collection<T::AccountId>;347348	fn deref(&self) -> &Self::Target {349		&self.collection350	}351}352353impl<T: Config> DerefMut for CollectionHandle<T> {354	fn deref_mut(&mut self) -> &mut Self::Target {355		&mut self.collection356	}357}358359impl<T: Config> CollectionHandle<T> {360	/// Checks if the `user` is the owner of the collection.361	pub fn check_is_owner(&self, user: &T::CrossAccountId) -> DispatchResult {362		ensure!(*user.as_sub() == self.owner, <Error<T>>::NoPermission);363		Ok(())364	}365366	/// Returns **true** if the `user` is the owner or administrator of the collection.367	pub fn is_owner_or_admin(&self, user: &T::CrossAccountId) -> bool {368		*user.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, user))369	}370371	/// Checks if the `user` is the owner or administrator of the collection.372	pub fn check_is_owner_or_admin(&self, user: &T::CrossAccountId) -> DispatchResult {373		ensure!(self.is_owner_or_admin(user), <Error<T>>::NoPermission);374		Ok(())375	}376377	/// Returns **true** if378	/// * the `user`is a collection owner or admin379	/// * the collection limits allow the owner/admins to transfer/burn any collection token380	pub fn ignores_token_restrictions(&self, user: &T::CrossAccountId) -> bool {381		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)382	}383384	/// Return **true** if `user` does not have enough token parts, and he can ignore such restrictions.385	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {386		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)387	}388389	/// Checks if the user is in the allow list. If not [Error::AddressNotInAllowlist] returns.390	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {391		ensure!(392			<Allowlist<T>>::get((self.id, user)),393			<Error<T>>::AddressNotInAllowlist394		);395		Ok(())396	}397398	/// Changes collection owner to another account399	/// #### Store read/writes400	/// 1 writes401	pub fn change_owner(402		&mut self,403		caller: T::CrossAccountId,404		new_owner: T::CrossAccountId,405	) -> DispatchResult {406		self.check_is_internal()?;407		self.check_is_owner(&caller)?;408		self.collection.owner = new_owner.as_sub().clone();409410		<Pallet<T>>::deposit_event(Event::<T>::CollectionOwnerChanged(411			self.id,412			new_owner.as_sub().clone(),413		));414		<PalletEvm<T>>::deposit_log(415			erc::CollectionHelpersEvents::CollectionChanged {416				collection_id: eth::collection_id_to_address(self.id),417			}418			.to_log(T::ContractAddress::get()),419		);420421		self.save()422	}423}424425#[frame_support::pallet]426pub mod pallet {427428	use super::*;429	use dispatch::CollectionDispatch;430	use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};431	use up_data_structs::{TokenId, mapping::TokenAddressMapping};432	use scale_info::TypeInfo;433	use weights::WeightInfo;434435	#[pallet::config]436	pub trait Config:437		frame_system::Config + pallet_evm_coder_substrate::Config + pallet_evm::Config + TypeInfo438	{439		/// Weight information for functions of this pallet.440		type WeightInfo: WeightInfo;441442		/// Events compatible with [`frame_system::Config::Event`].443		type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;444445		/// Handler of accounts and payment.446		type Currency: Balanced<Self::AccountId> + Inspect<Self::AccountId>;447448		/// Set price to create a collection.449		#[pallet::constant]450		type CollectionCreationPrice: Get<451			<<Self as Config>::Currency as Inspect<Self::AccountId>>::Balance,452		>;453454		/// Dispatcher of operations on collections.455		type CollectionDispatch: CollectionDispatch<Self>;456457		/// Account which holds the chain's treasury.458		type TreasuryAccountId: Get<Self::AccountId>;459460		/// Address under which the CollectionHelper contract would be available.461		#[pallet::constant]462		type ContractAddress: Get<H160>;463464		/// Mapper for token addresses to Ethereum addresses.465		type EvmTokenAddressMapping: TokenAddressMapping<H160>;466467		/// Mapper for token addresses to [`CrossAccountId`].468		type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;469	}470471	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);472473	#[pallet::pallet]474	#[pallet::storage_version(STORAGE_VERSION)]475	pub struct Pallet<T>(_);476477	#[pallet::extra_constants]478	impl<T: Config> Pallet<T> {479		/// Maximum admins per collection.480		pub fn collection_admins_limit() -> u32 {481			COLLECTION_ADMINS_LIMIT482		}483	}484485	#[pallet::genesis_config]486	pub struct GenesisConfig<T>(PhantomData<T>);487488	#[cfg(feature = "std")]489	impl<T: Config> Default for GenesisConfig<T> {490		fn default() -> Self {491			Self(Default::default())492		}493	}494495	#[pallet::genesis_build]496	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {497		fn build(&self) {498			StorageVersion::new(1).put::<Pallet<T>>();499		}500	}501502	impl<T: Config> Pallet<T> {503		/// Helper function that handles deposit events504		pub fn deposit_event(event: Event<T>) {505			let event = <T as Config>::RuntimeEvent::from(event);506			let event = event.into();507			<frame_system::Pallet<T>>::deposit_event(event)508		}509	}510511	#[pallet::event]512	pub enum Event<T: Config> {513		/// New collection was created514		CollectionCreated(515			/// Globally unique identifier of newly created collection.516			CollectionId,517			/// [`CollectionMode`] converted into _u8_.518			u8,519			/// Collection owner.520			T::AccountId,521		),522523		/// New collection was destroyed524		CollectionDestroyed(525			/// Globally unique identifier of collection.526			CollectionId,527		),528529		/// New item was created.530		ItemCreated(531			/// Id of the collection where item was created.532			CollectionId,533			/// Id of an item. Unique within the collection.534			TokenId,535			/// Owner of newly created item536			T::CrossAccountId,537			/// Always 1 for NFT538			u128,539		),540541		/// Collection item was burned.542		ItemDestroyed(543			/// Id of the collection where item was destroyed.544			CollectionId,545			/// Identifier of burned NFT.546			TokenId,547			/// Which user has destroyed its tokens.548			T::CrossAccountId,549			/// Amount of token pieces destroed. Always 1 for NFT.550			u128,551		),552553		/// Item was transferred554		Transfer(555			/// Id of collection to which item is belong.556			CollectionId,557			/// Id of an item.558			TokenId,559			/// Original owner of item.560			T::CrossAccountId,561			/// New owner of item.562			T::CrossAccountId,563			/// Amount of token pieces transfered. Always 1 for NFT.564			u128,565		),566567		/// Amount pieces of token owned by `sender` was approved for `spender`.568		Approved(569			/// Id of collection to which item is belong.570			CollectionId,571			/// Id of an item.572			TokenId,573			/// Original owner of item.574			T::CrossAccountId,575			/// Id for which the approval was granted.576			T::CrossAccountId,577			/// Amount of token pieces transfered. Always 1 for NFT.578			u128,579		),580581		/// A `sender` approves operations on all owned tokens for `spender`.582		ApprovedForAll(583			/// Id of collection to which item is belong.584			CollectionId,585			/// Owner of a wallet.586			T::CrossAccountId,587			/// Id for which operator status was granted or rewoked.588			T::CrossAccountId,589			/// Is operator status granted or revoked?590			bool,591		),592593		/// The colletion property has been added or edited.594		CollectionPropertySet(595			/// Id of collection to which property has been set.596			CollectionId,597			/// The property that was set.598			PropertyKey,599		),600601		/// The property has been deleted.602		CollectionPropertyDeleted(603			/// Id of collection to which property has been deleted.604			CollectionId,605			/// The property that was deleted.606			PropertyKey,607		),608609		/// The token property has been added or edited.610		TokenPropertySet(611			/// Identifier of the collection whose token has the property set.612			CollectionId,613			/// The token for which the property was set.614			TokenId,615			/// The property that was set.616			PropertyKey,617		),618619		/// The token property has been deleted.620		TokenPropertyDeleted(621			/// Identifier of the collection whose token has the property deleted.622			CollectionId,623			/// The token for which the property was deleted.624			TokenId,625			/// The property that was deleted.626			PropertyKey,627		),628629		/// The token property permission of a collection has been set.630		PropertyPermissionSet(631			/// ID of collection to which property permission has been set.632			CollectionId,633			/// The property permission that was set.634			PropertyKey,635		),636637		/// Address was added to the allow list.638		AllowListAddressAdded(639			/// ID of the affected collection.640			CollectionId,641			/// Address of the added account.642			T::CrossAccountId,643		),644645		/// Address was removed from the allow list.646		AllowListAddressRemoved(647			/// ID of the affected collection.648			CollectionId,649			/// Address of the removed account.650			T::CrossAccountId,651		),652653		/// Collection admin was added.654		CollectionAdminAdded(655			/// ID of the affected collection.656			CollectionId,657			/// Admin address.658			T::CrossAccountId,659		),660661		/// Collection admin was removed.662		CollectionAdminRemoved(663			/// ID of the affected collection.664			CollectionId,665			/// Removed admin address.666			T::CrossAccountId,667		),668669		/// Collection limits were set.670		CollectionLimitSet(671			/// ID of the affected collection.672			CollectionId,673		),674675		/// Collection owned was changed.676		CollectionOwnerChanged(677			/// ID of the affected collection.678			CollectionId,679			/// New owner address.680			T::AccountId,681		),682683		/// Collection permissions were set.684		CollectionPermissionSet(685			/// ID of the affected collection.686			CollectionId,687		),688689		/// Collection sponsor was set.690		CollectionSponsorSet(691			/// ID of the affected collection.692			CollectionId,693			/// New sponsor address.694			T::AccountId,695		),696697		/// New sponsor was confirm.698		SponsorshipConfirmed(699			/// ID of the affected collection.700			CollectionId,701			/// New sponsor address.702			T::AccountId,703		),704705		/// Collection sponsor was removed.706		CollectionSponsorRemoved(707			/// ID of the affected collection.708			CollectionId,709		),710	}711712	#[pallet::error]713	pub enum Error<T> {714		/// This collection does not exist.715		CollectionNotFound,716		/// Sender parameter and item owner must be equal.717		MustBeTokenOwner,718		/// No permission to perform action719		NoPermission,720		/// Destroying only empty collections is allowed721		CantDestroyNotEmptyCollection,722		/// Collection is not in mint mode.723		PublicMintingNotAllowed,724		/// Address is not in allow list.725		AddressNotInAllowlist,726727		/// Collection name can not be longer than 63 char.728		CollectionNameLimitExceeded,729		/// Collection description can not be longer than 255 char.730		CollectionDescriptionLimitExceeded,731		/// Token prefix can not be longer than 15 char.732		CollectionTokenPrefixLimitExceeded,733		/// Total collections bound exceeded.734		TotalCollectionsLimitExceeded,735		/// Exceeded max admin count736		CollectionAdminCountExceeded,737		/// Collection limit bounds per collection exceeded738		CollectionLimitBoundsExceeded,739		/// Tried to enable permissions which are only permitted to be disabled740		OwnerPermissionsCantBeReverted,741		/// Collection settings not allowing items transferring742		TransferNotAllowed,743		/// Account token limit exceeded per collection744		AccountTokenLimitExceeded,745		/// Collection token limit exceeded746		CollectionTokenLimitExceeded,747		/// Metadata flag frozen748		MetadataFlagFrozen,749750		/// Item does not exist751		TokenNotFound,752		/// Item is balance not enough753		TokenValueTooLow,754		/// Requested value is more than the approved755		ApprovedValueTooLow,756		/// Tried to approve more than owned757		CantApproveMoreThanOwned,758		/// Only spending from eth mirror could be approved759		AddressIsNotEthMirror,760761		/// Can't transfer tokens to ethereum zero address762		AddressIsZero,763764		/// The operation is not supported765		UnsupportedOperation,766767		/// Insufficient funds to perform an action768		NotSufficientFounds,769770		/// User does not satisfy the nesting rule771		UserIsNotAllowedToNest,772		/// Only tokens from specific collections may nest tokens under this one773		SourceCollectionIsNotAllowedToNest,774775		/// Tried to store more data than allowed in collection field776		CollectionFieldSizeExceeded,777778		/// Tried to store more property data than allowed779		NoSpaceForProperty,780781		/// Tried to store more property keys than allowed782		PropertyLimitReached,783784		/// Property key is too long785		PropertyKeyIsTooLong,786787		/// Only ASCII letters, digits, and symbols `_`, `-`, and `.` are allowed788		InvalidCharacterInPropertyKey,789790		/// Empty property keys are forbidden791		EmptyPropertyKey,792793		/// Tried to access an external collection with an internal API794		CollectionIsExternal,795796		/// Tried to access an internal collection with an external API797		CollectionIsInternal,798799		/// This address is not set as sponsor, use setCollectionSponsor first.800		ConfirmSponsorshipFail,801802		/// The user is not an administrator.803		UserIsNotCollectionAdmin,804	}805806	/// Storage of the count of created collections. Essentially contains the last collection ID.807	#[pallet::storage]808	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;809810	/// Storage of the count of deleted collections.811	#[pallet::storage]812	pub type DestroyedCollectionCount<T> =813		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;814815	/// Storage of collection info.816	#[pallet::storage]817	pub type CollectionById<T> = StorageMap<818		Hasher = Blake2_128Concat,819		Key = CollectionId,820		Value = Collection<<T as frame_system::Config>::AccountId>,821		QueryKind = OptionQuery,822	>;823824	/// Storage of collection properties.825	#[pallet::storage]826	#[pallet::getter(fn collection_properties)]827	pub type CollectionProperties<T> = StorageMap<828		Hasher = Blake2_128Concat,829		Key = CollectionId,830		Value = CollectionPropertiesT,831		QueryKind = ValueQuery,832	>;833834	/// Storage of token property permissions of a collection.835	#[pallet::storage]836	#[pallet::getter(fn property_permissions)]837	pub type CollectionPropertyPermissions<T> = StorageMap<838		Hasher = Blake2_128Concat,839		Key = CollectionId,840		Value = PropertiesPermissionMap,841		QueryKind = ValueQuery,842	>;843844	/// Storage of the amount of collection admins.845	#[pallet::storage]846	pub type AdminAmount<T> = StorageMap<847		Hasher = Blake2_128Concat,848		Key = CollectionId,849		Value = u32,850		QueryKind = ValueQuery,851	>;852853	/// List of collection admins.854	#[pallet::storage]855	pub type IsAdmin<T: Config> = StorageNMap<856		Key = (857			Key<Blake2_128Concat, CollectionId>,858			Key<Blake2_128Concat, T::CrossAccountId>,859		),860		Value = bool,861		QueryKind = ValueQuery,862	>;863864	/// Allowlisted collection users.865	#[pallet::storage]866	pub type Allowlist<T: Config> = StorageNMap<867		Key = (868			Key<Blake2_128Concat, CollectionId>,869			Key<Blake2_128Concat, T::CrossAccountId>,870		),871		Value = bool,872		QueryKind = ValueQuery,873	>;874875	/// Not used by code, exists only to provide some types to metadata.876	#[pallet::storage]877	pub type DummyStorageValue<T: Config> = StorageValue<878		Value = (879			CollectionStats,880			CollectionId,881			TokenId,882			TokenChild,883			PhantomType<(884				TokenData<T::CrossAccountId>,885				RpcCollection<T::AccountId>,886				// PoV Estimate Info887				PovInfo,888			)>,889		),890		QueryKind = OptionQuery,891	>;892}893894impl<T: Config> Pallet<T> {895	/// Enshure that receiver address is correct.896	///897	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens.898	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {899		ensure!(900			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,901			<Error<T>>::AddressIsZero902		);903		Ok(())904	}905906	/// Get a vector of collection admins.907	pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {908		<IsAdmin<T>>::iter_prefix((collection,))909			.map(|(a, _)| a)910			.collect()911	}912913	/// Get a vector of users allowed to mint tokens.914	pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {915		<Allowlist<T>>::iter_prefix((collection,))916			.map(|(a, _)| a)917			.collect()918	}919920	/// Is `user` allowed to mint token in `collection`.921	pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {922		<Allowlist<T>>::get((collection, user))923	}924925	/// Get statistics of collections.926	pub fn collection_stats() -> CollectionStats {927		let created = <CreatedCollectionCount<T>>::get();928		let destroyed = <DestroyedCollectionCount<T>>::get();929		CollectionStats {930			created: created.0,931			destroyed: destroyed.0,932			alive: created.0 - destroyed.0,933		}934	}935936	/// Get the effective limits for the collection.937	pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {938		let collection = <CollectionById<T>>::get(collection)?;939		let limits = collection.limits;940		let effective_limits = CollectionLimits {941			account_token_ownership_limit: Some(limits.account_token_ownership_limit()),942			sponsored_data_size: Some(limits.sponsored_data_size()),943			sponsored_data_rate_limit: Some(944				limits945					.sponsored_data_rate_limit946					.unwrap_or(SponsoringRateLimit::SponsoringDisabled),947			),948			token_limit: Some(limits.token_limit()),949			sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(950				match collection.mode {951					CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,952					CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,953					CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,954				},955			)),956			sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),957			owner_can_transfer: Some(limits.owner_can_transfer()),958			owner_can_destroy: Some(limits.owner_can_destroy()),959			transfers_enabled: Some(limits.transfers_enabled()),960		};961962		Some(effective_limits)963	}964965	/// Returns information about the `collection` adapted for rpc.966	pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {967		let Collection {968			name,969			description,970			owner,971			mode,972			token_prefix,973			sponsorship,974			limits,975			permissions,976			flags,977		} = <CollectionById<T>>::get(collection)?;978979		let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)980			.into_iter()981			.map(|(key, permission)| PropertyKeyPermission { key, permission })982			.collect();983984		let properties = <CollectionProperties<T>>::get(collection)985			.into_iter()986			.map(|(key, value)| Property { key, value })987			.collect();988989		let permissions = CollectionPermissions {990			access: Some(permissions.access()),991			mint_mode: Some(permissions.mint_mode()),992			nesting: Some(permissions.nesting().clone()),993		};994995		Some(RpcCollection {996			name: name.into_inner(),997			description: description.into_inner(),998			owner,999			mode,1000			token_prefix: token_prefix.into_inner(),1001			sponsorship,1002			limits,1003			permissions,1004			token_property_permissions,1005			properties,1006			read_only: flags.external,10071008			flags: RpcCollectionFlags {1009				foreign: flags.foreign,1010				erc721metadata: flags.erc721metadata,1011			},1012		})1013	}1014}10151016macro_rules! limit_default {1017	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{1018		$(1019			if let Some($new) = $new.$field {1020				let $old = $old.$field($($arg)?);1021				let _ = $new;1022				let _ = $old;1023				$check1024			} else {1025				$new.$field = $old.$field1026			}1027		)*1028	}};1029}1030macro_rules! limit_default_clone {1031	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{1032		$(1033			if let Some($new) = $new.$field.clone() {1034				let $old = $old.$field($($arg)?);1035				let _ = $new;1036				let _ = $old;1037				$check1038			} else {1039				$new.$field = $old.$field.clone()1040			}1041		)*1042	}};1043}10441045impl<T: Config> Pallet<T> {1046	/// Create new collection.1047	///1048	/// * `owner` - The owner of the collection.1049	/// * `data` - Description of the created collection.1050	/// * `flags` - Extra flags to store.1051	pub fn init_collection(1052		owner: T::CrossAccountId,1053		payer: T::CrossAccountId,1054		data: CreateCollectionData<T::AccountId>,1055		flags: CollectionFlags,1056	) -> Result<CollectionId, DispatchError> {1057		{1058			ensure!(1059				data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,1060				Error::<T>::CollectionTokenPrefixLimitExceeded1061			);1062		}10631064		let created_count = <CreatedCollectionCount<T>>::get()1065			.01066			.checked_add(1)1067			.ok_or(ArithmeticError::Overflow)?;1068		let destroyed_count = <DestroyedCollectionCount<T>>::get().0;1069		let id = CollectionId(created_count);10701071		// bound Total number of collections1072		ensure!(1073			created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,1074			<Error<T>>::TotalCollectionsLimitExceeded1075		);10761077		// =========10781079		let collection = Collection {1080			owner: owner.as_sub().clone(),1081			name: data.name,1082			mode: data.mode.clone(),1083			description: data.description,1084			token_prefix: data.token_prefix,1085			sponsorship: data1086				.pending_sponsor1087				.map(SponsorshipState::Unconfirmed)1088				.unwrap_or_default(),1089			limits: data1090				.limits1091				.map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))1092				.unwrap_or_else(|| Ok(CollectionLimits::default()))?,1093			permissions: data1094				.permissions1095				.map(|permissions| {1096					Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions)1097				})1098				.unwrap_or_else(|| Ok(CollectionPermissions::default()))?,1099			flags,1100		};11011102		let mut collection_properties = CollectionPropertiesT::new();1103		collection_properties1104			.try_set_from_iter(data.properties.into_iter())1105			.map_err(<Error<T>>::from)?;11061107		CollectionProperties::<T>::insert(id, collection_properties);11081109		let mut token_props_permissions = PropertiesPermissionMap::new();1110		token_props_permissions1111			.try_set_from_iter(data.token_property_permissions.into_iter())1112			.map_err(<Error<T>>::from)?;11131114		CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);11151116		// Take a (non-refundable) deposit of collection creation1117		{1118			let mut imbalance = <Debt<T::AccountId, <T as Config>::Currency>>::zero();1119			imbalance.subsume(<T as Config>::Currency::deposit(1120				&T::TreasuryAccountId::get(),1121				T::CollectionCreationPrice::get(),1122				Precision::Exact,1123			)?);1124			let credit =1125				<T as Config>::Currency::settle(payer.as_sub(), imbalance, Preservation::Preserve)1126					.map_err(|_| Error::<T>::NotSufficientFounds)?;11271128			debug_assert!(credit.peek().is_zero())1129		}11301131		<CreatedCollectionCount<T>>::put(created_count);1132		<Pallet<T>>::deposit_event(Event::CollectionCreated(1133			id,1134			data.mode.id(),1135			owner.as_sub().clone(),1136		));1137		<PalletEvm<T>>::deposit_log(1138			erc::CollectionHelpersEvents::CollectionCreated {1139				owner: *owner.as_eth(),1140				collection_id: eth::collection_id_to_address(id),1141			}1142			.to_log(T::ContractAddress::get()),1143		);1144		<CollectionById<T>>::insert(id, collection);1145		Ok(id)1146	}11471148	/// Destroy collection.1149	///1150	/// * `collection` - Collection handler.1151	/// * `sender` - The owner or administrator of the collection.1152	pub fn destroy_collection(1153		collection: CollectionHandle<T>,1154		sender: &T::CrossAccountId,1155	) -> DispatchResult {1156		ensure!(1157			collection.limits.owner_can_destroy(),1158			<Error<T>>::NoPermission,1159		);1160		collection.check_is_owner(sender)?;11611162		let destroyed_collections = <DestroyedCollectionCount<T>>::get()1163			.01164			.checked_add(1)1165			.ok_or(ArithmeticError::Overflow)?;11661167		// =========11681169		<DestroyedCollectionCount<T>>::put(destroyed_collections);1170		<CollectionById<T>>::remove(collection.id);1171		<AdminAmount<T>>::remove(collection.id);1172		let _ = <IsAdmin<T>>::clear_prefix((collection.id,), u32::MAX, None);1173		let _ = <Allowlist<T>>::clear_prefix((collection.id,), u32::MAX, None);1174		<CollectionProperties<T>>::remove(collection.id);11751176		<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));11771178		<PalletEvm<T>>::deposit_log(1179			erc::CollectionHelpersEvents::CollectionDestroyed {1180				collection_id: eth::collection_id_to_address(collection.id),1181			}1182			.to_log(T::ContractAddress::get()),1183		);1184		Ok(())1185	}11861187	/// This function sets or removes a collection properties according to1188	/// `properties_updates` contents:1189	/// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`1190	/// * removes a property under the <key> if the value is `None` `(<key>, None)`.1191	///1192	/// This function fires an event for each property change.1193	/// In case of an error, all the changes (including the events) will be reverted1194	/// since the function is transactional.1195	#[transactional]1196	fn modify_collection_properties(1197		collection: &CollectionHandle<T>,1198		sender: &T::CrossAccountId,1199		properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,1200	) -> DispatchResult {1201		collection.check_is_owner_or_admin(sender)?;12021203		let mut stored_properties = <CollectionProperties<T>>::get(collection.id);12041205		for (key, value) in properties_updates {1206			match value {1207				Some(value) => {1208					stored_properties1209						.try_set(key.clone(), value)1210						.map_err(<Error<T>>::from)?;12111212					Self::deposit_event(Event::CollectionPropertySet(collection.id, key));1213					<PalletEvm<T>>::deposit_log(1214						erc::CollectionHelpersEvents::CollectionChanged {1215							collection_id: eth::collection_id_to_address(collection.id),1216						}1217						.to_log(T::ContractAddress::get()),1218					);1219				}1220				None => {1221					stored_properties.remove(&key).map_err(<Error<T>>::from)?;12221223					Self::deposit_event(Event::CollectionPropertyDeleted(collection.id, key));1224					<PalletEvm<T>>::deposit_log(1225						erc::CollectionHelpersEvents::CollectionChanged {1226							collection_id: eth::collection_id_to_address(collection.id),1227						}1228						.to_log(T::ContractAddress::get()),1229					);1230				}1231			}1232		}12331234		<CollectionProperties<T>>::set(collection.id, stored_properties);12351236		Ok(())1237	}12381239	/// A batch operation to add, edit or remove properties for a token.1240	/// It sets or removes a token's properties according to1241	/// `properties_updates` contents:1242	/// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`1243	/// * removes a property under the <key> if the value is `None` `(<key>, None)`.1244	///1245	/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.1246	/// - `is_token_create`: Indicates that method is called during token initialization.1247	///   Allows to bypass ownership check.1248	///1249	/// All affected properties should have `mutable` permission1250	/// to be **deleted** or to be **set more than once**,1251	/// and the sender should have permission to edit those properties.1252	///1253	/// This function fires an event for each property change.1254	/// In case of an error, all the changes (including the events) will be reverted1255	/// since the function is transactional.1256	pub fn modify_token_properties(1257		collection: &CollectionHandle<T>,1258		sender: &T::CrossAccountId,1259		token_id: TokenId,1260		properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,1261		is_token_create: bool,1262		mut stored_properties: TokenProperties,1263		is_token_owner: impl Fn() -> Result<bool, DispatchError>,1264		set_token_properties: impl FnOnce(TokenProperties),1265		log: evm_coder::ethereum::Log,1266	) -> DispatchResult {1267		let is_collection_admin = collection.is_owner_or_admin(sender);1268		let permissions = Self::property_permissions(collection.id);12691270		let mut token_owner_result = None;1271		let mut is_token_owner = || -> Result<bool, DispatchError> {1272			*token_owner_result.get_or_insert_with(&is_token_owner)1273		};12741275		for (key, value) in properties_updates {1276			let permission = permissions1277				.get(&key)1278				.cloned()1279				.unwrap_or_else(PropertyPermission::none);12801281			let is_property_exists = stored_properties.get(&key).is_some();12821283			match permission {1284				PropertyPermission { mutable: false, .. } if is_property_exists => {1285					return Err(<Error<T>>::NoPermission.into());1286				}12871288				PropertyPermission {1289					collection_admin,1290					token_owner,1291					..1292				} => {1293					//TODO: investigate threats during public minting.1294					let is_token_create =1295						is_token_create && (collection_admin || token_owner) && value.is_some();1296					if !(is_token_create1297						|| (collection_admin && is_collection_admin)1298						|| (token_owner && is_token_owner()?))1299					{1300						fail!(<Error<T>>::NoPermission);1301					}1302				}1303			}13041305			match value {1306				Some(value) => {1307					stored_properties1308						.try_set(key.clone(), value)1309						.map_err(<Error<T>>::from)?;13101311					Self::deposit_event(Event::TokenPropertySet(collection.id, token_id, key));1312				}1313				None => {1314					stored_properties.remove(&key).map_err(<Error<T>>::from)?;13151316					Self::deposit_event(Event::TokenPropertyDeleted(collection.id, token_id, key));1317				}1318			}13191320			<PalletEvm<T>>::deposit_log(log.clone());1321		}13221323		set_token_properties(stored_properties);13241325		Ok(())1326	}13271328	/// Sets or unsets the approval of a given operator.1329	///1330	/// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1331	/// - `owner`: Token owner1332	/// - `operator`: Operator1333	/// - `approve`: Should operator status be granted or revoked?1334	pub fn set_allowance_for_all(1335		collection: &CollectionHandle<T>,1336		owner: &T::CrossAccountId,1337		operator: &T::CrossAccountId,1338		approve: bool,1339		set_allowance: impl FnOnce(),1340		log: evm_coder::ethereum::Log,1341	) -> DispatchResult {1342		if collection.permissions.access() == AccessMode::AllowList {1343			collection.check_allowlist(owner)?;1344			collection.check_allowlist(operator)?;1345		}13461347		Self::ensure_correct_receiver(operator)?;13481349		set_allowance();13501351		<PalletEvm<T>>::deposit_log(log);1352		Self::deposit_event(Event::ApprovedForAll(1353			collection.id,1354			owner.clone(),1355			operator.clone(),1356			approve,1357		));1358		Ok(())1359	}13601361	/// Set collection property.1362	///1363	/// * `collection` - Collection handler.1364	/// * `sender` - The owner or administrator of the collection.1365	/// * `property` - The property to set.1366	pub fn set_collection_property(1367		collection: &CollectionHandle<T>,1368		sender: &T::CrossAccountId,1369		property: Property,1370	) -> DispatchResult {1371		Self::set_collection_properties(collection, sender, [property].into_iter())1372	}13731374	/// Set a scoped collection property, where the scope is a special prefix1375	/// prohibiting a user access to change the property directly.1376	///1377	/// * `collection_id` - ID of the collection for which the property is being set.1378	/// * `scope` - Property scope.1379	/// * `property` - The property to set.1380	pub fn set_scoped_collection_property(1381		collection_id: CollectionId,1382		scope: PropertyScope,1383		property: Property,1384	) -> DispatchResult {1385		CollectionProperties::<T>::try_mutate(collection_id, |properties| {1386			properties.try_scoped_set(scope, property.key, property.value)1387		})1388		.map_err(<Error<T>>::from)?;13891390		Ok(())1391	}13921393	/// Set scoped collection properties, where the scope is a special prefix1394	/// prohibiting a user access to change the properties directly.1395	///1396	/// * `collection_id` - ID of the collection for which the properties is being set.1397	/// * `scope` - Property scope.1398	/// * `properties` - The properties to set.1399	pub fn set_scoped_collection_properties(1400		collection_id: CollectionId,1401		scope: PropertyScope,1402		properties: impl Iterator<Item = Property>,1403	) -> DispatchResult {1404		CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {1405			stored_properties.try_scoped_set_from_iter(scope, properties)1406		})1407		.map_err(<Error<T>>::from)?;14081409		Ok(())1410	}14111412	/// Set collection properties.1413	///1414	/// * `collection` - Collection handler.1415	/// * `sender` - The owner or administrator of the collection.1416	/// * `properties` - The properties to set.1417	pub fn set_collection_properties(1418		collection: &CollectionHandle<T>,1419		sender: &T::CrossAccountId,1420		properties: impl Iterator<Item = Property>,1421	) -> DispatchResult {1422		Self::modify_collection_properties(1423			collection,1424			sender,1425			properties.map(|property| (property.key, Some(property.value))),1426		)1427	}14281429	/// Delete collection property.1430	///1431	/// * `collection` - Collection handler.1432	/// * `sender` - The owner or administrator of the collection.1433	/// * `property` - The property to delete.1434	pub fn delete_collection_property(1435		collection: &CollectionHandle<T>,1436		sender: &T::CrossAccountId,1437		property_key: PropertyKey,1438	) -> DispatchResult {1439		Self::delete_collection_properties(collection, sender, [property_key].into_iter())1440	}14411442	/// Delete collection properties.1443	///1444	/// * `collection` - Collection handler.1445	/// * `sender` - The owner or administrator of the collection.1446	/// * `properties` - The properties to delete.1447	pub fn delete_collection_properties(1448		collection: &CollectionHandle<T>,1449		sender: &T::CrossAccountId,1450		property_keys: impl Iterator<Item = PropertyKey>,1451	) -> DispatchResult {1452		Self::modify_collection_properties(collection, sender, property_keys.map(|key| (key, None)))1453	}14541455	/// Set collection propetry permission without any checks.1456	///1457	/// Used for migrations.1458	///1459	/// * `collection` - Collection handler.1460	/// * `property_permissions` - Property permissions.1461	pub fn set_property_permission_unchecked(1462		collection: CollectionId,1463		property_permission: PropertyKeyPermission,1464	) -> DispatchResult {1465		<CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {1466			permissions.try_set(property_permission.key, property_permission.permission)1467		})1468		.map_err(<Error<T>>::from)?;1469		Ok(())1470	}14711472	/// Set collection property permission.1473	///1474	/// * `collection` - Collection handler.1475	/// * `sender` - The owner or administrator of the collection.1476	/// * `property_permission` - Property permission.1477	pub fn set_property_permission(1478		collection: &CollectionHandle<T>,1479		sender: &T::CrossAccountId,1480		property_permission: PropertyKeyPermission,1481	) -> DispatchResult {1482		Self::set_scoped_property_permission(1483			collection,1484			sender,1485			PropertyScope::None,1486			property_permission,1487		)1488	}14891490	/// Set collection property permission with scope.1491	///1492	/// * `collection` - Collection handler.1493	/// * `sender` - The owner or administrator of the collection.1494	/// * `scope` - Property scope.1495	/// * `property_permission` - Property permission.1496	pub fn set_scoped_property_permission(1497		collection: &CollectionHandle<T>,1498		sender: &T::CrossAccountId,1499		scope: PropertyScope,1500		property_permission: PropertyKeyPermission,1501	) -> DispatchResult {1502		collection.check_is_owner_or_admin(sender)?;15031504		let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);1505		let current_permission = all_permissions.get(&property_permission.key);1506		if matches![1507			current_permission,1508			Some(PropertyPermission { mutable: false, .. })1509		] {1510			return Err(<Error<T>>::NoPermission.into());1511		}15121513		CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {1514			let property_permission = property_permission.clone();1515			permissions.try_scoped_set(1516				scope,1517				property_permission.key,1518				property_permission.permission,1519			)1520		})1521		.map_err(<Error<T>>::from)?;15221523		Self::deposit_event(Event::PropertyPermissionSet(1524			collection.id,1525			property_permission.key,1526		));1527		<PalletEvm<T>>::deposit_log(1528			erc::CollectionHelpersEvents::CollectionChanged {1529				collection_id: eth::collection_id_to_address(collection.id),1530			}1531			.to_log(T::ContractAddress::get()),1532		);15331534		Ok(())1535	}15361537	/// Set token property permission.1538	///1539	/// * `collection` - Collection handler.1540	/// * `sender` - The owner or administrator of the collection.1541	/// * `property_permissions` - Property permissions.1542	#[transactional]1543	pub fn set_token_property_permissions(1544		collection: &CollectionHandle<T>,1545		sender: &T::CrossAccountId,1546		property_permissions: Vec<PropertyKeyPermission>,1547	) -> DispatchResult {1548		Self::set_scoped_token_property_permissions(1549			collection,1550			sender,1551			PropertyScope::None,1552			property_permissions,1553		)1554	}15551556	/// Set token property permission with scope.1557	///1558	/// * `collection` - Collection handler.1559	/// * `sender` - The owner or administrator of the collection.1560	/// * `scope` - Property scope.1561	/// * `property_permissions` - Property permissions.1562	#[transactional]1563	pub fn set_scoped_token_property_permissions(1564		collection: &CollectionHandle<T>,1565		sender: &T::CrossAccountId,1566		scope: PropertyScope,1567		property_permissions: Vec<PropertyKeyPermission>,1568	) -> DispatchResult {1569		for prop_pemission in property_permissions {1570			Self::set_scoped_property_permission(collection, sender, scope, prop_pemission)?;1571		}15721573		Ok(())1574	}15751576	/// Get collection property.1577	pub fn get_collection_property(1578		collection_id: CollectionId,1579		key: &PropertyKey,1580	) -> Option<PropertyValue> {1581		Self::collection_properties(collection_id).get(key).cloned()1582	}15831584	/// Convert byte vector to property key vector.1585	pub fn bytes_keys_to_property_keys(1586		keys: Vec<Vec<u8>>,1587	) -> Result<Vec<PropertyKey>, DispatchError> {1588		keys.into_iter()1589			.map(|key| -> Result<PropertyKey, DispatchError> {1590				key.try_into()1591					.map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())1592			})1593			.collect::<Result<Vec<PropertyKey>, DispatchError>>()1594	}15951596	/// Get properties according to given keys.1597	pub fn filter_collection_properties(1598		collection_id: CollectionId,1599		keys: Option<Vec<PropertyKey>>,1600	) -> Result<Vec<Property>, DispatchError> {1601		let properties = Self::collection_properties(collection_id);16021603		let properties = keys1604			.map(|keys| {1605				keys.into_iter()1606					.filter_map(|key| {1607						properties.get(&key).map(|value| Property {1608							key,1609							value: value.clone(),1610						})1611					})1612					.collect()1613			})1614			.unwrap_or_else(|| {1615				properties1616					.into_iter()1617					.map(|(key, value)| Property { key, value })1618					.collect()1619			});16201621		Ok(properties)1622	}16231624	/// Get property permissions according to given keys.1625	pub fn filter_property_permissions(1626		collection_id: CollectionId,1627		keys: Option<Vec<PropertyKey>>,1628	) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1629		let permissions = Self::property_permissions(collection_id);16301631		let key_permissions = keys1632			.map(|keys| {1633				keys.into_iter()1634					.filter_map(|key| {1635						permissions1636							.get(&key)1637							.map(|permission| PropertyKeyPermission {1638								key,1639								permission: permission.clone(),1640							})1641					})1642					.collect()1643			})1644			.unwrap_or_else(|| {1645				permissions1646					.into_iter()1647					.map(|(key, permission)| PropertyKeyPermission { key, permission })1648					.collect()1649			});16501651		Ok(key_permissions)1652	}16531654	/// Toggle `user` participation in the `collection`'s allow list.1655	/// #### Store read/writes1656	/// 1 writes1657	pub fn toggle_allowlist(1658		collection: &CollectionHandle<T>,1659		sender: &T::CrossAccountId,1660		user: &T::CrossAccountId,1661		allowed: bool,1662	) -> DispatchResult {1663		collection.check_is_owner_or_admin(sender)?;16641665		// =========16661667		if allowed {1668			<Allowlist<T>>::insert((collection.id, user), true);1669			Self::deposit_event(Event::<T>::AllowListAddressAdded(1670				collection.id,1671				user.clone(),1672			));1673		} else {1674			<Allowlist<T>>::remove((collection.id, user));1675			Self::deposit_event(Event::<T>::AllowListAddressRemoved(1676				collection.id,1677				user.clone(),1678			));1679		}16801681		<PalletEvm<T>>::deposit_log(1682			erc::CollectionHelpersEvents::CollectionChanged {1683				collection_id: eth::collection_id_to_address(collection.id),1684			}1685			.to_log(T::ContractAddress::get()),1686		);16871688		Ok(())1689	}16901691	/// Toggle `user` participation in the `collection`'s admin list.1692	/// #### Store read/writes1693	/// 2 reads, 2 writes1694	pub fn toggle_admin(1695		collection: &CollectionHandle<T>,1696		sender: &T::CrossAccountId,1697		user: &T::CrossAccountId,1698		admin: bool,1699	) -> DispatchResult {1700		collection.check_is_internal()?;1701		collection.check_is_owner(sender)?;17021703		let is_admin = <IsAdmin<T>>::get((collection.id, user));1704		if is_admin == admin {1705			if admin {1706				return Ok(());1707			} else {1708				return Err(Error::<T>::UserIsNotCollectionAdmin.into());1709			}1710		}1711		let amount = <AdminAmount<T>>::get(collection.id);17121713		// =========17141715		if admin {1716			let amount = amount1717				.checked_add(1)1718				.ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1719			ensure!(1720				amount <= Self::collection_admins_limit(),1721				<Error<T>>::CollectionAdminCountExceeded,1722			);17231724			<AdminAmount<T>>::insert(collection.id, amount);1725			<IsAdmin<T>>::insert((collection.id, user), true);17261727			Self::deposit_event(Event::<T>::CollectionAdminAdded(1728				collection.id,1729				user.clone(),1730			));1731		} else {1732			<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1733			<IsAdmin<T>>::remove((collection.id, user));17341735			Self::deposit_event(Event::<T>::CollectionAdminRemoved(1736				collection.id,1737				user.clone(),1738			));1739		}17401741		<PalletEvm<T>>::deposit_log(1742			erc::CollectionHelpersEvents::CollectionChanged {1743				collection_id: eth::collection_id_to_address(collection.id),1744			}1745			.to_log(T::ContractAddress::get()),1746		);17471748		Ok(())1749	}17501751	/// Update collection limits.1752	pub fn update_limits(1753		user: &T::CrossAccountId,1754		collection: &mut CollectionHandle<T>,1755		new_limit: CollectionLimits,1756	) -> DispatchResult {1757		collection.check_is_internal()?;1758		collection.check_is_owner_or_admin(user)?;17591760		collection.limits =1761			Self::clamp_limits(collection.mode.clone(), &collection.limits, new_limit)?;17621763		Self::deposit_event(Event::<T>::CollectionLimitSet(collection.id));1764		<PalletEvm<T>>::deposit_log(1765			erc::CollectionHelpersEvents::CollectionChanged {1766				collection_id: eth::collection_id_to_address(collection.id),1767			}1768			.to_log(T::ContractAddress::get()),1769		);17701771		collection.save()1772	}17731774	/// Merge set fields from `new_limit` to `old_limit`.1775	fn clamp_limits(1776		mode: CollectionMode,1777		old_limit: &CollectionLimits,1778		mut new_limit: CollectionLimits,1779	) -> Result<CollectionLimits, DispatchError> {1780		let limits = old_limit;1781		limit_default!(old_limit, new_limit,1782			account_token_ownership_limit => ensure!(1783				new_limit <= MAX_TOKEN_OWNERSHIP,1784				<Error<T>>::CollectionLimitBoundsExceeded,1785			),1786			sponsored_data_size => ensure!(1787				new_limit <= CUSTOM_DATA_LIMIT,1788				<Error<T>>::CollectionLimitBoundsExceeded,1789			),17901791			sponsored_data_rate_limit => {},1792			token_limit => ensure!(1793				old_limit >= new_limit && new_limit > 0,1794				<Error<T>>::CollectionTokenLimitExceeded1795			),17961797			sponsor_transfer_timeout(match mode {1798				CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1799				CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1800				CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1801			}) => ensure!(1802				new_limit <= MAX_SPONSOR_TIMEOUT,1803				<Error<T>>::CollectionLimitBoundsExceeded,1804			),1805			sponsor_approve_timeout => {},1806			owner_can_transfer => ensure!(1807				!limits.owner_can_transfer_instaled() ||1808				old_limit || !new_limit,1809				<Error<T>>::OwnerPermissionsCantBeReverted,1810			),1811			owner_can_destroy => ensure!(1812				old_limit || !new_limit,1813				<Error<T>>::OwnerPermissionsCantBeReverted,1814			),1815			transfers_enabled => {},1816		);1817		Ok(new_limit)1818	}18191820	/// Update collection permissions.1821	pub fn update_permissions(1822		user: &T::CrossAccountId,1823		collection: &mut CollectionHandle<T>,1824		new_permission: CollectionPermissions,1825	) -> DispatchResult {1826		collection.check_is_internal()?;1827		collection.check_is_owner_or_admin(user)?;1828		collection.permissions = Self::clamp_permissions(1829			collection.mode.clone(),1830			&collection.permissions,1831			new_permission,1832		)?;18331834		Self::deposit_event(Event::<T>::CollectionPermissionSet(collection.id));1835		<PalletEvm<T>>::deposit_log(1836			erc::CollectionHelpersEvents::CollectionChanged {1837				collection_id: eth::collection_id_to_address(collection.id),1838			}1839			.to_log(T::ContractAddress::get()),1840		);18411842		collection.save()1843	}18441845	/// Merge set fields from `new_permission` to `old_permission`.1846	fn clamp_permissions(1847		_mode: CollectionMode,1848		old_permission: &CollectionPermissions,1849		mut new_permission: CollectionPermissions,1850	) -> Result<CollectionPermissions, DispatchError> {1851		limit_default_clone!(old_permission, new_permission,1852			access => {},1853			mint_mode => {},1854			nesting => { /* todo check for permissive, if only it gets out of benchmarks */ },1855		);1856		Ok(new_permission)1857	}18581859	/// Repair possibly broken properties of a collection.1860	pub fn repair_collection(collection_id: CollectionId) -> DispatchResult {1861		CollectionProperties::<T>::mutate(collection_id, |properties| {1862			properties.recompute_consumed_space();1863		});18641865		Ok(())1866	}1867}18681869/// Indicates unsupported methods by returning [Error::UnsupportedOperation].1870#[macro_export]1871macro_rules! unsupported {1872	($runtime:path) => {1873		Err($crate::Error::<$runtime>::UnsupportedOperation.into())1874	};1875}18761877/// Return weights for various worst-case operations.1878pub trait CommonWeightInfo<CrossAccountId> {1879	/// Weight of item creation.1880	fn create_item(data: &CreateItemData) -> Weight {1881		Self::create_multiple_items(from_ref(data))1882	}18831884	/// Weight of items creation.1885	fn create_multiple_items(amount: &[CreateItemData]) -> Weight;18861887	/// Weight of items creation.1888	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;18891890	/// The weight of the burning item.1891	fn burn_item() -> Weight;18921893	/// Property setting weight.1894	///1895	/// * `amount`- The number of properties to set.1896	fn set_collection_properties(amount: u32) -> Weight;18971898	/// Collection property deletion weight.1899	///1900	/// * `amount`- The number of properties to set.1901	fn delete_collection_properties(amount: u32) -> Weight;19021903	/// Token property setting weight.1904	///1905	/// * `amount`- The number of properties to set.1906	fn set_token_properties(amount: u32) -> Weight;19071908	/// Token property deletion weight.1909	///1910	/// * `amount`- The number of properties to delete.1911	fn delete_token_properties(amount: u32) -> Weight;19121913	/// Token property permissions set weight.1914	///1915	/// * `amount`- The number of property permissions to set.1916	fn set_token_property_permissions(amount: u32) -> Weight;19171918	/// Transfer price of the token or its parts.1919	fn transfer() -> Weight;19201921	/// The price of setting the permission of the operation from another user.1922	fn approve() -> Weight;19231924	/// The price of setting the permission of the operation from another user for eth mirror.1925	fn approve_from() -> Weight;19261927	/// Transfer price from another user.1928	fn transfer_from() -> Weight;19291930	/// The price of burning a token from another user.1931	fn burn_from() -> Weight;19321933	/// Differs from burn_item in case of Fungible and Refungible, as it should burn1934	/// whole users's balance.1935	///1936	/// This method shouldn't be used directly, as it doesn't count breadth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1937	fn burn_recursively_self_raw() -> Weight;19381939	/// Cost of iterating over `amount` children while burning, without counting child burning itself.1940	///1941	/// This method shouldn't be used directly, as it doesn't count depth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1942	fn burn_recursively_breadth_raw(amount: u32) -> Weight;19431944	/// The price of recursive burning a token.1945	///1946	/// `max_selfs` - The maximum burning weight of the token itself.1947	/// `max_breadth` - The maximum number of nested tokens to burn.1948	fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight {1949		Self::burn_recursively_self_raw()1950			.saturating_mul(max_selfs.max(1) as u64)1951			.saturating_add(Self::burn_recursively_breadth_raw(max_breadth))1952	}19531954	/// The price of retrieving token owner1955	fn token_owner() -> Weight;19561957	/// The price of setting approval for all1958	fn set_allowance_for_all() -> Weight;19591960	/// The price of repairing an item.1961	fn force_repair_item() -> Weight;1962}19631964/// Weight info extension trait for refungible pallet.1965pub trait RefungibleExtensionsWeightInfo {1966	/// Weight of token repartition.1967	fn repartition() -> Weight;1968}19691970/// Common collection operations.1971///1972/// It wraps methods in Fungible, Nonfungible and Refungible pallets1973/// and adds weight info.1974pub trait CommonCollectionOperations<T: Config> {1975	/// Create token.1976	///1977	/// * `sender` - The user who mint the token and pays for the transaction.1978	/// * `to` - The user who will own the token.1979	/// * `data` - Token data.1980	/// * `nesting_budget` - A budget that can be spent on nesting tokens.1981	fn create_item(1982		&self,1983		sender: T::CrossAccountId,1984		to: T::CrossAccountId,1985		data: CreateItemData,1986		nesting_budget: &dyn Budget,1987	) -> DispatchResultWithPostInfo;19881989	/// Create multiple tokens.1990	///1991	/// * `sender` - The user who mint the token and pays for the transaction.1992	/// * `to` - The user who will own the token.1993	/// * `data` - Token data.1994	/// * `nesting_budget` - A budget that can be spent on nesting tokens.1995	fn create_multiple_items(1996		&self,1997		sender: T::CrossAccountId,1998		to: T::CrossAccountId,1999		data: Vec<CreateItemData>,2000		nesting_budget: &dyn Budget,2001	) -> DispatchResultWithPostInfo;20022003	/// Create multiple tokens.2004	///2005	/// * `sender` - The user who mint the token and pays for the transaction.2006	/// * `to` - The user who will own the token.2007	/// * `data` - Token data.2008	/// * `nesting_budget` - A budget that can be spent on nesting tokens.2009	fn create_multiple_items_ex(2010		&self,2011		sender: T::CrossAccountId,2012		data: CreateItemExData<T::CrossAccountId>,2013		nesting_budget: &dyn Budget,2014	) -> DispatchResultWithPostInfo;20152016	/// Burn token.2017	///2018	/// * `sender` - The user who owns the token.2019	/// * `token` - Token id that will burned.2020	/// * `amount` - The number of parts of the token that will be burned.2021	fn burn_item(2022		&self,2023		sender: T::CrossAccountId,2024		token: TokenId,2025		amount: u128,2026	) -> DispatchResultWithPostInfo;20272028	/// Burn token and all nested tokens recursievly.2029	///2030	/// * `sender` - The user who owns the token.2031	/// * `token` - Token id that will burned.2032	/// * `self_budget` - The budget that can be spent on burning tokens.2033	/// * `breadth_budget` - The budget that can be spent on burning nested tokens.2034	fn burn_item_recursively(2035		&self,2036		sender: T::CrossAccountId,2037		token: TokenId,2038		self_budget: &dyn Budget,2039		breadth_budget: &dyn Budget,2040	) -> DispatchResultWithPostInfo;20412042	/// Set collection properties.2043	///2044	/// * `sender` - Must be either the owner of the collection or its admin.2045	/// * `properties` - Properties to be set.2046	fn set_collection_properties(2047		&self,2048		sender: T::CrossAccountId,2049		properties: Vec<Property>,2050	) -> DispatchResultWithPostInfo;20512052	/// Delete collection properties.2053	///2054	/// * `sender` - Must be either the owner of the collection or its admin.2055	/// * `properties` - The properties to be removed.2056	fn delete_collection_properties(2057		&self,2058		sender: &T::CrossAccountId,2059		property_keys: Vec<PropertyKey>,2060	) -> DispatchResultWithPostInfo;20612062	/// Set token properties.2063	///2064	/// The appropriate [`PropertyPermission`] for the token property2065	/// must be set with [`Self::set_token_property_permissions`].2066	///2067	/// * `sender` - Must be either the owner of the token or its admin.2068	/// * `token_id` - The token for which the properties are being set.2069	/// * `properties` - Properties to be set.2070	/// * `budget` - Budget for setting properties.2071	fn set_token_properties(2072		&self,2073		sender: T::CrossAccountId,2074		token_id: TokenId,2075		properties: Vec<Property>,2076		budget: &dyn Budget,2077	) -> DispatchResultWithPostInfo;20782079	/// Remove token properties.2080	///2081	/// The appropriate [`PropertyPermission`] for the token property2082	/// must be set with [`Self::set_token_property_permissions`].2083	///2084	/// * `sender` - Must be either the owner of the token or its admin.2085	/// * `token_id` - The token for which the properties are being remove.2086	/// * `property_keys` - Keys to remove corresponding properties.2087	/// * `budget` - Budget for removing properties.2088	fn delete_token_properties(2089		&self,2090		sender: T::CrossAccountId,2091		token_id: TokenId,2092		property_keys: Vec<PropertyKey>,2093		budget: &dyn Budget,2094	) -> DispatchResultWithPostInfo;20952096	/// Set token property permissions.2097	///2098	/// * `sender` - Must be either the owner of the token or its admin.2099	/// * `token_id` - The token for which the properties are being set.2100	/// * `property_permissions` - Property permissions to be set.2101	/// * `budget` - Budget for setting properties.2102	fn set_token_property_permissions(2103		&self,2104		sender: &T::CrossAccountId,2105		property_permissions: Vec<PropertyKeyPermission>,2106	) -> DispatchResultWithPostInfo;21072108	/// Transfer amount of token pieces.2109	///2110	/// * `sender` - Donor user.2111	/// * `to` - Recepient user.2112	/// * `token` - The token of which parts are being sent.2113	/// * `amount` - The number of parts of the token that will be transferred.2114	/// * `budget` - The maximum budget that can be spent on the transfer.2115	fn transfer(2116		&self,2117		sender: T::CrossAccountId,2118		to: T::CrossAccountId,2119		token: TokenId,2120		amount: u128,2121		budget: &dyn Budget,2122	) -> DispatchResultWithPostInfo;21232124	/// Grant access to another account to transfer parts of the token owned by the calling user via [Self::transfer_from].2125	///2126	/// * `sender` - The user who grants access to the token.2127	/// * `spender` - The user to whom the rights are granted.2128	/// * `token` - The token to which access is granted.2129	/// * `amount` - The amount of pieces that another user can dispose of.2130	fn approve(2131		&self,2132		sender: T::CrossAccountId,2133		spender: T::CrossAccountId,2134		token: TokenId,2135		amount: u128,2136	) -> DispatchResultWithPostInfo;21372138	/// Grant access to another account to transfer parts of the token owned by the calling user's eth mirror via [Self::transfer_from].2139	///2140	/// * `sender` - The user who grants access to the token.2141	/// * `from` - Spender's eth mirror.2142	/// * `to` - The user to whom the rights are granted.2143	/// * `token` - The token to which access is granted.2144	/// * `amount` - The amount of pieces that another user can dispose of.2145	fn approve_from(2146		&self,2147		sender: T::CrossAccountId,2148		from: T::CrossAccountId,2149		to: T::CrossAccountId,2150		token: TokenId,2151		amount: u128,2152	) -> DispatchResultWithPostInfo;21532154	/// Send parts of a token owned by another user.2155	///2156	/// Before calling this method, you must grant rights to the calling user via [`Self::approve`].2157	///2158	/// * `sender` - The user who must have access to the token (see [`Self::approve`]).2159	/// * `from` - The user who owns the token.2160	/// * `to` - Recepient user.2161	/// * `token` - The token of which parts are being sent.2162	/// * `amount` - The number of parts of the token that will be transferred.2163	/// * `budget` - The maximum budget that can be spent on the transfer.2164	fn transfer_from(2165		&self,2166		sender: T::CrossAccountId,2167		from: T::CrossAccountId,2168		to: T::CrossAccountId,2169		token: TokenId,2170		amount: u128,2171		budget: &dyn Budget,2172	) -> DispatchResultWithPostInfo;21732174	/// Burn parts of a token owned by another user.2175	///2176	/// Before calling this method, you must grant rights to the calling user via [`Self::approve`].2177	///2178	/// * `sender` - The user who must have access to the token (see [`Self::approve`]).2179	/// * `from` - The user who owns the token.2180	/// * `token` - The token of which parts are being sent.2181	/// * `amount` - The number of parts of the token that will be transferred.2182	/// * `budget` - The maximum budget that can be spent on the burn.2183	fn burn_from(2184		&self,2185		sender: T::CrossAccountId,2186		from: T::CrossAccountId,2187		token: TokenId,2188		amount: u128,2189		budget: &dyn Budget,2190	) -> DispatchResultWithPostInfo;21912192	/// Check permission to nest token.2193	///2194	/// * `sender` - The user who initiated the check.2195	/// * `from` - The token that is checked for embedding.2196	/// * `under` - Token under which to check.2197	/// * `budget` - The maximum budget that can be spent on the check.2198	fn check_nesting(2199		&self,2200		sender: T::CrossAccountId,2201		from: (CollectionId, TokenId),2202		under: TokenId,2203		budget: &dyn Budget,2204	) -> DispatchResult;22052206	/// Nest one token into another.2207	///2208	/// * `under` - Token holder.2209	/// * `to_nest` - Nested token.2210	fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));22112212	/// Unnest token.2213	///2214	/// * `under` - Token holder.2215	/// * `to_nest` - Token to unnest.2216	fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId));22172218	/// Get all user tokens.2219	///2220	/// * `account` - Account for which you need to get tokens.2221	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;22222223	/// Get all the tokens in the collection.2224	fn collection_tokens(&self) -> Vec<TokenId>;22252226	/// Check if the token exists.2227	///2228	/// * `token` - Id token to check.2229	fn token_exists(&self, token: TokenId) -> bool;22302231	/// Get the id of the last minted token.2232	fn last_token_id(&self) -> TokenId;22332234	/// Get the owner of the token.2235	///2236	/// * `token` - The token for which you need to find out the owner.2237	fn token_owner(&self, token: TokenId) -> Result<T::CrossAccountId, TokenOwnerError>;22382239	/// Returns 10 tokens owners in no particular order.2240	///2241	/// * `token` - The token for which you need to find out the owners.2242	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId>;22432244	/// Get the value of the token property by key.2245	///2246	/// * `token` - Token with the property to get.2247	/// * `key` - Property name.2248	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;22492250	/// Get a set of token properties by key vector.2251	///2252	/// * `token` - Token with the property to get.2253	/// * `keys` - Vector of property keys. If this parameter is [None](sp_std::result::Result),2254	/// then all properties are returned.2255	fn token_properties(&self, token: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;22562257	/// Amount of unique collection tokens2258	fn total_supply(&self) -> u32;22592260	/// Amount of different tokens account has.2261	///2262	/// * `account` - The account for which need to get the balance.2263	fn account_balance(&self, account: T::CrossAccountId) -> u32;22642265	/// Amount of specific token account have.2266	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;22672268	/// Amount of token pieces2269	fn total_pieces(&self, token: TokenId) -> Option<u128>;22702271	/// Get the number of parts of the token that a trusted user can manage.2272	///2273	/// * `sender` - Trusted user.2274	/// * `spender` - Owner of the token.2275	/// * `token` - The token for which to get the value.2276	fn allowance(2277		&self,2278		sender: T::CrossAccountId,2279		spender: T::CrossAccountId,2280		token: TokenId,2281	) -> u128;22822283	/// Get extension for RFT collection.2284	fn refungible_extensions(&self) -> Option<&dyn RefungibleExtensions<T>>;22852286	/// The `operator` is allowed to transfer all tokens of the `owner` on their behalf.2287	/// * `owner` - Token owner2288	/// * `operator` - Operator2289	/// * `approve` - Should operator status be granted or revoked?2290	fn set_allowance_for_all(2291		&self,2292		owner: T::CrossAccountId,2293		operator: T::CrossAccountId,2294		approve: bool,2295	) -> DispatchResultWithPostInfo;22962297	/// Tells whether the given `owner` approves the `operator`.2298	fn allowance_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool;22992300	/// Repairs a possibly broken item.2301	fn repair_item(&self, token: TokenId) -> DispatchResultWithPostInfo;2302}23032304/// Extension for RFT collection.2305pub trait RefungibleExtensions<T>2306where2307	T: Config,2308{2309	/// Change the number of parts of the token.2310	///2311	/// When the value changes down, this function is equivalent to burning parts of the token.2312	///2313	/// * `sender` - The user calling the repartition operation. Must be the owner of the token.2314	/// * `token` - The token for which you want to change the number of parts.2315	/// * `amount` - The new value of the parts of the token.2316	fn repartition(2317		&self,2318		sender: &T::CrossAccountId,2319		token: TokenId,2320		amount: u128,2321	) -> DispatchResultWithPostInfo;2322}23232324/// Merge [`DispatchResult`] with [`Weight`] into [`DispatchResultWithPostInfo`].2325///2326/// Used for [`CommonCollectionOperations`] implementations and flexible enough to do so.2327pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {2328	let post_info = PostDispatchInfo {2329		actual_weight: Some(weight),2330		pays_fee: Pays::Yes,2331	};2332	match res {2333		Ok(()) => Ok(post_info),2334		Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),2335	}2336}23372338impl<T: Config> From<PropertiesError> for Error<T> {2339	fn from(error: PropertiesError) -> Self {2340		match error {2341			PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,2342			PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,2343			PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,2344			PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,2345			PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,2346		}2347	}2348}
after · pallets/common/src/lib.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Common pallet18//!19//! The Common pallet provides functionality for handling collections.20//!21//! ## Overview22//!23//! The Common pallet provides an interface for common collection operations for different collection types24//! (see [CommonCollectionOperations]), as well as a generic dispatcher for these, see [dispatch] module.25//! It also provides this functionality to EVM, see [erc] and [eth] modules.26//!27//! The Common pallet provides functions for:28//!29//! - Setting and approving collection sponsor.30//! - Get\set\delete allow list.31//! - Get\set\delete collection properties.32//! - Get\set\delete collection property permissions.33//! - Get\set\delete token property permissions.34//! - Get\set\delete collection administrators.35//! - Checking access permissions.36//!37//! ### Terminology38//! **Collection sponsor** - For the collection, you can set a sponsor, at whose expense it will39//! be possible to mint tokens.40//!41//! **Allow list** - List of users who have the right to minting tokens.42//!43//! **Collection properties** - Collection properties are simply key-value stores where various44//! metadata can be placed.45//!46//! **Permissions on token properties** - For each property in the token can be set permission47//! to change, see [`PropertyPermission`].48//!49//! **Collection administrator** - For a collection, you can set administrators who have the right50//! to most actions on the collection.5152#![warn(missing_docs)]53#![cfg_attr(not(feature = "std"), no_std)]54extern crate alloc;5556use core::{57	ops::{Deref, DerefMut},58	slice::from_ref,59};60use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};61use sp_std::vec::Vec;62use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};63use evm_coder::ToLog;64use frame_support::{65	dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, Weight, PostDispatchInfo},66	ensure,67	traits::{68		Get,69		fungible::{Balanced, Debt, Inspect},70		tokens::{Imbalance, Precision, Preservation},71	},72	dispatch::Pays,73	transactional, fail,74};75use up_data_structs::{76	AccessMode, COLLECTION_NUMBER_LIMIT, Collection, RpcCollection, CollectionFlags,77	RpcCollectionFlags, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH,78	COLLECTION_ADMINS_LIMIT, TokenId, TokenChild, CollectionStats, MAX_TOKEN_OWNERSHIP,79	CollectionMode, NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,80	REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits,81	CreateCollectionData, SponsorshipState, CreateItemExData, SponsoringRateLimit, budget::Budget,82	PhantomType, Property, CollectionProperties as CollectionPropertiesT, TokenProperties,83	PropertiesPermissionMap, PropertyKey, PropertyValue, PropertyPermission, PropertiesError,84	TokenOwnerError, PropertyKeyPermission, TokenData, TrySetProperty, PropertyScope,85	CollectionPermissions,86};87use up_pov_estimate_rpc::PovInfo;8889pub use pallet::*;90use sp_core::H160;91use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, traits::Zero};9293#[cfg(feature = "runtime-benchmarks")]94pub mod benchmarking;95pub mod dispatch;96pub mod erc;97pub mod eth;98pub mod helpers;99#[allow(missing_docs)]100pub mod weights;101/// Weight info.102pub type SelfWeightOf<T> = <T as Config>::WeightInfo;103104/// Collection handle contains information about collection data and id.105/// Also provides functionality to count consumed gas.106///107/// CollectionHandle is used as a generic wrapper for collections of all types (except native fungible).108/// It allows to perform common operations and queries on any collection type,109/// both completely general for all, as well as their respective implementations of [`CommonCollectionOperations`].110#[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]111pub struct CollectionHandle<T: Config> {112	/// Collection id113	pub id: CollectionId,114	collection: Collection<T::AccountId>,115	/// Substrate recorder for counting consumed gas116	pub recorder: SubstrateRecorder<T>,117}118119impl<T: Config> WithRecorder<T> for CollectionHandle<T> {120	fn recorder(&self) -> &SubstrateRecorder<T> {121		&self.recorder122	}123	fn into_recorder(self) -> SubstrateRecorder<T> {124		self.recorder125	}126}127128impl<T: Config> CollectionHandle<T> {129	/// Same as [CollectionHandle::new] but with an explicit gas limit.130	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {131		Self::new_with_recorder(id, SubstrateRecorder::new(gas_limit))132	}133134	/// Same as [CollectionHandle::new] but with an existed [`SubstrateRecorder`].135	pub fn new_with_recorder(id: CollectionId, recorder: SubstrateRecorder<T>) -> Option<Self> {136		<CollectionById<T>>::get(id).map(|collection| Self {137			id,138			collection,139			recorder,140		})141	}142143	/// Retrives collection data from storage and creates collection handle with default parameters.144	/// If collection not found return `None`145	pub fn new(id: CollectionId) -> Option<Self> {146		Self::new_with_gas_limit(id, u64::MAX)147	}148149	/// Same as [`CollectionHandle::new`] but if collection not found [CollectionNotFound](Error::CollectionNotFound) returned.150	pub fn try_get(id: CollectionId) -> Result<Self, DispatchError> {151		Ok(Self::new(id).ok_or(<Error<T>>::CollectionNotFound)?)152	}153154	/// Consume gas for reading.155	pub fn consume_store_reads(156		&self,157		reads: u64,158	) -> pallet_evm_coder_substrate::execution::Result<()> {159		self.recorder().consume_store_reads(reads)160	}161162	/// Consume gas for writing.163	pub fn consume_store_writes(164		&self,165		writes: u64,166	) -> pallet_evm_coder_substrate::execution::Result<()> {167		self.recorder().consume_store_writes(writes)168	}169170	/// Consume gas for reading and writing.171	pub fn consume_store_reads_and_writes(172		&self,173		reads: u64,174		writes: u64,175	) -> pallet_evm_coder_substrate::execution::Result<()> {176		self.recorder()177			.consume_store_reads_and_writes(reads, writes)178	}179180	/// Save collection to storage.181	pub fn save(&self) -> DispatchResult {182		<CollectionById<T>>::insert(self.id, &self.collection);183		Ok(())184	}185186	/// Set collection sponsor.187	///188	/// Unique collections allows sponsoring for certain actions.189	/// This method allows you to set the sponsor of the collection.190	/// In order for sponsorship to become active, it must be confirmed through [`Self::confirm_sponsorship`].191	pub fn set_sponsor(192		&mut self,193		sender: &T::CrossAccountId,194		sponsor: T::AccountId,195	) -> DispatchResult {196		self.check_is_internal()?;197		self.check_is_owner_or_admin(sender)?;198199		self.collection.sponsorship = SponsorshipState::Unconfirmed(sponsor.clone());200201		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(self.id, sponsor));202		<PalletEvm<T>>::deposit_log(203			erc::CollectionHelpersEvents::CollectionChanged {204				collection_id: eth::collection_id_to_address(self.id),205			}206			.to_log(T::ContractAddress::get()),207		);208209		self.save()210	}211212	/// Force set `sponsor`.213	///214	/// Differs from [`set_sponsor`][`Self::set_sponsor`] in that confirmation215	/// from the `sponsor` is not required.216	///217	/// # Arguments218	///219	/// * `sender`: Caller's account.220	/// * `sponsor`: ID of the account of the sponsor-to-be.221	pub fn force_set_sponsor(&mut self, sponsor: T::AccountId) -> DispatchResult {222		self.check_is_internal()?;223224		self.collection.sponsorship = SponsorshipState::Confirmed(sponsor.clone());225226		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorSet(self.id, sponsor.clone()));227		<Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(self.id, sponsor));228		<PalletEvm<T>>::deposit_log(229			erc::CollectionHelpersEvents::CollectionChanged {230				collection_id: eth::collection_id_to_address(self.id),231			}232			.to_log(T::ContractAddress::get()),233		);234235		self.save()236	}237238	/// Confirm sponsorship239	///240	/// In order for the sponsorship to become active, the user set as the sponsor must confirm their participation.241	/// Before confirming sponsorship, the user must be specified as the sponsor of the collection via [`Self::set_sponsor`].242	pub fn confirm_sponsorship(&mut self, sender: &T::AccountId) -> DispatchResult {243		self.check_is_internal()?;244		ensure!(245			self.collection.sponsorship.pending_sponsor() == Some(sender),246			Error::<T>::ConfirmSponsorshipFail247		);248249		self.collection.sponsorship = SponsorshipState::Confirmed(sender.clone());250251		<Pallet<T>>::deposit_event(Event::<T>::SponsorshipConfirmed(self.id, sender.clone()));252		<PalletEvm<T>>::deposit_log(253			erc::CollectionHelpersEvents::CollectionChanged {254				collection_id: eth::collection_id_to_address(self.id),255			}256			.to_log(T::ContractAddress::get()),257		);258259		self.save()260	}261262	/// Remove collection sponsor.263	pub fn remove_sponsor(&mut self, sender: &T::CrossAccountId) -> DispatchResult {264		self.check_is_internal()?;265		self.check_is_owner_or_admin(sender)?;266267		self.collection.sponsorship = SponsorshipState::Disabled;268269		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(self.id));270		<PalletEvm<T>>::deposit_log(271			erc::CollectionHelpersEvents::CollectionChanged {272				collection_id: eth::collection_id_to_address(self.id),273			}274			.to_log(T::ContractAddress::get()),275		);276		self.save()277	}278279	/// Force remove `sponsor`.280	///281	/// Differs from `remove_sponsor` in that282	/// it doesn't require consent from the `owner` of the collection.283	pub fn force_remove_sponsor(&mut self) -> DispatchResult {284		self.check_is_internal()?;285286		self.collection.sponsorship = SponsorshipState::Disabled;287288		<Pallet<T>>::deposit_event(Event::<T>::CollectionSponsorRemoved(self.id));289		<PalletEvm<T>>::deposit_log(290			erc::CollectionHelpersEvents::CollectionChanged {291				collection_id: eth::collection_id_to_address(self.id),292			}293			.to_log(T::ContractAddress::get()),294		);295		self.save()296	}297298	/// Checks that the collection was created with, and must be operated upon through **Unique API**.299	/// Now check only the `external` flag and if it's **true**, then return [`Error::CollectionIsExternal`] error.300	pub fn check_is_internal(&self) -> DispatchResult {301		if self.flags.external {302			return Err(<Error<T>>::CollectionIsExternal)?;303		}304305		Ok(())306	}307308	/// Checks that the collection was created with, and must be operated upon through an **assimilated API**.309	/// Now check only the `external` flag and if it's **false**, then return [`Error::CollectionIsInternal`] error.310	pub fn check_is_external(&self) -> DispatchResult {311		if !self.flags.external {312			return Err(<Error<T>>::CollectionIsInternal)?;313		}314315		Ok(())316	}317}318319impl<T: Config> Deref for CollectionHandle<T> {320	type Target = Collection<T::AccountId>;321322	fn deref(&self) -> &Self::Target {323		&self.collection324	}325}326327impl<T: Config> DerefMut for CollectionHandle<T> {328	fn deref_mut(&mut self) -> &mut Self::Target {329		&mut self.collection330	}331}332333impl<T: Config> CollectionHandle<T> {334	/// Checks if the `user` is the owner of the collection.335	pub fn check_is_owner(&self, user: &T::CrossAccountId) -> DispatchResult {336		ensure!(*user.as_sub() == self.owner, <Error<T>>::NoPermission);337		Ok(())338	}339340	/// Returns **true** if the `user` is the owner or administrator of the collection.341	pub fn is_owner_or_admin(&self, user: &T::CrossAccountId) -> bool {342		*user.as_sub() == self.owner || <IsAdmin<T>>::get((self.id, user))343	}344345	/// Checks if the `user` is the owner or administrator of the collection.346	pub fn check_is_owner_or_admin(&self, user: &T::CrossAccountId) -> DispatchResult {347		ensure!(self.is_owner_or_admin(user), <Error<T>>::NoPermission);348		Ok(())349	}350351	/// Returns **true** if352	/// * the `user`is a collection owner or admin353	/// * the collection limits allow the owner/admins to transfer/burn any collection token354	pub fn ignores_token_restrictions(&self, user: &T::CrossAccountId) -> bool {355		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)356	}357358	/// Return **true** if `user` does not have enough token parts, and he can ignore such restrictions.359	pub fn ignores_owned_amount(&self, user: &T::CrossAccountId) -> bool {360		self.limits.owner_can_transfer() && self.is_owner_or_admin(user)361	}362363	/// Checks if the user is in the allow list. If not [Error::AddressNotInAllowlist] returns.364	pub fn check_allowlist(&self, user: &T::CrossAccountId) -> DispatchResult {365		ensure!(366			<Allowlist<T>>::get((self.id, user)),367			<Error<T>>::AddressNotInAllowlist368		);369		Ok(())370	}371372	/// Changes collection owner to another account373	/// #### Store read/writes374	/// 1 writes375	pub fn change_owner(376		&mut self,377		caller: T::CrossAccountId,378		new_owner: T::CrossAccountId,379	) -> DispatchResult {380		self.check_is_internal()?;381		self.check_is_owner(&caller)?;382		self.collection.owner = new_owner.as_sub().clone();383384		<Pallet<T>>::deposit_event(Event::<T>::CollectionOwnerChanged(385			self.id,386			new_owner.as_sub().clone(),387		));388		<PalletEvm<T>>::deposit_log(389			erc::CollectionHelpersEvents::CollectionChanged {390				collection_id: eth::collection_id_to_address(self.id),391			}392			.to_log(T::ContractAddress::get()),393		);394395		self.save()396	}397}398399#[frame_support::pallet]400pub mod pallet {401402	use super::*;403	use dispatch::CollectionDispatch;404	use frame_support::{Blake2_128Concat, pallet_prelude::*, storage::Key, traits::StorageVersion};405	use up_data_structs::{TokenId, mapping::TokenAddressMapping};406	use scale_info::TypeInfo;407	use weights::WeightInfo;408409	#[pallet::config]410	pub trait Config:411		frame_system::Config + pallet_evm_coder_substrate::Config + pallet_evm::Config + TypeInfo412	{413		/// Weight information for functions of this pallet.414		type WeightInfo: WeightInfo;415416		/// Events compatible with [`frame_system::Config::Event`].417		type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;418419		/// Handler of accounts and payment.420		type Currency: Balanced<Self::AccountId> + Inspect<Self::AccountId>;421422		/// Set price to create a collection.423		#[pallet::constant]424		type CollectionCreationPrice: Get<425			<<Self as Config>::Currency as Inspect<Self::AccountId>>::Balance,426		>;427428		/// Dispatcher of operations on collections.429		type CollectionDispatch: CollectionDispatch<Self>;430431		/// Account which holds the chain's treasury.432		type TreasuryAccountId: Get<Self::AccountId>;433434		/// Address under which the CollectionHelper contract would be available.435		#[pallet::constant]436		type ContractAddress: Get<H160>;437438		/// Mapper for token addresses to Ethereum addresses.439		type EvmTokenAddressMapping: TokenAddressMapping<H160>;440441		/// Mapper for token addresses to [`CrossAccountId`].442		type CrossTokenAddressMapping: TokenAddressMapping<Self::CrossAccountId>;443	}444445	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);446	/// Collection id for native fungible collction.447	pub const NATIVE_FUNGIBLE_COLLECTION_ID: CollectionId = CollectionId(0);448449	#[pallet::pallet]450	#[pallet::storage_version(STORAGE_VERSION)]451	pub struct Pallet<T>(_);452453	#[pallet::extra_constants]454	impl<T: Config> Pallet<T> {455		/// Maximum admins per collection.456		pub fn collection_admins_limit() -> u32 {457			COLLECTION_ADMINS_LIMIT458		}459	}460461	#[pallet::genesis_config]462	pub struct GenesisConfig<T>(PhantomData<T>);463464	#[cfg(feature = "std")]465	impl<T: Config> Default for GenesisConfig<T> {466		fn default() -> Self {467			Self(Default::default())468		}469	}470471	#[pallet::genesis_build]472	impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {473		fn build(&self) {474			StorageVersion::new(1).put::<Pallet<T>>();475		}476	}477478	impl<T: Config> Pallet<T> {479		/// Helper function that handles deposit events480		pub fn deposit_event(event: Event<T>) {481			let event = <T as Config>::RuntimeEvent::from(event);482			let event = event.into();483			<frame_system::Pallet<T>>::deposit_event(event)484		}485	}486487	#[pallet::event]488	pub enum Event<T: Config> {489		/// New collection was created490		CollectionCreated(491			/// Globally unique identifier of newly created collection.492			CollectionId,493			/// [`CollectionMode`] converted into _u8_.494			u8,495			/// Collection owner.496			T::AccountId,497		),498499		/// New collection was destroyed500		CollectionDestroyed(501			/// Globally unique identifier of collection.502			CollectionId,503		),504505		/// New item was created.506		ItemCreated(507			/// Id of the collection where item was created.508			CollectionId,509			/// Id of an item. Unique within the collection.510			TokenId,511			/// Owner of newly created item512			T::CrossAccountId,513			/// Always 1 for NFT514			u128,515		),516517		/// Collection item was burned.518		ItemDestroyed(519			/// Id of the collection where item was destroyed.520			CollectionId,521			/// Identifier of burned NFT.522			TokenId,523			/// Which user has destroyed its tokens.524			T::CrossAccountId,525			/// Amount of token pieces destroed. Always 1 for NFT.526			u128,527		),528529		/// Item was transferred530		Transfer(531			/// Id of collection to which item is belong.532			CollectionId,533			/// Id of an item.534			TokenId,535			/// Original owner of item.536			T::CrossAccountId,537			/// New owner of item.538			T::CrossAccountId,539			/// Amount of token pieces transfered. Always 1 for NFT.540			u128,541		),542543		/// Amount pieces of token owned by `sender` was approved for `spender`.544		Approved(545			/// Id of collection to which item is belong.546			CollectionId,547			/// Id of an item.548			TokenId,549			/// Original owner of item.550			T::CrossAccountId,551			/// Id for which the approval was granted.552			T::CrossAccountId,553			/// Amount of token pieces transfered. Always 1 for NFT.554			u128,555		),556557		/// A `sender` approves operations on all owned tokens for `spender`.558		ApprovedForAll(559			/// Id of collection to which item is belong.560			CollectionId,561			/// Owner of a wallet.562			T::CrossAccountId,563			/// Id for which operator status was granted or rewoked.564			T::CrossAccountId,565			/// Is operator status granted or revoked?566			bool,567		),568569		/// The colletion property has been added or edited.570		CollectionPropertySet(571			/// Id of collection to which property has been set.572			CollectionId,573			/// The property that was set.574			PropertyKey,575		),576577		/// The property has been deleted.578		CollectionPropertyDeleted(579			/// Id of collection to which property has been deleted.580			CollectionId,581			/// The property that was deleted.582			PropertyKey,583		),584585		/// The token property has been added or edited.586		TokenPropertySet(587			/// Identifier of the collection whose token has the property set.588			CollectionId,589			/// The token for which the property was set.590			TokenId,591			/// The property that was set.592			PropertyKey,593		),594595		/// The token property has been deleted.596		TokenPropertyDeleted(597			/// Identifier of the collection whose token has the property deleted.598			CollectionId,599			/// The token for which the property was deleted.600			TokenId,601			/// The property that was deleted.602			PropertyKey,603		),604605		/// The token property permission of a collection has been set.606		PropertyPermissionSet(607			/// ID of collection to which property permission has been set.608			CollectionId,609			/// The property permission that was set.610			PropertyKey,611		),612613		/// Address was added to the allow list.614		AllowListAddressAdded(615			/// ID of the affected collection.616			CollectionId,617			/// Address of the added account.618			T::CrossAccountId,619		),620621		/// Address was removed from the allow list.622		AllowListAddressRemoved(623			/// ID of the affected collection.624			CollectionId,625			/// Address of the removed account.626			T::CrossAccountId,627		),628629		/// Collection admin was added.630		CollectionAdminAdded(631			/// ID of the affected collection.632			CollectionId,633			/// Admin address.634			T::CrossAccountId,635		),636637		/// Collection admin was removed.638		CollectionAdminRemoved(639			/// ID of the affected collection.640			CollectionId,641			/// Removed admin address.642			T::CrossAccountId,643		),644645		/// Collection limits were set.646		CollectionLimitSet(647			/// ID of the affected collection.648			CollectionId,649		),650651		/// Collection owned was changed.652		CollectionOwnerChanged(653			/// ID of the affected collection.654			CollectionId,655			/// New owner address.656			T::AccountId,657		),658659		/// Collection permissions were set.660		CollectionPermissionSet(661			/// ID of the affected collection.662			CollectionId,663		),664665		/// Collection sponsor was set.666		CollectionSponsorSet(667			/// ID of the affected collection.668			CollectionId,669			/// New sponsor address.670			T::AccountId,671		),672673		/// New sponsor was confirm.674		SponsorshipConfirmed(675			/// ID of the affected collection.676			CollectionId,677			/// New sponsor address.678			T::AccountId,679		),680681		/// Collection sponsor was removed.682		CollectionSponsorRemoved(683			/// ID of the affected collection.684			CollectionId,685		),686	}687688	#[pallet::error]689	pub enum Error<T> {690		/// This collection does not exist.691		CollectionNotFound,692		/// Sender parameter and item owner must be equal.693		MustBeTokenOwner,694		/// No permission to perform action695		NoPermission,696		/// Destroying only empty collections is allowed697		CantDestroyNotEmptyCollection,698		/// Collection is not in mint mode.699		PublicMintingNotAllowed,700		/// Address is not in allow list.701		AddressNotInAllowlist,702703		/// Collection name can not be longer than 63 char.704		CollectionNameLimitExceeded,705		/// Collection description can not be longer than 255 char.706		CollectionDescriptionLimitExceeded,707		/// Token prefix can not be longer than 15 char.708		CollectionTokenPrefixLimitExceeded,709		/// Total collections bound exceeded.710		TotalCollectionsLimitExceeded,711		/// Exceeded max admin count712		CollectionAdminCountExceeded,713		/// Collection limit bounds per collection exceeded714		CollectionLimitBoundsExceeded,715		/// Tried to enable permissions which are only permitted to be disabled716		OwnerPermissionsCantBeReverted,717		/// Collection settings not allowing items transferring718		TransferNotAllowed,719		/// Account token limit exceeded per collection720		AccountTokenLimitExceeded,721		/// Collection token limit exceeded722		CollectionTokenLimitExceeded,723		/// Metadata flag frozen724		MetadataFlagFrozen,725726		/// Item does not exist727		TokenNotFound,728		/// Item is balance not enough729		TokenValueTooLow,730		/// Requested value is more than the approved731		ApprovedValueTooLow,732		/// Tried to approve more than owned733		CantApproveMoreThanOwned,734		/// Only spending from eth mirror could be approved735		AddressIsNotEthMirror,736737		/// Can't transfer tokens to ethereum zero address738		AddressIsZero,739740		/// The operation is not supported741		UnsupportedOperation,742743		/// Insufficient funds to perform an action744		NotSufficientFounds,745746		/// User does not satisfy the nesting rule747		UserIsNotAllowedToNest,748		/// Only tokens from specific collections may nest tokens under this one749		SourceCollectionIsNotAllowedToNest,750751		/// Tried to store more data than allowed in collection field752		CollectionFieldSizeExceeded,753754		/// Tried to store more property data than allowed755		NoSpaceForProperty,756757		/// Tried to store more property keys than allowed758		PropertyLimitReached,759760		/// Property key is too long761		PropertyKeyIsTooLong,762763		/// Only ASCII letters, digits, and symbols `_`, `-`, and `.` are allowed764		InvalidCharacterInPropertyKey,765766		/// Empty property keys are forbidden767		EmptyPropertyKey,768769		/// Tried to access an external collection with an internal API770		CollectionIsExternal,771772		/// Tried to access an internal collection with an external API773		CollectionIsInternal,774775		/// This address is not set as sponsor, use setCollectionSponsor first.776		ConfirmSponsorshipFail,777778		/// The user is not an administrator.779		UserIsNotCollectionAdmin,780	}781782	/// Storage of the count of created collections. Essentially contains the last collection ID.783	#[pallet::storage]784	pub type CreatedCollectionCount<T> = StorageValue<Value = CollectionId, QueryKind = ValueQuery>;785786	/// Storage of the count of deleted collections.787	#[pallet::storage]788	pub type DestroyedCollectionCount<T> =789		StorageValue<Value = CollectionId, QueryKind = ValueQuery>;790791	/// Storage of collection info.792	#[pallet::storage]793	pub type CollectionById<T> = StorageMap<794		Hasher = Blake2_128Concat,795		Key = CollectionId,796		Value = Collection<<T as frame_system::Config>::AccountId>,797		QueryKind = OptionQuery,798	>;799800	/// Storage of collection properties.801	#[pallet::storage]802	#[pallet::getter(fn collection_properties)]803	pub type CollectionProperties<T> = StorageMap<804		Hasher = Blake2_128Concat,805		Key = CollectionId,806		Value = CollectionPropertiesT,807		QueryKind = ValueQuery,808	>;809810	/// Storage of token property permissions of a collection.811	#[pallet::storage]812	#[pallet::getter(fn property_permissions)]813	pub type CollectionPropertyPermissions<T> = StorageMap<814		Hasher = Blake2_128Concat,815		Key = CollectionId,816		Value = PropertiesPermissionMap,817		QueryKind = ValueQuery,818	>;819820	/// Storage of the amount of collection admins.821	#[pallet::storage]822	pub type AdminAmount<T> = StorageMap<823		Hasher = Blake2_128Concat,824		Key = CollectionId,825		Value = u32,826		QueryKind = ValueQuery,827	>;828829	/// List of collection admins.830	#[pallet::storage]831	pub type IsAdmin<T: Config> = StorageNMap<832		Key = (833			Key<Blake2_128Concat, CollectionId>,834			Key<Blake2_128Concat, T::CrossAccountId>,835		),836		Value = bool,837		QueryKind = ValueQuery,838	>;839840	/// Allowlisted collection users.841	#[pallet::storage]842	pub type Allowlist<T: Config> = StorageNMap<843		Key = (844			Key<Blake2_128Concat, CollectionId>,845			Key<Blake2_128Concat, T::CrossAccountId>,846		),847		Value = bool,848		QueryKind = ValueQuery,849	>;850851	/// Not used by code, exists only to provide some types to metadata.852	#[pallet::storage]853	pub type DummyStorageValue<T: Config> = StorageValue<854		Value = (855			CollectionStats,856			CollectionId,857			TokenId,858			TokenChild,859			PhantomType<(860				TokenData<T::CrossAccountId>,861				RpcCollection<T::AccountId>,862				// PoV Estimate Info863				PovInfo,864			)>,865		),866		QueryKind = OptionQuery,867	>;868}869870impl<T: Config> Pallet<T> {871	/// Enshure that receiver address is correct.872	///873	/// Ethereum receiver 0x0000000000000000000000000000000000000000 is reserved, and shouldn't own tokens.874	pub fn ensure_correct_receiver(receiver: &T::CrossAccountId) -> DispatchResult {875		ensure!(876			&T::CrossAccountId::from_eth(H160([0; 20])) != receiver,877			<Error<T>>::AddressIsZero878		);879		Ok(())880	}881882	/// Get a vector of collection admins.883	pub fn adminlist(collection: CollectionId) -> Vec<T::CrossAccountId> {884		<IsAdmin<T>>::iter_prefix((collection,))885			.map(|(a, _)| a)886			.collect()887	}888889	/// Get a vector of users allowed to mint tokens.890	pub fn allowlist(collection: CollectionId) -> Vec<T::CrossAccountId> {891		<Allowlist<T>>::iter_prefix((collection,))892			.map(|(a, _)| a)893			.collect()894	}895896	/// Is `user` allowed to mint token in `collection`.897	pub fn allowed(collection: CollectionId, user: T::CrossAccountId) -> bool {898		<Allowlist<T>>::get((collection, user))899	}900901	/// Get statistics of collections.902	pub fn collection_stats() -> CollectionStats {903		let created = <CreatedCollectionCount<T>>::get();904		let destroyed = <DestroyedCollectionCount<T>>::get();905		CollectionStats {906			created: created.0,907			destroyed: destroyed.0,908			alive: created.0 - destroyed.0,909		}910	}911912	/// Get the effective limits for the collection.913	pub fn effective_collection_limits(collection: CollectionId) -> Option<CollectionLimits> {914		let collection = <CollectionById<T>>::get(collection)?;915		let limits = collection.limits;916		let effective_limits = CollectionLimits {917			account_token_ownership_limit: Some(limits.account_token_ownership_limit()),918			sponsored_data_size: Some(limits.sponsored_data_size()),919			sponsored_data_rate_limit: Some(920				limits921					.sponsored_data_rate_limit922					.unwrap_or(SponsoringRateLimit::SponsoringDisabled),923			),924			token_limit: Some(limits.token_limit()),925			sponsor_transfer_timeout: Some(limits.sponsor_transfer_timeout(926				match collection.mode {927					CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,928					CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,929					CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,930				},931			)),932			sponsor_approve_timeout: Some(limits.sponsor_approve_timeout()),933			owner_can_transfer: Some(limits.owner_can_transfer()),934			owner_can_destroy: Some(limits.owner_can_destroy()),935			transfers_enabled: Some(limits.transfers_enabled()),936		};937938		Some(effective_limits)939	}940941	/// Returns information about the `collection` adapted for rpc.942	pub fn rpc_collection(collection: CollectionId) -> Option<RpcCollection<T::AccountId>> {943		let Collection {944			name,945			description,946			owner,947			mode,948			token_prefix,949			sponsorship,950			limits,951			permissions,952			flags,953		} = <CollectionById<T>>::get(collection)?;954955		let token_property_permissions = <CollectionPropertyPermissions<T>>::get(collection)956			.into_iter()957			.map(|(key, permission)| PropertyKeyPermission { key, permission })958			.collect();959960		let properties = <CollectionProperties<T>>::get(collection)961			.into_iter()962			.map(|(key, value)| Property { key, value })963			.collect();964965		let permissions = CollectionPermissions {966			access: Some(permissions.access()),967			mint_mode: Some(permissions.mint_mode()),968			nesting: Some(permissions.nesting().clone()),969		};970971		Some(RpcCollection {972			name: name.into_inner(),973			description: description.into_inner(),974			owner,975			mode,976			token_prefix: token_prefix.into_inner(),977			sponsorship,978			limits,979			permissions,980			token_property_permissions,981			properties,982			read_only: flags.external,983984			flags: RpcCollectionFlags {985				foreign: flags.foreign,986				erc721metadata: flags.erc721metadata,987			},988		})989	}990}991992macro_rules! limit_default {993	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{994		$(995			if let Some($new) = $new.$field {996				let $old = $old.$field($($arg)?);997				let _ = $new;998				let _ = $old;999				$check1000			} else {1001				$new.$field = $old.$field1002			}1003		)*1004	}};1005}1006macro_rules! limit_default_clone {1007	($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{1008		$(1009			if let Some($new) = $new.$field.clone() {1010				let $old = $old.$field($($arg)?);1011				let _ = $new;1012				let _ = $old;1013				$check1014			} else {1015				$new.$field = $old.$field.clone()1016			}1017		)*1018	}};1019}10201021impl<T: Config> Pallet<T> {1022	/// Create new collection.1023	///1024	/// * `owner` - The owner of the collection.1025	/// * `data` - Description of the created collection.1026	/// * `flags` - Extra flags to store.1027	pub fn init_collection(1028		owner: T::CrossAccountId,1029		payer: T::CrossAccountId,1030		data: CreateCollectionData<T::AccountId>,1031		flags: CollectionFlags,1032	) -> Result<CollectionId, DispatchError> {1033		{1034			ensure!(1035				data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize,1036				Error::<T>::CollectionTokenPrefixLimitExceeded1037			);1038		}10391040		let created_count = <CreatedCollectionCount<T>>::get()1041			.01042			.checked_add(1)1043			.ok_or(ArithmeticError::Overflow)?;1044		let destroyed_count = <DestroyedCollectionCount<T>>::get().0;1045		let id = CollectionId(created_count);10461047		// bound Total number of collections1048		ensure!(1049			created_count - destroyed_count <= COLLECTION_NUMBER_LIMIT,1050			<Error<T>>::TotalCollectionsLimitExceeded1051		);10521053		// =========10541055		let collection = Collection {1056			owner: owner.as_sub().clone(),1057			name: data.name,1058			mode: data.mode.clone(),1059			description: data.description,1060			token_prefix: data.token_prefix,1061			sponsorship: data1062				.pending_sponsor1063				.map(SponsorshipState::Unconfirmed)1064				.unwrap_or_default(),1065			limits: data1066				.limits1067				.map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits))1068				.unwrap_or_else(|| Ok(CollectionLimits::default()))?,1069			permissions: data1070				.permissions1071				.map(|permissions| {1072					Self::clamp_permissions(data.mode.clone(), &Default::default(), permissions)1073				})1074				.unwrap_or_else(|| Ok(CollectionPermissions::default()))?,1075			flags,1076		};10771078		let mut collection_properties = CollectionPropertiesT::new();1079		collection_properties1080			.try_set_from_iter(data.properties.into_iter())1081			.map_err(<Error<T>>::from)?;10821083		CollectionProperties::<T>::insert(id, collection_properties);10841085		let mut token_props_permissions = PropertiesPermissionMap::new();1086		token_props_permissions1087			.try_set_from_iter(data.token_property_permissions.into_iter())1088			.map_err(<Error<T>>::from)?;10891090		CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);10911092		// Take a (non-refundable) deposit of collection creation1093		{1094			let mut imbalance = <Debt<T::AccountId, <T as Config>::Currency>>::zero();1095			imbalance.subsume(<T as Config>::Currency::deposit(1096				&T::TreasuryAccountId::get(),1097				T::CollectionCreationPrice::get(),1098				Precision::Exact,1099			)?);1100			let credit =1101				<T as Config>::Currency::settle(payer.as_sub(), imbalance, Preservation::Preserve)1102					.map_err(|_| Error::<T>::NotSufficientFounds)?;11031104			debug_assert!(credit.peek().is_zero())1105		}11061107		<CreatedCollectionCount<T>>::put(created_count);1108		<Pallet<T>>::deposit_event(Event::CollectionCreated(1109			id,1110			data.mode.id(),1111			owner.as_sub().clone(),1112		));1113		<PalletEvm<T>>::deposit_log(1114			erc::CollectionHelpersEvents::CollectionCreated {1115				owner: *owner.as_eth(),1116				collection_id: eth::collection_id_to_address(id),1117			}1118			.to_log(T::ContractAddress::get()),1119		);1120		<CollectionById<T>>::insert(id, collection);1121		Ok(id)1122	}11231124	/// Destroy collection.1125	///1126	/// * `collection` - Collection handler.1127	/// * `sender` - The owner or administrator of the collection.1128	pub fn destroy_collection(1129		collection: CollectionHandle<T>,1130		sender: &T::CrossAccountId,1131	) -> DispatchResult {1132		ensure!(1133			collection.limits.owner_can_destroy(),1134			<Error<T>>::NoPermission,1135		);1136		collection.check_is_owner(sender)?;11371138		let destroyed_collections = <DestroyedCollectionCount<T>>::get()1139			.01140			.checked_add(1)1141			.ok_or(ArithmeticError::Overflow)?;11421143		// =========11441145		<DestroyedCollectionCount<T>>::put(destroyed_collections);1146		<CollectionById<T>>::remove(collection.id);1147		<AdminAmount<T>>::remove(collection.id);1148		let _ = <IsAdmin<T>>::clear_prefix((collection.id,), u32::MAX, None);1149		let _ = <Allowlist<T>>::clear_prefix((collection.id,), u32::MAX, None);1150		<CollectionProperties<T>>::remove(collection.id);11511152		<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));11531154		<PalletEvm<T>>::deposit_log(1155			erc::CollectionHelpersEvents::CollectionDestroyed {1156				collection_id: eth::collection_id_to_address(collection.id),1157			}1158			.to_log(T::ContractAddress::get()),1159		);1160		Ok(())1161	}11621163	/// This function sets or removes a collection properties according to1164	/// `properties_updates` contents:1165	/// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`1166	/// * removes a property under the <key> if the value is `None` `(<key>, None)`.1167	///1168	/// This function fires an event for each property change.1169	/// In case of an error, all the changes (including the events) will be reverted1170	/// since the function is transactional.1171	#[transactional]1172	fn modify_collection_properties(1173		collection: &CollectionHandle<T>,1174		sender: &T::CrossAccountId,1175		properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,1176	) -> DispatchResult {1177		collection.check_is_owner_or_admin(sender)?;11781179		let mut stored_properties = <CollectionProperties<T>>::get(collection.id);11801181		for (key, value) in properties_updates {1182			match value {1183				Some(value) => {1184					stored_properties1185						.try_set(key.clone(), value)1186						.map_err(<Error<T>>::from)?;11871188					Self::deposit_event(Event::CollectionPropertySet(collection.id, key));1189					<PalletEvm<T>>::deposit_log(1190						erc::CollectionHelpersEvents::CollectionChanged {1191							collection_id: eth::collection_id_to_address(collection.id),1192						}1193						.to_log(T::ContractAddress::get()),1194					);1195				}1196				None => {1197					stored_properties.remove(&key).map_err(<Error<T>>::from)?;11981199					Self::deposit_event(Event::CollectionPropertyDeleted(collection.id, key));1200					<PalletEvm<T>>::deposit_log(1201						erc::CollectionHelpersEvents::CollectionChanged {1202							collection_id: eth::collection_id_to_address(collection.id),1203						}1204						.to_log(T::ContractAddress::get()),1205					);1206				}1207			}1208		}12091210		<CollectionProperties<T>>::set(collection.id, stored_properties);12111212		Ok(())1213	}12141215	/// A batch operation to add, edit or remove properties for a token.1216	/// It sets or removes a token's properties according to1217	/// `properties_updates` contents:1218	/// * sets a property under the <key> with the value provided `(<key>, Some(<value>))`1219	/// * removes a property under the <key> if the value is `None` `(<key>, None)`.1220	///1221	/// - `nesting_budget`: Limit for searching parents in-depth to check ownership.1222	/// - `is_token_create`: Indicates that method is called during token initialization.1223	///   Allows to bypass ownership check.1224	///1225	/// All affected properties should have `mutable` permission1226	/// to be **deleted** or to be **set more than once**,1227	/// and the sender should have permission to edit those properties.1228	///1229	/// This function fires an event for each property change.1230	/// In case of an error, all the changes (including the events) will be reverted1231	/// since the function is transactional.1232	pub fn modify_token_properties(1233		collection: &CollectionHandle<T>,1234		sender: &T::CrossAccountId,1235		token_id: TokenId,1236		properties_updates: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,1237		is_token_create: bool,1238		mut stored_properties: TokenProperties,1239		is_token_owner: impl Fn() -> Result<bool, DispatchError>,1240		set_token_properties: impl FnOnce(TokenProperties),1241		log: evm_coder::ethereum::Log,1242	) -> DispatchResult {1243		let is_collection_admin = collection.is_owner_or_admin(sender);1244		let permissions = Self::property_permissions(collection.id);12451246		let mut token_owner_result = None;1247		let mut is_token_owner = || -> Result<bool, DispatchError> {1248			*token_owner_result.get_or_insert_with(&is_token_owner)1249		};12501251		for (key, value) in properties_updates {1252			let permission = permissions1253				.get(&key)1254				.cloned()1255				.unwrap_or_else(PropertyPermission::none);12561257			let is_property_exists = stored_properties.get(&key).is_some();12581259			match permission {1260				PropertyPermission { mutable: false, .. } if is_property_exists => {1261					return Err(<Error<T>>::NoPermission.into());1262				}12631264				PropertyPermission {1265					collection_admin,1266					token_owner,1267					..1268				} => {1269					//TODO: investigate threats during public minting.1270					let is_token_create =1271						is_token_create && (collection_admin || token_owner) && value.is_some();1272					if !(is_token_create1273						|| (collection_admin && is_collection_admin)1274						|| (token_owner && is_token_owner()?))1275					{1276						fail!(<Error<T>>::NoPermission);1277					}1278				}1279			}12801281			match value {1282				Some(value) => {1283					stored_properties1284						.try_set(key.clone(), value)1285						.map_err(<Error<T>>::from)?;12861287					Self::deposit_event(Event::TokenPropertySet(collection.id, token_id, key));1288				}1289				None => {1290					stored_properties.remove(&key).map_err(<Error<T>>::from)?;12911292					Self::deposit_event(Event::TokenPropertyDeleted(collection.id, token_id, key));1293				}1294			}12951296			<PalletEvm<T>>::deposit_log(log.clone());1297		}12981299		set_token_properties(stored_properties);13001301		Ok(())1302	}13031304	/// Sets or unsets the approval of a given operator.1305	///1306	/// The `operator` is allowed to transfer all token pieces of the `owner` on their behalf.1307	/// - `owner`: Token owner1308	/// - `operator`: Operator1309	/// - `approve`: Should operator status be granted or revoked?1310	pub fn set_allowance_for_all(1311		collection: &CollectionHandle<T>,1312		owner: &T::CrossAccountId,1313		operator: &T::CrossAccountId,1314		approve: bool,1315		set_allowance: impl FnOnce(),1316		log: evm_coder::ethereum::Log,1317	) -> DispatchResult {1318		if collection.permissions.access() == AccessMode::AllowList {1319			collection.check_allowlist(owner)?;1320			collection.check_allowlist(operator)?;1321		}13221323		Self::ensure_correct_receiver(operator)?;13241325		set_allowance();13261327		<PalletEvm<T>>::deposit_log(log);1328		Self::deposit_event(Event::ApprovedForAll(1329			collection.id,1330			owner.clone(),1331			operator.clone(),1332			approve,1333		));1334		Ok(())1335	}13361337	/// Set collection property.1338	///1339	/// * `collection` - Collection handler.1340	/// * `sender` - The owner or administrator of the collection.1341	/// * `property` - The property to set.1342	pub fn set_collection_property(1343		collection: &CollectionHandle<T>,1344		sender: &T::CrossAccountId,1345		property: Property,1346	) -> DispatchResult {1347		Self::set_collection_properties(collection, sender, [property].into_iter())1348	}13491350	/// Set a scoped collection property, where the scope is a special prefix1351	/// prohibiting a user access to change the property directly.1352	///1353	/// * `collection_id` - ID of the collection for which the property is being set.1354	/// * `scope` - Property scope.1355	/// * `property` - The property to set.1356	pub fn set_scoped_collection_property(1357		collection_id: CollectionId,1358		scope: PropertyScope,1359		property: Property,1360	) -> DispatchResult {1361		CollectionProperties::<T>::try_mutate(collection_id, |properties| {1362			properties.try_scoped_set(scope, property.key, property.value)1363		})1364		.map_err(<Error<T>>::from)?;13651366		Ok(())1367	}13681369	/// Set scoped collection properties, where the scope is a special prefix1370	/// prohibiting a user access to change the properties directly.1371	///1372	/// * `collection_id` - ID of the collection for which the properties is being set.1373	/// * `scope` - Property scope.1374	/// * `properties` - The properties to set.1375	pub fn set_scoped_collection_properties(1376		collection_id: CollectionId,1377		scope: PropertyScope,1378		properties: impl Iterator<Item = Property>,1379	) -> DispatchResult {1380		CollectionProperties::<T>::try_mutate(collection_id, |stored_properties| {1381			stored_properties.try_scoped_set_from_iter(scope, properties)1382		})1383		.map_err(<Error<T>>::from)?;13841385		Ok(())1386	}13871388	/// Set collection properties.1389	///1390	/// * `collection` - Collection handler.1391	/// * `sender` - The owner or administrator of the collection.1392	/// * `properties` - The properties to set.1393	pub fn set_collection_properties(1394		collection: &CollectionHandle<T>,1395		sender: &T::CrossAccountId,1396		properties: impl Iterator<Item = Property>,1397	) -> DispatchResult {1398		Self::modify_collection_properties(1399			collection,1400			sender,1401			properties.map(|property| (property.key, Some(property.value))),1402		)1403	}14041405	/// Delete collection property.1406	///1407	/// * `collection` - Collection handler.1408	/// * `sender` - The owner or administrator of the collection.1409	/// * `property` - The property to delete.1410	pub fn delete_collection_property(1411		collection: &CollectionHandle<T>,1412		sender: &T::CrossAccountId,1413		property_key: PropertyKey,1414	) -> DispatchResult {1415		Self::delete_collection_properties(collection, sender, [property_key].into_iter())1416	}14171418	/// Delete collection properties.1419	///1420	/// * `collection` - Collection handler.1421	/// * `sender` - The owner or administrator of the collection.1422	/// * `properties` - The properties to delete.1423	pub fn delete_collection_properties(1424		collection: &CollectionHandle<T>,1425		sender: &T::CrossAccountId,1426		property_keys: impl Iterator<Item = PropertyKey>,1427	) -> DispatchResult {1428		Self::modify_collection_properties(collection, sender, property_keys.map(|key| (key, None)))1429	}14301431	/// Set collection propetry permission without any checks.1432	///1433	/// Used for migrations.1434	///1435	/// * `collection` - Collection handler.1436	/// * `property_permissions` - Property permissions.1437	pub fn set_property_permission_unchecked(1438		collection: CollectionId,1439		property_permission: PropertyKeyPermission,1440	) -> DispatchResult {1441		<CollectionPropertyPermissions<T>>::try_mutate(collection, |permissions| {1442			permissions.try_set(property_permission.key, property_permission.permission)1443		})1444		.map_err(<Error<T>>::from)?;1445		Ok(())1446	}14471448	/// Set collection property permission.1449	///1450	/// * `collection` - Collection handler.1451	/// * `sender` - The owner or administrator of the collection.1452	/// * `property_permission` - Property permission.1453	pub fn set_property_permission(1454		collection: &CollectionHandle<T>,1455		sender: &T::CrossAccountId,1456		property_permission: PropertyKeyPermission,1457	) -> DispatchResult {1458		Self::set_scoped_property_permission(1459			collection,1460			sender,1461			PropertyScope::None,1462			property_permission,1463		)1464	}14651466	/// Set collection property permission with scope.1467	///1468	/// * `collection` - Collection handler.1469	/// * `sender` - The owner or administrator of the collection.1470	/// * `scope` - Property scope.1471	/// * `property_permission` - Property permission.1472	pub fn set_scoped_property_permission(1473		collection: &CollectionHandle<T>,1474		sender: &T::CrossAccountId,1475		scope: PropertyScope,1476		property_permission: PropertyKeyPermission,1477	) -> DispatchResult {1478		collection.check_is_owner_or_admin(sender)?;14791480		let all_permissions = CollectionPropertyPermissions::<T>::get(collection.id);1481		let current_permission = all_permissions.get(&property_permission.key);1482		if matches![1483			current_permission,1484			Some(PropertyPermission { mutable: false, .. })1485		] {1486			return Err(<Error<T>>::NoPermission.into());1487		}14881489		CollectionPropertyPermissions::<T>::try_mutate(collection.id, |permissions| {1490			let property_permission = property_permission.clone();1491			permissions.try_scoped_set(1492				scope,1493				property_permission.key,1494				property_permission.permission,1495			)1496		})1497		.map_err(<Error<T>>::from)?;14981499		Self::deposit_event(Event::PropertyPermissionSet(1500			collection.id,1501			property_permission.key,1502		));1503		<PalletEvm<T>>::deposit_log(1504			erc::CollectionHelpersEvents::CollectionChanged {1505				collection_id: eth::collection_id_to_address(collection.id),1506			}1507			.to_log(T::ContractAddress::get()),1508		);15091510		Ok(())1511	}15121513	/// Set token property permission.1514	///1515	/// * `collection` - Collection handler.1516	/// * `sender` - The owner or administrator of the collection.1517	/// * `property_permissions` - Property permissions.1518	#[transactional]1519	pub fn set_token_property_permissions(1520		collection: &CollectionHandle<T>,1521		sender: &T::CrossAccountId,1522		property_permissions: Vec<PropertyKeyPermission>,1523	) -> DispatchResult {1524		Self::set_scoped_token_property_permissions(1525			collection,1526			sender,1527			PropertyScope::None,1528			property_permissions,1529		)1530	}15311532	/// Set token property permission with scope.1533	///1534	/// * `collection` - Collection handler.1535	/// * `sender` - The owner or administrator of the collection.1536	/// * `scope` - Property scope.1537	/// * `property_permissions` - Property permissions.1538	#[transactional]1539	pub fn set_scoped_token_property_permissions(1540		collection: &CollectionHandle<T>,1541		sender: &T::CrossAccountId,1542		scope: PropertyScope,1543		property_permissions: Vec<PropertyKeyPermission>,1544	) -> DispatchResult {1545		for prop_pemission in property_permissions {1546			Self::set_scoped_property_permission(collection, sender, scope, prop_pemission)?;1547		}15481549		Ok(())1550	}15511552	/// Get collection property.1553	pub fn get_collection_property(1554		collection_id: CollectionId,1555		key: &PropertyKey,1556	) -> Option<PropertyValue> {1557		Self::collection_properties(collection_id).get(key).cloned()1558	}15591560	/// Convert byte vector to property key vector.1561	pub fn bytes_keys_to_property_keys(1562		keys: Vec<Vec<u8>>,1563	) -> Result<Vec<PropertyKey>, DispatchError> {1564		keys.into_iter()1565			.map(|key| -> Result<PropertyKey, DispatchError> {1566				key.try_into()1567					.map_err(|_| <Error<T>>::PropertyKeyIsTooLong.into())1568			})1569			.collect::<Result<Vec<PropertyKey>, DispatchError>>()1570	}15711572	/// Get properties according to given keys.1573	pub fn filter_collection_properties(1574		collection_id: CollectionId,1575		keys: Option<Vec<PropertyKey>>,1576	) -> Result<Vec<Property>, DispatchError> {1577		let properties = Self::collection_properties(collection_id);15781579		let properties = keys1580			.map(|keys| {1581				keys.into_iter()1582					.filter_map(|key| {1583						properties.get(&key).map(|value| Property {1584							key,1585							value: value.clone(),1586						})1587					})1588					.collect()1589			})1590			.unwrap_or_else(|| {1591				properties1592					.into_iter()1593					.map(|(key, value)| Property { key, value })1594					.collect()1595			});15961597		Ok(properties)1598	}15991600	/// Get property permissions according to given keys.1601	pub fn filter_property_permissions(1602		collection_id: CollectionId,1603		keys: Option<Vec<PropertyKey>>,1604	) -> Result<Vec<PropertyKeyPermission>, DispatchError> {1605		let permissions = Self::property_permissions(collection_id);16061607		let key_permissions = keys1608			.map(|keys| {1609				keys.into_iter()1610					.filter_map(|key| {1611						permissions1612							.get(&key)1613							.map(|permission| PropertyKeyPermission {1614								key,1615								permission: permission.clone(),1616							})1617					})1618					.collect()1619			})1620			.unwrap_or_else(|| {1621				permissions1622					.into_iter()1623					.map(|(key, permission)| PropertyKeyPermission { key, permission })1624					.collect()1625			});16261627		Ok(key_permissions)1628	}16291630	/// Toggle `user` participation in the `collection`'s allow list.1631	/// #### Store read/writes1632	/// 1 writes1633	pub fn toggle_allowlist(1634		collection: &CollectionHandle<T>,1635		sender: &T::CrossAccountId,1636		user: &T::CrossAccountId,1637		allowed: bool,1638	) -> DispatchResult {1639		collection.check_is_owner_or_admin(sender)?;16401641		// =========16421643		if allowed {1644			<Allowlist<T>>::insert((collection.id, user), true);1645			Self::deposit_event(Event::<T>::AllowListAddressAdded(1646				collection.id,1647				user.clone(),1648			));1649		} else {1650			<Allowlist<T>>::remove((collection.id, user));1651			Self::deposit_event(Event::<T>::AllowListAddressRemoved(1652				collection.id,1653				user.clone(),1654			));1655		}16561657		<PalletEvm<T>>::deposit_log(1658			erc::CollectionHelpersEvents::CollectionChanged {1659				collection_id: eth::collection_id_to_address(collection.id),1660			}1661			.to_log(T::ContractAddress::get()),1662		);16631664		Ok(())1665	}16661667	/// Toggle `user` participation in the `collection`'s admin list.1668	/// #### Store read/writes1669	/// 2 reads, 2 writes1670	pub fn toggle_admin(1671		collection: &CollectionHandle<T>,1672		sender: &T::CrossAccountId,1673		user: &T::CrossAccountId,1674		admin: bool,1675	) -> DispatchResult {1676		collection.check_is_internal()?;1677		collection.check_is_owner(sender)?;16781679		let is_admin = <IsAdmin<T>>::get((collection.id, user));1680		if is_admin == admin {1681			if admin {1682				return Ok(());1683			} else {1684				return Err(Error::<T>::UserIsNotCollectionAdmin.into());1685			}1686		}1687		let amount = <AdminAmount<T>>::get(collection.id);16881689		// =========16901691		if admin {1692			let amount = amount1693				.checked_add(1)1694				.ok_or(<Error<T>>::CollectionAdminCountExceeded)?;1695			ensure!(1696				amount <= Self::collection_admins_limit(),1697				<Error<T>>::CollectionAdminCountExceeded,1698			);16991700			<AdminAmount<T>>::insert(collection.id, amount);1701			<IsAdmin<T>>::insert((collection.id, user), true);17021703			Self::deposit_event(Event::<T>::CollectionAdminAdded(1704				collection.id,1705				user.clone(),1706			));1707		} else {1708			<AdminAmount<T>>::insert(collection.id, amount.saturating_sub(1));1709			<IsAdmin<T>>::remove((collection.id, user));17101711			Self::deposit_event(Event::<T>::CollectionAdminRemoved(1712				collection.id,1713				user.clone(),1714			));1715		}17161717		<PalletEvm<T>>::deposit_log(1718			erc::CollectionHelpersEvents::CollectionChanged {1719				collection_id: eth::collection_id_to_address(collection.id),1720			}1721			.to_log(T::ContractAddress::get()),1722		);17231724		Ok(())1725	}17261727	/// Update collection limits.1728	pub fn update_limits(1729		user: &T::CrossAccountId,1730		collection: &mut CollectionHandle<T>,1731		new_limit: CollectionLimits,1732	) -> DispatchResult {1733		collection.check_is_internal()?;1734		collection.check_is_owner_or_admin(user)?;17351736		collection.limits =1737			Self::clamp_limits(collection.mode.clone(), &collection.limits, new_limit)?;17381739		Self::deposit_event(Event::<T>::CollectionLimitSet(collection.id));1740		<PalletEvm<T>>::deposit_log(1741			erc::CollectionHelpersEvents::CollectionChanged {1742				collection_id: eth::collection_id_to_address(collection.id),1743			}1744			.to_log(T::ContractAddress::get()),1745		);17461747		collection.save()1748	}17491750	/// Merge set fields from `new_limit` to `old_limit`.1751	fn clamp_limits(1752		mode: CollectionMode,1753		old_limit: &CollectionLimits,1754		mut new_limit: CollectionLimits,1755	) -> Result<CollectionLimits, DispatchError> {1756		let limits = old_limit;1757		limit_default!(old_limit, new_limit,1758			account_token_ownership_limit => ensure!(1759				new_limit <= MAX_TOKEN_OWNERSHIP,1760				<Error<T>>::CollectionLimitBoundsExceeded,1761			),1762			sponsored_data_size => ensure!(1763				new_limit <= CUSTOM_DATA_LIMIT,1764				<Error<T>>::CollectionLimitBoundsExceeded,1765			),17661767			sponsored_data_rate_limit => {},1768			token_limit => ensure!(1769				old_limit >= new_limit && new_limit > 0,1770				<Error<T>>::CollectionTokenLimitExceeded1771			),17721773			sponsor_transfer_timeout(match mode {1774				CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT,1775				CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1776				CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT,1777			}) => ensure!(1778				new_limit <= MAX_SPONSOR_TIMEOUT,1779				<Error<T>>::CollectionLimitBoundsExceeded,1780			),1781			sponsor_approve_timeout => {},1782			owner_can_transfer => ensure!(1783				!limits.owner_can_transfer_instaled() ||1784				old_limit || !new_limit,1785				<Error<T>>::OwnerPermissionsCantBeReverted,1786			),1787			owner_can_destroy => ensure!(1788				old_limit || !new_limit,1789				<Error<T>>::OwnerPermissionsCantBeReverted,1790			),1791			transfers_enabled => {},1792		);1793		Ok(new_limit)1794	}17951796	/// Update collection permissions.1797	pub fn update_permissions(1798		user: &T::CrossAccountId,1799		collection: &mut CollectionHandle<T>,1800		new_permission: CollectionPermissions,1801	) -> DispatchResult {1802		collection.check_is_internal()?;1803		collection.check_is_owner_or_admin(user)?;1804		collection.permissions = Self::clamp_permissions(1805			collection.mode.clone(),1806			&collection.permissions,1807			new_permission,1808		)?;18091810		Self::deposit_event(Event::<T>::CollectionPermissionSet(collection.id));1811		<PalletEvm<T>>::deposit_log(1812			erc::CollectionHelpersEvents::CollectionChanged {1813				collection_id: eth::collection_id_to_address(collection.id),1814			}1815			.to_log(T::ContractAddress::get()),1816		);18171818		collection.save()1819	}18201821	/// Merge set fields from `new_permission` to `old_permission`.1822	fn clamp_permissions(1823		_mode: CollectionMode,1824		old_permission: &CollectionPermissions,1825		mut new_permission: CollectionPermissions,1826	) -> Result<CollectionPermissions, DispatchError> {1827		limit_default_clone!(old_permission, new_permission,1828			access => {},1829			mint_mode => {},1830			nesting => { /* todo check for permissive, if only it gets out of benchmarks */ },1831		);1832		Ok(new_permission)1833	}18341835	/// Repair possibly broken properties of a collection.1836	pub fn repair_collection(collection_id: CollectionId) -> DispatchResult {1837		CollectionProperties::<T>::mutate(collection_id, |properties| {1838			properties.recompute_consumed_space();1839		});18401841		Ok(())1842	}1843}18441845/// Indicates unsupported methods by returning [Error::UnsupportedOperation].1846#[macro_export]1847macro_rules! unsupported {1848	($runtime:path) => {1849		Err($crate::Error::<$runtime>::UnsupportedOperation.into())1850	};1851}18521853/// Return weights for various worst-case operations.1854pub trait CommonWeightInfo<CrossAccountId> {1855	/// Weight of item creation.1856	fn create_item(data: &CreateItemData) -> Weight {1857		Self::create_multiple_items(from_ref(data))1858	}18591860	/// Weight of items creation.1861	fn create_multiple_items(amount: &[CreateItemData]) -> Weight;18621863	/// Weight of items creation.1864	fn create_multiple_items_ex(cost: &CreateItemExData<CrossAccountId>) -> Weight;18651866	/// The weight of the burning item.1867	fn burn_item() -> Weight;18681869	/// Property setting weight.1870	///1871	/// * `amount`- The number of properties to set.1872	fn set_collection_properties(amount: u32) -> Weight;18731874	/// Collection property deletion weight.1875	///1876	/// * `amount`- The number of properties to set.1877	fn delete_collection_properties(amount: u32) -> Weight;18781879	/// Token property setting weight.1880	///1881	/// * `amount`- The number of properties to set.1882	fn set_token_properties(amount: u32) -> Weight;18831884	/// Token property deletion weight.1885	///1886	/// * `amount`- The number of properties to delete.1887	fn delete_token_properties(amount: u32) -> Weight;18881889	/// Token property permissions set weight.1890	///1891	/// * `amount`- The number of property permissions to set.1892	fn set_token_property_permissions(amount: u32) -> Weight;18931894	/// Transfer price of the token or its parts.1895	fn transfer() -> Weight;18961897	/// The price of setting the permission of the operation from another user.1898	fn approve() -> Weight;18991900	/// The price of setting the permission of the operation from another user for eth mirror.1901	fn approve_from() -> Weight;19021903	/// Transfer price from another user.1904	fn transfer_from() -> Weight;19051906	/// The price of burning a token from another user.1907	fn burn_from() -> Weight;19081909	/// Differs from burn_item in case of Fungible and Refungible, as it should burn1910	/// whole users's balance.1911	///1912	/// This method shouldn't be used directly, as it doesn't count breadth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1913	fn burn_recursively_self_raw() -> Weight;19141915	/// Cost of iterating over `amount` children while burning, without counting child burning itself.1916	///1917	/// This method shouldn't be used directly, as it doesn't count depth price, use [burn_recursively](CommonWeightInfo::burn_recursively) instead1918	fn burn_recursively_breadth_raw(amount: u32) -> Weight;19191920	/// The price of recursive burning a token.1921	///1922	/// `max_selfs` - The maximum burning weight of the token itself.1923	/// `max_breadth` - The maximum number of nested tokens to burn.1924	fn burn_recursively(max_selfs: u32, max_breadth: u32) -> Weight {1925		Self::burn_recursively_self_raw()1926			.saturating_mul(max_selfs.max(1) as u64)1927			.saturating_add(Self::burn_recursively_breadth_raw(max_breadth))1928	}19291930	/// The price of retrieving token owner1931	fn token_owner() -> Weight;19321933	/// The price of setting approval for all1934	fn set_allowance_for_all() -> Weight;19351936	/// The price of repairing an item.1937	fn force_repair_item() -> Weight;1938}19391940/// Weight info extension trait for refungible pallet.1941pub trait RefungibleExtensionsWeightInfo {1942	/// Weight of token repartition.1943	fn repartition() -> Weight;1944}19451946/// Common collection operations.1947///1948/// It wraps methods in Fungible, Nonfungible and Refungible pallets1949/// and adds weight info.1950pub trait CommonCollectionOperations<T: Config> {1951	/// Create token.1952	///1953	/// * `sender` - The user who mint the token and pays for the transaction.1954	/// * `to` - The user who will own the token.1955	/// * `data` - Token data.1956	/// * `nesting_budget` - A budget that can be spent on nesting tokens.1957	fn create_item(1958		&self,1959		sender: T::CrossAccountId,1960		to: T::CrossAccountId,1961		data: CreateItemData,1962		nesting_budget: &dyn Budget,1963	) -> DispatchResultWithPostInfo;19641965	/// Create multiple tokens.1966	///1967	/// * `sender` - The user who mint the token and pays for the transaction.1968	/// * `to` - The user who will own the token.1969	/// * `data` - Token data.1970	/// * `nesting_budget` - A budget that can be spent on nesting tokens.1971	fn create_multiple_items(1972		&self,1973		sender: T::CrossAccountId,1974		to: T::CrossAccountId,1975		data: Vec<CreateItemData>,1976		nesting_budget: &dyn Budget,1977	) -> DispatchResultWithPostInfo;19781979	/// Create multiple tokens.1980	///1981	/// * `sender` - The user who mint the token and pays for the transaction.1982	/// * `to` - The user who will own the token.1983	/// * `data` - Token data.1984	/// * `nesting_budget` - A budget that can be spent on nesting tokens.1985	fn create_multiple_items_ex(1986		&self,1987		sender: T::CrossAccountId,1988		data: CreateItemExData<T::CrossAccountId>,1989		nesting_budget: &dyn Budget,1990	) -> DispatchResultWithPostInfo;19911992	/// Burn token.1993	///1994	/// * `sender` - The user who owns the token.1995	/// * `token` - Token id that will burned.1996	/// * `amount` - The number of parts of the token that will be burned.1997	fn burn_item(1998		&self,1999		sender: T::CrossAccountId,2000		token: TokenId,2001		amount: u128,2002	) -> DispatchResultWithPostInfo;20032004	/// Burn token and all nested tokens recursievly.2005	///2006	/// * `sender` - The user who owns the token.2007	/// * `token` - Token id that will burned.2008	/// * `self_budget` - The budget that can be spent on burning tokens.2009	/// * `breadth_budget` - The budget that can be spent on burning nested tokens.2010	fn burn_item_recursively(2011		&self,2012		sender: T::CrossAccountId,2013		token: TokenId,2014		self_budget: &dyn Budget,2015		breadth_budget: &dyn Budget,2016	) -> DispatchResultWithPostInfo;20172018	/// Set collection properties.2019	///2020	/// * `sender` - Must be either the owner of the collection or its admin.2021	/// * `properties` - Properties to be set.2022	fn set_collection_properties(2023		&self,2024		sender: T::CrossAccountId,2025		properties: Vec<Property>,2026	) -> DispatchResultWithPostInfo;20272028	/// Delete collection properties.2029	///2030	/// * `sender` - Must be either the owner of the collection or its admin.2031	/// * `properties` - The properties to be removed.2032	fn delete_collection_properties(2033		&self,2034		sender: &T::CrossAccountId,2035		property_keys: Vec<PropertyKey>,2036	) -> DispatchResultWithPostInfo;20372038	/// Set token properties.2039	///2040	/// The appropriate [`PropertyPermission`] for the token property2041	/// must be set with [`Self::set_token_property_permissions`].2042	///2043	/// * `sender` - Must be either the owner of the token or its admin.2044	/// * `token_id` - The token for which the properties are being set.2045	/// * `properties` - Properties to be set.2046	/// * `budget` - Budget for setting properties.2047	fn set_token_properties(2048		&self,2049		sender: T::CrossAccountId,2050		token_id: TokenId,2051		properties: Vec<Property>,2052		budget: &dyn Budget,2053	) -> DispatchResultWithPostInfo;20542055	/// Remove token properties.2056	///2057	/// The appropriate [`PropertyPermission`] for the token property2058	/// must be set with [`Self::set_token_property_permissions`].2059	///2060	/// * `sender` - Must be either the owner of the token or its admin.2061	/// * `token_id` - The token for which the properties are being remove.2062	/// * `property_keys` - Keys to remove corresponding properties.2063	/// * `budget` - Budget for removing properties.2064	fn delete_token_properties(2065		&self,2066		sender: T::CrossAccountId,2067		token_id: TokenId,2068		property_keys: Vec<PropertyKey>,2069		budget: &dyn Budget,2070	) -> DispatchResultWithPostInfo;20712072	/// Set token property permissions.2073	///2074	/// * `sender` - Must be either the owner of the token or its admin.2075	/// * `token_id` - The token for which the properties are being set.2076	/// * `property_permissions` - Property permissions to be set.2077	/// * `budget` - Budget for setting properties.2078	fn set_token_property_permissions(2079		&self,2080		sender: &T::CrossAccountId,2081		property_permissions: Vec<PropertyKeyPermission>,2082	) -> DispatchResultWithPostInfo;20832084	/// Transfer amount of token pieces.2085	///2086	/// * `sender` - Donor user.2087	/// * `to` - Recepient user.2088	/// * `token` - The token of which parts are being sent.2089	/// * `amount` - The number of parts of the token that will be transferred.2090	/// * `budget` - The maximum budget that can be spent on the transfer.2091	fn transfer(2092		&self,2093		sender: T::CrossAccountId,2094		to: T::CrossAccountId,2095		token: TokenId,2096		amount: u128,2097		budget: &dyn Budget,2098	) -> DispatchResultWithPostInfo;20992100	/// Grant access to another account to transfer parts of the token owned by the calling user via [Self::transfer_from].2101	///2102	/// * `sender` - The user who grants access to the token.2103	/// * `spender` - The user to whom the rights are granted.2104	/// * `token` - The token to which access is granted.2105	/// * `amount` - The amount of pieces that another user can dispose of.2106	fn approve(2107		&self,2108		sender: T::CrossAccountId,2109		spender: T::CrossAccountId,2110		token: TokenId,2111		amount: u128,2112	) -> DispatchResultWithPostInfo;21132114	/// Grant access to another account to transfer parts of the token owned by the calling user's eth mirror via [Self::transfer_from].2115	///2116	/// * `sender` - The user who grants access to the token.2117	/// * `from` - Spender's eth mirror.2118	/// * `to` - The user to whom the rights are granted.2119	/// * `token` - The token to which access is granted.2120	/// * `amount` - The amount of pieces that another user can dispose of.2121	fn approve_from(2122		&self,2123		sender: T::CrossAccountId,2124		from: T::CrossAccountId,2125		to: T::CrossAccountId,2126		token: TokenId,2127		amount: u128,2128	) -> DispatchResultWithPostInfo;21292130	/// Send parts of a token owned by another user.2131	///2132	/// Before calling this method, you must grant rights to the calling user via [`Self::approve`].2133	///2134	/// * `sender` - The user who must have access to the token (see [`Self::approve`]).2135	/// * `from` - The user who owns the token.2136	/// * `to` - Recepient user.2137	/// * `token` - The token of which parts are being sent.2138	/// * `amount` - The number of parts of the token that will be transferred.2139	/// * `budget` - The maximum budget that can be spent on the transfer.2140	fn transfer_from(2141		&self,2142		sender: T::CrossAccountId,2143		from: T::CrossAccountId,2144		to: T::CrossAccountId,2145		token: TokenId,2146		amount: u128,2147		budget: &dyn Budget,2148	) -> DispatchResultWithPostInfo;21492150	/// Burn parts of a token owned by another user.2151	///2152	/// Before calling this method, you must grant rights to the calling user via [`Self::approve`].2153	///2154	/// * `sender` - The user who must have access to the token (see [`Self::approve`]).2155	/// * `from` - The user who owns the token.2156	/// * `token` - The token of which parts are being sent.2157	/// * `amount` - The number of parts of the token that will be transferred.2158	/// * `budget` - The maximum budget that can be spent on the burn.2159	fn burn_from(2160		&self,2161		sender: T::CrossAccountId,2162		from: T::CrossAccountId,2163		token: TokenId,2164		amount: u128,2165		budget: &dyn Budget,2166	) -> DispatchResultWithPostInfo;21672168	/// Check permission to nest token.2169	///2170	/// * `sender` - The user who initiated the check.2171	/// * `from` - The token that is checked for embedding.2172	/// * `under` - Token under which to check.2173	/// * `budget` - The maximum budget that can be spent on the check.2174	fn check_nesting(2175		&self,2176		sender: T::CrossAccountId,2177		from: (CollectionId, TokenId),2178		under: TokenId,2179		budget: &dyn Budget,2180	) -> DispatchResult;21812182	/// Nest one token into another.2183	///2184	/// * `under` - Token holder.2185	/// * `to_nest` - Nested token.2186	fn nest(&self, under: TokenId, to_nest: (CollectionId, TokenId));21872188	/// Unnest token.2189	///2190	/// * `under` - Token holder.2191	/// * `to_nest` - Token to unnest.2192	fn unnest(&self, under: TokenId, to_nest: (CollectionId, TokenId));21932194	/// Get all user tokens.2195	///2196	/// * `account` - Account for which you need to get tokens.2197	fn account_tokens(&self, account: T::CrossAccountId) -> Vec<TokenId>;21982199	/// Get all the tokens in the collection.2200	fn collection_tokens(&self) -> Vec<TokenId>;22012202	/// Check if the token exists.2203	///2204	/// * `token` - Id token to check.2205	fn token_exists(&self, token: TokenId) -> bool;22062207	/// Get the id of the last minted token.2208	fn last_token_id(&self) -> TokenId;22092210	/// Get the owner of the token.2211	///2212	/// * `token` - The token for which you need to find out the owner.2213	fn token_owner(&self, token: TokenId) -> Result<T::CrossAccountId, TokenOwnerError>;22142215	/// Returns 10 tokens owners in no particular order.2216	///2217	/// * `token` - The token for which you need to find out the owners.2218	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId>;22192220	/// Get the value of the token property by key.2221	///2222	/// * `token` - Token with the property to get.2223	/// * `key` - Property name.2224	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue>;22252226	/// Get a set of token properties by key vector.2227	///2228	/// * `token` - Token with the property to get.2229	/// * `keys` - Vector of property keys. If this parameter is [None](sp_std::result::Result),2230	/// then all properties are returned.2231	fn token_properties(&self, token: TokenId, keys: Option<Vec<PropertyKey>>) -> Vec<Property>;22322233	/// Amount of unique collection tokens2234	fn total_supply(&self) -> u32;22352236	/// Amount of different tokens account has.2237	///2238	/// * `account` - The account for which need to get the balance.2239	fn account_balance(&self, account: T::CrossAccountId) -> u32;22402241	/// Amount of specific token account have.2242	fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128;22432244	/// Amount of token pieces2245	fn total_pieces(&self, token: TokenId) -> Option<u128>;22462247	/// Get the number of parts of the token that a trusted user can manage.2248	///2249	/// * `sender` - Trusted user.2250	/// * `spender` - Owner of the token.2251	/// * `token` - The token for which to get the value.2252	fn allowance(2253		&self,2254		sender: T::CrossAccountId,2255		spender: T::CrossAccountId,2256		token: TokenId,2257	) -> u128;22582259	/// Get extension for RFT collection.2260	fn refungible_extensions(&self) -> Option<&dyn RefungibleExtensions<T>>;22612262	/// The `operator` is allowed to transfer all tokens of the `owner` on their behalf.2263	/// * `owner` - Token owner2264	/// * `operator` - Operator2265	/// * `approve` - Should operator status be granted or revoked?2266	fn set_allowance_for_all(2267		&self,2268		owner: T::CrossAccountId,2269		operator: T::CrossAccountId,2270		approve: bool,2271	) -> DispatchResultWithPostInfo;22722273	/// Tells whether the given `owner` approves the `operator`.2274	fn allowance_for_all(&self, owner: T::CrossAccountId, operator: T::CrossAccountId) -> bool;22752276	/// Repairs a possibly broken item.2277	fn repair_item(&self, token: TokenId) -> DispatchResultWithPostInfo;2278}22792280/// Extension for RFT collection.2281pub trait RefungibleExtensions<T>2282where2283	T: Config,2284{2285	/// Change the number of parts of the token.2286	///2287	/// When the value changes down, this function is equivalent to burning parts of the token.2288	///2289	/// * `sender` - The user calling the repartition operation. Must be the owner of the token.2290	/// * `token` - The token for which you want to change the number of parts.2291	/// * `amount` - The new value of the parts of the token.2292	fn repartition(2293		&self,2294		sender: &T::CrossAccountId,2295		token: TokenId,2296		amount: u128,2297	) -> DispatchResultWithPostInfo;2298}22992300/// Merge [`DispatchResult`] with [`Weight`] into [`DispatchResultWithPostInfo`].2301///2302/// Used for [`CommonCollectionOperations`] implementations and flexible enough to do so.2303pub fn with_weight(res: DispatchResult, weight: Weight) -> DispatchResultWithPostInfo {2304	let post_info = PostDispatchInfo {2305		actual_weight: Some(weight),2306		pays_fee: Pays::Yes,2307	};2308	match res {2309		Ok(()) => Ok(post_info),2310		Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }),2311	}2312}23132314impl<T: Config> From<PropertiesError> for Error<T> {2315	fn from(error: PropertiesError) -> Self {2316		match error {2317			PropertiesError::NoSpaceForProperty => Self::NoSpaceForProperty,2318			PropertiesError::PropertyLimitReached => Self::PropertyLimitReached,2319			PropertiesError::InvalidCharacterInPropertyKey => Self::InvalidCharacterInPropertyKey,2320			PropertiesError::PropertyKeyIsTooLong => Self::PropertyKeyIsTooLong,2321			PropertiesError::EmptyPropertyKey => Self::EmptyPropertyKey,2322		}2323	}2324}
modifiedpallets/evm-coder-substrate/src/lib.rsdiffbeforeafterboth
--- a/pallets/evm-coder-substrate/src/lib.rs
+++ b/pallets/evm-coder-substrate/src/lib.rs
@@ -37,7 +37,7 @@
 	ExitError, ExitRevert, ExitSucceed, GasWeightMapping, PrecompileFailure, PrecompileOutput,
 	PrecompileResult, PrecompileHandle,
 };
-use sp_core::H160;
+use sp_core::{Get, H160};
 // #[cfg(feature = "runtime-benchmarks")]
 // pub mod benchmarking;
 pub mod execution;
@@ -204,6 +204,40 @@
 			Err(Error::Error(e)) => Err(e.into()),
 		})
 	}
+
+	/// Consume gas for reading.
+	pub fn consume_store_reads(&self, reads: u64) -> execution::Result<()> {
+		self.consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_parts(
+			<T as frame_system::Config>::DbWeight::get()
+				.read
+				.saturating_mul(reads),
+			// TODO: measure proof
+			0,
+		)))
+	}
+
+	/// Consume gas for writing.
+	pub fn consume_store_writes(&self, writes: u64) -> execution::Result<()> {
+		self.consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_parts(
+			<T as frame_system::Config>::DbWeight::get()
+				.write
+				.saturating_mul(writes),
+			// TODO: measure proof
+			0,
+		)))
+	}
+
+	/// Consume gas for reading and writing.
+	pub fn consume_store_reads_and_writes(&self, reads: u64, writes: u64) -> execution::Result<()> {
+		let weight = <T as frame_system::Config>::DbWeight::get();
+		let reads = weight.read.saturating_mul(reads);
+		let writes = weight.read.saturating_mul(writes);
+		self.consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_parts(
+			reads.saturating_add(writes),
+			// TODO: measure proof
+			0,
+		)))
+	}
 }
 
 pub fn dispatch_to_evm<T: Config>(err: DispatchError) -> execution::Error {
modifiedpallets/fungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/fungible/src/erc.rs
+++ b/pallets/fungible/src/erc.rs
@@ -109,7 +109,8 @@
 			.recorder
 			.weight_calls_budget(<StructureWeight<T>>::find_parent());
 
-		<Pallet<T>>::transfer(self, &caller, &to, amount, &budget).map_err(|_| "transfer error")?;
+		<Pallet<T>>::transfer(self, &caller, &to, amount, &budget)
+			.map_err(|e| dispatch_to_evm::<T>(e.error))?;
 		Ok(true)
 	}
 
modifiedpallets/nonfungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/nonfungible/Cargo.toml
+++ b/pallets/nonfungible/Cargo.toml
@@ -12,6 +12,7 @@
 frame-benchmarking = { workspace = true, optional = true }
 frame-support = { workspace = true }
 frame-system = { workspace = true }
+pallet-balances = { workspace = true }
 pallet-common = { workspace = true }
 pallet-evm = { workspace = true }
 pallet-evm-coder-substrate = { workspace = true }
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -166,7 +166,11 @@
 
 	#[pallet::config]
 	pub trait Config:
-		frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config
+		frame_system::Config
+		+ pallet_common::Config
+		+ pallet_structure::Config
+		+ pallet_evm::Config
+		+ pallet_balances::Config
 	{
 		type WeightInfo: WeightInfo;
 	}
@@ -1325,11 +1329,15 @@
 	}
 
 	fn nest(under: (CollectionId, TokenId), to_nest: (CollectionId, TokenId)) {
-		<TokenChildren<T>>::insert((under.0, under.1, (to_nest.0, to_nest.1)), true);
+		if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+			<TokenChildren<T>>::insert((under.0, under.1, to_nest), true);
+		}
 	}
 
 	fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) {
-		<TokenChildren<T>>::remove((under.0, under.1, to_unnest));
+		if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+			<TokenChildren<T>>::remove((under.0, under.1, to_unnest));
+		}
 	}
 
 	fn collection_has_tokens(collection_id: CollectionId) -> bool {
modifiedpallets/structure/src/benchmarking.rsdiffbeforeafterboth
--- a/pallets/structure/src/benchmarking.rs
+++ b/pallets/structure/src/benchmarking.rs
@@ -42,7 +42,7 @@
 			},
 			CollectionFlags::default(),
 		)?;
-		let dispatch = T::CollectionDispatch::dispatch(CollectionHandle::try_get(CollectionId(1))?);
+		let dispatch = T::CollectionDispatch::dispatch(CollectionId(1))?;
 		let dispatch = dispatch.as_dyn();
 
 		dispatch.create_item(caller_cross.clone(), caller_cross.clone(), CreateItemData::NFT(CreateNftData::default()), &Unlimited)?;
modifiedpallets/structure/src/lib.rsdiffbeforeafterboth
--- a/pallets/structure/src/lib.rs
+++ b/pallets/structure/src/lib.rs
@@ -60,7 +60,7 @@
 use frame_support::dispatch::{DispatchError, DispatchResult, DispatchResultWithPostInfo};
 use frame_support::fail;
 pub use pallet::*;
-use pallet_common::{dispatch::CollectionDispatch, CollectionHandle};
+use pallet_common::{dispatch::CollectionDispatch};
 use up_data_structs::{
 	CollectionId, TokenId, mapping::TokenAddressMapping, budget::Budget, TokenOwnerError,
 };
@@ -155,11 +155,10 @@
 		token: TokenId,
 	) -> Result<Parent<T::CrossAccountId>, DispatchError> {
 		// TODO: Reduce cost by not reading collection config
-		let handle = match CollectionHandle::try_get(collection) {
+		let handle = match T::CollectionDispatch::dispatch(collection) {
 			Ok(v) => v,
 			Err(_) => return Ok(Parent::TokenNotFound),
 		};
-		let handle = T::CollectionDispatch::dispatch(handle);
 		let handle = handle.as_dyn();
 
 		Ok(match handle.token_owner(token) {
@@ -279,8 +278,7 @@
 		self_budget: &dyn Budget,
 		breadth_budget: &dyn Budget,
 	) -> DispatchResultWithPostInfo {
-		let handle = <CollectionHandle<T>>::try_get(collection)?;
-		let dispatch = T::CollectionDispatch::dispatch(handle);
+		let dispatch = T::CollectionDispatch::dispatch(collection)?;
 		let dispatch = dispatch.as_dyn();
 		dispatch.burn_item_recursively(from.clone(), token, self_budget, breadth_budget)
 	}
@@ -405,9 +403,7 @@
 			return Ok(())
 		};
 
-		let handle = <CollectionHandle<T>>::try_get(collection)?;
-
-		let dispatch = T::CollectionDispatch::dispatch(handle);
+		let dispatch = T::CollectionDispatch::dispatch(collection)?;
 		let dispatch = dispatch.as_dyn();
 
 		action(dispatch, token)
modifiedpallets/unique/Cargo.tomldiffbeforeafterboth
--- a/pallets/unique/Cargo.toml
+++ b/pallets/unique/Cargo.toml
@@ -24,6 +24,7 @@
 	'frame-benchmarking/std',
 	'frame-support/std',
 	'frame-system/std',
+	'pallet-balances-adapter/std',
 	'pallet-common/std',
 	'pallet-evm-coder-substrate/std',
 	'pallet-evm/std',
@@ -46,6 +47,7 @@
 frame-benchmarking = { workspace = true, optional = true }
 frame-support = { workspace = true }
 frame-system = { workspace = true }
+pallet-balances-adapter = { workspace = true }
 pallet-common = { workspace = true }
 pallet-evm = { workspace = true }
 pallet-evm-coder-substrate = { workspace = true }
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -440,6 +440,10 @@
 			collection_id: CollectionId,
 			address: T::CrossAccountId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
+
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			collection.check_is_internal()?;
@@ -467,6 +471,10 @@
 			collection_id: CollectionId,
 			address: T::CrossAccountId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
+
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			collection.check_is_internal()?;
@@ -493,6 +501,9 @@
 			collection_id: CollectionId,
 			new_owner: T::AccountId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let new_owner = T::CrossAccountId::from_sub(new_owner);
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
@@ -522,6 +533,9 @@
 			collection_id: CollectionId,
 			new_admin_id: T::CrossAccountId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			<PalletCommon<T>>::toggle_admin(&collection, &sender, &new_admin_id, true)
@@ -548,6 +562,9 @@
 			collection_id: CollectionId,
 			account_id: T::CrossAccountId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			<PalletCommon<T>>::toggle_admin(&collection, &sender, &account_id, false)
@@ -573,6 +590,9 @@
 			collection_id: CollectionId,
 			new_sponsor: T::AccountId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			target_collection.set_sponsor(&sender, new_sponsor.clone())
@@ -597,6 +617,9 @@
 			origin: OriginFor<T>,
 			collection_id: CollectionId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = ensure_signed(origin)?;
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			target_collection.confirm_sponsorship(&sender)
@@ -617,6 +640,9 @@
 			origin: OriginFor<T>,
 			collection_id: CollectionId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			target_collection.remove_sponsor(&sender)
@@ -894,6 +920,9 @@
 			collection_id: CollectionId,
 			value: bool,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			target_collection.check_is_internal()?;
@@ -1146,6 +1175,9 @@
 			collection_id: CollectionId,
 			new_limit: CollectionLimits,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			<PalletCommon<T>>::update_limits(&sender, &mut target_collection, new_limit)
@@ -1170,6 +1202,9 @@
 			collection_id: CollectionId,
 			new_permission: CollectionPermissions,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
 			let mut target_collection = <CollectionHandle<T>>::try_get(collection_id)?;
 			<PalletCommon<T>>::update_permissions(&sender, &mut target_collection, new_permission)
@@ -1238,6 +1273,9 @@
 			origin: OriginFor<T>,
 			collection_id: CollectionId,
 		) -> DispatchResult {
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+			}
 			ensure_root(origin)?;
 			<PalletCommon<T>>::repair_collection(collection_id)
 		}
@@ -1296,10 +1334,7 @@
 			sender: T::CrossAccountId,
 			collection_id: CollectionId,
 		) -> DispatchResult {
-			let collection = <CollectionHandle<T>>::try_get(collection_id)?;
-			collection.check_is_internal()?;
-
-			T::CollectionDispatch::destroy(sender, collection)?;
+			T::CollectionDispatch::destroy(sender, collection_id)?;
 
 			// TODO: basket cleanup should be moved elsewhere
 			// Maybe runtime dispatch.rs should perform it?
modifiedruntime/common/config/pallets/mod.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/mod.rs
+++ b/runtime/common/config/pallets/mod.rs
@@ -14,6 +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/>.
 
+use alloc::string::{String, ToString};
 use frame_support::parameter_types;
 use sp_runtime::traits::AccountIdConversion;
 use crate::{
@@ -23,9 +24,10 @@
 		weights::CommonWeights,
 		RelayChainBlockNumberProvider,
 	},
-	Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, Balances,
+	Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, RUNTIME_NAME, TOKEN_SYMBOL, DECIMALS,
+	Balances,
 };
-use frame_support::traits::{ConstU32, ConstU64};
+use frame_support::traits::{ConstU32, ConstU64, Currency};
 use up_common::{
 	types::{AccountId, Balance, BlockNumber},
 	constants::*,
@@ -51,8 +53,8 @@
 pub mod preimage;
 
 parameter_types! {
+	pub const CollectionCreationPrice: Balance = 2 * UNIQUE;
 	pub TreasuryAccountId: AccountId = TreasuryModuleId::get().into_account_truncating();
-	pub const CollectionCreationPrice: Balance = 2 * UNIQUE;
 }
 
 impl pallet_common::Config for Runtime {
@@ -85,6 +87,21 @@
 }
 
 parameter_types! {
+	pub const Decimals: u8 = DECIMALS;
+	pub Name: String = RUNTIME_NAME.to_string();
+	pub Symbol: String = TOKEN_SYMBOL.to_string();
+}
+impl pallet_balances_adapter::Config for Runtime {
+	type Inspect = Balances;
+	type Mutate = Balances;
+	type CurrencyBalance = <Balances as Currency<Self::AccountId>>::Balance;
+	type Decimals = Decimals;
+	type Name = Name;
+	type Symbol = Symbol;
+	type WeightInfo = pallet_balances::weights::SubstrateWeight<Self>;
+}
+
+parameter_types! {
 	pub const InflationBlockInterval: BlockNumber = 100; // every time per how many blocks inflation is applied
 }
 
modifiedruntime/common/construct_runtime.rsdiffbeforeafterboth
--- a/runtime/common/construct_runtime.rs
+++ b/runtime/common/construct_runtime.rs
@@ -106,6 +106,8 @@
 
 				Maintenance: pallet_maintenance = 154,
 
+				BalancesAdapter: pallet_balances_adapter = 155,
+
 				#[cfg(feature = "pallet-test-utils")]
 				TestUtils: pallet_test_utils = 255,
 			}
modifiedruntime/common/dispatch.rsdiffbeforeafterboth
--- a/runtime/common/dispatch.rs
+++ b/runtime/common/dispatch.rs
@@ -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/>.
 
-use frame_support::{dispatch::DispatchResult, ensure};
+use frame_support::{dispatch::DispatchResult, ensure, fail};
 use pallet_evm::{PrecompileHandle, PrecompileResult};
 use sp_core::H160;
 use sp_runtime::DispatchError;
@@ -25,6 +25,7 @@
 };
 pub use pallet_common::dispatch::CollectionDispatch;
 use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};
+use pallet_balances_adapter::{NativeFungibleHandle};
 use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};
 use pallet_refungible::{
 	Pallet as PalletRefungible, RefungibleHandle, erc_token::RefungibleTokenHandle,
@@ -39,20 +40,35 @@
 
 pub enum CollectionDispatchT<T>
 where
-	T: pallet_fungible::Config + pallet_nonfungible::Config + pallet_refungible::Config,
+	T: pallet_fungible::Config
+		+ pallet_nonfungible::Config
+		+ pallet_refungible::Config
+		+ pallet_balances_adapter::Config,
 {
 	Fungible(FungibleHandle<T>),
 	Nonfungible(NonfungibleHandle<T>),
 	Refungible(RefungibleHandle<T>),
+	NativeFungible(NativeFungibleHandle<T>),
 }
+
 impl<T> CollectionDispatch<T> for CollectionDispatchT<T>
 where
 	T: pallet_common::Config
 		+ pallet_unique::Config
 		+ pallet_fungible::Config
 		+ pallet_nonfungible::Config
-		+ pallet_refungible::Config,
+		+ pallet_refungible::Config
+		+ pallet_balances_adapter::Config,
 {
+	fn check_is_internal(&self) -> DispatchResult {
+		match self {
+			Self::Fungible(h) => h.check_is_internal(),
+			Self::Nonfungible(h) => h.check_is_internal(),
+			Self::Refungible(h) => h.check_is_internal(),
+			Self::NativeFungible(h) => h.check_is_internal(),
+		}
+	}
+
 	fn create(
 		sender: T::CrossAccountId,
 		payer: T::CrossAccountId,
@@ -83,7 +99,13 @@
 		Ok(id)
 	}
 
-	fn destroy(sender: T::CrossAccountId, collection: CollectionHandle<T>) -> DispatchResult {
+	fn destroy(sender: T::CrossAccountId, collection_id: CollectionId) -> DispatchResult {
+		if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+			fail!(<pallet_common::Error<T>>::UnsupportedOperation);
+		}
+
+		let collection = <CollectionHandle<T>>::try_get(collection_id)?;
+
 		match collection.mode {
 			CollectionMode::ReFungible => {
 				PalletRefungible::destroy_collection(RefungibleHandle::cast(collection), &sender)?
@@ -98,20 +120,19 @@
 		Ok(())
 	}
 
-	fn dispatch(handle: CollectionHandle<T>) -> Self {
-		match handle.mode {
+	fn dispatch(collection_id: CollectionId) -> Result<Self, DispatchError> {
+		if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+			return Ok(Self::NativeFungible(
+				NativeFungibleHandle::new_with_gas_limit(u64::MAX),
+			));
+		}
+
+		let handle = <CollectionHandle<T>>::try_get(collection_id)?;
+		Ok(match handle.mode {
 			CollectionMode::Fungible(_) => Self::Fungible(FungibleHandle::cast(handle)),
 			CollectionMode::NFT => Self::Nonfungible(NonfungibleHandle::cast(handle)),
 			CollectionMode::ReFungible => Self::Refungible(RefungibleHandle::cast(handle)),
-		}
-	}
-
-	fn into_inner(self) -> CollectionHandle<T> {
-		match self {
-			Self::Fungible(f) => f.into_inner(),
-			Self::Nonfungible(f) => f.into_inner(),
-			Self::Refungible(f) => f.into_inner(),
-		}
+		})
 	}
 
 	fn as_dyn(&self) -> &dyn CommonCollectionOperations<T> {
@@ -119,6 +140,7 @@
 			Self::Fungible(h) => h,
 			Self::Nonfungible(h) => h,
 			Self::Refungible(h) => h,
+			Self::NativeFungible(h) => h,
 		}
 	}
 }
@@ -129,7 +151,8 @@
 		+ pallet_unique::Config
 		+ pallet_fungible::Config
 		+ pallet_nonfungible::Config
-		+ pallet_refungible::Config,
+		+ pallet_refungible::Config
+		+ pallet_balances_adapter::Config,
 	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,
 {
 	fn is_reserved(target: &H160) -> bool {
@@ -166,14 +189,19 @@
 	}
 	fn call(handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
 		if let Some(collection_id) = map_eth_to_id(&handle.code_address()) {
-			let collection =
-				<CollectionHandle<T>>::new_with_gas_limit(collection_id, handle.remaining_gas())?;
-			let dispatched = Self::dispatch(collection);
+			if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID {
+				<NativeFungibleHandle<T>>::new_with_gas_limit(handle.remaining_gas()).call(handle)
+			} else {
+				let collection = <CollectionHandle<T>>::new_with_gas_limit(
+					collection_id,
+					handle.remaining_gas(),
+				)?;
 
-			match dispatched {
-				Self::Fungible(h) => h.call(handle),
-				Self::Nonfungible(h) => h.call(handle),
-				Self::Refungible(h) => h.call(handle),
+				match collection.mode {
+					CollectionMode::Fungible(_) => FungibleHandle::cast(collection).call(handle),
+					CollectionMode::NFT => NonfungibleHandle::cast(collection).call(handle),
+					CollectionMode::ReFungible => RefungibleHandle::cast(collection).call(handle),
+				}
 			}
 		} else if let Some((collection_id, token_id)) =
 			<T as pallet_common::Config>::EvmTokenAddressMapping::address_to_token(
modifiedruntime/common/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/runtime_apis.rs
+++ b/runtime/common/runtime_apis.rs
@@ -17,7 +17,7 @@
 #[macro_export]
 macro_rules! dispatch_unique_runtime {
 	($collection:ident.$method:ident($($name:ident),*) $($rest:tt)*) => {{
-		let collection = <Runtime as pallet_common::Config>::CollectionDispatch::dispatch(<pallet_common::CollectionHandle<Runtime>>::try_get($collection)?);
+		let collection = <Runtime as pallet_common::Config>::CollectionDispatch::dispatch($collection)?;
 		let dispatch = collection.as_dyn();
 
 		Ok::<_, DispatchError>(dispatch.$method($($name),*) $($rest)*)
modifiedruntime/common/weights/mod.rsdiffbeforeafterboth
--- a/runtime/common/weights/mod.rs
+++ b/runtime/common/weights/mod.rs
@@ -18,6 +18,9 @@
 use frame_support::{weights::Weight};
 use pallet_common::{CommonWeightInfo, dispatch::dispatch_weight, RefungibleExtensionsWeightInfo};
 
+use pallet_balances_adapter::{
+	Config as NativeFungibleConfig, common::CommonWeights as NativeFungibleWeights,
+};
 use pallet_fungible::{Config as FungibleConfig, common::CommonWeights as FungibleWeights};
 use pallet_nonfungible::{Config as NonfungibleConfig, common::CommonWeights as NonfungibleWeights};
 
@@ -32,6 +35,7 @@
 macro_rules! max_weight_of {
 	($method:ident ( $($args:tt)* )) => {{
 		let max_weight = <FungibleWeights<T>>::$method($($args)*)
+			.max(<NativeFungibleWeights<T>>::$method($($args)*))
 			.max(<NonfungibleWeights<T>>::$method($($args)*));
 
 		#[cfg(feature = "refungible")]
@@ -42,16 +46,22 @@
 }
 
 #[cfg(not(feature = "refungible"))]
-pub trait CommonWeightConfigs: FungibleConfig + NonfungibleConfig {}
+pub trait CommonWeightConfigs: FungibleConfig + NativeFungibleConfig + NonfungibleConfig {}
 
 #[cfg(not(feature = "refungible"))]
-impl<T: FungibleConfig + NonfungibleConfig> CommonWeightConfigs for T {}
+impl<T: FungibleConfig + NativeFungibleConfig + NonfungibleConfig> CommonWeightConfigs for T {}
 
 #[cfg(feature = "refungible")]
-pub trait CommonWeightConfigs: FungibleConfig + NonfungibleConfig + RefungibleConfig {}
+pub trait CommonWeightConfigs:
+	FungibleConfig + NativeFungibleConfig + NonfungibleConfig + RefungibleConfig
+{
+}
 
 #[cfg(feature = "refungible")]
-impl<T: FungibleConfig + NonfungibleConfig + RefungibleConfig> CommonWeightConfigs for T {}
+impl<T: FungibleConfig + NativeFungibleConfig + NonfungibleConfig + RefungibleConfig>
+	CommonWeightConfigs for T
+{
+}
 
 pub struct CommonWeights<T>(PhantomData<T>);
 
modifiedruntime/opal/Cargo.tomldiffbeforeafterboth
--- a/runtime/opal/Cargo.toml
+++ b/runtime/opal/Cargo.toml
@@ -18,9 +18,10 @@
 [features]
 default = ['opal-runtime', 'std']
 limit-testing = ['pallet-unique/limit-testing', 'up-data-structs/limit-testing']
-opal-runtime = ['app-promotion', 'collator-selection', 'foreign-assets', 'preimage', 'pallet-test-utils', 'refungible']
+opal-runtime = ['app-promotion', 'collator-selection', 'foreign-assets', 'pallet-test-utils', 'preimage', 'refungible']
 pov-estimate = []
 runtime-benchmarks = [
+	"pallet-preimage/runtime-benchmarks",
 	'cumulus-pallet-parachain-system/runtime-benchmarks',
 	'frame-benchmarking',
 	'frame-support/runtime-benchmarks',
@@ -41,7 +42,6 @@
 	'pallet-inflation/runtime-benchmarks',
 	'pallet-maintenance/runtime-benchmarks',
 	'pallet-nonfungible/runtime-benchmarks',
-	"pallet-preimage/runtime-benchmarks",
 	'pallet-refungible/runtime-benchmarks',
 	'pallet-structure/runtime-benchmarks',
 	'pallet-timestamp/runtime-benchmarks',
@@ -70,8 +70,8 @@
 	# 'pallet-contracts-primitives/std',
 	# 'pallet-contracts-rpc-runtime-api/std',
 	# 'pallet-contract-helpers/std',
-	"pallet-preimage/std",
 	"pallet-authorship/std",
+	"pallet-preimage/std",
 	"pallet-session/std",
 	"sp-consensus-aura/std",
 	'app-promotion-rpc/std',
@@ -79,6 +79,7 @@
 	'fp-rpc/std',
 	'fp-self-contained/std',
 	'pallet-app-promotion/std',
+	'pallet-balances-adapter/std',
 	'pallet-base-fee/std',
 	'pallet-charge-transaction/std',
 	'pallet-collator-selection/std',
@@ -128,8 +129,8 @@
 	"orml-tokens/std",
 	"orml-traits/std",
 	"orml-vesting/std",
-	"orml-xtokens/std",
 	"orml-xcm-support/std",
+	"orml-xtokens/std",
 	"pallet-foreign-assets/std",
 
 	'pallet-maintenance/std',
@@ -139,8 +140,8 @@
 	"pallet-authorship/try-runtime",
 	"pallet-collator-selection/try-runtime",
 	"pallet-identity/try-runtime",
+	"pallet-preimage/try-runtime",
 	"pallet-session/try-runtime",
-	"pallet-preimage/try-runtime",
 	'cumulus-pallet-aura-ext/try-runtime',
 	'cumulus-pallet-dmp-queue/try-runtime',
 	'cumulus-pallet-parachain-system/try-runtime',
@@ -158,6 +159,7 @@
 	'pallet-app-promotion/try-runtime',
 	'pallet-aura/try-runtime',
 	'pallet-balances/try-runtime',
+	'pallet-balances-adapter/try-runtime',
 	'pallet-base-fee/try-runtime',
 	'pallet-charge-transaction/try-runtime',
 	'pallet-common/try-runtime',
@@ -189,8 +191,8 @@
 app-promotion = []
 collator-selection = []
 foreign-assets = []
-preimage = []
 pallet-test-utils = []
+preimage = []
 refungible = []
 scheduler = []
 
@@ -216,8 +218,8 @@
 orml-tokens = { workspace = true }
 orml-traits = { workspace = true }
 orml-vesting = { workspace = true }
+orml-xcm-support = { workspace = true }
 orml-xtokens = { workspace = true }
-orml-xcm-support = { workspace = true }
 pallet-aura = { workspace = true }
 pallet-authorship = { workspace = true }
 pallet-balances = { features = ["insecure_zero_ed"], workspace = true }
@@ -258,6 +260,7 @@
 log = { workspace = true }
 num_enum = { workspace = true }
 pallet-app-promotion = { workspace = true }
+pallet-balances-adapter = { workspace = true }
 pallet-base-fee = { workspace = true }
 pallet-charge-transaction = { workspace = true }
 pallet-collator-selection = { workspace = true }
@@ -291,11 +294,11 @@
 ################################################################################
 # Optional dependencies
 
-frame-system-benchmarking = { workspace = true, optional = true}
 frame-benchmarking = { workspace = true, optional = true }
+frame-system-benchmarking = { workspace = true, optional = true }
 frame-try-runtime = { workspace = true, optional = true }
+hex-literal = { workspace = true, optional = true }
 serde = { workspace = true, optional = true }
-hex-literal = { workspace = true, optional = true }
 
 ################################################################################
 # Test dependencies
modifiedruntime/opal/src/lib.rsdiffbeforeafterboth
--- a/runtime/opal/src/lib.rs
+++ b/runtime/opal/src/lib.rs
@@ -45,6 +45,7 @@
 
 pub const RUNTIME_NAME: &str = "opal";
 pub const TOKEN_SYMBOL: &str = "OPL";
+pub const DECIMALS: u8 = 18;
 
 /// This runtime version.
 pub const VERSION: RuntimeVersion = RuntimeVersion {
modifiedruntime/quartz/Cargo.tomldiffbeforeafterboth
--- a/runtime/quartz/Cargo.toml
+++ b/runtime/quartz/Cargo.toml
@@ -22,6 +22,7 @@
 pov-estimate = []
 quartz-runtime = ['app-promotion', 'collator-selection', 'foreign-assets', 'preimage', 'refungible']
 runtime-benchmarks = [
+	"pallet-preimage/runtime-benchmarks",
 	'cumulus-pallet-parachain-system/runtime-benchmarks',
 	'frame-benchmarking',
 	'frame-support/runtime-benchmarks',
@@ -42,7 +43,6 @@
 	'pallet-inflation/runtime-benchmarks',
 	'pallet-maintenance/runtime-benchmarks',
 	'pallet-nonfungible/runtime-benchmarks',
-	"pallet-preimage/runtime-benchmarks",
 	'pallet-refungible/runtime-benchmarks',
 	'pallet-structure/runtime-benchmarks',
 	'pallet-timestamp/runtime-benchmarks',
@@ -66,13 +66,14 @@
 	'frame-try-runtime/std',
 	'pallet-aura/std',
 	'pallet-balances/std',
+	'pallet-balances-adapter/std',
 	# 'pallet-contracts/std',
 	# 'pallet-contracts-primitives/std',
 	# 'pallet-contracts-rpc-runtime-api/std',
 	# 'pallet-contract-helpers/std',
-	"pallet-preimage/std",
 	"pallet-authorship/std",
 	"pallet-identity/std",
+	"pallet-preimage/std",
 	"pallet-session/std",
 	"sp-consensus-aura/std",
 	'app-promotion-rpc/std',
@@ -127,8 +128,8 @@
 	"orml-tokens/std",
 	"orml-traits/std",
 	"orml-vesting/std",
+	"orml-xcm-support/std",
 	"orml-xtokens/std",
-	"orml-xcm-support/std",
 	"pallet-foreign-assets/std",
 	"pallet-maintenance/std",
 ]
@@ -136,8 +137,8 @@
 	"pallet-authorship/try-runtime",
 	"pallet-collator-selection/try-runtime",
 	"pallet-identity/try-runtime",
+	"pallet-preimage/try-runtime",
 	"pallet-session/try-runtime",
-	"pallet-preimage/try-runtime",
 	'cumulus-pallet-aura-ext/try-runtime',
 	'cumulus-pallet-dmp-queue/try-runtime',
 	'cumulus-pallet-parachain-system/try-runtime',
@@ -154,6 +155,7 @@
 	'pallet-app-promotion/try-runtime',
 	'pallet-aura/try-runtime',
 	'pallet-balances/try-runtime',
+	'pallet-balances-adapter/try-runtime',
 	'pallet-charge-transaction/try-runtime',
 	'pallet-common/try-runtime',
 	'pallet-configuration/try-runtime',
@@ -208,8 +210,8 @@
 orml-tokens = { workspace = true }
 orml-traits = { workspace = true }
 orml-vesting = { workspace = true }
+orml-xcm-support = { workspace = true }
 orml-xtokens = { workspace = true }
-orml-xcm-support = { workspace = true }
 pallet-aura = { workspace = true }
 pallet-authorship = { workspace = true }
 pallet-balances = { features = ["insecure_zero_ed"], workspace = true }
@@ -246,6 +248,7 @@
 fp-evm = { workspace = true }
 log = { workspace = true }
 pallet-app-promotion = { workspace = true }
+pallet-balances-adapter = { workspace = true }
 pallet-collator-selection = { workspace = true }
 pallet-common = { workspace = true }
 pallet-configuration = { workspace = true }
@@ -283,11 +286,11 @@
 ################################################################################
 # Optional dependencies
 
+frame-benchmarking = { workspace = true, optional = true }
 frame-system-benchmarking = { workspace = true, optional = true }
-frame-benchmarking = { workspace = true, optional = true }
 frame-try-runtime = { workspace = true, optional = true }
+hex-literal = { workspace = true, optional = true }
 serde = { workspace = true, optional = true }
-hex-literal = { workspace = true, optional = true }
 
 
 ################################################################################
modifiedruntime/quartz/src/lib.rsdiffbeforeafterboth
--- a/runtime/quartz/src/lib.rs
+++ b/runtime/quartz/src/lib.rs
@@ -48,6 +48,7 @@
 #[cfg(not(feature = "become-sapphire"))]
 pub const RUNTIME_NAME: &str = "quartz";
 pub const TOKEN_SYMBOL: &str = "QTZ";
+pub const DECIMALS: u8 = 18;
 
 /// This runtime version.
 pub const VERSION: RuntimeVersion = RuntimeVersion {
modifiedruntime/tests/Cargo.tomldiffbeforeafterboth
--- a/runtime/tests/Cargo.toml
+++ b/runtime/tests/Cargo.toml
@@ -26,6 +26,7 @@
 pallet-ethereum = { workspace = true }
 pallet-evm = { workspace = true }
 
+pallet-balances-adapter = { workspace = true }
 pallet-common = { workspace = true }
 pallet-fungible = { workspace = true }
 pallet-nonfungible = { workspace = true }
@@ -41,5 +42,3 @@
 evm-coder = { workspace = true }
 up-sponsorship = { workspace = true }
 xcm = { workspace = true }
-pallet-xcm = { workspace = true }
-pallet-configuration = { workspace = true }
modifiedruntime/tests/src/lib.rsdiffbeforeafterboth
--- a/runtime/tests/src/lib.rs
+++ b/runtime/tests/src/lib.rs
@@ -19,7 +19,7 @@
 use sp_core::{H160, H256, U256};
 use frame_support::{
 	parameter_types,
-	traits::{Everything, ConstU32, ConstU64},
+	traits::{Everything, ConstU32, ConstU64, Currency},
 	weights::IdentityFee,
 	pallet_prelude::Weight,
 };
@@ -266,6 +266,19 @@
 impl pallet_nonfungible::Config for Test {
 	type WeightInfo = ();
 }
+parameter_types! {
+	pub const Decimals: u8 = 18;
+	pub Name: String = "Test".to_string();
+	pub Symbol: String = "TST".to_string();
+}
+impl pallet_balances_adapter::Config for Test {
+	type Currency = Balances;
+	type CurrencyBalance = <Balances as Currency<Self::AccountId>>::Balance;
+	type Decimals = Decimals;
+	type Name = Name;
+	type Symbol = Symbol;
+	type WeightInfo = ();
+}
 
 parameter_types! {
 	// 0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f
modifiedruntime/unique/Cargo.tomldiffbeforeafterboth
--- a/runtime/unique/Cargo.toml
+++ b/runtime/unique/Cargo.toml
@@ -20,6 +20,7 @@
 limit-testing = ['pallet-unique/limit-testing', 'up-data-structs/limit-testing']
 pov-estimate = []
 runtime-benchmarks = [
+	"pallet-preimage/runtime-benchmarks",
 	'frame-benchmarking',
 	'frame-support/runtime-benchmarks',
 	'frame-system-benchmarking',
@@ -39,7 +40,6 @@
 	'pallet-inflation/runtime-benchmarks',
 	'pallet-maintenance/runtime-benchmarks',
 	'pallet-nonfungible/runtime-benchmarks',
-	"pallet-preimage/runtime-benchmarks",
 	'pallet-refungible/runtime-benchmarks',
 	'pallet-structure/runtime-benchmarks',
 	'pallet-timestamp/runtime-benchmarks',
@@ -68,9 +68,9 @@
 	# 'pallet-contracts-primitives/std',
 	# 'pallet-contracts-rpc-runtime-api/std',
 	# 'pallet-contract-helpers/std',
-	"pallet-preimage/std",
 	"pallet-authorship/std",
 	"pallet-identity/std",
+	"pallet-preimage/std",
 	"pallet-session/std",
 	"sp-consensus-aura/std",
 	'app-promotion-rpc/std',
@@ -78,6 +78,7 @@
 	'fp-rpc/std',
 	'fp-self-contained/std',
 	'pallet-app-promotion/std',
+	'pallet-balances-adapter/std',
 	'pallet-base-fee/std',
 	'pallet-charge-transaction/std',
 	'pallet-collator-selection/std',
@@ -124,8 +125,8 @@
 	"orml-tokens/std",
 	"orml-traits/std",
 	"orml-vesting/std",
+	"orml-xcm-support/std",
 	"orml-xtokens/std",
-	"orml-xcm-support/std",
 	"pallet-foreign-assets/std",
 	"pallet-maintenance/std",
 ]
@@ -134,8 +135,8 @@
 	"pallet-authorship/try-runtime",
 	"pallet-collator-selection/try-runtime",
 	"pallet-identity/try-runtime",
+	"pallet-preimage/try-runtime",
 	"pallet-session/try-runtime",
-	"pallet-preimage/try-runtime",
 	'cumulus-pallet-aura-ext/try-runtime',
 	'cumulus-pallet-dmp-queue/try-runtime',
 	'cumulus-pallet-parachain-system/try-runtime',
@@ -152,6 +153,7 @@
 	'pallet-app-promotion/try-runtime',
 	'pallet-aura/try-runtime',
 	'pallet-balances/try-runtime',
+	'pallet-balances-adapter/try-runtime',
 	'pallet-charge-transaction/try-runtime',
 	'pallet-common/try-runtime',
 	'pallet-configuration/try-runtime',
@@ -207,8 +209,8 @@
 orml-tokens = { workspace = true }
 orml-traits = { workspace = true }
 orml-vesting = { workspace = true }
+orml-xcm-support = { workspace = true }
 orml-xtokens = { workspace = true }
-orml-xcm-support = { workspace = true }
 pallet-aura = { workspace = true }
 pallet-authorship = { workspace = true }
 pallet-balances = { features = ["insecure_zero_ed"], workspace = true }
@@ -244,6 +246,7 @@
 derivative = { workspace = true }
 log = { workspace = true }
 pallet-app-promotion = { workspace = true }
+pallet-balances-adapter = { workspace = true }
 pallet-collator-selection = { workspace = true }
 pallet-common = { workspace = true }
 pallet-configuration = { workspace = true }
@@ -282,11 +285,11 @@
 ################################################################################
 # Optional dependencies
 
+frame-benchmarking = { workspace = true, optional = true }
 frame-system-benchmarking = { workspace = true, optional = true }
-frame-benchmarking = { workspace = true, optional = true }
 frame-try-runtime = { workspace = true, optional = true }
+hex-literal = { workspace = true, optional = true }
 serde = { workspace = true, optional = true }
-hex-literal = { workspace = true, optional = true }
 
 ################################################################################
 # Test dependencies
modifiedruntime/unique/src/lib.rsdiffbeforeafterboth
--- a/runtime/unique/src/lib.rs
+++ b/runtime/unique/src/lib.rs
@@ -45,6 +45,7 @@
 
 pub const RUNTIME_NAME: &str = "unique";
 pub const TOKEN_SYMBOL: &str = "UNQ";
+pub const DECIMALS: u8 = 18;
 
 /// This runtime version.
 pub const VERSION: RuntimeVersion = RuntimeVersion {
modifiedtests/.eslintrc.jsondiffbeforeafterboth
--- a/tests/.eslintrc.json
+++ b/tests/.eslintrc.json
@@ -86,6 +86,14 @@
         "array-bracket-spacing": "warn",
         "template-curly-spacing": "warn",
         "space-in-parens": "warn",
+        "arrow-body-style": ["warn", "as-needed"],
+        "no-restricted-syntax": [
+            "warn",
+            {
+                "message": "toHuman results in horrible, hard to debug conversions with no stability guarantees, use Codec/at least .toJson instead",
+                "selector": "MemberExpression > Identifier[name=\"toHuman\"]"
+            }
+        ],
         "@typescript-eslint/naming-convention": [
             "warn",
             {
modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -39,12 +39,14 @@
     "testParallelFull": "yarn testParallel && yarn testSequential",
     "testParallel": "yarn _testParallel './src/**/*.test.ts'",
     "testSequential": "yarn _test './src/**/*.seqtest.ts'",
-    "testStructure": "yarn setup && yarn _test ./**/nesting/*.*test.ts",
-    "testEth": "yarn setup && yarn _test './**/eth/**/*.*test.ts'",
-    "testEthNesting": "yarn setup && yarn _test './**/eth/nesting/**/*.*test.ts'",
-    "testEthFractionalizer": "yarn setup && yarn _test './**/eth/fractionalizer/**/*.*test.ts'",
-    "testEthMarketplace": "yarn setup && yarn _test './**/eth/marketplace/**/*.*test.ts'",
-    "testEvent": "yarn setup && yarn _test ./src/check-event/*.*test.ts",
+    "testStructure": "yarn _test ./**/nesting/*.*test.ts",
+    "testEth": "yarn _test './**/eth/**/*.*test.ts'",
+    "testEthNesting": "yarn _test './**/eth/nesting/**/*.*test.ts'",
+    "testEthFractionalizer": "yarn _test './**/eth/fractionalizer/**/*.*test.ts'",
+    "testEthMarketplace": "yarn _test './**/eth/marketplace/**/*.*test.ts'",
+    "testSub": "yarn _test './**/sub/**/*.*test.ts'",
+    "testSubNesting": "yarn _test './**/sub/nesting/**/*.*test.ts'",
+    "testEvent": "yarn _test ./src/check-event/*.*test.ts",
     "testEthPayable": "yarn _test './**/eth/payable.test.ts'",
     "testEvmCoder": "yarn _test './**/eth/evmCoder.test.ts'",
     "testNesting": "yarn _test ./**/nest.test.ts",
modifiedtests/src/addCollectionAdmin.test.tsdiffbeforeafterboth
--- a/tests/src/addCollectionAdmin.test.ts
+++ b/tests/src/addCollectionAdmin.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {itSub, usingPlaygrounds, expect} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => {
   let donor: IKeyringPair;
@@ -82,7 +83,7 @@
 
   itSub("Can't add collection admin of not existing collection.", async ({helper}) => {
     const [alice, bob] = await helper.arrange.createAccounts([10n, 10n, 10n], donor);
-    const collectionId = (1 << 32) - 1;
+    const collectionId = NON_EXISTENT_COLLECTION_ID;
 
     await expect(helper.collection.addAdmin(alice, collectionId, {Substrate: bob.address})).to.be.rejectedWith(/common\.CollectionNotFound/);
 
modifiedtests/src/allowLists.test.tsdiffbeforeafterboth
--- a/tests/src/allowLists.test.ts
+++ b/tests/src/allowLists.test.ts
@@ -16,7 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {usingPlaygrounds, expect, itSub} from './util';
-import {ICollectionPermissions} from './util/playgrounds/types';
+import {ICollectionPermissions, NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('Integration Test ext. Allow list tests', () => {
   let alice: IKeyringPair;
@@ -60,7 +60,7 @@
 
   describe('Negative', () => {
     itSub('Nobody can add address to allow list of non-existing collection', async ({helper}) => {
-      const collectionId = (1<<32) - 1;
+      const collectionId = NON_EXISTENT_COLLECTION_ID;
       await expect(helper.nft.addToAllowList(bob, collectionId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.CollectionNotFound/);
     });
@@ -140,7 +140,7 @@
     });
 
     itSub('Nobody can remove address from allow list of non-existing collection', async ({helper}) => {
-      const collectionId = (1<<32) - 1;
+      const collectionId = NON_EXISTENT_COLLECTION_ID;
       await expect(helper.collection.removeFromAllowList(bob, collectionId, {Substrate: charlie.address}))
         .to.be.rejectedWith(/common\.CollectionNotFound/);
     });
modifiedtests/src/benchmarks/utils/common.tsdiffbeforeafterboth
--- a/tests/src/benchmarks/utils/common.ts
+++ b/tests/src/benchmarks/utils/common.ts
@@ -91,4 +91,4 @@
     await collection.setTokenPropertyPermissions(donor, permissions);
 
   return collection;
-}
\ No newline at end of file
+}
modifiedtests/src/change-collection-owner.test.tsdiffbeforeafterboth
--- a/tests/src/change-collection-owner.test.ts
+++ b/tests/src/change-collection-owner.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {usingPlaygrounds, expect, itSub} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('Integration Test changeCollectionOwner(collection_id, new_owner):', () => {
   let alice: IKeyringPair;
@@ -127,7 +128,7 @@
   });
 
   itSub('Can\'t change owner of a non-existing collection.', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
+    const collectionId = NON_EXISTENT_COLLECTION_ID;
     const changeOwnerTx = () => helper.collection.changeOwner(bob, collectionId, bob.address);
     await expect(changeOwnerTx()).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
modifiedtests/src/confirmSponsorship.test.tsdiffbeforeafterboth
--- a/tests/src/confirmSponsorship.test.ts
+++ b/tests/src/confirmSponsorship.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {usingPlaygrounds, expect, itSub, Pallets} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 async function setSponsorHelper(collection: any, signer: IKeyringPair, sponsorAddress: string) {
   await collection.setSponsor(signer, sponsorAddress);
@@ -198,7 +199,7 @@
   });
 
   itSub('(!negative test!) Confirm sponsorship for a collection that never existed', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
+    const collectionId = NON_EXISTENT_COLLECTION_ID;
     const confirmSponsorshipTx = () => helper.collection.confirmSponsorship(bob, collectionId);
     await expect(confirmSponsorshipTx()).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
modifiedtests/src/creditFeesToTreasury.seqtest.tsdiffbeforeafterboth
--- a/tests/src/creditFeesToTreasury.seqtest.ts
+++ b/tests/src/creditFeesToTreasury.seqtest.ts
@@ -88,7 +88,7 @@
     expect(treasuryIncrease).to.be.equal(fee);
   });
 
-  itSub.only('Treasury balance increased by failed tx fee', async ({helper}) => {
+  itSub('Treasury balance increased by failed tx fee', async ({helper}) => {
     const api = helper.getApi();
     await helper.wait.newBlocks(1);
 
addedtests/src/eth/abi/nativeFungible.jsondiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/abi/nativeFungible.json
@@ -0,0 +1,201 @@
+[
+  {
+    "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": [
+      {
+        "components": [
+          { "internalType": "address", "name": "eth", "type": "address" },
+          { "internalType": "uint256", "name": "sub", "type": "uint256" }
+        ],
+        "internalType": "struct CrossAddress",
+        "name": "owner",
+        "type": "tuple"
+      }
+    ],
+    "name": "balanceOfCross",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "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": "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": [
+      {
+        "components": [
+          { "internalType": "address", "name": "eth", "type": "address" },
+          { "internalType": "uint256", "name": "sub", "type": "uint256" }
+        ],
+        "internalType": "struct CrossAddress",
+        "name": "to",
+        "type": "tuple"
+      },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "transferCross",
+    "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"
+  },
+  {
+    "inputs": [
+      {
+        "components": [
+          { "internalType": "address", "name": "eth", "type": "address" },
+          { "internalType": "uint256", "name": "sub", "type": "uint256" }
+        ],
+        "internalType": "struct CrossAddress",
+        "name": "from",
+        "type": "tuple"
+      },
+      {
+        "components": [
+          { "internalType": "address", "name": "eth", "type": "address" },
+          { "internalType": "uint256", "name": "sub", "type": "uint256" }
+        ],
+        "internalType": "struct CrossAddress",
+        "name": "to",
+        "type": "tuple"
+      },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "transferFromCross",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]
addedtests/src/eth/api/UniqueNativeFungible.soldiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/api/UniqueNativeFungible.sol
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+/// @dev common stubs holder
+interface Dummy {
+
+}
+
+interface ERC165 is Dummy {
+	function supportsInterface(bytes4 interfaceID) external view returns (bool);
+}
+
+/// @dev the ERC-165 identifier for this interface is 0x1313556c
+interface ERC20UniqueExtensions is Dummy, ERC165 {
+	/// @dev EVM selector for this function is: 0xec069398,
+	///  or in textual repr: balanceOfCross((address,uint256))
+	function balanceOfCross(CrossAddress memory owner) external view returns (uint256);
+
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(CrossAddress memory to, uint256 amount) external returns (bool);
+
+	/// @dev EVM selector for this function is: 0xd5cf430b,
+	///  or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256)
+	function transferFromCross(
+		CrossAddress memory from,
+		CrossAddress memory to,
+		uint256 amount
+	) external returns (bool);
+}
+
+/// Cross account struct
+struct CrossAddress {
+	address eth;
+	uint256 sub;
+}
+
+/// @dev inlined interface
+interface ERC20Events {
+	event Transfer(address indexed from, address indexed to, uint256 value);
+	event Approval(address indexed owner, address indexed spender, uint256 value);
+}
+
+/// @dev the ERC-165 identifier for this interface is 0x942e8b22
+interface ERC20 is Dummy, ERC165, ERC20Events {
+	/// @dev EVM selector for this function is: 0xdd62ed3e,
+	///  or in textual repr: allowance(address,address)
+	function allowance(address owner, address spender) external view returns (uint256);
+
+	/// @dev EVM selector for this function is: 0x095ea7b3,
+	///  or in textual repr: approve(address,uint256)
+	function approve(address spender, uint256 amount) external returns (bool);
+
+	/// @dev EVM selector for this function is: 0x70a08231,
+	///  or in textual repr: balanceOf(address)
+	function balanceOf(address owner) external view returns (uint256);
+
+	/// @dev EVM selector for this function is: 0x313ce567,
+	///  or in textual repr: decimals()
+	function decimals() external view returns (uint8);
+
+	/// @dev EVM selector for this function is: 0x06fdde03,
+	///  or in textual repr: name()
+	function name() external view returns (string memory);
+
+	/// @dev EVM selector for this function is: 0x95d89b41,
+	///  or in textual repr: symbol()
+	function symbol() external view returns (string memory);
+
+	/// @dev EVM selector for this function is: 0x18160ddd,
+	///  or in textual repr: totalSupply()
+	function totalSupply() external view returns (uint256);
+
+	/// @dev EVM selector for this function is: 0xa9059cbb,
+	///  or in textual repr: transfer(address,uint256)
+	function transfer(address to, uint256 amount) external returns (bool);
+
+	/// @dev EVM selector for this function is: 0x23b872dd,
+	///  or in textual repr: transferFrom(address,address,uint256)
+	function transferFrom(
+		address from,
+		address to,
+		uint256 amount
+	) external returns (bool);
+}
+
+interface UniqueNativeFungible is Dummy, ERC165, ERC20, ERC20UniqueExtensions {}
modifiedtests/src/eth/collectionSponsoring.test.tsdiffbeforeafterboth
--- a/tests/src/eth/collectionSponsoring.test.ts
+++ b/tests/src/eth/collectionSponsoring.test.ts
@@ -18,7 +18,7 @@
 import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../util/index';
 import {itEth, expect} from './util';
 
-describe.only('evm nft collection sponsoring', () => {
+describe('evm nft collection sponsoring', () => {
   let donor: IKeyringPair;
   let alice: IKeyringPair;
   let nominal: bigint;
@@ -319,7 +319,7 @@
   });
 });
 
-describe.only('evm RFT collection sponsoring', () => {
+describe('evm RFT collection sponsoring', () => {
   let donor: IKeyringPair;
   let alice: IKeyringPair;
   let nominal: bigint;
modifiedtests/src/eth/createFTCollection.seqtest.tsdiffbeforeafterboth
--- a/tests/src/eth/createFTCollection.seqtest.ts
+++ b/tests/src/eth/createFTCollection.seqtest.ts
@@ -20,7 +20,7 @@
 
 const DECIMALS = 18;
 
-describe.only('Create FT collection from EVM', () => {
+describe('Create FT collection from EVM', () => {
   let donor: IKeyringPair;
 
   before(async function() {
modifiedtests/src/eth/createFTCollection.test.tsdiffbeforeafterboth
--- a/tests/src/eth/createFTCollection.test.ts
+++ b/tests/src/eth/createFTCollection.test.ts
@@ -22,7 +22,7 @@
 
 const DECIMALS = 18;
 
-describe.only('Create FT collection from EVM', () => {
+describe('Create FT collection from EVM', () => {
   let donor: IKeyringPair;
 
   before(async function() {
@@ -129,7 +129,7 @@
   });
 });
 
-describe.only('(!negative tests!) Create FT collection from EVM', () => {
+describe('(!negative tests!) Create FT collection from EVM', () => {
   let donor: IKeyringPair;
   let nominal: bigint;
 
modifiedtests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth
--- a/tests/src/eth/createNFTCollection.test.ts
+++ b/tests/src/eth/createNFTCollection.test.ts
@@ -21,7 +21,7 @@
 import {COLLECTION_HELPER} from '../util';
 
 
-describe.only('Create NFT collection from EVM', () => {
+describe('Create NFT collection from EVM', () => {
   let donor: IKeyringPair;
 
   before(async function () {
@@ -143,7 +143,7 @@
   });
 });
 
-describe.only('(!negative tests!) Create NFT collection from EVM', () => {
+describe('(!negative tests!) Create NFT collection from EVM', () => {
   let donor: IKeyringPair;
   let nominal: bigint;
 
modifiedtests/src/eth/createRFTCollection.test.tsdiffbeforeafterboth
--- a/tests/src/eth/createRFTCollection.test.ts
+++ b/tests/src/eth/createRFTCollection.test.ts
@@ -21,7 +21,7 @@
 import {CollectionLimitField} from './util/playgrounds/types';
 
 
-describe.only('Create RFT collection from EVM', () => {
+describe('Create RFT collection from EVM', () => {
   let donor: IKeyringPair;
 
   before(async function() {
@@ -154,7 +154,7 @@
   });
 });
 
-describe.only('(!negative tests!) Create RFT collection from EVM', () => {
+describe('(!negative tests!) Create RFT collection from EVM', () => {
   let donor: IKeyringPair;
   let nominal: bigint;
 
modifiedtests/src/eth/fractionalizer/fractionalizer.test.tsdiffbeforeafterboth
--- a/tests/src/eth/fractionalizer/fractionalizer.test.ts
+++ b/tests/src/eth/fractionalizer/fractionalizer.test.ts
@@ -80,7 +80,7 @@
 };
 
 
-describe.only('Fractionalizer contract usage', () => {
+describe('Fractionalizer contract usage', () => {
   let donor: IKeyringPair;
 
   before(async function() {
addedtests/src/eth/nativeFungible.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/nativeFungible.test.ts
@@ -0,0 +1,173 @@
+// Copyright 2019-2023 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 {IKeyringPair} from '@polkadot/types/types';
+import {expect, itEth, usingEthPlaygrounds} from './util';
+import {UniqueHelper} from '../util/playgrounds/unique';
+
+describe('NativeFungible: ERC20 calls', () => {
+  let donor: IKeyringPair;
+
+  before(async function() {
+    await usingEthPlaygrounds(async (helper, privateKey) => {
+      donor = await privateKey({url: import.meta.url});
+    });
+  });
+
+  itEth('approve()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const spender = helper.eth.createAccount();
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    await expect(contract.methods.approve(spender, 100).call({from: owner})).to.be.rejectedWith('Approve not supported');
+  });
+
+  itEth('balanceOf()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor, 123n);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const balance = await contract.methods.balanceOf(owner).call({from: owner});
+    expect(balance).to.be.eq('123000000000000000000');
+  });
+
+  itEth('decimals()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const realDecimals = (await helper.chain.getChainProperties().tokenDecimals)[0].toString();
+    const decimals = await contract.methods.decimals().call({from: owner});
+    expect(decimals).to.be.eq(realDecimals);
+  });
+
+  itEth('name()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const realName = await UniqueHelper.detectNetwork(helper.getApi());
+    const name = await contract.methods.name().call({from: owner});
+    expect(name).to.be.eq(realName);
+  });
+
+  itEth('symbol()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const realName = (await helper.chain.getChainProperties().tokenSymbol)[0];
+    const name = await contract.methods.symbol().call({from: owner});
+    expect(name).to.be.eq(realName);
+  });
+
+  itEth('totalSupply()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const totalSupplyEth = BigInt(await contract.methods.totalSupply().call({from: owner}));
+    const totalSupplySub = await helper.balance.getTotalIssuance();
+    expect(totalSupplyEth).to.be.eq(totalSupplySub);
+  });
+
+  itEth('transfer()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const balanceOwnerBefore = await helper.balance.getEthereum(owner);
+    const balanceReceiverBefore = await helper.balance.getEthereum(receiver);
+
+    await contract.methods.transfer(receiver, 50).send({from: owner});
+
+    const balanceOwnerAfter = await helper.balance.getEthereum(owner);
+    const balanceReceiverAfter = await helper.balance.getEthereum(receiver);
+
+    expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true;
+    expect(balanceReceiverBefore + 50n).to.be.equal(balanceReceiverAfter);
+  });
+
+  itEth('transferFrom()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const balanceOwnerBefore = await helper.balance.getEthereum(owner);
+    const balanceReceiverBefore = await helper.balance.getEthereum(receiver);
+
+    await contract.methods.transferFrom(owner, receiver, 50).send({from: owner});
+
+    const balanceOwnerAfter = await helper.balance.getEthereum(owner);
+    const balanceReceiverAfter = await helper.balance.getEthereum(receiver);
+
+    expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true;
+    expect(balanceReceiverBefore === balanceReceiverAfter - 50n).to.be.true;
+
+    await expect(contract.methods.transferFrom(receiver, receiver, 50).call({from: owner})).to.be.rejectedWith('ApprovedValueTooLow');
+  });
+});
+
+describe('NativeFungible: ERC20UniqueExtensions calls', () => {
+  let donor: IKeyringPair;
+
+  before(async function() {
+    await usingEthPlaygrounds(async (helper, privateKey) => {
+      donor = await privateKey({url: import.meta.url});
+    });
+  });
+
+  itEth('transferCross()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const receiver = await helper.ethCrossAccount.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    const balanceOwnerBefore = await helper.balance.getEthereum(owner);
+    const balanceReceiverBefore = await helper.balance.getEthereum(receiver.eth);
+
+    await contract.methods.transferCross(receiver, 50).send({from: owner});
+
+    const balanceOwnerAfter = await helper.balance.getEthereum(owner);
+    const balanceReceiverAfter = await helper.balance.getEthereum(receiver.eth);
+
+    expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true;
+    expect(balanceReceiverBefore === balanceReceiverAfter - 50n).to.be.true;
+  });
+
+  itEth('transferFromCross()', async ({helper}) => {
+    const owner = await helper.ethCrossAccount.createAccountWithBalance(donor);
+    const receiver = await helper.ethCrossAccount.createAccountWithBalance(donor);
+    const collectionAddress = helper.ethAddress.fromCollectionId(0);
+    const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner.eth);
+
+    const balanceOwnerBefore = await helper.balance.getEthereum(owner.eth);
+    const balanceReceiverBefore = await helper.balance.getEthereum(receiver.eth);
+
+    await contract.methods.transferFromCross(owner, receiver, 50).send({from: owner.eth});
+
+    const balanceOwnerAfter = await helper.balance.getEthereum(owner.eth);
+    const balanceReceiverAfter = await helper.balance.getEthereum(receiver.eth);
+
+    expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true;
+    expect(balanceReceiverBefore === balanceReceiverAfter - 50n).to.be.true;
+
+    await expect(contract.methods.transferFromCross(receiver, receiver, 50).call({from: owner.eth})).to.be.rejectedWith('no permission');
+  });
+});
\ No newline at end of file
addedtests/src/eth/nativeRpc/estimateGas.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/nativeRpc/estimateGas.test.ts
@@ -0,0 +1,62 @@
+// 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 {expect, itEth, usingEthPlaygrounds} from '../util';
+import {IKeyringPair} from '@polkadot/types/types';
+
+
+describe('Ethereum native RPC calls', () => {
+  let donor: IKeyringPair;
+  const NATIVE_TOKEN_ADDRESS = '0x17c4e6453cc49aaaaeaca894e6d9683e00000000';
+
+  before(async function() {
+    await usingEthPlaygrounds(async (helper, privateKey) => {
+      donor = await privateKey({url: import.meta.url});
+    });
+  });
+
+  itEth.skip('estimate gas', async ({helper}) => {
+    const BALANCE = 100n;
+    const BALANCE_TO_TRANSFER = 90n;
+
+    const owner = await helper.eth.createAccountWithBalance(donor, BALANCE);
+    const recepient = helper.eth.createAccount();
+
+    const web3 = helper.getWeb3();
+    // data: transfer(recepient, 90);
+    const data = web3.eth.abi.encodeFunctionCall({
+      name: 'transfer',
+      type: 'function',
+      inputs: [{
+        type: 'address',
+        name: 'to',
+      },{
+        type: 'uint256',
+        name: 'amount',
+      }],
+    }, [recepient, (BALANCE_TO_TRANSFER * (10n ** 18n)).toString()]);
+
+    const estimateGas = await web3.eth.estimateGas({
+      to: NATIVE_TOKEN_ADDRESS,
+      value: '0x0',
+      data,
+      from: owner,
+      maxFeePerGas: '0x14c9338c61d',
+    });
+
+    expect(estimateGas).to.be.greaterThan(40000).and.to.be.lessThan(60000);
+  });
+});
modifiedtests/src/eth/nesting/nest.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nesting/nest.test.ts
+++ b/tests/src/eth/nesting/nest.test.ts
@@ -187,4 +187,84 @@
         .call()).to.be.rejectedWith('SourceCollectionIsNotAllowedToNest');
     });
   });
+
+  describe('Fungible', () => {
+    async function createFungibleCollection(helper: EthUniqueHelper, owner: string, mode: 'ft' | 'native ft') {
+      if (mode === 'ft') {
+        const {collectionAddress} = await helper.eth.createFungibleCollection(owner, '', 18, '', '');
+        const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+        await contract.methods.mint(owner, 100n).send({from: owner});
+        return {collectionAddress, contract};
+      }
+
+      // native ft
+      const collectionAddress = helper.ethAddress.fromCollectionId(0);
+      const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+      return {collectionAddress, contract};
+    }
+
+    [
+      {mode: 'ft' as const},
+      {mode: 'native ft' as const},
+    ].map(testCase => {
+      itEth(`Allow nest [${testCase.mode}]`, async ({helper}) => {
+        const owner = await helper.eth.createAccountWithBalance(donor);
+        const {collectionId: targetCollectionId, contract: targetContract} = await createNestingCollection(helper, owner);
+        const {contract: ftContract} = await createFungibleCollection(helper, owner, testCase.mode);
+
+        const mintingTargetTokenIdResult = await targetContract.methods.mint(owner).send({from: owner});
+        const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId;
+        const targetTokenAddress = helper.ethAddress.fromTokenId(targetCollectionId, targetTokenId);
+
+        await ftContract.methods.transfer(targetTokenAddress, 10n).send({from: owner});
+        expect(await ftContract.methods.balanceOf(targetTokenAddress).call({from: owner})).to.be.equal('10');
+      });
+    });
+
+    [
+      {mode: 'ft' as const},
+      {mode: 'native ft' as const},
+    ].map(testCase => {
+      itEth(`Allow partial/full unnest [${testCase.mode}]`, async ({helper}) => {
+        const owner = await helper.eth.createAccountWithBalance(donor);
+        const {collectionId: targetCollectionId, contract: targetContract} = await createNestingCollection(helper, owner);
+        const {contract: ftContract} = await createFungibleCollection(helper, owner, testCase.mode);
+
+        const mintingTargetTokenIdResult = await targetContract.methods.mint(owner).send({from: owner});
+        const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId;
+        const targetTokenAddress = helper.ethAddress.fromTokenId(targetCollectionId, targetTokenId);
+
+        await ftContract.methods.transfer(targetTokenAddress, 10n).send({from: owner});
+
+        await ftContract.methods.transferFrom(targetTokenAddress, owner, 5n).send({from: owner});
+        expect(await ftContract.methods.balanceOf(targetTokenAddress).call({from: owner})).to.be.equal('5');
+
+        await ftContract.methods.transferFrom(targetTokenAddress, owner, 5n).send({from: owner});
+        expect(await ftContract.methods.balanceOf(targetTokenAddress).call({from: owner})).to.be.equal('0');
+      });
+    });
+
+    [
+      {mode: 'ft' as const},
+      {mode: 'native ft' as const},
+    ].map(testCase => {
+      itEth(`Disallow nest into collection without nesting permission [${testCase.mode}] (except for native fungible collection)`, async ({helper}) => {
+        const owner = await helper.eth.createAccountWithBalance(donor);
+        const {collectionId: targetCollectionId, contract: targetContract} = await createNestingCollection(helper, owner);
+        await targetContract.methods.setCollectionNesting(false).send({from: owner});
+
+        const {contract: ftContract} = await createFungibleCollection(helper, owner, testCase.mode);
+
+        const mintingTargetTokenIdResult = await targetContract.methods.mint(owner).send({from: owner});
+        const targetTokenId = mintingTargetTokenIdResult.events.Transfer.returnValues.tokenId;
+        const targetTokenAddress = helper.ethAddress.fromTokenId(targetCollectionId, targetTokenId);
+
+        if (testCase.mode === 'ft') {
+          await expect(ftContract.methods.transfer(targetTokenAddress, 10n).call({from: owner})).to.be.rejectedWith('UserIsNotAllowedToNest');
+        } else {
+          await expect(ftContract.methods.transfer(targetTokenAddress, 10n).call({from: owner})).to.be.not.rejected;
+        }
+      });
+    });
+  });
 });
modifiedtests/src/eth/nonFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -929,7 +929,7 @@
     });
   });
 
-  itEth.only('Returns collection name', async ({helper}) => {
+  itEth('Returns collection name', async ({helper}) => {
     // FIXME: should not have balance to use .call()
     const caller = await helper.eth.createAccountWithBalance(donor);
     const tokenPropertyPermissions = [{
modifiedtests/src/eth/payable.test.tsdiffbeforeafterboth
--- a/tests/src/eth/payable.test.ts
+++ b/tests/src/eth/payable.test.ts
@@ -41,7 +41,7 @@
     expect(await contract.methods.getCollected().call()).to.be.equal('10000');
   });
 
-  itEth.only('Evm contract can receive wei from substrate account', async ({helper}) => {
+  itEth('Evm contract can receive wei from substrate account', async ({helper}) => {
     const deployer = await helper.eth.createAccountWithBalance(donor);
     const contract = await helper.eth.deployCollectorContract(deployer);
     const [alice] = await helper.arrange.createAccounts([40n], donor);
modifiedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/reFungible.test.ts
+++ b/tests/src/eth/reFungible.test.ts
@@ -679,7 +679,7 @@
     });
   });
 
-  itEth.only('Returns collection name', async ({helper}) => {
+  itEth('Returns collection name', async ({helper}) => {
     // FIXME: should not have balance to use .call()
     const caller = await helper.eth.createAccountWithBalance(alice);
     const tokenPropertyPermissions = [{
modifiedtests/src/eth/tokenProperties.test.tsdiffbeforeafterboth
--- a/tests/src/eth/tokenProperties.test.ts
+++ b/tests/src/eth/tokenProperties.test.ts
@@ -314,7 +314,7 @@
       expect(result.length).to.equal(0);
     }));
 
-  itEth.only('Can be read', async({helper}) => {
+  itEth('Can be read', async({helper}) => {
     // FIXME: User with no balance should be able to call
     const caller = await helper.eth.createAccountWithBalance(alice);
     const collection = await helper.nft.mintCollection(alice, {
modifiedtests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -22,6 +22,7 @@
 
 // Native contracts ABI
 import collectionHelpersAbi from '../../abi/collectionHelpers.json' assert {type: 'json'};
+import nativeFungibleAbi from '../../abi/nativeFungible.json' assert {type: 'json'};
 import fungibleAbi from '../../abi/fungible.json' assert {type: 'json'};
 import fungibleDeprecatedAbi from '../../abi/fungibleDeprecated.json' assert {type: 'json'};
 import nonFungibleAbi from '../../abi/nonFungible.json' assert {type: 'json'};
@@ -112,7 +113,6 @@
       data: object,
       from: signer,
       gas: gas ?? this.helper.eth.DEFAULT_GAS,
-      gasPrice: await this.getGasPrice(),
     });
     return await contract.deploy({data: object}).send({from: signer});
   }
@@ -121,30 +121,33 @@
 
 class NativeContractGroup extends EthGroupBase {
 
-  async contractHelpers(caller: string): Promise<Contract> {
+  contractHelpers(caller: string) {
     const web3 = this.helper.getWeb3();
     return new web3.eth.Contract(contractHelpersAbi as any, this.helper.getApi().consts.evmContractHelpers.contractAddress.toString(), {
       from: caller,
       gas: this.helper.eth.DEFAULT_GAS,
-      gasPrice: await this.getGasPrice(),
     });
   }
 
-  async collectionHelpers(caller: string) {
+  collectionHelpers(caller: string) {
     const web3 = this.helper.getWeb3();
     return new web3.eth.Contract(collectionHelpersAbi as any, this.helper.getApi().consts.common.contractAddress.toString(), {
       from: caller,
       gas: this.helper.eth.DEFAULT_GAS,
-      gasPrice: await this.getGasPrice(),
     });
   }
 
-  async collection(address: string, mode: TCollectionMode, caller?: string, mergeDeprecated = false) {
-    let abi = {
-      'nft': nonFungibleAbi,
-      'rft': refungibleAbi,
-      'ft': fungibleAbi,
-    }[mode];
+  collection(address: string, mode: TCollectionMode, caller?: string, mergeDeprecated = false) {
+    let abi;
+    if (address === this.helper.ethAddress.fromCollectionId(0)) {
+      abi = nativeFungibleAbi;
+    } else {
+      abi ={
+        'nft': nonFungibleAbi,
+        'rft': refungibleAbi,
+        'ft': fungibleAbi,
+      }[mode];
+    }
     if (mergeDeprecated) {
       const deprecated = {
         'nft': nonFungibleDeprecatedAbi,
@@ -156,7 +159,6 @@
     const web3 = this.helper.getWeb3();
     return new web3.eth.Contract(abi as any, address, {
       gas: this.helper.eth.DEFAULT_GAS,
-      gasPrice: await this.getGasPrice(),
       ...(caller ? {from: caller} : {}),
     });
   }
@@ -165,12 +167,11 @@
     return this.collection(this.helper.ethAddress.fromCollectionId(collectionId), mode, caller, mergeDeprecated);
   }
 
-  async rftToken(address: string, caller?: string, mergeDeprecated = false) {
+  rftToken(address: string, caller?: string, mergeDeprecated = false) {
     const web3 = this.helper.getWeb3();
     const abi = mergeDeprecated ? [...refungibleTokenAbi, ...refungibleTokenDeprecatedAbi] : refungibleTokenAbi;
     return new web3.eth.Contract(abi as any, address, {
       gas: this.helper.eth.DEFAULT_GAS,
-      gasPrice: await this.getGasPrice(),
       ...(caller ? {from: caller} : {}),
     });
   }
@@ -210,6 +211,7 @@
   async sendEVM(signer: IKeyringPair, contractAddress: string, abi: string, value: string, gasLimit?: number) {
     if (!gasLimit) gasLimit = this.DEFAULT_GAS;
     const web3 = this.helper.getWeb3();
+    // FIXME: can't send legacy transaction using tx.evm.call
     const gasPrice = await web3.eth.getGasPrice();
     // TODO: check execution status
     await this.helper.executeExtrinsic(
@@ -377,7 +379,7 @@
 class EthAddressGroup extends EthGroupBase {
   extractCollectionId(address: string): number {
     if (!(address.length === 42 || address.length === 40)) throw new Error('address wrong format');
-    return parseInt(address.substr(address.length - 8), 16);
+    return parseInt(address.slice(address.length - 8), 16);
   }
 
   fromCollectionId(collectionId: number): string {
modifiedtests/src/maintenance.seqtest.tsdiffbeforeafterboth
--- a/tests/src/maintenance.seqtest.ts
+++ b/tests/src/maintenance.seqtest.ts
@@ -325,7 +325,7 @@
       expect(await helper.preimage.getPreimageInfo(preimageHashes[0])).to.have.property('unrequested');
     });
 
-    itSub.only('Does not allow execution of a preimage that would fail', async ({helper}) => {
+    itSub('Does not allow execution of a preimage that would fail', async ({helper}) => {
       const [zeroAccount] = await helper.arrange.createAccounts([0n], superuser);
 
       const preimage = helper.constructApiCall('api.tx.balances.forceTransfer', [
addedtests/src/nativeFungible.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/nativeFungible.test.ts
@@ -0,0 +1,221 @@
+// Copyright 2019-2023 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 {IKeyringPair} from '@polkadot/types/types';
+import {expect, itSub, usingPlaygrounds} from './util';
+
+describe('Native fungible', () => {
+  let root: IKeyringPair;
+  let alice: IKeyringPair;
+  let bob: IKeyringPair;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      root = await privateKey('//Alice');
+      const donor = await privateKey({url: import.meta.url});
+      [alice, bob] = await helper.arrange.createAccounts([10n, 10n], donor);
+    });
+  });
+
+  itSub('destroy_collection()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.burn(alice)).to.be.rejectedWith('common.UnsupportedOperation');
+    await expect(helper.executeExtrinsic(alice, 'api.tx.unique.destroyCollection', [0])).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('add_to_allow_list()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.addToAllowList(alice, {Substrate: bob.address})).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('remove_from_allow_list()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.removeFromAllowList(alice, {Substrate: bob.address})).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('change_collection_owner()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.changeOwner(alice, bob.address)).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('add_collection_admin()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.addAdmin(alice, {Substrate: bob.address})).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('remove_collection_admin()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.removeAdmin(alice, {Substrate: bob.address})).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('set_collection_sponsor()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.setSponsor(alice, bob.address)).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('confirm_sponsorship()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.confirmSponsorship(alice)).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('remove_collection_sponsor()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.removeSponsor(alice)).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('create_item()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.mint(alice, 100n, {Substrate: bob.address})).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('set_collection_properties()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.setProperties(alice, [{key: 'value'}])).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('delete_collection_properties()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.deleteProperties(alice, ['key'])).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('set_token_properties()', async ({helper}) => {
+    await expect(helper.executeExtrinsic(
+      alice,
+      'api.tx.unique.setTokenProperties',
+      [0, 0, [{key: 'value'}]],
+      true,
+    )).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('delete_token_properties()', async ({helper}) => {
+    await expect(helper.executeExtrinsic(
+      alice,
+      'api.tx.unique.deleteTokenProperties',
+      [0, 0, ['key']],
+      true,
+    )).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('set_transfers_enabled_flag()', async ({helper}) => {
+    await expect(helper.executeExtrinsic(
+      alice,
+      'api.tx.unique.setTransfersEnabledFlag',
+      [0, true],
+      true,
+    )).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('burn_item()', async ({helper}) => {
+    await expect(helper.executeExtrinsic(
+      alice,
+      'api.tx.unique.burnItem',
+      [0, 0, 100n],
+      true,
+    )).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('burn_from()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.burnTokens(alice, 100n)).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('transfer()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    const balanceAliceBefore = await helper.balance.getSubstrate(alice.address);
+    const balanceBobBefore = await helper.balance.getSubstrate(bob.address);
+    await collection.transfer(alice, {Substrate: bob.address}, 100n);
+    const balanceAliceAfter = await helper.balance.getSubstrate(alice.address);
+    const balanceBobAfter = await helper.balance.getSubstrate(bob.address);
+    expect(balanceAliceBefore - balanceAliceAfter > 100n).to.be.true;
+    expect(balanceBobAfter - balanceBobBefore === 100n).to.be.true;
+  });
+
+  itSub('transfer_from()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    const balanceAliceBefore = await helper.balance.getSubstrate(alice.address);
+    const balanceBobBefore = await helper.balance.getSubstrate(bob.address);
+
+    await collection.transferFrom(alice, {Substrate: alice.address}, {Substrate: bob.address}, 100n);
+
+    const balanceAliceAfter = await helper.balance.getSubstrate(alice.address);
+    const balanceBobAfter = await helper.balance.getSubstrate(bob.address);
+    expect(balanceAliceBefore - balanceAliceAfter > 100n).to.be.true;
+    expect(balanceBobAfter - balanceBobBefore === 100n).to.be.true;
+
+    await expect(collection.transferFrom(alice, {Substrate: bob.address}, {Substrate: alice.address}, 100n)).to.be.rejectedWith('common.ApprovedValueTooLow');
+  });
+
+  itSub('approve()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.approveTokens(alice, {Substrate: bob.address}, 100n)).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('approve_from()', async ({helper}) => {
+    await expect(helper.executeExtrinsic(
+      alice,
+      'api.tx.unique.approveFrom',
+      [{Substrate: alice.address}, {Substrate: bob.address}, 0, 0, 100n],
+      true,
+    )).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('set_collection_limits()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.setLimits(alice, {accountTokenOwnershipLimit: 1})).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('set_collection_permissions()', async ({helper}) => {
+    const collection = helper.ft.getCollectionObject(0);
+    await expect(collection.setPermissions(alice, {access: 'AllowList'})).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('repartition()', async ({helper}) => {
+    await expect(helper.executeExtrinsic(
+      alice,
+      'api.tx.unique.repartition',
+      [0, 0, 100n],
+      true,
+    )).to.be.rejectedWith('unique.RepartitionCalledOnNonRefungibleCollection');
+  });
+
+  itSub('force_repair_collection()', async ({helper}) => {
+    await expect(helper.executeExtrinsic(
+      alice,
+      'api.tx.unique.forceRepairCollection',
+      [0],
+      true,
+    )).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('force_repair_item()', async ({helper}) => {
+    await expect(helper.getSudo().executeExtrinsic(
+      root,
+      'api.tx.unique.forceRepairItem',
+      [0, 0],
+      true,
+    )).to.be.rejectedWith('common.UnsupportedOperation');
+  });
+
+  itSub('Nest into NFT token()', async ({helper}) => {
+    const nftCollection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await nftCollection.mintToken(alice);
+
+    const collection = helper.ft.getCollectionObject(0);
+    await collection.transfer(alice, targetToken.nestingAccount(), 100n);
+
+    await collection.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 100n);
+  });
+});
\ No newline at end of file
modifiedtests/src/nesting/unnest.test.tsdiffbeforeafterboth
--- a/tests/src/nesting/unnest.test.ts
+++ b/tests/src/nesting/unnest.test.ts
@@ -47,6 +47,20 @@
     await expect(nestedToken.getOwner()).to.be.rejected;
   });
 
+  itSub('NativeFungible: allows the owner to successfully unnest a token', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const targetToken = await collection.mintToken(alice);
+
+    const collectionFT = helper.ft.getCollectionObject(0);
+
+    // Nest
+    await collectionFT.transfer(alice, targetToken.nestingAccount(), 10n);
+    // Unnest
+    await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
+
+    expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(1n);
+  });
+
   itSub('Fungible: allows the owner to successfully unnest a token', async ({helper}) => {
     const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
     const targetToken = await collection.mintToken(alice);
modifiedtests/src/pallet-presence.test.tsdiffbeforeafterboth
--- a/tests/src/pallet-presence.test.ts
+++ b/tests/src/pallet-presence.test.ts
@@ -19,6 +19,7 @@
 // Pallets that must always be present
 const requiredPallets = [
   'balances',
+  'balancesadapter',
   'common',
   'timestamp',
   'transactionpayment',
modifiedtests/src/removeCollectionAdmin.test.tsdiffbeforeafterboth
--- a/tests/src/removeCollectionAdmin.test.ts
+++ b/tests/src/removeCollectionAdmin.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {itSub, usingPlaygrounds, expect} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('Integration Test removeCollectionAdmin(collection_id, account_id):', () => {
   let alice: IKeyringPair;
@@ -68,7 +69,7 @@
   });
 
   itSub('Can\'t remove collection admin from not existing collection', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
+    const collectionId = NON_EXISTENT_COLLECTION_ID;
 
     await expect(helper.collection.removeAdmin(alice, collectionId, {Substrate: bob.address}))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
modifiedtests/src/removeCollectionSponsor.test.tsdiffbeforeafterboth
--- a/tests/src/removeCollectionSponsor.test.ts
+++ b/tests/src/removeCollectionSponsor.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {itSub, usingPlaygrounds, expect} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('integration test: ext. removeCollectionSponsor():', () => {
   let donor: IKeyringPair;
@@ -91,7 +92,7 @@
   });
 
   itSub('(!negative test!) Remove sponsor for a collection that never existed', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
+    const collectionId = NON_EXISTENT_COLLECTION_ID;
     await expect(helper.collection.removeSponsor(alice, collectionId)).to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
modifiedtests/src/setCollectionLimits.test.tsdiffbeforeafterboth
--- a/tests/src/setCollectionLimits.test.ts
+++ b/tests/src/setCollectionLimits.test.ts
@@ -17,6 +17,7 @@
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import {IKeyringPair} from '@polkadot/types/types';
 import {itSub, usingPlaygrounds, expect} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 const accountTokenOwnershipLimit = 0;
 const sponsoredDataSize = 0;
@@ -110,7 +111,7 @@
   });
 
   itSub('execute setCollectionLimits for not exists collection', async ({helper}) => {
-    const nonExistentCollectionId = (1 << 32) - 1;
+    const nonExistentCollectionId = NON_EXISTENT_COLLECTION_ID;
     await expect(helper.collection.setLimits(
       alice,
       nonExistentCollectionId,
modifiedtests/src/setCollectionSponsor.test.tsdiffbeforeafterboth
--- a/tests/src/setCollectionSponsor.test.ts
+++ b/tests/src/setCollectionSponsor.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {itSub, usingPlaygrounds, expect, Pallets} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('integration test: ext. setCollectionSponsor():', () => {
   let alice: IKeyringPair;
@@ -105,7 +106,7 @@
   });
 
   itSub('(!negative test!) Add sponsor to a collection that never existed', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
+    const collectionId = NON_EXISTENT_COLLECTION_ID;
     await expect(helper.collection.setSponsor(alice, collectionId, bob.address))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
   });
modifiedtests/src/setPermissions.test.tsdiffbeforeafterboth
--- a/tests/src/setPermissions.test.ts
+++ b/tests/src/setPermissions.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {itSub, usingPlaygrounds, expect} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('Integration Test: Set Permissions', () => {
   let alice: IKeyringPair;
@@ -85,7 +86,7 @@
   });
 
   itSub('fails on not existing collection', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
+    const collectionId = NON_EXISTENT_COLLECTION_ID;
     await expect(helper.collection.setPermissions(alice, collectionId, {access: 'AllowList', mintMode: true}))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
   });
modifiedtests/src/sub/appPromotion/appPromotion.test.tsdiffbeforeafterboth
--- a/tests/src/sub/appPromotion/appPromotion.test.ts
+++ b/tests/src/sub/appPromotion/appPromotion.test.ts
@@ -157,7 +157,8 @@
       await helper.staking.stake(staker, 1000n * nominal);
       await helper.staking.stake(staker, 199n * nominal);
       // check balances
-      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}, {id: 'appstake', amount: 1199n * nominal, reasons: 'All'}]);
+      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);
+      expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: 1199n * nominal}]);
       expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 1199n * nominal});
       expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);
@@ -453,7 +454,7 @@
       const [_unstake1, unstake2] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});
       await helper.wait.forParachainBlockNumber(unstake2.block);
 
-      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([]);
+      expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([]);
       expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});
       expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(999n);
       expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);
modifiedtests/src/sub/nesting/admin.test.tsdiffbeforeafterboth
--- a/tests/src/sub/nesting/admin.test.ts
+++ b/tests/src/sub/nesting/admin.test.ts
@@ -17,7 +17,7 @@
 import {IKeyringPair} from '@polkadot/types/types';
 import {expect, itSub, usingPlaygrounds} from '../../util';
 
-describe('Collection admin', () => {
+describe('Nesting by collection admin', () => {
   let alice: IKeyringPair;
   let bob: IKeyringPair;
   let charlie: IKeyringPair;
@@ -84,4 +84,3 @@
     expect(await newToken.getOwner()).to.be.deep.equal(targetToken.nestingAccount().toLowerCase());
   });
 });
-
modifiedtests/src/sub/nesting/common.test.tsdiffbeforeafterboth
--- a/tests/src/sub/nesting/common.test.ts
+++ b/tests/src/sub/nesting/common.test.ts
@@ -21,102 +21,144 @@
 let alice: IKeyringPair;
 let bob: IKeyringPair;
 
-before(async () => {
-  await usingPlaygrounds(async (helper, privateKey) => {
-    const donor = await privateKey({url: import.meta.url});
-    [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
+describe('Common nesting tests', () => {
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = await privateKey({url: import.meta.url});
+      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);
+    });
   });
-});
 
-[
-  {mode: 'nft' as const, restrictedMode: true, requiredPallets: []},
-  {mode: 'nft' as const, restrictedMode: false, requiredPallets: []},
-  {mode: 'rft' as const, restrictedMode: true, requiredPallets: [Pallets.ReFungible]},
-  {mode: 'rft' as const, restrictedMode: false, requiredPallets: [Pallets.ReFungible]},
-].map(testCase => {
-  itSub.ifWithPallets(`Token owner can nest ${testCase.mode.toUpperCase()} in NFT if "tokenOwner" permission set ${testCase.restrictedMode ? 'in restricted mode': ''}`, testCase.requiredPallets, async ({helper}) => {
-    // Only NFT can be target for nesting in
-    const targetNFTCollection = await helper.nft.mintCollection(alice);
-    const targetTokenBob = await targetNFTCollection.mintToken(alice, {Substrate: bob.address});
+  [
+    {mode: 'nft' as const, restrictedMode: true, requiredPallets: []},
+    {mode: 'nft' as const, restrictedMode: false, requiredPallets: []},
+    {mode: 'rft' as const, restrictedMode: true, requiredPallets: [Pallets.ReFungible]},
+    {mode: 'rft' as const, restrictedMode: false, requiredPallets: [Pallets.ReFungible]},
+  ].map(testCase => {
+    itSub.ifWithPallets(`Token owner can nest ${testCase.mode.toUpperCase()} in NFT if "tokenOwner" permission set ${testCase.restrictedMode ? 'in restricted mode': ''}`, testCase.requiredPallets, async ({helper}) => {
+      // Only NFT can be target for nesting in
+      const targetNFTCollection = await helper.nft.mintCollection(alice);
+      const targetTokenBob = await targetNFTCollection.mintToken(alice, {Substrate: bob.address});
+
+      const collectionForNesting = await helper[testCase.mode].mintCollection(bob);
+      // permissions should be set:
+      await targetNFTCollection.setPermissions(alice, {
+        nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null},
+      });
 
-    const collectionForNesting = await helper[testCase.mode].mintCollection(bob);
-    // permissions should be set:
-    await targetNFTCollection.setPermissions(alice, {
-      nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null},
+      // 1. Bob can immediately create nested token:
+      const nestedToken1 = testCase.mode === 'nft'
+        ? await (collectionForNesting as UniqueNFTCollection).mintToken(bob, targetTokenBob.nestingAccount())
+        : await (collectionForNesting as UniqueRFTCollection).mintToken(bob, 10n, targetTokenBob.nestingAccount());
+      expect(await nestedToken1.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
+      expect(await nestedToken1.getOwner()).to.be.deep.equal(targetTokenBob.nestingAccount().toLowerCase());
+
+      // 2. Bob can mint and nest token:
+      const nestedToken2 = await collectionForNesting.mintToken(bob);
+      await nestedToken2.nest(bob, targetTokenBob);
+      expect(await nestedToken2.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
+      expect(await nestedToken2.getOwner()).to.be.deep.equal(targetTokenBob.nestingAccount().toLowerCase());
     });
+  });
 
-    // 1. Bob can immediately create nested token:
-    const nestedToken1 = testCase.mode === 'nft'
-      ? await (collectionForNesting as UniqueNFTCollection).mintToken(bob, targetTokenBob.nestingAccount())
-      : await (collectionForNesting as UniqueRFTCollection).mintToken(bob, 10n, targetTokenBob.nestingAccount());
-    expect(await nestedToken1.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
-    expect(await nestedToken1.getOwner()).to.be.deep.equal(targetTokenBob.nestingAccount().toLowerCase());
 
-    // 2. Bob can mint and nest token:
-    const nestedToken2 = await collectionForNesting.mintToken(bob);
-    await nestedToken2.nest(bob, targetTokenBob);
-    expect(await nestedToken2.getTopmostOwner()).to.be.deep.equal({Substrate: bob.address});
-    expect(await nestedToken2.getOwner()).to.be.deep.equal(targetTokenBob.nestingAccount().toLowerCase());
+  [
+    {restrictedMode: true},
+    {restrictedMode: false},
+  ].map(testCase => {
+    itSub(`Token owner can nest FT in NFT if "tokenOwner" permission set  ${testCase.restrictedMode ? 'in restricted mode': ''}`, async ({helper}) => {
+      // Only NFT allows nesting, permissions should be set:
+      const targetNFTCollection = await helper.nft.mintCollection(alice);
+      const targetTokenBob = await targetNFTCollection.mintToken(alice, {Substrate: bob.address});
+
+      const collectionForNesting = await helper.ft.mintCollection(bob);
+      // permissions should be set:
+      await targetNFTCollection.setPermissions(alice, {
+        nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null},
+      });
+
+      // 1. Alice can immediately create nested tokens:
+      await collectionForNesting.mint(bob, 100n, targetTokenBob.nestingAccount());
+      expect(await collectionForNesting.getTop10Owners()).deep.eq([targetTokenBob.nestingAccount().toLowerCase()]);
+      expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(100n);
+
+      // 2. Alice can mint and nest token:
+      await collectionForNesting.mint(bob, 100n);
+      await collectionForNesting.transfer(bob, targetTokenBob.nestingAccount(), 50n);
+      expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(150n);
+      expect(await targetTokenBob.getChildren()).to.be.deep.equal([{collectionId: collectionForNesting.collectionId, tokenId: 0}]);
+    });
   });
-});
 
+  [
+    {restrictedMode: true},
+    {restrictedMode: false},
+  ].map(testCase => {
+    itSub(`Token owner can nest Native FT in NFT if "tokenOwner" permission set  ${testCase.restrictedMode ? 'in restricted mode': ''}`, async ({helper}) => {
+      // Only NFT allows nesting, permissions should be set:
+      const targetNFTCollection = await helper.nft.mintCollection(alice);
+      const targetTokenBob = await targetNFTCollection.mintToken(alice, {Substrate: bob.address});
+      expect(await targetTokenBob.getChildren()).to.be.empty;
 
-[
-  {restrictedMode: true},
-  {restrictedMode: false},
-].map(testCase => {
-  itSub(`Token owner can nest FT in NFT if "tokenOwner" permission set  ${testCase.restrictedMode ? 'in restricted mode': ''}`, async ({helper}) => {
-    // Only NFT allows nesting, permissions should be set:
-    const targetNFTCollection = await helper.nft.mintCollection(alice);
-    const targetTokenBob = await targetNFTCollection.mintToken(alice, {Substrate: bob.address});
+      const collectionForNesting = helper.ft.getCollectionObject(0);
+      // permissions should be set:
+      await targetNFTCollection.setPermissions(alice, {
+        nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null},
+      });
 
-    const collectionForNesting = await helper.ft.mintCollection(bob);
-    // permissions should be set:
-    await targetNFTCollection.setPermissions(alice, {
-      nesting: {tokenOwner: true, restricted: testCase.restrictedMode ? [collectionForNesting.collectionId] : null},
+      // Bob can nest Native FT into their NFT:
+      await collectionForNesting.transfer(bob, targetTokenBob.nestingAccount(), 50n);
+      expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(50n);
+      // Native FT should't be visible in NFT children:
+      expect(await targetTokenBob.getChildren()).to.be.deep.equal([]);
     });
+  });
+
 
-    // 1. Alice can immediately create nested tokens:
-    await collectionForNesting.mint(bob, 100n, targetTokenBob.nestingAccount());
-    expect(await collectionForNesting.getTop10Owners()).deep.eq([targetTokenBob.nestingAccount().toLowerCase()]);
-    expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(100n);
+  itSub.ifWithPallets('Owner can unnest tokens using transferFrom', [Pallets.ReFungible], async ({helper}) => {
+    const collectionToNest = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const tokenA = await collectionToNest.mintToken(alice);
+    const tokenB = await collectionToNest.mintToken(alice);
 
-    // 2. Alice can mint and nest token:
-    await collectionForNesting.mint(bob, 100n);
-    await collectionForNesting.transfer(bob, targetTokenBob.nestingAccount(), 50n);
-    expect(await collectionForNesting.getBalance(targetTokenBob.nestingAccount())).eq(150n);
-  });
-});
+    // Create a nested token
+    const nftCollectionToBeNested = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const rftCollectionToBeNested = await helper.rft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const ftCollectionToBeNested = await helper.ft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    const nativeFtCollectionToBeNested = helper.ft.getCollectionObject(0);
 
+    const nestedNFT = await nftCollectionToBeNested.mintToken(alice, tokenA.nestingAccount());
+    const nestedRFT = await rftCollectionToBeNested.mintToken(alice, 100n, tokenA.nestingAccount());
+    const _nestedFT = await ftCollectionToBeNested.mint(alice, 100n, tokenA.nestingAccount());
+    await nativeFtCollectionToBeNested.transfer(alice, tokenA.nestingAccount(), 100n);
+    expect(await nestedNFT.getOwner()).to.be.deep.equal(tokenA.nestingAccount().toLowerCase());
+    expect(await nestedRFT.getOwner()).to.be.deep.equal(tokenA.nestingAccount().toLowerCase());
+    expect(await ftCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(100n);
+    expect(await nativeFtCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(100n);
 
-itSub.ifWithPallets('Owner can unnest tokens using transferFrom', [Pallets.ReFungible], async ({helper}) => {
-  const collectionToNest = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
-  const tokenA = await collectionToNest.mintToken(alice);
-  const tokenB = await collectionToNest.mintToken(alice);
+    expect(await tokenA.getChildren()).to.be.length(3);
+    expect(await tokenB.getChildren()).to.be.length(0);
 
-  // Create a nested token
-  const nftCollectionToBeNested = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
-  const rftCollectionToBeNested = await helper.rft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
-  const ftCollectionToBeNested = await helper.ft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    // Transfer the nested token to another token
+    await nestedNFT.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount());
+    await nestedRFT.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount(), 25n);
+    await ftCollectionToBeNested.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount(), 25n);
+    await nativeFtCollectionToBeNested.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount(), 25n);
 
-  const nestedNFT = await nftCollectionToBeNested.mintToken(alice, tokenA.nestingAccount());
-  const nestedRFT = await rftCollectionToBeNested.mintToken(alice, 100n, tokenA.nestingAccount());
-  const _nestedFT = await ftCollectionToBeNested.mint(alice, 100n, tokenA.nestingAccount());
-  expect(await nestedNFT.getOwner()).to.be.deep.equal(tokenA.nestingAccount().toLowerCase());
-  expect(await nestedRFT.getOwner()).to.be.deep.equal(tokenA.nestingAccount().toLowerCase());
-  expect(await ftCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(100n);
+    expect(await nestedNFT.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
+    expect(await nestedNFT.getOwner()).to.be.deep.equal(tokenB.nestingAccount().toLowerCase());
 
-  // Transfer the nested token to another token
-  await nestedNFT.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount());
-  await nestedRFT.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount(), 25n);
-  await ftCollectionToBeNested.transferFrom(alice, tokenA.nestingAccount(), tokenB.nestingAccount(), 25n);
+    expect(await nestedRFT.getBalance(tokenB.nestingAccount())).to.equal(25n);
+    expect(await nestedRFT.getBalance(tokenA.nestingAccount())).to.equal(75n);
 
-  expect(await nestedNFT.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
-  expect(await nestedNFT.getOwner()).to.be.deep.equal(tokenB.nestingAccount().toLowerCase());
+    expect(await ftCollectionToBeNested.getBalance(tokenB.nestingAccount())).to.equal(25n);
+    expect(await ftCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(75n);
 
-  expect(await nestedRFT.getBalance(tokenB.nestingAccount())).to.equal(25n);
-  expect(await nestedRFT.getBalance(tokenA.nestingAccount())).to.equal(75n);
+    expect(await nativeFtCollectionToBeNested.getBalance(tokenB.nestingAccount())).to.equal(25n);
+    expect(await nativeFtCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(75n);
 
-  expect(await ftCollectionToBeNested.getBalance(tokenB.nestingAccount())).to.equal(25n);
-  expect(await ftCollectionToBeNested.getBalance(tokenA.nestingAccount())).to.equal(75n);
+    // RFT, FT, and without native FT
+    expect(await tokenA.getChildren()).to.be.length(2);
+    // NFT, RFT, FT, and without native FT
+    expect(await tokenB.getChildren()).to.be.length(3);
+  });
 });
modifiedtests/src/sub/nesting/e2e.test.tsdiffbeforeafterboth
--- a/tests/src/sub/nesting/e2e.test.ts
+++ b/tests/src/sub/nesting/e2e.test.ts
@@ -32,6 +32,7 @@
     const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
     const collectionB = await helper.ft.mintCollection(alice);
     const collectionC = await helper.rft.mintCollection(alice);
+    const collectionNative = helper.ft.getCollectionObject(0);
 
     const targetToken = await collectionA.mintToken(alice);
     expect((await targetToken.getChildren()).length).to.be.equal(0, 'Children length check at creation');
@@ -73,6 +74,14 @@
       {tokenId: tokenC.tokenId, collectionId: collectionC.collectionId},
     ]).and.has.length(3);
 
+    // Nest native fungible token into another collection
+    await collectionNative.transfer(alice, targetToken.nestingAccount(), 2n);
+    expect(await targetToken.getChildren()).to.have.deep.members([
+      {tokenId: tokenA.tokenId, collectionId: collectionA.collectionId},
+      {tokenId: 0, collectionId: collectionB.collectionId},
+      {tokenId: tokenC.tokenId, collectionId: collectionC.collectionId},
+    ]).and.has.length(3);
+
     // Burn all nested pieces
     await tokenC.burnFrom(alice, targetToken.nestingAccount(), 2n);
     expect(await targetToken.getChildren()).to.have.deep.members([
modifiedtests/src/sub/nesting/nesting.negative.test.tsdiffbeforeafterboth
--- a/tests/src/sub/nesting/nesting.negative.test.ts
+++ b/tests/src/sub/nesting/nesting.negative.test.ts
@@ -23,238 +23,272 @@
 let bob: IKeyringPair;
 let charlie: IKeyringPair;
 
-before(async () => {
-  await usingPlaygrounds(async (helper, privateKey) => {
-    const donor = await privateKey({url: import.meta.url});
-    [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
+describe('Negative Test: Nesting', () => {
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = await privateKey({url: import.meta.url});
+      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
+    });
   });
-});
 
-[
-  {mode: 'nft' as const, requiredPallets: []},
-  {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
-].map(testCase => {
-  itSub.ifWithPallets(`Owner cannot nest ${testCase.mode.toUpperCase()} if nesting is disabled`, testCase.requiredPallets, async ({helper}) => {
-    // Create default collection, permissions are not set:
-    const aliceNFTCollection = await helper.nft.mintCollection(alice);
-    const targetToken = await aliceNFTCollection.mintToken(alice);
+  [
+    {mode: 'nft' as const, requiredPallets: []},
+    {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+  ].map(testCase => {
+    itSub.ifWithPallets(`Owner cannot nest ${testCase.mode.toUpperCase()} if nesting is disabled`, testCase.requiredPallets, async ({helper}) => {
+      // Create default collection, permissions are not set:
+      const aliceNFTCollection = await helper.nft.mintCollection(alice);
+      const targetToken = await aliceNFTCollection.mintToken(alice);
 
-    const collectionForNesting = await helper[testCase.mode].mintCollection(alice);
+      const collectionForNesting = await helper[testCase.mode].mintCollection(alice);
 
-    // 1. Alice cannot create immediately nested tokens:
-    const nestingTx = testCase.mode === 'nft'
-      ? (collectionForNesting as UniqueNFTCollection).mintToken(alice, targetToken.nestingAccount())
-      : (collectionForNesting as UniqueRFTCollection).mintToken(alice, 10n, targetToken.nestingAccount());
-    await expect(nestingTx).to.be.rejectedWith('common.UserIsNotAllowedToNest');
+      // 1. Alice cannot create immediately nested tokens:
+      const nestingTx = testCase.mode === 'nft'
+        ? (collectionForNesting as UniqueNFTCollection).mintToken(alice, targetToken.nestingAccount())
+        : (collectionForNesting as UniqueRFTCollection).mintToken(alice, 10n, targetToken.nestingAccount());
+      await expect(nestingTx).to.be.rejectedWith('common.UserIsNotAllowedToNest');
 
-    // 2. Alice cannot mint and nest token:
-    const nestedToken2 = await collectionForNesting.mintToken(alice);
-    await expect(nestedToken2.nest(alice, targetToken)).to.be.rejectedWith('common.UserIsNotAllowedToNest');
+      // 2. Alice cannot mint and nest token:
+      const nestedToken2 = await collectionForNesting.mintToken(alice);
+      await expect(nestedToken2.nest(alice, targetToken)).to.be.rejectedWith('common.UserIsNotAllowedToNest');
+    });
   });
-});
 
-itSub('Owner cannot nest FT if nesting is disabled', async ({helper}) => {
-  // Create default collection, permissions are not set:
-  const aliceNFTCollection = await helper.nft.mintCollection(alice);
-  const targetToken = await aliceNFTCollection.mintToken(alice);
+  [
+    {mode: 'ft'},
+    {mode: 'nativeFt'},
+  ].map(testCase => {
+    itSub(`Owner cannot nest [${testCase.mode}] if nesting is disabled (except for native fungible collection)`, async ({helper}) => {
+    // Create default collection, permissions are not set:
+      const aliceNFTCollection = await helper.nft.mintCollection(alice);
+      const targetToken = await aliceNFTCollection.mintToken(alice);
 
-  const collectionForNesting = await helper.ft.mintCollection(alice);
+      const collectionForNesting = testCase.mode === 'ft' ? await helper.ft.mintCollection(alice) : helper.ft.getCollectionObject(0);
 
-  // 1. Alice cannot create immediately nested tokens:
-  await expect(collectionForNesting.mint(alice, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest');
+      // Alice cannot create immediately nested tokens:
+      if (testCase.mode === 'ft') {
+        await expect(collectionForNesting.mint(alice, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest');
+      } else {
+        await expect(collectionForNesting.transfer(alice, targetToken.nestingAccount(), 100n)).to.be.not.rejected;
+      }
 
-  // 2. Alice can mint and nest token:
-  await collectionForNesting.mint(alice, 100n);
-  await expect(collectionForNesting.transfer(alice, targetToken.nestingAccount(), 50n)).to.be.rejectedWith('common.UserIsNotAllowedToNest');
-});
+      // Alice can't mint and nest tokens:
+      if (testCase.mode === 'ft') {
+        await collectionForNesting.mint(alice, 100n);
+      }
 
-[
-  {mode: 'nft' as const},
-  {mode: 'rft' as const},
-  {mode: 'ft' as const},
-].map(testCase => {
-  itSub(`Non-owner and non-admin cannot nest ${testCase.mode.toUpperCase()} in someone else's tokens`, async ({helper}) => {
-    const targetCollection = await helper.nft.mintCollection(alice, {permissions:
-      {nesting: {tokenOwner: true, collectionAdmin: true}},
+      if (testCase.mode === 'ft') {
+        await expect(collectionForNesting.transfer(alice, targetToken.nestingAccount(), 50n)).to.be.rejectedWith('common.UserIsNotAllowedToNest');
+      } else {
+        await expect(collectionForNesting.transfer(alice, targetToken.nestingAccount(), 50n)).to.be.not.rejected;
+      }
     });
-    const targetToken = await targetCollection.mintToken(alice);
+  });
+
+  [
+    {mode: 'nft' as const},
+    {mode: 'rft' as const},
+    {mode: 'ft' as const},
+    {mode: 'native ft' as const},
+  ].map(testCase => {
+    itSub(`Non-owner and non-admin cannot nest ${testCase.mode.toUpperCase()} in someone else's tokens (except for native fungible collection)`, async ({helper}) => {
+      const targetCollection = await helper.nft.mintCollection(alice, {permissions:
+        {nesting: {tokenOwner: true, collectionAdmin: true}},
+      });
+      const targetToken = await targetCollection.mintToken(alice);
 
-    const nestedCollectionBob = await helper[testCase.mode].mintCollection(bob);
+      const nestedCollectionBob = await (
+        testCase.mode === 'native ft'
+          ? helper.ft.getCollectionObject(0)
+          : helper[testCase.mode].mintCollection(bob)
+      );
 
-    let nestedTokenBob: UniqueNFToken | UniqueRFToken;
-    switch (testCase.mode) {
-      case 'nft': nestedTokenBob = await (nestedCollectionBob as UniqueNFTCollection).mintToken(bob); break;
-      case 'rft': nestedTokenBob = await (nestedCollectionBob as UniqueRFTCollection).mintToken(bob, 100n); break;
-      case 'ft': await (nestedCollectionBob as UniqueFTCollection).mint(bob, 100n); break;
-    }
+      let nestedTokenBob: UniqueNFToken | UniqueRFToken;
+      switch (testCase.mode) {
+        case 'nft': nestedTokenBob = await (nestedCollectionBob as UniqueNFTCollection).mintToken(bob); break;
+        case 'rft': nestedTokenBob = await (nestedCollectionBob as UniqueRFTCollection).mintToken(bob, 100n); break;
+        case 'ft': await (nestedCollectionBob as UniqueFTCollection).mint(bob, 100n); break;
+        case 'native ft': await expect((nestedCollectionBob as UniqueFTCollection).mint(bob, 100n)).to.be.rejectedWith('common.UnsupportedOperation'); break;
+      }
 
-    // Bob non-owner of targetToken and non admin of targetCollection, so
-    // 1. cannot mint nested token:
-    switch (testCase.mode) {
-      case 'nft': await expect((nestedCollectionBob as UniqueNFTCollection).mintToken(bob, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
-      case 'rft': await expect((nestedCollectionBob as UniqueRFTCollection).mintToken(bob, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
-      case 'ft': await expect((nestedCollectionBob as UniqueFTCollection).mint(bob, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
-    }
+      // Bob non-owner of targetToken and non admin of targetCollection, so
+      // 1. cannot mint nested token:
+      switch (testCase.mode) {
+        case 'nft': await expect((nestedCollectionBob as UniqueNFTCollection).mintToken(bob, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
+        case 'rft': await expect((nestedCollectionBob as UniqueRFTCollection).mintToken(bob, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
+        case 'ft': await expect((nestedCollectionBob as UniqueFTCollection).mint(bob, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
+        case 'native ft': await expect((nestedCollectionBob as UniqueFTCollection).mint(bob, 100n, targetToken.nestingAccount())).to.be.rejectedWith('common.UnsupportedOperation'); break;
+      }
 
-    // 2. cannot nest existing token:
-    switch (testCase.mode) {
-      case 'nft':
-      case 'rft': await expect(nestedTokenBob!.transfer(bob, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
-      case 'ft': await expect((nestedCollectionBob as UniqueFTCollection).transfer(bob, targetToken.nestingAccount(), 100n)).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
-    }
+      // 2. cannot nest existing token:
+      switch (testCase.mode) {
+        case 'nft':
+        case 'rft': await expect(nestedTokenBob!.transfer(bob, targetToken.nestingAccount())).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
+        case 'ft': await expect((nestedCollectionBob as UniqueFTCollection).transfer(bob, targetToken.nestingAccount(), 100n)).to.be.rejectedWith('common.UserIsNotAllowedToNest'); break;
+        case 'native ft': await expect((nestedCollectionBob as UniqueFTCollection).transfer(bob, targetToken.nestingAccount(), 100n)).to.be.not.rejected; break;
+      }
+    });
   });
-});
 
-[
-  {mode: 'nft' as const, nesting: {tokenOwner: true,  collectionAdmin: false}},
-  {mode: 'nft' as const, nesting: {tokenOwner: false, collectionAdmin: true}},
-].map(testCase => {
-  itSub(`${testCase.nesting.tokenOwner ? 'Admin' : 'Token owner'} cannot nest when only ${testCase.nesting.tokenOwner ? 'tokenOwner' : 'collectionAdmin'} is allowed`, async ({helper}) => {
-    // Create collection with tokenOwner or create collection with collectionAdmin permission:
-    const targetCollection = await helper.nft.mintCollection(alice, {permissions: {nesting: testCase.nesting}});
-    const targetTokenCharlie = await targetCollection.mintToken(alice, {Substrate: charlie.address});
-    await targetCollection.addAdmin(alice, {Substrate: bob.address});
+  [
+    {mode: 'nft' as const, nesting: {tokenOwner: true,  collectionAdmin: false}},
+    {mode: 'nft' as const, nesting: {tokenOwner: false, collectionAdmin: true}},
+  ].map(testCase => {
+    itSub(`${testCase.nesting.tokenOwner ? 'Admin' : 'Token owner'} cannot nest when only ${testCase.nesting.tokenOwner ? 'tokenOwner' : 'collectionAdmin'} is allowed`, async ({helper}) => {
+      // Create collection with tokenOwner or create collection with collectionAdmin permission:
+      const targetCollection = await helper.nft.mintCollection(alice, {permissions: {nesting: testCase.nesting}});
+      const targetTokenCharlie = await targetCollection.mintToken(alice, {Substrate: charlie.address});
+      await targetCollection.addAdmin(alice, {Substrate: bob.address});
 
-    const nestedCollectionCharlie = await helper[testCase.mode].mintCollection(charlie);
-    const nestedCollectionBob = await helper[testCase.mode].mintCollection(bob);
-    // if nesting permissions restricted for token owner – minter is bob (admin),
-    // if collectionAdmin – charlie (owner)
-    testCase.nesting.tokenOwner
-      ? await expect(nestedCollectionBob.mintToken(bob, targetTokenCharlie.nestingAccount())).to.be.rejectedWith(/common\.UserIsNotAllowedToNest/)
-      : await expect(nestedCollectionCharlie.mintToken(charlie, targetTokenCharlie.nestingAccount())).to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+      const nestedCollectionCharlie = await helper[testCase.mode].mintCollection(charlie);
+      const nestedCollectionBob = await helper[testCase.mode].mintCollection(bob);
+      // if nesting permissions restricted for token owner – minter is bob (admin),
+      // if collectionAdmin – charlie (owner)
+      testCase.nesting.tokenOwner
+        ? await expect(nestedCollectionBob.mintToken(bob, targetTokenCharlie.nestingAccount())).to.be.rejectedWith(/common\.UserIsNotAllowedToNest/)
+        : await expect(nestedCollectionCharlie.mintToken(charlie, targetTokenCharlie.nestingAccount())).to.be.rejectedWith(/common\.UserIsNotAllowedToNest/);
+    });
   });
-});
 
-itSub.ifWithPallets('Cannot nest in non existing token', [Pallets.ReFungible], async ({helper}) => {
-  const collection = await helper.nft.mintCollection(alice);
-  // To avoid UserIsNotAllowedToNest error
-  await helper.collection.setPermissions(alice, collection.collectionId, {nesting: {collectionAdmin: true}});
+  itSub.ifWithPallets('Cannot nest in non existing token (except for native fungible collection)', [Pallets.ReFungible], async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice);
+    // To avoid UserIsNotAllowedToNest error
+    await helper.collection.setPermissions(alice, collection.collectionId, {nesting: {collectionAdmin: true}});
 
-  // The list of non-existing tokens:
-  const tokenFromNonExistingCollection = helper.nft.getTokenObject(9999999, 1);
-  const tokenBurnt = await collection.mintToken(alice);
-  await tokenBurnt.burn(alice);
-  const tokenNotMintedYet = helper.nft.getTokenObject(collection.collectionId, 2);
+    // The list of non-existing tokens:
+    const tokenFromNonExistingCollection = helper.nft.getTokenObject(9999999, 1);
+    const tokenBurnt = await collection.mintToken(alice);
+    await tokenBurnt.burn(alice);
+    const tokenNotMintedYet = helper.nft.getTokenObject(collection.collectionId, 2);
 
-  // The list of collections to nest tokens from:
-  const nftCollectionForNesting = await helper.nft.mintCollection(alice);
-  const rftCollectionForNesting = await helper.rft.mintCollection(alice);
-  const ftCollectionForNesting = await helper.ft.mintCollection(alice);
+    // The list of collections to nest tokens from:
+    const nftCollectionForNesting = await helper.nft.mintCollection(alice);
+    const rftCollectionForNesting = await helper.rft.mintCollection(alice);
+    const ftCollectionForNesting = await helper.ft.mintCollection(alice);
+    const nativeFtCollectionForNesting = helper.ft.getCollectionObject(0);
 
-  const testCases = [
-    {token: tokenFromNonExistingCollection, error: 'CollectionNotFound'},
-    {token: tokenBurnt, error: 'TokenNotFound'},
-    {token: tokenNotMintedYet, error: 'TokenNotFound'},
-  ];
+    const testCases = [
+      {token: tokenFromNonExistingCollection, error: 'CollectionNotFound'},
+      {token: tokenBurnt, error: 'TokenNotFound'},
+      {token: tokenNotMintedYet, error: 'TokenNotFound'},
+    ];
 
-  for(const testCase of testCases) {
-    // 1. Alice cannot create nested token to non-existing token
-    await expect(nftCollectionForNesting.mintToken(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
-    await expect(rftCollectionForNesting.mintToken(alice, 10n, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
-    await expect(ftCollectionForNesting.mint(alice, 10n, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
+    for(const testCase of testCases) {
+      // 1. Alice cannot create nested token to non-existing token
+      await expect(nftCollectionForNesting.mintToken(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
+      await expect(rftCollectionForNesting.mintToken(alice, 10n, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
+      await expect(ftCollectionForNesting.mint(alice, 10n, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
 
-    // 2. Alice cannot mint and nest token:
-    const nft = await nftCollectionForNesting.mintToken(alice);
-    const rft = await rftCollectionForNesting.mintToken(alice, 100n);
-    const _ft = await ftCollectionForNesting.mint(alice, 100n);
-    await expect(nft.transfer(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
-    await expect(rft.transfer(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
-    await expect(ftCollectionForNesting.transfer(alice, testCase.token.nestingAccount(), 50n)).to.be.rejectedWith(testCase.error);
-  }
-});
+      // 2. Alice cannot mint and nest token:
+      const nft = await nftCollectionForNesting.mintToken(alice);
+      const rft = await rftCollectionForNesting.mintToken(alice, 100n);
+      const _ft = await ftCollectionForNesting.mint(alice, 100n);
+      await expect(nft.transfer(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
+      await expect(rft.transfer(alice, testCase.token.nestingAccount())).to.be.rejectedWith(testCase.error);
+      await expect(ftCollectionForNesting.transfer(alice, testCase.token.nestingAccount(), 50n)).to.be.rejectedWith(testCase.error);
+      await expect(nativeFtCollectionForNesting.transfer(alice, testCase.token.nestingAccount(), 50n)).to.be.not.rejected;
+    }
+  });
 
-itEth.ifWithPallets('Cannot nest in collection address', [Pallets.ReFungible], async({helper}) => {
-  const existingCollection = await helper.nft.mintCollection(alice);
-  const existingCollectionAddress = helper.ethAddress.fromCollectionId(existingCollection.collectionId);
-  const futureCollectionAddress = helper.ethAddress.fromCollectionId(99999999);
+  itEth.ifWithPallets('Cannot nest in collection address', [Pallets.ReFungible], async({helper}) => {
+    const existingCollection = await helper.nft.mintCollection(alice);
+    const existingCollectionAddress = helper.ethAddress.fromCollectionId(existingCollection.collectionId);
+    const futureCollectionAddress = helper.ethAddress.fromCollectionId(99999999);
 
-  const nftCollectionForNesting = await helper.nft.mintCollection(alice);
+    const nftCollectionForNesting = await helper.nft.mintCollection(alice);
 
-  // 1. Alice cannot create nested token to collection address
-  await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
-  await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
+    // 1. Alice cannot create nested token to collection address
+    await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
+    await expect(nftCollectionForNesting.mintToken(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
 
-  // 2. Alice cannot mint and nest token to collection address:
-  const nft = await nftCollectionForNesting.mintToken(alice);
-  await expect(nft.transfer(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
-  await expect(nft.transfer(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
-});
+    // 2. Alice cannot mint and nest token to collection address:
+    const nft = await nftCollectionForNesting.mintToken(alice);
+    await expect(nft.transfer(alice, {Ethereum: existingCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
+    await expect(nft.transfer(alice, {Ethereum: futureCollectionAddress})).to.be.rejectedWith('CantNestTokenUnderCollection');
+  });
 
-itEth.ifWithPallets('Cannot nest in RFT or FT', [Pallets.ReFungible], async ({helper}) => {
-  // Create default collection, permissions are not set:
-  const rftCollection = await helper.rft.mintCollection(alice);
-  const ftCollection = await helper.ft.mintCollection(alice);
+  itEth.ifWithPallets('Cannot nest in RFT or FT (except for native fungible collection)', [Pallets.ReFungible], async ({helper}) => {
+    // Create default collection, permissions are not set:
+    const rftCollection = await helper.rft.mintCollection(alice);
+    const ftCollection = await helper.ft.mintCollection(alice);
+    const nativeFtCollection = helper.ft.getCollectionObject(0);
 
-  const rftToken = await rftCollection.mintToken(alice);
-  const _ftToken = await ftCollection.mint(alice, 100n);
+    const rftToken = await rftCollection.mintToken(alice);
+    const _ftToken = await ftCollection.mint(alice, 100n);
 
-  const collectionForNesting = await helper.nft.mintCollection(alice);
+    const collectionForNesting = await helper.nft.mintCollection(alice);
 
-  // 1. Alice cannot create immediately nested tokens:
-  await expect(collectionForNesting.mintToken(alice, rftToken.nestingAccount())).to.be.rejectedWith('refungible.RefungibleDisallowsNesting');
-  await expect(collectionForNesting.mintToken(alice, {Ethereum: helper.ethAddress.fromTokenId(ftCollection.collectionId, 0)})).to.be.rejectedWith('fungible.FungibleDisallowsNesting');
+    // 1. Alice cannot create immediately nested tokens:
+    await expect(collectionForNesting.mintToken(alice, rftToken.nestingAccount())).to.be.rejectedWith('refungible.RefungibleDisallowsNesting');
+    await expect(collectionForNesting.mintToken(alice, {Ethereum: helper.ethAddress.fromTokenId(ftCollection.collectionId, 0)})).to.be.rejectedWith('fungible.FungibleDisallowsNesting');
+    await expect(collectionForNesting.mintToken(alice, {Ethereum: helper.ethAddress.fromTokenId(nativeFtCollection.collectionId, 0)})).to.be.rejectedWith('common.UnsupportedOperation');
 
-  // 2. Alice cannot mint and nest token:
-  const nestedToken2 = await collectionForNesting.mintToken(alice);
-  await expect(nestedToken2.nest(alice, rftToken)).to.be.rejectedWith('refungible.RefungibleDisallowsNesting');
-  await expect(ftCollection.transfer(alice, {Ethereum: helper.ethAddress.fromTokenId(ftCollection.collectionId, 0)})).to.be.rejectedWith('fungible.FungibleDisallowsNesting');
-});
+    // 2. Alice cannot mint and nest token:
+    const nestedToken2 = await collectionForNesting.mintToken(alice);
+    await expect(nestedToken2.nest(alice, rftToken)).to.be.rejectedWith('refungible.RefungibleDisallowsNesting');
+    await expect(ftCollection.transfer(alice, {Ethereum: helper.ethAddress.fromTokenId(ftCollection.collectionId, 0)})).to.be.rejectedWith('fungible.FungibleDisallowsNesting');
+    await expect(nativeFtCollection.transfer(alice, {Ethereum: helper.ethAddress.fromTokenId(nativeFtCollection.collectionId, 0)})).to.be.not.rejected;
+  });
 
-itSub('Cannot nest in restricted collection if collection is not in the list', async ({helper}) => {
-  const {collectionId: allowedCollectionId} = await helper.nft.mintCollection(alice);
-  const notAllowedCollectionNFT = await helper.nft.mintCollection(alice);
-  const notAllowedCollectionRFT = await helper.rft.mintCollection(alice);
-  const notAllowedCollectionFT = await helper.ft.mintCollection(alice);
+  itSub('Cannot nest in restricted collection if collection is not in the list (except native fungible collection)', async ({helper}) => {
+    const {collectionId: allowedCollectionId} = await helper.nft.mintCollection(alice);
+    const notAllowedCollectionNFT = await helper.nft.mintCollection(alice);
+    const notAllowedCollectionRFT = await helper.rft.mintCollection(alice);
+    const notAllowedCollectionFT = await helper.ft.mintCollection(alice);
+    const allowedCollectionNativeFT = helper.ft.getCollectionObject(0);
 
-  // Collection restricted to allowedCollectionId
-  const restrictedCollectionA = await helper.nft.mintCollection(alice, {permissions:
-    {nesting: {tokenOwner: true, restricted: [allowedCollectionId]}},
-  });
-  // Create collection with restricted nesting -- even self is not allowed
-  const restrictedCollectionB = await helper.nft.mintCollection(alice, {permissions:
-    {nesting: {tokenOwner: true, restricted: []}},
-  });
+    // Collection restricted to allowedCollectionId
+    const restrictedCollectionA = await helper.nft.mintCollection(alice, {permissions:
+      {nesting: {tokenOwner: true, restricted: [allowedCollectionId]}},
+    });
+    // Create collection with restricted nesting -- even self is not allowed
+    const restrictedCollectionB = await helper.nft.mintCollection(alice, {permissions:
+      {nesting: {tokenOwner: true, restricted: []}},
+    });
 
-  const targetTokenA = await restrictedCollectionA.mintToken(alice);
-  const targetTokenB = await restrictedCollectionB.mintToken(alice);
+    const targetTokenA = await restrictedCollectionA.mintToken(alice);
+    const targetTokenB = await restrictedCollectionB.mintToken(alice);
 
-  // 1. Cannot mint in own collection after allowlisting the accounts:
-  await expect(restrictedCollectionA.mintToken(alice, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-  await expect(restrictedCollectionB.mintToken(alice, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    // 1. Cannot mint in own collection after allowlisting the accounts:
+    await expect(restrictedCollectionA.mintToken(alice, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    await expect(restrictedCollectionB.mintToken(alice, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
 
-  // 2. Cannot mint from notAllowedCollection:
-  await expect(notAllowedCollectionNFT.mintToken(alice, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-  await expect(notAllowedCollectionNFT.mintToken(alice, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-  await expect(notAllowedCollectionRFT.mintToken(alice, 100n, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-  await expect(notAllowedCollectionRFT.mintToken(alice, 100n, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-  await expect(notAllowedCollectionFT.mint(alice, 100n, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-  await expect(notAllowedCollectionFT.mint(alice, 100n, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
-});
+    // 2. Cannot mint from notAllowedCollection:
+    await expect(notAllowedCollectionNFT.mintToken(alice, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    await expect(notAllowedCollectionNFT.mintToken(alice, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    await expect(notAllowedCollectionRFT.mintToken(alice, 100n, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    await expect(notAllowedCollectionRFT.mintToken(alice, 100n, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    await expect(notAllowedCollectionFT.mint(alice, 100n, targetTokenA.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    await expect(notAllowedCollectionFT.mint(alice, 100n, targetTokenB.nestingAccount())).to.be.rejectedWith(/common\.SourceCollectionIsNotAllowedToNest/);
+    await expect(allowedCollectionNativeFT.transfer(alice, targetTokenA.nestingAccount(), 100n)).to.be.not.rejected;
+    await expect(allowedCollectionNativeFT.transfer(alice, targetTokenB.nestingAccount(), 100n)).to.be.not.rejected;
+  });
 
-itSub('Cannot create nesting chains greater than 5', async ({helper}) => {
-  const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
-  let token = await collection.mintToken(alice);
+  itSub('Cannot create nesting chains greater than 5', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+    let token = await collection.mintToken(alice);
 
-  const maxNestingLevel = 5;
+    const maxNestingLevel = 5;
 
-  // Create a nested-token matryoshka
-  for (let i = 0; i < maxNestingLevel; i++) {
-    token = await collection.mintToken(alice, token.nestingAccount());
-  }
+    // Create a nested-token matryoshka
+    for (let i = 0; i < maxNestingLevel; i++) {
+      token = await collection.mintToken(alice, token.nestingAccount());
+    }
 
-  // The nesting depth is limited by `maxNestingLevel`
-  // 1. Cannot mint:
-  await expect(collection.mintToken(alice, token.nestingAccount()))
-    .to.be.rejectedWith(/structure\.DepthLimit/);
-  // 2. Cannot transfer:
-  const anotherToken = await collection.mintToken(alice);
-  await expect(anotherToken.transfer(alice, token.nestingAccount()))
-    .to.be.rejectedWith(/structure\.DepthLimit/);
-  // 3. Cannot nest FT pieces:
-  const ftCollection = await helper.ft.mintCollection(alice);
-  await expect(ftCollection.mint(alice, 100n, token.nestingAccount()))
-    .to.be.rejectedWith(/structure\.DepthLimit/);
+    // The nesting depth is limited by `maxNestingLevel`
+    // 1. Cannot mint:
+    await expect(collection.mintToken(alice, token.nestingAccount()))
+      .to.be.rejectedWith(/structure\.DepthLimit/);
+    // 2. Cannot transfer:
+    const anotherToken = await collection.mintToken(alice);
+    await expect(anotherToken.transfer(alice, token.nestingAccount()))
+      .to.be.rejectedWith(/structure\.DepthLimit/);
+    // 3. Cannot nest FT pieces:
+    const ftCollection = await helper.ft.mintCollection(alice);
+    await expect(ftCollection.mint(alice, 100n, token.nestingAccount()))
+      .to.be.rejectedWith(/structure\.DepthLimit/);
 
-  expect(await token.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
-  expect(await token.getChildren()).to.has.length(0);
+    expect(await token.getTopmostOwner()).to.be.deep.equal({Substrate: alice.address});
+    expect(await token.getChildren()).to.has.length(0);
+  });
 });
modifiedtests/src/sub/nesting/refungible.test.tsdiffbeforeafterboth
--- a/tests/src/sub/nesting/refungible.test.ts
+++ b/tests/src/sub/nesting/refungible.test.ts
@@ -17,44 +17,45 @@
 import {IKeyringPair} from '@polkadot/types/types';
 import {expect, itSub, Pallets, usingPlaygrounds} from '../../util';
 
-// ReFungible specific nesting tests
-let alice: IKeyringPair;
+describe('ReFungible-specific nesting tests', () => {
+  let alice: IKeyringPair;
 
-before(async () => {
-  await usingPlaygrounds(async (helper, privateKey) => {
-    const donor = await privateKey({url: import.meta.url});
-    [alice] = await helper.arrange.createAccounts([200n], donor);
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      const donor = await privateKey({url: import.meta.url});
+      [alice] = await helper.arrange.createAccounts([200n], donor);
+    });
   });
-});
 
-itSub.ifWithPallets('ReFungible: getTopmostOwner works correctly with Nesting', [Pallets.ReFungible], async({helper}) => {
-  const collectionNFT = await helper.nft.mintCollection(alice, {
-    permissions: {
-      nesting: {
-        tokenOwner: true,
+  itSub.ifWithPallets('ReFungible: getTopmostOwner works correctly with Nesting', [Pallets.ReFungible], async({helper}) => {
+    const collectionNFT = await helper.nft.mintCollection(alice, {
+      permissions: {
+        nesting: {
+          tokenOwner: true,
+        },
       },
-    },
-  });
-  const collectionRFT = await helper.rft.mintCollection(alice);
+    });
+    const collectionRFT = await helper.rft.mintCollection(alice);
 
-  const nft = await collectionNFT.mintToken(alice, {Substrate: alice.address});
-  const rft = await collectionRFT.mintToken(alice, 100n, {Substrate: alice.address});
+    const nft = await collectionNFT.mintToken(alice, {Substrate: alice.address});
+    const rft = await collectionRFT.mintToken(alice, 100n, {Substrate: alice.address});
 
-  expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
+    expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
 
-  await rft.transfer(alice, nft.nestingAccount(), 40n);
+    await rft.transfer(alice, nft.nestingAccount(), 40n);
 
-  expect(await rft.getTopmostOwner()).deep.equal(null);
+    expect(await rft.getTopmostOwner()).deep.equal(null);
 
-  await rft.transfer(alice, nft.nestingAccount(), 60n);
+    await rft.transfer(alice, nft.nestingAccount(), 60n);
 
-  expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
+    expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
 
-  await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 30n);
+    await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 30n);
 
-  expect(await rft.getTopmostOwner()).deep.equal(null);
+    expect(await rft.getTopmostOwner()).deep.equal(null);
 
-  await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 70n);
+    await rft.transferFrom(alice, nft.nestingAccount(), {Substrate: alice.address}, 70n);
 
-  expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
+    expect(await rft.getTopmostOwner()).deep.equal({Substrate: alice.address});
+  });
 });
modifiedtests/src/sub/nesting/unnesting.negative.test.tsdiffbeforeafterboth
--- a/tests/src/sub/nesting/unnesting.negative.test.ts
+++ b/tests/src/sub/nesting/unnesting.negative.test.ts
@@ -52,12 +52,19 @@
   });
 
   [
-    {restrictedMode: true},
-    {restrictedMode: false},
+    {mode: 'ft' as const},
+    {mode: 'native ft' as const},
+  ].map(md => [
+    {mode: md.mode, restrictedMode: true},
+    {mode: md.mode, restrictedMode: false},
   ].map(testCase => {
-    itSub(`Fungible: disallows a non-Owner to unnest someone else's token ${testCase.restrictedMode ? '(Restricted nesting)' : ''}`, async ({helper}) => {
+    itSub(`Fungible: disallows a non-Owner to unnest someone else's token [${testCase.mode}${testCase.restrictedMode ? ' (Restricted nesting)' : ''}]`, async ({helper}) => {
       const collectionNFT = await helper.nft.mintCollection(alice);
-      const collectionFT = await helper.ft.mintCollection(alice);
+      const collectionFT = await (
+        testCase.mode === 'ft'
+          ? helper.ft.mintCollection(alice)
+          : helper.ft.getCollectionObject(0)
+      );
       const targetToken = await collectionNFT.mintToken(alice, {Substrate: bob.address});
 
       await collectionNFT.setPermissions(alice, {nesting: {
@@ -65,12 +72,16 @@
       }});
 
       // Nest some tokens as Alice into Bob's token
-      await collectionFT.mint(alice, 5n, targetToken.nestingAccount());
+      await (
+        testCase.mode === 'ft'
+          ? collectionFT.mint(alice, 5n, targetToken.nestingAccount())
+          : collectionFT.transfer(alice, targetToken.nestingAccount(), 5n)
+      );
 
       // Try to pull it out as Alice still
       await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: bob.address}, 1n))
         .to.be.rejectedWith(/common\.ApprovedValueTooLow/);
       expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(5n);
     });
-  });
+  }));
 });
modifiedtests/src/transfer.test.tsdiffbeforeafterboth
--- a/tests/src/transfer.test.ts
+++ b/tests/src/transfer.test.ts
@@ -17,6 +17,7 @@
 import {IKeyringPair} from '@polkadot/types/types';
 import {itEth, usingEthPlaygrounds} from './eth/util';
 import {itSub, Pallets, usingPlaygrounds, expect} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('Integration Test Transfer(recipient, collection_id, item_id, value)', () => {
   let donor: IKeyringPair;
@@ -124,20 +125,17 @@
 
 
   itSub('[nft] Transfer with not existed collection_id', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
-    await expect(helper.nft.transferToken(alice, collectionId, 1, {Substrate: bob.address}))
+    await expect(helper.nft.transferToken(alice, NON_EXISTENT_COLLECTION_ID, 1, {Substrate: bob.address}))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
   itSub('[fungible] Transfer with not existed collection_id', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
-    await expect(helper.ft.transfer(alice, collectionId, {Substrate: bob.address}))
+    await expect(helper.ft.transfer(alice, NON_EXISTENT_COLLECTION_ID, {Substrate: bob.address}))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
   itSub.ifWithPallets('[refungible] Transfer with not existed collection_id', [Pallets.ReFungible], async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
-    await expect(helper.rft.transferToken(alice, collectionId, 1, {Substrate: bob.address}))
+    await expect(helper.rft.transferToken(alice, NON_EXISTENT_COLLECTION_ID, 1, {Substrate: bob.address}))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
modifiedtests/src/transferFrom.test.tsdiffbeforeafterboth
--- a/tests/src/transferFrom.test.ts
+++ b/tests/src/transferFrom.test.ts
@@ -16,6 +16,7 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import {itSub, Pallets, usingPlaygrounds, expect} from './util';
+import {NON_EXISTENT_COLLECTION_ID} from './util/playgrounds/types';
 
 describe('Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {
   let alice: IKeyringPair;
@@ -97,10 +98,9 @@
   });
 
   itSub('transferFrom for a collection that does not exist', async ({helper}) => {
-    const collectionId = (1 << 32) - 1;
-    await expect(helper.collection.approveToken(alice, collectionId, 0, {Substrate: bob.address}, 1n))
+    await expect(helper.collection.approveToken(alice, NON_EXISTENT_COLLECTION_ID, 0, {Substrate: bob.address}, 1n))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
-    await expect(helper.collection.transferTokenFrom(bob, collectionId, 0, {Substrate: alice.address}, {Substrate: bob.address}, 1n))
+    await expect(helper.collection.transferTokenFrom(bob, NON_EXISTENT_COLLECTION_ID, 0, {Substrate: alice.address}, {Substrate: bob.address}, 1n))
       .to.be.rejectedWith(/common\.CollectionNotFound/);
   });
 
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -3,6 +3,8 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 
+export const NON_EXISTENT_COLLECTION_ID = 4_294_967_295;
+
 export interface IEvent {
   section: string;
   method: string;
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -7,11 +7,8 @@
 
 import {ApiPromise, WsProvider, Keyring} from '@polkadot/api';
 import {SignerOptions} from '@polkadot/api/types/submittable';
-import '../../interfaces/augment-api-tx';
+import '../../interfaces/augment-api';
 import {AugmentedSubmittables} from '@polkadot/api-base/types/submittable';
-import {RpcInterface} from '@polkadot/rpc-core/types';
-import {QueryableStorage} from '@polkadot/api-base/types/storage';
-import {DecoratedRpc} from '@polkadot/api-base/types/rpc';
 import {ApiInterfaceEvents} from '@polkadot/api/types';
 import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm, base58Encode, blake2AsU8a} from '@polkadot/util-crypto';
 import {IKeyringPair} from '@polkadot/types/types';
@@ -50,7 +47,7 @@
 } from './types';
 import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';
 import type {Vec} from '@polkadot/types-codec';
-import {FrameSystemEventRecord} from '@polkadot/types/lookup';
+import {FrameSystemEventRecord, PalletBalancesIdAmount} from '@polkadot/types/lookup';
 
 export class CrossAccountId {
   Substrate!: TSubstrateAccount;
@@ -379,15 +376,6 @@
 type Get2<T, P extends string, E> =
   P extends `${infer Key}.${infer Key2}` ? Key extends keyof T ? Key2 extends keyof T[Key] ? T[Key][Key2] : E : E : E;
 type ForceFunction<T> = T extends (...args: any) => any ? T : (...args: any) => Invalid<'not a function'>;
-type ReturnTypeWithArgs<T extends (...args: any[]) => any, ARGS_T> =
-  Extract<
-    T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; } ? [A1, R1] | [A2, R2] | [A3, R3] | [A4, R4] :
-    T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; } ? [A1, R1] | [A2, R2] | [A3, R3] :
-    T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2; } ? [A1, R1] | [A2, R2] :
-    T extends { (...args: infer A1): infer R1; } ? [A1, R1] :
-    never,
-    [ARGS_T, any]
-  >[1]
 
 export class ChainHelperBase {
   helperBase: any;
@@ -498,7 +486,7 @@
 
     if (xcmChains.indexOf(spec.specName) > -1) return spec.specName;
 
-    if (['quartz', 'unique'].indexOf(spec.specName) > -1) return spec.specName;
+    if (['quartz', 'unique', 'sapphire'].indexOf(spec.specName) > -1) return spec.specName;
     return 'opal';
   }
 
@@ -677,12 +665,12 @@
   async executeExtrinsic<
     E extends string,
     V extends (
-...args: any) => any = ForceFunction<
-      Get2<
-        AugmentedSubmittables<'promise'>,
-        E, (...args: any) => Invalid<'not found'>
+      ...args: any) => any = ForceFunction<
+        Get2<
+          AugmentedSubmittables<'promise'>,
+          E, (...args: any) => Invalid<'not found'>
+        >
       >
-    >
   >(
     sender: TSigner,
     extrinsic: `api.tx.${E}`,
@@ -1756,9 +1744,7 @@
       children = await this.helper.callRpc('api.rpc.unique.tokenChildren', [collectionId, tokenId, blockHashAt]);
     }
 
-    return children.toJSON().map((x: any) => {
-      return {collectionId: x.collection, tokenId: x.token};
-    });
+    return children.toJSON().map((x: any) => ({collectionId: x.collection, tokenId: x.token}));
   }
 
   /**
@@ -2400,14 +2386,22 @@
     return {free: accountInfo.free.toBigInt(), frozen: accountInfo.frozen.toBigInt(), reserved: accountInfo.reserved.toBigInt()};
   }
 
+  /**
+   * Get total issuance
+   * @returns
+   */
+  async getTotalIssuance(): Promise<bigint> {
+    const total = (await this.helper.callRpc('api.query.balances.totalIssuance', []));
+    return total.toBigInt();
+  }
+
   async getLocked(address: TSubstrateAccount): Promise<[{ id: string, amount: bigint, reason: string }]> {
     const locks = (await this.helper.callRpc('api.query.balances.locks', [address])).toHuman();
-    return locks.map((lock: any) => { return {id: lock.id, amount: BigInt(lock.amount.replace(/,/g, '')), reasons: lock.reasons}; });
+    return locks.map((lock: any) => ({id: lock.id, amount: BigInt(lock.amount.replace(/,/g, '')), reasons: lock.reasons}));
   }
-
-  async getFrozen(address: TSubstrateAccount): Promise<[{ id: string, amount: bigint}]> {
-    const locks = (await this.helper.callRpc('api.query.balances.freezes', [address])).toHuman();
-    return locks.map((lock: any) => { return {id: lock.id, amount: BigInt(lock.amount.replace(/,/g, ''))}; });
+  async getFrozen(address: TSubstrateAccount): Promise<{ id: string, amount: bigint }[]> {
+    const locks = await this.helper.api!.query.balances.freezes(address);
+    return locks.map(lock => ({id: lock.id.toString(), amount: lock.amount.toBigInt()}));
   }
 }
 
@@ -2493,9 +2487,18 @@
   }
 
   /**
+   * Get total issuance
+   * @returns
+   */
+  getTotalIssuance(): Promise<bigint> {
+    return this.subBalanceGroup.getTotalIssuance();
+  }
+
+  /**
    * Get locked balances
    * @param address substrate address
    * @returns locked balances with reason via api.query.balances.locks
+   * @deprecated all the methods should switch to getFrozen
    */
   getLocked(address: TSubstrateAccount) {
     return this.subBalanceGroup.getLocked(address);
modifiedtests/src/vesting.test.tsdiffbeforeafterboth
--- a/tests/src/vesting.test.ts
+++ b/tests/src/vesting.test.ts
@@ -103,7 +103,7 @@
     expect(balanceSender.reserved).to.eq(0n);
   });
 
-  itSub.only('cannot send more tokens than have', async ({helper}) => {
+  itSub('cannot send more tokens than have', async ({helper}) => {
     const [sender, receiver] = await helper.arrange.createAccounts([1000n, 1n], donor);
     const schedule = {start: 0n, period: 1n, periodCount: 1n, perPeriod: 100n * nominal};
     const manyPeriodsSchedule = {start: 0n, period: 1n, periodCount: 100n, perPeriod: 10n * nominal};