git.delta.rocks / unique-network / refs/commits / 5131df7cd674

difftreelog

source

pallets/common/src/erc.rs22.9 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, PropertyKey,31};32use alloc::format;3334use crate::{35	Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,36	eth::{37		convert_cross_account_to_uint256, convert_cross_account_to_tuple,38		convert_tuple_to_cross_account,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	/// The collection has been destroyed.57	CollectionDestroyed {58		/// Collection ID.59		#[indexed]60		collection_id: address,61	},62}6364/// Does not always represent a full collection, for RFT it is either65/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).66pub trait CommonEvmHandler {67	const CODE: &'static [u8];6869	/// Call precompiled handle.70	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;71}7273/// @title A contract that allows you to work with collections.74#[solidity_interface(name = Collection)]75impl<T: Config> CollectionHandle<T>76where77	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,78{79	/// Set collection property.80	///81	/// @param key Property key.82	/// @param value Propery value.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<(string, bytes)>,108	) -> Result<void> {109		for (key, value) in properties.into_iter() {110			self.set_collection_property(caller, key, value)?;111		}112		Ok(())113	}114115	/// Delete collection property.116	///117	/// @param key Property key.118	#[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]119	fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {120		let caller = T::CrossAccountId::from_eth(caller);121		let key = <Vec<u8>>::from(key)122			.try_into()123			.map_err(|_| "key too large")?;124125		<Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)126	}127128	/// Delete collection properties.129	///130	/// @param keys Properties keys.131	#[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]132	fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {133		let caller = T::CrossAccountId::from_eth(caller);134		let keys = keys135			.into_iter()136			.map(|key| {137				<Vec<u8>>::from(key)138					.try_into()139					.map_err(|_| Error::Revert("key too large".into()))140			})141			.collect::<Result<Vec<_>>>()?;142143		<Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)144	}145146	/// Get collection property.147	///148	/// @dev Throws error if key not found.149	///150	/// @param key Property key.151	/// @return bytes The property corresponding to the key.152	fn collection_property(&self, key: string) -> Result<bytes> {153		let key = <Vec<u8>>::from(key)154			.try_into()155			.map_err(|_| "key too large")?;156157		let props = CollectionProperties::<T>::get(self.id);158		let prop = props.get(&key).ok_or("key not found")?;159160		Ok(bytes(prop.to_vec()))161	}162163	/// Get collection properties.164	///165	/// @param keys Properties keys. Empty keys for all propertyes.166	/// @return Vector of properties key/value pairs.167	fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<(string, bytes)>> {168		let keys = keys169			.into_iter()170			.map(|key| {171				<Vec<u8>>::from(key)172					.try_into()173					.map_err(|_| Error::Revert("key too large".into()))174			})175			.collect::<Result<Vec<_>>>()?;176177		let properties = Pallet::<T>::filter_collection_properties(178			self.id,179			if keys.is_empty() { None } else { Some(keys) },180		)181		.map_err(dispatch_to_evm::<T>)?;182183		let properties = properties184			.into_iter()185			.map(|p| {186				let key =187					string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;188				let value = bytes(p.value.to_vec());189				Ok((key, value))190			})191			.collect::<Result<Vec<_>>>()?;192		Ok(properties)193	}194195	/// Set the sponsor of the collection.196	///197	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.198	///199	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.200	fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {201		self.consume_store_reads_and_writes(1, 1)?;202203		check_is_owner_or_admin(caller, self)?;204205		let sponsor = T::CrossAccountId::from_eth(sponsor);206		self.set_sponsor(sponsor.as_sub().clone())207			.map_err(dispatch_to_evm::<T>)?;208		save(self)209	}210211	/// Set the sponsor of the collection.212	///213	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.214	///215	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.216	fn set_collection_sponsor_cross(217		&mut self,218		caller: caller,219		sponsor: (address, uint256),220	) -> Result<void> {221		self.consume_store_reads_and_writes(1, 1)?;222223		check_is_owner_or_admin(caller, self)?;224225		let sponsor = convert_tuple_to_cross_account::<T>(sponsor)?;226		self.set_sponsor(sponsor.as_sub().clone())227			.map_err(dispatch_to_evm::<T>)?;228		save(self)229	}230231	/// Whether there is a pending sponsor.232	fn has_collection_pending_sponsor(&self) -> Result<bool> {233		Ok(matches!(234			self.collection.sponsorship,235			SponsorshipState::Unconfirmed(_)236		))237	}238239	/// Collection sponsorship confirmation.240	///241	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.242	fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {243		self.consume_store_writes(1)?;244245		let caller = T::CrossAccountId::from_eth(caller);246		if !self247			.confirm_sponsorship(caller.as_sub())248			.map_err(dispatch_to_evm::<T>)?249		{250			return Err("caller is not set as sponsor".into());251		}252		save(self)253	}254255	/// Remove collection sponsor.256	fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {257		self.consume_store_reads_and_writes(1, 1)?;258		check_is_owner_or_admin(caller, self)?;259		self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;260		save(self)261	}262263	/// Get current sponsor.264	///265	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.266	fn collection_sponsor(&self) -> Result<(address, uint256)> {267		let sponsor = match self.collection.sponsorship.sponsor() {268			Some(sponsor) => sponsor,269			None => return Ok(Default::default()),270		};271		let sponsor = T::CrossAccountId::from_sub(sponsor.clone());272		let result: (address, uint256) = if sponsor.is_canonical_substrate() {273			let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);274			(Default::default(), sponsor)275		} else {276			let sponsor = *sponsor.as_eth();277			(sponsor, Default::default())278		};279		Ok(result)280	}281282	/// Set limits for the collection.283	/// @dev Throws error if limit not found.284	/// @param limit Name of the limit. Valid names:285	/// 	"accountTokenOwnershipLimit",286	/// 	"sponsoredDataSize",287	/// 	"sponsoredDataRateLimit",288	/// 	"tokenLimit",289	/// 	"sponsorTransferTimeout",290	/// 	"sponsorApproveTimeout"291	/// @param value Value of the limit.292	#[solidity(rename_selector = "setCollectionLimit")]293	fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {294		self.consume_store_reads_and_writes(1, 1)?;295296		check_is_owner_or_admin(caller, self)?;297		let mut limits = self.limits.clone();298299		match limit.as_str() {300			"accountTokenOwnershipLimit" => {301				limits.account_token_ownership_limit = Some(value);302			}303			"sponsoredDataSize" => {304				limits.sponsored_data_size = Some(value);305			}306			"sponsoredDataRateLimit" => {307				limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));308			}309			"tokenLimit" => {310				limits.token_limit = Some(value);311			}312			"sponsorTransferTimeout" => {313				limits.sponsor_transfer_timeout = Some(value);314			}315			"sponsorApproveTimeout" => {316				limits.sponsor_approve_timeout = Some(value);317			}318			_ => {319				return Err(Error::Revert(format!(320					"unknown integer limit \"{}\"",321					limit322				)))323			}324		}325		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)326			.map_err(dispatch_to_evm::<T>)?;327		save(self)328	}329330	/// Set limits for the collection.331	/// @dev Throws error if limit not found.332	/// @param limit Name of the limit. Valid names:333	/// 	"ownerCanTransfer",334	/// 	"ownerCanDestroy",335	/// 	"transfersEnabled"336	/// @param value Value of the limit.337	#[solidity(rename_selector = "setCollectionLimit")]338	fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {339		self.consume_store_reads_and_writes(1, 1)?;340341		check_is_owner_or_admin(caller, self)?;342		let mut limits = self.limits.clone();343344		match limit.as_str() {345			"ownerCanTransfer" => {346				limits.owner_can_transfer = Some(value);347			}348			"ownerCanDestroy" => {349				limits.owner_can_destroy = Some(value);350			}351			"transfersEnabled" => {352				limits.transfers_enabled = Some(value);353			}354			_ => {355				return Err(Error::Revert(format!(356					"unknown boolean limit \"{}\"",357					limit358				)))359			}360		}361		self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)362			.map_err(dispatch_to_evm::<T>)?;363		save(self)364	}365366	/// Get contract address.367	fn contract_address(&self) -> Result<address> {368		Ok(crate::eth::collection_id_to_address(self.id))369	}370371	/// Add collection admin.372	/// @param newAdmin Cross account administrator address.373	fn add_collection_admin_cross(374		&mut self,375		caller: caller,376		new_admin: (address, uint256),377	) -> Result<void> {378		self.consume_store_writes(2)?;379380		let caller = T::CrossAccountId::from_eth(caller);381		let new_admin = convert_tuple_to_cross_account::<T>(new_admin)?;382		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;383		Ok(())384	}385386	/// Remove collection admin.387	/// @param admin Cross account administrator address.388	fn remove_collection_admin_cross(389		&mut self,390		caller: caller,391		admin: (address, uint256),392	) -> Result<void> {393		self.consume_store_writes(2)?;394395		let caller = T::CrossAccountId::from_eth(caller);396		let admin = convert_tuple_to_cross_account::<T>(admin)?;397		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;398		Ok(())399	}400401	/// Add collection admin.402	/// @param newAdmin Address of the added administrator.403	fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {404		self.consume_store_writes(2)?;405406		let caller = T::CrossAccountId::from_eth(caller);407		let new_admin = T::CrossAccountId::from_eth(new_admin);408		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;409		Ok(())410	}411412	/// Remove collection admin.413	///414	/// @param admin Address of the removed administrator.415	fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {416		self.consume_store_writes(2)?;417418		let caller = T::CrossAccountId::from_eth(caller);419		let admin = T::CrossAccountId::from_eth(admin);420		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;421		Ok(())422	}423424	/// Toggle accessibility of collection nesting.425	///426	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'427	#[solidity(rename_selector = "setCollectionNesting")]428	fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {429		self.consume_store_reads_and_writes(1, 1)?;430431		check_is_owner_or_admin(caller, self)?;432433		let mut permissions = self.collection.permissions.clone();434		let mut nesting = permissions.nesting().clone();435		nesting.token_owner = enable;436		nesting.restricted = None;437		permissions.nesting = Some(nesting);438439		self.collection.permissions = <Pallet<T>>::clamp_permissions(440			self.collection.mode.clone(),441			&self.collection.permissions,442			permissions,443		)444		.map_err(dispatch_to_evm::<T>)?;445446		save(self)447	}448449	/// Toggle accessibility of collection nesting.450	///451	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'452	/// @param collections Addresses of collections that will be available for nesting.453	#[solidity(rename_selector = "setCollectionNesting")]454	fn set_nesting(455		&mut self,456		caller: caller,457		enable: bool,458		collections: Vec<address>,459	) -> Result<void> {460		self.consume_store_reads_and_writes(1, 1)?;461462		if collections.is_empty() {463			return Err("no addresses provided".into());464		}465		check_is_owner_or_admin(caller, self)?;466467		let mut permissions = self.collection.permissions.clone();468		match enable {469			false => {470				let mut nesting = permissions.nesting().clone();471				nesting.token_owner = false;472				nesting.restricted = None;473				permissions.nesting = Some(nesting);474			}475			true => {476				let mut bv = OwnerRestrictedSet::new();477				for i in collections {478					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {479						Error::Revert("Can't convert address into collection id".into())480					})?)481					.map_err(|_| "too many collections")?;482				}483				let mut nesting = permissions.nesting().clone();484				nesting.token_owner = true;485				nesting.restricted = Some(bv);486				permissions.nesting = Some(nesting);487			}488		};489490		self.collection.permissions = <Pallet<T>>::clamp_permissions(491			self.collection.mode.clone(),492			&self.collection.permissions,493			permissions,494		)495		.map_err(dispatch_to_evm::<T>)?;496497		save(self)498	}499500	/// Set the collection access method.501	/// @param mode Access mode502	/// 	0 for Normal503	/// 	1 for AllowList504	fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {505		self.consume_store_reads_and_writes(1, 1)?;506507		check_is_owner_or_admin(caller, self)?;508		let permissions = CollectionPermissions {509			access: Some(match mode {510				0 => AccessMode::Normal,511				1 => AccessMode::AllowList,512				_ => return Err("not supported access mode".into()),513			}),514			..Default::default()515		};516		self.collection.permissions = <Pallet<T>>::clamp_permissions(517			self.collection.mode.clone(),518			&self.collection.permissions,519			permissions,520		)521		.map_err(dispatch_to_evm::<T>)?;522523		save(self)524	}525526	/// Checks that user allowed to operate with collection.527	///528	/// @param user User address to check.529	fn allowed(&self, user: address) -> Result<bool> {530		Ok(Pallet::<T>::allowed(531			self.id,532			T::CrossAccountId::from_eth(user),533		))534	}535536	/// Add the user to the allowed list.537	///538	/// @param user Address of a trusted user.539	fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {540		self.consume_store_writes(1)?;541542		let caller = T::CrossAccountId::from_eth(caller);543		let user = T::CrossAccountId::from_eth(user);544		<Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;545		Ok(())546	}547548	/// Add user to allowed list.549	///550	/// @param user User cross account address.551	fn add_to_collection_allow_list_cross(552		&mut self,553		caller: caller,554		user: (address, uint256),555	) -> Result<void> {556		self.consume_store_writes(1)?;557558		let caller = T::CrossAccountId::from_eth(caller);559		let user = convert_tuple_to_cross_account::<T>(user)?;560		Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;561		Ok(())562	}563564	/// Remove the user from the allowed list.565	///566	/// @param user Address of a removed user.567	fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {568		self.consume_store_writes(1)?;569570		let caller = T::CrossAccountId::from_eth(caller);571		let user = T::CrossAccountId::from_eth(user);572		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;573		Ok(())574	}575576	/// Remove user from allowed list.577	///578	/// @param user User cross account address.579	fn remove_from_collection_allow_list_cross(580		&mut self,581		caller: caller,582		user: (address, uint256),583	) -> Result<void> {584		self.consume_store_writes(1)?;585586		let caller = T::CrossAccountId::from_eth(caller);587		let user = convert_tuple_to_cross_account::<T>(user)?;588		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;589		Ok(())590	}591592	/// Switch permission for minting.593	///594	/// @param mode Enable if "true".595	fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {596		self.consume_store_reads_and_writes(1, 1)?;597598		check_is_owner_or_admin(caller, self)?;599		let permissions = CollectionPermissions {600			mint_mode: Some(mode),601			..Default::default()602		};603		self.collection.permissions = <Pallet<T>>::clamp_permissions(604			self.collection.mode.clone(),605			&self.collection.permissions,606			permissions,607		)608		.map_err(dispatch_to_evm::<T>)?;609610		save(self)611	}612613	/// Check that account is the owner or admin of the collection614	///615	/// @param user account to verify616	/// @return "true" if account is the owner or admin617	#[solidity(rename_selector = "isOwnerOrAdmin")]618	fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {619		let user = T::CrossAccountId::from_eth(user);620		Ok(self.is_owner_or_admin(&user))621	}622623	/// Check that account is the owner or admin of the collection624	///625	/// @param user User cross account to verify626	/// @return "true" if account is the owner or admin627	fn is_owner_or_admin_cross(&self, user: (address, uint256)) -> Result<bool> {628		let user = convert_tuple_to_cross_account::<T>(user)?;629		Ok(self.is_owner_or_admin(&user))630	}631632	/// Returns collection type633	///634	/// @return `Fungible` or `NFT` or `ReFungible`635	fn unique_collection_type(&self) -> Result<string> {636		let mode = match self.collection.mode {637			CollectionMode::Fungible(_) => "Fungible",638			CollectionMode::NFT => "NFT",639			CollectionMode::ReFungible => "ReFungible",640		};641		Ok(mode.into())642	}643644	/// Get collection owner.645	///646	/// @return Tuble with sponsor address and his substrate mirror.647	/// If address is canonical then substrate mirror is zero and vice versa.648	fn collection_owner(&self) -> Result<(address, uint256)> {649		Ok(convert_cross_account_to_tuple::<T>(650			&T::CrossAccountId::from_sub(self.owner.clone()),651		))652	}653654	/// Changes collection owner to another account655	///656	/// @dev Owner can be changed only by current owner657	/// @param newOwner new owner account658	#[solidity(rename_selector = "changeCollectionOwner")]659	fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {660		self.consume_store_writes(1)?;661662		let caller = T::CrossAccountId::from_eth(caller);663		let new_owner = T::CrossAccountId::from_eth(new_owner);664		self.set_owner_internal(caller, new_owner)665			.map_err(dispatch_to_evm::<T>)666	}667668	/// Get collection administrators669	///670	/// @return Vector of tuples with admins address and his substrate mirror.671	/// If address is canonical then substrate mirror is zero and vice versa.672	fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {673		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))674			.map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::<T>(&admin))675			.collect();676		Ok(result)677	}678679	/// Changes collection owner to another account680	///681	/// @dev Owner can be changed only by current owner682	/// @param newOwner new owner cross account683	fn set_owner_cross(&mut self, caller: caller, new_owner: (address, uint256)) -> Result<void> {684		self.consume_store_writes(1)?;685686		let caller = T::CrossAccountId::from_eth(caller);687		let new_owner = convert_tuple_to_cross_account::<T>(new_owner)?;688		self.set_owner_internal(caller, new_owner)689			.map_err(dispatch_to_evm::<T>)690	}691}692693/// ### Note694/// Do not forget to add: `self.consume_store_reads(1)?;`695fn check_is_owner_or_admin<T: Config>(696	caller: caller,697	collection: &CollectionHandle<T>,698) -> Result<T::CrossAccountId> {699	let caller = T::CrossAccountId::from_eth(caller);700	collection701		.check_is_owner_or_admin(&caller)702		.map_err(dispatch_to_evm::<T>)?;703	Ok(caller)704}705706/// ### Note707/// Do not forget to add: `self.consume_store_writes(1)?;`708fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {709	collection710		.check_is_internal()711		.map_err(dispatch_to_evm::<T>)?;712	collection.save().map_err(dispatch_to_evm::<T>)?;713	Ok(())714}715716/// Contains static property keys and values.717pub mod static_property {718	use evm_coder::{719		execution::{Result, Error},720	};721	use alloc::format;722723	const EXPECT_CONVERT_ERROR: &str = "length < limit";724725	/// Keys.726	pub mod key {727		use super::*;728729		/// Key "baseURI".730		pub fn base_uri() -> up_data_structs::PropertyKey {731			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)732		}733734		/// Key "url".735		pub fn url() -> up_data_structs::PropertyKey {736			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)737		}738739		/// Key "suffix".740		pub fn suffix() -> up_data_structs::PropertyKey {741			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)742		}743744		/// Key "parentNft".745		pub fn parent_nft() -> up_data_structs::PropertyKey {746			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)747		}748	}749750	/// Convert `byte` to [`PropertyKey`].751	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {752		bytes.to_vec().try_into().map_err(|_| {753			Error::Revert(format!(754				"Property key is too long. Max length is {}.",755				up_data_structs::PropertyKey::bound()756			))757		})758	}759760	/// Convert `bytes` to [`PropertyValue`].761	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {762		bytes.to_vec().try_into().map_err(|_| {763			Error::Revert(format!(764				"Property key is too long. Max length is {}.",765				up_data_structs::PropertyKey::bound()766			))767		})768	}769}