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

difftreelog

source

pallets/common/src/erc.rs23.2 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	custom_signature::{SignatureUnit, FunctionSignature, SignaturePreferences},25	make_signature,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, convert_tuple_to_cross_account},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}6263/// Does not always represent a full collection, for RFT it is either64/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).65pub trait CommonEvmHandler {66	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	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]83	fn set_collection_property(84		&mut self,85		caller: caller,86		key: string,87		value: bytes,88	) -> Result<void> {89		let caller = T::CrossAccountId::from_eth(caller);90		let key = <Vec<u8>>::from(key)91			.try_into()92			.map_err(|_| "key too large")?;93		let value = value.0.try_into().map_err(|_| "value too large")?;9495		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })96			.map_err(dispatch_to_evm::<T>)97	}9899	/// Set collection properties.100	///101	/// @param properties Vector of properties key/value pair.102	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]103	fn set_collection_properties(104		&mut self,105		caller: caller,106		properties: Vec<(string, bytes)>,107	) -> Result<void> {108		let caller = T::CrossAccountId::from_eth(caller);109110		let properties = properties111			.into_iter()112			.map(|(key, value)| {113				let key = <Vec<u8>>::from(key)114					.try_into()115					.map_err(|_| "key too large")?;116117				let value = value.0.try_into().map_err(|_| "value too large")?;118119				Ok(Property { key, value })120			})121			.collect::<Result<Vec<_>>>()?;122123		<Pallet<T>>::set_collection_properties(self, &caller, properties)124			.map_err(dispatch_to_evm::<T>)125	}126127	/// Delete collection property.128	///129	/// @param key Property key.130	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]131	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {132		let caller = T::CrossAccountId::from_eth(caller);133		let key = <Vec<u8>>::from(key)134			.try_into()135			.map_err(|_| "key too large")?;136137		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)138	}139140	/// Delete collection properties.141	///142	/// @param keys Properties keys.143	#[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]144	fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {145		let caller = T::CrossAccountId::from_eth(caller);146		let keys = keys147			.into_iter()148			.map(|key| {149				<Vec<u8>>::from(key)150					.try_into()151					.map_err(|_| Error::Revert("key too large".into()))152			})153			.collect::<Result<Vec<_>>>()?;154155		<Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)156	}157158	/// Get collection property.159	///160	/// @dev Throws error if key not found.161	///162	/// @param key Property key.163	/// @return bytes The property corresponding to the key.164	fn collection_property(&self, key: string) -> Result<bytes> {165		let key = <Vec<u8>>::from(key)166			.try_into()167			.map_err(|_| "key too large")?;168169		let props = CollectionProperties::<T>::get(self.id);170		let prop = props.get(&key).ok_or("key not found")?;171172		Ok(bytes(prop.to_vec()))173	}174175	/// Get collection properties.176	///177	/// @param keys Properties keys. Empty keys for all propertyes.178	/// @return Vector of properties key/value pairs.179	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<(string, bytes)>> {180		let keys = keys181			.into_iter()182			.map(|key| {183				<Vec<u8>>::from(key)184					.try_into()185					.map_err(|_| Error::Revert("key too large".into()))186			})187			.collect::<Result<Vec<_>>>()?;188189		let properties = Pallet::<T>::filter_collection_properties(190			self.id,191			if keys.is_empty() { None } else { Some(keys) },192		)193		.map_err(dispatch_to_evm::<T>)?;194195		let properties = properties196			.into_iter()197			.map(|p| {198				let key =199					string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;200				let value = bytes(p.value.to_vec());201				Ok((key, value))202			})203			.collect::<Result<Vec<_>>>()?;204		Ok(properties)205	}206207	/// Set the sponsor of the collection.208	///209	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.210	///211	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.212	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {213		self.consume_store_reads_and_writes(1, 1)?;214215		check_is_owner_or_admin(caller, self)?;216217		let sponsor = T::CrossAccountId::from_eth(sponsor);218		self.set_sponsor(sponsor.as_sub().clone())219			.map_err(dispatch_to_evm::<T>)?;220		save(self)221	}222223	/// Set the sponsor of the collection.224	///225	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.226	///227	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.228	fn set_collection_sponsor_cross(229		&mut self,230		caller: caller,231		sponsor: EthCrossAccount,232	) -> Result<void> {233		self.consume_store_reads_and_writes(1, 1)?;234235		check_is_owner_or_admin(caller, self)?;236237		let sponsor = sponsor.into_sub_cross_account::<T>()?;238		self.set_sponsor(sponsor.as_sub().clone())239			.map_err(dispatch_to_evm::<T>)?;240		save(self)241	}242243	/// Whether there is a pending sponsor.244	fn has_collection_pending_sponsor(&self) -> Result<bool> {245		Ok(matches!(246			self.collection.sponsorship,247			SponsorshipState::Unconfirmed(_)248		))249	}250251	/// Collection sponsorship confirmation.252	///253	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.254	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {255		self.consume_store_writes(1)?;256257		let caller = T::CrossAccountId::from_eth(caller);258		if !self259			.confirm_sponsorship(caller.as_sub())260			.map_err(dispatch_to_evm::<T>)?261		{262			return Err("caller is not set as sponsor".into());263		}264		save(self)265	}266267	/// Remove collection sponsor.268	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {269		self.consume_store_reads_and_writes(1, 1)?;270		check_is_owner_or_admin(caller, self)?;271		self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;272		save(self)273	}274275	/// Get current sponsor.276	///277	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.278	fn collection_sponsor(&self) -> Result<(address, uint256)> {279		let sponsor = match self.collection.sponsorship.sponsor() {280			Some(sponsor) => sponsor,281			None => return Ok(Default::default()),282		};283		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());284		let result: (address, uint256) = if sponsor.is_canonical_substrate() {285			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);286			(Default::default(), sponsor)287		} else {288			let sponsor = *sponsor.as_eth();289			(sponsor, Default::default())290		};291		Ok(result)292	}293294	/// Set limits for the collection.295	/// @dev Throws error if limit not found.296	/// @param limit Name of the limit. Valid names:297	/// 	"accountTokenOwnershipLimit",298	/// 	"sponsoredDataSize",299	/// 	"sponsoredDataRateLimit",300	/// 	"tokenLimit",301	/// 	"sponsorTransferTimeout",302	/// 	"sponsorApproveTimeout"303	/// @param value Value of the limit.304	#[solidity(rename_selector = "setCollectionLimit")]305	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {306		self.consume_store_reads_and_writes(1, 1)?;307308		check_is_owner_or_admin(caller, self)?;309		let mut limits = self.limits.clone();310311		match limit.as_str() {312			"accountTokenOwnershipLimit" => {313				limits.account_token_ownership_limit = Some(value);314			}315			"sponsoredDataSize" => {316				limits.sponsored_data_size = Some(value);317			}318			"sponsoredDataRateLimit" => {319				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));320			}321			"tokenLimit" => {322				limits.token_limit = Some(value);323			}324			"sponsorTransferTimeout" => {325				limits.sponsor_transfer_timeout = Some(value);326			}327			"sponsorApproveTimeout" => {328				limits.sponsor_approve_timeout = Some(value);329			}330			_ => {331				return Err(Error::Revert(format!(332					"unknown integer limit \"{}\"",333					limit334				)))335			}336		}337		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)338			.map_err(dispatch_to_evm::<T>)?;339		save(self)340	}341342	/// Set limits for the collection.343	/// @dev Throws error if limit not found.344	/// @param limit Name of the limit. Valid names:345	/// 	"ownerCanTransfer",346	/// 	"ownerCanDestroy",347	/// 	"transfersEnabled"348	/// @param value Value of the limit.349	#[solidity(rename_selector = "setCollectionLimit")]350	fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {351		self.consume_store_reads_and_writes(1, 1)?;352353		check_is_owner_or_admin(caller, self)?;354		let mut limits = self.limits.clone();355356		match limit.as_str() {357			"ownerCanTransfer" => {358				limits.owner_can_transfer = Some(value);359			}360			"ownerCanDestroy" => {361				limits.owner_can_destroy = Some(value);362			}363			"transfersEnabled" => {364				limits.transfers_enabled = Some(value);365			}366			_ => {367				return Err(Error::Revert(format!(368					"unknown boolean limit \"{}\"",369					limit370				)))371			}372		}373		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)374			.map_err(dispatch_to_evm::<T>)?;375		save(self)376	}377378	/// Get contract address.379	fn contract_address(&self) -> Result<address> {380		Ok(crate::eth::collection_id_to_address(self.id))381	}382383	/// Add collection admin.384	/// @param newAdmin Cross account administrator address.385	fn add_collection_admin_cross(386		&mut self,387		caller: caller,388		new_admin: EthCrossAccount,389	) -> Result<void> {390		self.consume_store_writes(2)?;391392		let caller = T::CrossAccountId::from_eth(caller);393		let new_admin = new_admin.into_sub_cross_account::<T>()?;394		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;395		Ok(())396	}397398	/// Remove collection admin.399	/// @param admin Cross account administrator address.400	fn remove_collection_admin_cross(401		&mut self,402		caller: caller,403		admin: EthCrossAccount,404	) -> Result<void> {405		self.consume_store_writes(2)?;406407		let caller = T::CrossAccountId::from_eth(caller);408		let admin = admin.into_sub_cross_account::<T>()?;409		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;410		Ok(())411	}412413	/// Add collection admin.414	/// @param newAdmin Address of the added administrator.415	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {416		self.consume_store_writes(2)?;417418		let caller = T::CrossAccountId::from_eth(caller);419		let new_admin = T::CrossAccountId::from_eth(new_admin);420		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;421		Ok(())422	}423424	/// Remove collection admin.425	///426	/// @param admin Address of the removed administrator.427	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {428		self.consume_store_writes(2)?;429430		let caller = T::CrossAccountId::from_eth(caller);431		let admin = T::CrossAccountId::from_eth(admin);432		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;433		Ok(())434	}435436	/// Toggle accessibility of collection nesting.437	///438	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'439	#[solidity(rename_selector = "setCollectionNesting")]440	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {441		self.consume_store_reads_and_writes(1, 1)?;442443		check_is_owner_or_admin(caller, self)?;444445		let mut permissions = self.collection.permissions.clone();446		let mut nesting = permissions.nesting().clone();447		nesting.token_owner = enable;448		nesting.restricted = None;449		permissions.nesting = Some(nesting);450451		self.collection.permissions = <Pallet<T>>::clamp_permissions(452			self.collection.mode.clone(),453			&self.collection.permissions,454			permissions,455		)456		.map_err(dispatch_to_evm::<T>)?;457458		save(self)459	}460461	/// Toggle accessibility of collection nesting.462	///463	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'464	/// @param collections Addresses of collections that will be available for nesting.465	#[solidity(rename_selector = "setCollectionNesting")]466	fn set_nesting(467		&mut self,468		caller: caller,469		enable: bool,470		collections: Vec<address>,471	) -> Result<void> {472		self.consume_store_reads_and_writes(1, 1)?;473474		if collections.is_empty() {475			return Err("no addresses provided".into());476		}477		check_is_owner_or_admin(caller, self)?;478479		let mut permissions = self.collection.permissions.clone();480		match enable {481			false => {482				let mut nesting = permissions.nesting().clone();483				nesting.token_owner = false;484				nesting.restricted = None;485				permissions.nesting = Some(nesting);486			}487			true => {488				let mut bv = OwnerRestrictedSet::new();489				for i in collections {490					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {491						Error::Revert("Can't convert address into collection id".into())492					})?)493					.map_err(|_| "too many collections")?;494				}495				let mut nesting = permissions.nesting().clone();496				nesting.token_owner = true;497				nesting.restricted = Some(bv);498				permissions.nesting = Some(nesting);499			}500		};501502		self.collection.permissions = <Pallet<T>>::clamp_permissions(503			self.collection.mode.clone(),504			&self.collection.permissions,505			permissions,506		)507		.map_err(dispatch_to_evm::<T>)?;508509		save(self)510	}511512	/// Set the collection access method.513	/// @param mode Access mode514	/// 	0 for Normal515	/// 	1 for AllowList516	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {517		self.consume_store_reads_and_writes(1, 1)?;518519		check_is_owner_or_admin(caller, self)?;520		let permissions = CollectionPermissions {521			access: Some(match mode {522				0 => AccessMode::Normal,523				1 => AccessMode::AllowList,524				_ => return Err("not supported access mode".into()),525			}),526			..Default::default()527		};528		self.collection.permissions = <Pallet<T>>::clamp_permissions(529			self.collection.mode.clone(),530			&self.collection.permissions,531			permissions,532		)533		.map_err(dispatch_to_evm::<T>)?;534535		save(self)536	}537538	/// Checks that user allowed to operate with collection.539	///540	/// @param user User address to check.541	fn allowed(&self, user: address) -> Result<bool> {542		Ok(Pallet::<T>::allowed(543			self.id,544			T::CrossAccountId::from_eth(user),545		))546	}547548	/// Add the user to the allowed list.549	///550	/// @param user Address of a trusted user.551	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {552		self.consume_store_writes(1)?;553554		let caller = T::CrossAccountId::from_eth(caller);555		let user = T::CrossAccountId::from_eth(user);556		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;557		Ok(())558	}559560	/// Add user to allowed list.561	///562	/// @param user User cross account address.563	fn add_to_collection_allow_list_cross(564		&mut self,565		caller: caller,566		user: EthCrossAccount,567	) -> Result<void> {568		self.consume_store_writes(1)?;569570		let caller = T::CrossAccountId::from_eth(caller);571		let user = user.into_sub_cross_account::<T>()?;572		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;573		Ok(())574	}575576	/// Remove the user from the allowed list.577	///578	/// @param user Address of a removed user.579	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {580		self.consume_store_writes(1)?;581582		let caller = T::CrossAccountId::from_eth(caller);583		let user = T::CrossAccountId::from_eth(user);584		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;585		Ok(())586	}587588	/// Remove user from allowed list.589	///590	/// @param user User cross account address.591	fn remove_from_collection_allow_list_cross(592		&mut self,593		caller: caller,594		user: EthCrossAccount,595	) -> Result<void> {596		self.consume_store_writes(1)?;597598		let caller = T::CrossAccountId::from_eth(caller);599		let user = user.into_sub_cross_account::<T>()?;600		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;601		Ok(())602	}603604	/// Switch permission for minting.605	///606	/// @param mode Enable if "true".607	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {608		self.consume_store_reads_and_writes(1, 1)?;609610		check_is_owner_or_admin(caller, self)?;611		let permissions = CollectionPermissions {612			mint_mode: Some(mode),613			..Default::default()614		};615		self.collection.permissions = <Pallet<T>>::clamp_permissions(616			self.collection.mode.clone(),617			&self.collection.permissions,618			permissions,619		)620		.map_err(dispatch_to_evm::<T>)?;621622		save(self)623	}624625	/// Check that account is the owner or admin of the collection626	///627	/// @param user account to verify628	/// @return "true" if account is the owner or admin629	#[solidity(rename_selector = "isOwnerOrAdmin")]630	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {631		let user = T::CrossAccountId::from_eth(user);632		Ok(self.is_owner_or_admin(&user))633	}634635	/// Check that account is the owner or admin of the collection636	///637	/// @param user User cross account to verify638	/// @return "true" if account is the owner or admin639	fn is_owner_or_admin_cross(&self, user: EthCrossAccount) -> Result<bool> {640		let user = user.into_sub_cross_account::<T>()?;641		Ok(self.is_owner_or_admin(&user))642	}643644	/// Returns collection type645	///646	/// @return `Fungible` or `NFT` or `ReFungible`647	fn unique_collection_type(&self) -> Result<string> {648		let mode = match self.collection.mode {649			CollectionMode::Fungible(_) => "Fungible",650			CollectionMode::NFT => "NFT",651			CollectionMode::ReFungible => "ReFungible",652		};653		Ok(mode.into())654	}655656	/// Get collection owner.657	///658	/// @return Tuble with sponsor address and his substrate mirror.659	/// If address is canonical then substrate mirror is zero and vice versa.660	fn collection_owner(&self) -> Result<EthCrossAccount> {661		Ok(EthCrossAccount::from_sub_cross_account::<T>(662			&T::CrossAccountId::from_sub(self.owner.clone()),663		))664	}665666	/// Changes collection owner to another account667	///668	/// @dev Owner can be changed only by current owner669	/// @param newOwner new owner account670	#[solidity(rename_selector = "changeCollectionOwner")]671	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {672		self.consume_store_writes(1)?;673674		let caller = T::CrossAccountId::from_eth(caller);675		let new_owner = T::CrossAccountId::from_eth(new_owner);676		self.set_owner_internal(caller, new_owner)677			.map_err(dispatch_to_evm::<T>)678	}679680	/// Get collection administrators681	///682	/// @return Vector of tuples with admins address and his substrate mirror.683	/// If address is canonical then substrate mirror is zero and vice versa.684	fn collection_admins(&self) -> Result<Vec<EthCrossAccount>> {685		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))686			.map(|(admin, _)| EthCrossAccount::from_sub_cross_account::<T>(&admin))687			.collect();688		Ok(result)689	}690691	/// Changes collection owner to another account692	///693	/// @dev Owner can be changed only by current owner694	/// @param newOwner new owner cross account695	fn set_owner_cross(&mut self, caller: caller, new_owner: EthCrossAccount) -> Result<void> {696		self.consume_store_writes(1)?;697698		let caller = T::CrossAccountId::from_eth(caller);699		let new_owner = new_owner.into_sub_cross_account::<T>()?;700		self.set_owner_internal(caller, new_owner)701			.map_err(dispatch_to_evm::<T>)702	}703}704705/// ### Note706/// Do not forget to add: `self.consume_store_reads(1)?;`707fn check_is_owner_or_admin<T: Config>(708	caller: caller,709	collection: &CollectionHandle<T>,710) -> Result<T::CrossAccountId> {711	let caller = T::CrossAccountId::from_eth(caller);712	collection713		.check_is_owner_or_admin(&caller)714		.map_err(dispatch_to_evm::<T>)?;715	Ok(caller)716}717718/// ### Note719/// Do not forget to add: `self.consume_store_writes(1)?;`720fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {721	collection722		.check_is_internal()723		.map_err(dispatch_to_evm::<T>)?;724	collection.save().map_err(dispatch_to_evm::<T>)?;725	Ok(())726}727728/// Contains static property keys and values.729pub mod static_property {730	use evm_coder::{731		execution::{Result, Error},732	};733	use alloc::format;734735	const EXPECT_CONVERT_ERROR: &str = "length < limit";736737	/// Keys.738	pub mod key {739		use super::*;740741		/// Key "baseURI".742		pub fn base_uri() -> up_data_structs::PropertyKey {743			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)744		}745746		/// Key "url".747		pub fn url() -> up_data_structs::PropertyKey {748			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)749		}750751		/// Key "suffix".752		pub fn suffix() -> up_data_structs::PropertyKey {753			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)754		}755756		/// Key "parentNft".757		pub fn parent_nft() -> up_data_structs::PropertyKey {758			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)759		}760	}761762	/// Convert `byte` to [`PropertyKey`].763	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {764		bytes.to_vec().try_into().map_err(|_| {765			Error::Revert(format!(766				"Property key is too long. Max length is {}.",767				up_data_structs::PropertyKey::bound()768			))769		})770	}771772	/// Convert `bytes` to [`PropertyValue`].773	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {774		bytes.to_vec().try_into().map_err(|_| {775			Error::Revert(format!(776				"Property key is too long. Max length is {}.",777				up_data_structs::PropertyKey::bound()778			))779		})780	}781}