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
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -72,7 +72,6 @@
 	dispatch::Pays,
 	transactional, fail,
 };
-use pallet_evm::GasWeightMapping;
 use up_data_structs::{
 	AccessMode, COLLECTION_NUMBER_LIMIT, Collection, RpcCollection, CollectionFlags,
 	RpcCollectionFlags, CollectionId, CreateItemData, MAX_TOKEN_PREFIX_LENGTH,
@@ -105,7 +104,7 @@
 /// Collection handle contains information about collection data and id.
 /// Also provides functionality to count consumed gas.
 ///
-/// CollectionHandle is used as a generic wrapper for collections of all types.
+/// CollectionHandle is used as a generic wrapper for collections of all types (except native fungible).
 /// It allows to perform common operations and queries on any collection type,
 /// both completely general for all, as well as their respective implementations of [`CommonCollectionOperations`].
 #[must_use = "Should call submit_logs or save, otherwise some data will be lost for evm side"]
@@ -129,11 +128,7 @@
 impl<T: Config> CollectionHandle<T> {
 	/// Same as [CollectionHandle::new] but with an explicit gas limit.
 	pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option<Self> {
-		<CollectionById<T>>::get(id).map(|collection| Self {
-			id,
-			collection,
-			recorder: SubstrateRecorder::new(gas_limit),
-		})
+		Self::new_with_recorder(id, SubstrateRecorder::new(gas_limit))
 	}
 
 	/// Same as [CollectionHandle::new] but with an existed [`SubstrateRecorder`].
@@ -161,14 +156,7 @@
 		&self,
 		reads: u64,
 	) -> pallet_evm_coder_substrate::execution::Result<()> {
-		self.recorder
-			.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,
-			)))
+		self.recorder().consume_store_reads(reads)
 	}
 
 	/// Consume gas for writing.
@@ -176,14 +164,7 @@
 		&self,
 		writes: u64,
 	) -> pallet_evm_coder_substrate::execution::Result<()> {
-		self.recorder
-			.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,
-			)))
+		self.recorder().consume_store_writes(writes)
 	}
 
 	/// Consume gas for reading and writing.
@@ -192,15 +173,8 @@
 		reads: u64,
 		writes: u64,
 	) -> pallet_evm_coder_substrate::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.recorder
-			.consume_gas(T::GasWeightMapping::weight_to_gas(Weight::from_parts(
-				reads.saturating_add(writes),
-				// TODO: measure proof
-				0,
-			)))
+		self.recorder()
+			.consume_store_reads_and_writes(reads, writes)
 	}
 
 	/// Save collection to storage.
@@ -469,6 +443,8 @@
 	}
 
 	const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
+	/// Collection id for native fungible collction.
+	pub const NATIVE_FUNGIBLE_COLLECTION_ID: CollectionId = CollectionId(0);
 
 	#[pallet::pallet]
 	#[pallet::storage_version(STORAGE_VERSION)]
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
before · tests/src/sub/appPromotion/appPromotion.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19  itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip, LOCKING_PERIOD, UNLOCKING_PERIOD,20} from '../../util';21import {DevUniqueHelper} from '../../util/playgrounds/unique.dev';22import {itEth, expect, SponsoringMode} from '../../eth/util';2324let donor: IKeyringPair;25let palletAdmin: IKeyringPair;26let nominal: bigint;27let palletAddress: string;28let accounts: IKeyringPair[];29let usedAccounts: IKeyringPair[] = [];3031async function getAccounts(accountsNumber: number, balance?: bigint) {32  let accs: IKeyringPair[] = [];33  if (balance) {34    await usingPlaygrounds(async (helper) => {35      accs = await helper.arrange.createAccounts(new Array(accountsNumber).fill(balance), donor);36    });37  } else {38    accs = accounts.splice(0, accountsNumber);39  }40  usedAccounts.push(...accs);41  return accs;42}43// App promotion periods:44// LOCKING_PERIOD = 12 blocks of relay45// UNLOCKING_PERIOD = 6 blocks of parachain4647describe('App promotion', () => {48  before(async function () {49    await usingPlaygrounds(async (helper, privateKey) => {50      requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]);51      donor = await privateKey({url: import.meta.url});52      palletAddress = helper.arrange.calculatePalletAddress('appstake');53      palletAdmin = await privateKey('//PromotionAdmin');54      nominal = helper.balance.getOneTokenNominal();5556      const accountBalances = new Array(200).fill(1000n);57      accounts = await helper.arrange.createAccounts(accountBalances, donor); // create accounts-pool to speed up tests58    });59  });6061  afterEach(async () => {62    await usingPlaygrounds(async (helper) => {63      let unstakeTxs = [];64      for (const account of usedAccounts) {65        if (unstakeTxs.length === 3) {66          await Promise.all(unstakeTxs);67          unstakeTxs = [];68        }69        unstakeTxs.push(helper.staking.unstakeAll(account));70      }71      await Promise.all(unstakeTxs);72      usedAccounts = [];73      expect(await helper.staking.getTotalStaked()).to.eq(0n); // there are no active stakes after each test74      // Make sure previousCalculatedRecord is None to avoid problem with payout stakers;75      await helper.admin.payoutStakers(palletAdmin, 100);76      expect((await helper.getApi().query.appPromotion.previousCalculatedRecord() as any).isNone).to.be.true;77    });78  });7980  describe('stake extrinsic', () => {81    itSub('should "freeze" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {82      const [staker, recepient] = await getAccounts(2);83      const totalStakedBefore = await helper.staking.getTotalStaked();8485      // Minimum stake amount is 100:86      await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.rejected;87      await helper.staking.stake(staker, 100n * nominal);8889      // Staker balance is: frozen: 100, reserved: 0n...90      // ...so he can not transfer 90091      expect(await helper.balance.getSubstrateFull(staker.address)).to.contain({frozen: 100n * nominal, reserved: 0n});92      expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: 100n * nominal}]);93      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejectedWith(/^Token: Frozen$/);9495      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);96      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);97      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo?98      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased99100101      await helper.staking.stake(staker, 200n * nominal);102      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);103      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});104      expect(totalStakedPerBlock[0].amount).to.equal(100n * nominal);105      expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);106    });107108    [109      {unstake: 'unstakeAll' as const},110      {unstake: 'unstakePartial' as const},111    ].map(testCase => {112      itSub(`[${testCase.unstake}] should allow to create maximum 10 stakes for account`, async ({helper}) => {113        const [staker] = await getAccounts(1, 2000n);114        const ONE_STAKE = 100n * nominal;115        for (let i = 0; i < 10; i++) {116          await helper.staking.stake(staker, ONE_STAKE);117        }118119        // can have 10 stakes120        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);121        expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);122123        await expect(helper.staking.stake(staker, ONE_STAKE)).to.be.rejectedWith('appPromotion.NoPermission');124125        // After unstake can stake again126127        // CASE 1: unstakeAll128        if (testCase.unstake === 'unstakeAll') {129          await helper.staking.unstakeAll(staker);130          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);131          await helper.staking.stake(staker, 100n * nominal);132          expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);133        }134        // CASE 2: unstakePartial135        else {136          await helper.staking.unstakePartial(staker, ONE_STAKE);137          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);138          await helper.staking.stake(staker, 100n * nominal);139          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(10);140          await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.NoPermission');141          await helper.staking.unstakePartial(staker, 150n * nominal);142          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);143          expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(850n * nominal);144        }145      });146    });147    // Now AppPromo makes freezes. Probably this test should be changed\removed.148    itSub.skip('should allow to stake() if balance is locked with different id', async ({helper}) => {149      const [staker] = await getAccounts(1);150151      // staker has tokens locked with vesting id:152      await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal});153      expect(await helper.balance.getSubstrateFull(staker.address))154        .to.deep.contain({free: 1200n * nominal, frozen: 200n * nominal, reserved: 0n});155156      // Locked balance can be staked. staker can stake 1200 tokens (minus fee):157      await helper.staking.stake(staker, 1000n * nominal);158      await helper.staking.stake(staker, 199n * nominal);159      // check balances160      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}, {id: 'appstake', amount: 1199n * nominal, reasons: 'All'}]);161      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 1199n * nominal});162      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);163      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);164165      // staker can unstake166      await helper.staking.unstakeAll(staker);167      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1199n * nominal);168      const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});169      await helper.wait.forParachainBlockNumber(pendingUnstake.block);170171      // check balances172      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);173      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 200n * nominal});174      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);175      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);176177      // staker can transfer balances now178      await helper.balance.transferToSubstrate(staker, donor.address, 900n * nominal);179    });180181    itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => {182      const [staker] = await getAccounts(1);183184      // Can't stake full balance because Alice needs to pay some fee185      await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; // With('Arithmetic')186      await helper.staking.stake(staker, 500n * nominal);187188      // Can't stake 500 tkn because Alice has Less than 500 transferable;189      await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejected; // With('Arithmetic');190      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);191    });192193    itSub('for different accounts in one block is possible', async ({helper}) => {194      const crowd = await getAccounts(4);195196      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));197      await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled;198199      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));200      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);201    });202  });203204  describe('Unstaking', () => {205    [206      {method: 'unstakeAll' as const},207      {method: 'unstakePartial' as const},208    ].map(testCase => {209      itSub(`[${testCase.method}] should move tokens to "pendingUnstake" and subtract it from totalStaked`, async ({helper}) => {210        const [staker, recepient] = await getAccounts(2);211        const totalStakedBefore = await helper.staking.getTotalStaked();212        const STAKE_AMOUNT = 900n * nominal;213214        await helper.staking.stake(staker, STAKE_AMOUNT);215        testCase.method === 'unstakeAll'216          ? await helper.staking.unstakeAll(staker)217          : await helper.staking.unstakePartial(staker, STAKE_AMOUNT);218219        // Right after unstake tokens are still locked220        expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);221        expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: STAKE_AMOUNT}]);222        expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: STAKE_AMOUNT});223        // Staker can not transfer224        await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith(/^Token: Frozen$/);225        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(STAKE_AMOUNT);226        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);227        expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);228      });229    });230231    [232      {method: 'unstakeAll' as const},233      {method: 'unstakePartial' as const},234    ].map(testCase => {235      itSub(`[${testCase.method}] should unlock balance after unlocking period ends and remove it from "pendingUnstake"`, async ({helper}) => {236        const [staker] = await getAccounts(1);237        await helper.staking.stake(staker, 100n * nominal);238        testCase.method === 'unstakeAll'239          ? await helper.staking.unstakeAll(staker)240          : await helper.staking.unstakePartial(staker, 100n * nominal);241        const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});242243        // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n244        await helper.wait.forParachainBlockNumber(pendingUnstake.block);245        expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});246        expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);247248        // staker can transfer:249        await helper.balance.transferToSubstrate(staker, donor.address, 998n * nominal);250        expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);251      });252    });253254    [255      {method: 'unstakeAll' as const},256      {method: 'unstakePartial' as const},257    ].map(testCase => {258      itSub(`[${testCase.method}] should successfully unstake multiple stakes`, async ({helper}) => {259        const [staker] = await getAccounts(1);260        await helper.staking.stake(staker, 100n * nominal);261        await helper.staking.stake(staker, 200n * nominal);262        await helper.staking.stake(staker, 300n * nominal);263264        // staked: [100, 200, 300]; unstaked: 0265        let totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});266        let pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});267        let stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});268        expect(totalPendingUnstake).to.be.deep.equal(0n);269        expect(pendingUnstake).to.be.deep.equal([]);270        expect(stakes[0].amount).to.equal(100n * nominal);271        expect(stakes[1].amount).to.equal(200n * nominal);272        expect(stakes[2].amount).to.equal(300n * nominal);273274        // Can unstake multiple stakes275        testCase.method === 'unstakeAll'276          ? await helper.staking.unstakeAll(staker)277          : await helper.staking.unstakePartial(staker, 600n * nominal);278279        pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});280        totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});281        stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});282        expect(totalPendingUnstake).to.be.equal(600n * nominal);283        expect(stakes).to.be.deep.equal([]);284        expect(pendingUnstake[0].amount).to.equal(600n * nominal);285286        expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 600n * nominal});287        expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);288        await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);289        expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});290        expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);291      });292    });293294    [295      {method: 'unstakeAll' as const},296      {method: 'unstakePartial' as const},297    ].map(testCase => {298      itSub(`[${testCase.method}] should not have any effects if no active stakes`, async ({helper}) => {299        const [staker] = await getAccounts(1);300301        // unstake has no effect if no stakes at all302        testCase.method === 'unstakeAll'303          ? await helper.staking.unstakeAll(staker)304          : await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');305306        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);307        expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper308309        // TODO stake() unstake() waitUnstaked() unstake();310311        // can't unstake if there are only pendingUnstakes312        await helper.staking.stake(staker, 100n * nominal);313314        if (testCase.method === 'unstakeAll') {315          await helper.staking.unstakeAll(staker);316          await helper.staking.unstakeAll(staker);317        } else {318          await helper.staking.unstakePartial(staker, 100n * nominal);319          await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');320        }321322        expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);323        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);324        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);325      });326    });327328    [329      {method: 'unstakeAll' as const},330      {method: 'unstakePartial' as const},331    ].map(testCase => {332      itSub(`[${testCase.method}] should create different pending-unlock for each unlocking stake`, async ({helper}) => {333        const [staker] = await getAccounts(1);334        await helper.staking.stake(staker, 100n * nominal);335        testCase.method === 'unstakeAll'336          ? await helper.staking.unstakeAll(staker)337          : await helper.staking.unstakePartial(staker, 100n * nominal);338        await helper.staking.stake(staker, 120n * nominal);339        testCase.method === 'unstakeAll'340          ? await helper.staking.unstakeAll(staker)341          : await helper.staking.unstakePartial(staker, 120n * nominal);342343        const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});344        expect(unstakingPerBlock).has.length(2);345        expect(unstakingPerBlock[0].amount).to.equal(100n * nominal);346        expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);347        expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.equal(0);348      });349    });350351    [352      {method: 'unstakeAll' as const},353      {method: 'unstakePartial' as const},354    ].map(testCase => {355      itSub(`[${testCase.method}] should be possible for 3 accounts in one block`, async ({helper}) => {356        const stakers = await getAccounts(3);357358        await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));359        await Promise.all(stakers.map(staker => {360          return testCase.method === 'unstakeAll'361            ? helper.staking.unstakeAll(staker)362            : helper.staking.unstakePartial(staker, 100n * nominal);363        }));364365        await Promise.all(stakers.map(async (staker) => {366          expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);367          expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);368        }));369      });370    });371372    itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => {373      if (!await helper.arrange.isDevNode()) {374        const stakers = await getAccounts(10);375376        await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));377        const unstakingResults = await Promise.allSettled(stakers.map((staker, i) => {378          return i % 2 === 0379            ? helper.staking.unstakeAll(staker)380            : helper.staking.unstakePartial(staker, 100n * nominal);381        }));382383        const successfulUnstakes = unstakingResults.filter(result => result.status === 'fulfilled');384        expect(successfulUnstakes).to.have.length(3);385      }386    });387388    itSub('Cannot partially unstake more than staked', async ({helper}) => {389      const [staker] = await getAccounts(1);390      // Staker stakes 300:391      await helper.staking.stake(staker, 100n * nominal);392      await helper.staking.stake(staker, 200n * nominal);393394      // cannot usntake 300.00000...1395      await expect(helper.staking.unstakePartial(staker, 300n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');396      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(2);397398      await helper.staking.unstakePartial(staker, 150n * nominal);399      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);400      await expect(helper.staking.unstakePartial(staker, 150n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');401      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);402403      // nothing broken, can unstake full amount:404      await helper.staking.unstakePartial(staker, 150n * nominal);405      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(0);406    });407408    itSub('Can partially unstake arbitrary amount', async ({helper}) => {409      const [staker] = await getAccounts(1);410      await helper.staking.stake(staker, 100n * nominal);411      await helper.staking.stake(staker, 200n * nominal);412413      // 0. Staker cannot unstake negative amount414      await expect(helper.staking.unstakePartial(staker, -1n)).to.be.rejected;415416      // 1. Staker can unstake 0 wei417      await helper.staking.unstakePartial(staker, 0n);418      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);419      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal);420      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);421422      // 2. Staker can unstake 1 wei423      await helper.staking.unstakePartial(staker, 1n);424      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);425      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal - 1n);426      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1n);427      // 2.1 The oldest stake decreased:428      let [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});429      expect(stake1.amount).to.eq(100n * nominal - 1n);430      expect(stake2.amount).to.eq(200n * nominal);431432      // 3. Staker can unstake all but 1 wei433      await helper.staking.unstakePartial(staker, 100n * nominal - 2n);434      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);435      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(200n * nominal + 1n);436      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(100n * nominal - 1n);437      [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});438      expect(stake1.amount).to.eq(1n);439      expect(stake2.amount).to.eq(200n * nominal);440    });441442    itSub('can mix different type of unstakes', async ({helper}) => {443      const [staker] = await getAccounts(1);444      await helper.staking.stake(staker, 100n * nominal);445      await helper.staking.stake(staker, 200n * nominal);446447      await helper.staking.unstakePartial(staker, 50n * nominal);448      await helper.staking.unstakeAll(staker);449      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);450      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);451      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(300n * nominal);452453      const [_unstake1, unstake2] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});454      await helper.wait.forParachainBlockNumber(unstake2.block);455456      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([]);457      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});458      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(999n);459      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);460      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);461      expect(await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).to.deep.eq([]);462    });463  });464465  describe('collection sponsoring', () => {466    itSub('should actually sponsor transactions', async ({helper}) => {467      const api = helper.getApi();468      const [collectionOwner, tokenSender, receiver] = await getAccounts(3);469      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});470      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});471      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));472      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);473474      await token.transfer(tokenSender, {Substrate: receiver.address});475      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});476      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);477478      // senders balance the same, transaction has sponsored479      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);480      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;481    });482483    itSub('can not be set by non admin', async ({helper}) => {484      const api = helper.getApi();485      const [collectionOwner, nonAdmin] = await getAccounts(2);486487      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});488489      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;490      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');491    });492493    itSub('should set pallet address as confirmed admin', async ({helper}) => {494      const api = helper.getApi();495      const [collectionOwner, oldSponsor] = await getAccounts(2);496497      // Can set sponsoring for collection without sponsor498      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});499      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.fulfilled;500      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});501502      // Can set sponsoring for collection with unconfirmed sponsor503      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});504      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});505      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.fulfilled;506      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});507508      // Can set sponsoring for collection with confirmed sponsor509      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});510      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);511      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.fulfilled;512      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});513    });514515    itSub('can be overwritten by collection owner', async ({helper}) => {516      const api = helper.getApi();517      const [collectionOwner, newSponsor] = await getAccounts(2);518      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});519      const collectionId = collection.collectionId;520521      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionId))).to.be.fulfilled;522523      // Collection limits still can be changed by the owner524      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;525      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);526      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});527528      // Collection sponsor can be changed too529      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;530      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});531    });532533    itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => {534      const [owner] = await getAccounts(1);535      const api = helper.getApi();536      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};537      const collectionWithLimits = await helper.nft.mintCollection(owner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});538539      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled;540      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);541    });542543    itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => {544      const api = helper.getApi();545      const [collectionOwner] = await getAccounts(1);546547      // collection has never existed548      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected;549      // collection has been burned550      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});551      await collection.burn(collectionOwner);552553      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;554    });555  });556557  describe('stopSponsoringCollection', () => {558    itSub('can not be called by non-admin', async ({helper}) => {559      const api = helper.getApi();560      const [collectionOwner, nonAdmin] = await getAccounts(2);561      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});562563      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled;564565      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;566      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});567    });568569    itSub('should set sponsoring as disabled', async ({helper}) => {570      const api = helper.getApi();571      const [collectionOwner, recepient] = await getAccounts(2);572      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});573      const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});574575      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));576      await helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId));577578      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');579580      // Transactions are not sponsored anymore:581      const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);582      await token.transfer(collectionOwner, {Substrate: recepient.address});583      const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);584      expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);585    });586587    itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => {588      const api = helper.getApi();589      const [collectionOwner] = await getAccounts(1);590      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});591      await collection.confirmSponsorship(collectionOwner);592593      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;594595      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});596    });597598    itSub('should reject transaction if collection does not exist', async ({helper}) => {599      const [collectionOwner] = await getAccounts(1);600      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});601602      await collection.burn(collectionOwner);603      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [collection.collectionId], true)).to.be.rejectedWith('common.CollectionNotFound');604      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [999_999_999], true)).to.be.rejectedWith('common.CollectionNotFound');605    });606  });607608  describe('contract sponsoring', () => {609    itEth('should set palletes address as a sponsor', async ({helper}) => {610      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();611      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);612      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);613614      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);615616      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;617      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);618      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({619        confirmed: {620          substrate: palletAddress,621        },622      });623    });624625    itEth('should overwrite sponsoring mode and existed sponsor', async ({helper}) => {626      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();627      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);628      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);629630      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;631632      // Contract is self sponsored633      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.be.deep.equal({634        confirmed: {635          ethereum: flipper.options.address.toLowerCase(),636        },637      });638639      // set promotion sponsoring640      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);641642      // new sponsor is pallet address643      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;644      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);645      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({646        confirmed: {647          substrate: palletAddress,648        },649      });650    });651652    itEth('can be overwritten by contract owner', async ({helper}) => {653      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();654      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);655      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);656657      // contract sponsored by pallet658      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);659660      // owner sets self sponsoring661      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;662663      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;664      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);665      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({666        confirmed: {667          ethereum: flipper.options.address.toLowerCase(),668        },669      });670    });671672    itEth('can not be set by non admin', async ({helper}) => {673      const [nonAdmin] = await getAccounts(1);674      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();675      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);676      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);677678      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;679680      // nonAdmin calls sponsorContract681      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');682683      // contract still self-sponsored684      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({685        confirmed: {686          ethereum: flipper.options.address.toLowerCase(),687        },688      });689    });690691    itEth('should actually sponsor transactions', async ({helper}) => {692      // Contract caller693      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);694      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);695696      // Deploy flipper697      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();698      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);699      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);700701      // Owner sets to sponsor every tx702      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});703      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});704      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address, 1000n); // transferBalanceToEth(api, alice, flipper.options.address, 1000n);705706      // Set promotion to the Flipper707      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);708709      // Caller calls Flipper710      await flipper.methods.flip().send({from: caller});711      expect(await flipper.methods.getValue().call()).to.be.true;712713      // The contracts and caller balances have not changed714      const callerBalance = await helper.balance.getEthereum(caller);715      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);716      expect(callerBalance).to.be.equal(1000n * nominal);717      expect(1000n * nominal === contractBalanceAfter).to.be.true;718719      // The pallet balance has decreased720      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);721      expect(palletBalanceAfter < palletBalanceBefore).to.be.true;722    });723  });724725  describe('stopSponsoringContract', () => {726    itEth('should remove pallet address from contract sponsors', async ({helper}) => {727      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);728      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();729      const flipper = await helper.eth.deployFlipper(contractOwner);730      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address);731      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);732733      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});734      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);735      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true);736737      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;738      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);739      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({740        disabled: null,741      });742743      await flipper.methods.flip().send({from: caller});744      expect(await flipper.methods.getValue().call()).to.be.true;745746      const callerBalance = await helper.balance.getEthereum(caller);747      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);748749      // caller payed for call750      expect(1000n * nominal > callerBalance).to.be.true;751      expect(contractBalanceAfter).to.be.equal(100n * nominal);752    });753754    itEth('can not be called by non-admin', async ({helper}) => {755      const [nonAdmin] = await getAccounts(1);756      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();757      const flipper = await helper.eth.deployFlipper(contractOwner);758759      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);760      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address]))761        .to.be.rejectedWith(/appPromotion\.NoPermission/);762    });763764    itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => {765      const [nonAdmin] = await getAccounts(1);766      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();767      const flipper = await helper.eth.deployFlipper(contractOwner);768      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);769      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;770771      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');772    });773  });774775  describe('payoutStakers', () => {776    itSub('can not be called by non admin', async ({helper}) => {777      const [nonAdmin] = await getAccounts(1);778      await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission');779    });780781    itSub('should increase total staked', async ({helper}) => {782      const [staker] = await getAccounts(1);783      const totalStakedBefore = await helper.staking.getTotalStaked();784      await helper.staking.stake(staker, 100n * nominal);785786      // Wait for rewards and pay787      const [stakedInBlock] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});788      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stakedInBlock.block));789790      const payout = await helper.admin.payoutStakers(palletAdmin, 100);791      const totalPayout = payout.reduce((prev, payout) => prev + payout.payout, 0n);792      const stakerReward = payout.find(p => p.staker === staker.address);793794      expect(stakerReward?.payout).to.eq(calculateIncome(100n * nominal) - (100n * nominal));795796      const totalStakedAfter = await helper.staking.getTotalStaked();797      expect(totalStakedAfter).to.equal(totalStakedBefore + (100n * nominal) + totalPayout);798      // staker can unstake799      await helper.staking.unstakeAll(staker);800      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedAfter - calculateIncome(100n * nominal));801    });802803    itSub('should credit 0.05% for staking period', async ({helper}) => {804      const [staker] = await getAccounts(1);805806      await waitPromotionPeriodDoesntEnd(helper);807808      await helper.staking.stake(staker, 100n * nominal);809      await helper.staking.stake(staker, 200n * nominal);810811      // wait rewards are available:812      const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});813      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));814815      const payoutToStaker = (await helper.admin.payoutStakers(palletAdmin, 100)).find((payout) => payout.staker === staker.address)!.payout;816      expect(payoutToStaker + 300n * nominal).to.equal(calculateIncome(300n * nominal));817818      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});819      const income1 = calculateIncome(100n * nominal);820      const income2 = calculateIncome(200n * nominal);821      expect(totalStakedPerBlock[0].amount).to.equal(income1);822      expect(totalStakedPerBlock[1].amount).to.equal(income2);823824      const stakerBalance = await helper.balance.getSubstrateFull(staker.address);825      expect(stakerBalance).to.contain({frozen: income1 + income2, reserved: 0n});826      expect(stakerBalance.free / nominal).to.eq(999n);827    });828829    itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => {830      const [staker] = await getAccounts(1);831832      await helper.staking.stake(staker, 100n * nominal);833      // wait for two rewards are available:834      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});835      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);836837      await helper.admin.payoutStakers(palletAdmin, 100);838      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});839      const frozenBalanceShouldBe = calculateIncome(100n * nominal, 2);840      expect(stake.amount).to.be.equal(frozenBalanceShouldBe);841842      const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);843844      expect(stakerFullBalance).to.contain({reserved: 0n, frozen: frozenBalanceShouldBe});845    });846847    itSub('should not be credited for pending-unstaked tokens', async ({helper}) => {848      // staker unstakes before rewards been payed849      const [staker] = await getAccounts(1);850      await helper.staking.stake(staker, 100n * nominal);851      const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});852      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);853      await helper.staking.unstakeAll(staker);854855      // so he did not receive any rewards856      const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);857      await helper.admin.payoutStakers(palletAdmin, 100);858      const totalBalanceAfter = await helper.balance.getSubstrate(staker.address);859860      expect(totalBalanceBefore).to.be.equal(totalBalanceAfter);861    });862863    itSub('should bring compound interest', async ({helper}) => {864      const [staker] = await getAccounts(1);865866      await helper.staking.stake(staker, 100n * nominal);867868      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});869      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));870871      await helper.admin.payoutStakers(palletAdmin, 100);872      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});873      expect(stake.amount).to.equal(calculateIncome(100n * nominal));874875      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);876      await helper.admin.payoutStakers(palletAdmin, 100);877      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});878      expect(stake.amount).to.equal(calculateIncome(100n * nominal, 2));879    });880881    itSub('can calculate reward for tiny stake', async ({helper}) => {882      const [staker] = await getAccounts(1);883      await helper.staking.stake(staker, 100n * nominal);884      await helper.staking.stake(staker, 100n * nominal);885      await helper.staking.unstakePartial(staker, 100n * nominal - 1n);886887      const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});888      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));889890      const stakerPayout = await payUntilRewardFor(staker.address, helper);891      expect(stakerPayout.stake).to.eq(100n * nominal + 1n);892    });893894    itSub('can eventually pay all rewards', async ({helper}) => {895      const stakers = await getAccounts(30);896      // Create 30 stakes:897      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));898899      let unstakingTxs = [];900      for (const staker of stakers) {901        if (unstakingTxs.length == 3) {902          await Promise.all(unstakingTxs);903          unstakingTxs = [];904        }905        unstakingTxs.push(helper.staking.unstakePartial(staker, 100n * nominal - 1n));906      }907908      const [staker] = await getAccounts(1);909      await helper.staking.stake(staker, 100n * nominal);910      const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});911      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));912913      let payouts;914      do {915        payouts = await helper.admin.payoutStakers(palletAdmin, 20);916      } while (payouts.length !== 0);917    });918  });919920  describe('events', () => {921    [922      {method: 'unstakePartial' as const},923      {method: 'unstakeAll' as const},924    ].map(testCase => {925      itSub(testCase.method, async ({helper}) => {926        const unstakeParams: [] | [bigint] = testCase.method === 'unstakePartial'927          ? [100n * nominal - 1n]928          : [];929        const [staker] = await getAccounts(1);930        await helper.staking.stake(staker, 100n * nominal);931        await helper.staking.stake(staker, 200n * nominal);932        const {result} = await helper.executeExtrinsic(staker, `api.tx.appPromotion.${testCase.method}`, unstakeParams);933934        const event = result.events.find(e => e.event.section === 'appPromotion' && e.event.method === 'Unstake');935        const unstakerEvents = event?.event.data[0].toString();936        const unstakedEvents = BigInt(event?.event.data[1].toString());937        expect(unstakerEvents).to.eq(staker.address);938        expect(unstakedEvents).to.eq(testCase.method === 'unstakeAll' ? 300n * nominal : 100n * nominal - 1n);939      });940    });941942    itSub('stake', async ({helper}) => {943      const [staker] = await getAccounts(1);944      const {result} = await helper.executeExtrinsic(staker, 'api.tx.appPromotion.stake', [100n * nominal]);945946      const event = result.events.find(e => e.event.section === 'appPromotion' && e.event.method === 'Stake');947      const stakerEvents = event?.event.data[0].toString();948      const stakedEvents = BigInt(event?.event.data[1].toString());949      expect(stakerEvents).to.eq(staker.address);950      expect(stakedEvents).to.eq(100n * nominal);951    });952953    // Flaky954    itSub.skip('payoutStakers', async ({helper}) => {955      const [staker1, staker2] = await getAccounts(2);956      const STAKE1 = 100n * nominal;957      const STAKE2 = 200n * nominal;958      await helper.staking.stake(staker1, STAKE1);959      await helper.staking.stake(staker2, STAKE2);960961      const [stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker2.address});962      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));963964      const results = await helper.admin.payoutStakers(palletAdmin, 100);965      const stakersEvents = results.filter(ev => ev.staker === staker1.address || ev.staker === staker2.address);966      expect(stakersEvents).has.length(2);967      expect(stakersEvents).has.not.ordered.members([968        {staker: staker1.address, stake: STAKE1, payout: calculateIncome(STAKE1) - STAKE1},969        {staker: staker2.address, stake: STAKE2, payout: calculateIncome(STAKE2) - STAKE2},970      ]);971    });972  });973});974975976// Sometimes is is required to make a cycle in order for the payment to be calculated for a specific account977async function payUntilRewardFor(account: string, helper: DevUniqueHelper) {978  for (let i = 0; i < 3; i++) {979    const payouts = await helper.admin.payoutStakers(palletAdmin, 100);980    const accountPayout = payouts.find(p => p.staker === account);981    if (accountPayout) return accountPayout;982  }983  throw Error(`Cannot find payout for ${account}`);984}985986function calculateIncome(base: bigint, iter = 0, calcPeriod: bigint = UNLOCKING_PERIOD): bigint {987  const DAY = 7200n;988  const ACCURACY = 1_000_000_000n;989  // 5n / 10_000n = 0.05% p/day990  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;991992  if (iter > 1) {993    return calculateIncome(income, iter - 1, calcPeriod);994  } else return income;995}996997function rewardAvailableInBlock(stakedInBlock: bigint) {998  if (stakedInBlock % LOCKING_PERIOD === 0n) return stakedInBlock + LOCKING_PERIOD;999  return (stakedInBlock - stakedInBlock % LOCKING_PERIOD) + (LOCKING_PERIOD * 2n);1000}10011002// Wait while promotion period less than specified block, to avoid boundary cases1003// 0 if this should be the beginning of the period.1004async function waitPromotionPeriodDoesntEnd(helper: DevUniqueHelper, waitBlockLessThan = LOCKING_PERIOD / 3n) {1005  const relayBlockNumber = (await helper.callRpc('api.query.parachainSystem.validationData', [])).value.relayParentNumber.toNumber(); // await helper.chain.getLatestBlockNumber();1006  const currentPeriodBlock = BigInt(relayBlockNumber) % LOCKING_PERIOD;10071008  if (currentPeriodBlock > waitBlockLessThan) {1009    await helper.wait.forRelayBlockNumber(BigInt(relayBlockNumber) + LOCKING_PERIOD - currentPeriodBlock);1010  }1011}
after · tests/src/sub/appPromotion/appPromotion.test.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19  itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip, LOCKING_PERIOD, UNLOCKING_PERIOD,20} from '../../util';21import {DevUniqueHelper} from '../../util/playgrounds/unique.dev';22import {itEth, expect, SponsoringMode} from '../../eth/util';2324let donor: IKeyringPair;25let palletAdmin: IKeyringPair;26let nominal: bigint;27let palletAddress: string;28let accounts: IKeyringPair[];29let usedAccounts: IKeyringPair[] = [];3031async function getAccounts(accountsNumber: number, balance?: bigint) {32  let accs: IKeyringPair[] = [];33  if (balance) {34    await usingPlaygrounds(async (helper) => {35      accs = await helper.arrange.createAccounts(new Array(accountsNumber).fill(balance), donor);36    });37  } else {38    accs = accounts.splice(0, accountsNumber);39  }40  usedAccounts.push(...accs);41  return accs;42}43// App promotion periods:44// LOCKING_PERIOD = 12 blocks of relay45// UNLOCKING_PERIOD = 6 blocks of parachain4647describe('App promotion', () => {48  before(async function () {49    await usingPlaygrounds(async (helper, privateKey) => {50      requirePalletsOrSkip(this, helper, [Pallets.AppPromotion]);51      donor = await privateKey({url: import.meta.url});52      palletAddress = helper.arrange.calculatePalletAddress('appstake');53      palletAdmin = await privateKey('//PromotionAdmin');54      nominal = helper.balance.getOneTokenNominal();5556      const accountBalances = new Array(200).fill(1000n);57      accounts = await helper.arrange.createAccounts(accountBalances, donor); // create accounts-pool to speed up tests58    });59  });6061  afterEach(async () => {62    await usingPlaygrounds(async (helper) => {63      let unstakeTxs = [];64      for (const account of usedAccounts) {65        if (unstakeTxs.length === 3) {66          await Promise.all(unstakeTxs);67          unstakeTxs = [];68        }69        unstakeTxs.push(helper.staking.unstakeAll(account));70      }71      await Promise.all(unstakeTxs);72      usedAccounts = [];73      expect(await helper.staking.getTotalStaked()).to.eq(0n); // there are no active stakes after each test74      // Make sure previousCalculatedRecord is None to avoid problem with payout stakers;75      await helper.admin.payoutStakers(palletAdmin, 100);76      expect((await helper.getApi().query.appPromotion.previousCalculatedRecord() as any).isNone).to.be.true;77    });78  });7980  describe('stake extrinsic', () => {81    itSub('should "freeze" staking balance, add it to "staked" map, and increase "totalStaked" amount', async ({helper}) => {82      const [staker, recepient] = await getAccounts(2);83      const totalStakedBefore = await helper.staking.getTotalStaked();8485      // Minimum stake amount is 100:86      await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.rejected;87      await helper.staking.stake(staker, 100n * nominal);8889      // Staker balance is: frozen: 100, reserved: 0n...90      // ...so he can not transfer 90091      expect(await helper.balance.getSubstrateFull(staker.address)).to.contain({frozen: 100n * nominal, reserved: 0n});92      expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: 100n * nominal}]);93      await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejectedWith(/^Token: Frozen$/);9495      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);96      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);97      // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo?98      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased99100101      await helper.staking.stake(staker, 200n * nominal);102      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);103      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});104      expect(totalStakedPerBlock[0].amount).to.equal(100n * nominal);105      expect(totalStakedPerBlock[1].amount).to.equal(200n * nominal);106    });107108    [109      {unstake: 'unstakeAll' as const},110      {unstake: 'unstakePartial' as const},111    ].map(testCase => {112      itSub(`[${testCase.unstake}] should allow to create maximum 10 stakes for account`, async ({helper}) => {113        const [staker] = await getAccounts(1, 2000n);114        const ONE_STAKE = 100n * nominal;115        for (let i = 0; i < 10; i++) {116          await helper.staking.stake(staker, ONE_STAKE);117        }118119        // can have 10 stakes120        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);121        expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);122123        await expect(helper.staking.stake(staker, ONE_STAKE)).to.be.rejectedWith('appPromotion.NoPermission');124125        // After unstake can stake again126127        // CASE 1: unstakeAll128        if (testCase.unstake === 'unstakeAll') {129          await helper.staking.unstakeAll(staker);130          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);131          await helper.staking.stake(staker, 100n * nominal);132          expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);133        }134        // CASE 2: unstakePartial135        else {136          await helper.staking.unstakePartial(staker, ONE_STAKE);137          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);138          await helper.staking.stake(staker, 100n * nominal);139          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(10);140          await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.NoPermission');141          await helper.staking.unstakePartial(staker, 150n * nominal);142          expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(9);143          expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(850n * nominal);144        }145      });146    });147    // Now AppPromo makes freezes. Probably this test should be changed\removed.148    itSub.skip('should allow to stake() if balance is locked with different id', async ({helper}) => {149      const [staker] = await getAccounts(1);150151      // staker has tokens locked with vesting id:152      await helper.balance.vestedTransfer(donor, staker.address, {start: 0n, period: 1n, periodCount: 1n, perPeriod: 200n * nominal});153      expect(await helper.balance.getSubstrateFull(staker.address))154        .to.deep.contain({free: 1200n * nominal, frozen: 200n * nominal, reserved: 0n});155156      // Locked balance can be staked. staker can stake 1200 tokens (minus fee):157      await helper.staking.stake(staker, 1000n * nominal);158      await helper.staking.stake(staker, 199n * nominal);159      // check balances160      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);161      expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: 1199n * nominal}]);162      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 1199n * nominal});163      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);164      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal);165166      // staker can unstake167      await helper.staking.unstakeAll(staker);168      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1199n * nominal);169      const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});170      await helper.wait.forParachainBlockNumber(pendingUnstake.block);171172      // check balances173      expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]);174      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 200n * nominal});175      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n);176      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);177178      // staker can transfer balances now179      await helper.balance.transferToSubstrate(staker, donor.address, 900n * nominal);180    });181182    itSub('should not allow to stake(), if stake amount is more than total free balance minus locked by staking', async ({helper}) => {183      const [staker] = await getAccounts(1);184185      // Can't stake full balance because Alice needs to pay some fee186      await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.rejected; // With('Arithmetic')187      await helper.staking.stake(staker, 500n * nominal);188189      // Can't stake 500 tkn because Alice has Less than 500 transferable;190      await expect(helper.staking.stake(staker, 500n * nominal)).to.be.rejected; // With('Arithmetic');191      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);192    });193194    itSub('for different accounts in one block is possible', async ({helper}) => {195      const crowd = await getAccounts(4);196197      const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));198      await expect(Promise.all(crowdStartsToStake)).to.be.fulfilled;199200      const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));201      expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);202    });203  });204205  describe('Unstaking', () => {206    [207      {method: 'unstakeAll' as const},208      {method: 'unstakePartial' as const},209    ].map(testCase => {210      itSub(`[${testCase.method}] should move tokens to "pendingUnstake" and subtract it from totalStaked`, async ({helper}) => {211        const [staker, recepient] = await getAccounts(2);212        const totalStakedBefore = await helper.staking.getTotalStaked();213        const STAKE_AMOUNT = 900n * nominal;214215        await helper.staking.stake(staker, STAKE_AMOUNT);216        testCase.method === 'unstakeAll'217          ? await helper.staking.unstakeAll(staker)218          : await helper.staking.unstakePartial(staker, STAKE_AMOUNT);219220        // Right after unstake tokens are still locked221        expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);222        expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: STAKE_AMOUNT}]);223        expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: STAKE_AMOUNT});224        // Staker can not transfer225        await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejectedWith(/^Token: Frozen$/);226        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(STAKE_AMOUNT);227        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);228        expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);229      });230    });231232    [233      {method: 'unstakeAll' as const},234      {method: 'unstakePartial' as const},235    ].map(testCase => {236      itSub(`[${testCase.method}] should unlock balance after unlocking period ends and remove it from "pendingUnstake"`, async ({helper}) => {237        const [staker] = await getAccounts(1);238        await helper.staking.stake(staker, 100n * nominal);239        testCase.method === 'unstakeAll'240          ? await helper.staking.unstakeAll(staker)241          : await helper.staking.unstakePartial(staker, 100n * nominal);242        const [pendingUnstake] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});243244        // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n245        await helper.wait.forParachainBlockNumber(pendingUnstake.block);246        expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});247        expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);248249        // staker can transfer:250        await helper.balance.transferToSubstrate(staker, donor.address, 998n * nominal);251        expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);252      });253    });254255    [256      {method: 'unstakeAll' as const},257      {method: 'unstakePartial' as const},258    ].map(testCase => {259      itSub(`[${testCase.method}] should successfully unstake multiple stakes`, async ({helper}) => {260        const [staker] = await getAccounts(1);261        await helper.staking.stake(staker, 100n * nominal);262        await helper.staking.stake(staker, 200n * nominal);263        await helper.staking.stake(staker, 300n * nominal);264265        // staked: [100, 200, 300]; unstaked: 0266        let totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});267        let pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});268        let stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});269        expect(totalPendingUnstake).to.be.deep.equal(0n);270        expect(pendingUnstake).to.be.deep.equal([]);271        expect(stakes[0].amount).to.equal(100n * nominal);272        expect(stakes[1].amount).to.equal(200n * nominal);273        expect(stakes[2].amount).to.equal(300n * nominal);274275        // Can unstake multiple stakes276        testCase.method === 'unstakeAll'277          ? await helper.staking.unstakeAll(staker)278          : await helper.staking.unstakePartial(staker, 600n * nominal);279280        pendingUnstake = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});281        totalPendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});282        stakes = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});283        expect(totalPendingUnstake).to.be.equal(600n * nominal);284        expect(stakes).to.be.deep.equal([]);285        expect(pendingUnstake[0].amount).to.equal(600n * nominal);286287        expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 600n * nominal});288        expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);289        await helper.wait.forParachainBlockNumber(pendingUnstake[0].block);290        expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});291        expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);292      });293    });294295    [296      {method: 'unstakeAll' as const},297      {method: 'unstakePartial' as const},298    ].map(testCase => {299      itSub(`[${testCase.method}] should not have any effects if no active stakes`, async ({helper}) => {300        const [staker] = await getAccounts(1);301302        // unstake has no effect if no stakes at all303        testCase.method === 'unstakeAll'304          ? await helper.staking.unstakeAll(staker)305          : await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');306307        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);308        expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper309310        // TODO stake() unstake() waitUnstaked() unstake();311312        // can't unstake if there are only pendingUnstakes313        await helper.staking.stake(staker, 100n * nominal);314315        if (testCase.method === 'unstakeAll') {316          await helper.staking.unstakeAll(staker);317          await helper.staking.unstakeAll(staker);318        } else {319          await helper.staking.unstakePartial(staker, 100n * nominal);320          await expect(helper.staking.unstakePartial(staker, 100n * nominal)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');321        }322323        expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);324        expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);325        expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);326      });327    });328329    [330      {method: 'unstakeAll' as const},331      {method: 'unstakePartial' as const},332    ].map(testCase => {333      itSub(`[${testCase.method}] should create different pending-unlock for each unlocking stake`, async ({helper}) => {334        const [staker] = await getAccounts(1);335        await helper.staking.stake(staker, 100n * nominal);336        testCase.method === 'unstakeAll'337          ? await helper.staking.unstakeAll(staker)338          : await helper.staking.unstakePartial(staker, 100n * nominal);339        await helper.staking.stake(staker, 120n * nominal);340        testCase.method === 'unstakeAll'341          ? await helper.staking.unstakeAll(staker)342          : await helper.staking.unstakePartial(staker, 120n * nominal);343344        const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});345        expect(unstakingPerBlock).has.length(2);346        expect(unstakingPerBlock[0].amount).to.equal(100n * nominal);347        expect(unstakingPerBlock[1].amount).to.equal(120n * nominal);348        expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.equal(0);349      });350    });351352    [353      {method: 'unstakeAll' as const},354      {method: 'unstakePartial' as const},355    ].map(testCase => {356      itSub(`[${testCase.method}] should be possible for 3 accounts in one block`, async ({helper}) => {357        const stakers = await getAccounts(3);358359        await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));360        await Promise.all(stakers.map(staker => {361          return testCase.method === 'unstakeAll'362            ? helper.staking.unstakeAll(staker)363            : helper.staking.unstakePartial(staker, 100n * nominal);364        }));365366        await Promise.all(stakers.map(async (staker) => {367          expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);368          expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);369        }));370      });371    });372373    itSub('should not be possible for more than 3 accounts in one block', async ({helper}) => {374      if (!await helper.arrange.isDevNode()) {375        const stakers = await getAccounts(10);376377        await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));378        const unstakingResults = await Promise.allSettled(stakers.map((staker, i) => {379          return i % 2 === 0380            ? helper.staking.unstakeAll(staker)381            : helper.staking.unstakePartial(staker, 100n * nominal);382        }));383384        const successfulUnstakes = unstakingResults.filter(result => result.status === 'fulfilled');385        expect(successfulUnstakes).to.have.length(3);386      }387    });388389    itSub('Cannot partially unstake more than staked', async ({helper}) => {390      const [staker] = await getAccounts(1);391      // Staker stakes 300:392      await helper.staking.stake(staker, 100n * nominal);393      await helper.staking.stake(staker, 200n * nominal);394395      // cannot usntake 300.00000...1396      await expect(helper.staking.unstakePartial(staker, 300n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');397      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(2);398399      await helper.staking.unstakePartial(staker, 150n * nominal);400      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);401      await expect(helper.staking.unstakePartial(staker, 150n * nominal + 1n)).to.be.rejectedWith('appPromotion.InsufficientStakedBalance');402      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(1);403404      // nothing broken, can unstake full amount:405      await helper.staking.unstakePartial(staker, 150n * nominal);406      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).eq(0);407    });408409    itSub('Can partially unstake arbitrary amount', async ({helper}) => {410      const [staker] = await getAccounts(1);411      await helper.staking.stake(staker, 100n * nominal);412      await helper.staking.stake(staker, 200n * nominal);413414      // 0. Staker cannot unstake negative amount415      await expect(helper.staking.unstakePartial(staker, -1n)).to.be.rejected;416417      // 1. Staker can unstake 0 wei418      await helper.staking.unstakePartial(staker, 0n);419      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);420      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal);421      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);422423      // 2. Staker can unstake 1 wei424      await helper.staking.unstakePartial(staker, 1n);425      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);426      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(300n * nominal - 1n);427      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(1n);428      // 2.1 The oldest stake decreased:429      let [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});430      expect(stake1.amount).to.eq(100n * nominal - 1n);431      expect(stake2.amount).to.eq(200n * nominal);432433      // 3. Staker can unstake all but 1 wei434      await helper.staking.unstakePartial(staker, 100n * nominal - 2n);435      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(2);436      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(200n * nominal + 1n);437      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(100n * nominal - 1n);438      [stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});439      expect(stake1.amount).to.eq(1n);440      expect(stake2.amount).to.eq(200n * nominal);441    });442443    itSub('can mix different type of unstakes', async ({helper}) => {444      const [staker] = await getAccounts(1);445      await helper.staking.stake(staker, 100n * nominal);446      await helper.staking.stake(staker, 200n * nominal);447448      await helper.staking.unstakePartial(staker, 50n * nominal);449      await helper.staking.unstakeAll(staker);450      expect(await helper.staking.getStakesNumber({Substrate: staker.address})).to.eq(0);451      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);452      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(300n * nominal);453454      const [_unstake1, unstake2] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});455      await helper.wait.forParachainBlockNumber(unstake2.block);456457      expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([]);458      expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n});459      expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(999n);460      expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n);461      expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.eq(0n);462      expect(await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).to.deep.eq([]);463    });464  });465466  describe('collection sponsoring', () => {467    itSub('should actually sponsor transactions', async ({helper}) => {468      const api = helper.getApi();469      const [collectionOwner, tokenSender, receiver] = await getAccounts(3);470      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});471      const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});472      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));473      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);474475      await token.transfer(tokenSender, {Substrate: receiver.address});476      expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});477      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);478479      // senders balance the same, transaction has sponsored480      expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);481      expect (palletBalanceBefore > palletBalanceAfter).to.be.true;482    });483484    itSub('can not be set by non admin', async ({helper}) => {485      const api = helper.getApi();486      const [collectionOwner, nonAdmin] = await getAccounts(2);487488      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});489490      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;491      expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');492    });493494    itSub('should set pallet address as confirmed admin', async ({helper}) => {495      const api = helper.getApi();496      const [collectionOwner, oldSponsor] = await getAccounts(2);497498      // Can set sponsoring for collection without sponsor499      const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});500      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.fulfilled;501      expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});502503      // Can set sponsoring for collection with unconfirmed sponsor504      const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});505      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});506      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.fulfilled;507      expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});508509      // Can set sponsoring for collection with confirmed sponsor510      const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});511      await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);512      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.fulfilled;513      expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});514    });515516    itSub('can be overwritten by collection owner', async ({helper}) => {517      const api = helper.getApi();518      const [collectionOwner, newSponsor] = await getAccounts(2);519      const collection  = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});520      const collectionId = collection.collectionId;521522      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionId))).to.be.fulfilled;523524      // Collection limits still can be changed by the owner525      expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;526      expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);527      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});528529      // Collection sponsor can be changed too530      expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;531      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});532    });533534    itSub('should not overwrite collection limits set by the owner earlier', async ({helper}) => {535      const [owner] = await getAccounts(1);536      const api = helper.getApi();537      const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};538      const collectionWithLimits = await helper.nft.mintCollection(owner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});539540      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.fulfilled;541      expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);542    });543544    itSub('should reject transaction if collection doesn\'t exist', async ({helper}) => {545      const api = helper.getApi();546      const [collectionOwner] = await getAccounts(1);547548      // collection has never existed549      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(999999999))).to.be.rejected;550      // collection has been burned551      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});552      await collection.burn(collectionOwner);553554      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.rejected;555    });556  });557558  describe('stopSponsoringCollection', () => {559    itSub('can not be called by non-admin', async ({helper}) => {560      const api = helper.getApi();561      const [collectionOwner, nonAdmin] = await getAccounts(2);562      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});563564      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId))).to.be.fulfilled;565566      await expect(helper.signTransaction(nonAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;567      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});568    });569570    itSub('should set sponsoring as disabled', async ({helper}) => {571      const api = helper.getApi();572      const [collectionOwner, recepient] = await getAccounts(2);573      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});574      const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});575576      await helper.signTransaction(palletAdmin, api.tx.appPromotion.sponsorCollection(collection.collectionId));577      await helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId));578579      expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');580581      // Transactions are not sponsored anymore:582      const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);583      await token.transfer(collectionOwner, {Substrate: recepient.address});584      const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);585      expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);586    });587588    itSub('should not affect collection which is not sponsored by pallete', async ({helper}) => {589      const api = helper.getApi();590      const [collectionOwner] = await getAccounts(1);591      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});592      await collection.confirmSponsorship(collectionOwner);593594      await expect(helper.signTransaction(palletAdmin, api.tx.appPromotion.stopSponsoringCollection(collection.collectionId))).to.be.rejected;595596      expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});597    });598599    itSub('should reject transaction if collection does not exist', async ({helper}) => {600      const [collectionOwner] = await getAccounts(1);601      const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});602603      await collection.burn(collectionOwner);604      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [collection.collectionId], true)).to.be.rejectedWith('common.CollectionNotFound');605      await expect(helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringCollection', [999_999_999], true)).to.be.rejectedWith('common.CollectionNotFound');606    });607  });608609  describe('contract sponsoring', () => {610    itEth('should set palletes address as a sponsor', async ({helper}) => {611      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();612      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);613      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);614615      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);616617      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;618      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);619      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({620        confirmed: {621          substrate: palletAddress,622        },623      });624    });625626    itEth('should overwrite sponsoring mode and existed sponsor', async ({helper}) => {627      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();628      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);629      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);630631      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;632633      // Contract is self sponsored634      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.be.deep.equal({635        confirmed: {636          ethereum: flipper.options.address.toLowerCase(),637        },638      });639640      // set promotion sponsoring641      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);642643      // new sponsor is pallet address644      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;645      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);646      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({647        confirmed: {648          substrate: palletAddress,649        },650      });651    });652653    itEth('can be overwritten by contract owner', async ({helper}) => {654      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();655      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);656      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);657658      // contract sponsored by pallet659      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);660661      // owner sets self sponsoring662      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;663664      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.true;665      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);666      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({667        confirmed: {668          ethereum: flipper.options.address.toLowerCase(),669        },670      });671    });672673    itEth('can not be set by non admin', async ({helper}) => {674      const [nonAdmin] = await getAccounts(1);675      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();676      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);677      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);678679      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;680681      // nonAdmin calls sponsorContract682      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');683684      // contract still self-sponsored685      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({686        confirmed: {687          ethereum: flipper.options.address.toLowerCase(),688        },689      });690    });691692    itEth('should actually sponsor transactions', async ({helper}) => {693      // Contract caller694      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);695      const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);696697      // Deploy flipper698      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();699      const flipper = await helper.eth.deployFlipper(contractOwner); // await deployFlipper(web3, contractOwner);700      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);701702      // Owner sets to sponsor every tx703      await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});704      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});705      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address, 1000n); // transferBalanceToEth(api, alice, flipper.options.address, 1000n);706707      // Set promotion to the Flipper708      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);709710      // Caller calls Flipper711      await flipper.methods.flip().send({from: caller});712      expect(await flipper.methods.getValue().call()).to.be.true;713714      // The contracts and caller balances have not changed715      const callerBalance = await helper.balance.getEthereum(caller);716      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);717      expect(callerBalance).to.be.equal(1000n * nominal);718      expect(1000n * nominal === contractBalanceAfter).to.be.true;719720      // The pallet balance has decreased721      const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);722      expect(palletBalanceAfter < palletBalanceBefore).to.be.true;723    });724  });725726  describe('stopSponsoringContract', () => {727    itEth('should remove pallet address from contract sponsors', async ({helper}) => {728      const caller = await helper.eth.createAccountWithBalance(donor, 1000n);729      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();730      const flipper = await helper.eth.deployFlipper(contractOwner);731      await helper.eth.transferBalanceFromSubstrate(donor, flipper.options.address);732      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);733734      await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});735      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address], true);736      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true);737738      expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false;739      expect((await helper.callRpc('api.query.evmContractHelpers.owner', [flipper.options.address])).toJSON()).to.be.equal(contractOwner);740      expect((await helper.callRpc('api.query.evmContractHelpers.sponsoring', [flipper.options.address])).toJSON()).to.deep.equal({741        disabled: null,742      });743744      await flipper.methods.flip().send({from: caller});745      expect(await flipper.methods.getValue().call()).to.be.true;746747      const callerBalance = await helper.balance.getEthereum(caller);748      const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);749750      // caller payed for call751      expect(1000n * nominal > callerBalance).to.be.true;752      expect(contractBalanceAfter).to.be.equal(100n * nominal);753    });754755    itEth('can not be called by non-admin', async ({helper}) => {756      const [nonAdmin] = await getAccounts(1);757      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();758      const flipper = await helper.eth.deployFlipper(contractOwner);759760      await helper.executeExtrinsic(palletAdmin, 'api.tx.appPromotion.sponsorContract', [flipper.options.address]);761      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address]))762        .to.be.rejectedWith(/appPromotion\.NoPermission/);763    });764765    itEth('should not affect a contract which is not sponsored by pallete', async ({helper}) => {766      const [nonAdmin] = await getAccounts(1);767      const contractOwner = (await helper.eth.createAccountWithBalance(donor, 1000n)).toLowerCase();768      const flipper = await helper.eth.deployFlipper(contractOwner);769      const contractHelper = await helper.ethNativeContract.contractHelpers(contractOwner);770      await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.fulfilled;771772      await expect(helper.executeExtrinsic(nonAdmin, 'api.tx.appPromotion.stopSponsoringContract', [flipper.options.address], true)).to.be.rejectedWith('appPromotion.NoPermission');773    });774  });775776  describe('payoutStakers', () => {777    itSub('can not be called by non admin', async ({helper}) => {778      const [nonAdmin] = await getAccounts(1);779      await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission');780    });781782    itSub('should increase total staked', async ({helper}) => {783      const [staker] = await getAccounts(1);784      const totalStakedBefore = await helper.staking.getTotalStaked();785      await helper.staking.stake(staker, 100n * nominal);786787      // Wait for rewards and pay788      const [stakedInBlock] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});789      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stakedInBlock.block));790791      const payout = await helper.admin.payoutStakers(palletAdmin, 100);792      const totalPayout = payout.reduce((prev, payout) => prev + payout.payout, 0n);793      const stakerReward = payout.find(p => p.staker === staker.address);794795      expect(stakerReward?.payout).to.eq(calculateIncome(100n * nominal) - (100n * nominal));796797      const totalStakedAfter = await helper.staking.getTotalStaked();798      expect(totalStakedAfter).to.equal(totalStakedBefore + (100n * nominal) + totalPayout);799      // staker can unstake800      await helper.staking.unstakeAll(staker);801      expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedAfter - calculateIncome(100n * nominal));802    });803804    itSub('should credit 0.05% for staking period', async ({helper}) => {805      const [staker] = await getAccounts(1);806807      await waitPromotionPeriodDoesntEnd(helper);808809      await helper.staking.stake(staker, 100n * nominal);810      await helper.staking.stake(staker, 200n * nominal);811812      // wait rewards are available:813      const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});814      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));815816      const payoutToStaker = (await helper.admin.payoutStakers(palletAdmin, 100)).find((payout) => payout.staker === staker.address)!.payout;817      expect(payoutToStaker + 300n * nominal).to.equal(calculateIncome(300n * nominal));818819      const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});820      const income1 = calculateIncome(100n * nominal);821      const income2 = calculateIncome(200n * nominal);822      expect(totalStakedPerBlock[0].amount).to.equal(income1);823      expect(totalStakedPerBlock[1].amount).to.equal(income2);824825      const stakerBalance = await helper.balance.getSubstrateFull(staker.address);826      expect(stakerBalance).to.contain({frozen: income1 + income2, reserved: 0n});827      expect(stakerBalance.free / nominal).to.eq(999n);828    });829830    itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => {831      const [staker] = await getAccounts(1);832833      await helper.staking.stake(staker, 100n * nominal);834      // wait for two rewards are available:835      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});836      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);837838      await helper.admin.payoutStakers(palletAdmin, 100);839      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});840      const frozenBalanceShouldBe = calculateIncome(100n * nominal, 2);841      expect(stake.amount).to.be.equal(frozenBalanceShouldBe);842843      const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);844845      expect(stakerFullBalance).to.contain({reserved: 0n, frozen: frozenBalanceShouldBe});846    });847848    itSub('should not be credited for pending-unstaked tokens', async ({helper}) => {849      // staker unstakes before rewards been payed850      const [staker] = await getAccounts(1);851      await helper.staking.stake(staker, 100n * nominal);852      const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});853      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);854      await helper.staking.unstakeAll(staker);855856      // so he did not receive any rewards857      const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);858      await helper.admin.payoutStakers(palletAdmin, 100);859      const totalBalanceAfter = await helper.balance.getSubstrate(staker.address);860861      expect(totalBalanceBefore).to.be.equal(totalBalanceAfter);862    });863864    itSub('should bring compound interest', async ({helper}) => {865      const [staker] = await getAccounts(1);866867      await helper.staking.stake(staker, 100n * nominal);868869      let [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});870      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));871872      await helper.admin.payoutStakers(palletAdmin, 100);873      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});874      expect(stake.amount).to.equal(calculateIncome(100n * nominal));875876      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block) + LOCKING_PERIOD);877      await helper.admin.payoutStakers(palletAdmin, 100);878      [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});879      expect(stake.amount).to.equal(calculateIncome(100n * nominal, 2));880    });881882    itSub('can calculate reward for tiny stake', async ({helper}) => {883      const [staker] = await getAccounts(1);884      await helper.staking.stake(staker, 100n * nominal);885      await helper.staking.stake(staker, 100n * nominal);886      await helper.staking.unstakePartial(staker, 100n * nominal - 1n);887888      const [_stake1, stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});889      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));890891      const stakerPayout = await payUntilRewardFor(staker.address, helper);892      expect(stakerPayout.stake).to.eq(100n * nominal + 1n);893    });894895    itSub('can eventually pay all rewards', async ({helper}) => {896      const stakers = await getAccounts(30);897      // Create 30 stakes:898      await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));899900      let unstakingTxs = [];901      for (const staker of stakers) {902        if (unstakingTxs.length == 3) {903          await Promise.all(unstakingTxs);904          unstakingTxs = [];905        }906        unstakingTxs.push(helper.staking.unstakePartial(staker, 100n * nominal - 1n));907      }908909      const [staker] = await getAccounts(1);910      await helper.staking.stake(staker, 100n * nominal);911      const [stake] = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});912      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake.block));913914      let payouts;915      do {916        payouts = await helper.admin.payoutStakers(palletAdmin, 20);917      } while (payouts.length !== 0);918    });919  });920921  describe('events', () => {922    [923      {method: 'unstakePartial' as const},924      {method: 'unstakeAll' as const},925    ].map(testCase => {926      itSub(testCase.method, async ({helper}) => {927        const unstakeParams: [] | [bigint] = testCase.method === 'unstakePartial'928          ? [100n * nominal - 1n]929          : [];930        const [staker] = await getAccounts(1);931        await helper.staking.stake(staker, 100n * nominal);932        await helper.staking.stake(staker, 200n * nominal);933        const {result} = await helper.executeExtrinsic(staker, `api.tx.appPromotion.${testCase.method}`, unstakeParams);934935        const event = result.events.find(e => e.event.section === 'appPromotion' && e.event.method === 'Unstake');936        const unstakerEvents = event?.event.data[0].toString();937        const unstakedEvents = BigInt(event?.event.data[1].toString());938        expect(unstakerEvents).to.eq(staker.address);939        expect(unstakedEvents).to.eq(testCase.method === 'unstakeAll' ? 300n * nominal : 100n * nominal - 1n);940      });941    });942943    itSub('stake', async ({helper}) => {944      const [staker] = await getAccounts(1);945      const {result} = await helper.executeExtrinsic(staker, 'api.tx.appPromotion.stake', [100n * nominal]);946947      const event = result.events.find(e => e.event.section === 'appPromotion' && e.event.method === 'Stake');948      const stakerEvents = event?.event.data[0].toString();949      const stakedEvents = BigInt(event?.event.data[1].toString());950      expect(stakerEvents).to.eq(staker.address);951      expect(stakedEvents).to.eq(100n * nominal);952    });953954    // Flaky955    itSub.skip('payoutStakers', async ({helper}) => {956      const [staker1, staker2] = await getAccounts(2);957      const STAKE1 = 100n * nominal;958      const STAKE2 = 200n * nominal;959      await helper.staking.stake(staker1, STAKE1);960      await helper.staking.stake(staker2, STAKE2);961962      const [stake2] = await helper.staking.getTotalStakedPerBlock({Substrate: staker2.address});963      await helper.wait.forRelayBlockNumber(rewardAvailableInBlock(stake2.block));964965      const results = await helper.admin.payoutStakers(palletAdmin, 100);966      const stakersEvents = results.filter(ev => ev.staker === staker1.address || ev.staker === staker2.address);967      expect(stakersEvents).has.length(2);968      expect(stakersEvents).has.not.ordered.members([969        {staker: staker1.address, stake: STAKE1, payout: calculateIncome(STAKE1) - STAKE1},970        {staker: staker2.address, stake: STAKE2, payout: calculateIncome(STAKE2) - STAKE2},971      ]);972    });973  });974});975976977// Sometimes is is required to make a cycle in order for the payment to be calculated for a specific account978async function payUntilRewardFor(account: string, helper: DevUniqueHelper) {979  for (let i = 0; i < 3; i++) {980    const payouts = await helper.admin.payoutStakers(palletAdmin, 100);981    const accountPayout = payouts.find(p => p.staker === account);982    if (accountPayout) return accountPayout;983  }984  throw Error(`Cannot find payout for ${account}`);985}986987function calculateIncome(base: bigint, iter = 0, calcPeriod: bigint = UNLOCKING_PERIOD): bigint {988  const DAY = 7200n;989  const ACCURACY = 1_000_000_000n;990  // 5n / 10_000n = 0.05% p/day991  const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;992993  if (iter > 1) {994    return calculateIncome(income, iter - 1, calcPeriod);995  } else return income;996}997998function rewardAvailableInBlock(stakedInBlock: bigint) {999  if (stakedInBlock % LOCKING_PERIOD === 0n) return stakedInBlock + LOCKING_PERIOD;1000  return (stakedInBlock - stakedInBlock % LOCKING_PERIOD) + (LOCKING_PERIOD * 2n);1001}10021003// Wait while promotion period less than specified block, to avoid boundary cases1004// 0 if this should be the beginning of the period.1005async function waitPromotionPeriodDoesntEnd(helper: DevUniqueHelper, waitBlockLessThan = LOCKING_PERIOD / 3n) {1006  const relayBlockNumber = (await helper.callRpc('api.query.parachainSystem.validationData', [])).value.relayParentNumber.toNumber(); // await helper.chain.getLatestBlockNumber();1007  const currentPeriodBlock = BigInt(relayBlockNumber) % LOCKING_PERIOD;10081009  if (currentPeriodBlock > waitBlockLessThan) {1010    await helper.wait.forRelayBlockNumber(BigInt(relayBlockNumber) + LOCKING_PERIOD - currentPeriodBlock);1011  }1012}
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};