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

difftreelog

source

pallets/common/src/erc.rs21.8 KiBsourcehistory
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,31};32use alloc::format;3334use crate::{35	Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,36	eth::{37		convert_cross_account_to_uint256, convert_uint256_to_cross_account,38		convert_cross_account_to_tuple,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}5758/// Does not always represent a full collection, for RFT it is either59/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).60pub trait CommonEvmHandler {61	const CODE: &'static [u8];6263	/// Call precompiled handle.64	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;65}6667/// @title A contract that allows you to work with collections.68#[solidity_interface(name = Collection)]69impl<T: Config> CollectionHandle<T>70where71	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,72{73	/// Set collection property.74	///75	/// @param key Property key.76	/// @param value Propery value.77	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]78	fn set_collection_property(79		&mut self,80		caller: caller,81		key: string,82		value: bytes,83	) -> Result<void> {84		let caller = T::CrossAccountId::from_eth(caller);85		let key = <Vec<u8>>::from(key)86			.try_into()87			.map_err(|_| "key too large")?;88		let value = value.try_into().map_err(|_| "value too large")?;8990		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })91			.map_err(dispatch_to_evm::<T>)92	}9394	/// Delete collection property.95	///96	/// @param key Property key.97	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]98	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {99		self.consume_store_reads_and_writes(1, 1)?;100101		let caller = T::CrossAccountId::from_eth(caller);102		let key = <Vec<u8>>::from(key)103			.try_into()104			.map_err(|_| "key too large")?;105106		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)107	}108109	/// Get collection property.110	///111	/// @dev Throws error if key not found.112	///113	/// @param key Property key.114	/// @return bytes The property corresponding to the key.115	fn collection_property(&self, key: string) -> Result<bytes> {116		let key = <Vec<u8>>::from(key)117			.try_into()118			.map_err(|_| "key too large")?;119120		let props = <CollectionProperties<T>>::get(self.id);121		let prop = props.get(&key).ok_or("key not found")?;122123		Ok(prop.to_vec())124	}125126	/// Set the sponsor of the collection.127	///128	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.129	///130	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.131	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {132		self.consume_store_reads_and_writes(1, 1)?;133134		check_is_owner_or_admin(caller, self)?;135136		let sponsor = T::CrossAccountId::from_eth(sponsor);137		self.set_sponsor(sponsor.as_sub().clone())138			.map_err(dispatch_to_evm::<T>)?;139		save(self)140	}141142	// TODO: Temprorary off. Need refactor143	// /// Set the substrate sponsor of the collection.144	// ///145	// /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.146	// ///147	// /// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract.148	// fn set_collection_sponsor_substrate(149	// 	&mut self,150	// 	caller: caller,151	// 	sponsor: uint256,152	// ) -> Result<void> {153	// 	self.consume_store_reads_and_writes(1, 1)?;154155	// 	check_is_owner_or_admin(caller, self)?;156157	// 	let sponsor = convert_uint256_to_cross_account::<T>(sponsor);158	// 	self.set_sponsor(sponsor.as_sub().clone())159	// 		.map_err(dispatch_to_evm::<T>)?;160	// 	save(self)161	// }162163	/// Whether there is a pending sponsor.164	fn has_collection_pending_sponsor(&self) -> Result<bool> {165		Ok(matches!(166			self.collection.sponsorship,167			SponsorshipState::Unconfirmed(_)168		))169	}170171	/// Collection sponsorship confirmation.172	///173	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.174	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {175		self.consume_store_writes(1)?;176177		let caller = T::CrossAccountId::from_eth(caller);178		if !self179			.confirm_sponsorship(caller.as_sub())180			.map_err(dispatch_to_evm::<T>)?181		{182			return Err("caller is not set as sponsor".into());183		}184		save(self)185	}186187	/// Remove collection sponsor.188	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {189		self.consume_store_reads_and_writes(1, 1)?;190		check_is_owner_or_admin(caller, self)?;191		self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;192		save(self)193	}194195	/// Get current sponsor.196	///197	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.198	fn collection_sponsor(&self) -> Result<(address, uint256)> {199		let sponsor = match self.collection.sponsorship.sponsor() {200			Some(sponsor) => sponsor,201			None => return Ok(Default::default()),202		};203		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());204		let result: (address, uint256) = if sponsor.is_canonical_substrate() {205			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);206			(Default::default(), sponsor)207		} else {208			let sponsor = *sponsor.as_eth();209			(sponsor, Default::default())210		};211		Ok(result)212	}213214	/// Set limits for the collection.215	/// @dev Throws error if limit not found.216	/// @param limit Name of the limit. Valid names:217	/// 	"accountTokenOwnershipLimit",218	/// 	"sponsoredDataSize",219	/// 	"sponsoredDataRateLimit",220	/// 	"tokenLimit",221	/// 	"sponsorTransferTimeout",222	/// 	"sponsorApproveTimeout"223	/// @param value Value of the limit.224	#[solidity(rename_selector = "setCollectionLimit")]225	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {226		self.consume_store_reads_and_writes(1, 1)?;227228		check_is_owner_or_admin(caller, self)?;229		let mut limits = self.limits.clone();230231		match limit.as_str() {232			"accountTokenOwnershipLimit" => {233				limits.account_token_ownership_limit = Some(value);234			}235			"sponsoredDataSize" => {236				limits.sponsored_data_size = Some(value);237			}238			"sponsoredDataRateLimit" => {239				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));240			}241			"tokenLimit" => {242				limits.token_limit = Some(value);243			}244			"sponsorTransferTimeout" => {245				limits.sponsor_transfer_timeout = Some(value);246			}247			"sponsorApproveTimeout" => {248				limits.sponsor_approve_timeout = Some(value);249			}250			_ => {251				return Err(Error::Revert(format!(252					"unknown integer limit \"{}\"",253					limit254				)))255			}256		}257		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)258			.map_err(dispatch_to_evm::<T>)?;259		save(self)260	}261262	/// Set limits for the collection.263	/// @dev Throws error if limit not found.264	/// @param limit Name of the limit. Valid names:265	/// 	"ownerCanTransfer",266	/// 	"ownerCanDestroy",267	/// 	"transfersEnabled"268	/// @param value Value of the limit.269	#[solidity(rename_selector = "setCollectionLimit")]270	fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {271		self.consume_store_reads_and_writes(1, 1)?;272273		check_is_owner_or_admin(caller, self)?;274		let mut limits = self.limits.clone();275276		match limit.as_str() {277			"ownerCanTransfer" => {278				limits.owner_can_transfer = Some(value);279			}280			"ownerCanDestroy" => {281				limits.owner_can_destroy = Some(value);282			}283			"transfersEnabled" => {284				limits.transfers_enabled = Some(value);285			}286			_ => {287				return Err(Error::Revert(format!(288					"unknown boolean limit \"{}\"",289					limit290				)))291			}292		}293		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)294			.map_err(dispatch_to_evm::<T>)?;295		save(self)296	}297298	/// Get contract address.299	fn contract_address(&self) -> Result<address> {300		Ok(crate::eth::collection_id_to_address(self.id))301	}302303	// TODO: Temprorary off. Need refactor304	// /// Add collection admin by substrate address.305	// /// @param newAdmin Substrate administrator address.306	// fn add_collection_admin_substrate(307	// 	&mut self,308	// 	caller: caller,309	// 	new_admin: uint256,310	// ) -> Result<void> {311	// 	self.consume_store_writes(2)?;312313	// 	let caller = T::CrossAccountId::from_eth(caller);314	// 	let new_admin = convert_uint256_to_cross_account::<T>(new_admin);315	// 	<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;316	// 	Ok(())317	// }318319	// TODO: Temprorary off. Need refactor320	// /// Remove collection admin by substrate address.321	// /// @param admin Substrate administrator address.322	// fn remove_collection_admin_substrate(323	// 	&mut self,324	// 	caller: caller,325	// 	admin: uint256,326	// ) -> Result<void> {327	// 	self.consume_store_writes(2)?;328329	// 	let caller = T::CrossAccountId::from_eth(caller);330	// 	let admin = convert_uint256_to_cross_account::<T>(admin);331	// 	<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;332	// 	Ok(())333	// }334335	/// Add collection admin.336	/// @param newAdmin Address of the added administrator.337	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {338		self.consume_store_writes(2)?;339340		let caller = T::CrossAccountId::from_eth(caller);341		let new_admin = T::CrossAccountId::from_eth(new_admin);342		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;343		Ok(())344	}345346	/// Remove collection admin.347	///348	/// @param admin Address of the removed administrator.349	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {350		self.consume_store_writes(2)?;351352		let caller = T::CrossAccountId::from_eth(caller);353		let admin = T::CrossAccountId::from_eth(admin);354		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;355		Ok(())356	}357358	/// Toggle accessibility of collection nesting.359	///360	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'361	#[solidity(rename_selector = "setCollectionNesting")]362	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {363		self.consume_store_reads_and_writes(1, 1)?;364365		check_is_owner_or_admin(caller, self)?;366367		let mut permissions = self.collection.permissions.clone();368		let mut nesting = permissions.nesting().clone();369		nesting.token_owner = enable;370		nesting.restricted = None;371		permissions.nesting = Some(nesting);372373		self.collection.permissions = <Pallet<T>>::clamp_permissions(374			self.collection.mode.clone(),375			&self.collection.permissions,376			permissions,377		)378		.map_err(dispatch_to_evm::<T>)?;379380		save(self)381	}382383	/// Toggle accessibility of collection nesting.384	///385	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'386	/// @param collections Addresses of collections that will be available for nesting.387	#[solidity(rename_selector = "setCollectionNesting")]388	fn set_nesting(389		&mut self,390		caller: caller,391		enable: bool,392		collections: Vec<address>,393	) -> Result<void> {394		self.consume_store_reads_and_writes(1, 1)?;395396		if collections.is_empty() {397			return Err("no addresses provided".into());398		}399		check_is_owner_or_admin(caller, self)?;400401		let mut permissions = self.collection.permissions.clone();402		match enable {403			false => {404				let mut nesting = permissions.nesting().clone();405				nesting.token_owner = false;406				nesting.restricted = None;407				permissions.nesting = Some(nesting);408			}409			true => {410				let mut bv = OwnerRestrictedSet::new();411				for i in collections {412					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {413						Error::Revert("Can't convert address into collection id".into())414					})?)415					.map_err(|_| "too many collections")?;416				}417				let mut nesting = permissions.nesting().clone();418				nesting.token_owner = true;419				nesting.restricted = Some(bv);420				permissions.nesting = Some(nesting);421			}422		};423424		self.collection.permissions = <Pallet<T>>::clamp_permissions(425			self.collection.mode.clone(),426			&self.collection.permissions,427			permissions,428		)429		.map_err(dispatch_to_evm::<T>)?;430431		save(self)432	}433434	/// Set the collection access method.435	/// @param mode Access mode436	/// 	0 for Normal437	/// 	1 for AllowList438	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {439		self.consume_store_reads_and_writes(1, 1)?;440441		check_is_owner_or_admin(caller, self)?;442		let permissions = CollectionPermissions {443			access: Some(match mode {444				0 => AccessMode::Normal,445				1 => AccessMode::AllowList,446				_ => return Err("not supported access mode".into()),447			}),448			..Default::default()449		};450		self.collection.permissions = <Pallet<T>>::clamp_permissions(451			self.collection.mode.clone(),452			&self.collection.permissions,453			permissions,454		)455		.map_err(dispatch_to_evm::<T>)?;456457		save(self)458	}459460	/// Checks that user allowed to operate with collection.461	///462	/// @param user User address to check.463	fn allowed(&self, user: address) -> Result<bool> {464		Ok(Pallet::<T>::allowed(465			self.id,466			T::CrossAccountId::from_eth(user),467		))468	}469470	/// Add the user to the allowed list.471	///472	/// @param user Address of a trusted user.473	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {474		self.consume_store_writes(1)?;475476		let caller = T::CrossAccountId::from_eth(caller);477		let user = T::CrossAccountId::from_eth(user);478		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;479		Ok(())480	}481482	// TODO: Temprorary off. Need refactor483	// /// Add substrate user to allowed list.484	// ///485	// /// @param user User substrate address.486	// fn add_to_collection_allow_list_substrate(487	// 	&mut self,488	// 	caller: caller,489	// 	user: uint256,490	// ) -> Result<void> {491	// 	self.consume_store_writes(1)?;492493	// 	let caller = T::CrossAccountId::from_eth(caller);494	// 	let user = convert_uint256_to_cross_account::<T>(user);495	// 	Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;496	// 	Ok(())497	// }498499	/// Remove the user from the allowed list.500	///501	/// @param user Address of a removed user.502	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {503		self.consume_store_writes(1)?;504505		let caller = T::CrossAccountId::from_eth(caller);506		let user = T::CrossAccountId::from_eth(user);507		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;508		Ok(())509	}510511	// TODO: Temprorary off. Need refactor512	// /// Remove substrate user from allowed list.513	// ///514	// /// @param user User substrate address.515	// fn remove_from_collection_allow_list_substrate(516	// 	&mut self,517	// 	caller: caller,518	// 	user: uint256,519	// ) -> Result<void> {520	// 	self.consume_store_writes(1)?;521522	// 	let caller = T::CrossAccountId::from_eth(caller);523	// 	let user = convert_uint256_to_cross_account::<T>(user);524	// 	Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;525	// 	Ok(())526	// }527528	/// Switch permission for minting.529	///530	/// @param mode Enable if "true".531	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {532		self.consume_store_reads_and_writes(1, 1)?;533534		check_is_owner_or_admin(caller, self)?;535		let permissions = CollectionPermissions {536			mint_mode: Some(mode),537			..Default::default()538		};539		self.collection.permissions = <Pallet<T>>::clamp_permissions(540			self.collection.mode.clone(),541			&self.collection.permissions,542			permissions,543		)544		.map_err(dispatch_to_evm::<T>)?;545546		save(self)547	}548549	/// Check that account is the owner or admin of the collection550	///551	/// @param user account to verify552	/// @return "true" if account is the owner or admin553	#[solidity(rename_selector = "isOwnerOrAdmin")]554	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {555		let user = T::CrossAccountId::from_eth(user);556		Ok(self.is_owner_or_admin(&user))557	}558559	// TODO: Temprorary off. Need refactor560	// /// Check that substrate account is the owner or admin of the collection561	// ///562	// /// @param user account to verify563	// /// @return "true" if account is the owner or admin564	// fn is_owner_or_admin_substrate(&self, user: uint256) -> Result<bool> {565	// 	let user = convert_uint256_to_cross_account::<T>(user);566	// 	Ok(self.is_owner_or_admin(&user))567	// }568569	/// Returns collection type570	///571	/// @return `Fungible` or `NFT` or `ReFungible`572	fn unique_collection_type(&self) -> Result<string> {573		let mode = match self.collection.mode {574			CollectionMode::Fungible(_) => "Fungible",575			CollectionMode::NFT => "NFT",576			CollectionMode::ReFungible => "ReFungible",577		};578		Ok(mode.into())579	}580581	/// Get collection owner.582	///583	/// @return Tuble with sponsor address and his substrate mirror.584	/// If address is canonical then substrate mirror is zero and vice versa.585	fn collection_owner(&self) -> Result<(address, uint256)> {586		Ok(convert_cross_account_to_tuple::<T>(587			&T::CrossAccountId::from_sub(self.owner.clone()),588		))589	}590591	/// Changes collection owner to another account592	///593	/// @dev Owner can be changed only by current owner594	/// @param newOwner new owner account595	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {596		self.consume_store_writes(1)?;597598		let caller = T::CrossAccountId::from_eth(caller);599		let new_owner = T::CrossAccountId::from_eth(new_owner);600		self.set_owner_internal(caller, new_owner)601			.map_err(dispatch_to_evm::<T>)602	}603604	// TODO: Temprorary off. Need refactor605	// /// Changes collection owner to another substrate account606	// ///607	// /// @dev Owner can be changed only by current owner608	// /// @param newOwner new owner substrate account609	// fn set_owner_substrate(&mut self, caller: caller, new_owner: uint256) -> Result<void> {610	// 	self.consume_store_writes(1)?;611612	// 	let caller = T::CrossAccountId::from_eth(caller);613	// 	let new_owner = convert_uint256_to_cross_account::<T>(new_owner);614	// 	self.set_owner_internal(caller, new_owner)615	// 		.map_err(dispatch_to_evm::<T>)616	// }617618	// TODO: need implement AbiWriter for &Vec<T>619	// fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {620	// 	let result = pallet_common::IsAdmin::<T>::iter_prefix((self.id,))621	// 		.map(|(admin, _)| pallet_common::eth::convert_cross_account_to_tuple::<T>(&admin))622	// 		.collect();623	// 	Ok(result)624	// }625}626627/// ### Note628/// Do not forget to add: `self.consume_store_reads(1)?;`629fn check_is_owner_or_admin<T: Config>(630	caller: caller,631	collection: &CollectionHandle<T>,632) -> Result<T::CrossAccountId> {633	let caller = T::CrossAccountId::from_eth(caller);634	collection635		.check_is_owner_or_admin(&caller)636		.map_err(dispatch_to_evm::<T>)?;637	Ok(caller)638}639640/// ### Note641/// Do not forget to add: `self.consume_store_writes(1)?;`642fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {643	collection644		.check_is_internal()645		.map_err(dispatch_to_evm::<T>)?;646	collection.save().map_err(dispatch_to_evm::<T>)?;647	Ok(())648}649650/// Contains static property keys and values.651pub mod static_property {652	use evm_coder::{653		execution::{Result, Error},654	};655	use alloc::format;656657	const EXPECT_CONVERT_ERROR: &str = "length < limit";658659	/// Keys.660	pub mod key {661		use super::*;662663		/// Key "schemaName".664		pub fn schema_name() -> up_data_structs::PropertyKey {665			property_key_from_bytes(b"schemaName").expect(EXPECT_CONVERT_ERROR)666		}667668		/// Key "baseURI".669		pub fn base_uri() -> up_data_structs::PropertyKey {670			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)671		}672673		/// Key "url".674		pub fn url() -> up_data_structs::PropertyKey {675			property_key_from_bytes(b"url").expect(EXPECT_CONVERT_ERROR)676		}677678		/// Key "suffix".679		pub fn suffix() -> up_data_structs::PropertyKey {680			property_key_from_bytes(b"suffix").expect(EXPECT_CONVERT_ERROR)681		}682683		/// Key "parentNft".684		pub fn parent_nft() -> up_data_structs::PropertyKey {685			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)686		}687	}688689	/// Values.690	pub mod value {691		use super::*;692693		/// Value "ERC721Metadata".694		pub const ERC721_METADATA: &[u8] = b"ERC721Metadata";695696		/// Value for [`ERC721_METADATA`].697		pub fn erc721() -> up_data_structs::PropertyValue {698			property_value_from_bytes(ERC721_METADATA).expect(EXPECT_CONVERT_ERROR)699		}700	}701702	/// Convert `byte` to [`PropertyKey`].703	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {704		bytes.to_vec().try_into().map_err(|_| {705			Error::Revert(format!(706				"Property key is too long. Max length is {}.",707				up_data_structs::PropertyKey::bound()708			))709		})710	}711712	/// Convert `bytes` to [`PropertyValue`].713	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {714		bytes.to_vec().try_into().map_err(|_| {715			Error::Revert(format!(716				"Property key is too long. Max length is {}.",717				up_data_structs::PropertyKey::bound()718			))719		})720	}721}