git.delta.rocks / unique-network / refs/commits / 4afba3743846

difftreelog

source

pallets/common/src/erc.rs23.0 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	abi::AbiType,21	solidity_interface, solidity, ToLog,22	types::*,23	types::Property as PropertyStruct,24	execution::{Result, Error},25	weight,26};27pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};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::convert_cross_account_to_uint256, weights::WeightInfo,39};4041/// Events for ethereum collection helper.42#[derive(ToLog)]43pub enum CollectionHelpersEvents {44	/// The collection has been created.45	CollectionCreated {46		/// Collection owner.47		#[indexed]48		owner: address,4950		/// Collection ID.51		#[indexed]52		collection_id: address,53	},54	/// The collection has been destroyed.55	CollectionDestroyed {56		/// Collection ID.57		#[indexed]58		collection_id: address,59	},60}6162/// Does not always represent a full collection, for RFT it is either63/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).64pub trait CommonEvmHandler {65	/// Raw compiled binary code of the contract stub66	const CODE: &'static [u8];6768	/// Call precompiled handle.69	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;70}7172/// @title A contract that allows you to work with collections.73#[solidity_interface(name = Collection)]74impl<T: Config> CollectionHandle<T>75where76	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,77{78	/// Set collection property.79	///80	/// @param key Property key.81	/// @param value Propery value.82	#[solidity(hide)]83	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]84	fn set_collection_property(85		&mut self,86		caller: caller,87		key: string,88		value: bytes,89	) -> Result<void> {90		let caller = T::CrossAccountId::from_eth(caller);91		let key = <Vec<u8>>::from(key)92			.try_into()93			.map_err(|_| "key too large")?;94		let value = value.0.try_into().map_err(|_| "value too large")?;9596		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })97			.map_err(dispatch_to_evm::<T>)98	}99100	/// Set collection properties.101	///102	/// @param properties Vector of properties key/value pair.103	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]104	fn set_collection_properties(105		&mut self,106		caller: caller,107		properties: Vec<PropertyStruct>,108	) -> Result<void> {109		let caller = T::CrossAccountId::from_eth(caller);110111		let properties = properties112			.into_iter()113			.map(|PropertyStruct { key, value }| {114				let key = <Vec<u8>>::from(key)115					.try_into()116					.map_err(|_| "key too large")?;117118				let value = value.0.try_into().map_err(|_| "value too large")?;119120				Ok(Property { key, value })121			})122			.collect::<Result<Vec<_>>>()?;123124		<Pallet<T>>::set_collection_properties(self, &caller, properties)125			.map_err(dispatch_to_evm::<T>)126	}127128	/// Delete collection property.129	///130	/// @param key Property key.131	#[solidity(hide)]132	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]133	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {134		let caller = T::CrossAccountId::from_eth(caller);135		let key = <Vec<u8>>::from(key)136			.try_into()137			.map_err(|_| "key too large")?;138139		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)140	}141142	/// Delete collection properties.143	///144	/// @param keys Properties keys.145	#[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]146	fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {147		let caller = T::CrossAccountId::from_eth(caller);148		let keys = keys149			.into_iter()150			.map(|key| {151				<Vec<u8>>::from(key)152					.try_into()153					.map_err(|_| Error::Revert("key too large".into()))154			})155			.collect::<Result<Vec<_>>>()?;156157		<Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)158	}159160	/// Get collection property.161	///162	/// @dev Throws error if key not found.163	///164	/// @param key Property key.165	/// @return bytes The property corresponding to the key.166	fn collection_property(&self, key: string) -> Result<bytes> {167		let key = <Vec<u8>>::from(key)168			.try_into()169			.map_err(|_| "key too large")?;170171		let props = CollectionProperties::<T>::get(self.id);172		let prop = props.get(&key).ok_or("key not found")?;173174		Ok(bytes(prop.to_vec()))175	}176177	/// Get collection properties.178	///179	/// @param keys Properties keys. Empty keys for all propertyes.180	/// @return Vector of properties key/value pairs.181	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<PropertyStruct>> {182		let keys = keys183			.into_iter()184			.map(|key| {185				<Vec<u8>>::from(key)186					.try_into()187					.map_err(|_| Error::Revert("key too large".into()))188			})189			.collect::<Result<Vec<_>>>()?;190191		let properties = Pallet::<T>::filter_collection_properties(192			self.id,193			if keys.is_empty() { None } else { Some(keys) },194		)195		.map_err(dispatch_to_evm::<T>)?;196197		let properties = properties198			.into_iter()199			.map(|p| {200				let key =201					string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;202				let value = bytes(p.value.to_vec());203				Ok(PropertyStruct { key, value })204			})205			.collect::<Result<Vec<_>>>()?;206		Ok(properties)207	}208209	/// Set the sponsor of the collection.210	///211	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.212	///213	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.214	#[solidity(hide)]215	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {216		self.consume_store_reads_and_writes(1, 1)?;217218		check_is_owner_or_admin(caller, self)?;219220		let sponsor = T::CrossAccountId::from_eth(sponsor);221		self.set_sponsor(sponsor.as_sub().clone())222			.map_err(dispatch_to_evm::<T>)?;223		save(self)224	}225226	/// Set the sponsor of the collection.227	///228	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.229	///230	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.231	fn set_collection_sponsor_cross(232		&mut self,233		caller: caller,234		sponsor: EthCrossAccount,235	) -> Result<void> {236		self.consume_store_reads_and_writes(1, 1)?;237238		check_is_owner_or_admin(caller, self)?;239240		let sponsor = sponsor.into_sub_cross_account::<T>()?;241		self.set_sponsor(sponsor.as_sub().clone())242			.map_err(dispatch_to_evm::<T>)?;243		save(self)244	}245246	/// Whether there is a pending sponsor.247	fn has_collection_pending_sponsor(&self) -> Result<bool> {248		Ok(matches!(249			self.collection.sponsorship,250			SponsorshipState::Unconfirmed(_)251		))252	}253254	/// Collection sponsorship confirmation.255	///256	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.257	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {258		self.consume_store_writes(1)?;259260		let caller = T::CrossAccountId::from_eth(caller);261		if !self262			.confirm_sponsorship(caller.as_sub())263			.map_err(dispatch_to_evm::<T>)?264		{265			return Err("caller is not set as sponsor".into());266		}267		save(self)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		check_is_owner_or_admin(caller, self)?;274		self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;275		save(self)276	}277278	/// Get current sponsor.279	///280	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.281	fn collection_sponsor(&self) -> Result<(address, uint256)> {282		let sponsor = match self.collection.sponsorship.sponsor() {283			Some(sponsor) => sponsor,284			None => return Ok(Default::default()),285		};286		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());287		let result: (address, uint256) = if sponsor.is_canonical_substrate() {288			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);289			(Default::default(), sponsor)290		} else {291			let sponsor = *sponsor.as_eth();292			(sponsor, Default::default())293		};294		Ok(result)295	}296297	/// Set limits for the collection.298	/// @dev Throws error if limit not found.299	/// @param limit Name of the limit. Valid names:300	/// 	"accountTokenOwnershipLimit",301	/// 	"sponsoredDataSize",302	/// 	"sponsoredDataRateLimit",303	/// 	"tokenLimit",304	/// 	"sponsorTransferTimeout",305	/// 	"sponsorApproveTimeout"306	///  	"ownerCanTransfer",307	/// 	"ownerCanDestroy",308	/// 	"transfersEnabled"309	/// @param value Value of the limit.310	#[solidity(rename_selector = "setCollectionLimit")]311	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint256) -> Result<void> {312		self.consume_store_reads_and_writes(1, 1)?;313314		let value = value315			.try_into()316			.map_err(|_| Error::Revert(format!("can't convert value to u32 \"{}\"", value)))?;317318		let convert_value_to_bool = || match value {319			0 => Ok(false),320			1 => Ok(true),321			_ => {322				return Err(Error::Revert(format!(323					"can't convert value to boolean \"{}\"",324					value325				)))326			}327		};328329		check_is_owner_or_admin(caller, self)?;330		let mut limits = self.limits.clone();331332		match limit.as_str() {333			"accountTokenOwnershipLimit" => {334				limits.account_token_ownership_limit = Some(value);335			}336			"sponsoredDataSize" => {337				limits.sponsored_data_size = Some(value);338			}339			"sponsoredDataRateLimit" => {340				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));341			}342			"tokenLimit" => {343				limits.token_limit = Some(value);344			}345			"sponsorTransferTimeout" => {346				limits.sponsor_transfer_timeout = Some(value);347			}348			"sponsorApproveTimeout" => {349				limits.sponsor_approve_timeout = Some(value);350			}351			"ownerCanTransfer" => {352				limits.owner_can_transfer = Some(convert_value_to_bool()?);353			}354			"ownerCanDestroy" => {355				limits.owner_can_destroy = Some(convert_value_to_bool()?);356			}357			"transfersEnabled" => {358				limits.transfers_enabled = Some(convert_value_to_bool()?);359			}360			_ => return Err(Error::Revert(format!("unknown limit \"{}\"", limit))),361		}362		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)363			.map_err(dispatch_to_evm::<T>)?;364		save(self)365	}366367	/// Get contract address.368	fn contract_address(&self) -> Result<address> {369		Ok(crate::eth::collection_id_to_address(self.id))370	}371372	/// Add collection admin.373	/// @param newAdmin Cross account administrator address.374	fn add_collection_admin_cross(375		&mut self,376		caller: caller,377		new_admin: EthCrossAccount,378	) -> Result<void> {379		self.consume_store_writes(2)?;380381		let caller = T::CrossAccountId::from_eth(caller);382		let new_admin = new_admin.into_sub_cross_account::<T>()?;383		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;384		Ok(())385	}386387	/// Remove collection admin.388	/// @param admin Cross account administrator address.389	fn remove_collection_admin_cross(390		&mut self,391		caller: caller,392		admin: EthCrossAccount,393	) -> Result<void> {394		self.consume_store_writes(2)?;395396		let caller = T::CrossAccountId::from_eth(caller);397		let admin = admin.into_sub_cross_account::<T>()?;398		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;399		Ok(())400	}401402	/// Add collection admin.403	/// @param newAdmin Address of the added administrator.404	#[solidity(hide)]405	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {406		self.consume_store_writes(2)?;407408		let caller = T::CrossAccountId::from_eth(caller);409		let new_admin = T::CrossAccountId::from_eth(new_admin);410		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;411		Ok(())412	}413414	/// Remove collection admin.415	///416	/// @param admin Address of the removed administrator.417	#[solidity(hide)]418	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {419		self.consume_store_writes(2)?;420421		let caller = T::CrossAccountId::from_eth(caller);422		let admin = T::CrossAccountId::from_eth(admin);423		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;424		Ok(())425	}426427	/// Toggle accessibility of collection nesting.428	///429	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'430	#[solidity(rename_selector = "setCollectionNesting")]431	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {432		self.consume_store_reads_and_writes(1, 1)?;433434		check_is_owner_or_admin(caller, self)?;435436		let mut permissions = self.collection.permissions.clone();437		let mut nesting = permissions.nesting().clone();438		nesting.token_owner = enable;439		nesting.restricted = None;440		permissions.nesting = Some(nesting);441442		self.collection.permissions = <Pallet<T>>::clamp_permissions(443			self.collection.mode.clone(),444			&self.collection.permissions,445			permissions,446		)447		.map_err(dispatch_to_evm::<T>)?;448449		save(self)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		check_is_owner_or_admin(caller, self)?;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		self.collection.permissions = <Pallet<T>>::clamp_permissions(494			self.collection.mode.clone(),495			&self.collection.permissions,496			permissions,497		)498		.map_err(dispatch_to_evm::<T>)?;499500		save(self)501	}502503	/// Set the collection access method.504	/// @param mode Access mode505	/// 	0 for Normal506	/// 	1 for AllowList507	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {508		self.consume_store_reads_and_writes(1, 1)?;509510		check_is_owner_or_admin(caller, self)?;511		let permissions = CollectionPermissions {512			access: Some(match mode {513				0 => AccessMode::Normal,514				1 => AccessMode::AllowList,515				_ => return Err("not supported access mode".into()),516			}),517			..Default::default()518		};519		self.collection.permissions = <Pallet<T>>::clamp_permissions(520			self.collection.mode.clone(),521			&self.collection.permissions,522			permissions,523		)524		.map_err(dispatch_to_evm::<T>)?;525526		save(self)527	}528529	/// Checks that user allowed to operate with collection.530	///531	/// @param user User address to check.532	fn allowlisted_cross(&self, user: EthCrossAccount) -> Result<bool> {533		let user = user.into_sub_cross_account::<T>()?;534		Ok(Pallet::<T>::allowed(self.id, user))535	}536537	/// Add the user to the allowed list.538	///539	/// @param user Address of a trusted user.540	#[solidity(hide)]541	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {542		self.consume_store_writes(1)?;543544		let caller = T::CrossAccountId::from_eth(caller);545		let user = T::CrossAccountId::from_eth(user);546		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;547		Ok(())548	}549550	/// Add user to allowed list.551	///552	/// @param user User cross account address.553	fn add_to_collection_allow_list_cross(554		&mut self,555		caller: caller,556		user: EthCrossAccount,557	) -> Result<void> {558		self.consume_store_writes(1)?;559560		let caller = T::CrossAccountId::from_eth(caller);561		let user = user.into_sub_cross_account::<T>()?;562		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;563		Ok(())564	}565566	/// Remove the user from the allowed list.567	///568	/// @param user Address of a removed user.569	#[solidity(hide)]570	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {571		self.consume_store_writes(1)?;572573		let caller = T::CrossAccountId::from_eth(caller);574		let user = T::CrossAccountId::from_eth(user);575		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;576		Ok(())577	}578579	/// Remove user from allowed list.580	///581	/// @param user User cross account address.582	fn remove_from_collection_allow_list_cross(583		&mut self,584		caller: caller,585		user: EthCrossAccount,586	) -> Result<void> {587		self.consume_store_writes(1)?;588589		let caller = T::CrossAccountId::from_eth(caller);590		let user = user.into_sub_cross_account::<T>()?;591		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;592		Ok(())593	}594595	/// Switch permission for minting.596	///597	/// @param mode Enable if "true".598	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {599		self.consume_store_reads_and_writes(1, 1)?;600601		check_is_owner_or_admin(caller, self)?;602		let permissions = CollectionPermissions {603			mint_mode: Some(mode),604			..Default::default()605		};606		self.collection.permissions = <Pallet<T>>::clamp_permissions(607			self.collection.mode.clone(),608			&self.collection.permissions,609			permissions,610		)611		.map_err(dispatch_to_evm::<T>)?;612613		save(self)614	}615616	/// Check that account is the owner or admin of the collection617	///618	/// @param user account to verify619	/// @return "true" if account is the owner or admin620	#[solidity(hide, rename_selector = "isOwnerOrAdmin")]621	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {622		let user = T::CrossAccountId::from_eth(user);623		Ok(self.is_owner_or_admin(&user))624	}625626	/// Check that account is the owner or admin of the collection627	///628	/// @param user User cross account to verify629	/// @return "true" if account is the owner or admin630	fn is_owner_or_admin_cross(&self, user: EthCrossAccount) -> Result<bool> {631		let user = user.into_sub_cross_account::<T>()?;632		Ok(self.is_owner_or_admin(&user))633	}634635	/// Returns collection type636	///637	/// @return `Fungible` or `NFT` or `ReFungible`638	fn unique_collection_type(&self) -> Result<string> {639		let mode = match self.collection.mode {640			CollectionMode::Fungible(_) => "Fungible",641			CollectionMode::NFT => "NFT",642			CollectionMode::ReFungible => "ReFungible",643		};644		Ok(mode.into())645	}646647	/// Get collection owner.648	///649	/// @return Tuble with sponsor address and his substrate mirror.650	/// If address is canonical then substrate mirror is zero and vice versa.651	fn collection_owner(&self) -> Result<EthCrossAccount> {652		Ok(EthCrossAccount::from_sub_cross_account::<T>(653			&T::CrossAccountId::from_sub(self.owner.clone()),654		))655	}656657	/// Changes collection owner to another account658	///659	/// @dev Owner can be changed only by current owner660	/// @param newOwner new owner account661	#[solidity(hide, rename_selector = "changeCollectionOwner")]662	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {663		self.consume_store_writes(1)?;664665		let caller = T::CrossAccountId::from_eth(caller);666		let new_owner = T::CrossAccountId::from_eth(new_owner);667		self.set_owner_internal(caller, new_owner)668			.map_err(dispatch_to_evm::<T>)669	}670671	/// Get collection administrators672	///673	/// @return Vector of tuples with admins address and his substrate mirror.674	/// If address is canonical then substrate mirror is zero and vice versa.675	fn collection_admins(&self) -> Result<Vec<EthCrossAccount>> {676		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))677			.map(|(admin, _)| EthCrossAccount::from_sub_cross_account::<T>(&admin))678			.collect();679		Ok(result)680	}681682	/// Changes collection owner to another account683	///684	/// @dev Owner can be changed only by current owner685	/// @param newOwner new owner cross account686	fn change_collection_owner_cross(687		&mut self,688		caller: caller,689		new_owner: EthCrossAccount,690	) -> Result<void> {691		self.consume_store_writes(1)?;692693		let caller = T::CrossAccountId::from_eth(caller);694		let new_owner = new_owner.into_sub_cross_account::<T>()?;695		self.set_owner_internal(caller, new_owner)696			.map_err(dispatch_to_evm::<T>)697	}698}699700/// ### Note701/// Do not forget to add: `self.consume_store_reads(1)?;`702fn check_is_owner_or_admin<T: Config>(703	caller: caller,704	collection: &CollectionHandle<T>,705) -> Result<T::CrossAccountId> {706	let caller = T::CrossAccountId::from_eth(caller);707	collection708		.check_is_owner_or_admin(&caller)709		.map_err(dispatch_to_evm::<T>)?;710	Ok(caller)711}712713/// ### Note714/// Do not forget to add: `self.consume_store_writes(1)?;`715fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {716	collection717		.check_is_internal()718		.map_err(dispatch_to_evm::<T>)?;719	collection.save().map_err(dispatch_to_evm::<T>)?;720	Ok(())721}722723/// Contains static property keys and values.724pub mod static_property {725	use evm_coder::{726		execution::{Result, Error},727	};728	use alloc::format;729730	const EXPECT_CONVERT_ERROR: &str = "length < limit";731732	/// Keys.733	pub mod key {734		use super::*;735736		/// Key "baseURI".737		pub fn base_uri() -> up_data_structs::PropertyKey {738			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)739		}740741		/// Key "url".742		pub fn url() -> up_data_structs::PropertyKey {743			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)744		}745746		/// Key "suffix".747		pub fn suffix() -> up_data_structs::PropertyKey {748			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)749		}750751		/// Key "parentNft".752		pub fn parent_nft() -> up_data_structs::PropertyKey {753			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)754		}755	}756757	/// Convert `byte` to [`PropertyKey`].758	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {759		bytes.to_vec().try_into().map_err(|_| {760			Error::Revert(format!(761				"Property key is too long. Max length is {}.",762				up_data_structs::PropertyKey::bound()763			))764		})765	}766767	/// Convert `bytes` to [`PropertyValue`].768	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {769		bytes.to_vec().try_into().map_err(|_| {770			Error::Revert(format!(771				"Property key is too long. Max length is {}.",772				up_data_structs::PropertyKey::bound()773			))774		})775	}776}