git.delta.rocks / unique-network / refs/commits / a66e7a4819dc

difftreelog

Add properties RPC

Daniel Shiposha2022-05-05parent: #a7c09a3.patch.diff
in: master

8 files changed

modifiedclient/rpc/src/lib.rsdiffbeforeafterboth
--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -19,7 +19,10 @@
 use codec::Decode;
 use jsonrpc_core::{Error as RpcError, ErrorCode, Result};
 use jsonrpc_derive::rpc;
-use up_data_structs::{RpcCollection, CollectionId, CollectionStats, CollectionLimits, TokenId};
+use up_data_structs::{
+	RpcCollection, CollectionId, CollectionStats, CollectionLimits, TokenId, Property, PropertyKey,
+	PropertyKeyPermission,
+};
 use sp_api::{BlockId, BlockT, ProvideRuntimeApi, ApiExt};
 use sp_blockchain::HeaderBackend;
 use up_rpc::UniqueApi as UniqueRuntimeApi;
@@ -76,6 +79,31 @@
 		at: Option<BlockHash>,
 	) -> Result<Vec<u8>>;
 
+	#[rpc(name = "unique_collectionProperties")]
+	fn collection_properties(
+		&self,
+		collection: CollectionId,
+		keys: Vec<String>,
+		at: Option<BlockHash>,
+	) -> Result<Vec<Property>>;
+
+	#[rpc(name = "unique_tokenProperties")]
+	fn token_properties(
+		&self,
+		collection: CollectionId,
+		token_id: TokenId,
+		properties: Vec<String>,
+		at: Option<BlockHash>,
+	) -> Result<Vec<Property>>;
+
+	#[rpc(name = "unique_propertyPermissions")]
+	fn property_permissions(
+		&self,
+		collection: CollectionId,
+		keys: Vec<String>,
+		at: Option<BlockHash>,
+	) -> Result<Vec<PropertyKeyPermission>>;
+
 	#[rpc(name = "unique_totalSupply")]
 	fn total_supply(&self, collection: CollectionId, at: Option<BlockHash>) -> Result<u32>;
 	#[rpc(name = "unique_accountBalance")]
@@ -177,7 +205,9 @@
 
 macro_rules! pass_method {
 	(
-		$method_name:ident($($name:ident: $ty:ty),* $(,)?) -> $result:ty $(=> $mapper:expr)?
+		$method_name:ident(
+			$($(#[map(|$map_arg:ident| $map:expr)])? $name:ident: $ty:ty),* $(,)?
+		) -> $result:ty $(=> $mapper:expr)?
 		$(; changed_in $ver:expr, $changed_method_name:ident ($($changed_name:expr), * $(,)?) => $fixer:expr)*
 	) => {
 		fn $method_name(
@@ -205,7 +235,7 @@
 			let result = $(if _api_version < $ver {
 				api.$changed_method_name(&at, $($changed_name),*).map(|r| r.map($fixer))
 			} else)*
-			{ api.$method_name(&at, $($name),*) };
+			{ api.$method_name(&at, $($((|$map_arg: $ty| $map))? ($name)),*) };
 
 			let result = result.map_err(|e| RpcError {
 				code: ErrorCode::ServerError(Error::RuntimeError.into()),
@@ -242,6 +272,28 @@
 	pass_method!(const_metadata(collection: CollectionId, token: TokenId) -> Vec<u8>);
 	pass_method!(variable_metadata(collection: CollectionId, token: TokenId) -> Vec<u8>);
 
+	pass_method!(collection_properties(
+		collection: CollectionId,
+
+		#[map(|keys| string_keys_to_bytes_keys(keys))]
+		keys: Vec<String>
+	) -> Vec<Property>);
+
+	pass_method!(token_properties(
+		collection: CollectionId,
+		token_id: TokenId,
+
+		#[map(|keys| string_keys_to_bytes_keys(keys))]
+		properties: Vec<String>
+	) -> Vec<Property>);
+
+	pass_method!(property_permissions(
+		collection: CollectionId,
+
+		#[map(|keys| string_keys_to_bytes_keys(keys))]
+		keys: Vec<String>
+	) -> Vec<PropertyKeyPermission>);
+
 	pass_method!(total_supply(collection: CollectionId) -> u32);
 	pass_method!(account_balance(collection: CollectionId, account: CrossAccountId) -> u32);
 	pass_method!(balance(collection: CollectionId, account: CrossAccountId, token: TokenId) -> String => |v| v.to_string());
@@ -256,3 +308,7 @@
 	pass_method!(next_sponsored(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Option<u64>);
 	pass_method!(effective_collection_limits(collection_id: CollectionId) -> Option<CollectionLimits>);
 }
+
+fn string_keys_to_bytes_keys(keys: Vec<String>) -> Vec<Vec<u8>> {
+	keys.into_iter().map(|key| key.into_bytes()).collect()
+}
modifiedpallets/common/src/lib.rsdiffbeforeafterboth
--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -388,6 +388,7 @@
 
 	/// Collection properties
 	#[pallet::storage]
+	#[pallet::getter(fn collection_properties)]
 	pub type CollectionProperties<T> = StorageMap<
 		Hasher = Blake2_128Concat,
 		Key = CollectionId,
@@ -397,7 +398,7 @@
 	>;
 
 	#[pallet::storage]
-	#[pallet::getter(fn property_permission)]
+	#[pallet::getter(fn property_permissions)]
 	pub type CollectionPropertyPermissions<T> = StorageMap<
 		Hasher = Blake2_128Concat,
 		Key = CollectionId,
@@ -656,9 +657,7 @@
 		CollectionProperties::<T>::insert(
 			id,
 			Properties::from_collection_props_vec(data.properties)
-				.map_err(|e| -> Error::<T> {
-					e.into()
-				})?,
+				.map_err(|e| -> Error<T> { e.into() })?,
 		);
 
 		let token_props_permissions: PropertiesPermissionMap = data
@@ -667,9 +666,7 @@
 			.map(|property| (property.key, property.permission))
 			.collect::<BTreeMap<_, _>>()
 			.try_into()
-			.map_err(|_| -> Error::<T> {
-				PropertiesError::PropertyLimitReached.into()
-			})?;
+			.map_err(|_| -> Error<T> { PropertiesError::PropertyLimitReached.into() })?;
 
 		CollectionPropertyPermissions::<T>::insert(id, token_props_permissions);
 
@@ -754,9 +751,7 @@
 		CollectionProperties::<T>::try_mutate(collection.id, |properties| {
 			properties.try_set_property(property.clone())
 		})
-		.map_err(|e| -> Error::<T> {
-			e.into()
-		})?;
+		.map_err(|e| -> Error<T> { e.into() })?;
 
 		Self::deposit_event(Event::CollectionPropertySet(collection.id, property));
 
@@ -786,7 +781,10 @@
 			properties.remove_property(&property_key);
 		});
 
-		Self::deposit_event(Event::CollectionPropertyDeleted(collection.id, property_key));
+		Self::deposit_event(Event::CollectionPropertyDeleted(
+			collection.id,
+			property_key,
+		));
 
 		Ok(())
 	}
@@ -823,9 +821,7 @@
 			let property_permission = property_permission.clone();
 			permissions.try_insert(property_permission.key, property_permission.permission)
 		})
-		.map_err(|_| -> Error::<T> {
-			PropertiesError::PropertyLimitReached.into()
-		})?;
+		.map_err(|_| -> Error<T> { PropertiesError::PropertyLimitReached.into() })?;
 
 		Self::deposit_event(Event::PropertyPermissionSet(
 			collection.id,
modifiedpallets/nonfungible/src/common.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/common.rs
+++ b/pallets/nonfungible/src/common.rs
@@ -184,7 +184,7 @@
 
 		with_weight(
 			<Pallet<T>>::delete_collection_properties(self, &sender, property_keys),
-			weight
+			weight,
 		)
 	}
 
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
before · pallets/nonfungible/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#![cfg_attr(not(feature = "std"), no_std)]1819use erc::ERC721Events;20use frame_support::{BoundedVec, ensure, fail};21use up_data_structs::{22	AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData, CreateNftExData,23	mapping::TokenAddressMapping, NestingRule, budget::Budget, Property, PropertyPermission,24	PropertyKey, PropertyKeyPermission,25};26use pallet_evm::account::CrossAccountId;27use pallet_common::{28	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,29	dispatch::CollectionDispatch,30};31use pallet_structure::Pallet as PalletStructure;32use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};33use sp_core::H160;34use sp_runtime::{ArithmeticError, DispatchError, DispatchResult};35use sp_std::{vec::Vec, vec};36use core::ops::Deref;37use sp_std::collections::btree_map::BTreeMap;38use codec::{Encode, Decode, MaxEncodedLen};39use scale_info::TypeInfo;4041pub use pallet::*;42#[cfg(feature = "runtime-benchmarks")]43pub mod benchmarking;44pub mod common;45pub mod erc;46pub mod weights;4748pub type CreateItemData<T> = CreateNftExData<<T as pallet_evm::account::Config>::CrossAccountId>;49pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;5051#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]52pub struct ItemData<CrossAccountId> {53	pub const_data: BoundedVec<u8, CustomDataLimit>,54	pub variable_data: BoundedVec<u8, CustomDataLimit>,55	pub owner: CrossAccountId,56}5758#[frame_support::pallet]59pub mod pallet {60	use super::*;61	use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key};62	use up_data_structs::{CollectionId, TokenId};63	use super::weights::WeightInfo;6465	#[pallet::error]66	pub enum Error<T> {67		/// Not Nonfungible item data used to mint in Nonfungible collection.68		NotNonfungibleDataUsedToMintFungibleCollectionToken,69		/// Used amount > 1 with NFT70		NonfungibleItemsHaveNoAmount,71	}7273	#[pallet::config]74	pub trait Config:75		frame_system::Config + pallet_common::Config + pallet_structure::Config76	{77		type WeightInfo: WeightInfo;78	}7980	#[pallet::pallet]81	#[pallet::generate_store(pub(super) trait Store)]82	pub struct Pallet<T>(_);8384	#[pallet::storage]85	pub type TokensMinted<T: Config> =86		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;87	#[pallet::storage]88	pub type TokensBurnt<T: Config> =89		StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;9091	#[pallet::storage]92	pub type TokenData<T: Config> = StorageNMap<93		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),94		Value = ItemData<T::CrossAccountId>,95		QueryKind = OptionQuery,96	>;9798	#[pallet::storage]99	pub type TokenProperties<T: Config> = StorageNMap<100		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),101		Value = up_data_structs::Properties,102		QueryKind = ValueQuery,103		OnEmpty = up_data_structs::TokenProperties,104	>;105106	/// Used to enumerate tokens owned by account107	#[pallet::storage]108	pub type Owned<T: Config> = StorageNMap<109		Key = (110			Key<Twox64Concat, CollectionId>,111			Key<Blake2_128Concat, T::CrossAccountId>,112			Key<Twox64Concat, TokenId>,113		),114		Value = bool,115		QueryKind = ValueQuery,116	>;117118	#[pallet::storage]119	pub type AccountBalance<T: Config> = StorageNMap<120		Key = (121			Key<Twox64Concat, CollectionId>,122			Key<Blake2_128Concat, T::CrossAccountId>,123		),124		Value = u32,125		QueryKind = ValueQuery,126	>;127128	#[pallet::storage]129	pub type Allowance<T: Config> = StorageNMap<130		Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),131		Value = T::CrossAccountId,132		QueryKind = OptionQuery,133	>;134}135136pub struct NonfungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);137impl<T: Config> NonfungibleHandle<T> {138	pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {139		Self(inner)140	}141	pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {142		self.0143	}144}145impl<T: Config> WithRecorder<T> for NonfungibleHandle<T> {146	fn recorder(&self) -> &SubstrateRecorder<T> {147		self.0.recorder()148	}149	fn into_recorder(self) -> SubstrateRecorder<T> {150		self.0.into_recorder()151	}152}153impl<T: Config> Deref for NonfungibleHandle<T> {154	type Target = pallet_common::CollectionHandle<T>;155156	fn deref(&self) -> &Self::Target {157		&self.0158	}159}160161impl<T: Config> Pallet<T> {162	pub fn total_supply(collection: &NonfungibleHandle<T>) -> u32 {163		<TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)164	}165	pub fn token_exists(collection: &NonfungibleHandle<T>, token: TokenId) -> bool {166		<TokenData<T>>::contains_key((collection.id, token))167	}168}169170// unchecked calls skips any permission checks171impl<T: Config> Pallet<T> {172	pub fn init_collection(173		owner: T::AccountId,174		data: CreateCollectionData<T::AccountId>,175	) -> Result<CollectionId, DispatchError> {176		<PalletCommon<T>>::init_collection(owner, data)177	}178	pub fn destroy_collection(179		collection: NonfungibleHandle<T>,180		sender: &T::CrossAccountId,181	) -> DispatchResult {182		let id = collection.id;183184		// =========185186		PalletCommon::destroy_collection(collection.0, sender)?;187188		<TokenData<T>>::remove_prefix((id,), None);189		<Owned<T>>::remove_prefix((id,), None);190		<TokensMinted<T>>::remove(id);191		<TokensBurnt<T>>::remove(id);192		<Allowance<T>>::remove_prefix((id,), None);193		<AccountBalance<T>>::remove_prefix((id,), None);194		Ok(())195	}196197	pub fn burn(198		collection: &NonfungibleHandle<T>,199		sender: &T::CrossAccountId,200		token: TokenId,201	) -> DispatchResult {202		let token_data =203			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;204		ensure!(205			&token_data.owner == sender206				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(sender)),207			<CommonError<T>>::NoPermission208		);209210		if collection.access == AccessMode::AllowList {211			collection.check_allowlist(sender)?;212		}213214		let burnt = <TokensBurnt<T>>::get(collection.id)215			.checked_add(1)216			.ok_or(ArithmeticError::Overflow)?;217218		let balance = <AccountBalance<T>>::get((collection.id, token_data.owner.clone()))219			.checked_sub(1)220			.ok_or(ArithmeticError::Overflow)?;221222		if balance == 0 {223			<AccountBalance<T>>::remove((collection.id, token_data.owner.clone()));224		} else {225			<AccountBalance<T>>::insert((collection.id, token_data.owner.clone()), balance);226		}227		// =========228229		<Owned<T>>::remove((collection.id, &token_data.owner, token));230		<TokensBurnt<T>>::insert(collection.id, burnt);231		<TokenData<T>>::remove((collection.id, token));232		let old_spender = <Allowance<T>>::take((collection.id, token));233234		if let Some(old_spender) = old_spender {235			<PalletCommon<T>>::deposit_event(CommonEvent::Approved(236				collection.id,237				token,238				sender.clone(),239				old_spender,240				0,241			));242		}243244		collection.log_mirrored(ERC721Events::Transfer {245			from: *token_data.owner.as_eth(),246			to: H160::default(),247			token_id: token.into(),248		});249		<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(250			collection.id,251			token,252			token_data.owner,253			1,254		));255		Ok(())256	}257258	pub fn set_token_property(259		collection: &NonfungibleHandle<T>,260		sender: &T::CrossAccountId,261		token_id: TokenId,262		property: Property,263	) -> DispatchResult {264		Self::check_token_change_permission(collection, sender, token_id, &property.key)?;265266		<TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {267			properties.try_set_property(property.clone())268		}).map_err(|e| -> CommonError::<T> {269			e.into()270		})?;271272		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(273			collection.id,274			token_id,275			property,276		));277278		Ok(())279	}280281	pub fn set_token_properties(282		collection: &NonfungibleHandle<T>,283		sender: &T::CrossAccountId,284		token_id: TokenId,285		properties: Vec<Property>,286	) -> DispatchResult {287		for property in properties {288			Self::set_token_property(collection, sender, token_id, property)?;289		}290291		Ok(())292	}293294	pub fn delete_token_property(295		collection: &NonfungibleHandle<T>,296		sender: &T::CrossAccountId,297		token_id: TokenId,298		property_key: PropertyKey,299	) -> DispatchResult {300		Self::check_token_change_permission(collection, sender, token_id, &property_key)?;301302		<TokenProperties<T>>::mutate((collection.id, token_id), |properties| {303			properties.remove_property(&property_key);304		});305306		<PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(307			collection.id,308			token_id,309			property_key,310		));311312		Ok(())313	}314315	fn check_token_change_permission(316		collection: &NonfungibleHandle<T>,317		sender: &T::CrossAccountId,318		token_id: TokenId,319		property_key: &PropertyKey,320	) -> DispatchResult {321		let permission = <PalletCommon<T>>::property_permission(collection.id)322			.get(property_key)323			.map(|p| p.clone())324			.unwrap_or(PropertyPermission::None);325326		let token_data = <TokenData<T>>::get((collection.id, token_id))327			.ok_or(<CommonError<T>>::TokenNotFound)?;328329		let check_token_owner = || -> DispatchResult {330			ensure!(&token_data.owner == sender, <CommonError<T>>::NoPermission);331			Ok(())332		};333334		let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))335			.get_property(property_key)336			.is_some();337338		match (permission, is_property_exists) {339			(PropertyPermission::AdminConst, false) => collection.check_is_owner_or_admin(sender),340			(PropertyPermission::Admin, _) => collection.check_is_owner_or_admin(sender),341			(PropertyPermission::ItemOwnerConst, false) => check_token_owner(),342			(PropertyPermission::ItemOwner, _) => check_token_owner(),343			(PropertyPermission::ItemOwnerOrAdmin, _) => {344				check_token_owner().or(collection.check_is_owner_or_admin(sender))345			}346			_ => Err(<CommonError<T>>::NoPermission.into()),347		}348	}349350	pub fn delete_token_properties(351		collection: &NonfungibleHandle<T>,352		sender: &T::CrossAccountId,353		token_id: TokenId,354		property_keys: Vec<PropertyKey>,355	) -> DispatchResult {356		for key in property_keys {357			Self::delete_token_property(collection, sender, token_id, key)?;358		}359360		Ok(())361	}362363	pub fn set_collection_properties(364		collection: &NonfungibleHandle<T>,365		sender: &T::CrossAccountId,366		properties: Vec<Property>,367	) -> DispatchResult {368		<PalletCommon<T>>::set_collection_properties(collection, sender, properties)369	}370371	pub fn delete_collection_properties(372		collection: &CollectionHandle<T>,373		sender: &T::CrossAccountId,374		property_keys: Vec<PropertyKey>,375	) -> DispatchResult {376		<PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)377	}378379	pub fn set_property_permissions(380		collection: &CollectionHandle<T>,381		sender: &T::CrossAccountId,382		property_permissions: Vec<PropertyKeyPermission>,383	) -> DispatchResult {384		<PalletCommon<T>>::set_property_permissions(collection, sender, property_permissions)385	}386387	pub fn transfer(388		collection: &NonfungibleHandle<T>,389		from: &T::CrossAccountId,390		to: &T::CrossAccountId,391		token: TokenId,392		nesting_budget: &dyn Budget,393	) -> DispatchResult {394		ensure!(395			collection.limits.transfers_enabled(),396			<CommonError<T>>::TransferNotAllowed397		);398399		let token_data =400			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;401		// TODO: require sender to be token, owner, require admins to go through transfer_from402		ensure!(403			&token_data.owner == from404				|| (collection.limits.owner_can_transfer() && collection.is_owner_or_admin(from)),405			<CommonError<T>>::NoPermission406		);407408		if collection.access == AccessMode::AllowList {409			collection.check_allowlist(from)?;410			collection.check_allowlist(to)?;411		}412		<PalletCommon<T>>::ensure_correct_receiver(to)?;413414		let balance_from = <AccountBalance<T>>::get((collection.id, from))415			.checked_sub(1)416			.ok_or(<CommonError<T>>::TokenValueTooLow)?;417		let balance_to = if from != to {418			let balance_to = <AccountBalance<T>>::get((collection.id, to))419				.checked_add(1)420				.ok_or(ArithmeticError::Overflow)?;421422			ensure!(423				balance_to < collection.limits.account_token_ownership_limit(),424				<CommonError<T>>::AccountTokenLimitExceeded,425			);426427			Some(balance_to)428		} else {429			None430		};431432		if let Some(target) = T::CrossTokenAddressMapping::address_to_token(to) {433			let handle = <CollectionHandle<T>>::try_get(target.0)?;434			let dispatch = T::CollectionDispatch::dispatch(handle);435			let dispatch = dispatch.as_dyn();436437			dispatch.check_nesting(438				from.clone(),439				(collection.id, token),440				target.1,441				nesting_budget,442			)?;443		}444445		// =========446447		<TokenData<T>>::insert(448			(collection.id, token),449			ItemData {450				owner: to.clone(),451				..token_data452			},453		);454455		if let Some(balance_to) = balance_to {456			// from != to457			if balance_from == 0 {458				<AccountBalance<T>>::remove((collection.id, from));459			} else {460				<AccountBalance<T>>::insert((collection.id, from), balance_from);461			}462			<AccountBalance<T>>::insert((collection.id, to), balance_to);463			<Owned<T>>::remove((collection.id, from, token));464			<Owned<T>>::insert((collection.id, to, token), true);465		}466		Self::set_allowance_unchecked(collection, from, token, None, true);467468		collection.log_mirrored(ERC721Events::Transfer {469			from: *from.as_eth(),470			to: *to.as_eth(),471			token_id: token.into(),472		});473		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(474			collection.id,475			token,476			from.clone(),477			to.clone(),478			1,479		));480		Ok(())481	}482483	pub fn create_multiple_items(484		collection: &NonfungibleHandle<T>,485		sender: &T::CrossAccountId,486		data: Vec<CreateItemData<T>>,487		nesting_budget: &dyn Budget,488	) -> DispatchResult {489		if !collection.is_owner_or_admin(sender) {490			ensure!(491				collection.mint_mode,492				<CommonError<T>>::PublicMintingNotAllowed493			);494			collection.check_allowlist(sender)?;495496			for item in data.iter() {497				collection.check_allowlist(&item.owner)?;498			}499		}500501		for data in data.iter() {502			<PalletCommon<T>>::ensure_correct_receiver(&data.owner)?;503		}504505		let first_token = <TokensMinted<T>>::get(collection.id);506		let tokens_minted = first_token507			.checked_add(data.len() as u32)508			.ok_or(ArithmeticError::Overflow)?;509		ensure!(510			tokens_minted <= collection.limits.token_limit(),511			<CommonError<T>>::CollectionTokenLimitExceeded512		);513514		let mut balances = BTreeMap::new();515		for data in &data {516			let balance = balances517				.entry(&data.owner)518				.or_insert_with(|| <AccountBalance<T>>::get((collection.id, &data.owner)));519			*balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;520521			ensure!(522				*balance <= collection.limits.account_token_ownership_limit(),523				<CommonError<T>>::AccountTokenLimitExceeded,524			);525		}526527		for (i, data) in data.iter().enumerate() {528			let token = TokenId(first_token + i as u32 + 1);529			if let Some(target) = T::CrossTokenAddressMapping::address_to_token(&data.owner) {530				let handle = <CollectionHandle<T>>::try_get(target.0)?;531				let dispatch = T::CollectionDispatch::dispatch(handle);532				let dispatch = dispatch.as_dyn();533				dispatch.check_nesting(534					sender.clone(),535					(collection.id, token),536					target.1,537					nesting_budget,538				)?;539			}540		}541542		// =========543544		<TokensMinted<T>>::insert(collection.id, tokens_minted);545		for (account, balance) in balances {546			<AccountBalance<T>>::insert((collection.id, account), balance);547		}548		for (i, data) in data.into_iter().enumerate() {549			let token = first_token + i as u32 + 1;550551			<TokenData<T>>::insert(552				(collection.id, token),553				ItemData {554					const_data: data.const_data,555					variable_data: data.variable_data,556					owner: data.owner.clone(),557				},558			);559			<Owned<T>>::insert((collection.id, &data.owner, token), true);560561			collection.log_mirrored(ERC721Events::Transfer {562				from: H160::default(),563				to: *data.owner.as_eth(),564				token_id: token.into(),565			});566			<PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(567				collection.id,568				TokenId(token),569				data.owner.clone(),570				1,571			));572		}573		Ok(())574	}575576	pub fn set_allowance_unchecked(577		collection: &NonfungibleHandle<T>,578		sender: &T::CrossAccountId,579		token: TokenId,580		spender: Option<&T::CrossAccountId>,581		assume_implicit_eth: bool,582	) {583		if let Some(spender) = spender {584			let old_spender = <Allowance<T>>::get((collection.id, token));585			<Allowance<T>>::insert((collection.id, token), spender);586			// In ERC721 there is only one possible approved user of token, so we set587			// approved user to spender588			collection.log_mirrored(ERC721Events::Approval {589				owner: *sender.as_eth(),590				approved: *spender.as_eth(),591				token_id: token.into(),592			});593			// In Unique chain, any token can have any amount of approved users, so we need to594			// set allowance of old owner to 0, and allowance of new owner to 1595			if old_spender.as_ref() != Some(spender) {596				if let Some(old_owner) = old_spender {597					<PalletCommon<T>>::deposit_event(CommonEvent::Approved(598						collection.id,599						token,600						sender.clone(),601						old_owner,602						0,603					));604				}605				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(606					collection.id,607					token,608					sender.clone(),609					spender.clone(),610					1,611				));612			}613		} else {614			let old_spender = <Allowance<T>>::take((collection.id, token));615			if !assume_implicit_eth {616				// In ERC721 there is only one possible approved user of token, so we set617				// approved user to zero address618				collection.log_mirrored(ERC721Events::Approval {619					owner: *sender.as_eth(),620					approved: H160::default(),621					token_id: token.into(),622				});623			}624			// In Unique chain, any token can have any amount of approved users, so we need to625			// set allowance of old owner to 0626			if let Some(old_spender) = old_spender {627				<PalletCommon<T>>::deposit_event(CommonEvent::Approved(628					collection.id,629					token,630					sender.clone(),631					old_spender,632					0,633				));634			}635		}636	}637638	pub fn set_allowance(639		collection: &NonfungibleHandle<T>,640		sender: &T::CrossAccountId,641		token: TokenId,642		spender: Option<&T::CrossAccountId>,643	) -> DispatchResult {644		if collection.access == AccessMode::AllowList {645			collection.check_allowlist(sender)?;646			if let Some(spender) = spender {647				collection.check_allowlist(spender)?;648			}649		}650651		if let Some(spender) = spender {652			<PalletCommon<T>>::ensure_correct_receiver(spender)?;653		}654		let token_data =655			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;656		if &token_data.owner != sender {657			ensure!(658				collection.ignores_owned_amount(sender),659				<CommonError<T>>::CantApproveMoreThanOwned660			);661		}662663		// =========664665		Self::set_allowance_unchecked(collection, sender, token, spender, false);666		Ok(())667	}668669	fn check_allowed(670		collection: &NonfungibleHandle<T>,671		spender: &T::CrossAccountId,672		from: &T::CrossAccountId,673		token: TokenId,674		nesting_budget: &dyn Budget,675	) -> DispatchResult {676		if spender.conv_eq(from) {677			return Ok(());678		}679		if collection.access == AccessMode::AllowList {680			// `from`, `to` checked in [`transfer`]681			collection.check_allowlist(spender)?;682		}683		if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {684			// TODO: should collection owner be allowed to perform this transfer?685			ensure!(686				<PalletStructure<T>>::check_indirectly_owned(687					spender.clone(),688					source.0,689					source.1,690					None,691					nesting_budget692				)?,693				<CommonError<T>>::ApprovedValueTooLow,694			);695			return Ok(());696		}697		if <Allowance<T>>::get((collection.id, token)).as_ref() == Some(spender) {698			return Ok(());699		}700		ensure!(701			collection.ignores_allowance(spender),702			<CommonError<T>>::ApprovedValueTooLow703		);704		Ok(())705	}706707	pub fn transfer_from(708		collection: &NonfungibleHandle<T>,709		spender: &T::CrossAccountId,710		from: &T::CrossAccountId,711		to: &T::CrossAccountId,712		token: TokenId,713		nesting_budget: &dyn Budget,714	) -> DispatchResult {715		Self::check_allowed(collection, spender, from, token, nesting_budget)?;716717		// =========718719		// Allowance is reset in [`transfer`]720		Self::transfer(collection, from, to, token, nesting_budget)721	}722723	pub fn burn_from(724		collection: &NonfungibleHandle<T>,725		spender: &T::CrossAccountId,726		from: &T::CrossAccountId,727		token: TokenId,728		nesting_budget: &dyn Budget,729	) -> DispatchResult {730		Self::check_allowed(collection, spender, from, token, nesting_budget)?;731732		// =========733734		Self::burn(collection, from, token)735	}736737	pub fn set_variable_metadata(738		collection: &NonfungibleHandle<T>,739		sender: &T::CrossAccountId,740		token: TokenId,741		data: BoundedVec<u8, CustomDataLimit>,742	) -> DispatchResult {743		let token_data =744			<TokenData<T>>::get((collection.id, token)).ok_or(<CommonError<T>>::TokenNotFound)?;745		collection.check_can_update_meta(sender, &token_data.owner)?;746747		// =========748749		<TokenData<T>>::insert(750			(collection.id, token),751			ItemData {752				variable_data: data,753				..token_data754			},755		);756		Ok(())757	}758759	pub fn check_nesting(760		handle: &NonfungibleHandle<T>,761		sender: T::CrossAccountId,762		from: (CollectionId, TokenId),763		under: TokenId,764		nesting_budget: &dyn Budget,765	) -> DispatchResult {766		fn ensure_sender_allowed<T: Config>(767			collection: CollectionId,768			token: TokenId,769			for_nest: (CollectionId, TokenId),770			sender: T::CrossAccountId,771			budget: &dyn Budget,772		) -> DispatchResult {773			ensure!(774				<PalletStructure<T>>::check_indirectly_owned(775					sender,776					collection,777					token,778					Some(for_nest),779					budget780				)?,781				<CommonError<T>>::OnlyOwnerAllowedToNest,782			);783			Ok(())784		}785		match handle.limits.nesting_rule() {786			NestingRule::Disabled => fail!(<CommonError<T>>::NestingIsDisabled),787			NestingRule::Owner => {788				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?789			}790			NestingRule::OwnerRestricted(whitelist) => {791				ensure!(792					whitelist.contains(&from.0),793					<CommonError<T>>::SourceCollectionIsNotAllowedToNest794				);795				ensure_sender_allowed::<T>(handle.id, under, from, sender, nesting_budget)?796			}797		}798		Ok(())799	}800801	/// Delegated to `create_multiple_items`802	pub fn create_item(803		collection: &NonfungibleHandle<T>,804		sender: &T::CrossAccountId,805		data: CreateItemData<T>,806		nesting_budget: &dyn Budget,807	) -> DispatchResult {808		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)809	}810}
modifiedprimitives/data-structs/src/lib.rsdiffbeforeafterboth
--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -634,6 +634,7 @@
 pub type PropertyValue = BoundedVec<u8, ConstU32<MAX_PROPERTY_VALUE_LENGTH>>;
 
 #[derive(Encode, Decode, TypeInfo, Debug, MaxEncodedLen, PartialEq, Clone)]
+#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
 pub enum PropertyPermission {
 	None,
 	AdminConst,
@@ -644,14 +645,21 @@
 }
 
 #[derive(Encode, Decode, Debug, TypeInfo, Clone, PartialEq, MaxEncodedLen)]
+#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
 pub struct Property {
+	#[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))]
 	pub key: PropertyKey,
+
+	#[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))]
 	pub value: PropertyValue,
 }
 
 #[derive(Encode, Decode, TypeInfo, Debug, MaxEncodedLen, PartialEq, Clone)]
+#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
 pub struct PropertyKeyPermission {
+	#[cfg_attr(feature = "serde1", serde(with = "bounded::vec_serde"))]
 	pub key: PropertyKey,
+
 	pub permission: PropertyPermission,
 }
 
@@ -723,6 +731,10 @@
 	pub fn get_property(&self, key: &PropertyKey) -> Option<&PropertyValue> {
 		self.map.get(key)
 	}
+
+	pub fn iter(&self) -> impl Iterator<Item = (&PropertyKey, &PropertyValue)> {
+		self.map.iter()
+	}
 }
 
 pub struct CollectionProperties;
modifiedprimitives/rpc/src/lib.rsdiffbeforeafterboth
--- a/primitives/rpc/src/lib.rs
+++ b/primitives/rpc/src/lib.rs
@@ -16,7 +16,10 @@
 
 #![cfg_attr(not(feature = "std"), no_std)]
 
-use up_data_structs::{CollectionId, TokenId, RpcCollection, CollectionStats, CollectionLimits};
+use up_data_structs::{
+	CollectionId, TokenId, RpcCollection, CollectionStats, CollectionLimits, Property, PropertyKey,
+	PropertyKeyPermission,
+};
 use sp_std::vec::Vec;
 use codec::Decode;
 use sp_runtime::DispatchError;
@@ -41,6 +44,19 @@
 		fn const_metadata(collection: CollectionId, token: TokenId) -> Result<Vec<u8>>;
 		fn variable_metadata(collection: CollectionId, token: TokenId) -> Result<Vec<u8>>;
 
+		fn collection_properties(collection: CollectionId, properties: Vec<Vec<u8>>) -> Result<Vec<Property>>;
+
+		fn token_properties(
+			collection: CollectionId,
+			token_id: TokenId,
+			properties: Vec<Vec<u8>>
+		) -> Result<Vec<Property>>;
+
+		fn property_permissions(
+			collection: CollectionId,
+			properties: Vec<Vec<u8>>
+		) -> Result<Vec<PropertyKeyPermission>>;
+
 		fn total_supply(collection: CollectionId) -> Result<u32>;
 		fn account_balance(collection: CollectionId, account: CrossAccountId) -> Result<u32>;
 		fn balance(collection: CollectionId, account: CrossAccountId, token: TokenId) -> Result<u128>;
modifiedruntime/common/src/runtime_apis.rsdiffbeforeafterboth
--- a/runtime/common/src/runtime_apis.rs
+++ b/runtime/common/src/runtime_apis.rs
@@ -7,6 +7,14 @@
             $($custom_apis:tt)+
         )?
     ) => {
+        fn bytes_keys_to_property_keys(keys: Vec<Vec<u8>>) -> Result<Vec<PropertyKey>, DispatchError> {
+            keys.into_iter()
+                .map(|key| -> Result<PropertyKey, DispatchError> {
+                    key.try_into().map_err(|_| DispatchError::Other("Can't read property key"))
+                })
+                .collect::<Result<Vec<PropertyKey>, DispatchError>>()
+        }
+
         impl_runtime_apis! {
             $($($custom_apis)+)?
 
@@ -36,6 +44,76 @@
                     dispatch_unique_runtime!(collection.variable_metadata(token))
                 }
 
+                fn collection_properties(
+                    collection: CollectionId,
+                    keys: Vec<Vec<u8>>
+                ) -> Result<Vec<Property>, DispatchError> {
+                    let keys = bytes_keys_to_property_keys(keys)?;
+
+                    let properties = pallet_common::Pallet::<Runtime>::collection_properties(collection);
+
+                    let properties = keys.into_iter()
+                        .filter_map(|key| {
+                            properties.get_property(&key)
+                                .map(|value| {
+                                    Property {
+                                        key,
+                                        value: value.clone()
+                                    }
+                                })
+                        })
+                        .collect();
+
+                    Ok(properties)
+                }
+
+                fn token_properties(
+                    collection: CollectionId,
+                    token_id: TokenId,
+                    keys: Vec<Vec<u8>>
+                ) -> Result<Vec<Property>, DispatchError> {
+                    let keys = bytes_keys_to_property_keys(keys)?;
+
+                    let properties = pallet_nonfungible::Pallet::<Runtime>::token_properties((collection, token_id));
+
+                    let properties = keys.into_iter()
+                        .filter_map(|key| {
+                            properties.get_property(&key)
+                                .map(|value| {
+                                    Property {
+                                        key,
+                                        value: value.clone()
+                                    }
+                                })
+                        })
+                        .collect();
+
+                    Ok(properties)
+                }
+
+                fn property_permissions(
+                    collection: CollectionId,
+                    keys: Vec<Vec<u8>>
+                ) -> Result<Vec<PropertyKeyPermission>, DispatchError> {
+                    let keys = bytes_keys_to_property_keys(keys)?;
+
+                    let permissions = pallet_common::Pallet::<Runtime>::property_permissions(collection);
+
+                    let key_permissions = keys.into_iter()
+                        .filter_map(|key| {
+                            permissions.get(&key)
+                                .map(|permission| {
+                                    PropertyKeyPermission {
+                                        key,
+                                        permission: permission.clone()
+                                    }
+                                })
+                        })
+                        .collect();
+
+                    Ok(key_permissions)
+                }
+
                 fn total_supply(collection: CollectionId) -> Result<u32, DispatchError> {
                     dispatch_unique_runtime!(collection.total_supply())
                 }
modifiedruntime/opal/src/lib.rsdiffbeforeafterboth
--- a/runtime/opal/src/lib.rs
+++ b/runtime/opal/src/lib.rs
@@ -67,7 +67,7 @@
 	},
 };
 use up_data_structs::mapping::{EvmTokenAddressMapping, CrossTokenAddressMapping};
-use up_data_structs::{CollectionId, TokenId, CollectionStats, CollectionLimits, RpcCollection};
+use up_data_structs::*;
 // use pallet_contracts::weights::WeightInfo;
 // #[cfg(any(feature = "std", test))]
 use frame_system::{