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

difftreelog

source

pallets/nonfungible/src/erc.rs36.8 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//! # Nonfungible Pallet EVM API18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Nonfungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.2122extern crate alloc;2324use alloc::string::ToString;25use core::{26	char::{decode_utf16, REPLACEMENT_CHARACTER},27	convert::TryInto,28};2930use evm_coder::{abi::AbiType, generate_stubgen, solidity_interface, types::*, AbiCoder, ToLog};31use frame_support::BoundedVec;32use pallet_common::{33	erc::{static_property::key, CollectionCall, CommonEvmHandler, PrecompileResult},34	eth::{self, TokenUri},35	CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations, CommonWeightInfo,36};37use pallet_evm::{account::CrossAccountId, PrecompileHandle};38use pallet_evm_coder_substrate::{39	call, dispatch_to_evm,40	execution::{Error, PreDispatch, Result},41	frontier_contract, SubstrateRecorder,42};43use pallet_structure::{weights::WeightInfo as _, SelfWeightOf as StructureWeight};44use sp_core::{Get, U256};45use sp_std::{vec, vec::Vec};46use up_data_structs::{47	budget::Budget, CollectionId, CollectionPropertiesVec, Property, PropertyKey,48	PropertyKeyPermission, PropertyPermission, TokenId,49};5051use crate::{52	common::{mint_with_props_weight, CommonWeights},53	weights::WeightInfo,54	AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, SelfWeightOf, TokenData,55	TokenProperties, TokensMinted,56};5758/// Nft events.59#[derive(ToLog)]60pub enum ERC721TokenEvent {61	/// The token has been changed.62	TokenChanged {63		/// Token ID.64		#[indexed]65		token_id: U256,66	},67}6869/// Token minting parameters70#[derive(AbiCoder, Default, Debug)]71pub struct MintTokenData {72	/// Minted token owner73	pub owner: eth::CrossAddress,74	/// Minted token properties75	pub properties: Vec<eth::Property>,76}7778frontier_contract! {79	macro_rules! NonfungibleHandle_result {...}80	impl<T: Config> Contract for NonfungibleHandle<T> {...}81}8283fn nesting_budget<T: Config>(recorder: &SubstrateRecorder<T>) -> impl Budget + '_ {84	recorder.weight_calls_budget(<StructureWeight<T>>::find_parent())85}8687/// @title A contract that allows to set and delete token properties and change token property permissions.88#[solidity_interface(name = TokenProperties, events(ERC721TokenEvent), enum(derive(PreDispatch)), enum_attr(weight))]89impl<T: Config> NonfungibleHandle<T> {90	/// @notice Set permissions for token property.91	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.92	/// @param key Property key.93	/// @param isMutable Permission to mutate property.94	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.95	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.96	#[solidity(hide)]97	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]98	fn set_token_property_permission(99		&mut self,100		caller: Caller,101		key: String,102		is_mutable: bool,103		collection_admin: bool,104		token_owner: bool,105	) -> Result<()> {106		let caller = T::CrossAccountId::from_eth(caller);107		<Pallet<T>>::set_token_property_permissions(108			self,109			&caller,110			vec![PropertyKeyPermission {111				key: <Vec<u8>>::from(key)112					.try_into()113					.map_err(|_| "too long key")?,114				permission: PropertyPermission {115					mutable: is_mutable,116					collection_admin,117					token_owner,118				},119			}],120		)121		.map_err(dispatch_to_evm::<T>)122	}123124	/// @notice Set permissions for token property.125	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.126	/// @param permissions Permissions for keys.127	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(permissions.len() as u32))]128	fn set_token_property_permissions(129		&mut self,130		caller: Caller,131		permissions: Vec<eth::TokenPropertyPermission>,132	) -> Result<()> {133		let caller = T::CrossAccountId::from_eth(caller);134		let perms = eth::TokenPropertyPermission::into_property_key_permissions(permissions)?;135136		<Pallet<T>>::set_token_property_permissions(self, &caller, perms)137			.map_err(dispatch_to_evm::<T>)138	}139140	/// @notice Get permissions for token properties.141	fn token_property_permissions(&self) -> Result<Vec<eth::TokenPropertyPermission>> {142		let perms = <Pallet<T>>::token_property_permission(self.id);143		Ok(perms144			.into_iter()145			.map(eth::TokenPropertyPermission::from)146			.collect())147	}148149	/// @notice Set token property value.150	/// @dev Throws error if `msg.sender` has no permission to edit the property.151	/// @param tokenId ID of the token.152	/// @param key Property key.153	/// @param value Property value.154	#[solidity(hide)]155	#[weight(<CommonWeights<T>>::set_token_properties(1))]156	fn set_property(157		&mut self,158		caller: Caller,159		token_id: U256,160		key: String,161		value: Bytes,162	) -> Result<()> {163		let caller = T::CrossAccountId::from_eth(caller);164		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;165		let key = <Vec<u8>>::from(key)166			.try_into()167			.map_err(|_| "key too long")?;168		let value = value.0.try_into().map_err(|_| "value too long")?;169170		<Pallet<T>>::set_token_property(171			self,172			&caller,173			TokenId(token_id),174			Property { key, value },175			&nesting_budget(&self.recorder),176		)177		.map_err(dispatch_to_evm::<T>)178	}179180	/// @notice Set token properties value.181	/// @dev Throws error if `msg.sender` has no permission to edit the property.182	/// @param tokenId ID of the token.183	/// @param properties settable properties184	#[weight(<CommonWeights<T>>::set_token_properties(properties.len() as u32))]185	fn set_properties(186		&mut self,187		caller: Caller,188		token_id: U256,189		properties: Vec<eth::Property>,190	) -> Result<()> {191		let caller = T::CrossAccountId::from_eth(caller);192		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;193194		let properties = properties195			.into_iter()196			.map(eth::Property::try_into)197			.collect::<Result<Vec<_>>>()?;198199		<Pallet<T>>::set_token_properties(200			self,201			&caller,202			TokenId(token_id),203			properties.into_iter(),204			&nesting_budget(&self.recorder),205		)206		.map_err(dispatch_to_evm::<T>)207	}208209	/// @notice Delete token property value.210	/// @dev Throws error if `msg.sender` has no permission to edit the property.211	/// @param tokenId ID of the token.212	/// @param key Property key.213	#[solidity(hide)]214	#[weight(<CommonWeights<T>>::delete_token_properties(1))]215	fn delete_property(&mut self, token_id: U256, caller: Caller, key: String) -> Result<()> {216		let caller = T::CrossAccountId::from_eth(caller);217		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;218		let key = <Vec<u8>>::from(key)219			.try_into()220			.map_err(|_| "key too long")?;221222		<Pallet<T>>::delete_token_property(223			self,224			&caller,225			TokenId(token_id),226			key,227			&nesting_budget(&self.recorder),228		)229		.map_err(dispatch_to_evm::<T>)230	}231232	/// @notice Delete token properties value.233	/// @dev Throws error if `msg.sender` has no permission to edit the property.234	/// @param tokenId ID of the token.235	/// @param keys Properties key.236	#[weight(<CommonWeights<T>>::delete_token_properties(keys.len() as u32))]237	fn delete_properties(238		&mut self,239		token_id: U256,240		caller: Caller,241		keys: Vec<String>,242	) -> Result<()> {243		let caller = T::CrossAccountId::from_eth(caller);244		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;245		let keys = keys246			.into_iter()247			.map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))248			.collect::<Result<Vec<_>>>()?;249250		<Pallet<T>>::delete_token_properties(251			self,252			&caller,253			TokenId(token_id),254			keys.into_iter(),255			&nesting_budget(&self.recorder),256		)257		.map_err(dispatch_to_evm::<T>)258	}259260	/// @notice Get token property value.261	/// @dev Throws error if key not found262	/// @param tokenId ID of the token.263	/// @param key Property key.264	/// @return Property value bytes265	fn property(&self, token_id: U256, key: String) -> Result<Bytes> {266		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;267		let key = <Vec<u8>>::from(key)268			.try_into()269			.map_err(|_| "key too long")?;270271		let props =272			<TokenProperties<T>>::get((self.id, token_id)).ok_or("token properties not found")?;273		let prop = props.get(&key).ok_or("key not found")?;274275		Ok(prop.to_vec().into())276	}277}278279#[derive(ToLog)]280pub enum ERC721Events {281	/// @dev This emits when ownership of any NFT changes by any mechanism.282	///  This event emits when NFTs are created (`from` == 0) and destroyed283	///  (`to` == 0). Exception: during contract creation, any number of NFTs284	///  may be created and assigned without emitting Transfer. At the time of285	///  any transfer, the approved address for that NFT (if any) is reset to none.286	Transfer {287		#[indexed]288		from: Address,289		#[indexed]290		to: Address,291		#[indexed]292		token_id: U256,293	},294	/// @dev This emits when the approved address for an NFT is changed or295	///  reaffirmed. The zero address indicates there is no approved address.296	///  When a Transfer event emits, this also indicates that the approved297	///  address for that NFT (if any) is reset to none.298	Approval {299		#[indexed]300		owner: Address,301		#[indexed]302		approved: Address,303		#[indexed]304		token_id: U256,305	},306	/// @dev This emits when an operator is enabled or disabled for an owner.307	///  The operator can manage all NFTs of the owner.308	#[allow(dead_code)]309	ApprovalForAll {310		#[indexed]311		owner: Address,312		#[indexed]313		operator: Address,314		approved: bool,315	},316}317318/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension319/// @dev See https://eips.ethereum.org/EIPS/eip-721320#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f, enum(derive(PreDispatch)), enum_attr(weight))]321impl<T: Config> NonfungibleHandle<T>322where323	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,324{325	/// @notice A descriptive name for a collection of NFTs in this contract326	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`327	#[solidity(hide, rename_selector = "name")]328	fn name_proxy(&self) -> String {329		self.name()330	}331332	/// @notice An abbreviated name for NFTs in this contract333	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`334	#[solidity(hide, rename_selector = "symbol")]335	fn symbol_proxy(&self) -> String {336		self.symbol()337	}338339	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.340	///341	/// @dev If the token has a `url` property and it is not empty, it is returned.342	///  Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`.343	///  If the collection property `baseURI` is empty or absent, return "" (empty string)344	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix345	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).346	///347	/// @return token's const_metadata348	#[solidity(rename_selector = "tokenURI")]349	fn token_uri(&self, token_id: U256) -> Result<String> {350		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;351352		match get_token_property(self, token_id_u32, &key::url()).as_deref() {353			Err(_) | Ok("") => (),354			Ok(url) => {355				return Ok(url.into());356			}357		};358359		let base_uri =360			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())361				.map(BoundedVec::into_inner)362				.map(String::from_utf8)363				.transpose()364				.map_err(|e| {365					Error::Revert(alloc::format!(366						"can not convert value \"baseURI\" to string with error \"{e}\""367					))368				})?;369370		let base_uri = match base_uri.as_deref() {371			None | Some("") => {372				return Ok("".into());373			}374			Some(base_uri) => base_uri.into(),375		};376377		Ok(378			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {379				Err(_) | Ok("") => base_uri,380				Ok(suffix) => base_uri + suffix,381			},382		)383	}384}385386/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension387/// @dev See https://eips.ethereum.org/EIPS/eip-721388#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63, enum(derive(PreDispatch)), enum_attr(weight))]389impl<T: Config> NonfungibleHandle<T> {390	/// @notice Enumerate valid NFTs391	/// @param index A counter less than `totalSupply()`392	/// @return The token identifier for the `index`th NFT,393	///  (sort order not specified)394	fn token_by_index(&self, index: U256) -> U256 {395		index396	}397398	/// @dev Not implemented399	fn token_of_owner_by_index(&self, _owner: Address, _index: U256) -> Result<U256> {400		// TODO: Not implemetable401		Err("not implemented".into())402	}403404	/// @notice Count NFTs tracked by this contract405	/// @return A count of valid NFTs tracked by this contract, where each one of406	///  them has an assigned and queryable owner not equal to the zero address407	fn total_supply(&self) -> Result<U256> {408		self.consume_store_reads(1)?;409		Ok(<Pallet<T>>::total_supply(self).into())410	}411}412413/// @title ERC-721 Non-Fungible Token Standard414/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md415#[solidity_interface(name = ERC721, events(ERC721Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x80ac58cd)]416impl<T: Config> NonfungibleHandle<T> {417	/// @notice Count all NFTs assigned to an owner418	/// @dev NFTs assigned to the zero address are considered invalid, and this419	///  function throws for queries about the zero address.420	/// @param owner An address for whom to query the balance421	/// @return The number of NFTs owned by `owner`, possibly zero422	fn balance_of(&self, owner: Address) -> Result<U256> {423		self.consume_store_reads(1)?;424		let owner = T::CrossAccountId::from_eth(owner);425		let balance = <AccountBalance<T>>::get((self.id, owner));426		Ok(balance.into())427	}428	/// @notice Find the owner of an NFT429	/// @dev NFTs assigned to zero address are considered invalid, and queries430	///  about them do throw.431	/// @param tokenId The identifier for an NFT432	/// @return The address of the owner of the NFT433	fn owner_of(&self, token_id: U256) -> Result<Address> {434		self.consume_store_reads(1)?;435		let token: TokenId = token_id.try_into()?;436		Ok(*<TokenData<T>>::get((self.id, token))437			.ok_or("token not found")?438			.owner439			.as_eth())440	}441	/// @dev Not implemented442	#[solidity(rename_selector = "safeTransferFrom")]443	fn safe_transfer_from_with_data(444		&mut self,445		_from: Address,446		_to: Address,447		_token_id: U256,448		_data: Bytes,449	) -> Result<()> {450		// TODO: Not implemetable451		Err("not implemented".into())452	}453	/// @dev Not implemented454	fn safe_transfer_from(&mut self, _from: Address, _to: Address, _token_id: U256) -> Result<()> {455		// TODO: Not implemetable456		Err("not implemented".into())457	}458459	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE460	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE461	///  THEY MAY BE PERMANENTLY LOST462	/// @dev Throws unless `msg.sender` is the current owner or an authorized463	///  operator for this NFT. Throws if `from` is not the current owner. Throws464	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.465	/// @param from The current owner of the NFT466	/// @param to The new owner467	/// @param tokenId The NFT to transfer468	#[weight(<CommonWeights<T>>::transfer_from())]469	fn transfer_from(470		&mut self,471		caller: Caller,472		from: Address,473		to: Address,474		token_id: U256,475	) -> Result<()> {476		let caller = T::CrossAccountId::from_eth(caller);477		let from = T::CrossAccountId::from_eth(from);478		let to = T::CrossAccountId::from_eth(to);479		let token = token_id.try_into()?;480481		<Pallet<T>>::transfer_from(482			self,483			&caller,484			&from,485			&to,486			token,487			&nesting_budget(&self.recorder),488		)489		.map_err(|e| dispatch_to_evm::<T>(e.error))?;490		Ok(())491	}492493	/// @notice Set or reaffirm the approved address for an NFT494	/// @dev The zero address indicates there is no approved address.495	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized496	///  operator of the current owner.497	/// @param approved The new approved NFT controller498	/// @param tokenId The NFT to approve499	#[weight(<SelfWeightOf<T>>::approve())]500	fn approve(&mut self, caller: Caller, approved: Address, token_id: U256) -> Result<()> {501		let caller = T::CrossAccountId::from_eth(caller);502		let approved = T::CrossAccountId::from_eth(approved);503		let token = token_id.try_into()?;504505		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))506			.map_err(dispatch_to_evm::<T>)?;507		Ok(())508	}509510	/// @notice Sets or unsets the approval of a given operator.511	/// The `operator` is allowed to transfer all tokens of the `caller` on their behalf.512	/// @param operator Operator513	/// @param approved Should operator status be granted or revoked?514	#[weight(<SelfWeightOf<T>>::set_allowance_for_all())]515	fn set_approval_for_all(516		&mut self,517		caller: Caller,518		operator: Address,519		approved: bool,520	) -> Result<()> {521		let caller = T::CrossAccountId::from_eth(caller);522		let operator = T::CrossAccountId::from_eth(operator);523524		<Pallet<T>>::set_allowance_for_all(self, &caller, &operator, approved)525			.map_err(dispatch_to_evm::<T>)?;526		Ok(())527	}528529	/// @notice Get the approved address for a single NFT530	/// @dev Throws if `tokenId` is not a valid NFT531	/// @param tokenId The NFT to find the approved address for532	/// @return The approved address for this NFT, or the zero address if there is none533	fn get_approved(&self, token_id: U256) -> Result<Address> {534		let token_id = token_id.try_into()?;535		let operator = <Pallet<T>>::get_allowance(self, token_id).map_err(dispatch_to_evm::<T>)?;536		Ok(if let Some(operator) = operator {537			*operator.as_eth()538		} else {539			Address::zero()540		})541	}542543	/// @notice Tells whether the given `owner` approves the `operator`.544	#[weight(<SelfWeightOf<T>>::allowance_for_all())]545	fn is_approved_for_all(&self, owner: Address, operator: Address) -> Result<bool> {546		let owner = T::CrossAccountId::from_eth(owner);547		let operator = T::CrossAccountId::from_eth(operator);548549		Ok(<Pallet<T>>::allowance_for_all(self, &owner, &operator))550	}551}552553/// @title ERC721 Token that can be irreversibly burned (destroyed).554#[solidity_interface(name = ERC721Burnable, enum(derive(PreDispatch)), enum_attr(weight))]555impl<T: Config> NonfungibleHandle<T> {556	/// @notice Burns a specific ERC721 token.557	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized558	///  operator of the current owner.559	/// @param tokenId The NFT to approve560	#[weight(<SelfWeightOf<T>>::burn_item())]561	fn burn(&mut self, caller: Caller, token_id: U256) -> Result<()> {562		let caller = T::CrossAccountId::from_eth(caller);563		let token = token_id.try_into()?;564565		<Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;566		Ok(())567	}568}569570/// @title ERC721 minting logic.571#[solidity_interface(name = ERC721UniqueMintable, enum(derive(PreDispatch)), enum_attr(weight))]572impl<T: Config> NonfungibleHandle<T> {573	/// @notice Function to mint a token.574	/// @param to The new owner575	/// @return uint256 The id of the newly minted token576	#[weight(<SelfWeightOf<T>>::create_item())]577	fn mint(&mut self, caller: Caller, to: Address) -> Result<U256> {578		let token_id: U256 = <TokensMinted<T>>::get(self.id)579			.checked_add(1)580			.ok_or("item id overflow")?581			.into();582		self.mint_check_id(caller, to, token_id)?;583		Ok(token_id)584	}585586	/// @notice Function to mint a token.587	/// @dev `tokenId` should be obtained with `nextTokenId` method,588	///  unlike standard, you can't specify it manually589	/// @param to The new owner590	/// @param tokenId ID of the minted NFT591	#[solidity(hide, rename_selector = "mint")]592	#[weight(<SelfWeightOf<T>>::create_item())]593	fn mint_check_id(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<bool> {594		let caller = T::CrossAccountId::from_eth(caller);595		let to = T::CrossAccountId::from_eth(to);596		let token_id: u32 = token_id.try_into()?;597598		if <TokensMinted<T>>::get(self.id)599			.checked_add(1)600			.ok_or("item id overflow")?601			!= token_id602		{603			return Err("item id should be next".into());604		}605606		<Pallet<T>>::create_item(607			self,608			&caller,609			CreateItemData::<T> {610				properties: BoundedVec::default(),611				owner: to,612			},613			&nesting_budget(&self.recorder),614		)615		.map_err(dispatch_to_evm::<T>)?;616617		Ok(true)618	}619620	/// @notice Function to mint token with the given tokenUri.621	/// @param to The new owner622	/// @param tokenUri Token URI that would be stored in the NFT properties623	/// @return uint256 The id of the newly minted token624	#[solidity(rename_selector = "mintWithTokenURI")]625	#[weight(mint_with_props_weight::<T>(<SelfWeightOf<T>>::create_item(), [1].into_iter()))]626	fn mint_with_token_uri(627		&mut self,628		caller: Caller,629		to: Address,630		token_uri: String,631	) -> Result<U256> {632		let token_id: U256 = <TokensMinted<T>>::get(self.id)633			.checked_add(1)634			.ok_or("item id overflow")?635			.into();636		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;637		Ok(token_id)638	}639640	/// @notice Function to mint token with the given tokenUri.641	/// @dev `tokenId` should be obtained with `nextTokenId` method,642	///  unlike standard, you can't specify it manually643	/// @param to The new owner644	/// @param tokenId ID of the minted NFT645	/// @param tokenUri Token URI that would be stored in the NFT properties646	#[solidity(hide, rename_selector = "mintWithTokenURI")]647	#[weight(mint_with_props_weight::<T>(<SelfWeightOf<T>>::create_item(), [1].into_iter()))]648	fn mint_with_token_uri_check_id(649		&mut self,650		caller: Caller,651		to: Address,652		token_id: U256,653		token_uri: String,654	) -> Result<bool> {655		let key = key::url();656		let permission = get_token_permission::<T>(self.id, &key)?;657		if !permission.collection_admin {658			return Err("operation is not allowed".into());659		}660661		let caller = T::CrossAccountId::from_eth(caller);662		let to = T::CrossAccountId::from_eth(to);663		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;664665		if <TokensMinted<T>>::get(self.id)666			.checked_add(1)667			.ok_or("item id overflow")?668			!= token_id669		{670			return Err("item id should be next".into());671		}672673		let mut properties = CollectionPropertiesVec::default();674		properties675			.try_push(Property {676				key,677				value: token_uri678					.into_bytes()679					.try_into()680					.map_err(|_| "token uri is too long")?,681			})682			.map_err(|e| Error::Revert(alloc::format!("can't add property: {e:?}")))?;683684		<Pallet<T>>::create_item(685			self,686			&caller,687			CreateItemData::<T> {688				properties,689				owner: to,690			},691			&nesting_budget(&self.recorder),692		)693		.map_err(dispatch_to_evm::<T>)?;694		Ok(true)695	}696}697698fn get_token_property<T: Config>(699	collection: &CollectionHandle<T>,700	token_id: u32,701	key: &up_data_structs::PropertyKey,702) -> Result<String> {703	collection.consume_store_reads(1)?;704	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))705		.map_err(|_| Error::Revert("token properties not found".into()))?;706	if let Some(property) = properties.get(key) {707		return Ok(String::from_utf8_lossy(property).into());708	}709710	Err("property tokenURI not found".into())711}712713fn get_token_permission<T: Config>(714	collection_id: CollectionId,715	key: &PropertyKey,716) -> Result<PropertyPermission> {717	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)718		.map_err(|_| Error::Revert("no permissions for collection".into()))?;719	let a = token_property_permissions720		.get(key)721		.map(Clone::clone)722		.ok_or_else(|| {723			let key = String::from_utf8(key.clone().into_inner()).unwrap_or_default();724			Error::Revert(alloc::format!("no permission for key {key}"))725		})?;726	Ok(a)727}728729/// @title Unique extensions for ERC721.730#[solidity_interface(name = ERC721UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))]731impl<T: Config> NonfungibleHandle<T>732where733	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,734{735	/// @notice A descriptive name for a collection of NFTs in this contract736	fn name(&self) -> String {737		decode_utf16(self.name.iter().copied())738			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))739			.collect::<String>()740	}741742	/// @notice An abbreviated name for NFTs in this contract743	fn symbol(&self) -> String {744		String::from_utf8_lossy(&self.token_prefix).into()745	}746747	/// @notice A description for the collection.748	fn description(&self) -> String {749		decode_utf16(self.description.iter().copied())750			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))751			.collect::<String>()752	}753754	/// Returns the owner (in cross format) of the token.755	///756	/// @param tokenId Id for the token.757	#[solidity(hide)]758	fn cross_owner_of(&self, token_id: U256) -> Result<eth::CrossAddress> {759		Self::owner_of_cross(self, token_id)760	}761762	/// Returns the owner (in cross format) of the token.763	///764	/// @param tokenId Id for the token.765	fn owner_of_cross(&self, token_id: U256) -> Result<eth::CrossAddress> {766		Self::token_owner(self, token_id.try_into()?)767			.map(|o| eth::CrossAddress::from_sub_cross_account::<T>(&o))768			.map_err(|_| Error::Revert("token not found".into()))769	}770771	/// @notice Count all NFTs assigned to an owner772	/// @param owner An cross address for whom to query the balance773	/// @return The number of NFTs owned by `owner`, possibly zero774	fn balance_of_cross(&self, owner: eth::CrossAddress) -> Result<U256> {775		self.consume_store_reads(1)?;776		let balance = <AccountBalance<T>>::get((self.id, owner.into_sub_cross_account::<T>()?));777		Ok(balance.into())778	}779780	/// Returns the token properties.781	///782	/// @param tokenId Id for the token.783	/// @param keys Properties keys. Empty keys for all propertyes.784	/// @return Vector of properties key/value pairs.785	fn properties(&self, token_id: U256, keys: Vec<String>) -> Result<Vec<eth::Property>> {786		let keys = keys787			.into_iter()788			.map(|key| {789				<Vec<u8>>::from(key)790					.try_into()791					.map_err(|_| Error::Revert("key too large".into()))792			})793			.collect::<Result<Vec<_>>>()?;794795		<Self as CommonCollectionOperations<T>>::token_properties(796			self,797			token_id.try_into()?,798			if keys.is_empty() { None } else { Some(keys) },799		)800		.into_iter()801		.map(eth::Property::try_from)802		.collect::<Result<Vec<_>>>()803	}804805	/// @notice Set or reaffirm the approved address for an NFT806	/// @dev The zero address indicates there is no approved address.807	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized808	///  operator of the current owner.809	/// @param approved The new substrate address approved NFT controller810	/// @param tokenId The NFT to approve811	#[weight(<SelfWeightOf<T>>::approve())]812	fn approve_cross(813		&mut self,814		caller: Caller,815		approved: eth::CrossAddress,816		token_id: U256,817	) -> Result<()> {818		let caller = T::CrossAccountId::from_eth(caller);819		let approved = approved.into_sub_cross_account::<T>()?;820		let token = token_id.try_into()?;821822		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))823			.map_err(dispatch_to_evm::<T>)?;824		Ok(())825	}826827	/// @notice Transfer ownership of an NFT828	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`829	///  is the zero address. Throws if `tokenId` is not a valid NFT.830	/// @param to The new owner831	/// @param tokenId The NFT to transfer832	#[weight(<CommonWeights<T>>::transfer())]833	fn transfer(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<()> {834		let caller = T::CrossAccountId::from_eth(caller);835		let to = T::CrossAccountId::from_eth(to);836		let token = token_id.try_into()?;837838		<Pallet<T>>::transfer(self, &caller, &to, token, &nesting_budget(&self.recorder))839			.map_err(|e| dispatch_to_evm::<T>(e.error))?;840		Ok(())841	}842843	/// @notice Transfer ownership of an NFT844	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`845	///  is the zero address. Throws if `tokenId` is not a valid NFT.846	/// @param to The new owner847	/// @param tokenId The NFT to transfer848	#[weight(<CommonWeights<T>>::transfer())]849	fn transfer_cross(850		&mut self,851		caller: Caller,852		to: eth::CrossAddress,853		token_id: U256,854	) -> Result<()> {855		let caller = T::CrossAccountId::from_eth(caller);856		let to = to.into_sub_cross_account::<T>()?;857		let token = token_id.try_into()?;858859		<Pallet<T>>::transfer(self, &caller, &to, token, &nesting_budget(&self.recorder))860			.map_err(|e| dispatch_to_evm::<T>(e.error))?;861		Ok(())862	}863864	/// @notice Transfer ownership of an NFT from cross account address to cross account address865	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`866	///  is the zero address. Throws if `tokenId` is not a valid NFT.867	/// @param from Cross acccount address of current owner868	/// @param to Cross acccount address of new owner869	/// @param tokenId The NFT to transfer870	#[weight(<CommonWeights<T>>::transfer_from())]871	fn transfer_from_cross(872		&mut self,873		caller: Caller,874		from: eth::CrossAddress,875		to: eth::CrossAddress,876		token_id: U256,877	) -> Result<()> {878		let caller = T::CrossAccountId::from_eth(caller);879		let from = from.into_sub_cross_account::<T>()?;880		let to = to.into_sub_cross_account::<T>()?;881		let token_id = token_id.try_into()?;882883		Pallet::<T>::transfer_from(884			self,885			&caller,886			&from,887			&to,888			token_id,889			&nesting_budget(&self.recorder),890		)891		.map_err(|e| dispatch_to_evm::<T>(e.error))?;892		Ok(())893	}894895	/// @notice Burns a specific ERC721 token.896	/// @dev Throws unless `msg.sender` is the current owner or an authorized897	///  operator for this NFT. Throws if `from` is not the current owner. Throws898	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.899	/// @param from The current owner of the NFT900	/// @param tokenId The NFT to transfer901	#[solidity(hide)]902	#[weight(<SelfWeightOf<T>>::burn_from())]903	fn burn_from(&mut self, caller: Caller, from: Address, token_id: U256) -> Result<()> {904		let caller = T::CrossAccountId::from_eth(caller);905		let from = T::CrossAccountId::from_eth(from);906		let token = token_id.try_into()?;907908		<Pallet<T>>::burn_from(self, &caller, &from, token, &nesting_budget(&self.recorder))909			.map_err(dispatch_to_evm::<T>)?;910		Ok(())911	}912913	/// @notice Burns a specific ERC721 token.914	/// @dev Throws unless `msg.sender` is the current owner or an authorized915	///  operator for this NFT. Throws if `from` is not the current owner. Throws916	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.917	/// @param from The current owner of the NFT918	/// @param tokenId The NFT to transfer919	#[weight(<SelfWeightOf<T>>::burn_from())]920	fn burn_from_cross(921		&mut self,922		caller: Caller,923		from: eth::CrossAddress,924		token_id: U256,925	) -> Result<()> {926		let caller = T::CrossAccountId::from_eth(caller);927		let from = from.into_sub_cross_account::<T>()?;928		let token = token_id.try_into()?;929930		<Pallet<T>>::burn_from(self, &caller, &from, token, &nesting_budget(&self.recorder))931			.map_err(dispatch_to_evm::<T>)?;932		Ok(())933	}934935	/// @notice Returns next free NFT ID.936	fn next_token_id(&self) -> Result<U256> {937		self.consume_store_reads(1)?;938		Ok(<Pallet<T>>::next_token_id(self)939			.map_err(dispatch_to_evm::<T>)?940			.into())941	}942943	/// @notice Function to mint multiple tokens.944	/// @dev `tokenIds` should be an array of consecutive numbers and first number945	///  should be obtained with `nextTokenId` method946	/// @param to The new owner947	/// @param tokenIds IDs of the minted NFTs948	#[solidity(hide)]949	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]950	fn mint_bulk(&mut self, caller: Caller, to: Address, token_ids: Vec<U256>) -> Result<bool> {951		let caller = T::CrossAccountId::from_eth(caller);952		let to = T::CrossAccountId::from_eth(to);953		let mut expected_index = <TokensMinted<T>>::get(self.id)954			.checked_add(1)955			.ok_or("item id overflow")?;956957		let total_tokens = token_ids.len();958		for id in token_ids.into_iter() {959			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;960			if id != expected_index {961				return Err("item id should be next".into());962			}963			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;964		}965		let data = (0..total_tokens)966			.map(|_| CreateItemData::<T> {967				properties: BoundedVec::default(),968				owner: to.clone(),969			})970			.collect();971972		<Pallet<T>>::create_multiple_items(self, &caller, data, &nesting_budget(&self.recorder))973			.map_err(dispatch_to_evm::<T>)?;974		Ok(true)975	}976977	/// @notice Function to mint a token.978	/// @param data Array of pairs of token owner and token's properties for minted token979	#[weight(980		mint_with_props_weight::<T>(981			<SelfWeightOf<T>>::create_multiple_items_ex(data.len() as u32),982			data.iter().map(|d| d.properties.len() as u32),983		)984	)]985	fn mint_bulk_cross(&mut self, caller: Caller, data: Vec<MintTokenData>) -> Result<bool> {986		let caller = T::CrossAccountId::from_eth(caller);987988		let mut create_nft_data = Vec::with_capacity(data.len());989		for MintTokenData { owner, properties } in data {990			let owner = owner.into_sub_cross_account::<T>()?;991			create_nft_data.push(CreateItemData::<T> {992				properties: properties993					.into_iter()994					.map(|property| property.try_into())995					.collect::<Result<Vec<_>>>()?996					.try_into()997					.map_err(|_| "too many properties")?,998				owner,999			});1000		}10011002		<Pallet<T>>::create_multiple_items(1003			self,1004			&caller,1005			create_nft_data,1006			&nesting_budget(&self.recorder),1007		)1008		.map_err(dispatch_to_evm::<T>)?;1009		Ok(true)1010	}10111012	/// @notice Function to mint multiple tokens with the given tokenUris.1013	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive1014	///  numbers and first number should be obtained with `nextTokenId` method1015	/// @param to The new owner1016	/// @param tokens array of pairs of token ID and token URI for minted tokens1017	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]1018	#[weight(1019		mint_with_props_weight::<T>(1020			<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32),1021			tokens.iter().map(|_| 1),1022		)1023	)]1024	fn mint_bulk_with_token_uri(1025		&mut self,1026		caller: Caller,1027		to: Address,1028		tokens: Vec<TokenUri>,1029	) -> Result<bool> {1030		let key = key::url();1031		let caller = T::CrossAccountId::from_eth(caller);1032		let to = T::CrossAccountId::from_eth(to);1033		let mut expected_index = <TokensMinted<T>>::get(self.id)1034			.checked_add(1)1035			.ok_or("item id overflow")?;10361037		let mut data = Vec::with_capacity(tokens.len());1038		for TokenUri { id, uri } in tokens {1039			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;1040			if id != expected_index {1041				return Err("item id should be next".into());1042			}1043			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;10441045			let mut properties = CollectionPropertiesVec::default();1046			properties1047				.try_push(Property {1048					key: key.clone(),1049					value: uri1050						.into_bytes()1051						.try_into()1052						.map_err(|_| "token uri is too long")?,1053				})1054				.map_err(|e| Error::Revert(alloc::format!("can't add property: {e:?}")))?;10551056			data.push(CreateItemData::<T> {1057				properties,1058				owner: to.clone(),1059			});1060		}10611062		<Pallet<T>>::create_multiple_items(self, &caller, data, &nesting_budget(&self.recorder))1063			.map_err(dispatch_to_evm::<T>)?;1064		Ok(true)1065	}10661067	/// @notice Function to mint a token.1068	/// @param to The new owner crossAccountId1069	/// @param properties Properties of minted token1070	/// @return uint256 The id of the newly minted token1071	#[weight(mint_with_props_weight::<T>(<SelfWeightOf<T>>::create_item(), [properties.len() as u32].into_iter()))]1072	fn mint_cross(1073		&mut self,1074		caller: Caller,1075		to: eth::CrossAddress,1076		properties: Vec<eth::Property>,1077	) -> Result<U256> {1078		let token_id = <TokensMinted<T>>::get(self.id)1079			.checked_add(1)1080			.ok_or("item id overflow")?;10811082		let to = to.into_sub_cross_account::<T>()?;10831084		let properties = properties1085			.into_iter()1086			.map(eth::Property::try_into)1087			.collect::<Result<Vec<_>>>()?1088			.try_into()1089			.map_err(|_| Error::Revert("too many properties".to_string()))?;10901091		let caller = T::CrossAccountId::from_eth(caller);10921093		<Pallet<T>>::create_item(1094			self,1095			&caller,1096			CreateItemData::<T> {1097				properties,1098				owner: to,1099			},1100			&nesting_budget(&self.recorder),1101		)1102		.map_err(dispatch_to_evm::<T>)?;11031104		Ok(token_id.into())1105	}11061107	/// @notice Returns collection helper contract address1108	fn collection_helper_address(&self) -> Address {1109		T::ContractAddress::get()1110	}1111}11121113#[solidity_interface(1114	name = UniqueNFT,1115	is(1116		ERC721,1117		ERC721Enumerable,1118		ERC721UniqueExtensions,1119		ERC721UniqueMintable,1120		ERC721Burnable,1121		ERC721Metadata(if(this.flags.erc721metadata)),1122		Collection(via(common_mut returns CollectionHandle<T>)),1123		TokenProperties,1124	),1125	enum(derive(PreDispatch)),1126)]1127impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}11281129// Not a tests, but code generators1130generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);1131generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);11321133impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>1134where1135	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1136{1137	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");11381139	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {1140		call::<T, UniqueNFTCall<T>, _, _>(handle, self)1141	}1142}