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}
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