git.delta.rocks / unique-network / refs/commits / 62d6042adc19

difftreelog

feat add event TokenChanged

Trubnikov Sergey2022-12-08parent: #7680e66.patch.diff
in: master

8 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.1819pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};20use evm_coder::{21	abi::AbiType,22	solidity_interface, solidity, ToLog,23	types::*,24	types::Property as PropertyStruct,25	execution::{Result, Error},26	weight,27};28use pallet_evm_coder_substrate::dispatch_to_evm;29use sp_std::vec::Vec;30use up_data_structs::{31	AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,32	SponsoringRateLimit, SponsorshipState,33};34use alloc::format;3536use crate::{37	Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,38	eth::{EthCrossAccount, convert_cross_account_to_uint256},39	weights::WeightInfo,40};4142/// Events for ethereum collection helper.43#[derive(ToLog)]44pub enum CollectionHelpersEvents {45	/// The collection has been created.46	CollectionCreated {47		/// Collection owner.48		#[indexed]49		owner: address,5051		/// Collection ID.52		#[indexed]53		collection_id: address,54	},55	/// The collection has been destroyed.56	CollectionDestroyed {57		/// Collection ID.58		#[indexed]59		collection_id: address,60	},61	/// The collection has been changed.62	CollectionChanged {63		/// Collection ID.64		#[indexed]65		collection_id: address,66	},67}6869/// Does not always represent a full collection, for RFT it is either70/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).71pub trait CommonEvmHandler {72	/// Raw compiled binary code of the contract stub73	const CODE: &'static [u8];7475	/// Call precompiled handle.76	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;77}7879/// @title A contract that allows you to work with collections.80#[solidity_interface(name = Collection)]81impl<T: Config> CollectionHandle<T>82where83	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,84{85	/// Set collection property.86	///87	/// @param key Property key.88	/// @param value Propery value.89	#[solidity(hide)]90	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]91	fn set_collection_property(92		&mut self,93		caller: caller,94		key: string,95		value: bytes,96	) -> Result<void> {97		let caller = T::CrossAccountId::from_eth(caller);98		let key = <Vec<u8>>::from(key)99			.try_into()100			.map_err(|_| "key too large")?;101		let value = value.0.try_into().map_err(|_| "value too large")?;102103		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })104			.map_err(dispatch_to_evm::<T>)105	}106107	/// Set collection properties.108	///109	/// @param properties Vector of properties key/value pair.110	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]111	fn set_collection_properties(112		&mut self,113		caller: caller,114		properties: Vec<PropertyStruct>,115	) -> Result<void> {116		let caller = T::CrossAccountId::from_eth(caller);117118		let properties = properties119			.into_iter()120			.map(|PropertyStruct { key, value }| {121				let key = <Vec<u8>>::from(key)122					.try_into()123					.map_err(|_| "key too large")?;124125				let value = value.0.try_into().map_err(|_| "value too large")?;126127				Ok(Property { key, value })128			})129			.collect::<Result<Vec<_>>>()?;130131		<Pallet<T>>::set_collection_properties(self, &caller, properties)132			.map_err(dispatch_to_evm::<T>)133	}134135	/// Delete collection property.136	///137	/// @param key Property key.138	#[solidity(hide)]139	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]140	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {141		let caller = T::CrossAccountId::from_eth(caller);142		let key = <Vec<u8>>::from(key)143			.try_into()144			.map_err(|_| "key too large")?;145146		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)147	}148149	/// Delete collection properties.150	///151	/// @param keys Properties keys.152	#[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]153	fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {154		let caller = T::CrossAccountId::from_eth(caller);155		let keys = keys156			.into_iter()157			.map(|key| {158				<Vec<u8>>::from(key)159					.try_into()160					.map_err(|_| Error::Revert("key too large".into()))161			})162			.collect::<Result<Vec<_>>>()?;163164		<Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)165	}166167	/// Get collection property.168	///169	/// @dev Throws error if key not found.170	///171	/// @param key Property key.172	/// @return bytes The property corresponding to the key.173	fn collection_property(&self, key: string) -> Result<bytes> {174		let key = <Vec<u8>>::from(key)175			.try_into()176			.map_err(|_| "key too large")?;177178		let props = CollectionProperties::<T>::get(self.id);179		let prop = props.get(&key).ok_or("key not found")?;180181		Ok(bytes(prop.to_vec()))182	}183184	/// Get collection properties.185	///186	/// @param keys Properties keys. Empty keys for all propertyes.187	/// @return Vector of properties key/value pairs.188	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<PropertyStruct>> {189		let keys = keys190			.into_iter()191			.map(|key| {192				<Vec<u8>>::from(key)193					.try_into()194					.map_err(|_| Error::Revert("key too large".into()))195			})196			.collect::<Result<Vec<_>>>()?;197198		let properties = Pallet::<T>::filter_collection_properties(199			self.id,200			if keys.is_empty() { None } else { Some(keys) },201		)202		.map_err(dispatch_to_evm::<T>)?;203204		let properties = properties205			.into_iter()206			.map(|p| {207				let key =208					string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;209				let value = bytes(p.value.to_vec());210				Ok(PropertyStruct { key, value })211			})212			.collect::<Result<Vec<_>>>()?;213		Ok(properties)214	}215216	/// Set the sponsor of the collection.217	///218	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.219	///220	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.221	#[solidity(hide)]222	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {223		self.consume_store_reads_and_writes(1, 1)?;224225		let caller = T::CrossAccountId::from_eth(caller);226227		let sponsor = T::CrossAccountId::from_eth(sponsor);228		self.set_sponsor(&caller, sponsor.as_sub().clone())229			.map_err(dispatch_to_evm::<T>)230	}231232	/// Set the sponsor of the collection.233	///234	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.235	///236	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.237	fn set_collection_sponsor_cross(238		&mut self,239		caller: caller,240		sponsor: EthCrossAccount,241	) -> Result<void> {242		self.consume_store_reads_and_writes(1, 1)?;243244		let caller = T::CrossAccountId::from_eth(caller);245246		let sponsor = sponsor.into_sub_cross_account::<T>()?;247		self.set_sponsor(&caller, sponsor.as_sub().clone())248			.map_err(dispatch_to_evm::<T>)249	}250251	/// Whether there is a pending sponsor.252	fn has_collection_pending_sponsor(&self) -> Result<bool> {253		Ok(matches!(254			self.collection.sponsorship,255			SponsorshipState::Unconfirmed(_)256		))257	}258259	/// Collection sponsorship confirmation.260	///261	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.262	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {263		self.consume_store_writes(1)?;264265		let caller = T::CrossAccountId::from_eth(caller);266		self.confirm_sponsorship(caller.as_sub())267			.map_err(dispatch_to_evm::<T>)268	}269270	/// Remove collection sponsor.271	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {272		self.consume_store_reads_and_writes(1, 1)?;273		let caller = T::CrossAccountId::from_eth(caller);274		self.remove_sponsor(&caller).map_err(dispatch_to_evm::<T>)275	}276277	/// Get current sponsor.278	///279	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.280	fn collection_sponsor(&self) -> Result<(address, uint256)> {281		let sponsor = match self.collection.sponsorship.sponsor() {282			Some(sponsor) => sponsor,283			None => return Ok(Default::default()),284		};285		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());286		let result: (address, uint256) = if sponsor.is_canonical_substrate() {287			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);288			(Default::default(), sponsor)289		} else {290			let sponsor = *sponsor.as_eth();291			(sponsor, Default::default())292		};293		Ok(result)294	}295296	/// Set limits for the collection.297	/// @dev Throws error if limit not found.298	/// @param limit Name of the limit. Valid names:299	/// 	"accountTokenOwnershipLimit",300	/// 	"sponsoredDataSize",301	/// 	"sponsoredDataRateLimit",302	/// 	"tokenLimit",303	/// 	"sponsorTransferTimeout",304	/// 	"sponsorApproveTimeout"305	///  	"ownerCanTransfer",306	/// 	"ownerCanDestroy",307	/// 	"transfersEnabled"308	/// @param value Value of the limit.309	#[solidity(rename_selector = "setCollectionLimit")]310	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint256) -> Result<void> {311		self.consume_store_reads_and_writes(1, 1)?;312313		let value = value314			.try_into()315			.map_err(|_| Error::Revert(format!("can't convert value to u32 \"{}\"", value)))?;316317		let convert_value_to_bool = || match value {318			0 => Ok(false),319			1 => Ok(true),320			_ => {321				return Err(Error::Revert(format!(322					"can't convert value to boolean \"{}\"",323					value324				)))325			}326		};327328		let mut limits = self.limits.clone();329330		match limit.as_str() {331			"accountTokenOwnershipLimit" => {332				limits.account_token_ownership_limit = Some(value);333			}334			"sponsoredDataSize" => {335				limits.sponsored_data_size = Some(value);336			}337			"sponsoredDataRateLimit" => {338				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));339			}340			"tokenLimit" => {341				limits.token_limit = Some(value);342			}343			"sponsorTransferTimeout" => {344				limits.sponsor_transfer_timeout = Some(value);345			}346			"sponsorApproveTimeout" => {347				limits.sponsor_approve_timeout = Some(value);348			}349			"ownerCanTransfer" => {350				limits.owner_can_transfer = Some(convert_value_to_bool()?);351			}352			"ownerCanDestroy" => {353				limits.owner_can_destroy = Some(convert_value_to_bool()?);354			}355			"transfersEnabled" => {356				limits.transfers_enabled = Some(convert_value_to_bool()?);357			}358			_ => return Err(Error::Revert(format!("unknown limit \"{}\"", limit))),359		}360361		let caller = T::CrossAccountId::from_eth(caller);362		<Pallet<T>>::update_limits(&caller, self, limits).map_err(dispatch_to_evm::<T>)363	}364365	/// Get contract address.366	fn contract_address(&self) -> Result<address> {367		Ok(crate::eth::collection_id_to_address(self.id))368	}369370	/// Add collection admin.371	/// @param newAdmin Cross account administrator address.372	fn add_collection_admin_cross(373		&mut self,374		caller: caller,375		new_admin: EthCrossAccount,376	) -> Result<void> {377		self.consume_store_reads_and_writes(2, 2)?;378379		let caller = T::CrossAccountId::from_eth(caller);380		let new_admin = new_admin.into_sub_cross_account::<T>()?;381		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;382		Ok(())383	}384385	/// Remove collection admin.386	/// @param admin Cross account administrator address.387	fn remove_collection_admin_cross(388		&mut self,389		caller: caller,390		admin: EthCrossAccount,391	) -> Result<void> {392		self.consume_store_reads_and_writes(2, 2)?;393394		let caller = T::CrossAccountId::from_eth(caller);395		let admin = admin.into_sub_cross_account::<T>()?;396		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;397		Ok(())398	}399400	/// Add collection admin.401	/// @param newAdmin Address of the added administrator.402	#[solidity(hide)]403	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {404		self.consume_store_reads_and_writes(2, 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	#[solidity(hide)]416	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {417		self.consume_store_reads_and_writes(2, 2)?;418419		let caller = T::CrossAccountId::from_eth(caller);420		let admin = T::CrossAccountId::from_eth(admin);421		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;422		Ok(())423	}424425	/// Toggle accessibility of collection nesting.426	///427	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'428	#[solidity(rename_selector = "setCollectionNesting")]429	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {430		self.consume_store_reads_and_writes(1, 1)?;431432		let caller = T::CrossAccountId::from_eth(caller);433434		let mut permissions = self.collection.permissions.clone();435		let mut nesting = permissions.nesting().clone();436		nesting.token_owner = enable;437		nesting.restricted = None;438		permissions.nesting = Some(nesting);439440		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)441	}442443	/// Toggle accessibility of collection nesting.444	///445	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'446	/// @param collections Addresses of collections that will be available for nesting.447	#[solidity(rename_selector = "setCollectionNesting")]448	fn set_nesting(449		&mut self,450		caller: caller,451		enable: bool,452		collections: Vec<address>,453	) -> Result<void> {454		self.consume_store_reads_and_writes(1, 1)?;455456		if collections.is_empty() {457			return Err("no addresses provided".into());458		}459		let caller = T::CrossAccountId::from_eth(caller);460461		let mut permissions = self.collection.permissions.clone();462		match enable {463			false => {464				let mut nesting = permissions.nesting().clone();465				nesting.token_owner = false;466				nesting.restricted = None;467				permissions.nesting = Some(nesting);468			}469			true => {470				let mut bv = OwnerRestrictedSet::new();471				for i in collections {472					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {473						Error::Revert("Can't convert address into collection id".into())474					})?)475					.map_err(|_| "too many collections")?;476				}477				let mut nesting = permissions.nesting().clone();478				nesting.token_owner = true;479				nesting.restricted = Some(bv);480				permissions.nesting = Some(nesting);481			}482		};483484		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)485	}486487	/// Set the collection access method.488	/// @param mode Access mode489	/// 	0 for Normal490	/// 	1 for AllowList491	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {492		self.consume_store_reads_and_writes(1, 1)?;493494		let caller = T::CrossAccountId::from_eth(caller);495		let permissions = CollectionPermissions {496			access: Some(match mode {497				0 => AccessMode::Normal,498				1 => AccessMode::AllowList,499				_ => return Err("not supported access mode".into()),500			}),501			..Default::default()502		};503		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)504	}505506	/// Checks that user allowed to operate with collection.507	///508	/// @param user User address to check.509	fn allowlisted_cross(&self, user: EthCrossAccount) -> Result<bool> {510		let user = user.into_sub_cross_account::<T>()?;511		Ok(Pallet::<T>::allowed(self.id, user))512	}513514	/// Add the user to the allowed list.515	///516	/// @param user Address of a trusted user.517	#[solidity(hide)]518	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {519		self.consume_store_writes(1)?;520521		let caller = T::CrossAccountId::from_eth(caller);522		let user = T::CrossAccountId::from_eth(user);523		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;524		Ok(())525	}526527	/// Add user to allowed list.528	///529	/// @param user User cross account address.530	fn add_to_collection_allow_list_cross(531		&mut self,532		caller: caller,533		user: EthCrossAccount,534	) -> Result<void> {535		self.consume_store_writes(1)?;536537		let caller = T::CrossAccountId::from_eth(caller);538		let user = user.into_sub_cross_account::<T>()?;539		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;540		Ok(())541	}542543	/// Remove the user from the allowed list.544	///545	/// @param user Address of a removed user.546	#[solidity(hide)]547	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {548		self.consume_store_writes(1)?;549550		let caller = T::CrossAccountId::from_eth(caller);551		let user = T::CrossAccountId::from_eth(user);552		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;553		Ok(())554	}555556	/// Remove user from allowed list.557	///558	/// @param user User cross account address.559	fn remove_from_collection_allow_list_cross(560		&mut self,561		caller: caller,562		user: EthCrossAccount,563	) -> Result<void> {564		self.consume_store_writes(1)?;565566		let caller = T::CrossAccountId::from_eth(caller);567		let user = user.into_sub_cross_account::<T>()?;568		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;569		Ok(())570	}571572	/// Switch permission for minting.573	///574	/// @param mode Enable if "true".575	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {576		self.consume_store_reads_and_writes(1, 1)?;577578		let caller = T::CrossAccountId::from_eth(caller);579		let permissions = CollectionPermissions {580			mint_mode: Some(mode),581			..Default::default()582		};583		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)584	}585586	/// Check that account is the owner or admin of the collection587	///588	/// @param user account to verify589	/// @return "true" if account is the owner or admin590	#[solidity(hide, rename_selector = "isOwnerOrAdmin")]591	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {592		let user = T::CrossAccountId::from_eth(user);593		Ok(self.is_owner_or_admin(&user))594	}595596	/// Check that account is the owner or admin of the collection597	///598	/// @param user User cross account to verify599	/// @return "true" if account is the owner or admin600	fn is_owner_or_admin_cross(&self, user: EthCrossAccount) -> Result<bool> {601		let user = user.into_sub_cross_account::<T>()?;602		Ok(self.is_owner_or_admin(&user))603	}604605	/// Returns collection type606	///607	/// @return `Fungible` or `NFT` or `ReFungible`608	fn unique_collection_type(&self) -> Result<string> {609		let mode = match self.collection.mode {610			CollectionMode::Fungible(_) => "Fungible",611			CollectionMode::NFT => "NFT",612			CollectionMode::ReFungible => "ReFungible",613		};614		Ok(mode.into())615	}616617	/// Get collection owner.618	///619	/// @return Tuble with sponsor address and his substrate mirror.620	/// If address is canonical then substrate mirror is zero and vice versa.621	fn collection_owner(&self) -> Result<EthCrossAccount> {622		Ok(EthCrossAccount::from_sub_cross_account::<T>(623			&T::CrossAccountId::from_sub(self.owner.clone()),624		))625	}626627	/// Changes collection owner to another account628	///629	/// @dev Owner can be changed only by current owner630	/// @param newOwner new owner account631	#[solidity(hide, rename_selector = "changeCollectionOwner")]632	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {633		self.consume_store_writes(1)?;634635		let caller = T::CrossAccountId::from_eth(caller);636		let new_owner = T::CrossAccountId::from_eth(new_owner);637		self.change_owner(caller, new_owner)638			.map_err(dispatch_to_evm::<T>)639	}640641	/// Get collection administrators642	///643	/// @return Vector of tuples with admins address and his substrate mirror.644	/// If address is canonical then substrate mirror is zero and vice versa.645	fn collection_admins(&self) -> Result<Vec<EthCrossAccount>> {646		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))647			.map(|(admin, _)| EthCrossAccount::from_sub_cross_account::<T>(&admin))648			.collect();649		Ok(result)650	}651652	/// Changes collection owner to another account653	///654	/// @dev Owner can be changed only by current owner655	/// @param newOwner new owner cross account656	fn change_collection_owner_cross(657		&mut self,658		caller: caller,659		new_owner: EthCrossAccount,660	) -> Result<void> {661		self.consume_store_writes(1)?;662663		let caller = T::CrossAccountId::from_eth(caller);664		let new_owner = new_owner.into_sub_cross_account::<T>()?;665		self.change_owner(caller, new_owner)666			.map_err(dispatch_to_evm::<T>)667	}668}669670/// ### Note671/// Do not forget to add: `self.consume_store_reads(1)?;`672fn check_is_owner_or_admin<T: Config>(673	caller: caller,674	collection: &CollectionHandle<T>,675) -> Result<T::CrossAccountId> {676	let caller = T::CrossAccountId::from_eth(caller);677	collection678		.check_is_owner_or_admin(&caller)679		.map_err(dispatch_to_evm::<T>)?;680	Ok(caller)681}682683/// ### Note684/// Do not forget to add: `self.consume_store_writes(1)?;`685fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {686	collection687		.check_is_internal()688		.map_err(dispatch_to_evm::<T>)?;689	collection.save().map_err(dispatch_to_evm::<T>)?;690	Ok(())691}692693/// Contains static property keys and values.694pub mod static_property {695	use evm_coder::{696		execution::{Result, Error},697	};698	use alloc::format;699700	const EXPECT_CONVERT_ERROR: &str = "length < limit";701702	/// Keys.703	pub mod key {704		use super::*;705706		/// Key "baseURI".707		pub fn base_uri() -> up_data_structs::PropertyKey {708			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)709		}710711		/// Key "url".712		pub fn url() -> up_data_structs::PropertyKey {713			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)714		}715716		/// Key "suffix".717		pub fn suffix() -> up_data_structs::PropertyKey {718			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)719		}720721		/// Key "parentNft".722		pub fn parent_nft() -> up_data_structs::PropertyKey {723			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)724		}725	}726727	/// Convert `byte` to [`PropertyKey`].728	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {729		bytes.to_vec().try_into().map_err(|_| {730			Error::Revert(format!(731				"Property key is too long. Max length is {}.",732				up_data_structs::PropertyKey::bound()733			))734		})735	}736737	/// Convert `bytes` to [`PropertyValue`].738	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {739		bytes.to_vec().try_into().map_err(|_| {740			Error::Revert(format!(741				"Property key is too long. Max length is {}.",742				up_data_structs::PropertyKey::bound()743			))744		})745	}746}
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.1819pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};20use evm_coder::{21	abi::AbiType,22	solidity_interface, solidity, ToLog,23	types::*,24	types::Property as PropertyStruct,25	execution::{Result, Error},26	weight,27};28use pallet_evm_coder_substrate::dispatch_to_evm;29use sp_std::vec::Vec;30use up_data_structs::{31	AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,32	SponsoringRateLimit, SponsorshipState,33};34use alloc::format;3536use crate::{37	Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,38	eth::{EthCrossAccount, convert_cross_account_to_uint256},39	weights::WeightInfo,40};4142/// Events for ethereum collection helper.43#[derive(ToLog)]44pub enum CollectionHelpersEvents {45	/// The collection has been created.46	CollectionCreated {47		/// Collection owner.48		#[indexed]49		owner: address,5051		/// Collection ID.52		#[indexed]53		collection_id: address,54	},55	/// The collection has been destroyed.56	CollectionDestroyed {57		/// Collection ID.58		#[indexed]59		collection_id: address,60	},61	/// The collection has been changed.62	CollectionChanged {63		/// Collection ID.64		#[indexed]65		collection_id: address,66	},6768	/// The token has been changed.69	TokenChanged {70		/// Collection ID.71		#[indexed]72		collection_id: address,73		/// Token ID.74		token_id: uint256,75	},76}7778/// Does not always represent a full collection, for RFT it is either79/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).80pub trait CommonEvmHandler {81	/// Raw compiled binary code of the contract stub82	const CODE: &'static [u8];8384	/// Call precompiled handle.85	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;86}8788/// @title A contract that allows you to work with collections.89#[solidity_interface(name = Collection)]90impl<T: Config> CollectionHandle<T>91where92	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,93{94	/// Set collection property.95	///96	/// @param key Property key.97	/// @param value Propery value.98	#[solidity(hide)]99	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]100	fn set_collection_property(101		&mut self,102		caller: caller,103		key: string,104		value: bytes,105	) -> Result<void> {106		let caller = T::CrossAccountId::from_eth(caller);107		let key = <Vec<u8>>::from(key)108			.try_into()109			.map_err(|_| "key too large")?;110		let value = value.0.try_into().map_err(|_| "value too large")?;111112		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })113			.map_err(dispatch_to_evm::<T>)114	}115116	/// Set collection properties.117	///118	/// @param properties Vector of properties key/value pair.119	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]120	fn set_collection_properties(121		&mut self,122		caller: caller,123		properties: Vec<PropertyStruct>,124	) -> Result<void> {125		let caller = T::CrossAccountId::from_eth(caller);126127		let properties = properties128			.into_iter()129			.map(|PropertyStruct { key, value }| {130				let key = <Vec<u8>>::from(key)131					.try_into()132					.map_err(|_| "key too large")?;133134				let value = value.0.try_into().map_err(|_| "value too large")?;135136				Ok(Property { key, value })137			})138			.collect::<Result<Vec<_>>>()?;139140		<Pallet<T>>::set_collection_properties(self, &caller, properties)141			.map_err(dispatch_to_evm::<T>)142	}143144	/// Delete collection property.145	///146	/// @param key Property key.147	#[solidity(hide)]148	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]149	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {150		let caller = T::CrossAccountId::from_eth(caller);151		let key = <Vec<u8>>::from(key)152			.try_into()153			.map_err(|_| "key too large")?;154155		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)156	}157158	/// Delete collection properties.159	///160	/// @param keys Properties keys.161	#[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]162	fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {163		let caller = T::CrossAccountId::from_eth(caller);164		let keys = keys165			.into_iter()166			.map(|key| {167				<Vec<u8>>::from(key)168					.try_into()169					.map_err(|_| Error::Revert("key too large".into()))170			})171			.collect::<Result<Vec<_>>>()?;172173		<Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)174	}175176	/// Get collection property.177	///178	/// @dev Throws error if key not found.179	///180	/// @param key Property key.181	/// @return bytes The property corresponding to the key.182	fn collection_property(&self, key: string) -> Result<bytes> {183		let key = <Vec<u8>>::from(key)184			.try_into()185			.map_err(|_| "key too large")?;186187		let props = CollectionProperties::<T>::get(self.id);188		let prop = props.get(&key).ok_or("key not found")?;189190		Ok(bytes(prop.to_vec()))191	}192193	/// Get collection properties.194	///195	/// @param keys Properties keys. Empty keys for all propertyes.196	/// @return Vector of properties key/value pairs.197	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<PropertyStruct>> {198		let keys = keys199			.into_iter()200			.map(|key| {201				<Vec<u8>>::from(key)202					.try_into()203					.map_err(|_| Error::Revert("key too large".into()))204			})205			.collect::<Result<Vec<_>>>()?;206207		let properties = Pallet::<T>::filter_collection_properties(208			self.id,209			if keys.is_empty() { None } else { Some(keys) },210		)211		.map_err(dispatch_to_evm::<T>)?;212213		let properties = properties214			.into_iter()215			.map(|p| {216				let key =217					string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;218				let value = bytes(p.value.to_vec());219				Ok(PropertyStruct { key, value })220			})221			.collect::<Result<Vec<_>>>()?;222		Ok(properties)223	}224225	/// Set the sponsor of the collection.226	///227	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.228	///229	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.230	#[solidity(hide)]231	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {232		self.consume_store_reads_and_writes(1, 1)?;233234		let caller = T::CrossAccountId::from_eth(caller);235236		let sponsor = T::CrossAccountId::from_eth(sponsor);237		self.set_sponsor(&caller, sponsor.as_sub().clone())238			.map_err(dispatch_to_evm::<T>)239	}240241	/// Set the sponsor of the collection.242	///243	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.244	///245	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.246	fn set_collection_sponsor_cross(247		&mut self,248		caller: caller,249		sponsor: EthCrossAccount,250	) -> Result<void> {251		self.consume_store_reads_and_writes(1, 1)?;252253		let caller = T::CrossAccountId::from_eth(caller);254255		let sponsor = sponsor.into_sub_cross_account::<T>()?;256		self.set_sponsor(&caller, sponsor.as_sub().clone())257			.map_err(dispatch_to_evm::<T>)258	}259260	/// Whether there is a pending sponsor.261	fn has_collection_pending_sponsor(&self) -> Result<bool> {262		Ok(matches!(263			self.collection.sponsorship,264			SponsorshipState::Unconfirmed(_)265		))266	}267268	/// Collection sponsorship confirmation.269	///270	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.271	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {272		self.consume_store_writes(1)?;273274		let caller = T::CrossAccountId::from_eth(caller);275		self.confirm_sponsorship(caller.as_sub())276			.map_err(dispatch_to_evm::<T>)277	}278279	/// Remove collection sponsor.280	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {281		self.consume_store_reads_and_writes(1, 1)?;282		let caller = T::CrossAccountId::from_eth(caller);283		self.remove_sponsor(&caller).map_err(dispatch_to_evm::<T>)284	}285286	/// Get current sponsor.287	///288	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.289	fn collection_sponsor(&self) -> Result<(address, uint256)> {290		let sponsor = match self.collection.sponsorship.sponsor() {291			Some(sponsor) => sponsor,292			None => return Ok(Default::default()),293		};294		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());295		let result: (address, uint256) = if sponsor.is_canonical_substrate() {296			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);297			(Default::default(), sponsor)298		} else {299			let sponsor = *sponsor.as_eth();300			(sponsor, Default::default())301		};302		Ok(result)303	}304305	/// Set limits for the collection.306	/// @dev Throws error if limit not found.307	/// @param limit Name of the limit. Valid names:308	/// 	"accountTokenOwnershipLimit",309	/// 	"sponsoredDataSize",310	/// 	"sponsoredDataRateLimit",311	/// 	"tokenLimit",312	/// 	"sponsorTransferTimeout",313	/// 	"sponsorApproveTimeout"314	///  	"ownerCanTransfer",315	/// 	"ownerCanDestroy",316	/// 	"transfersEnabled"317	/// @param value Value of the limit.318	#[solidity(rename_selector = "setCollectionLimit")]319	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint256) -> Result<void> {320		self.consume_store_reads_and_writes(1, 1)?;321322		let value = value323			.try_into()324			.map_err(|_| Error::Revert(format!("can't convert value to u32 \"{}\"", value)))?;325326		let convert_value_to_bool = || match value {327			0 => Ok(false),328			1 => Ok(true),329			_ => {330				return Err(Error::Revert(format!(331					"can't convert value to boolean \"{}\"",332					value333				)))334			}335		};336337		let mut limits = self.limits.clone();338339		match limit.as_str() {340			"accountTokenOwnershipLimit" => {341				limits.account_token_ownership_limit = Some(value);342			}343			"sponsoredDataSize" => {344				limits.sponsored_data_size = Some(value);345			}346			"sponsoredDataRateLimit" => {347				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));348			}349			"tokenLimit" => {350				limits.token_limit = Some(value);351			}352			"sponsorTransferTimeout" => {353				limits.sponsor_transfer_timeout = Some(value);354			}355			"sponsorApproveTimeout" => {356				limits.sponsor_approve_timeout = Some(value);357			}358			"ownerCanTransfer" => {359				limits.owner_can_transfer = Some(convert_value_to_bool()?);360			}361			"ownerCanDestroy" => {362				limits.owner_can_destroy = Some(convert_value_to_bool()?);363			}364			"transfersEnabled" => {365				limits.transfers_enabled = Some(convert_value_to_bool()?);366			}367			_ => return Err(Error::Revert(format!("unknown limit \"{}\"", limit))),368		}369370		let caller = T::CrossAccountId::from_eth(caller);371		<Pallet<T>>::update_limits(&caller, self, limits).map_err(dispatch_to_evm::<T>)372	}373374	/// Get contract address.375	fn contract_address(&self) -> Result<address> {376		Ok(crate::eth::collection_id_to_address(self.id))377	}378379	/// Add collection admin.380	/// @param newAdmin Cross account administrator address.381	fn add_collection_admin_cross(382		&mut self,383		caller: caller,384		new_admin: EthCrossAccount,385	) -> Result<void> {386		self.consume_store_reads_and_writes(2, 2)?;387388		let caller = T::CrossAccountId::from_eth(caller);389		let new_admin = new_admin.into_sub_cross_account::<T>()?;390		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;391		Ok(())392	}393394	/// Remove collection admin.395	/// @param admin Cross account administrator address.396	fn remove_collection_admin_cross(397		&mut self,398		caller: caller,399		admin: EthCrossAccount,400	) -> Result<void> {401		self.consume_store_reads_and_writes(2, 2)?;402403		let caller = T::CrossAccountId::from_eth(caller);404		let admin = admin.into_sub_cross_account::<T>()?;405		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;406		Ok(())407	}408409	/// Add collection admin.410	/// @param newAdmin Address of the added administrator.411	#[solidity(hide)]412	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {413		self.consume_store_reads_and_writes(2, 2)?;414415		let caller = T::CrossAccountId::from_eth(caller);416		let new_admin = T::CrossAccountId::from_eth(new_admin);417		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;418		Ok(())419	}420421	/// Remove collection admin.422	///423	/// @param admin Address of the removed administrator.424	#[solidity(hide)]425	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {426		self.consume_store_reads_and_writes(2, 2)?;427428		let caller = T::CrossAccountId::from_eth(caller);429		let admin = T::CrossAccountId::from_eth(admin);430		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;431		Ok(())432	}433434	/// Toggle accessibility of collection nesting.435	///436	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'437	#[solidity(rename_selector = "setCollectionNesting")]438	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {439		self.consume_store_reads_and_writes(1, 1)?;440441		let caller = T::CrossAccountId::from_eth(caller);442443		let mut permissions = self.collection.permissions.clone();444		let mut nesting = permissions.nesting().clone();445		nesting.token_owner = enable;446		nesting.restricted = None;447		permissions.nesting = Some(nesting);448449		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)450	}451452	/// Toggle accessibility of collection nesting.453	///454	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'455	/// @param collections Addresses of collections that will be available for nesting.456	#[solidity(rename_selector = "setCollectionNesting")]457	fn set_nesting(458		&mut self,459		caller: caller,460		enable: bool,461		collections: Vec<address>,462	) -> Result<void> {463		self.consume_store_reads_and_writes(1, 1)?;464465		if collections.is_empty() {466			return Err("no addresses provided".into());467		}468		let caller = T::CrossAccountId::from_eth(caller);469470		let mut permissions = self.collection.permissions.clone();471		match enable {472			false => {473				let mut nesting = permissions.nesting().clone();474				nesting.token_owner = false;475				nesting.restricted = None;476				permissions.nesting = Some(nesting);477			}478			true => {479				let mut bv = OwnerRestrictedSet::new();480				for i in collections {481					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {482						Error::Revert("Can't convert address into collection id".into())483					})?)484					.map_err(|_| "too many collections")?;485				}486				let mut nesting = permissions.nesting().clone();487				nesting.token_owner = true;488				nesting.restricted = Some(bv);489				permissions.nesting = Some(nesting);490			}491		};492493		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)494	}495496	/// Set the collection access method.497	/// @param mode Access mode498	/// 	0 for Normal499	/// 	1 for AllowList500	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {501		self.consume_store_reads_and_writes(1, 1)?;502503		let caller = T::CrossAccountId::from_eth(caller);504		let permissions = CollectionPermissions {505			access: Some(match mode {506				0 => AccessMode::Normal,507				1 => AccessMode::AllowList,508				_ => return Err("not supported access mode".into()),509			}),510			..Default::default()511		};512		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)513	}514515	/// Checks that user allowed to operate with collection.516	///517	/// @param user User address to check.518	fn allowlisted_cross(&self, user: EthCrossAccount) -> Result<bool> {519		let user = user.into_sub_cross_account::<T>()?;520		Ok(Pallet::<T>::allowed(self.id, user))521	}522523	/// Add the user to the allowed list.524	///525	/// @param user Address of a trusted user.526	#[solidity(hide)]527	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {528		self.consume_store_writes(1)?;529530		let caller = T::CrossAccountId::from_eth(caller);531		let user = T::CrossAccountId::from_eth(user);532		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;533		Ok(())534	}535536	/// Add user to allowed list.537	///538	/// @param user User cross account address.539	fn add_to_collection_allow_list_cross(540		&mut self,541		caller: caller,542		user: EthCrossAccount,543	) -> Result<void> {544		self.consume_store_writes(1)?;545546		let caller = T::CrossAccountId::from_eth(caller);547		let user = user.into_sub_cross_account::<T>()?;548		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;549		Ok(())550	}551552	/// Remove the user from the allowed list.553	///554	/// @param user Address of a removed user.555	#[solidity(hide)]556	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {557		self.consume_store_writes(1)?;558559		let caller = T::CrossAccountId::from_eth(caller);560		let user = T::CrossAccountId::from_eth(user);561		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;562		Ok(())563	}564565	/// Remove user from allowed list.566	///567	/// @param user User cross account address.568	fn remove_from_collection_allow_list_cross(569		&mut self,570		caller: caller,571		user: EthCrossAccount,572	) -> Result<void> {573		self.consume_store_writes(1)?;574575		let caller = T::CrossAccountId::from_eth(caller);576		let user = user.into_sub_cross_account::<T>()?;577		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;578		Ok(())579	}580581	/// Switch permission for minting.582	///583	/// @param mode Enable if "true".584	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {585		self.consume_store_reads_and_writes(1, 1)?;586587		let caller = T::CrossAccountId::from_eth(caller);588		let permissions = CollectionPermissions {589			mint_mode: Some(mode),590			..Default::default()591		};592		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)593	}594595	/// Check that account is the owner or admin of the collection596	///597	/// @param user account to verify598	/// @return "true" if account is the owner or admin599	#[solidity(hide, rename_selector = "isOwnerOrAdmin")]600	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {601		let user = T::CrossAccountId::from_eth(user);602		Ok(self.is_owner_or_admin(&user))603	}604605	/// Check that account is the owner or admin of the collection606	///607	/// @param user User cross account to verify608	/// @return "true" if account is the owner or admin609	fn is_owner_or_admin_cross(&self, user: EthCrossAccount) -> Result<bool> {610		let user = user.into_sub_cross_account::<T>()?;611		Ok(self.is_owner_or_admin(&user))612	}613614	/// Returns collection type615	///616	/// @return `Fungible` or `NFT` or `ReFungible`617	fn unique_collection_type(&self) -> Result<string> {618		let mode = match self.collection.mode {619			CollectionMode::Fungible(_) => "Fungible",620			CollectionMode::NFT => "NFT",621			CollectionMode::ReFungible => "ReFungible",622		};623		Ok(mode.into())624	}625626	/// Get collection owner.627	///628	/// @return Tuble with sponsor address and his substrate mirror.629	/// If address is canonical then substrate mirror is zero and vice versa.630	fn collection_owner(&self) -> Result<EthCrossAccount> {631		Ok(EthCrossAccount::from_sub_cross_account::<T>(632			&T::CrossAccountId::from_sub(self.owner.clone()),633		))634	}635636	/// Changes collection owner to another account637	///638	/// @dev Owner can be changed only by current owner639	/// @param newOwner new owner account640	#[solidity(hide, rename_selector = "changeCollectionOwner")]641	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {642		self.consume_store_writes(1)?;643644		let caller = T::CrossAccountId::from_eth(caller);645		let new_owner = T::CrossAccountId::from_eth(new_owner);646		self.change_owner(caller, new_owner)647			.map_err(dispatch_to_evm::<T>)648	}649650	/// Get collection administrators651	///652	/// @return Vector of tuples with admins address and his substrate mirror.653	/// If address is canonical then substrate mirror is zero and vice versa.654	fn collection_admins(&self) -> Result<Vec<EthCrossAccount>> {655		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))656			.map(|(admin, _)| EthCrossAccount::from_sub_cross_account::<T>(&admin))657			.collect();658		Ok(result)659	}660661	/// Changes collection owner to another account662	///663	/// @dev Owner can be changed only by current owner664	/// @param newOwner new owner cross account665	fn change_collection_owner_cross(666		&mut self,667		caller: caller,668		new_owner: EthCrossAccount,669	) -> Result<void> {670		self.consume_store_writes(1)?;671672		let caller = T::CrossAccountId::from_eth(caller);673		let new_owner = new_owner.into_sub_cross_account::<T>()?;674		self.change_owner(caller, new_owner)675			.map_err(dispatch_to_evm::<T>)676	}677}678679/// ### Note680/// Do not forget to add: `self.consume_store_reads(1)?;`681fn check_is_owner_or_admin<T: Config>(682	caller: caller,683	collection: &CollectionHandle<T>,684) -> Result<T::CrossAccountId> {685	let caller = T::CrossAccountId::from_eth(caller);686	collection687		.check_is_owner_or_admin(&caller)688		.map_err(dispatch_to_evm::<T>)?;689	Ok(caller)690}691692/// ### Note693/// Do not forget to add: `self.consume_store_writes(1)?;`694fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {695	collection696		.check_is_internal()697		.map_err(dispatch_to_evm::<T>)?;698	collection.save().map_err(dispatch_to_evm::<T>)?;699	Ok(())700}701702/// Contains static property keys and values.703pub mod static_property {704	use evm_coder::{705		execution::{Result, Error},706	};707	use alloc::format;708709	const EXPECT_CONVERT_ERROR: &str = "length < limit";710711	/// Keys.712	pub mod key {713		use super::*;714715		/// Key "baseURI".716		pub fn base_uri() -> up_data_structs::PropertyKey {717			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)718		}719720		/// Key "url".721		pub fn url() -> up_data_structs::PropertyKey {722			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)723		}724725		/// Key "suffix".726		pub fn suffix() -> up_data_structs::PropertyKey {727			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)728		}729730		/// Key "parentNft".731		pub fn parent_nft() -> up_data_structs::PropertyKey {732			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)733		}734	}735736	/// Convert `byte` to [`PropertyKey`].737	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {738		bytes.to_vec().try_into().map_err(|_| {739			Error::Revert(format!(740				"Property key is too long. Max length is {}.",741				up_data_structs::PropertyKey::bound()742			))743		})744	}745746	/// Convert `bytes` to [`PropertyValue`].747	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {748		bytes.to_vec().try_into().map_err(|_| {749			Error::Revert(format!(750				"Property key is too long. Max length is {}.",751				up_data_structs::PropertyKey::bound()752			))753		})754	}755}
modifiedpallets/nonfungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -108,11 +108,11 @@
 use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
 use pallet_common::{
 	Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,
-	eth::collection_id_to_address,
+	eth::collection_id_to_address, erc::CollectionHelpersEvents,
 };
 use pallet_structure::{Pallet as PalletStructure, Error as StructureError};
 use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
-use sp_core::H160;
+use sp_core::{H160, Get};
 use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};
 use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};
 use core::ops::Deref;
@@ -673,6 +673,14 @@
 					));
 				}
 			}
+
+			<PalletEvm<T>>::deposit_log(
+				CollectionHelpersEvents::TokenChanged {
+					collection_id: collection_id_to_address(collection.id),
+					token_id: token_id.into(),
+				}
+				.to_log(T::ContractAddress::get()),
+			);
 		}
 
 		Ok(())
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -102,11 +102,11 @@
 use pallet_evm_coder_substrate::WithRecorder;
 use pallet_common::{
 	CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address,
-	Event as CommonEvent, Pallet as PalletCommon,
+	Event as CommonEvent, Pallet as PalletCommon, erc::CollectionHelpersEvents,
 };
 use pallet_structure::Pallet as PalletStructure;
 use scale_info::TypeInfo;
-use sp_core::H160;
+use sp_core::{Get, H160};
 use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};
 use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};
 use up_data_structs::{
@@ -649,6 +649,14 @@
 					));
 				}
 			}
+
+			<PalletEvm<T>>::deposit_log(
+				CollectionHelpersEvents::TokenChanged {
+					collection_id: collection_id_to_address(collection.id),
+					token_id: token_id.into(),
+				}
+				.to_log(T::ContractAddress::get()),
+			);
 		}
 
 		Ok(())
modifiedpallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth
--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -22,6 +22,7 @@
 	event CollectionCreated(address indexed owner, address indexed collectionId);
 	event CollectionDestroyed(address indexed collectionId);
 	event CollectionChanged(address indexed collectionId);
+	event TokenChanged(address indexed collectionId, uint256 tokenId);
 }
 
 /// @title Contract, which allows users to operate with collections
modifiedtests/src/eth/abi/collectionHelpers.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/collectionHelpers.json
+++ b/tests/src/eth/abi/collectionHelpers.json
@@ -45,6 +45,25 @@
     "type": "event"
   },
   {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "collectionId",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "tokenId",
+        "type": "uint256"
+      }
+    ],
+    "name": "TokenChanged",
+    "type": "event"
+  },
+  {
     "inputs": [
       { "internalType": "uint32", "name": "collectionId", "type": "uint32" }
     ],
modifiedtests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth
--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -17,6 +17,7 @@
 	event CollectionCreated(address indexed owner, address indexed collectionId);
 	event CollectionDestroyed(address indexed collectionId);
 	event CollectionChanged(address indexed collectionId);
+	event TokenChanged(address indexed collectionId, uint256 tokenId);
 }
 
 /// @title Contract, which allows users to operate with collections
modifiedtests/src/eth/events.test.tsdiffbeforeafterboth
--- a/tests/src/eth/events.test.ts
+++ b/tests/src/eth/events.test.ts
@@ -354,5 +354,48 @@
         }
         unsubscribe();
     });
-    
+     
+    itEth('CollectionChanged event for TokenPropertySet, TokenPropertyDeleted', async ({helper}) => {
+        const owner = await helper.eth.createAccountWithBalance(donor);
+        const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+        const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+        const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+        const result = await collection.methods.mint(owner).send({from: owner});
+        const tokenId = result.events.Transfer.returnValues.tokenId;
+        await collection.methods.setTokenPropertyPermission('A', true, true, true).send({from: owner});
+
+
+        const ethEvents: any = [];
+        collectionHelper.events.allEvents((_: any, event: any) => {
+            ethEvents.push(event);
+        });
+        const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['TokenPropertySet', 'TokenPropertyDeleted']}]);
+        {
+            await collection.methods.setProperties(tokenId, [{key: 'A', value: [1,2,3]}]).send({from: owner});
+            expect(ethEvents).to.be.like([
+                {
+                    event: 'TokenChanged',
+                    returnValues: {
+                        collectionId: collectionAddress
+                    }
+                }
+            ]);
+            expect(subEvents).to.be.like([{method: 'TokenPropertySet'}]);
+            ethEvents.pop();
+            subEvents.pop();
+        }
+        {
+            await collection.methods.deleteProperties(tokenId, ['A']).send({from: owner});
+            expect(ethEvents).to.be.like([
+                {
+                    event: 'TokenChanged',
+                    returnValues: {
+                        collectionId: collectionAddress
+                    }
+                }
+            ]);
+            expect(subEvents).to.be.like([{method: 'TokenPropertyDeleted'}]);
+        }
+        unsubscribe();
+    });
 });
\ No newline at end of file