From 833ab943fbb55308e5146d34b98eb87bc87b175f Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 30 May 2023 08:06:04 +0000 Subject: [PATCH] feat: merge native erc20 adapter --- --- 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", --- 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" - --- 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 --- 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(), --- /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"] --- /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(PhantomData); + +// All implementations with `Weight::default` used in methods that return error `UnsupportedOperation`. +impl CommonWeightInfo for CommonWeights { + fn create_multiple_items(_amount: &[up_data_structs::CreateItemData]) -> Weight { + Weight::default() + } + + fn create_multiple_items_ex( + _cost: &up_data_structs::CreateItemExData, + ) -> 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 { + as WeightInfo>::transfer_allow_death() + } + + fn approve() -> Weight { + Weight::default() + } + + fn approve_from() -> Weight { + Weight::default() + } + + fn transfer_from() -> Weight { + 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 CommonCollectionOperations for NativeFungibleHandle { + fn create_item( + &self, + _sender: ::CrossAccountId, + _to: ::CrossAccountId, + _data: up_data_structs::CreateItemData, + _nesting_budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn create_multiple_items( + &self, + _sender: ::CrossAccountId, + _to: ::CrossAccountId, + _data: Vec, + _nesting_budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn create_multiple_items_ex( + &self, + _sender: ::CrossAccountId, + _data: up_data_structs::CreateItemExData<::CrossAccountId>, + _nesting_budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn burn_item( + &self, + _sender: ::CrossAccountId, + _token: TokenId, + _amount: u128, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn burn_item_recursively( + &self, + _sender: ::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!(>::UnsupportedOperation); + } + + fn set_collection_properties( + &self, + _sender: ::CrossAccountId, + _properties: Vec, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn delete_collection_properties( + &self, + _sender: &::CrossAccountId, + _property_keys: Vec, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn set_token_properties( + &self, + _sender: ::CrossAccountId, + _token_id: TokenId, + _properties: Vec, + _budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn delete_token_properties( + &self, + _sender: ::CrossAccountId, + _token_id: TokenId, + _property_keys: Vec, + _budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn set_token_property_permissions( + &self, + _sender: &::CrossAccountId, + _property_permissions: Vec, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn transfer( + &self, + sender: ::CrossAccountId, + to: ::CrossAccountId, + _token: TokenId, + amount: u128, + budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + >::transfer(self, &sender, &to, amount, budget) + } + + fn approve( + &self, + _sender: ::CrossAccountId, + _spender: ::CrossAccountId, + _token: TokenId, + _amount: u128, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn approve_from( + &self, + _sender: ::CrossAccountId, + _from: ::CrossAccountId, + _to: ::CrossAccountId, + _token: TokenId, + _amount: u128, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn transfer_from( + &self, + sender: ::CrossAccountId, + from: ::CrossAccountId, + to: ::CrossAccountId, + _token: TokenId, + amount: u128, + budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + >::transfer_from(self, &sender, &from, &to, amount, budget) + } + + fn burn_from( + &self, + _sender: ::CrossAccountId, + _from: ::CrossAccountId, + _token: TokenId, + _amount: u128, + _budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn check_nesting( + &self, + _sender: ::CrossAccountId, + _from: (up_data_structs::CollectionId, TokenId), + _under: TokenId, + _budget: &dyn up_data_structs::budget::Budget, + ) -> frame_support::sp_runtime::DispatchResult { + fail!(>::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: ::CrossAccountId) -> Vec { + let balance = >::total_balance(&account); + if balance != 0 { + vec![TokenId::default()] + } else { + vec![] + } + } + + fn collection_tokens(&self) -> Vec { + 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<::CrossAccountId, up_data_structs::TokenOwnerError> { + Err(up_data_structs::TokenOwnerError::MultipleOwners) + } + + fn token_owners(&self, _token: TokenId) -> Vec<::CrossAccountId> { + vec![] + } + + fn token_property( + &self, + _token_id: TokenId, + _key: &up_data_structs::PropertyKey, + ) -> Option { + None + } + + fn token_properties( + &self, + _token: TokenId, + _keys: Option>, + ) -> Vec { + vec![] + } + + fn total_supply(&self) -> u32 { + 1 + } + + fn account_balance(&self, account: T::CrossAccountId) -> u32 { + let balance = >::balance_of(&account); + (balance != 0).into() + } + + fn balance(&self, account: T::CrossAccountId, token: TokenId) -> u128 { + if token != TokenId::default() { + return 0; + } + >::balance_of(&account) + } + + fn total_pieces(&self, token: TokenId) -> Option { + if token != TokenId::default() { + return None; + } + Some(>::total_issuance()) + } + + fn allowance( + &self, + _sender: ::CrossAccountId, + _spender: ::CrossAccountId, + _token: TokenId, + ) -> u128 { + 0 + } + + fn refungible_extensions(&self) -> Option<&dyn pallet_common::RefungibleExtensions> { + None + } + + fn set_allowance_for_all( + &self, + _owner: ::CrossAccountId, + _operator: ::CrossAccountId, + _approve: bool, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } + + fn allowance_for_all( + &self, + _owner: ::CrossAccountId, + _operator: ::CrossAccountId, + ) -> bool { + false + } + + fn repair_item( + &self, + _token: TokenId, + ) -> frame_support::pallet_prelude::DispatchResultWithPostInfo { + fail!(>::UnsupportedOperation); + } +} --- /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 Contract for NativeFungibleHandle {...} +} + +#[solidity_interface(name = ERC20, enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x942e8b22)] +impl NativeFungibleHandle { + fn allowance(&self, _owner: Address, _spender: Address) -> Result { + Ok(U256::zero()) + } + + fn approve(&mut self, _caller: Caller, _spender: Address, _amount: U256) -> Result { + Err("Approve not supported".into()) + } + + fn balance_of(&self, owner: Address) -> Result { + self.consume_store_reads(1)?; + let owner = T::CrossAccountId::from_eth(owner); + let balance = >::balance_of(&owner); + Ok(balance.into()) + } + + fn decimals(&self) -> Result { + Ok(T::Decimals::get()) + } + + fn name(&self) -> Result { + Ok(T::Name::get()) + } + + fn symbol(&self) -> Result { + Ok(T::Symbol::get()) + } + + fn total_supply(&self) -> Result { + self.consume_store_reads(1)?; + Ok(>::total_issuance().into()) + } + + #[weight(>::transfer_allow_death())] + fn transfer(&mut self, caller: Caller, to: Address, amount: U256) -> Result { + 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(>::find_parent()); + + >::transfer(self, &caller, &to, amount, &budget) + .map_err(|e| dispatch_to_evm::(e.error))?; + Ok(true) + } + + #[weight(>::transfer_allow_death())] + fn transfer_from( + &mut self, + caller: Caller, + from: Address, + to: Address, + amount: U256, + ) -> Result { + 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(>::find_parent()); + + >::transfer_from(self, &caller, &from, &to, amount, &budget) + .map_err(|e| dispatch_to_evm::(e.error))?; + Ok(true) + } +} + +#[solidity_interface(name = ERC20UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))] +impl NativeFungibleHandle +where + T::AccountId: From<[u8; 32]>, +{ + fn balance_of_cross(&self, owner: CrossAddress) -> Result { + self.consume_store_reads(1)?; + let owner = owner.into_sub_cross_account::()?; + let balance = >::balance_of(&owner); + Ok(balance.into()) + } + + #[weight(>::transfer_allow_death())] + fn transfer_cross(&mut self, caller: Caller, to: CrossAddress, amount: U256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = to.into_sub_cross_account::()?; + let amount = amount.try_into().map_err(|_| "amount overflow")?; + let budget = self + .recorder() + .weight_calls_budget(>::find_parent()); + + >::transfer(self, &caller, &to, amount, &budget) + .map_err(|e| dispatch_to_evm::(e.error))?; + + Ok(true) + } + + #[weight(>::transfer_allow_death())] + fn transfer_from_cross( + &mut self, + caller: Caller, + from: CrossAddress, + to: CrossAddress, + amount: U256, + ) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let from = from.into_sub_cross_account::()?; + let to = to.into_sub_cross_account::()?; + let amount = amount.try_into().map_err(|_| "amount overflow")?; + + if from != caller { + return Err("no permission".into()); + } + + let budget = self + .recorder() + .weight_calls_budget(>::find_parent()); + + >::transfer_from(self, &caller, &from, &to, amount, &budget) + .map_err(|e| dispatch_to_evm::(e.error))?; + + Ok(true) + } +} + +#[solidity_interface( + name = UniqueNativeFungible, + is(ERC20, ERC20UniqueExtensions), + enum(derive(PreDispatch)) +)] +impl NativeFungibleHandle where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {} + +generate_stubgen!(gen_impl, UniqueNativeFungibleCall<()>, true); +generate_stubgen!(gen_iface, UniqueNativeFungibleCall<()>, false); + +impl CommonEvmHandler for NativeFungibleHandle +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 { + call::, _, _>(handle, self) + } +} --- /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 = ::WeightInfo; + +/// Handle for native fungible collection +pub struct NativeFungibleHandle(SubstrateRecorder); +impl NativeFungibleHandle { + /// Creates a handle + pub fn new() -> NativeFungibleHandle { + Self(SubstrateRecorder::new(u64::MAX)) + } + + /// Creates a handle + pub fn new_with_gas_limit(gas_limit: u64) -> NativeFungibleHandle { + Self(SubstrateRecorder::new(gas_limit)) + } + + /// Check if the collection is internal + pub fn check_is_internal(&self) -> DispatchResult { + Ok(()) + } +} + +impl WithRecorder for NativeFungibleHandle { + fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder { + &self.0 + } + fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder { + self.0 + } +} + +impl Deref for NativeFungibleHandle { + type Target = SubstrateRecorder; + + 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 + TryFrom + TryFrom + Into; + + /// Decimals of balance + type Decimals: Get; + /// Collection name + type Name: Get; + /// Collection symbol + type Symbol: Get; + + /// Weight information + type WeightInfo: WeightInfo; + } + #[pallet::pallet] + pub struct Pallet(_); + + impl Pallet { + 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 { + if let Some((collection_id, token_id)) = + T::CrossTokenAddressMapping::address_to_token(from) + { + ensure!( + >::check_indirectly_owned( + spender.clone(), + collection_id, + token_id, + None, + nesting_budget + )?, + >::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, + from: &T::CrossAccountId, + to: &T::CrossAccountId, + amount: u128, + _nesting_budget: &dyn Budget, + ) -> DispatchResultWithPostInfo { + >::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(>::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, + 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(>::ApprovedValueTooLow.into()); + } + Self::transfer(collection, from, to, amount, nesting_budget) + } + } +} --- /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 {} --- 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::()), - 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::()), @@ -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 { + /// 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) -> DispatchResult; + fn destroy(sender: T::CrossAccountId, collection_id: CollectionId) -> DispatchResult; /// Get a specialized collection from the handle. /// /// * `handle` - Collection handle. - fn dispatch(handle: CollectionHandle) -> Self; - - /// Get the collection handle for the corresponding implementation. - fn into_inner(self) -> CollectionHandle; + fn dispatch(collection_id: CollectionId) -> Result + where + Self: Sized; /// Get the implementation of [`CommonCollectionOperations`]. fn as_dyn(&self) -> &dyn CommonCollectionOperations; --- 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 CollectionHandle { /// Same as [CollectionHandle::new] but with an explicit gas limit. pub fn new_with_gas_limit(id: CollectionId, gas_limit: u64) -> Option { - >::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( - ::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( - ::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 = ::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)] --- 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( + ::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( + ::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 = ::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(err: DispatchError) -> execution::Error { --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -109,7 +109,8 @@ .recorder .weight_calls_budget(>::find_parent()); - >::transfer(self, &caller, &to, amount, &budget).map_err(|_| "transfer error")?; + >::transfer(self, &caller, &to, amount, &budget) + .map_err(|e| dispatch_to_evm::(e.error))?; Ok(true) } --- 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 } --- 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)) { - >::insert((under.0, under.1, (to_nest.0, to_nest.1)), true); + if to_nest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + >::insert((under.0, under.1, to_nest), true); + } } fn unnest(under: (CollectionId, TokenId), to_unnest: (CollectionId, TokenId)) { - >::remove((under.0, under.1, to_unnest)); + if to_unnest.0 != pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + >::remove((under.0, under.1, to_unnest)); + } } fn collection_has_tokens(collection_id: CollectionId) -> bool { --- 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)?; --- 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, 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 = >::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 = >::try_get(collection)?; - - let dispatch = T::CollectionDispatch::dispatch(handle); + let dispatch = T::CollectionDispatch::dispatch(collection)?; let dispatch = dispatch.as_dyn(); action(dispatch, token) --- 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 } --- 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!(>::UnsupportedOperation); + } + let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let collection = >::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!(>::UnsupportedOperation); + } + let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let collection = >::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!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let new_owner = T::CrossAccountId::from_sub(new_owner); let mut target_collection = >::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!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let collection = >::try_get(collection_id)?; >::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!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let collection = >::try_get(collection_id)?; >::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!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let mut target_collection = >::try_get(collection_id)?; target_collection.set_sponsor(&sender, new_sponsor.clone()) @@ -597,6 +617,9 @@ origin: OriginFor, collection_id: CollectionId, ) -> DispatchResult { + if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + fail!(>::UnsupportedOperation); + } let sender = ensure_signed(origin)?; let mut target_collection = >::try_get(collection_id)?; target_collection.confirm_sponsorship(&sender) @@ -617,6 +640,9 @@ origin: OriginFor, collection_id: CollectionId, ) -> DispatchResult { + if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + fail!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let mut target_collection = >::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!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let mut target_collection = >::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!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let mut target_collection = >::try_get(collection_id)?; >::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!(>::UnsupportedOperation); + } let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let mut target_collection = >::try_get(collection_id)?; >::update_permissions(&sender, &mut target_collection, new_permission) @@ -1238,6 +1273,9 @@ origin: OriginFor, collection_id: CollectionId, ) -> DispatchResult { + if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + fail!(>::UnsupportedOperation); + } ensure_root(origin)?; >::repair_collection(collection_id) } @@ -1296,10 +1334,7 @@ sender: T::CrossAccountId, collection_id: CollectionId, ) -> DispatchResult { - let collection = >::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? --- 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 . +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 = >::Balance; + type Decimals = Decimals; + type Name = Name; + type Symbol = Symbol; + type WeightInfo = pallet_balances::weights::SubstrateWeight; +} + +parameter_types! { pub const InflationBlockInterval: BlockNumber = 100; // every time per how many blocks inflation is applied } --- 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, } --- 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 . -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 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), Nonfungible(NonfungibleHandle), Refungible(RefungibleHandle), + NativeFungible(NativeFungibleHandle), } + impl CollectionDispatch for CollectionDispatchT 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) -> DispatchResult { + fn destroy(sender: T::CrossAccountId, collection_id: CollectionId) -> DispatchResult { + if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + fail!(>::UnsupportedOperation); + } + + let collection = >::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) -> Self { - match handle.mode { + fn dispatch(collection_id: CollectionId) -> Result { + if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + return Ok(Self::NativeFungible( + NativeFungibleHandle::new_with_gas_limit(u64::MAX), + )); + } + + let handle = >::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 { - 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 { @@ -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 { if let Some(collection_id) = map_eth_to_id(&handle.code_address()) { - let collection = - >::new_with_gas_limit(collection_id, handle.remaining_gas())?; - let dispatched = Self::dispatch(collection); + if collection_id == pallet_common::NATIVE_FUNGIBLE_COLLECTION_ID { + >::new_with_gas_limit(handle.remaining_gas()).call(handle) + } else { + let collection = >::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)) = ::EvmTokenAddressMapping::address_to_token( --- 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 = ::CollectionDispatch::dispatch(>::try_get($collection)?); + let collection = ::CollectionDispatch::dispatch($collection)?; let dispatch = collection.as_dyn(); Ok::<_, DispatchError>(dispatch.$method($($name),*) $($rest)*) --- 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 = >::$method($($args)*) + .max(>::$method($($args)*)) .max(>::$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 CommonWeightConfigs for T {} +impl CommonWeightConfigs for T {} #[cfg(feature = "refungible")] -pub trait CommonWeightConfigs: FungibleConfig + NonfungibleConfig + RefungibleConfig {} +pub trait CommonWeightConfigs: + FungibleConfig + NativeFungibleConfig + NonfungibleConfig + RefungibleConfig +{ +} #[cfg(feature = "refungible")] -impl CommonWeightConfigs for T {} +impl + CommonWeightConfigs for T +{ +} pub struct CommonWeights(PhantomData); --- 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 --- 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 { --- 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 } ################################################################################ --- 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 { --- 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 } --- 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 = >::Balance; + type Decimals = Decimals; + type Name = Name; + type Symbol = Symbol; + type WeightInfo = (); +} parameter_types! { // 0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f --- 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 --- 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 { --- 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", { --- 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", --- 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/); --- 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/); }); --- 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 +} --- 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/); }); --- 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/); }); --- 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); --- /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" + } +] --- /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 {} --- 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; --- 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() { --- 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; --- 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; --- 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; --- 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() { --- /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 . + +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 --- /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 . + +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); + }); +}); --- 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; + } + }); + }); + }); }); --- 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 = [{ --- 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); --- 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 = [{ --- 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, { --- 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 { + 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 { --- 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', [ --- /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 . + +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 --- 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); --- 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', --- 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/); --- 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/); }); --- 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, --- 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/); }); --- 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/); }); --- a/tests/src/sub/appPromotion/appPromotion.test.ts +++ b/tests/src/sub/appPromotion/appPromotion.test.ts @@ -157,7 +157,8 @@ await helper.staking.stake(staker, 1000n * nominal); await helper.staking.stake(staker, 199n * nominal); // check balances - expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}, {id: 'appstake', amount: 1199n * nominal, reasons: 'All'}]); + expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'ormlvest', amount: 200n * nominal, reasons: 'All'}]); + expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([{id: 'appstakeappstake', amount: 1199n * nominal}]); expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 1199n * nominal}); expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(1199n); expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(1199n * nominal); @@ -453,7 +454,7 @@ const [_unstake1, unstake2] = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address}); await helper.wait.forParachainBlockNumber(unstake2.block); - expect(await helper.balance.getLocked(staker.address)).to.deep.eq([]); + expect(await helper.balance.getFrozen(staker.address)).to.deep.eq([]); expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, frozen: 0n}); expect(await helper.balance.getSubstrate(staker.address) / nominal).to.eq(999n); expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.eq(0n); --- 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()); }); }); - --- 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); + }); }); --- 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([ --- 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); + }); }); --- 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}); + }); }); --- 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); }); - }); + })); }); --- 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/); }); --- 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/); }); --- 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; --- 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 = P extends `${infer Key}.${infer Key2}` ? Key extends keyof T ? Key2 extends keyof T[Key] ? T[Key][Key2] : E : E : E; type ForceFunction = T extends (...args: any) => any ? T : (...args: any) => Invalid<'not a function'>; -type ReturnTypeWithArgs 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 { + 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 { + 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); --- 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}; -- gitstuff