git.delta.rocks / unique-network / refs/commits / 36fc5abfd9d0

difftreelog

feat add delete properties

Trubnikov Sergey2022-10-24parent: #ec90529.patch.diff
in: master

17 files changed

modifiedpallets/common/src/erc.rsdiffbeforeafterboth
before · pallets/common/src/erc.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//! This module contains the implementation of pallet methods for evm.1819use evm_coder::{20	solidity_interface, solidity, ToLog,21	types::*,22	execution::{Result, Error},23	weight,24};25pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};26use pallet_evm_coder_substrate::dispatch_to_evm;27use sp_std::vec::Vec;28use up_data_structs::{29	AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,30	SponsoringRateLimit, SponsorshipState, PropertyKey,31};32use alloc::format;3334use crate::{35	Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,36	eth::{37		convert_cross_account_to_uint256, convert_cross_account_to_tuple,38		convert_tuple_to_cross_account,39	},40	weights::WeightInfo,41};4243/// Events for ethereum collection helper.44#[derive(ToLog)]45pub enum CollectionHelpersEvents {46	/// The collection has been created.47	CollectionCreated {48		/// Collection owner.49		#[indexed]50		owner: address,5152		/// Collection ID.53		#[indexed]54		collection_id: address,55	},56	/// The collection has been destroyed.57	CollectionDestroyed {58		/// Collection ID.59		#[indexed]60		collection_id: address,61	},62}6364/// Does not always represent a full collection, for RFT it is either65/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).66pub trait CommonEvmHandler {67	const CODE: &'static [u8];6869	/// Call precompiled handle.70	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;71}7273/// @title A contract that allows you to work with collections.74#[solidity_interface(name = Collection)]75impl<T: Config> CollectionHandle<T>76where77	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,78{79	/// Set collection property.80	///81	/// @param key Property key.82	/// @param value Propery value.83	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]84	fn set_collection_property(85		&mut self,86		caller: caller,87		key: string,88		value: bytes,89	) -> Result<void> {90		let caller = T::CrossAccountId::from_eth(caller);91		let key = <Vec<u8>>::from(key)92			.try_into()93			.map_err(|_| "key too large")?;94		let value = value.0.try_into().map_err(|_| "value too large")?;9596		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })97			.map_err(dispatch_to_evm::<T>)98	}99100	/// Set collection properties.101	///102	/// @param properties Vector of properties key/value pair.103	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]104	fn set_collection_properties(105		&mut self,106		caller: caller,107		properties: Vec<(string, bytes)>,108	) -> Result<void> {109		for (key, value) in properties.into_iter() {110			self.set_collection_property(caller, key, value)?;111		}112		Ok(())113	}114115	/// Delete collection property.116	///117	/// @param key Property key.118	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]119	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {120		self.consume_store_reads_and_writes(1, 1)?;121122		let caller = T::CrossAccountId::from_eth(caller);123		let key = <Vec<u8>>::from(key)124			.try_into()125			.map_err(|_| "key too large")?;126127		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)128	}129130	/// Get collection property.131	///132	/// @dev Throws error if key not found.133	///134	/// @param key Property key.135	/// @return bytes The property corresponding to the key.136	fn collection_property(&self, key: string) -> Result<bytes> {137		let key = <Vec<u8>>::from(key)138			.try_into()139			.map_err(|_| "key too large")?;140141		let props = CollectionProperties::<T>::get(self.id);142		let prop = props.get(&key).ok_or("key not found")?;143144		Ok(bytes(prop.to_vec()))145	}146147	/// Get collection properties.148	///149	/// @param keys Properties keys.150	/// @return Vector of properties key/value pairs.151	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<(string, bytes)>> {152		let mut keys_ = Vec::<PropertyKey>::with_capacity(keys.len());153		for key in keys {154			keys_.push(155				<Vec<u8>>::from(key)156					.try_into()157					.map_err(|_| Error::Revert("key too large".into()))?,158			)159		}160		let properties = Pallet::<T>::filter_collection_properties(self.id, Some(keys_))161			.map_err(dispatch_to_evm::<T>)?;162163		let mut properties_ = Vec::<(string, bytes)>::with_capacity(properties.len());164		for p in properties {165			let key =166				string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;167			let value = bytes(p.value.to_vec());168			properties_.push((key, value));169		}170		Ok(properties_)171	}172173	/// Set the sponsor of the collection.174	///175	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.176	///177	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.178	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {179		self.consume_store_reads_and_writes(1, 1)?;180181		check_is_owner_or_admin(caller, self)?;182183		let sponsor = T::CrossAccountId::from_eth(sponsor);184		self.set_sponsor(sponsor.as_sub().clone())185			.map_err(dispatch_to_evm::<T>)?;186		save(self)187	}188189	/// Set the sponsor of the collection.190	///191	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.192	///193	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.194	fn set_collection_sponsor_cross(195		&mut self,196		caller: caller,197		sponsor: (address, uint256),198	) -> Result<void> {199		self.consume_store_reads_and_writes(1, 1)?;200201		check_is_owner_or_admin(caller, self)?;202203		let sponsor = convert_tuple_to_cross_account::<T>(sponsor)?;204		self.set_sponsor(sponsor.as_sub().clone())205			.map_err(dispatch_to_evm::<T>)?;206		save(self)207	}208209	/// Whether there is a pending sponsor.210	fn has_collection_pending_sponsor(&self) -> Result<bool> {211		Ok(matches!(212			self.collection.sponsorship,213			SponsorshipState::Unconfirmed(_)214		))215	}216217	/// Collection sponsorship confirmation.218	///219	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.220	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {221		self.consume_store_writes(1)?;222223		let caller = T::CrossAccountId::from_eth(caller);224		if !self225			.confirm_sponsorship(caller.as_sub())226			.map_err(dispatch_to_evm::<T>)?227		{228			return Err("caller is not set as sponsor".into());229		}230		save(self)231	}232233	/// Remove collection sponsor.234	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {235		self.consume_store_reads_and_writes(1, 1)?;236		check_is_owner_or_admin(caller, self)?;237		self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;238		save(self)239	}240241	/// Get current sponsor.242	///243	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.244	fn collection_sponsor(&self) -> Result<(address, uint256)> {245		let sponsor = match self.collection.sponsorship.sponsor() {246			Some(sponsor) => sponsor,247			None => return Ok(Default::default()),248		};249		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());250		let result: (address, uint256) = if sponsor.is_canonical_substrate() {251			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);252			(Default::default(), sponsor)253		} else {254			let sponsor = *sponsor.as_eth();255			(sponsor, Default::default())256		};257		Ok(result)258	}259260	/// Set limits for the collection.261	/// @dev Throws error if limit not found.262	/// @param limit Name of the limit. Valid names:263	/// 	"accountTokenOwnershipLimit",264	/// 	"sponsoredDataSize",265	/// 	"sponsoredDataRateLimit",266	/// 	"tokenLimit",267	/// 	"sponsorTransferTimeout",268	/// 	"sponsorApproveTimeout"269	/// @param value Value of the limit.270	#[solidity(rename_selector = "setCollectionLimit")]271	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {272		self.consume_store_reads_and_writes(1, 1)?;273274		check_is_owner_or_admin(caller, self)?;275		let mut limits = self.limits.clone();276277		match limit.as_str() {278			"accountTokenOwnershipLimit" => {279				limits.account_token_ownership_limit = Some(value);280			}281			"sponsoredDataSize" => {282				limits.sponsored_data_size = Some(value);283			}284			"sponsoredDataRateLimit" => {285				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));286			}287			"tokenLimit" => {288				limits.token_limit = Some(value);289			}290			"sponsorTransferTimeout" => {291				limits.sponsor_transfer_timeout = Some(value);292			}293			"sponsorApproveTimeout" => {294				limits.sponsor_approve_timeout = Some(value);295			}296			_ => {297				return Err(Error::Revert(format!(298					"unknown integer limit \"{}\"",299					limit300				)))301			}302		}303		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)304			.map_err(dispatch_to_evm::<T>)?;305		save(self)306	}307308	/// Set limits for the collection.309	/// @dev Throws error if limit not found.310	/// @param limit Name of the limit. Valid names:311	/// 	"ownerCanTransfer",312	/// 	"ownerCanDestroy",313	/// 	"transfersEnabled"314	/// @param value Value of the limit.315	#[solidity(rename_selector = "setCollectionLimit")]316	fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {317		self.consume_store_reads_and_writes(1, 1)?;318319		check_is_owner_or_admin(caller, self)?;320		let mut limits = self.limits.clone();321322		match limit.as_str() {323			"ownerCanTransfer" => {324				limits.owner_can_transfer = Some(value);325			}326			"ownerCanDestroy" => {327				limits.owner_can_destroy = Some(value);328			}329			"transfersEnabled" => {330				limits.transfers_enabled = Some(value);331			}332			_ => {333				return Err(Error::Revert(format!(334					"unknown boolean limit \"{}\"",335					limit336				)))337			}338		}339		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)340			.map_err(dispatch_to_evm::<T>)?;341		save(self)342	}343344	/// Get contract address.345	fn contract_address(&self) -> Result<address> {346		Ok(crate::eth::collection_id_to_address(self.id))347	}348349	/// Add collection admin.350	/// @param newAdmin Cross account administrator address.351	fn add_collection_admin_cross(352		&mut self,353		caller: caller,354		new_admin: (address, uint256),355	) -> Result<void> {356		self.consume_store_writes(2)?;357358		let caller = T::CrossAccountId::from_eth(caller);359		let new_admin = convert_tuple_to_cross_account::<T>(new_admin)?;360		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;361		Ok(())362	}363364	/// Remove collection admin.365	/// @param admin Cross account administrator address.366	fn remove_collection_admin_cross(367		&mut self,368		caller: caller,369		admin: (address, uint256),370	) -> Result<void> {371		self.consume_store_writes(2)?;372373		let caller = T::CrossAccountId::from_eth(caller);374		let admin = convert_tuple_to_cross_account::<T>(admin)?;375		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;376		Ok(())377	}378379	/// Add collection admin.380	/// @param newAdmin Address of the added administrator.381	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {382		self.consume_store_writes(2)?;383384		let caller = T::CrossAccountId::from_eth(caller);385		let new_admin = T::CrossAccountId::from_eth(new_admin);386		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;387		Ok(())388	}389390	/// Remove collection admin.391	///392	/// @param admin Address of the removed administrator.393	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {394		self.consume_store_writes(2)?;395396		let caller = T::CrossAccountId::from_eth(caller);397		let admin = T::CrossAccountId::from_eth(admin);398		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;399		Ok(())400	}401402	/// Toggle accessibility of collection nesting.403	///404	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'405	#[solidity(rename_selector = "setCollectionNesting")]406	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {407		self.consume_store_reads_and_writes(1, 1)?;408409		check_is_owner_or_admin(caller, self)?;410411		let mut permissions = self.collection.permissions.clone();412		let mut nesting = permissions.nesting().clone();413		nesting.token_owner = enable;414		nesting.restricted = None;415		permissions.nesting = Some(nesting);416417		self.collection.permissions = <Pallet<T>>::clamp_permissions(418			self.collection.mode.clone(),419			&self.collection.permissions,420			permissions,421		)422		.map_err(dispatch_to_evm::<T>)?;423424		save(self)425	}426427	/// Toggle accessibility of collection nesting.428	///429	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'430	/// @param collections Addresses of collections that will be available for nesting.431	#[solidity(rename_selector = "setCollectionNesting")]432	fn set_nesting(433		&mut self,434		caller: caller,435		enable: bool,436		collections: Vec<address>,437	) -> Result<void> {438		self.consume_store_reads_and_writes(1, 1)?;439440		if collections.is_empty() {441			return Err("no addresses provided".into());442		}443		check_is_owner_or_admin(caller, self)?;444445		let mut permissions = self.collection.permissions.clone();446		match enable {447			false => {448				let mut nesting = permissions.nesting().clone();449				nesting.token_owner = false;450				nesting.restricted = None;451				permissions.nesting = Some(nesting);452			}453			true => {454				let mut bv = OwnerRestrictedSet::new();455				for i in collections {456					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {457						Error::Revert("Can't convert address into collection id".into())458					})?)459					.map_err(|_| "too many collections")?;460				}461				let mut nesting = permissions.nesting().clone();462				nesting.token_owner = true;463				nesting.restricted = Some(bv);464				permissions.nesting = Some(nesting);465			}466		};467468		self.collection.permissions = <Pallet<T>>::clamp_permissions(469			self.collection.mode.clone(),470			&self.collection.permissions,471			permissions,472		)473		.map_err(dispatch_to_evm::<T>)?;474475		save(self)476	}477478	/// Set the collection access method.479	/// @param mode Access mode480	/// 	0 for Normal481	/// 	1 for AllowList482	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {483		self.consume_store_reads_and_writes(1, 1)?;484485		check_is_owner_or_admin(caller, self)?;486		let permissions = CollectionPermissions {487			access: Some(match mode {488				0 => AccessMode::Normal,489				1 => AccessMode::AllowList,490				_ => return Err("not supported access mode".into()),491			}),492			..Default::default()493		};494		self.collection.permissions = <Pallet<T>>::clamp_permissions(495			self.collection.mode.clone(),496			&self.collection.permissions,497			permissions,498		)499		.map_err(dispatch_to_evm::<T>)?;500501		save(self)502	}503504	/// Checks that user allowed to operate with collection.505	///506	/// @param user User address to check.507	fn allowed(&self, user: address) -> Result<bool> {508		Ok(Pallet::<T>::allowed(509			self.id,510			T::CrossAccountId::from_eth(user),511		))512	}513514	/// Add the user to the allowed list.515	///516	/// @param user Address of a trusted user.517	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {518		self.consume_store_writes(1)?;519520		let caller = T::CrossAccountId::from_eth(caller);521		let user = T::CrossAccountId::from_eth(user);522		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;523		Ok(())524	}525526	/// Add user to allowed list.527	///528	/// @param user User cross account address.529	fn add_to_collection_allow_list_cross(530		&mut self,531		caller: caller,532		user: (address, uint256),533	) -> Result<void> {534		self.consume_store_writes(1)?;535536		let caller = T::CrossAccountId::from_eth(caller);537		let user = convert_tuple_to_cross_account::<T>(user)?;538		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;539		Ok(())540	}541542	/// Remove the user from the allowed list.543	///544	/// @param user Address of a removed user.545	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {546		self.consume_store_writes(1)?;547548		let caller = T::CrossAccountId::from_eth(caller);549		let user = T::CrossAccountId::from_eth(user);550		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;551		Ok(())552	}553554	/// Remove user from allowed list.555	///556	/// @param user User cross account address.557	fn remove_from_collection_allow_list_cross(558		&mut self,559		caller: caller,560		user: (address, uint256),561	) -> Result<void> {562		self.consume_store_writes(1)?;563564		let caller = T::CrossAccountId::from_eth(caller);565		let user = convert_tuple_to_cross_account::<T>(user)?;566		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;567		Ok(())568	}569570	/// Switch permission for minting.571	///572	/// @param mode Enable if "true".573	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {574		self.consume_store_reads_and_writes(1, 1)?;575576		check_is_owner_or_admin(caller, self)?;577		let permissions = CollectionPermissions {578			mint_mode: Some(mode),579			..Default::default()580		};581		self.collection.permissions = <Pallet<T>>::clamp_permissions(582			self.collection.mode.clone(),583			&self.collection.permissions,584			permissions,585		)586		.map_err(dispatch_to_evm::<T>)?;587588		save(self)589	}590591	/// Check that account is the owner or admin of the collection592	///593	/// @param user account to verify594	/// @return "true" if account is the owner or admin595	#[solidity(rename_selector = "isOwnerOrAdmin")]596	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {597		let user = T::CrossAccountId::from_eth(user);598		Ok(self.is_owner_or_admin(&user))599	}600601	/// Check that account is the owner or admin of the collection602	///603	/// @param user User cross account to verify604	/// @return "true" if account is the owner or admin605	fn is_owner_or_admin_cross(&self, user: (address, uint256)) -> Result<bool> {606		let user = convert_tuple_to_cross_account::<T>(user)?;607		Ok(self.is_owner_or_admin(&user))608	}609610	/// Returns collection type611	///612	/// @return `Fungible` or `NFT` or `ReFungible`613	fn unique_collection_type(&self) -> Result<string> {614		let mode = match self.collection.mode {615			CollectionMode::Fungible(_) => "Fungible",616			CollectionMode::NFT => "NFT",617			CollectionMode::ReFungible => "ReFungible",618		};619		Ok(mode.into())620	}621622	/// Get collection owner.623	///624	/// @return Tuble with sponsor address and his substrate mirror.625	/// If address is canonical then substrate mirror is zero and vice versa.626	fn collection_owner(&self) -> Result<(address, uint256)> {627		Ok(convert_cross_account_to_tuple::<T>(628			&T::CrossAccountId::from_sub(self.owner.clone()),629		))630	}631632	/// Changes collection owner to another account633	///634	/// @dev Owner can be changed only by current owner635	/// @param newOwner new owner account636	#[solidity(rename_selector = "changeCollectionOwner")]637	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {638		self.consume_store_writes(1)?;639640		let caller = T::CrossAccountId::from_eth(caller);641		let new_owner = T::CrossAccountId::from_eth(new_owner);642		self.set_owner_internal(caller, new_owner)643			.map_err(dispatch_to_evm::<T>)644	}645646	/// Get collection administrators647	///648	/// @return Vector of tuples with admins address and his substrate mirror.649	/// If address is canonical then substrate mirror is zero and vice versa.650	fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {651		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))652			.map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::<T>(&admin))653			.collect();654		Ok(result)655	}656657	/// Changes collection owner to another account658	///659	/// @dev Owner can be changed only by current owner660	/// @param newOwner new owner cross account661	fn set_owner_cross(&mut self, caller: caller, new_owner: (address, uint256)) -> Result<void> {662		self.consume_store_writes(1)?;663664		let caller = T::CrossAccountId::from_eth(caller);665		let new_owner = convert_tuple_to_cross_account::<T>(new_owner)?;666		self.set_owner_internal(caller, new_owner)667			.map_err(dispatch_to_evm::<T>)668	}669}670671/// ### Note672/// Do not forget to add: `self.consume_store_reads(1)?;`673fn check_is_owner_or_admin<T: Config>(674	caller: caller,675	collection: &CollectionHandle<T>,676) -> Result<T::CrossAccountId> {677	let caller = T::CrossAccountId::from_eth(caller);678	collection679		.check_is_owner_or_admin(&caller)680		.map_err(dispatch_to_evm::<T>)?;681	Ok(caller)682}683684/// ### Note685/// Do not forget to add: `self.consume_store_writes(1)?;`686fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {687	collection688		.check_is_internal()689		.map_err(dispatch_to_evm::<T>)?;690	collection.save().map_err(dispatch_to_evm::<T>)?;691	Ok(())692}693694/// Contains static property keys and values.695pub mod static_property {696	use evm_coder::{697		execution::{Result, Error},698	};699	use alloc::format;700701	const EXPECT_CONVERT_ERROR: &str = "length < limit";702703	/// Keys.704	pub mod key {705		use super::*;706707		/// Key "baseURI".708		pub fn base_uri() -> up_data_structs::PropertyKey {709			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)710		}711712		/// Key "url".713		pub fn url() -> up_data_structs::PropertyKey {714			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)715		}716717		/// Key "suffix".718		pub fn suffix() -> up_data_structs::PropertyKey {719			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)720		}721722		/// Key "parentNft".723		pub fn parent_nft() -> up_data_structs::PropertyKey {724			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)725		}726	}727728	/// Convert `byte` to [`PropertyKey`].729	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {730		bytes.to_vec().try_into().map_err(|_| {731			Error::Revert(format!(732				"Property key is too long. Max length is {}.",733				up_data_structs::PropertyKey::bound()734			))735		})736	}737738	/// Convert `bytes` to [`PropertyValue`].739	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {740		bytes.to_vec().try_into().map_err(|_| {741			Error::Revert(format!(742				"Property key is too long. Max length is {}.",743				up_data_structs::PropertyKey::bound()744			))745		})746	}747}
after · pallets/common/src/erc.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//! This module contains the implementation of pallet methods for evm.1819use evm_coder::{20	solidity_interface, solidity, ToLog,21	types::*,22	execution::{Result, Error},23	weight,24};25pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};26use pallet_evm_coder_substrate::dispatch_to_evm;27use sp_std::vec::Vec;28use up_data_structs::{29	AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,30	SponsoringRateLimit, SponsorshipState, PropertyKey,31};32use alloc::format;3334use crate::{35	Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,36	eth::{37		convert_cross_account_to_uint256, convert_cross_account_to_tuple,38		convert_tuple_to_cross_account,39	},40	weights::WeightInfo,41};4243/// Events for ethereum collection helper.44#[derive(ToLog)]45pub enum CollectionHelpersEvents {46	/// The collection has been created.47	CollectionCreated {48		/// Collection owner.49		#[indexed]50		owner: address,5152		/// Collection ID.53		#[indexed]54		collection_id: address,55	},56	/// The collection has been destroyed.57	CollectionDestroyed {58		/// Collection ID.59		#[indexed]60		collection_id: address,61	},62}6364/// Does not always represent a full collection, for RFT it is either65/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).66pub trait CommonEvmHandler {67	const CODE: &'static [u8];6869	/// Call precompiled handle.70	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;71}7273/// @title A contract that allows you to work with collections.74#[solidity_interface(name = Collection)]75impl<T: Config> CollectionHandle<T>76where77	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,78{79	/// Set collection property.80	///81	/// @param key Property key.82	/// @param value Propery value.83	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]84	fn set_collection_property(85		&mut self,86		caller: caller,87		key: string,88		value: bytes,89	) -> Result<void> {90		let caller = T::CrossAccountId::from_eth(caller);91		let key = <Vec<u8>>::from(key)92			.try_into()93			.map_err(|_| "key too large")?;94		let value = value.0.try_into().map_err(|_| "value too large")?;9596		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })97			.map_err(dispatch_to_evm::<T>)98	}99100	/// Set collection properties.101	///102	/// @param properties Vector of properties key/value pair.103	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]104	fn set_collection_properties(105		&mut self,106		caller: caller,107		properties: Vec<(string, bytes)>,108	) -> Result<void> {109		for (key, value) in properties.into_iter() {110			self.set_collection_property(caller, key, value)?;111		}112		Ok(())113	}114115	/// Delete collection property.116	///117	/// @param key Property key.118	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]119	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {120		let caller = T::CrossAccountId::from_eth(caller);121		let key = <Vec<u8>>::from(key)122			.try_into()123			.map_err(|_| "key too large")?;124125		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)126	}127128	/// Delete collection properties.129	///130	/// @param keys Properties keys.131	#[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]132	fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {133		let caller = T::CrossAccountId::from_eth(caller);134		let keys = keys135			.into_iter()136			.map(|key| {137				<Vec<u8>>::from(key)138					.try_into()139					.map_err(|_| Error::Revert("key too large".into()))140			})141			.collect::<Result<Vec<_>>>()?;142143		<Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)144	}145146	/// Get collection property.147	///148	/// @dev Throws error if key not found.149	///150	/// @param key Property key.151	/// @return bytes The property corresponding to the key.152	fn collection_property(&self, key: string) -> Result<bytes> {153		let key = <Vec<u8>>::from(key)154			.try_into()155			.map_err(|_| "key too large")?;156157		let props = CollectionProperties::<T>::get(self.id);158		let prop = props.get(&key).ok_or("key not found")?;159160		Ok(bytes(prop.to_vec()))161	}162163	/// Get collection properties.164	///165	/// @param keys Properties keys. Empty keys for all propertyes.166	/// @return Vector of properties key/value pairs.167	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<(string, bytes)>> {168		let keys = keys169			.into_iter()170			.map(|key| {171				<Vec<u8>>::from(key)172					.try_into()173					.map_err(|_| Error::Revert("key too large".into()))174			})175			.collect::<Result<Vec<_>>>()?;176177		let properties = Pallet::<T>::filter_collection_properties(178			self.id,179			if keys.is_empty() { None } else { Some(keys) },180		)181		.map_err(dispatch_to_evm::<T>)?;182183		let properties = properties184			.into_iter()185			.map(|p| {186				let key =187					string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;188				let value = bytes(p.value.to_vec());189				Ok((key, value))190			})191			.collect::<Result<Vec<_>>>()?;192		Ok(properties)193	}194195	/// Set the sponsor of the collection.196	///197	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.198	///199	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.200	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {201		self.consume_store_reads_and_writes(1, 1)?;202203		check_is_owner_or_admin(caller, self)?;204205		let sponsor = T::CrossAccountId::from_eth(sponsor);206		self.set_sponsor(sponsor.as_sub().clone())207			.map_err(dispatch_to_evm::<T>)?;208		save(self)209	}210211	/// Set the sponsor of the collection.212	///213	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.214	///215	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.216	fn set_collection_sponsor_cross(217		&mut self,218		caller: caller,219		sponsor: (address, uint256),220	) -> Result<void> {221		self.consume_store_reads_and_writes(1, 1)?;222223		check_is_owner_or_admin(caller, self)?;224225		let sponsor = convert_tuple_to_cross_account::<T>(sponsor)?;226		self.set_sponsor(sponsor.as_sub().clone())227			.map_err(dispatch_to_evm::<T>)?;228		save(self)229	}230231	/// Whether there is a pending sponsor.232	fn has_collection_pending_sponsor(&self) -> Result<bool> {233		Ok(matches!(234			self.collection.sponsorship,235			SponsorshipState::Unconfirmed(_)236		))237	}238239	/// Collection sponsorship confirmation.240	///241	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.242	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {243		self.consume_store_writes(1)?;244245		let caller = T::CrossAccountId::from_eth(caller);246		if !self247			.confirm_sponsorship(caller.as_sub())248			.map_err(dispatch_to_evm::<T>)?249		{250			return Err("caller is not set as sponsor".into());251		}252		save(self)253	}254255	/// Remove collection sponsor.256	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {257		self.consume_store_reads_and_writes(1, 1)?;258		check_is_owner_or_admin(caller, self)?;259		self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;260		save(self)261	}262263	/// Get current sponsor.264	///265	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.266	fn collection_sponsor(&self) -> Result<(address, uint256)> {267		let sponsor = match self.collection.sponsorship.sponsor() {268			Some(sponsor) => sponsor,269			None => return Ok(Default::default()),270		};271		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());272		let result: (address, uint256) = if sponsor.is_canonical_substrate() {273			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);274			(Default::default(), sponsor)275		} else {276			let sponsor = *sponsor.as_eth();277			(sponsor, Default::default())278		};279		Ok(result)280	}281282	/// Set limits for the collection.283	/// @dev Throws error if limit not found.284	/// @param limit Name of the limit. Valid names:285	/// 	"accountTokenOwnershipLimit",286	/// 	"sponsoredDataSize",287	/// 	"sponsoredDataRateLimit",288	/// 	"tokenLimit",289	/// 	"sponsorTransferTimeout",290	/// 	"sponsorApproveTimeout"291	/// @param value Value of the limit.292	#[solidity(rename_selector = "setCollectionLimit")]293	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {294		self.consume_store_reads_and_writes(1, 1)?;295296		check_is_owner_or_admin(caller, self)?;297		let mut limits = self.limits.clone();298299		match limit.as_str() {300			"accountTokenOwnershipLimit" => {301				limits.account_token_ownership_limit = Some(value);302			}303			"sponsoredDataSize" => {304				limits.sponsored_data_size = Some(value);305			}306			"sponsoredDataRateLimit" => {307				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));308			}309			"tokenLimit" => {310				limits.token_limit = Some(value);311			}312			"sponsorTransferTimeout" => {313				limits.sponsor_transfer_timeout = Some(value);314			}315			"sponsorApproveTimeout" => {316				limits.sponsor_approve_timeout = Some(value);317			}318			_ => {319				return Err(Error::Revert(format!(320					"unknown integer limit \"{}\"",321					limit322				)))323			}324		}325		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)326			.map_err(dispatch_to_evm::<T>)?;327		save(self)328	}329330	/// Set limits for the collection.331	/// @dev Throws error if limit not found.332	/// @param limit Name of the limit. Valid names:333	/// 	"ownerCanTransfer",334	/// 	"ownerCanDestroy",335	/// 	"transfersEnabled"336	/// @param value Value of the limit.337	#[solidity(rename_selector = "setCollectionLimit")]338	fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {339		self.consume_store_reads_and_writes(1, 1)?;340341		check_is_owner_or_admin(caller, self)?;342		let mut limits = self.limits.clone();343344		match limit.as_str() {345			"ownerCanTransfer" => {346				limits.owner_can_transfer = Some(value);347			}348			"ownerCanDestroy" => {349				limits.owner_can_destroy = Some(value);350			}351			"transfersEnabled" => {352				limits.transfers_enabled = Some(value);353			}354			_ => {355				return Err(Error::Revert(format!(356					"unknown boolean limit \"{}\"",357					limit358				)))359			}360		}361		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)362			.map_err(dispatch_to_evm::<T>)?;363		save(self)364	}365366	/// Get contract address.367	fn contract_address(&self) -> Result<address> {368		Ok(crate::eth::collection_id_to_address(self.id))369	}370371	/// Add collection admin.372	/// @param newAdmin Cross account administrator address.373	fn add_collection_admin_cross(374		&mut self,375		caller: caller,376		new_admin: (address, uint256),377	) -> Result<void> {378		self.consume_store_writes(2)?;379380		let caller = T::CrossAccountId::from_eth(caller);381		let new_admin = convert_tuple_to_cross_account::<T>(new_admin)?;382		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;383		Ok(())384	}385386	/// Remove collection admin.387	/// @param admin Cross account administrator address.388	fn remove_collection_admin_cross(389		&mut self,390		caller: caller,391		admin: (address, uint256),392	) -> Result<void> {393		self.consume_store_writes(2)?;394395		let caller = T::CrossAccountId::from_eth(caller);396		let admin = convert_tuple_to_cross_account::<T>(admin)?;397		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;398		Ok(())399	}400401	/// Add collection admin.402	/// @param newAdmin Address of the added administrator.403	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {404		self.consume_store_writes(2)?;405406		let caller = T::CrossAccountId::from_eth(caller);407		let new_admin = T::CrossAccountId::from_eth(new_admin);408		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;409		Ok(())410	}411412	/// Remove collection admin.413	///414	/// @param admin Address of the removed administrator.415	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {416		self.consume_store_writes(2)?;417418		let caller = T::CrossAccountId::from_eth(caller);419		let admin = T::CrossAccountId::from_eth(admin);420		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;421		Ok(())422	}423424	/// Toggle accessibility of collection nesting.425	///426	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'427	#[solidity(rename_selector = "setCollectionNesting")]428	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {429		self.consume_store_reads_and_writes(1, 1)?;430431		check_is_owner_or_admin(caller, self)?;432433		let mut permissions = self.collection.permissions.clone();434		let mut nesting = permissions.nesting().clone();435		nesting.token_owner = enable;436		nesting.restricted = None;437		permissions.nesting = Some(nesting);438439		self.collection.permissions = <Pallet<T>>::clamp_permissions(440			self.collection.mode.clone(),441			&self.collection.permissions,442			permissions,443		)444		.map_err(dispatch_to_evm::<T>)?;445446		save(self)447	}448449	/// Toggle accessibility of collection nesting.450	///451	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'452	/// @param collections Addresses of collections that will be available for nesting.453	#[solidity(rename_selector = "setCollectionNesting")]454	fn set_nesting(455		&mut self,456		caller: caller,457		enable: bool,458		collections: Vec<address>,459	) -> Result<void> {460		self.consume_store_reads_and_writes(1, 1)?;461462		if collections.is_empty() {463			return Err("no addresses provided".into());464		}465		check_is_owner_or_admin(caller, self)?;466467		let mut permissions = self.collection.permissions.clone();468		match enable {469			false => {470				let mut nesting = permissions.nesting().clone();471				nesting.token_owner = false;472				nesting.restricted = None;473				permissions.nesting = Some(nesting);474			}475			true => {476				let mut bv = OwnerRestrictedSet::new();477				for i in collections {478					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {479						Error::Revert("Can't convert address into collection id".into())480					})?)481					.map_err(|_| "too many collections")?;482				}483				let mut nesting = permissions.nesting().clone();484				nesting.token_owner = true;485				nesting.restricted = Some(bv);486				permissions.nesting = Some(nesting);487			}488		};489490		self.collection.permissions = <Pallet<T>>::clamp_permissions(491			self.collection.mode.clone(),492			&self.collection.permissions,493			permissions,494		)495		.map_err(dispatch_to_evm::<T>)?;496497		save(self)498	}499500	/// Set the collection access method.501	/// @param mode Access mode502	/// 	0 for Normal503	/// 	1 for AllowList504	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {505		self.consume_store_reads_and_writes(1, 1)?;506507		check_is_owner_or_admin(caller, self)?;508		let permissions = CollectionPermissions {509			access: Some(match mode {510				0 => AccessMode::Normal,511				1 => AccessMode::AllowList,512				_ => return Err("not supported access mode".into()),513			}),514			..Default::default()515		};516		self.collection.permissions = <Pallet<T>>::clamp_permissions(517			self.collection.mode.clone(),518			&self.collection.permissions,519			permissions,520		)521		.map_err(dispatch_to_evm::<T>)?;522523		save(self)524	}525526	/// Checks that user allowed to operate with collection.527	///528	/// @param user User address to check.529	fn allowed(&self, user: address) -> Result<bool> {530		Ok(Pallet::<T>::allowed(531			self.id,532			T::CrossAccountId::from_eth(user),533		))534	}535536	/// Add the user to the allowed list.537	///538	/// @param user Address of a trusted user.539	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {540		self.consume_store_writes(1)?;541542		let caller = T::CrossAccountId::from_eth(caller);543		let user = T::CrossAccountId::from_eth(user);544		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;545		Ok(())546	}547548	/// Add user to allowed list.549	///550	/// @param user User cross account address.551	fn add_to_collection_allow_list_cross(552		&mut self,553		caller: caller,554		user: (address, uint256),555	) -> Result<void> {556		self.consume_store_writes(1)?;557558		let caller = T::CrossAccountId::from_eth(caller);559		let user = convert_tuple_to_cross_account::<T>(user)?;560		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;561		Ok(())562	}563564	/// Remove the user from the allowed list.565	///566	/// @param user Address of a removed user.567	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {568		self.consume_store_writes(1)?;569570		let caller = T::CrossAccountId::from_eth(caller);571		let user = T::CrossAccountId::from_eth(user);572		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;573		Ok(())574	}575576	/// Remove user from allowed list.577	///578	/// @param user User cross account address.579	fn remove_from_collection_allow_list_cross(580		&mut self,581		caller: caller,582		user: (address, uint256),583	) -> Result<void> {584		self.consume_store_writes(1)?;585586		let caller = T::CrossAccountId::from_eth(caller);587		let user = convert_tuple_to_cross_account::<T>(user)?;588		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;589		Ok(())590	}591592	/// Switch permission for minting.593	///594	/// @param mode Enable if "true".595	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {596		self.consume_store_reads_and_writes(1, 1)?;597598		check_is_owner_or_admin(caller, self)?;599		let permissions = CollectionPermissions {600			mint_mode: Some(mode),601			..Default::default()602		};603		self.collection.permissions = <Pallet<T>>::clamp_permissions(604			self.collection.mode.clone(),605			&self.collection.permissions,606			permissions,607		)608		.map_err(dispatch_to_evm::<T>)?;609610		save(self)611	}612613	/// Check that account is the owner or admin of the collection614	///615	/// @param user account to verify616	/// @return "true" if account is the owner or admin617	#[solidity(rename_selector = "isOwnerOrAdmin")]618	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {619		let user = T::CrossAccountId::from_eth(user);620		Ok(self.is_owner_or_admin(&user))621	}622623	/// Check that account is the owner or admin of the collection624	///625	/// @param user User cross account to verify626	/// @return "true" if account is the owner or admin627	fn is_owner_or_admin_cross(&self, user: (address, uint256)) -> Result<bool> {628		let user = convert_tuple_to_cross_account::<T>(user)?;629		Ok(self.is_owner_or_admin(&user))630	}631632	/// Returns collection type633	///634	/// @return `Fungible` or `NFT` or `ReFungible`635	fn unique_collection_type(&self) -> Result<string> {636		let mode = match self.collection.mode {637			CollectionMode::Fungible(_) => "Fungible",638			CollectionMode::NFT => "NFT",639			CollectionMode::ReFungible => "ReFungible",640		};641		Ok(mode.into())642	}643644	/// Get collection owner.645	///646	/// @return Tuble with sponsor address and his substrate mirror.647	/// If address is canonical then substrate mirror is zero and vice versa.648	fn collection_owner(&self) -> Result<(address, uint256)> {649		Ok(convert_cross_account_to_tuple::<T>(650			&T::CrossAccountId::from_sub(self.owner.clone()),651		))652	}653654	/// Changes collection owner to another account655	///656	/// @dev Owner can be changed only by current owner657	/// @param newOwner new owner account658	#[solidity(rename_selector = "changeCollectionOwner")]659	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {660		self.consume_store_writes(1)?;661662		let caller = T::CrossAccountId::from_eth(caller);663		let new_owner = T::CrossAccountId::from_eth(new_owner);664		self.set_owner_internal(caller, new_owner)665			.map_err(dispatch_to_evm::<T>)666	}667668	/// Get collection administrators669	///670	/// @return Vector of tuples with admins address and his substrate mirror.671	/// If address is canonical then substrate mirror is zero and vice versa.672	fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {673		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))674			.map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::<T>(&admin))675			.collect();676		Ok(result)677	}678679	/// Changes collection owner to another account680	///681	/// @dev Owner can be changed only by current owner682	/// @param newOwner new owner cross account683	fn set_owner_cross(&mut self, caller: caller, new_owner: (address, uint256)) -> Result<void> {684		self.consume_store_writes(1)?;685686		let caller = T::CrossAccountId::from_eth(caller);687		let new_owner = convert_tuple_to_cross_account::<T>(new_owner)?;688		self.set_owner_internal(caller, new_owner)689			.map_err(dispatch_to_evm::<T>)690	}691}692693/// ### Note694/// Do not forget to add: `self.consume_store_reads(1)?;`695fn check_is_owner_or_admin<T: Config>(696	caller: caller,697	collection: &CollectionHandle<T>,698) -> Result<T::CrossAccountId> {699	let caller = T::CrossAccountId::from_eth(caller);700	collection701		.check_is_owner_or_admin(&caller)702		.map_err(dispatch_to_evm::<T>)?;703	Ok(caller)704}705706/// ### Note707/// Do not forget to add: `self.consume_store_writes(1)?;`708fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {709	collection710		.check_is_internal()711		.map_err(dispatch_to_evm::<T>)?;712	collection.save().map_err(dispatch_to_evm::<T>)?;713	Ok(())714}715716/// Contains static property keys and values.717pub mod static_property {718	use evm_coder::{719		execution::{Result, Error},720	};721	use alloc::format;722723	const EXPECT_CONVERT_ERROR: &str = "length < limit";724725	/// Keys.726	pub mod key {727		use super::*;728729		/// Key "baseURI".730		pub fn base_uri() -> up_data_structs::PropertyKey {731			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)732		}733734		/// Key "url".735		pub fn url() -> up_data_structs::PropertyKey {736			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)737		}738739		/// Key "suffix".740		pub fn suffix() -> up_data_structs::PropertyKey {741			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)742		}743744		/// Key "parentNft".745		pub fn parent_nft() -> up_data_structs::PropertyKey {746			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)747		}748	}749750	/// Convert `byte` to [`PropertyKey`].751	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {752		bytes.to_vec().try_into().map_err(|_| {753			Error::Revert(format!(754				"Property key is too long. Max length is {}.",755				up_data_structs::PropertyKey::bound()756			))757		})758	}759760	/// Convert `bytes` to [`PropertyValue`].761	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {762		bytes.to_vec().try_into().map_err(|_| {763			Error::Revert(format!(764				"Property key is too long. Max length is {}.",765				up_data_structs::PropertyKey::bound()766			))767		})768	}769}
modifiedpallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth
--- a/pallets/fungible/src/stubs/UniqueFungible.sol
+++ b/pallets/fungible/src/stubs/UniqueFungible.sol
@@ -18,7 +18,7 @@
 }
 
 /// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x5d354410
+/// @dev the ERC-165 identifier for this interface is 0xb3152af3
 contract Collection is Dummy, ERC165 {
 	/// Set collection property.
 	///
@@ -55,6 +55,17 @@
 		dummy = 0;
 	}
 
+	/// Delete collection properties.
+	///
+	/// @param keys Properties keys.
+	/// @dev EVM selector for this function is: 0xee206ee3,
+	///  or in textual repr: deleteCollectionProperties(string[])
+	function deleteCollectionProperties(string[] memory keys) public {
+		require(false, stub_error);
+		keys;
+		dummy = 0;
+	}
+
 	/// Get collection property.
 	///
 	/// @dev Throws error if key not found.
@@ -72,7 +83,7 @@
 
 	/// Get collection properties.
 	///
-	/// @param keys Properties keys.
+	/// @param keys Properties keys. Empty keys for all propertyes.
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
modifiedpallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth
--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -91,7 +91,7 @@
 }
 
 /// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x5d354410
+/// @dev the ERC-165 identifier for this interface is 0xb3152af3
 contract Collection is Dummy, ERC165 {
 	/// Set collection property.
 	///
@@ -128,6 +128,17 @@
 		dummy = 0;
 	}
 
+	/// Delete collection properties.
+	///
+	/// @param keys Properties keys.
+	/// @dev EVM selector for this function is: 0xee206ee3,
+	///  or in textual repr: deleteCollectionProperties(string[])
+	function deleteCollectionProperties(string[] memory keys) public {
+		require(false, stub_error);
+		keys;
+		dummy = 0;
+	}
+
 	/// Get collection property.
 	///
 	/// @dev Throws error if key not found.
@@ -145,7 +156,7 @@
 
 	/// Get collection properties.
 	///
-	/// @param keys Properties keys.
+	/// @param keys Properties keys. Empty keys for all propertyes.
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -91,7 +91,7 @@
 }
 
 /// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x5d354410
+/// @dev the ERC-165 identifier for this interface is 0xb3152af3
 contract Collection is Dummy, ERC165 {
 	/// Set collection property.
 	///
@@ -128,6 +128,17 @@
 		dummy = 0;
 	}
 
+	/// Delete collection properties.
+	///
+	/// @param keys Properties keys.
+	/// @dev EVM selector for this function is: 0xee206ee3,
+	///  or in textual repr: deleteCollectionProperties(string[])
+	function deleteCollectionProperties(string[] memory keys) public {
+		require(false, stub_error);
+		keys;
+		dummy = 0;
+	}
+
 	/// Get collection property.
 	///
 	/// @dev Throws error if key not found.
@@ -145,7 +156,7 @@
 
 	/// Get collection properties.
 	///
-	/// @param keys Properties keys.
+	/// @param keys Properties keys. Empty keys for all propertyes.
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
modifiedtests/src/eth/api/UniqueFungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueFungible.sol
+++ b/tests/src/eth/api/UniqueFungible.sol
@@ -13,7 +13,7 @@
 }
 
 /// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x5d354410
+/// @dev the ERC-165 identifier for this interface is 0xb3152af3
 interface Collection is Dummy, ERC165 {
 	/// Set collection property.
 	///
@@ -37,6 +37,13 @@
 	///  or in textual repr: deleteCollectionProperty(string)
 	function deleteCollectionProperty(string memory key) external;
 
+	/// Delete collection properties.
+	///
+	/// @param keys Properties keys.
+	/// @dev EVM selector for this function is: 0xee206ee3,
+	///  or in textual repr: deleteCollectionProperties(string[])
+	function deleteCollectionProperties(string[] memory keys) external;
+
 	/// Get collection property.
 	///
 	/// @dev Throws error if key not found.
@@ -49,7 +56,7 @@
 
 	/// Get collection properties.
 	///
-	/// @param keys Properties keys.
+	/// @param keys Properties keys. Empty keys for all propertyes.
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
modifiedtests/src/eth/api/UniqueNFT.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -62,7 +62,7 @@
 }
 
 /// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x5d354410
+/// @dev the ERC-165 identifier for this interface is 0xb3152af3
 interface Collection is Dummy, ERC165 {
 	/// Set collection property.
 	///
@@ -86,6 +86,13 @@
 	///  or in textual repr: deleteCollectionProperty(string)
 	function deleteCollectionProperty(string memory key) external;
 
+	/// Delete collection properties.
+	///
+	/// @param keys Properties keys.
+	/// @dev EVM selector for this function is: 0xee206ee3,
+	///  or in textual repr: deleteCollectionProperties(string[])
+	function deleteCollectionProperties(string[] memory keys) external;
+
 	/// Get collection property.
 	///
 	/// @dev Throws error if key not found.
@@ -98,7 +105,7 @@
 
 	/// Get collection properties.
 	///
-	/// @param keys Properties keys.
+	/// @param keys Properties keys. Empty keys for all propertyes.
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -62,7 +62,7 @@
 }
 
 /// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x5d354410
+/// @dev the ERC-165 identifier for this interface is 0xb3152af3
 interface Collection is Dummy, ERC165 {
 	/// Set collection property.
 	///
@@ -86,6 +86,13 @@
 	///  or in textual repr: deleteCollectionProperty(string)
 	function deleteCollectionProperty(string memory key) external;
 
+	/// Delete collection properties.
+	///
+	/// @param keys Properties keys.
+	/// @dev EVM selector for this function is: 0xee206ee3,
+	///  or in textual repr: deleteCollectionProperties(string[])
+	function deleteCollectionProperties(string[] memory keys) external;
+
 	/// Get collection property.
 	///
 	/// @dev Throws error if key not found.
@@ -98,7 +105,7 @@
 
 	/// Get collection properties.
 	///
-	/// @param keys Properties keys.
+	/// @param keys Properties keys. Empty keys for all propertyes.
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
modifiedtests/src/eth/collectionProperties.test.tsdiffbeforeafterboth
--- a/tests/src/eth/collectionProperties.test.ts
+++ b/tests/src/eth/collectionProperties.test.ts
@@ -16,7 +16,7 @@
 
 import {itEth, usingEthPlaygrounds, expect, EthUniqueHelper} from './util';
 import {Pallets} from '../util';
-import {IProperty, ITokenPropertyPermission} from '../util/playgrounds/types';
+import {IProperty, ITokenPropertyPermission, TCollectionMode} from '../util/playgrounds/types';
 import {IKeyringPair} from '@polkadot/types/types';
 
 describe('EVM collection properties', () => {
@@ -163,29 +163,89 @@
 });
 
 describe('EVM collection property', () => {
-  itEth('Set/read properties', async ({helper, privateKey}) => {
-    const alice = await privateKey('//Alice');
-    const collection = await helper.nft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'});
+  let alice: IKeyringPair;
+
+  before(() => {
+    usingEthPlaygrounds(async (_helper, privateKey) => {
+      alice = await privateKey('//Alice');
+    });
+  });
+
+  async function testSetReadProperties(helper: EthUniqueHelper, mode: TCollectionMode) {
+    const collection = await helper[mode].mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'});
 
     const sender = await helper.eth.createAccountWithBalance(alice, 100n);
     await collection.addAdmin(alice, {Ethereum: sender});
 
     const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
-    const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', sender);
+    const contract = helper.ethNativeContract.collection(collectionAddress, mode, sender);
 
-    const key1 = 'key1';
-    const value1 = Buffer.from('value1');
-    
-    const key2 = 'key2';
-    const value2 = Buffer.from('value2');
+    const keys = ['key0', 'key1'];
 
     const writeProperties = [
-      [key1, '0x'+value1.toString('hex')],
-      [key2, '0x'+value2.toString('hex')],
+      helper.ethProperty.property(keys[0], 'value0'),
+      helper.ethProperty.property(keys[1], 'value1'),
     ];
 
     await contract.methods.setCollectionProperties(writeProperties).send();
-    const readProperties = await contract.methods.collectionProperties([key1, key2]).call();
+    const readProperties = await contract.methods.collectionProperties([keys[0], keys[1]]).call();
     expect(readProperties).to.be.like(writeProperties);
+  }
+
+  itEth('Set/read properties ft', async ({helper}) => {
+    await testSetReadProperties(helper, 'ft');
+  });
+  itEth('Set/read properties rft', async ({helper}) => {
+    await testSetReadProperties(helper, 'rft');
+  });
+  itEth('Set/read properties nft', async ({helper}) => {
+    await testSetReadProperties(helper, 'nft');
+  });
+
+  async function testDeleteProperties(helper: EthUniqueHelper, mode: TCollectionMode) {
+    const collection = await helper[mode].mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+    const sender = await helper.eth.createAccountWithBalance(alice, 100n);
+    await collection.addAdmin(alice, {Ethereum: sender});
+
+    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+    const contract = helper.ethNativeContract.collection(collectionAddress, mode, sender);
+
+    const keys = ['key0', 'key1', 'key2', 'key3'];
+
+    {
+      const writeProperties = [
+        helper.ethProperty.property(keys[0], 'value0'),
+        helper.ethProperty.property(keys[1], 'value1'),
+        helper.ethProperty.property(keys[2], 'value2'),
+        helper.ethProperty.property(keys[3], 'value3'),
+      ];
+
+      await contract.methods.setCollectionProperties(writeProperties).send();
+      const readProperties = await contract.methods.collectionProperties([keys[0], keys[1], keys[2], keys[3]]).call();
+      expect(readProperties).to.be.like(writeProperties);
+    }
+
+    {
+      const expectProperties = [
+        helper.ethProperty.property(keys[0], 'value0'),
+        helper.ethProperty.property(keys[1], 'value1'),
+      ];
+
+      await contract.methods.deleteCollectionProperties([keys[2], keys[3]]).send();
+      const readProperties = await contract.methods.collectionProperties([]).call();
+      expect(readProperties).to.be.like(expectProperties);
+    }
+  }
+  
+  itEth('Delete properties ft', async ({helper}) => {
+    await testDeleteProperties(helper, 'ft');
+  });
+  itEth('Delete properties rft', async ({helper}) => {
+    await testDeleteProperties(helper, 'rft');
   });
+  itEth('Delete properties nft', async ({helper}) => {
+    await testDeleteProperties(helper, 'nft');
+  });
+    
 });
modifiedtests/src/eth/fungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/fungibleAbi.json
+++ b/tests/src/eth/fungibleAbi.json
@@ -293,6 +293,15 @@
     "type": "function"
   },
   {
+    "inputs": [
+      { "internalType": "string[]", "name": "keys", "type": "string[]" }
+    ],
+    "name": "deleteCollectionProperties",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
     "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
     "name": "deleteCollectionProperty",
     "outputs": [],
modifiedtests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/nonFungibleAbi.json
+++ b/tests/src/eth/nonFungibleAbi.json
@@ -316,6 +316,15 @@
     "type": "function"
   },
   {
+    "inputs": [
+      { "internalType": "string[]", "name": "keys", "type": "string[]" }
+    ],
+    "name": "deleteCollectionProperties",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
     "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
     "name": "deleteCollectionProperty",
     "outputs": [],
modifiedtests/src/eth/reFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/reFungibleAbi.json
+++ b/tests/src/eth/reFungibleAbi.json
@@ -298,6 +298,15 @@
     "type": "function"
   },
   {
+    "inputs": [
+      { "internalType": "string[]", "name": "keys", "type": "string[]" }
+    ],
+    "name": "deleteCollectionProperties",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
     "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
     "name": "deleteCollectionProperty",
     "outputs": [],
modifiedtests/src/eth/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/types.ts
+++ b/tests/src/eth/util/playgrounds/types.ts
@@ -19,3 +19,6 @@
   readonly field_0: string,
   readonly field_1: string | Uint8Array,
 }
+
+export type EthProperty = string[];
+
modifiedtests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -18,7 +18,7 @@
 
 import {DevUniqueHelper} from '../../../util/playgrounds/unique.dev';
 
-import {ContractImports, CompiledContract, TEthCrossAccount, NormalizedEvent} from './types';
+import {ContractImports, CompiledContract, TEthCrossAccount, NormalizedEvent, EthProperty} from './types';
 
 // Native contracts ABI
 import collectionHelpersAbi from '../../collectionHelpersAbi.json';
@@ -28,6 +28,7 @@
 import refungibleTokenAbi from '../../reFungibleTokenAbi.json';
 import contractHelpersAbi from './../contractHelpersAbi.json';
 import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types';
+import {TCollectionMode} from '../../../util/playgrounds/types';
 
 class EthGroupBase {
   helper: EthUniqueHelper;
@@ -107,7 +108,7 @@
     return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, gas: this.helper.eth.DEFAULT_GAS});
   }
 
-  collection(address: string, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract {
+  collection(address: string, mode: TCollectionMode, caller?: string): Contract {
     const abi = {
       'nft': nonFungibleAbi,
       'rft': refungibleAbi,
@@ -336,8 +337,16 @@
   normalizeAddress(address: string): string {
     return '0x' + address.substring(address.length - 40);
   }
-}
+}  
 
+export class EthPropertyGroup extends EthGroupBase {
+  property(key: string, value: string): EthProperty {
+    return [
+      key, 
+      '0x'+Buffer.from(value).toString('hex'),
+    ];
+  }
+}
 export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper;
 
 export class EthCrossAccountGroup extends EthGroupBase {
@@ -369,6 +378,7 @@
   ethNativeContract: NativeContractGroup;
   ethContract: ContractGroup;
   ethCrossAccount: EthCrossAccountGroup;
+  ethProperty: EthPropertyGroup;
 
   constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
     options.helperBase = options.helperBase ?? EthUniqueHelper;
@@ -379,6 +389,7 @@
     this.ethCrossAccount = new EthCrossAccountGroup(this);
     this.ethNativeContract = new NativeContractGroup(this);
     this.ethContract = new ContractGroup(this);
+    this.ethProperty = new EthPropertyGroup(this);
   }
 
   getWeb3(): Web3 {
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -224,3 +224,4 @@
 export type TRelayNetworks = 'rococo' | 'westend';
 export type TNetworks = TUniqueNetworks | TSiblingNetworkds | TRelayNetworks;
 export type TSigner = IKeyringPair; // | 'string'
+export type TCollectionMode = 'nft' | 'rft' | 'ft';