git.delta.rocks / unique-network / refs/commits / 509c8280fa0c

difftreelog

Merge pull request #426 from UniqueNetwork/feature/token_owners

Yaroslav Bolyukin2022-07-22parents: #c2a34ed #10fad85.patch.diff
in: master

31 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5953,7 +5953,7 @@
 
 [[package]]
 name = "pallet-fungible"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -6198,7 +6198,7 @@
 
 [[package]]
 name = "pallet-nonfungible"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -12377,7 +12377,7 @@
 
 [[package]]
 name = "uc-rpc"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "anyhow",
  "jsonrpsee",
@@ -12682,7 +12682,7 @@
 
 [[package]]
 name = "unique-runtime-common"
-version = "0.9.24"
+version = "0.9.25"
 dependencies = [
  "evm-coder",
  "fp-rpc",
@@ -12754,7 +12754,7 @@
 
 [[package]]
 name = "up-rpc"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "pallet-common",
  "pallet-evm",
addedclient/rpc/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/client/rpc/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.1] - 2022-07-14
+
+### Added
+
+ - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
+    This was an internal request to improve the web interface and support fractionalization event. 
+ 
\ No newline at end of file
modifiedclient/rpc/Cargo.tomldiffbeforeafterboth
--- a/client/rpc/Cargo.toml
+++ b/client/rpc/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "uc-rpc"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedclient/rpc/src/lib.rsdiffbeforeafterboth
--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -70,6 +70,16 @@
 		token: TokenId,
 		at: Option<BlockHash>,
 	) -> Result<Option<CrossAccountId>>;
+
+	/// Returns 10 tokens owners in no particular order.
+	#[method(name = "unique_tokenOwners")]
+	fn token_owners(
+		&self,
+		collection: CollectionId,
+		token: TokenId,
+		at: Option<BlockHash>,
+	) -> Result<Vec<CrossAccountId>>;
+
 	#[method(name = "unique_topmostTokenOwner")]
 	fn topmost_token_owner(
 		&self,
@@ -473,6 +483,7 @@
 	pass_method!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option<u64>, unique_api);
 	pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option<CollectionLimits>, unique_api);
 	pass_method!(total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128>, unique_api);
+	pass_method!(token_owners(collection: CollectionId, token: TokenId) -> Vec<CrossAccountId>, unique_api);
 }
 
 #[allow(deprecated)]
addedpallets/common/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/common/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.1] - 2022-07-14
+
+### Added
+
+ - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
+    This was an internal request to improve the web interface and support fractionalization event. 
+
+ 
\ No newline at end of file
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -1748,6 +1748,11 @@
 	/// * `token` - The token for which you need to find out the owner.
 	fn token_owner(&self, token: TokenId) -> Option<T::CrossAccountId>;
 
+	/// Returns 10 tokens owners in no particular order.
+	///
+	/// * `token` - The token for which you need to find out the owners.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId>;
+
 	/// Get the value of the token property by key.
 	///
 	/// * `token` - Token with the property to get.
addedpallets/fungible/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/fungible/CHANGELOG.md
@@ -0,0 +1,13 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.1] - 2022-07-14
+
+### Added
+
+ - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
+    This was an internal request to improve the web interface and support fractionalization event. 
+    
+ 
+ 
\ No newline at end of file
modifiedpallets/fungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/fungible/Cargo.toml
+++ b/pallets/fungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-fungible"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/fungible/src/common.rsdiffbeforeafterboth
--- a/pallets/fungible/src/common.rs
+++ b/pallets/fungible/src/common.rs
@@ -362,6 +362,11 @@
 		None
 	}
 
+	/// Returns 10 tokens owners in no particular order.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId> {
+		<Pallet<T>>::token_owners(self.id, token).unwrap_or_default()
+	}
+
 	fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option<PropertyValue> {
 		None
 	}
modifiedpallets/fungible/src/lib.rsdiffbeforeafterboth
before · pallets/fungible/src/lib.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Fungible Pallet18//!19//! The Fungible pallet provides functionality for dealing with fungible assets.20//!21//! - [`CreateItemData`]22//! - [`Config`]23//! - [`FungibleHandle`]24//! - [`Pallet`]25//! - [`TotalSupply`]26//! - [`Balance`]27//! - [`Allowance`]28//! - [`Error`]29//!30//! ## Fungible tokens31//!32//! Fungible tokens or assets are divisible and non-unique. For instance,33//! fiat currencies like the dollar are fungible: A $1 bill34//! in New York City has the same value as a $1 bill in Miami.35//! A fungible token can also be a cryptocurrency like Bitcoin: 1 BTC is worth 1 BTC,36//! no matter where it is issued. Thus, the fungibility refers to a specific currency’s37//! ability to maintain one standard value. As well, it needs to have uniform acceptance.38//! This means that a currency’s history should not be able to affect its value,39//! and this is due to the fact that each piece that is a part of the currency is equal40//! in value when compared to every other piece of that exact same currency.41//! In the world of cryptocurrencies, this is essentially a coin or a token42//! that can be replaced by another identical coin or token, and they are43//! both mutually interchangeable. A popular implementation of fungible tokens is44//! the ERC-20 token standard.45//!46//! ### ERC-2047//!48//! The [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) (Ethereum Request for Comments 20), proposed by Fabian Vogelsteller in November 2015,49//! is a Token Standard that implements an API for tokens within Smart Contracts.50//!51//! Example functionalities ERC-20 provides:52//!53//! * transfer tokens from one account to another54//! * get the current token balance of an account55//! * get the total supply of the token available on the network56//! * approve whether an amount of token from an account can be spent by a third-party account57//!58//! ## Overview59//!60//! The module provides functionality for asset management of fungible asset, supports ERC-20 standart, includes:61//!62//! * Asset Issuance63//! * Asset Transferal64//! * Asset Destruction65//! * Delegated Asset Transfers66//!67//! **NOTE:** The created fungible asset always has `token_id` = 0.68//! So `tokenA` and `tokenB` will have different `collection_id`.69//!70//! ### Implementations71//!72//! The Fungible pallet provides implementations for the following traits.73//!74//! - [`WithRecorder`](pallet_evm_coder_substrate::WithRecorder): Trait for EVM support75//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing with collections76//!	- [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight77//! - [`CommonEvmHandler`](pallet_common::erc::CommonEvmHandler): Function for handling EVM runtime calls7879#![cfg_attr(not(feature = "std"), no_std)]8081use core::ops::Deref;82use evm_coder::ToLog;83use frame_support::{ensure};84use pallet_evm::account::CrossAccountId;85use up_data_structs::{86	AccessMode, CollectionId, TokenId, CreateCollectionData, mapping::TokenAddressMapping,87	budget::Budget,88};89use pallet_common::{90	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,91	eth::collection_id_to_address,92};93use pallet_evm::Pallet as PalletEvm;94use pallet_structure::Pallet as PalletStructure;95use pallet_evm_coder_substrate::WithRecorder;96use sp_core::H160;97use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};98use sp_std::{collections::btree_map::BTreeMap};99100pub use pallet::*;101102use crate::erc::ERC20Events;103#[cfg(feature = "runtime-benchmarks")]104pub mod benchmarking;105pub mod common;106pub mod erc;107pub mod weights;108109pub type CreateItemData<T> = (<T as pallet_evm::account::Config>::CrossAccountId, u128);110pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;111112#[frame_support::pallet]113pub mod pallet {114	use frame_support::{Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};115	use up_data_structs::CollectionId;116	use super::weights::WeightInfo;117118	#[pallet::error]119	pub enum Error<T> {120		/// Not Fungible item data used to mint in Fungible collection.121		NotFungibleDataUsedToMintFungibleCollectionToken,122		/// Not default id passed as TokenId argument.123		/// The default value of TokenId for Fungible collection is 0.124		FungibleItemsHaveNoId,125		/// Tried to set data for fungible item.126		FungibleItemsDontHaveData,127		/// Fungible token does not support nesting.128		FungibleDisallowsNesting,129		/// Setting item properties is not allowed.130		SettingPropertiesNotAllowed,131	}132133	#[pallet::config]134	pub trait Config:135		frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config136	{137		type WeightInfo: WeightInfo;138	}139140	#[pallet::pallet]141	#[pallet::generate_store(pub(super) trait Store)]142	pub struct Pallet<T>(_);143144	/// Total amount of fungible tokens inside a collection.145	#[pallet::storage]146	pub type TotalSupply<T: Config> =147		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u128, QueryKind = ValueQuery>;148149	/// Amount of tokens owned by an account inside a collection.150	#[pallet::storage]151	pub type Balance<T: Config> = StorageNMap<152		Key = (153			Key<Twox64Concat, CollectionId>,154			Key<Blake2_128Concat, T::CrossAccountId>,155		),156		Value = u128,157		QueryKind = ValueQuery,158	>;159160	/// Storage for delegated assets.161	#[pallet::storage]162	pub type Allowance<T: Config> = StorageNMap<163		Key = (164			Key<Twox64Concat, CollectionId>,165			Key<Blake2_128, T::CrossAccountId>,166			Key<Blake2_128Concat, T::CrossAccountId>,167		),168		Value = u128,169		QueryKind = ValueQuery,170	>;171}172173/// Wrapper around untyped collection handle, asserting inner collection is of fungible type.174/// Required for interaction with Fungible collections, type safety and implementation [`solidity_interface`][`evm_coder::solidity_interface`].175176pub struct FungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);177178/// Implementation of methods required for dispatching during runtime.179impl<T: Config> FungibleHandle<T> {180	/// Casts [`CollectionHandle`][`pallet_common::CollectionHandle`] into [`FungibleHandle`].181	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {182		Self(inner)183	}184185	/// Casts [`FungibleHandle`] into [`CollectionHandle`][`pallet_common::CollectionHandle`].186	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {187		self.0188	}189	/// Returns a mutable reference to the internal [`CollectionHandle`][`pallet_common::CollectionHandle`].190	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {191		&mut self.0192	}193}194impl<T: Config> WithRecorder<T> for FungibleHandle<T> {195	fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {196		self.0.recorder()197	}198	fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {199		self.0.into_recorder()200	}201}202impl<T: Config> Deref for FungibleHandle<T> {203	type Target = pallet_common::CollectionHandle<T>;204205	fn deref(&self) -> &Self::Target {206		&self.0207	}208}209210/// Pallet implementation for fungible assets211impl<T: Config> Pallet<T> {212	/// Initializes the collection. Returns [CollectionId] on success, [DispatchError] otherwise.213	pub fn init_collection(214		owner: T::CrossAccountId,215		data: CreateCollectionData<T::AccountId>,216	) -> Result<CollectionId, DispatchError> {217		<PalletCommon<T>>::init_collection(owner, data, false)218	}219220	/// Destroys a collection.221	pub fn destroy_collection(222		collection: FungibleHandle<T>,223		sender: &T::CrossAccountId,224	) -> DispatchResult {225		let id = collection.id;226227		if Self::collection_has_tokens(id) {228			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());229		}230231		// =========232233		PalletCommon::destroy_collection(collection.0, sender)?;234235		<TotalSupply<T>>::remove(id);236		<Balance<T>>::remove_prefix((id,), None);237		<Allowance<T>>::remove_prefix((id,), None);238		Ok(())239	}240241	///Checks if collection has tokens. Return `true` if it has.242	fn collection_has_tokens(collection_id: CollectionId) -> bool {243		<TotalSupply<T>>::get(collection_id) != 0244	}245246	/// Burns the specified amount of the token. If the token balance247	/// or total supply is less than the given value,248	/// it will return [DispatchError].249	pub fn burn(250		collection: &FungibleHandle<T>,251		owner: &T::CrossAccountId,252		amount: u128,253	) -> DispatchResult {254		let total_supply = <TotalSupply<T>>::get(collection.id)255			.checked_sub(amount)256			.ok_or(<CommonError<T>>::TokenValueTooLow)?;257258		let balance = <Balance<T>>::get((collection.id, owner))259			.checked_sub(amount)260			.ok_or(<CommonError<T>>::TokenValueTooLow)?;261262		if collection.permissions.access() == AccessMode::AllowList {263			collection.check_allowlist(owner)?;264		}265266		// =========267268		if balance == 0 {269			<Balance<T>>::remove((collection.id, owner));270			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, TokenId::default());271		} else {272			<Balance<T>>::insert((collection.id, owner), balance);273		}274		<TotalSupply<T>>::insert(collection.id, total_supply);275276		<PalletEvm<T>>::deposit_log(277			ERC20Events::Transfer {278				from: *owner.as_eth(),279				to: H160::default(),280				value: amount.into(),281			}282			.to_log(collection_id_to_address(collection.id)),283		);284		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(285			collection.id,286			TokenId::default(),287			owner.clone(),288			amount,289		));290		Ok(())291	}292293	/// Transfers the specified amount of tokens. Will check that294	/// the transfer is allowed for the token.295	///296	/// - `from`: Owner of tokens to transfer.297	/// - `to`: Recepient of transfered tokens.298	/// - `amount`: Amount of tokens to transfer.299	/// - `collection`: Collection that contains the token300	pub fn transfer(301		collection: &FungibleHandle<T>,302		from: &T::CrossAccountId,303		to: &T::CrossAccountId,304		amount: u128,305		nesting_budget: &dyn Budget,306	) -> DispatchResult {307		ensure!(308			collection.limits.transfers_enabled(),309			<CommonError<T>>::TransferNotAllowed,310		);311312		if collection.permissions.access() == AccessMode::AllowList {313			collection.check_allowlist(from)?;314			collection.check_allowlist(to)?;315		}316		<PalletCommon<T>>::ensure_correct_receiver(to)?;317318		let balance_from = <Balance<T>>::get((collection.id, from))319			.checked_sub(amount)320			.ok_or(<CommonError<T>>::TokenValueTooLow)?;321		let balance_to = if from != to {322			Some(323				<Balance<T>>::get((collection.id, to))324					.checked_add(amount)325					.ok_or(ArithmeticError::Overflow)?,326			)327		} else {328			None329		};330331		// =========332333		<PalletStructure<T>>::nest_if_sent_to_token(334			from.clone(),335			to,336			collection.id,337			TokenId::default(),338			nesting_budget,339		)?;340341		if let Some(balance_to) = balance_to {342			// from != to343			if balance_from == 0 {344				<Balance<T>>::remove((collection.id, from));345				<PalletStructure<T>>::unnest_if_nested(from, collection.id, TokenId::default());346			} else {347				<Balance<T>>::insert((collection.id, from), balance_from);348			}349			<Balance<T>>::insert((collection.id, to), balance_to);350		}351352		<PalletEvm<T>>::deposit_log(353			ERC20Events::Transfer {354				from: *from.as_eth(),355				to: *to.as_eth(),356				value: amount.into(),357			}358			.to_log(collection_id_to_address(collection.id)),359		);360		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(361			collection.id,362			TokenId::default(),363			from.clone(),364			to.clone(),365			amount,366		));367		Ok(())368	}369370	/// Minting tokens for multiple IDs.371	/// See [`create_item`][`Pallet::create_item`] for more details.372	pub fn create_multiple_items(373		collection: &FungibleHandle<T>,374		sender: &T::CrossAccountId,375		data: BTreeMap<T::CrossAccountId, u128>,376		nesting_budget: &dyn Budget,377	) -> DispatchResult {378		if !collection.is_owner_or_admin(sender) {379			ensure!(380				collection.permissions.mint_mode(),381				<CommonError<T>>::PublicMintingNotAllowed382			);383			collection.check_allowlist(sender)?;384385			for (owner, _) in data.iter() {386				collection.check_allowlist(owner)?;387			}388		}389390		let total_supply = data391			.iter()392			.map(|(_, v)| *v)393			.try_fold(<TotalSupply<T>>::get(collection.id), |acc, v| {394				acc.checked_add(v)395			})396			.ok_or(ArithmeticError::Overflow)?;397398		let mut balances = data;399		for (k, v) in balances.iter_mut() {400			*v = <Balance<T>>::get((collection.id, &k))401				.checked_add(*v)402				.ok_or(ArithmeticError::Overflow)?;403		}404405		for (to, _) in balances.iter() {406			<PalletStructure<T>>::check_nesting(407				sender.clone(),408				to,409				collection.id,410				TokenId::default(),411				nesting_budget,412			)?;413		}414415		// =========416417		<TotalSupply<T>>::insert(collection.id, total_supply);418		for (user, amount) in balances {419			<Balance<T>>::insert((collection.id, &user), amount);420			<PalletStructure<T>>::nest_if_sent_to_token_unchecked(421				&user,422				collection.id,423				TokenId::default(),424			);425			<PalletEvm<T>>::deposit_log(426				ERC20Events::Transfer {427					from: H160::default(),428					to: *user.as_eth(),429					value: amount.into(),430				}431				.to_log(collection_id_to_address(collection.id)),432			);433			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(434				collection.id,435				TokenId::default(),436				user.clone(),437				amount,438			));439		}440441		Ok(())442	}443444	fn set_allowance_unchecked(445		collection: &FungibleHandle<T>,446		owner: &T::CrossAccountId,447		spender: &T::CrossAccountId,448		amount: u128,449	) {450		if amount == 0 {451			<Allowance<T>>::remove((collection.id, owner, spender));452		} else {453			<Allowance<T>>::insert((collection.id, owner, spender), amount);454		}455456		<PalletEvm<T>>::deposit_log(457			ERC20Events::Approval {458				owner: *owner.as_eth(),459				spender: *spender.as_eth(),460				value: amount.into(),461			}462			.to_log(collection_id_to_address(collection.id)),463		);464		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(465			collection.id,466			TokenId(0),467			owner.clone(),468			spender.clone(),469			amount,470		));471	}472473	/// Set allowance for the spender to `transfer` or `burn` owner's tokens.474	///475	/// - `collection`: Collection that contains the token476	/// - `owner`: Owner of tokens that sets the allowance.477	/// - `spender`: Recipient of the allowance rights.478	/// - `amount`: Amount of tokens the spender is allowed to `transfer` or `burn`.479	pub fn set_allowance(480		collection: &FungibleHandle<T>,481		owner: &T::CrossAccountId,482		spender: &T::CrossAccountId,483		amount: u128,484	) -> DispatchResult {485		if collection.permissions.access() == AccessMode::AllowList {486			collection.check_allowlist(owner)?;487			collection.check_allowlist(spender)?;488		}489490		if <Balance<T>>::get((collection.id, owner)) < amount {491			ensure!(492				collection.ignores_owned_amount(owner),493				<CommonError<T>>::CantApproveMoreThanOwned494			);495		}496497		// =========498499		Self::set_allowance_unchecked(collection, owner, spender, amount);500		Ok(())501	}502503	/// Checks if a non-owner has (enough) allowance from the owner to perform operations on the tokens.504	/// Returns the expected remaining allowance - it should be set manually if the transaction proceeds.505	///506	/// - `collection`: Collection that contains the token.507	/// - `spender`: CrossAccountId who has the allowance rights.508	/// - `from`: The owner of the tokens who sets the allowance.509	/// - `amount`: Amount of tokens by which the allowance sholud be reduced.510	fn check_allowed(511		collection: &FungibleHandle<T>,512		spender: &T::CrossAccountId,513		from: &T::CrossAccountId,514		amount: u128,515		nesting_budget: &dyn Budget,516	) -> Result<Option<u128>, DispatchError> {517		if spender.conv_eq(from) {518			return Ok(None);519		}520		if collection.permissions.access() == AccessMode::AllowList {521			// `from`, `to` checked in [`transfer`]522			collection.check_allowlist(spender)?;523		}524		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {525			// TODO: should collection owner be allowed to perform this transfer?526			ensure!(527				<PalletStructure<T>>::check_indirectly_owned(528					spender.clone(),529					source.0,530					source.1,531					None,532					nesting_budget533				)?,534				<CommonError<T>>::ApprovedValueTooLow,535			);536			return Ok(None);537		}538		let allowance = <Allowance<T>>::get((collection.id, from, spender)).checked_sub(amount);539		if allowance.is_none() {540			ensure!(541				collection.ignores_allowance(spender),542				<CommonError<T>>::ApprovedValueTooLow543			);544		}545546		Ok(allowance)547	}548549	/// Transfer fungible tokens from one account to another.550	/// Same as the [`transfer`][`Pallet::transfer`] but spender doesn't needs to be an owner of the token pieces.551	/// The owner should set allowance for the spender to transfer pieces.552	///	See [`set_allowance`][`Pallet::set_allowance`] for more details.553554	pub fn transfer_from(555		collection: &FungibleHandle<T>,556		spender: &T::CrossAccountId,557		from: &T::CrossAccountId,558		to: &T::CrossAccountId,559		amount: u128,560		nesting_budget: &dyn Budget,561	) -> DispatchResult {562		let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?;563564		// =========565566		Self::transfer(collection, from, to, amount, nesting_budget)?;567		if let Some(allowance) = allowance {568			Self::set_allowance_unchecked(collection, from, spender, allowance);569		}570		Ok(())571	}572573	/// Burn fungible tokens from the account.574	///575	/// Same as the [`burn`][`Pallet::burn`] but spender doesn't need to be an owner of the tokens. The `from` should576	/// set allowance for the spender to burn tokens.577	/// See [`set_allowance`][`Pallet::set_allowance`] for more details.578	pub fn burn_from(579		collection: &FungibleHandle<T>,580		spender: &T::CrossAccountId,581		from: &T::CrossAccountId,582		amount: u128,583		nesting_budget: &dyn Budget,584	) -> DispatchResult {585		let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?;586587		// =========588589		Self::burn(collection, from, amount)?;590		if let Some(allowance) = allowance {591			Self::set_allowance_unchecked(collection, from, spender, allowance);592		}593		Ok(())594	}595596	///	Creates fungible token.597	///598	/// The sender should be the owner/admin of the collection or collection should be configured599	/// to allow public minting.600	///601	/// - `data`: Contains user who will become the owners of the tokens and amount602	///   of tokens he will receive.603	pub fn create_item(604		collection: &FungibleHandle<T>,605		sender: &T::CrossAccountId,606		data: CreateItemData<T>,607		nesting_budget: &dyn Budget,608	) -> DispatchResult {609		Self::create_multiple_items(610			collection,611			sender,612			[(data.0, data.1)].into_iter().collect(),613			nesting_budget,614		)615	}616}
after · pallets/fungible/src/lib.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Fungible Pallet18//!19//! The Fungible pallet provides functionality for dealing with fungible assets.20//!21//! - [`CreateItemData`]22//! - [`Config`]23//! - [`FungibleHandle`]24//! - [`Pallet`]25//! - [`TotalSupply`]26//! - [`Balance`]27//! - [`Allowance`]28//! - [`Error`]29//!30//! ## Fungible tokens31//!32//! Fungible tokens or assets are divisible and non-unique. For instance,33//! fiat currencies like the dollar are fungible: A $1 bill34//! in New York City has the same value as a $1 bill in Miami.35//! A fungible token can also be a cryptocurrency like Bitcoin: 1 BTC is worth 1 BTC,36//! no matter where it is issued. Thus, the fungibility refers to a specific currency’s37//! ability to maintain one standard value. As well, it needs to have uniform acceptance.38//! This means that a currency’s history should not be able to affect its value,39//! and this is due to the fact that each piece that is a part of the currency is equal40//! in value when compared to every other piece of that exact same currency.41//! In the world of cryptocurrencies, this is essentially a coin or a token42//! that can be replaced by another identical coin or token, and they are43//! both mutually interchangeable. A popular implementation of fungible tokens is44//! the ERC-20 token standard.45//!46//! ### ERC-2047//!48//! The [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) (Ethereum Request for Comments 20), proposed by Fabian Vogelsteller in November 2015,49//! is a Token Standard that implements an API for tokens within Smart Contracts.50//!51//! Example functionalities ERC-20 provides:52//!53//! * transfer tokens from one account to another54//! * get the current token balance of an account55//! * get the total supply of the token available on the network56//! * approve whether an amount of token from an account can be spent by a third-party account57//!58//! ## Overview59//!60//! The module provides functionality for asset management of fungible asset, supports ERC-20 standart, includes:61//!62//! * Asset Issuance63//! * Asset Transferal64//! * Asset Destruction65//! * Delegated Asset Transfers66//!67//! **NOTE:** The created fungible asset always has `token_id` = 0.68//! So `tokenA` and `tokenB` will have different `collection_id`.69//!70//! ### Implementations71//!72//! The Fungible pallet provides implementations for the following traits.73//!74//! - [`WithRecorder`](pallet_evm_coder_substrate::WithRecorder): Trait for EVM support75//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing with collections76//!	- [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight77//! - [`CommonEvmHandler`](pallet_common::erc::CommonEvmHandler): Function for handling EVM runtime calls7879#![cfg_attr(not(feature = "std"), no_std)]8081use core::ops::Deref;82use evm_coder::ToLog;83use frame_support::{ensure};84use pallet_evm::account::CrossAccountId;85use up_data_structs::{86	AccessMode, CollectionId, TokenId, CreateCollectionData, mapping::TokenAddressMapping,87	budget::Budget,88};89use pallet_common::{90	Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,91	eth::collection_id_to_address,92};93use pallet_evm::Pallet as PalletEvm;94use pallet_structure::Pallet as PalletStructure;95use pallet_evm_coder_substrate::WithRecorder;96use sp_core::H160;97use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};98use sp_std::{collections::btree_map::BTreeMap, vec::Vec};99100pub use pallet::*;101102use crate::erc::ERC20Events;103#[cfg(feature = "runtime-benchmarks")]104pub mod benchmarking;105pub mod common;106pub mod erc;107pub mod weights;108109pub type CreateItemData<T> = (<T as pallet_evm::account::Config>::CrossAccountId, u128);110pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;111112#[frame_support::pallet]113pub mod pallet {114	use frame_support::{Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};115	use up_data_structs::CollectionId;116	use super::weights::WeightInfo;117118	#[pallet::error]119	pub enum Error<T> {120		/// Not Fungible item data used to mint in Fungible collection.121		NotFungibleDataUsedToMintFungibleCollectionToken,122		/// Not default id passed as TokenId argument.123		/// The default value of TokenId for Fungible collection is 0.124		FungibleItemsHaveNoId,125		/// Tried to set data for fungible item.126		FungibleItemsDontHaveData,127		/// Fungible token does not support nesting.128		FungibleDisallowsNesting,129		/// Setting item properties is not allowed.130		SettingPropertiesNotAllowed,131	}132133	#[pallet::config]134	pub trait Config:135		frame_system::Config + pallet_common::Config + pallet_structure::Config + pallet_evm::Config136	{137		type WeightInfo: WeightInfo;138	}139140	#[pallet::pallet]141	#[pallet::generate_store(pub(super) trait Store)]142	pub struct Pallet<T>(_);143144	/// Total amount of fungible tokens inside a collection.145	#[pallet::storage]146	pub type TotalSupply<T: Config> =147		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u128, QueryKind = ValueQuery>;148149	/// Amount of tokens owned by an account inside a collection.150	#[pallet::storage]151	pub type Balance<T: Config> = StorageNMap<152		Key = (153			Key<Twox64Concat, CollectionId>,154			Key<Blake2_128Concat, T::CrossAccountId>,155		),156		Value = u128,157		QueryKind = ValueQuery,158	>;159160	/// Storage for delegated assets.161	#[pallet::storage]162	pub type Allowance<T: Config> = StorageNMap<163		Key = (164			Key<Twox64Concat, CollectionId>,165			Key<Blake2_128, T::CrossAccountId>,166			Key<Blake2_128Concat, T::CrossAccountId>,167		),168		Value = u128,169		QueryKind = ValueQuery,170	>;171}172173/// Wrapper around untyped collection handle, asserting inner collection is of fungible type.174/// Required for interaction with Fungible collections, type safety and implementation [`solidity_interface`][`evm_coder::solidity_interface`].175176pub struct FungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);177178/// Implementation of methods required for dispatching during runtime.179impl<T: Config> FungibleHandle<T> {180	/// Casts [`CollectionHandle`][`pallet_common::CollectionHandle`] into [`FungibleHandle`].181	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {182		Self(inner)183	}184185	/// Casts [`FungibleHandle`] into [`CollectionHandle`][`pallet_common::CollectionHandle`].186	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {187		self.0188	}189	/// Returns a mutable reference to the internal [`CollectionHandle`][`pallet_common::CollectionHandle`].190	pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {191		&mut self.0192	}193}194impl<T: Config> WithRecorder<T> for FungibleHandle<T> {195	fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {196		self.0.recorder()197	}198	fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {199		self.0.into_recorder()200	}201}202impl<T: Config> Deref for FungibleHandle<T> {203	type Target = pallet_common::CollectionHandle<T>;204205	fn deref(&self) -> &Self::Target {206		&self.0207	}208}209210/// Pallet implementation for fungible assets211impl<T: Config> Pallet<T> {212	/// Initializes the collection. Returns [CollectionId] on success, [DispatchError] otherwise.213	pub fn init_collection(214		owner: T::CrossAccountId,215		data: CreateCollectionData<T::AccountId>,216	) -> Result<CollectionId, DispatchError> {217		<PalletCommon<T>>::init_collection(owner, data, false)218	}219220	/// Destroys a collection.221	pub fn destroy_collection(222		collection: FungibleHandle<T>,223		sender: &T::CrossAccountId,224	) -> DispatchResult {225		let id = collection.id;226227		if Self::collection_has_tokens(id) {228			return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());229		}230231		// =========232233		PalletCommon::destroy_collection(collection.0, sender)?;234235		<TotalSupply<T>>::remove(id);236		<Balance<T>>::remove_prefix((id,), None);237		<Allowance<T>>::remove_prefix((id,), None);238		Ok(())239	}240241	///Checks if collection has tokens. Return `true` if it has.242	fn collection_has_tokens(collection_id: CollectionId) -> bool {243		<TotalSupply<T>>::get(collection_id) != 0244	}245246	/// Burns the specified amount of the token. If the token balance247	/// or total supply is less than the given value,248	/// it will return [DispatchError].249	pub fn burn(250		collection: &FungibleHandle<T>,251		owner: &T::CrossAccountId,252		amount: u128,253	) -> DispatchResult {254		let total_supply = <TotalSupply<T>>::get(collection.id)255			.checked_sub(amount)256			.ok_or(<CommonError<T>>::TokenValueTooLow)?;257258		let balance = <Balance<T>>::get((collection.id, owner))259			.checked_sub(amount)260			.ok_or(<CommonError<T>>::TokenValueTooLow)?;261262		if collection.permissions.access() == AccessMode::AllowList {263			collection.check_allowlist(owner)?;264		}265266		// =========267268		if balance == 0 {269			<Balance<T>>::remove((collection.id, owner));270			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, TokenId::default());271		} else {272			<Balance<T>>::insert((collection.id, owner), balance);273		}274		<TotalSupply<T>>::insert(collection.id, total_supply);275276		<PalletEvm<T>>::deposit_log(277			ERC20Events::Transfer {278				from: *owner.as_eth(),279				to: H160::default(),280				value: amount.into(),281			}282			.to_log(collection_id_to_address(collection.id)),283		);284		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(285			collection.id,286			TokenId::default(),287			owner.clone(),288			amount,289		));290		Ok(())291	}292293	/// Transfers the specified amount of tokens. Will check that294	/// the transfer is allowed for the token.295	///296	/// - `from`: Owner of tokens to transfer.297	/// - `to`: Recepient of transfered tokens.298	/// - `amount`: Amount of tokens to transfer.299	/// - `collection`: Collection that contains the token300	pub fn transfer(301		collection: &FungibleHandle<T>,302		from: &T::CrossAccountId,303		to: &T::CrossAccountId,304		amount: u128,305		nesting_budget: &dyn Budget,306	) -> DispatchResult {307		ensure!(308			collection.limits.transfers_enabled(),309			<CommonError<T>>::TransferNotAllowed,310		);311312		if collection.permissions.access() == AccessMode::AllowList {313			collection.check_allowlist(from)?;314			collection.check_allowlist(to)?;315		}316		<PalletCommon<T>>::ensure_correct_receiver(to)?;317318		let balance_from = <Balance<T>>::get((collection.id, from))319			.checked_sub(amount)320			.ok_or(<CommonError<T>>::TokenValueTooLow)?;321		let balance_to = if from != to {322			Some(323				<Balance<T>>::get((collection.id, to))324					.checked_add(amount)325					.ok_or(ArithmeticError::Overflow)?,326			)327		} else {328			None329		};330331		// =========332333		<PalletStructure<T>>::nest_if_sent_to_token(334			from.clone(),335			to,336			collection.id,337			TokenId::default(),338			nesting_budget,339		)?;340341		if let Some(balance_to) = balance_to {342			// from != to343			if balance_from == 0 {344				<Balance<T>>::remove((collection.id, from));345				<PalletStructure<T>>::unnest_if_nested(from, collection.id, TokenId::default());346			} else {347				<Balance<T>>::insert((collection.id, from), balance_from);348			}349			<Balance<T>>::insert((collection.id, to), balance_to);350		}351352		<PalletEvm<T>>::deposit_log(353			ERC20Events::Transfer {354				from: *from.as_eth(),355				to: *to.as_eth(),356				value: amount.into(),357			}358			.to_log(collection_id_to_address(collection.id)),359		);360		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(361			collection.id,362			TokenId::default(),363			from.clone(),364			to.clone(),365			amount,366		));367		Ok(())368	}369370	/// Minting tokens for multiple IDs.371	/// See [`create_item`][`Pallet::create_item`] for more details.372	pub fn create_multiple_items(373		collection: &FungibleHandle<T>,374		sender: &T::CrossAccountId,375		data: BTreeMap<T::CrossAccountId, u128>,376		nesting_budget: &dyn Budget,377	) -> DispatchResult {378		if !collection.is_owner_or_admin(sender) {379			ensure!(380				collection.permissions.mint_mode(),381				<CommonError<T>>::PublicMintingNotAllowed382			);383			collection.check_allowlist(sender)?;384385			for (owner, _) in data.iter() {386				collection.check_allowlist(owner)?;387			}388		}389390		let total_supply = data391			.iter()392			.map(|(_, v)| *v)393			.try_fold(<TotalSupply<T>>::get(collection.id), |acc, v| {394				acc.checked_add(v)395			})396			.ok_or(ArithmeticError::Overflow)?;397398		let mut balances = data;399		for (k, v) in balances.iter_mut() {400			*v = <Balance<T>>::get((collection.id, &k))401				.checked_add(*v)402				.ok_or(ArithmeticError::Overflow)?;403		}404405		for (to, _) in balances.iter() {406			<PalletStructure<T>>::check_nesting(407				sender.clone(),408				to,409				collection.id,410				TokenId::default(),411				nesting_budget,412			)?;413		}414415		// =========416417		<TotalSupply<T>>::insert(collection.id, total_supply);418		for (user, amount) in balances {419			<Balance<T>>::insert((collection.id, &user), amount);420			<PalletStructure<T>>::nest_if_sent_to_token_unchecked(421				&user,422				collection.id,423				TokenId::default(),424			);425			<PalletEvm<T>>::deposit_log(426				ERC20Events::Transfer {427					from: H160::default(),428					to: *user.as_eth(),429					value: amount.into(),430				}431				.to_log(collection_id_to_address(collection.id)),432			);433			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(434				collection.id,435				TokenId::default(),436				user.clone(),437				amount,438			));439		}440441		Ok(())442	}443444	fn set_allowance_unchecked(445		collection: &FungibleHandle<T>,446		owner: &T::CrossAccountId,447		spender: &T::CrossAccountId,448		amount: u128,449	) {450		if amount == 0 {451			<Allowance<T>>::remove((collection.id, owner, spender));452		} else {453			<Allowance<T>>::insert((collection.id, owner, spender), amount);454		}455456		<PalletEvm<T>>::deposit_log(457			ERC20Events::Approval {458				owner: *owner.as_eth(),459				spender: *spender.as_eth(),460				value: amount.into(),461			}462			.to_log(collection_id_to_address(collection.id)),463		);464		<PalletCommon<T>>::deposit_event(CommonEvent::Approved(465			collection.id,466			TokenId(0),467			owner.clone(),468			spender.clone(),469			amount,470		));471	}472473	/// Set allowance for the spender to `transfer` or `burn` owner's tokens.474	///475	/// - `collection`: Collection that contains the token476	/// - `owner`: Owner of tokens that sets the allowance.477	/// - `spender`: Recipient of the allowance rights.478	/// - `amount`: Amount of tokens the spender is allowed to `transfer` or `burn`.479	pub fn set_allowance(480		collection: &FungibleHandle<T>,481		owner: &T::CrossAccountId,482		spender: &T::CrossAccountId,483		amount: u128,484	) -> DispatchResult {485		if collection.permissions.access() == AccessMode::AllowList {486			collection.check_allowlist(owner)?;487			collection.check_allowlist(spender)?;488		}489490		if <Balance<T>>::get((collection.id, owner)) < amount {491			ensure!(492				collection.ignores_owned_amount(owner),493				<CommonError<T>>::CantApproveMoreThanOwned494			);495		}496497		// =========498499		Self::set_allowance_unchecked(collection, owner, spender, amount);500		Ok(())501	}502503	/// Checks if a non-owner has (enough) allowance from the owner to perform operations on the tokens.504	/// Returns the expected remaining allowance - it should be set manually if the transaction proceeds.505	///506	/// - `collection`: Collection that contains the token.507	/// - `spender`: CrossAccountId who has the allowance rights.508	/// - `from`: The owner of the tokens who sets the allowance.509	/// - `amount`: Amount of tokens by which the allowance sholud be reduced.510	fn check_allowed(511		collection: &FungibleHandle<T>,512		spender: &T::CrossAccountId,513		from: &T::CrossAccountId,514		amount: u128,515		nesting_budget: &dyn Budget,516	) -> Result<Option<u128>, DispatchError> {517		if spender.conv_eq(from) {518			return Ok(None);519		}520		if collection.permissions.access() == AccessMode::AllowList {521			// `from`, `to` checked in [`transfer`]522			collection.check_allowlist(spender)?;523		}524		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {525			// TODO: should collection owner be allowed to perform this transfer?526			ensure!(527				<PalletStructure<T>>::check_indirectly_owned(528					spender.clone(),529					source.0,530					source.1,531					None,532					nesting_budget533				)?,534				<CommonError<T>>::ApprovedValueTooLow,535			);536			return Ok(None);537		}538		let allowance = <Allowance<T>>::get((collection.id, from, spender)).checked_sub(amount);539		if allowance.is_none() {540			ensure!(541				collection.ignores_allowance(spender),542				<CommonError<T>>::ApprovedValueTooLow543			);544		}545546		Ok(allowance)547	}548549	/// Transfer fungible tokens from one account to another.550	/// Same as the [`transfer`][`Pallet::transfer`] but spender doesn't needs to be an owner of the token pieces.551	/// The owner should set allowance for the spender to transfer pieces.552	///	See [`set_allowance`][`Pallet::set_allowance`] for more details.553554	pub fn transfer_from(555		collection: &FungibleHandle<T>,556		spender: &T::CrossAccountId,557		from: &T::CrossAccountId,558		to: &T::CrossAccountId,559		amount: u128,560		nesting_budget: &dyn Budget,561	) -> DispatchResult {562		let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?;563564		// =========565566		Self::transfer(collection, from, to, amount, nesting_budget)?;567		if let Some(allowance) = allowance {568			Self::set_allowance_unchecked(collection, from, spender, allowance);569		}570		Ok(())571	}572573	/// Burn fungible tokens from the account.574	///575	/// Same as the [`burn`][`Pallet::burn`] but spender doesn't need to be an owner of the tokens. The `from` should576	/// set allowance for the spender to burn tokens.577	/// See [`set_allowance`][`Pallet::set_allowance`] for more details.578	pub fn burn_from(579		collection: &FungibleHandle<T>,580		spender: &T::CrossAccountId,581		from: &T::CrossAccountId,582		amount: u128,583		nesting_budget: &dyn Budget,584	) -> DispatchResult {585		let allowance = Self::check_allowed(collection, spender, from, amount, nesting_budget)?;586587		// =========588589		Self::burn(collection, from, amount)?;590		if let Some(allowance) = allowance {591			Self::set_allowance_unchecked(collection, from, spender, allowance);592		}593		Ok(())594	}595596	///	Creates fungible token.597	///598	/// The sender should be the owner/admin of the collection or collection should be configured599	/// to allow public minting.600	///601	/// - `data`: Contains user who will become the owners of the tokens and amount602	///   of tokens he will receive.603	pub fn create_item(604		collection: &FungibleHandle<T>,605		sender: &T::CrossAccountId,606		data: CreateItemData<T>,607		nesting_budget: &dyn Budget,608	) -> DispatchResult {609		Self::create_multiple_items(610			collection,611			sender,612			[(data.0, data.1)].into_iter().collect(),613			nesting_budget,614		)615	}616617	/// Returns 10 tokens owners in no particular order618	///619	/// There is no direct way to get token holders in ascending order,620	/// since `iter_prefix` returns values in no particular order.621	/// Therefore, getting the 10 largest holders with a large value of holders622	/// can lead to impact memory allocation + sorting with  `n * log (n)`.623	pub fn token_owners(624		collection: CollectionId,625		_token: TokenId,626	) -> Option<Vec<T::CrossAccountId>> {627		let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection,))628			.map(|(owner, _amount)| owner)629			.take(10)630			.collect();631632		if res.is_empty() {633			None634		} else {635			Some(res)636		}637	}638}
addedpallets/nonfungible/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/pallets/nonfungible/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.1] - 2022-07-14
+
+### Added
+
+- Implementation of RPC method `token_owners`.
+   For reasons of compatibility with this pallet, returns only one owner if token exists.
+   This was an internal request to improve the web interface and support fractionalization event. 
\ No newline at end of file
modifiedpallets/nonfungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/nonfungible/Cargo.toml
+++ b/pallets/nonfungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-nonfungible"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -26,7 +26,7 @@
 	weights::WeightInfo as _,
 };
 use sp_runtime::DispatchError;
-use sp_std::vec::Vec;
+use sp_std::{vec::Vec, vec};
 
 use crate::{
 	AccountBalance, Allowance, Config, CreateItemData, Error, NonfungibleHandle, Owned, Pallet,
@@ -422,6 +422,11 @@
 		<TokenData<T>>::get((self.id, token)).map(|t| t.owner)
 	}
 
+	/// Returns token owners.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId> {
+		self.token_owner(token).map_or_else(|| vec![], |t| vec![t])
+	}
+
 	fn token_property(&self, token_id: TokenId, key: &PropertyKey) -> Option<PropertyValue> {
 		<Pallet<T>>::token_properties((self.id, token_id))
 			.get(key)
modifiedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -1,6 +1,18 @@
-## v0.1.2 - 2022-07
+# Change Log
 
-### Refungible Pallet
+All notable changes to this project will be documented in this file.
 
+## [v0.1.2] - 2022-07-14
+
+### Other changes
+
 feat(refungible-pallet): add ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
-test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
\ No newline at end of file
+test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
+
+## [v0.1.1] - 2022-07-14
+
+### Other changes
+
+- feat: RPC method `token_owners` returning 10 owners in no particular order.
+
+This was an internal request to improve the web interface and support fractionalization event. 
modifiedpallets/refungible/src/common.rsdiffbeforeafterboth
--- a/pallets/refungible/src/common.rs
+++ b/pallets/refungible/src/common.rs
@@ -438,6 +438,11 @@
 		<Pallet<T>>::token_owner(self.id, token)
 	}
 
+	/// Returns 10 token in no particular order.
+	fn token_owners(&self, token: TokenId) -> Vec<T::CrossAccountId> {
+		<Pallet<T>>::token_owners(self.id, token).unwrap_or_default()
+	}
+
 	fn token_property(&self, _token_id: TokenId, _key: &PropertyKey) -> Option<PropertyValue> {
 		None
 	}
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -1235,4 +1235,26 @@
 	) -> DispatchResult {
 		<PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)
 	}
+
+	/// Returns 10 token in no particular order.
+	///
+	/// There is no direct way to get token holders in ascending order,
+	/// since `iter_prefix` returns values in no particular order.
+	/// Therefore, getting the 10 largest holders with a large value of holders
+	/// can lead to impact memory allocation + sorting with  `n * log (n)`.
+	pub fn token_owners(
+		collection_id: CollectionId,
+		token: TokenId,
+	) -> Option<Vec<T::CrossAccountId>> {
+		let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))
+			.map(|(owner, _amount)| owner)
+			.take(10)
+			.collect();
+
+		if res.is_empty() {
+			None
+		} else {
+			Some(res)
+		}
+	}
 }
addedprimitives/rpc/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/primitives/rpc/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.1.1] - 2022-07-14
+
+### Added
+
+ - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
+   This was an internal request to improve the web interface and support fractionalization event. 
\ No newline at end of file
modifiedprimitives/rpc/Cargo.tomldiffbeforeafterboth
--- a/primitives/rpc/Cargo.toml
+++ b/primitives/rpc/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "up-rpc"
-version = "0.1.0"
+version = "0.1.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedprimitives/rpc/src/lib.rsdiffbeforeafterboth
--- a/primitives/rpc/src/lib.rs
+++ b/primitives/rpc/src/lib.rs
@@ -81,5 +81,6 @@
 		fn next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result<Option<u64>>;
 		fn effective_collection_limits(collection_id: CollectionId) -> Result<Option<CollectionLimits>>;
 		fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Result<Option<u128>>;
+		fn token_owners(collection: CollectionId, token: TokenId) -> Result<Vec<CrossAccountId>>;
 	}
 }
addedruntime/common/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/runtime/common/CHANGELOG.md
@@ -0,0 +1,13 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## [0.9.25] - 2022-07-14
+
+### Added
+
+ - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
+    This was an internal request to improve the web interface and support fractionalization event. 
+
+
+ 
\ No newline at end of file
modifiedruntime/common/src/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -40,6 +40,11 @@
                 fn token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
                     dispatch_unique_runtime!(collection.token_owner(token))
                 }
+
+                fn token_owners(collection: CollectionId, token: TokenId) -> Result<Vec::<CrossAccountId>, DispatchError>  {
+                   dispatch_unique_runtime!(collection.token_owners(token))
+                }
+
                 fn topmost_token_owner(collection: CollectionId, token: TokenId) -> Result<Option<CrossAccountId>, DispatchError> {
                     let budget = up_data_structs::budget::Value::new(10);
 
addedtests/CHANGELOG.mddiffbeforeafterboth
--- /dev/null
+++ b/tests/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+## 2022-07-14
+
+### Added
+ - Integrintegration tests of RPC method `token_owners`.
+ - Integrintegration tests of Fungible Pallet.
+  
+ 
\ No newline at end of file
modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -80,6 +80,8 @@
     "testLimits": "mocha --timeout 9999999 -r ts-node/register ./**/limits.test.ts",
     "testEthCreateCollection": "mocha --timeout 9999999 -r ts-node/register ./**/eth/createCollection.test.ts",
     "testRFT": "mocha --timeout 9999999 -r ts-node/register ./**/refungible.test.ts",
+    "testFT": "mocha --timeout 9999999 -r ts-node/register ./**/fungible.test.ts",
+    "testRPC": "mocha --timeout 9999999 -r ts-node/register ./**/rpc.test.ts",
     "polkadot-types-fetch-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/interfaces/metadata.json",
     "polkadot-types-from-defs": "ts-node ./node_modules/.bin/polkadot-types-from-defs --endpoint src/interfaces/metadata.json --input src/interfaces/ --package .",
     "polkadot-types-from-chain": "ts-node ./node_modules/.bin/polkadot-types-from-chain --endpoint src/interfaces/metadata.json --output src/interfaces/ --package .",
addedtests/src/fungible.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/fungible.test.ts
@@ -0,0 +1,184 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {default as usingApi} from './substrate/substrate-api';
+import {IKeyringPair} from '@polkadot/types/types';
+import {
+  getBalance,
+  createMultipleItemsExpectSuccess,
+  isTokenExists,
+  getLastTokenId,
+  getAllowance,
+  approve,
+  transferFrom,
+  createCollection,
+  transfer,
+  burnItem,
+  normalizeAccountId,
+  CrossAccountId,
+  createFungibleItemExpectSuccess,
+  U128_MAX,
+  burnFromExpectSuccess,
+} from './util/helpers';
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+let alice: IKeyringPair;
+let bob: IKeyringPair;
+
+describe('integration test: Fungible functionality:', () => {
+  before(async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      alice = privateKeyWrapper('//Alice');
+      bob = privateKeyWrapper('//Bob');
+    });
+  });
+
+  it('Create fungible collection and token', async () => {
+    await usingApi(async api => {
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
+      expect(createCollectionResult.success).to.be.true;
+      const collectionId  = createCollectionResult.collectionId;
+      const defaultTokenId = await getLastTokenId(api, collectionId);
+      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
+      const aliceBalance = await getBalance(api, collectionId, alice, aliceTokenId); 
+      const itemCountAfter = await getLastTokenId(api, collectionId);
+      
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(itemCountAfter).to.be.equal(defaultTokenId);
+      expect(aliceBalance).to.be.equal(U128_MAX);
+    });
+  });
+  
+  it('RPC method tokenOnewrs for fungible collection and token', async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
+      
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
+      const collectionId = createCollectionResult.collectionId;
+      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
+     
+      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
+      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
+            
+      for (let i = 0; i < 7; i++) {
+        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 1);
+      } 
+      
+      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
+      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
+      const aliceID = normalizeAccountId(alice);
+      const bobId = normalizeAccountId(bob);
+
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
+      expect(owners.length == 10).to.be.true;
+      
+      const eleven = privateKeyWrapper('11');
+      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
+      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+    });
+  });
+  
+  it('Transfer token', async () => {
+    await usingApi(async api => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const tokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address);
+
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n);
+      expect(await transfer(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
+      expect(await transfer(api, collectionId, tokenId, alice, ethAcc, 140n)).to.be.true;
+
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(300n);
+      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(60n);
+      expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(140n);
+      await expect(transfer(api, collectionId, tokenId, alice, bob, 350n)).to.eventually.be.rejected;
+    });
+  });
+
+  it('Tokens multiple creation', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      
+      const args = [
+        {Fungible: {Value: 500n}},
+        {Fungible: {Value: 400n}},
+        {Fungible: {Value: 300n}},
+      ];
+      
+      await createMultipleItemsExpectSuccess(alice, collectionId, args);
+      expect(await getBalance(api, collectionId, alice, 0)).to.be.equal(1200n);
+    });   
+  });
+
+  it('Burn some tokens ', async () => {
+    await usingApi(async api => {   
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address));
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(500n);
+      expect(await burnItem(api, alice, collectionId, tokenId, 499n)).to.be.true;
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(1n);
+    });
+  });
+  
+  it('Burn all tokens ', async () => {
+    await usingApi(async api => {   
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 500n}, alice.address));
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      expect(await burnItem(api, alice, collectionId, tokenId, 500n)).to.be.true;
+      expect(await isTokenExists(api, collectionId, tokenId)).to.be.true;
+      
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(0n);
+      expect((await api.rpc.unique.totalPieces(collectionId, tokenId)).value.toBigInt()).to.be.equal(0n);
+    });
+  });
+
+  it('Set allowance for token', async () => {
+    await usingApi(async api => {
+      const collectionId = (await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}})).collectionId;
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      
+      const tokenId = (await createFungibleItemExpectSuccess(alice, collectionId, {Value: 100n}, alice.address));
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(100n);
+
+      expect(await approve(api, collectionId, tokenId, alice, bob, 60n)).to.be.true;
+      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(60n);
+      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(0n);
+      
+      expect(await transferFrom(api, collectionId, tokenId, bob, alice, bob,  20n)).to.be.true;
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(80n);
+      expect(await getBalance(api, collectionId, bob, tokenId)).to.be.equal(20n);
+      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(40n);
+      
+      await burnFromExpectSuccess(bob, alice, collectionId, tokenId, 10n);
+     
+      expect(await getBalance(api, collectionId, alice, tokenId)).to.be.equal(70n);
+      expect(await getAllowance(api, collectionId, alice, bob, tokenId)).to.be.equal(30n);
+      expect(await transferFrom(api, collectionId, tokenId, bob, alice, ethAcc,  10n)).to.be.true;
+      expect(await getBalance(api, collectionId, ethAcc, tokenId)).to.be.equal(10n);
+    });
+  });
+});
modifiedtests/src/interfaces/augment-api-consts.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-consts.ts
+++ b/tests/src/interfaces/augment-api-consts.ts
@@ -29,7 +29,13 @@
       [key: string]: Codec;
     };
     common: {
+      /**
+       * Maximum admins per collection.
+       **/
       collectionAdminsLimit: u32 & AugmentedConst<ApiType>;
+      /**
+       * Set price to create a collection.
+       **/
       collectionCreationPrice: u128 & AugmentedConst<ApiType>;
       /**
        * Generic const
modifiedtests/src/interfaces/augment-api-events.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-events.ts
+++ b/tests/src/interfaces/augment-api-events.ts
@@ -59,82 +59,47 @@
     };
     common: {
       /**
-       * * collection_id
-       * 
-       * * item_id
-       * 
-       * * sender
-       * 
-       * * spender
-       * 
-       * * amount
+       * Amount pieces of token owned by `sender` was approved for `spender`.
        **/
       Approved: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
       /**
        * New collection was created
-       * 
-       * # Arguments
-       * 
-       * * collection_id: Globally unique identifier of newly created collection.
-       * 
-       * * mode: [CollectionMode] converted into u8.
-       * 
-       * * account_id: Collection owner.
        **/
       CollectionCreated: AugmentedEvent<ApiType, [u32, u8, AccountId32]>;
       /**
        * New collection was destroyed
-       * 
-       * # Arguments
-       * 
-       * * collection_id: Globally unique identifier of collection.
        **/
       CollectionDestroyed: AugmentedEvent<ApiType, [u32]>;
+      /**
+       * The property has been deleted.
+       **/
       CollectionPropertyDeleted: AugmentedEvent<ApiType, [u32, Bytes]>;
+      /**
+       * The colletion property has been set.
+       **/
       CollectionPropertySet: AugmentedEvent<ApiType, [u32, Bytes]>;
       /**
        * New item was created.
-       * 
-       * # Arguments
-       * 
-       * * collection_id: Id of the collection where item was created.
-       * 
-       * * item_id: Id of an item. Unique within the collection.
-       * 
-       * * recipient: Owner of newly created item
-       * 
-       * * amount: Always 1 for NFT
        **/
       ItemCreated: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
       /**
        * Collection item was burned.
-       * 
-       * # Arguments
-       * 
-       * * collection_id.
-       * 
-       * * item_id: Identifier of burned NFT.
-       * 
-       * * owner: which user has destroyed its tokens
-       * 
-       * * amount: Always 1 for NFT
        **/
       ItemDestroyed: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
+      /**
+       * The colletion property permission has been set.
+       **/
       PropertyPermissionSet: AugmentedEvent<ApiType, [u32, Bytes]>;
+      /**
+       * The token property has been deleted.
+       **/
       TokenPropertyDeleted: AugmentedEvent<ApiType, [u32, u32, Bytes]>;
+      /**
+       * The token property has been set.
+       **/
       TokenPropertySet: AugmentedEvent<ApiType, [u32, u32, Bytes]>;
       /**
        * Item was transferred
-       * 
-       * * collection_id: Id of collection to which item is belong
-       * 
-       * * item_id: Id of an item
-       * 
-       * * sender: Original owner of item
-       * 
-       * * recipient: New owner of item
-       * 
-       * * amount: Always 1 for NFT
        **/
       Transfer: AugmentedEvent<ApiType, [u32, u32, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmAccountBasicCrossAccountIdRepr, u128]>;
       /**
modifiedtests/src/interfaces/augment-api-query.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-query.ts
+++ b/tests/src/interfaces/augment-api-query.ts
@@ -69,24 +69,36 @@
       [key: string]: QueryableStorageEntry<ApiType>;
     };
     common: {
+      /**
+       * Storage of collection admins count.
+       **/
       adminAmount: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
        * Allowlisted collection users
        **/
       allowlist: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<bool>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
       /**
-       * Collection info
+       * Storage of collection info.
        **/
       collectionById: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<Option<UpDataStructsCollection>>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
-       * Collection properties
+       * Storage of collection properties.
        **/
       collectionProperties: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<UpDataStructsProperties>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
+      /**
+       * Storage of collection properties permissions.
+       **/
       collectionPropertyPermissions: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<BTreeMap<Bytes, UpDataStructsPropertyPermission>>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
+      /**
+       * Storage of the count of created collections.
+       **/
       createdCollectionCount: AugmentedQuery<ApiType, () => Observable<u32>, []> & QueryableStorageEntry<ApiType, []>;
+      /**
+       * Storage of the count of deleted collections.
+       **/
       destroyedCollectionCount: AugmentedQuery<ApiType, () => Observable<u32>, []> & QueryableStorageEntry<ApiType, []>;
       /**
-       * Not used by code, exists only to provide some types to metadata
+       * Not used by code, exists only to provide some types to metadata.
        **/
       dummyStorageValue: AugmentedQuery<ApiType, () => Observable<Option<ITuple<[UpDataStructsCollectionStats, u32, u32, UpDataStructsTokenChild, PhantomTypeUpDataStructs]>>>, []> & QueryableStorageEntry<ApiType, []>;
       /**
@@ -231,20 +243,42 @@
       [key: string]: QueryableStorageEntry<ApiType>;
     };
     nonfungible: {
+      /**
+       * Amount of tokens owned by account.
+       **/
       accountBalance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => Observable<u32>, [u32, PalletEvmAccountBasicCrossAccountIdRepr]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr]>;
+      /**
+       * Allowance set by an owner for a spender for a token.
+       **/
       allowance: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<Option<PalletEvmAccountBasicCrossAccountIdRepr>>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
       /**
-       * Used to enumerate tokens owned by account
+       * Used to enumerate tokens owned by account.
        **/
       owned: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array, arg3: u32 | AnyNumber | Uint8Array) => Observable<bool>, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32]> & QueryableStorageEntry<ApiType, [u32, PalletEvmAccountBasicCrossAccountIdRepr, u32]>;
+      /**
+       * Custom data that is serialized to bytes and attached to a token property.
+       * Currently used to store RMRK data.
+       **/
       tokenAuxProperties: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array, arg3: UpDataStructsPropertyScope | 'None' | 'Rmrk' | number | Uint8Array, arg4: Bytes | string | Uint8Array) => Observable<Option<Bytes>>, [u32, u32, UpDataStructsPropertyScope, Bytes]> & QueryableStorageEntry<ApiType, [u32, u32, UpDataStructsPropertyScope, Bytes]>;
       /**
-       * Used to enumerate token's children
+       * Used to enumerate token's children.
        **/
       tokenChildren: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array, arg3: ITuple<[u32, u32]> | [u32 | AnyNumber | Uint8Array, u32 | AnyNumber | Uint8Array]) => Observable<bool>, [u32, u32, ITuple<[u32, u32]>]> & QueryableStorageEntry<ApiType, [u32, u32, ITuple<[u32, u32]>]>;
+      /**
+       * Custom data serialized to bytes for token.
+       **/
       tokenData: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<Option<PalletNonfungibleItemData>>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
+      /**
+       * Key-Value map stored for token.
+       **/
       tokenProperties: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<UpDataStructsProperties>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
+      /**
+       * Amount of burnt tokens for collection.
+       **/
       tokensBurnt: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
+      /**
+       * Amount of tokens minted for collection.
+       **/
       tokensMinted: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
        * Generic query
@@ -430,6 +464,9 @@
        **/
       tokenData: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<PalletRefungibleItemData>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
       tokenProperties: AugmentedQuery<ApiType, (arg1: u32 | AnyNumber | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<UpDataStructsProperties>, [u32, u32]> & QueryableStorageEntry<ApiType, [u32, u32]>;
+      /**
+       * Amount of burnt tokens for collection
+       **/
       tokensBurnt: AugmentedQuery<ApiType, (arg: u32 | AnyNumber | Uint8Array) => Observable<u32>, [u32]> & QueryableStorageEntry<ApiType, [u32]>;
       /**
        * Amount of tokens minted for collection
modifiedtests/src/interfaces/augment-api-rpc.tsdiffbeforeafterboth
--- a/tests/src/interfaces/augment-api-rpc.ts
+++ b/tests/src/interfaces/augment-api-rpc.ts
@@ -709,6 +709,10 @@
        **/
       tokenOwner: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Option<PalletEvmAccountBasicCrossAccountIdRepr>>>;
       /**
+       * Returns 10 tokens owners in no particular order
+       **/
+      tokenOwners: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, at?: Hash | string | Uint8Array) => Observable<Vec<PalletEvmAccountBasicCrossAccountIdRepr>>>;
+      /**
        * Get token properties
        **/
       tokenProperties: AugmentedRpc<(collection: u32 | AnyNumber | Uint8Array, tokenId: u32 | AnyNumber | Uint8Array, propertyKeys?: Vec<Text> | (Text | string)[], at?: Hash | string | Uint8Array) => Observable<Vec<UpDataStructsProperty>>>;
modifiedtests/src/interfaces/unique/definitions.tsdiffbeforeafterboth
--- a/tests/src/interfaces/unique/definitions.ts
+++ b/tests/src/interfaces/unique/definitions.ts
@@ -49,6 +49,7 @@
     balance: fun('Get amount of specific account token', [collectionParam, crossAccountParam(), tokenParam], 'u128'),
     allowance: fun('Get allowed amount', [collectionParam, crossAccountParam('sender'), crossAccountParam('spender'), tokenParam], 'u128'),
     tokenOwner: fun('Get token owner', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`),
+    tokenOwners: fun('Returns 10 tokens owners in no particular order', [collectionParam, tokenParam], `Vec<${CROSS_ACCOUNT_ID_TYPE}>`),
     topmostTokenOwner: fun('Get token owner, in case of nested token - find parent recursive', [collectionParam, tokenParam], `Option<${CROSS_ACCOUNT_ID_TYPE}>`),
     tokenChildren: fun('Get tokens nested directly into the token', [collectionParam, tokenParam], 'Vec<UpDataStructsTokenChild>'),
     constMetadata: fun('Get token constant metadata', [collectionParam, tokenParam], 'Vec<u8>'),
modifiedtests/src/refungible.test.tsdiffbeforeafterboth
--- a/tests/src/refungible.test.ts
+++ b/tests/src/refungible.test.ts
@@ -32,6 +32,8 @@
   repartitionRFT,
   createCollectionWithPropsExpectSuccess,
   getDetailedCollectionInfo,
+  normalizeAccountId,
+  CrossAccountId,
   getCreateItemsResult,
   getDestroyItemsResult,
 } from './util/helpers';
@@ -55,14 +57,14 @@
   it('Create refungible collection and token', async () => {
     await usingApi(async api => {
       const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}});
-      expect(createCollectionResult.success).to.be.true;    
+      expect(createCollectionResult.success).to.be.true;
       const collectionId  = createCollectionResult.collectionId;
-
+      
       const itemCountBefore = await getLastTokenId(api, collectionId);
       const result = await createRefungibleToken(api, alice, collectionId, 100n);
-
+      
       const itemCountAfter = await getLastTokenId(api, collectionId);
-
+      
       // What to expect
       // tslint:disable-next-line:no-unused-expression
       expect(result.success).to.be.true;
@@ -71,7 +73,43 @@
       expect(itemCountAfter.toString()).to.be.equal(result.itemId.toString());
     });
   });
-
+  
+  it('RPC method tokenOnewrs for refungible collection and token', async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
+      
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'ReFungible'}});
+      const collectionId = createCollectionResult.collectionId;
+      
+      const result = await createRefungibleToken(api, alice, collectionId, 10_000n);
+      const aliceTokenId = result.itemId;
+      
+      
+      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
+      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
+      
+      for (let i = 0; i < 7; i++) {
+        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 50*(i+1));
+      } 
+      
+      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
+      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
+      
+      const aliceID = normalizeAccountId(alice);
+      const bobId = normalizeAccountId(bob);
+      
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
+      expect(owners.length).to.be.equal(10);
+      
+      const eleven = privateKeyWrapper('11');
+      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
+      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+    });
+  });
+  
   it('Transfer token pieces', async () => {
     await usingApi(async api => {
       const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
modifiedtests/src/rpc.test.tsdiffbeforeafterboth
--- a/tests/src/rpc.test.ts
+++ b/tests/src/rpc.test.ts
@@ -1,12 +1,57 @@
+import {IKeyringPair} from '@polkadot/types/types';
 import {expect} from 'chai';
 import usingApi from './substrate/substrate-api';
-import {createCollectionExpectSuccess, getTokenOwner} from './util/helpers';
+import {createCollection, createCollectionExpectSuccess, createFungibleItemExpectSuccess, CrossAccountId, getTokenOwner, normalizeAccountId, transfer, U128_MAX} from './util/helpers';
+
+let alice: IKeyringPair;
+let bob: IKeyringPair;
 
-describe('getTokenOwner', () => {
+
+describe('integration test: RPC methods', () => {
+  before(async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      alice = privateKeyWrapper('//Alice');
+      bob = privateKeyWrapper('//Bob');
+    });
+  });
+
+  
   it('returns None for fungible collection', async () => {
     await usingApi(async api => {
       const collection = await createCollectionExpectSuccess({mode: {type: 'Fungible', decimalPoints: 0}});
       await expect(getTokenOwner(api, collection, 0)).to.be.rejectedWith(/^owner == null$/);
     });
   });
+  
+  it('RPC method tokenOwners for fungible collection and token', async () => {
+    await usingApi(async (api, privateKeyWrapper) => {
+      const ethAcc = {Ethereum: '0x67fb3503a61b284dc83fa96dceec4192db47dc7c'};
+      const facelessCrowd = Array.from(Array(7).keys()).map(i => normalizeAccountId(privateKeyWrapper(i.toString())));
+      
+      const createCollectionResult = await createCollection(api, alice, {mode: {type: 'Fungible', decimalPoints: 0}});
+      const collectionId = createCollectionResult.collectionId;
+      const aliceTokenId = await createFungibleItemExpectSuccess(alice, collectionId, {Value: U128_MAX}, alice.address);
+     
+      await transfer(api, collectionId, aliceTokenId, alice, bob, 1000n);
+      await transfer(api, collectionId, aliceTokenId, alice, ethAcc, 900n);
+            
+      for (let i = 0; i < 7; i++) {
+        await transfer(api, collectionId, aliceTokenId, alice, facelessCrowd[i], 1);
+      } 
+      
+      const owners = await api.rpc.unique.tokenOwners(collectionId, aliceTokenId);
+      const ids = (owners.toJSON() as CrossAccountId[]).map(s => normalizeAccountId(s));
+      const aliceID = normalizeAccountId(alice);
+      const bobId = normalizeAccountId(bob);
+
+      // What to expect
+      // tslint:disable-next-line:no-unused-expression
+      expect(ids).to.deep.include.members([aliceID, ethAcc, bobId, ...facelessCrowd]);
+      expect(owners.length == 10).to.be.true;
+      
+      const eleven = privateKeyWrapper('11');
+      expect(await transfer(api, collectionId, aliceTokenId, alice, eleven, 10n)).to.be.true;
+      expect((await api.rpc.unique.tokenOwners(collectionId, aliceTokenId)).length).to.be.equal(10);
+    });
+  });
 });
\ No newline at end of file