git.delta.rocks / unique-network / refs/commits / 2ca9f675f8fe

difftreelog

source

pallets/common/src/erc.rs23.1 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	execution::{Result, Error},24	weight,25};26pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};27use pallet_evm_coder_substrate::dispatch_to_evm;28use sp_std::vec::Vec;29use up_data_structs::{30	AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,31	SponsoringRateLimit, SponsorshipState,32};33use alloc::format;3435use crate::{36	Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,37	eth::convert_cross_account_to_uint256, weights::WeightInfo,38};3940/// Events for ethereum collection helper.41#[derive(ToLog)]42pub enum CollectionHelpersEvents {43	/// The collection has been created.44	CollectionCreated {45		/// Collection owner.46		#[indexed]47		owner: address,4849		/// Collection ID.50		#[indexed]51		collection_id: address,52	},53	/// The collection has been destroyed.54	CollectionDestroyed {55		/// Collection ID.56		#[indexed]57		collection_id: address,58	},59}6061/// Does not always represent a full collection, for RFT it is either62/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).63pub trait CommonEvmHandler {64	/// Raw compiled binary code of the contract stub65	const CODE: &'static [u8];6667	/// Call precompiled handle.68	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;69}7071/// @title A contract that allows you to work with collections.72#[solidity_interface(name = Collection)]73impl<T: Config> CollectionHandle<T>74where75	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,76{77	/// Set collection property.78	///79	/// @param key Property key.80	/// @param value Propery value.81	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]82	fn set_collection_property(83		&mut self,84		caller: caller,85		key: string,86		value: bytes,87	) -> Result<void> {88		let caller = T::CrossAccountId::from_eth(caller);89		let key = <Vec<u8>>::from(key)90			.try_into()91			.map_err(|_| "key too large")?;92		let value = value.0.try_into().map_err(|_| "value too large")?;9394		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })95			.map_err(dispatch_to_evm::<T>)96	}9798	/// Set collection properties.99	///100	/// @param properties Vector of properties key/value pair.101	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]102	fn set_collection_properties(103		&mut self,104		caller: caller,105		properties: Vec<(string, bytes)>,106	) -> Result<void> {107		let caller = T::CrossAccountId::from_eth(caller);108109		let properties = properties110			.into_iter()111			.map(|(key, value)| {112				let key = <Vec<u8>>::from(key)113					.try_into()114					.map_err(|_| "key too large")?;115116				let value = value.0.try_into().map_err(|_| "value too large")?;117118				Ok(Property { key, value })119			})120			.collect::<Result<Vec<_>>>()?;121122		<Pallet<T>>::set_collection_properties(self, &caller, properties)123			.map_err(dispatch_to_evm::<T>)124	}125126	/// Delete collection property.127	///128	/// @param key Property key.129	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]130	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {131		let caller = T::CrossAccountId::from_eth(caller);132		let key = <Vec<u8>>::from(key)133			.try_into()134			.map_err(|_| "key too large")?;135136		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)137	}138139	/// Delete collection properties.140	///141	/// @param keys Properties keys.142	#[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]143	fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {144		let caller = T::CrossAccountId::from_eth(caller);145		let keys = keys146			.into_iter()147			.map(|key| {148				<Vec<u8>>::from(key)149					.try_into()150					.map_err(|_| Error::Revert("key too large".into()))151			})152			.collect::<Result<Vec<_>>>()?;153154		<Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)155	}156157	/// Get collection property.158	///159	/// @dev Throws error if key not found.160	///161	/// @param key Property key.162	/// @return bytes The property corresponding to the key.163	fn collection_property(&self, key: string) -> Result<bytes> {164		let key = <Vec<u8>>::from(key)165			.try_into()166			.map_err(|_| "key too large")?;167168		let props = CollectionProperties::<T>::get(self.id);169		let prop = props.get(&key).ok_or("key not found")?;170171		Ok(bytes(prop.to_vec()))172	}173174	/// Get collection properties.175	///176	/// @param keys Properties keys. Empty keys for all propertyes.177	/// @return Vector of properties key/value pairs.178	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<(string, bytes)>> {179		let keys = keys180			.into_iter()181			.map(|key| {182				<Vec<u8>>::from(key)183					.try_into()184					.map_err(|_| Error::Revert("key too large".into()))185			})186			.collect::<Result<Vec<_>>>()?;187188		let properties = Pallet::<T>::filter_collection_properties(189			self.id,190			if keys.is_empty() { None } else { Some(keys) },191		)192		.map_err(dispatch_to_evm::<T>)?;193194		let properties = properties195			.into_iter()196			.map(|p| {197				let key =198					string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;199				let value = bytes(p.value.to_vec());200				Ok((key, value))201			})202			.collect::<Result<Vec<_>>>()?;203		Ok(properties)204	}205206	/// Set the sponsor of the collection.207	///208	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.209	///210	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.211	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {212		self.consume_store_reads_and_writes(1, 1)?;213214		check_is_owner_or_admin(caller, self)?;215216		let sponsor = T::CrossAccountId::from_eth(sponsor);217		self.set_sponsor(sponsor.as_sub().clone())218			.map_err(dispatch_to_evm::<T>)?;219		save(self)220	}221222	/// Set the sponsor of the collection.223	///224	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.225	///226	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.227	fn set_collection_sponsor_cross(228		&mut self,229		caller: caller,230		sponsor: EthCrossAccount,231	) -> Result<void> {232		self.consume_store_reads_and_writes(1, 1)?;233234		check_is_owner_or_admin(caller, self)?;235236		let sponsor = sponsor.into_sub_cross_account::<T>()?;237		self.set_sponsor(sponsor.as_sub().clone())238			.map_err(dispatch_to_evm::<T>)?;239		save(self)240	}241242	/// Whether there is a pending sponsor.243	fn has_collection_pending_sponsor(&self) -> Result<bool> {244		Ok(matches!(245			self.collection.sponsorship,246			SponsorshipState::Unconfirmed(_)247		))248	}249250	/// Collection sponsorship confirmation.251	///252	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.253	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {254		self.consume_store_writes(1)?;255256		let caller = T::CrossAccountId::from_eth(caller);257		if !self258			.confirm_sponsorship(caller.as_sub())259			.map_err(dispatch_to_evm::<T>)?260		{261			return Err("caller is not set as sponsor".into());262		}263		save(self)264	}265266	/// Remove collection sponsor.267	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {268		self.consume_store_reads_and_writes(1, 1)?;269		check_is_owner_or_admin(caller, self)?;270		self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;271		save(self)272	}273274	/// Get current sponsor.275	///276	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.277	fn collection_sponsor(&self) -> Result<(address, uint256)> {278		let sponsor = match self.collection.sponsorship.sponsor() {279			Some(sponsor) => sponsor,280			None => return Ok(Default::default()),281		};282		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());283		let result: (address, uint256) = if sponsor.is_canonical_substrate() {284			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);285			(Default::default(), sponsor)286		} else {287			let sponsor = *sponsor.as_eth();288			(sponsor, Default::default())289		};290		Ok(result)291	}292293	/// Set limits for the collection.294	/// @dev Throws error if limit not found.295	/// @param limit Name of the limit. Valid names:296	/// 	"accountTokenOwnershipLimit",297	/// 	"sponsoredDataSize",298	/// 	"sponsoredDataRateLimit",299	/// 	"tokenLimit",300	/// 	"sponsorTransferTimeout",301	/// 	"sponsorApproveTimeout"302	/// @param value Value of the limit.303	#[solidity(rename_selector = "setCollectionLimit")]304	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {305		self.consume_store_reads_and_writes(1, 1)?;306307		check_is_owner_or_admin(caller, self)?;308		let mut limits = self.limits.clone();309310		match limit.as_str() {311			"accountTokenOwnershipLimit" => {312				limits.account_token_ownership_limit = Some(value);313			}314			"sponsoredDataSize" => {315				limits.sponsored_data_size = Some(value);316			}317			"sponsoredDataRateLimit" => {318				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));319			}320			"tokenLimit" => {321				limits.token_limit = Some(value);322			}323			"sponsorTransferTimeout" => {324				limits.sponsor_transfer_timeout = Some(value);325			}326			"sponsorApproveTimeout" => {327				limits.sponsor_approve_timeout = Some(value);328			}329			_ => {330				return Err(Error::Revert(format!(331					"unknown integer limit \"{}\"",332					limit333				)))334			}335		}336		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)337			.map_err(dispatch_to_evm::<T>)?;338		save(self)339	}340341	/// Set limits for the collection.342	/// @dev Throws error if limit not found.343	/// @param limit Name of the limit. Valid names:344	/// 	"ownerCanTransfer",345	/// 	"ownerCanDestroy",346	/// 	"transfersEnabled"347	/// @param value Value of the limit.348	#[solidity(rename_selector = "setCollectionLimit")]349	fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {350		self.consume_store_reads_and_writes(1, 1)?;351352		check_is_owner_or_admin(caller, self)?;353		let mut limits = self.limits.clone();354355		match limit.as_str() {356			"ownerCanTransfer" => {357				limits.owner_can_transfer = Some(value);358			}359			"ownerCanDestroy" => {360				limits.owner_can_destroy = Some(value);361			}362			"transfersEnabled" => {363				limits.transfers_enabled = Some(value);364			}365			_ => {366				return Err(Error::Revert(format!(367					"unknown boolean limit \"{}\"",368					limit369				)))370			}371		}372		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)373			.map_err(dispatch_to_evm::<T>)?;374		save(self)375	}376377	/// Get contract address.378	fn contract_address(&self) -> Result<address> {379		Ok(crate::eth::collection_id_to_address(self.id))380	}381382	/// Add collection admin.383	/// @param newAdmin Cross account administrator address.384	fn add_collection_admin_cross(385		&mut self,386		caller: caller,387		new_admin: EthCrossAccount,388	) -> Result<void> {389		self.consume_store_writes(2)?;390391		let caller = T::CrossAccountId::from_eth(caller);392		let new_admin = new_admin.into_sub_cross_account::<T>()?;393		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;394		Ok(())395	}396397	/// Remove collection admin.398	/// @param admin Cross account administrator address.399	fn remove_collection_admin_cross(400		&mut self,401		caller: caller,402		admin: EthCrossAccount,403	) -> Result<void> {404		self.consume_store_writes(2)?;405406		let caller = T::CrossAccountId::from_eth(caller);407		let admin = admin.into_sub_cross_account::<T>()?;408		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;409		Ok(())410	}411412	/// Add collection admin.413	/// @param newAdmin Address of the added administrator.414	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {415		self.consume_store_writes(2)?;416417		let caller = T::CrossAccountId::from_eth(caller);418		let new_admin = T::CrossAccountId::from_eth(new_admin);419		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;420		Ok(())421	}422423	/// Remove collection admin.424	///425	/// @param admin Address of the removed administrator.426	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {427		self.consume_store_writes(2)?;428429		let caller = T::CrossAccountId::from_eth(caller);430		let admin = T::CrossAccountId::from_eth(admin);431		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;432		Ok(())433	}434435	/// Toggle accessibility of collection nesting.436	///437	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'438	#[solidity(rename_selector = "setCollectionNesting")]439	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {440		self.consume_store_reads_and_writes(1, 1)?;441442		check_is_owner_or_admin(caller, self)?;443444		let mut permissions = self.collection.permissions.clone();445		let mut nesting = permissions.nesting().clone();446		nesting.token_owner = enable;447		nesting.restricted = None;448		permissions.nesting = Some(nesting);449450		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	/// Toggle accessibility of collection nesting.461	///462	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'463	/// @param collections Addresses of collections that will be available for nesting.464	#[solidity(rename_selector = "setCollectionNesting")]465	fn set_nesting(466		&mut self,467		caller: caller,468		enable: bool,469		collections: Vec<address>,470	) -> Result<void> {471		self.consume_store_reads_and_writes(1, 1)?;472473		if collections.is_empty() {474			return Err("no addresses provided".into());475		}476		check_is_owner_or_admin(caller, self)?;477478		let mut permissions = self.collection.permissions.clone();479		match enable {480			false => {481				let mut nesting = permissions.nesting().clone();482				nesting.token_owner = false;483				nesting.restricted = None;484				permissions.nesting = Some(nesting);485			}486			true => {487				let mut bv = OwnerRestrictedSet::new();488				for i in collections {489					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {490						Error::Revert("Can't convert address into collection id".into())491					})?)492					.map_err(|_| "too many collections")?;493				}494				let mut nesting = permissions.nesting().clone();495				nesting.token_owner = true;496				nesting.restricted = Some(bv);497				permissions.nesting = Some(nesting);498			}499		};500501		self.collection.permissions = <Pallet<T>>::clamp_permissions(502			self.collection.mode.clone(),503			&self.collection.permissions,504			permissions,505		)506		.map_err(dispatch_to_evm::<T>)?;507508		save(self)509	}510511	/// Set the collection access method.512	/// @param mode Access mode513	/// 	0 for Normal514	/// 	1 for AllowList515	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {516		self.consume_store_reads_and_writes(1, 1)?;517518		check_is_owner_or_admin(caller, self)?;519		let permissions = CollectionPermissions {520			access: Some(match mode {521				0 => AccessMode::Normal,522				1 => AccessMode::AllowList,523				_ => return Err("not supported access mode".into()),524			}),525			..Default::default()526		};527		self.collection.permissions = <Pallet<T>>::clamp_permissions(528			self.collection.mode.clone(),529			&self.collection.permissions,530			permissions,531		)532		.map_err(dispatch_to_evm::<T>)?;533534		save(self)535	}536537	/// Checks that user allowed to operate with collection.538	///539	/// @param user User address to check.540	fn allowed(&self, user: address) -> Result<bool> {541		Ok(Pallet::<T>::allowed(542			self.id,543			T::CrossAccountId::from_eth(user),544		))545	}546547	/// Add the user to the allowed list.548	///549	/// @param user Address of a trusted user.550	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {551		self.consume_store_writes(1)?;552553		let caller = T::CrossAccountId::from_eth(caller);554		let user = T::CrossAccountId::from_eth(user);555		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;556		Ok(())557	}558559	/// Add user to allowed list.560	///561	/// @param user User cross account address.562	fn add_to_collection_allow_list_cross(563		&mut self,564		caller: caller,565		user: EthCrossAccount,566	) -> Result<void> {567		self.consume_store_writes(1)?;568569		let caller = T::CrossAccountId::from_eth(caller);570		let user = user.into_sub_cross_account::<T>()?;571		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;572		Ok(())573	}574575	/// Remove the user from the allowed list.576	///577	/// @param user Address of a removed user.578	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {579		self.consume_store_writes(1)?;580581		let caller = T::CrossAccountId::from_eth(caller);582		let user = T::CrossAccountId::from_eth(user);583		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;584		Ok(())585	}586587	/// Remove user from allowed list.588	///589	/// @param user User cross account address.590	fn remove_from_collection_allow_list_cross(591		&mut self,592		caller: caller,593		user: EthCrossAccount,594	) -> Result<void> {595		self.consume_store_writes(1)?;596597		let caller = T::CrossAccountId::from_eth(caller);598		let user = user.into_sub_cross_account::<T>()?;599		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;600		Ok(())601	}602603	/// Switch permission for minting.604	///605	/// @param mode Enable if "true".606	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {607		self.consume_store_reads_and_writes(1, 1)?;608609		check_is_owner_or_admin(caller, self)?;610		let permissions = CollectionPermissions {611			mint_mode: Some(mode),612			..Default::default()613		};614		self.collection.permissions = <Pallet<T>>::clamp_permissions(615			self.collection.mode.clone(),616			&self.collection.permissions,617			permissions,618		)619		.map_err(dispatch_to_evm::<T>)?;620621		save(self)622	}623624	/// Check that account is the owner or admin of the collection625	///626	/// @param user account to verify627	/// @return "true" if account is the owner or admin628	#[solidity(rename_selector = "isOwnerOrAdmin")]629	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {630		let user = T::CrossAccountId::from_eth(user);631		Ok(self.is_owner_or_admin(&user))632	}633634	/// Check that account is the owner or admin of the collection635	///636	/// @param user User cross account to verify637	/// @return "true" if account is the owner or admin638	fn is_owner_or_admin_cross(&self, user: EthCrossAccount) -> Result<bool> {639		let user = user.into_sub_cross_account::<T>()?;640		Ok(self.is_owner_or_admin(&user))641	}642643	/// Returns collection type644	///645	/// @return `Fungible` or `NFT` or `ReFungible`646	fn unique_collection_type(&self) -> Result<string> {647		let mode = match self.collection.mode {648			CollectionMode::Fungible(_) => "Fungible",649			CollectionMode::NFT => "NFT",650			CollectionMode::ReFungible => "ReFungible",651		};652		Ok(mode.into())653	}654655	/// Get collection owner.656	///657	/// @return Tuble with sponsor address and his substrate mirror.658	/// If address is canonical then substrate mirror is zero and vice versa.659	fn collection_owner(&self) -> Result<EthCrossAccount> {660		Ok(EthCrossAccount::from_sub_cross_account::<T>(661			&T::CrossAccountId::from_sub(self.owner.clone()),662		))663	}664665	/// Changes collection owner to another account666	///667	/// @dev Owner can be changed only by current owner668	/// @param newOwner new owner account669	#[solidity(rename_selector = "changeCollectionOwner")]670	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {671		self.consume_store_writes(1)?;672673		let caller = T::CrossAccountId::from_eth(caller);674		let new_owner = T::CrossAccountId::from_eth(new_owner);675		self.set_owner_internal(caller, new_owner)676			.map_err(dispatch_to_evm::<T>)677	}678679	/// Get collection administrators680	///681	/// @return Vector of tuples with admins address and his substrate mirror.682	/// If address is canonical then substrate mirror is zero and vice versa.683	fn collection_admins(&self) -> Result<Vec<EthCrossAccount>> {684		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))685			.map(|(admin, _)| EthCrossAccount::from_sub_cross_account::<T>(&admin))686			.collect();687		Ok(result)688	}689690	/// Changes collection owner to another account691	///692	/// @dev Owner can be changed only by current owner693	/// @param newOwner new owner cross account694	fn set_owner_cross(&mut self, caller: caller, new_owner: EthCrossAccount) -> Result<void> {695		self.consume_store_writes(1)?;696697		let caller = T::CrossAccountId::from_eth(caller);698		let new_owner = new_owner.into_sub_cross_account::<T>()?;699		self.set_owner_internal(caller, new_owner)700			.map_err(dispatch_to_evm::<T>)701	}702}703704/// ### Note705/// Do not forget to add: `self.consume_store_reads(1)?;`706fn check_is_owner_or_admin<T: Config>(707	caller: caller,708	collection: &CollectionHandle<T>,709) -> Result<T::CrossAccountId> {710	let caller = T::CrossAccountId::from_eth(caller);711	collection712		.check_is_owner_or_admin(&caller)713		.map_err(dispatch_to_evm::<T>)?;714	Ok(caller)715}716717/// ### Note718/// Do not forget to add: `self.consume_store_writes(1)?;`719fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {720	collection721		.check_is_internal()722		.map_err(dispatch_to_evm::<T>)?;723	collection.save().map_err(dispatch_to_evm::<T>)?;724	Ok(())725}726727/// Contains static property keys and values.728pub mod static_property {729	use evm_coder::{730		execution::{Result, Error},731	};732	use alloc::format;733734	const EXPECT_CONVERT_ERROR: &str = "length < limit";735736	/// Keys.737	pub mod key {738		use super::*;739740		/// Key "baseURI".741		pub fn base_uri() -> up_data_structs::PropertyKey {742			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)743		}744745		/// Key "url".746		pub fn url() -> up_data_structs::PropertyKey {747			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)748		}749750		/// Key "suffix".751		pub fn suffix() -> up_data_structs::PropertyKey {752			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)753		}754755		/// Key "parentNft".756		pub fn parent_nft() -> up_data_structs::PropertyKey {757			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)758		}759	}760761	/// Convert `byte` to [`PropertyKey`].762	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {763		bytes.to_vec().try_into().map_err(|_| {764			Error::Revert(format!(765				"Property key is too long. Max length is {}.",766				up_data_structs::PropertyKey::bound()767			))768		})769	}770771	/// Convert `bytes` to [`PropertyValue`].772	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {773		bytes.to_vec().try_into().map_err(|_| {774			Error::Revert(format!(775				"Property key is too long. Max length is {}.",776				up_data_structs::PropertyKey::bound()777			))778		})779	}780}