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

difftreelog

source

pallets/common/src/erc.rs24.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.1819pub use pallet_evm::{20	account::CrossAccountId, PrecompileHandle, PrecompileOutput, PrecompileResult,21};22use pallet_evm_coder_substrate::{23	abi::AbiType,24	dispatch_to_evm,25	execution::{Error, PreDispatch, Result},26	frontier_contract, solidity_interface,27	types::*,28	ToLog,29};30use sp_std::{vec, vec::Vec};31use up_data_structs::{32	CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property, SponsoringRateLimit,33	SponsorshipState,34};3536use crate::{37	eth, weights::WeightInfo, CollectionHandle, CollectionProperties, Config, Pallet, SelfWeightOf,38};3940frontier_contract! {41	macro_rules! CollectionHandle_result {...}42	impl<T: Config> Contract for CollectionHandle<T> {...}43}4445/// Events for ethereum collection helper.46#[derive(ToLog)]47pub enum CollectionHelpersEvents {48	/// The collection has been created.49	CollectionCreated {50		/// Collection owner.51		#[indexed]52		owner: Address,5354		/// Collection ID.55		#[indexed]56		collection_id: Address,57	},58	/// The collection has been destroyed.59	CollectionDestroyed {60		/// Collection ID.61		#[indexed]62		collection_id: Address,63	},64	/// The collection has been changed.65	CollectionChanged {66		/// Collection ID.67		#[indexed]68		collection_id: Address,69	},70}7172/// Does not always represent a full collection, for RFT it is either73/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).74pub trait CommonEvmHandler {75	/// Raw compiled binary code of the contract stub76	const CODE: &'static [u8];7778	/// Call precompiled handle.79	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;80}8182/// @title A contract that allows you to work with collections.83#[solidity_interface(name = Collection, enum(derive(PreDispatch)), enum_attr(weight))]84impl<T: Config> CollectionHandle<T>85where86	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,87{88	/// Set collection property.89	///90	/// @param key Property key.91	/// @param value Propery value.92	#[solidity(hide)]93	#[weight(<SelfWeightOf<T>>::set_collection_properties(1))]94	fn set_collection_property(&mut self, caller: Caller, key: String, value: Bytes) -> Result<()> {95		let caller = T::CrossAccountId::from_eth(caller);96		let key = <Vec<u8>>::from(key)97			.try_into()98			.map_err(|_| "key too large")?;99		let value = value.0.try_into().map_err(|_| "value too large")?;100101		<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })102			.map_err(dispatch_to_evm::<T>)103	}104105	/// Set collection properties.106	///107	/// @param properties Vector of properties key/value pair.108	#[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]109	fn set_collection_properties(110		&mut self,111		caller: Caller,112		properties: Vec<eth::Property>,113	) -> Result<()> {114		let caller = T::CrossAccountId::from_eth(caller);115116		let properties = properties117			.into_iter()118			.map(eth::Property::try_into)119			.collect::<Result<Vec<_>>>()?;120121		<Pallet<T>>::set_collection_properties(self, &caller, properties.into_iter())122			.map_err(dispatch_to_evm::<T>)123	}124125	/// Delete collection property.126	///127	/// @param key Property key.128	#[solidity(hide)]129	#[weight(<SelfWeightOf<T>>::set_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>>::set_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.into_iter())155			.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<eth::Property>> {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(Property::try_into)198			.collect::<Result<Vec<_>>>()?;199		Ok(properties)200	}201202	/// Set the sponsor of the collection.203	///204	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.205	///206	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.207	#[solidity(hide)]208	fn set_collection_sponsor(&mut self, caller: Caller, sponsor: Address) -> Result<()> {209		self.consume_store_reads_and_writes(1, 1)?;210211		let caller = T::CrossAccountId::from_eth(caller);212213		let sponsor = T::CrossAccountId::from_eth(sponsor);214		self.set_sponsor(&caller, sponsor.as_sub().clone())215			.map_err(dispatch_to_evm::<T>)216	}217218	/// Set the sponsor of the collection.219	///220	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.221	///222	/// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.223	fn set_collection_sponsor_cross(224		&mut self,225		caller: Caller,226		sponsor: eth::CrossAddress,227	) -> Result<()> {228		self.consume_store_reads_and_writes(1, 1)?;229230		let caller = T::CrossAccountId::from_eth(caller);231232		let sponsor = sponsor.into_sub_cross_account::<T>()?;233		self.set_sponsor(&caller, sponsor.as_sub().clone())234			.map_err(dispatch_to_evm::<T>)235	}236237	/// Whether there is a pending sponsor.238	fn has_collection_pending_sponsor(&self) -> Result<bool> {239		Ok(matches!(240			self.collection.sponsorship,241			SponsorshipState::Unconfirmed(_)242		))243	}244245	/// Collection sponsorship confirmation.246	///247	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.248	fn confirm_collection_sponsorship(&mut self, caller: Caller) -> Result<()> {249		self.consume_store_writes(1)?;250251		let caller = T::CrossAccountId::from_eth(caller);252		self.confirm_sponsorship(caller.as_sub())253			.map_err(dispatch_to_evm::<T>)254	}255256	/// Remove collection sponsor.257	fn remove_collection_sponsor(&mut self, caller: Caller) -> Result<()> {258		self.consume_store_reads_and_writes(1, 1)?;259		let caller = T::CrossAccountId::from_eth(caller);260		self.remove_sponsor(&caller).map_err(dispatch_to_evm::<T>)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<eth::CrossAddress> {267		let sponsor = match self.collection.sponsorship.sponsor() {268			Some(sponsor) => sponsor,269			None => return Ok(Default::default()),270		};271272		Ok(eth::CrossAddress::from_sub::<T>(sponsor))273	}274275	/// Get current collection limits.276	///277	/// @return Array of collection limits278	fn collection_limits(&self) -> Result<Vec<eth::CollectionLimit>> {279		let limits = &self.collection.limits;280281		Ok(vec![282			eth::CollectionLimit::new(283				eth::CollectionLimitField::AccountTokenOwnership,284				limits.account_token_ownership_limit,285			),286			eth::CollectionLimit::new(287				eth::CollectionLimitField::SponsoredDataSize,288				limits.sponsored_data_size,289			),290			limits291				.sponsored_data_rate_limit292				.and_then(|limit| {293					if let SponsoringRateLimit::Blocks(blocks) = limit {294						Some(eth::CollectionLimit::new(295							eth::CollectionLimitField::SponsoredDataRateLimit,296							Some(blocks),297						))298					} else {299						None300					}301				})302				.unwrap_or_else(|| {303					eth::CollectionLimit::new(304						eth::CollectionLimitField::SponsoredDataRateLimit,305						Default::default(),306					)307				}),308			eth::CollectionLimit::new(eth::CollectionLimitField::TokenLimit, limits.token_limit),309			eth::CollectionLimit::new(310				eth::CollectionLimitField::SponsorTransferTimeout,311				limits.sponsor_transfer_timeout,312			),313			eth::CollectionLimit::new(314				eth::CollectionLimitField::SponsorApproveTimeout,315				limits.sponsor_approve_timeout,316			),317			eth::CollectionLimit::new(318				eth::CollectionLimitField::OwnerCanTransfer,319				limits.owner_can_transfer.map(u32::from),320			),321			eth::CollectionLimit::new(322				eth::CollectionLimitField::OwnerCanDestroy,323				limits.owner_can_destroy.map(u32::from),324			),325			eth::CollectionLimit::new(326				eth::CollectionLimitField::TransferEnabled,327				limits.transfers_enabled.map(u32::from),328			),329		])330	}331332	/// Set limits for the collection.333	/// @dev Throws error if limit not found.334	/// @param limit Some limit.335	#[solidity(rename_selector = "setCollectionLimit")]336	fn set_collection_limit(&mut self, caller: Caller, limit: eth::CollectionLimit) -> Result<()> {337		self.consume_store_reads_and_writes(1, 1)?;338339		if !limit.has_value() {340			return Err(Error::Revert("user can't disable limits".into()));341		}342343		let caller = T::CrossAccountId::from_eth(caller);344		<Pallet<T>>::update_limits(&caller, self, limit.try_into()?).map_err(dispatch_to_evm::<T>)345	}346347	/// Get contract address.348	fn contract_address(&self) -> Result<Address> {349		Ok(crate::eth::collection_id_to_address(self.id))350	}351352	/// Add collection admin.353	/// @param newAdmin Cross account administrator address.354	fn add_collection_admin_cross(355		&mut self,356		caller: Caller,357		new_admin: eth::CrossAddress,358	) -> Result<()> {359		self.consume_store_reads_and_writes(2, 2)?;360361		let caller = T::CrossAccountId::from_eth(caller);362		let new_admin = new_admin.into_sub_cross_account::<T>()?;363		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;364		Ok(())365	}366367	/// Remove collection admin.368	/// @param admin Cross account administrator address.369	fn remove_collection_admin_cross(370		&mut self,371		caller: Caller,372		admin: eth::CrossAddress,373	) -> Result<()> {374		self.consume_store_reads_and_writes(2, 2)?;375376		let caller = T::CrossAccountId::from_eth(caller);377		let admin = admin.into_sub_cross_account::<T>()?;378		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;379		Ok(())380	}381382	/// Add collection admin.383	/// @param newAdmin Address of the added administrator.384	#[solidity(hide)]385	fn add_collection_admin(&mut self, caller: Caller, new_admin: Address) -> Result<()> {386		self.consume_store_reads_and_writes(2, 2)?;387388		let caller = T::CrossAccountId::from_eth(caller);389		let new_admin = T::CrossAccountId::from_eth(new_admin);390		<Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;391		Ok(())392	}393394	/// Remove collection admin.395	///396	/// @param admin Address of the removed administrator.397	#[solidity(hide)]398	fn remove_collection_admin(&mut self, caller: Caller, admin: Address) -> Result<()> {399		self.consume_store_reads_and_writes(2, 2)?;400401		let caller = T::CrossAccountId::from_eth(caller);402		let admin = T::CrossAccountId::from_eth(admin);403		<Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;404		Ok(())405	}406407	#[solidity(rename_selector = "setCollectionNesting")]408	fn set_nesting(409		&mut self,410		caller: Caller,411		collection_nesting_and_permissions: eth::CollectionNestingAndPermission,412	) -> Result<()> {413		self.consume_store_reads_and_writes(1, 1)?;414415		let caller = T::CrossAccountId::from_eth(caller);416417		let mut permissions = self.collection.permissions.clone();418		let mut nesting = permissions.nesting().clone();419420		let bv = if !collection_nesting_and_permissions.restricted.is_empty() {421			let mut bv = OwnerRestrictedSet::new();422			for address in collection_nesting_and_permissions.restricted.iter() {423				bv.try_insert(crate::eth::map_eth_to_id(address).ok_or_else(|| {424					Error::Revert("Can't convert address into collection id".into())425				})?)426				.map_err(|_| "too many collections")?;427			}428			Some(bv)429		} else {430			None431		};432433		nesting.token_owner = collection_nesting_and_permissions.token_owner;434		nesting.collection_admin = collection_nesting_and_permissions.collection_admin;435		nesting.restricted = bv;436		permissions.nesting = Some(nesting);437438		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)439	}440441	/// Toggle accessibility of collection nesting.442	///443	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'444	#[solidity(hide, rename_selector = "setCollectionNesting")]445	fn set_nesting_bool(&mut self, caller: Caller, enable: bool) -> Result<()> {446		self.consume_store_reads_and_writes(1, 1)?;447448		let caller = T::CrossAccountId::from_eth(caller);449450		let mut permissions = self.collection.permissions.clone();451		let mut nesting = permissions.nesting().clone();452		nesting.token_owner = enable;453		nesting.restricted = None;454		permissions.nesting = Some(nesting);455456		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)457	}458459	/// Toggle accessibility of collection nesting.460	///461	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'462	/// @param collections Addresses of collections that will be available for nesting.463	#[solidity(hide, rename_selector = "setCollectionNesting")]464	fn set_nesting_collection_ids(465		&mut self,466		caller: Caller,467		enable: bool,468		collections: Vec<Address>,469	) -> Result<()> {470		self.consume_store_reads_and_writes(1, 1)?;471472		if collections.is_empty() {473			return Err("no addresses provided".into());474		}475		let caller = T::CrossAccountId::from_eth(caller);476477		let mut permissions = self.collection.permissions.clone();478		match enable {479			false => {480				let mut nesting = permissions.nesting().clone();481				nesting.token_owner = false;482				nesting.restricted = None;483				permissions.nesting = Some(nesting);484			}485			true => {486				let mut bv = OwnerRestrictedSet::new();487				for i in collections {488					bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {489						Error::Revert("Can't convert address into collection id".into())490					})?)491					.map_err(|_| "too many collections")?;492				}493				let mut nesting = permissions.nesting().clone();494				nesting.token_owner = true;495				nesting.restricted = Some(bv);496				permissions.nesting = Some(nesting);497			}498		};499500		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)501	}502503	#[solidity(rename_selector = "collectionNesting")]504	fn collection_nesting(&self) -> Result<eth::CollectionNestingAndPermission> {505		let nesting = self.collection.permissions.nesting();506507		Ok(eth::CollectionNestingAndPermission::new(508			nesting.token_owner,509			nesting.collection_admin,510			nesting511				.restricted512				.clone()513				.map(|b| {514					b.0.into_inner()515						.iter()516						.map(|id| crate::eth::collection_id_to_address(id.0.into()))517						.collect()518				})519				.unwrap_or_default(),520		))521	}522523	/// Returns nesting for a collection524	#[solidity(hide, rename_selector = "collectionNestingRestrictedCollectionIds")]525	fn collection_nesting_restricted_ids(&self) -> Result<eth::CollectionNesting> {526		let nesting = self.collection.permissions.nesting();527528		Ok(eth::CollectionNesting::new(529			nesting.token_owner,530			nesting531				.restricted532				.clone()533				.map(|b| b.0.into_inner().iter().map(|id| id.0.into()).collect())534				.unwrap_or_default(),535		))536	}537538	/// Returns permissions for a collection539	#[solidity(hide)]540	fn collection_nesting_permissions(&self) -> Result<Vec<eth::CollectionNestingPermission>> {541		let nesting = self.collection.permissions.nesting();542		Ok(vec![543			eth::CollectionNestingPermission::new(544				eth::CollectionPermissionField::CollectionAdmin,545				nesting.collection_admin,546			),547			eth::CollectionNestingPermission::new(548				eth::CollectionPermissionField::TokenOwner,549				nesting.token_owner,550			),551		])552	}553	/// Set the collection access method.554	/// @param mode Access mode555	fn set_collection_access(&mut self, caller: Caller, mode: eth::AccessMode) -> Result<()> {556		self.consume_store_reads_and_writes(1, 1)?;557558		let caller = T::CrossAccountId::from_eth(caller);559		let permissions = CollectionPermissions {560			access: Some(mode.into()),561			..Default::default()562		};563		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)564	}565566	/// Checks that user allowed to operate with collection.567	///568	/// @param user User address to check.569	fn allowlisted_cross(&self, user: eth::CrossAddress) -> Result<bool> {570		let user = user.into_sub_cross_account::<T>()?;571		Ok(Pallet::<T>::allowed(self.id, user))572	}573574	/// Add the user to the allowed list.575	///576	/// @param user Address of a trusted user.577	#[solidity(hide)]578	fn add_to_collection_allow_list(&mut self, caller: Caller, user: Address) -> Result<()> {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, true).map_err(dispatch_to_evm::<T>)?;584		Ok(())585	}586587	/// Add user to allowed list.588	///589	/// @param user User cross account address.590	fn add_to_collection_allow_list_cross(591		&mut self,592		caller: Caller,593		user: eth::CrossAddress,594	) -> Result<()> {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, true).map_err(dispatch_to_evm::<T>)?;600		Ok(())601	}602603	/// Remove the user from the allowed list.604	///605	/// @param user Address of a removed user.606	#[solidity(hide)]607	fn remove_from_collection_allow_list(&mut self, caller: Caller, user: Address) -> Result<()> {608		self.consume_store_writes(1)?;609610		let caller = T::CrossAccountId::from_eth(caller);611		let user = T::CrossAccountId::from_eth(user);612		<Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;613		Ok(())614	}615616	/// Remove user from allowed list.617	///618	/// @param user User cross account address.619	fn remove_from_collection_allow_list_cross(620		&mut self,621		caller: Caller,622		user: eth::CrossAddress,623	) -> Result<()> {624		self.consume_store_writes(1)?;625626		let caller = T::CrossAccountId::from_eth(caller);627		let user = user.into_sub_cross_account::<T>()?;628		Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;629		Ok(())630	}631632	/// Switch permission for minting.633	///634	/// @param mode Enable if "true".635	fn set_collection_mint_mode(&mut self, caller: Caller, mode: bool) -> Result<()> {636		self.consume_store_reads_and_writes(1, 1)?;637638		let caller = T::CrossAccountId::from_eth(caller);639		let permissions = CollectionPermissions {640			mint_mode: Some(mode),641			..Default::default()642		};643		<Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)644	}645646	/// Check that account is the owner or admin of the collection647	///648	/// @param user account to verify649	/// @return "true" if account is the owner or admin650	#[solidity(hide, rename_selector = "isOwnerOrAdmin")]651	fn is_owner_or_admin_eth(&self, user: Address) -> Result<bool> {652		let user = T::CrossAccountId::from_eth(user);653		Ok(self.is_owner_or_admin(&user))654	}655656	/// Check that account is the owner or admin of the collection657	///658	/// @param user User cross account to verify659	/// @return "true" if account is the owner or admin660	fn is_owner_or_admin_cross(&self, user: eth::CrossAddress) -> Result<bool> {661		let user = user.into_sub_cross_account::<T>()?;662		Ok(self.is_owner_or_admin(&user))663	}664665	/// Returns collection type666	///667	/// @return `Fungible` or `NFT` or `ReFungible`668	fn unique_collection_type(&self) -> Result<String> {669		let mode = match self.collection.mode {670			CollectionMode::Fungible(_) => "Fungible",671			CollectionMode::NFT => "NFT",672			CollectionMode::ReFungible => "ReFungible",673		};674		Ok(mode.into())675	}676677	/// Get collection owner.678	///679	/// @return Tuble with sponsor address and his substrate mirror.680	/// If address is canonical then substrate mirror is zero and vice versa.681	fn collection_owner(&self) -> Result<eth::CrossAddress> {682		Ok(eth::CrossAddress::from_sub_cross_account::<T>(683			&T::CrossAccountId::from_sub(self.owner.clone()),684		))685	}686687	/// Changes collection owner to another account688	///689	/// @dev Owner can be changed only by current owner690	/// @param newOwner new owner account691	#[solidity(hide, rename_selector = "changeCollectionOwner")]692	fn set_owner(&mut self, caller: Caller, new_owner: Address) -> Result<()> {693		self.consume_store_writes(1)?;694695		let caller = T::CrossAccountId::from_eth(caller);696		let new_owner = T::CrossAccountId::from_eth(new_owner);697		self.change_owner(caller, new_owner)698			.map_err(dispatch_to_evm::<T>)699	}700701	/// Get collection administrators702	///703	/// @return Vector of tuples with admins address and his substrate mirror.704	/// If address is canonical then substrate mirror is zero and vice versa.705	fn collection_admins(&self) -> Result<Vec<eth::CrossAddress>> {706		let result = crate::IsAdmin::<T>::iter_prefix((self.id,))707			.map(|(admin, _)| eth::CrossAddress::from_sub_cross_account::<T>(&admin))708			.collect();709		Ok(result)710	}711712	/// Changes collection owner to another account713	///714	/// @dev Owner can be changed only by current owner715	/// @param newOwner new owner cross account716	fn change_collection_owner_cross(717		&mut self,718		caller: Caller,719		new_owner: eth::CrossAddress,720	) -> Result<()> {721		self.consume_store_writes(1)?;722723		let caller = T::CrossAccountId::from_eth(caller);724		let new_owner = new_owner.into_sub_cross_account::<T>()?;725		self.change_owner(caller, new_owner)726			.map_err(dispatch_to_evm::<T>)727	}728}729730/// Contains static property keys and values.731pub mod static_property {732	use alloc::format;733734	use pallet_evm_coder_substrate::execution::{Error, Result};735736	const EXPECT_CONVERT_ERROR: &str = "length < limit";737738	/// Keys.739	pub mod key {740		use super::*;741742		/// Key "baseURI".743		pub fn base_uri() -> up_data_structs::PropertyKey {744			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)745		}746747		/// Key "url".748		pub fn url() -> up_data_structs::PropertyKey {749			property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)750		}751752		/// Key "suffix".753		pub fn suffix() -> up_data_structs::PropertyKey {754			property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)755		}756757		/// Key "parentNft".758		pub fn parent_nft() -> up_data_structs::PropertyKey {759			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)760		}761	}762763	/// Convert `byte` to [`PropertyKey`].764	pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {765		bytes.to_vec().try_into().map_err(|_| {766			Error::Revert(format!(767				"Property key is too long. Max length is {}.",768				up_data_structs::PropertyKey::bound()769			))770		})771	}772773	/// Convert `bytes` to [`PropertyValue`].774	pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {775		bytes.to_vec().try_into().map_err(|_| {776			Error::Revert(format!(777				"Property key is too long. Max length is {}.",778				up_data_structs::PropertyKey::bound()779			))780		})781	}782}