git.delta.rocks / unique-network / refs/commits / 82643c5daa1a

difftreelog

feat add tests

Trubnikov Sergey2023-04-25parent: #f0ad1b0.patch.diff
in: master

12 files changed

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(),
modifiedpallets/balances-adapter/src/erc.rsdiffbeforeafterboth
--- a/pallets/balances-adapter/src/erc.rs
+++ b/pallets/balances-adapter/src/erc.rs
@@ -38,14 +38,14 @@
 
 #[solidity_interface(name = ERC20, events(ERC20Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x942e8b22)]
 impl<T: Config> NativeFungibleHandle<T> {
-	fn allowance(&self, owner: Address, spender: Address) -> Result<U256> {
+	fn allowance(&self, _owner: Address, _spender: Address) -> Result<U256> {
 		Ok(U256::zero())
 	}
 
 	// #[weight(<SelfWeightOf<T>>::approve())]
-	fn approve(&mut self, caller: Caller, spender: Address, amount: U256) -> Result<bool> {
+	fn approve(&mut self, _caller: Caller, _spender: Address, _amount: U256) -> Result<bool> {
 		// self.consume_store_reads(1)?;
-		Err("Approve not supported now".into())
+		Err("Approve not supported".into())
 	}
 
 	fn balance_of(&self, owner: Address) -> Result<U256> {
@@ -106,7 +106,7 @@
 		let to = T::CrossAccountId::from_eth(to);
 		let amount = amount.try_into().map_err(|_| "amount overflow")?;
 
-		if from != to {
+		if from != caller {
 			return Err("no permission".into());
 		}
 		// let budget = self
@@ -171,7 +171,7 @@
 		let to = to.into_sub_cross_account::<T>()?;
 		let amount = amount.try_into().map_err(|_| "amount overflow")?;
 
-		if from != to {
+		if from != caller {
 			return Err("no permission".into());
 		}
 
modifiedpallets/common/src/dispatch.rsdiffbeforeafterboth
--- a/pallets/common/src/dispatch.rs
+++ b/pallets/common/src/dispatch.rs
@@ -83,7 +83,7 @@
 	///
 	/// * `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.
 	///
modifiedpallets/unique/src/lib.rsdiffbeforeafterboth
--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -1296,10 +1296,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
@@ -24,7 +24,8 @@
 		weights::CommonWeights,
 		RelayChainBlockNumberProvider,
 	},
-	Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, RUNTIME_NAME, TOKEN_SYMBOL, Balances,
+	Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, RUNTIME_NAME, TOKEN_SYMBOL, DECIMALS,
+	Balances,
 };
 use frame_support::traits::{ConstU32, ConstU64, Currency};
 use up_common::{
@@ -53,7 +54,7 @@
 
 parameter_types! {
 	pub const CollectionCreationPrice: Balance = 2 * UNIQUE;
-	pub const Decimals: u8 = 32;
+	pub const Decimals: u8 = DECIMALS;
 	pub TreasuryAccountId: AccountId = TreasuryModuleId::get().into_account_truncating();
 	pub Name: String = RUNTIME_NAME.to_string();
 	pub Symbol: String = TOKEN_SYMBOL.to_string();
modifiedruntime/common/dispatch.rsdiffbeforeafterboth
before · runtime/common/dispatch.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617use frame_support::{dispatch::DispatchResult, ensure};18use pallet_evm::{PrecompileHandle, PrecompileResult};19use sp_core::H160;20use sp_runtime::DispatchError;21use sp_std::{borrow::ToOwned, vec::Vec};22use pallet_common::{23	CollectionById, CollectionHandle, CommonCollectionOperations, erc::CommonEvmHandler,24	eth::map_eth_to_id,25};26pub use pallet_common::dispatch::CollectionDispatch;27use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};28use pallet_balances_adapter::{Pallet as PalletNativeFungible, NativeFungibleHandle};29use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};30use pallet_refungible::{31	Pallet as PalletRefungible, RefungibleHandle, erc_token::RefungibleTokenHandle,32};33use up_data_structs::{34	CollectionMode, CreateCollectionData, MAX_DECIMAL_POINTS, mapping::TokenAddressMapping,35	CollectionId, CollectionFlags,36};3738#[cfg(not(feature = "refungible"))]39use pallet_common::unsupported;4041pub enum CollectionDispatchT<T>42where43	T: pallet_fungible::Config44		+ pallet_nonfungible::Config45		+ pallet_refungible::Config46		+ pallet_balances_adapter::Config,47{48	Fungible(FungibleHandle<T>),49	Nonfungible(NonfungibleHandle<T>),50	Refungible(RefungibleHandle<T>),51	NativeFungible(NativeFungibleHandle<T>),52}5354impl<T> CollectionDispatch<T> for CollectionDispatchT<T>55where56	T: pallet_common::Config57		+ pallet_unique::Config58		+ pallet_fungible::Config59		+ pallet_nonfungible::Config60		+ pallet_refungible::Config61		+ pallet_balances_adapter::Config,62{63	fn check_is_internal(&self) -> DispatchResult {64		match self {65			Self::Fungible(h) => h.check_is_internal(),66			Self::Nonfungible(h) => h.check_is_internal(),67			Self::Refungible(h) => h.check_is_internal(),68			Self::NativeFungible(h) => h.check_is_internal(),69		}70	}7172	fn create(73		sender: T::CrossAccountId,74		payer: T::CrossAccountId,75		data: CreateCollectionData<T::AccountId>,76		flags: CollectionFlags,77	) -> Result<CollectionId, DispatchError> {78		let id = match data.mode {79			CollectionMode::NFT => {80				<PalletNonfungible<T>>::init_collection(sender, payer, data, flags)?81			}82			CollectionMode::Fungible(decimal_points) => {83				// check params84				ensure!(85					decimal_points <= MAX_DECIMAL_POINTS,86					pallet_unique::Error::<T>::CollectionDecimalPointLimitExceeded87				);88				<PalletFungible<T>>::init_collection(sender, payer, data, flags)?89			}9091			#[cfg(feature = "refungible")]92			CollectionMode::ReFungible => {93				<PalletRefungible<T>>::init_collection(sender, payer, data, flags)?94			}9596			#[cfg(not(feature = "refungible"))]97			CollectionMode::ReFungible => return unsupported!(T),98		};99		Ok(id)100	}101102	fn destroy(sender: T::CrossAccountId, collection: CollectionHandle<T>) -> DispatchResult {103		match collection.mode {104			CollectionMode::ReFungible => {105				PalletRefungible::destroy_collection(RefungibleHandle::cast(collection), &sender)?106			}107			CollectionMode::Fungible(_) => {108				PalletFungible::destroy_collection(FungibleHandle::cast(collection), &sender)?109			}110			CollectionMode::NFT => {111				PalletNonfungible::destroy_collection(NonfungibleHandle::cast(collection), &sender)?112			}113		}114		Ok(())115	}116117	fn dispatch(collection_id: CollectionId) -> Result<Self, DispatchError> {118		if collection_id == CollectionId(0) {119			return Ok(Self::NativeFungible(NativeFungibleHandle::new()));120		}121122		let handle = <CollectionHandle<T>>::try_get(collection_id)?;123		Ok(match handle.mode {124			CollectionMode::Fungible(_) => Self::Fungible(FungibleHandle::cast(handle)),125			CollectionMode::NFT => Self::Nonfungible(NonfungibleHandle::cast(handle)),126			CollectionMode::ReFungible => Self::Refungible(RefungibleHandle::cast(handle)),127		})128	}129130	fn as_dyn(&self) -> &dyn CommonCollectionOperations<T> {131		match self {132			Self::Fungible(h) => h,133			Self::Nonfungible(h) => h,134			Self::Refungible(h) => h,135			Self::NativeFungible(h) => h,136		}137	}138}139140impl<T> pallet_evm::OnMethodCall<T> for CollectionDispatchT<T>141where142	T: pallet_common::Config143		+ pallet_unique::Config144		+ pallet_fungible::Config145		+ pallet_nonfungible::Config146		+ pallet_refungible::Config147		+ pallet_balances_adapter::Config,148	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,149{150	fn is_reserved(target: &H160) -> bool {151		map_eth_to_id(target).is_some()152	}153	fn is_used(target: &H160) -> bool {154		map_eth_to_id(target)155			.map(<CollectionById<T>>::contains_key)156			.unwrap_or(false)157	}158	fn get_code(target: &H160) -> Option<Vec<u8>> {159		if let Some(collection_id) = map_eth_to_id(target) {160			let collection = <CollectionById<T>>::get(collection_id)?;161			Some(162				match collection.mode {163					CollectionMode::NFT => <NonfungibleHandle<T>>::CODE,164					CollectionMode::Fungible(_) => <FungibleHandle<T>>::CODE,165					CollectionMode::ReFungible => <RefungibleHandle<T>>::CODE,166				}167				.to_owned(),168			)169		} else if let Some((collection_id, _token_id)) =170			<T as pallet_common::Config>::EvmTokenAddressMapping::address_to_token(target)171		{172			let collection = <CollectionById<T>>::get(collection_id)?;173			if collection.mode != CollectionMode::ReFungible {174				return None;175			}176			// TODO: check token existence177			Some(<RefungibleTokenHandle<T>>::CODE.to_owned())178		} else {179			None180		}181	}182	fn call(handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {183		if let Some(collection_id) = map_eth_to_id(&handle.code_address()) {184			if collection_id == CollectionId(0) {185				<NativeFungibleHandle<T>>::new().call(handle)186			} else {187				let collection = <CollectionHandle<T>>::new_with_gas_limit(188					collection_id,189					handle.remaining_gas(),190				)?;191192				match collection.mode {193					CollectionMode::Fungible(_) => FungibleHandle::cast(collection).call(handle),194					CollectionMode::NFT => NonfungibleHandle::cast(collection).call(handle),195					CollectionMode::ReFungible => RefungibleHandle::cast(collection).call(handle),196				}197			}198		} else if let Some((collection_id, token_id)) =199			<T as pallet_common::Config>::EvmTokenAddressMapping::address_to_token(200				&handle.code_address(),201			) {202			let collection =203				<CollectionHandle<T>>::new_with_gas_limit(collection_id, handle.remaining_gas())?;204			if collection.mode != CollectionMode::ReFungible {205				return None;206			}207208			let h = RefungibleHandle::cast(collection);209			// TODO: check token existence210			RefungibleTokenHandle(h, token_id).call(handle)211		} else {212			None213		}214	}215}
after · runtime/common/dispatch.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617use frame_support::{dispatch::DispatchResult, ensure};18use pallet_evm::{PrecompileHandle, PrecompileResult};19use sp_core::H160;20use sp_runtime::DispatchError;21use sp_std::{borrow::ToOwned, vec::Vec};22use pallet_common::{23	CollectionById, CollectionHandle, CommonCollectionOperations, erc::CommonEvmHandler,24	eth::map_eth_to_id,25};26pub use pallet_common::dispatch::CollectionDispatch;27use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};28use pallet_balances_adapter::{Pallet as PalletNativeFungible, NativeFungibleHandle};29use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};30use pallet_refungible::{31	Pallet as PalletRefungible, RefungibleHandle, erc_token::RefungibleTokenHandle,32};33use up_data_structs::{34	CollectionMode, CreateCollectionData, MAX_DECIMAL_POINTS, mapping::TokenAddressMapping,35	CollectionId, CollectionFlags,36};3738#[cfg(not(feature = "refungible"))]39use pallet_common::unsupported;4041pub enum CollectionDispatchT<T>42where43	T: pallet_fungible::Config44		+ pallet_nonfungible::Config45		+ pallet_refungible::Config46		+ pallet_balances_adapter::Config,47{48	Fungible(FungibleHandle<T>),49	Nonfungible(NonfungibleHandle<T>),50	Refungible(RefungibleHandle<T>),51	NativeFungible(NativeFungibleHandle<T>),52}5354impl<T> CollectionDispatch<T> for CollectionDispatchT<T>55where56	T: pallet_common::Config57		+ pallet_unique::Config58		+ pallet_fungible::Config59		+ pallet_nonfungible::Config60		+ pallet_refungible::Config61		+ pallet_balances_adapter::Config,62{63	fn check_is_internal(&self) -> DispatchResult {64		match self {65			Self::Fungible(h) => h.check_is_internal(),66			Self::Nonfungible(h) => h.check_is_internal(),67			Self::Refungible(h) => h.check_is_internal(),68			Self::NativeFungible(h) => h.check_is_internal(),69		}70	}7172	fn create(73		sender: T::CrossAccountId,74		payer: T::CrossAccountId,75		data: CreateCollectionData<T::AccountId>,76		flags: CollectionFlags,77	) -> Result<CollectionId, DispatchError> {78		let id = match data.mode {79			CollectionMode::NFT => {80				<PalletNonfungible<T>>::init_collection(sender, payer, data, flags)?81			}82			CollectionMode::Fungible(decimal_points) => {83				// check params84				ensure!(85					decimal_points <= MAX_DECIMAL_POINTS,86					pallet_unique::Error::<T>::CollectionDecimalPointLimitExceeded87				);88				<PalletFungible<T>>::init_collection(sender, payer, data, flags)?89			}9091			#[cfg(feature = "refungible")]92			CollectionMode::ReFungible => {93				<PalletRefungible<T>>::init_collection(sender, payer, data, flags)?94			}9596			#[cfg(not(feature = "refungible"))]97			CollectionMode::ReFungible => return unsupported!(T),98		};99		Ok(id)100	}101102	fn destroy(sender: T::CrossAccountId, collection_id: CollectionId) -> DispatchResult {103		let collection = <CollectionHandle<T>>::try_get(collection_id)?;104		collection.check_is_internal()?;105106		match collection.mode {107			CollectionMode::ReFungible => {108				PalletRefungible::destroy_collection(RefungibleHandle::cast(collection), &sender)?109			}110			CollectionMode::Fungible(_) => {111				PalletFungible::destroy_collection(FungibleHandle::cast(collection), &sender)?112			}113			CollectionMode::NFT => {114				PalletNonfungible::destroy_collection(NonfungibleHandle::cast(collection), &sender)?115			}116		}117		Ok(())118	}119120	fn dispatch(collection_id: CollectionId) -> Result<Self, DispatchError> {121		if collection_id == CollectionId(0) {122			return Ok(Self::NativeFungible(NativeFungibleHandle::new()));123		}124125		let handle = <CollectionHandle<T>>::try_get(collection_id)?;126		Ok(match handle.mode {127			CollectionMode::Fungible(_) => Self::Fungible(FungibleHandle::cast(handle)),128			CollectionMode::NFT => Self::Nonfungible(NonfungibleHandle::cast(handle)),129			CollectionMode::ReFungible => Self::Refungible(RefungibleHandle::cast(handle)),130		})131	}132133	fn as_dyn(&self) -> &dyn CommonCollectionOperations<T> {134		match self {135			Self::Fungible(h) => h,136			Self::Nonfungible(h) => h,137			Self::Refungible(h) => h,138			Self::NativeFungible(h) => h,139		}140	}141}142143impl<T> pallet_evm::OnMethodCall<T> for CollectionDispatchT<T>144where145	T: pallet_common::Config146		+ pallet_unique::Config147		+ pallet_fungible::Config148		+ pallet_nonfungible::Config149		+ pallet_refungible::Config150		+ pallet_balances_adapter::Config,151	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,152{153	fn is_reserved(target: &H160) -> bool {154		map_eth_to_id(target).is_some()155	}156	fn is_used(target: &H160) -> bool {157		map_eth_to_id(target)158			.map(<CollectionById<T>>::contains_key)159			.unwrap_or(false)160	}161	fn get_code(target: &H160) -> Option<Vec<u8>> {162		if let Some(collection_id) = map_eth_to_id(target) {163			let collection = <CollectionById<T>>::get(collection_id)?;164			Some(165				match collection.mode {166					CollectionMode::NFT => <NonfungibleHandle<T>>::CODE,167					CollectionMode::Fungible(_) => <FungibleHandle<T>>::CODE,168					CollectionMode::ReFungible => <RefungibleHandle<T>>::CODE,169				}170				.to_owned(),171			)172		} else if let Some((collection_id, _token_id)) =173			<T as pallet_common::Config>::EvmTokenAddressMapping::address_to_token(target)174		{175			let collection = <CollectionById<T>>::get(collection_id)?;176			if collection.mode != CollectionMode::ReFungible {177				return None;178			}179			// TODO: check token existence180			Some(<RefungibleTokenHandle<T>>::CODE.to_owned())181		} else {182			None183		}184	}185	fn call(handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {186		if let Some(collection_id) = map_eth_to_id(&handle.code_address()) {187			if collection_id == CollectionId(0) {188				<NativeFungibleHandle<T>>::new().call(handle)189			} else {190				let collection = <CollectionHandle<T>>::new_with_gas_limit(191					collection_id,192					handle.remaining_gas(),193				)?;194195				match collection.mode {196					CollectionMode::Fungible(_) => FungibleHandle::cast(collection).call(handle),197					CollectionMode::NFT => NonfungibleHandle::cast(collection).call(handle),198					CollectionMode::ReFungible => RefungibleHandle::cast(collection).call(handle),199				}200			}201		} else if let Some((collection_id, token_id)) =202			<T as pallet_common::Config>::EvmTokenAddressMapping::address_to_token(203				&handle.code_address(),204			) {205			let collection =206				<CollectionHandle<T>>::new_with_gas_limit(collection_id, handle.remaining_gas())?;207			if collection.mode != CollectionMode::ReFungible {208				return None;209			}210211			let h = RefungibleHandle::cast(collection);212			// TODO: check token existence213			RefungibleTokenHandle(h, token_id).call(handle)214		} else {215			None216		}217	}218}
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/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/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/src/eth/nativeFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nativeFungible.test.ts
+++ b/tests/src/eth/nativeFungible.test.ts
@@ -15,29 +15,157 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {IKeyringPair} from '@polkadot/types/types';
-import {itEth, usingEthPlaygrounds} from './util';
+import {expect, itEth, usingEthPlaygrounds} from './util';
 
-describe('NativeFungible: Plain calls', () => {
+describe('NativeFungible: ERC20 calls', () => {
   let donor: IKeyringPair;
-  let alice: IKeyringPair;
-  let owner: IKeyringPair;
 
   before(async function() {
     await usingEthPlaygrounds(async (helper, privateKey) => {
       donor = await privateKey({url: import.meta.url});
-      [alice, owner] = await helper.arrange.createAccounts([30n, 20n], donor);
+      // [alice] = await helper.arrange.createAccounts([30n], donor);
     });
   });
 
-  itEth.skip('Can perform approve()', async ({helper}) => {
+  itEth('approve()', async ({helper}) => {
     const owner = await helper.eth.createAccountWithBalance(donor);
     const spender = helper.eth.createAccount();
-    const collection = await helper.ft.mintCollection(alice);
-    await collection.mint(alice, 200n, {Ethereum: owner});
+    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 decimals = await contract.methods.decimals().call({from: owner});
+    expect(decimals).to.be.eq('18');
+  });
+
+  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 name = await contract.methods.name().call({from: owner});
+    expect(name).to.be.eq('opal');
+  });
+
+  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 name = await contract.methods.symbol().call({from: owner});
+    expect(name).to.be.eq('OPL');
+  });
+
+  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 === balanceReceiverAfter - 50n).to.be.true;
+  });
 
+  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);
 
-    await contract.methods.approve(spender, 100).send({from: 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('no permission');
+  });
+});
+
+describe('NativeFungible: ERC20UniqueExtensions calls', () => {
+  let donor: IKeyringPair;
+
+  before(async function() {
+    await usingEthPlaygrounds(async (helper, privateKey) => {
+      donor = await privateKey({url: import.meta.url});
+      // [alice] = await helper.arrange.createAccounts([30n], donor);
+    });
+  });
+
+  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
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
@@ -142,7 +142,7 @@
 
   async collection(address: string, mode: TCollectionMode, caller?: string, mergeDeprecated = false) {
     let abi;
-    if (address === '0x17C4e6453cC49aaAAEaCA894E6d9683e00000000' && mode === 'ft') {
+    if (address === '0x17C4e6453cC49aaAAEaCA894E6d9683e00000000') {
       abi = nativeFungibleAbi;
     } else {
       abi ={
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -2400,7 +2400,16 @@
     return {free: accountInfo.free.toBigInt(), frozen: accountInfo.frozen.toBigInt(), reserved: accountInfo.reserved.toBigInt()};
   }
 
-  async getLocked(address: TSubstrateAccount): Promise<[{ id: string, amount: bigint, reason: string }]> {
+  /**
+   * 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}; });
   }
@@ -2488,6 +2497,14 @@
   }
 
   /**
+   * 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