git.delta.rocks / unique-network / refs/commits / 6afd9fe97b2d

difftreelog

source

pallets/nonfungible/src/erc.rs29.4 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;23use alloc::string::ToString;24use core::{25	char::{REPLACEMENT_CHARACTER, decode_utf16},26	convert::TryInto,27};28use evm_coder::{29	ToLog,30	execution::*,31	generate_stubgen, solidity, solidity_interface,32	types::*,33	weight,34	custom_signature::{SignatureUnit, FunctionSignature, SignaturePreferences},35	make_signature,36};37use frame_support::BoundedVec;38use up_data_structs::{39	TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,40	CollectionPropertiesVec,41};42use pallet_evm_coder_substrate::dispatch_to_evm;43use sp_std::vec::Vec;44use pallet_common::{45	erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},46	CollectionHandle, CollectionPropertyPermissions,47	eth::convert_tuple_to_cross_account,48};49use pallet_evm::{account::CrossAccountId, PrecompileHandle};50use pallet_evm_coder_substrate::call;51use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};5253use crate::{54	AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,55	SelfWeightOf, weights::WeightInfo, TokenProperties,56};5758/// @title A contract that allows to set and delete token properties and change token property permissions.59#[solidity_interface(name = TokenProperties)]60impl<T: Config> NonfungibleHandle<T> {61	/// @notice Set permissions for token property.62	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.63	/// @param key Property key.64	/// @param isMutable Permission to mutate property.65	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.66	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.67	fn set_token_property_permission(68		&mut self,69		caller: caller,70		key: string,71		is_mutable: bool,72		collection_admin: bool,73		token_owner: bool,74	) -> Result<()> {75		let caller = T::CrossAccountId::from_eth(caller);76		<Pallet<T>>::set_property_permission(77			self,78			&caller,79			PropertyKeyPermission {80				key: <Vec<u8>>::from(key)81					.try_into()82					.map_err(|_| "too long key")?,83				permission: PropertyPermission {84					mutable: is_mutable,85					collection_admin,86					token_owner,87				},88			},89		)90		.map_err(dispatch_to_evm::<T>)91	}9293	/// @notice Set token property value.94	/// @dev Throws error if `msg.sender` has no permission to edit the property.95	/// @param tokenId ID of the token.96	/// @param key Property key.97	/// @param value Property value.98	fn set_property(99		&mut self,100		caller: caller,101		token_id: uint256,102		key: string,103		value: bytes,104	) -> Result<()> {105		let caller = T::CrossAccountId::from_eth(caller);106		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;107		let key = <Vec<u8>>::from(key)108			.try_into()109			.map_err(|_| "key too long")?;110		let value = value.0.try_into().map_err(|_| "value too long")?;111112		let nesting_budget = self113			.recorder114			.weight_calls_budget(<StructureWeight<T>>::find_parent());115116		<Pallet<T>>::set_token_property(117			self,118			&caller,119			TokenId(token_id),120			Property { key, value },121			&nesting_budget,122		)123		.map_err(dispatch_to_evm::<T>)124	}125126	/// @notice Set token properties value.127	/// @dev Throws error if `msg.sender` has no permission to edit the property.128	/// @param tokenId ID of the token.129	/// @param properties settable properties130	#[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]131	fn set_properties(132		&mut self,133		caller: caller,134		token_id: uint256,135		properties: Vec<(string, bytes)>,136	) -> Result<()> {137		let caller = T::CrossAccountId::from_eth(caller);138		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;139140		let nesting_budget = self141			.recorder142			.weight_calls_budget(<StructureWeight<T>>::find_parent());143144		let properties = properties145			.into_iter()146			.map(|(key, value)| {147				let key = <Vec<u8>>::from(key)148					.try_into()149					.map_err(|_| "key too large")?;150151				let value = value.0.try_into().map_err(|_| "value too large")?;152153				Ok(Property { key, value })154			})155			.collect::<Result<Vec<_>>>()?;156157		<Pallet<T>>::set_token_properties(158			self,159			&caller,160			TokenId(token_id),161			properties.into_iter(),162			<Pallet<T>>::token_exists(&self, TokenId(token_id)),163			&nesting_budget,164		)165		.map_err(dispatch_to_evm::<T>)166	}167168	/// @notice Delete token property value.169	/// @dev Throws error if `msg.sender` has no permission to edit the property.170	/// @param tokenId ID of the token.171	/// @param key Property key.172	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {173		let caller = T::CrossAccountId::from_eth(caller);174		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;175		let key = <Vec<u8>>::from(key)176			.try_into()177			.map_err(|_| "key too long")?;178179		let nesting_budget = self180			.recorder181			.weight_calls_budget(<StructureWeight<T>>::find_parent());182183		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)184			.map_err(dispatch_to_evm::<T>)185	}186187	/// @notice Get token property value.188	/// @dev Throws error if key not found189	/// @param tokenId ID of the token.190	/// @param key Property key.191	/// @return Property value bytes192	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {193		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;194		let key = <Vec<u8>>::from(key)195			.try_into()196			.map_err(|_| "key too long")?;197198		let props = <TokenProperties<T>>::get((self.id, token_id));199		let prop = props.get(&key).ok_or("key not found")?;200201		Ok(prop.to_vec().into())202	}203}204205#[derive(ToLog)]206pub enum ERC721Events {207	/// @dev This emits when ownership of any NFT changes by any mechanism.208	///  This event emits when NFTs are created (`from` == 0) and destroyed209	///  (`to` == 0). Exception: during contract creation, any number of NFTs210	///  may be created and assigned without emitting Transfer. At the time of211	///  any transfer, the approved address for that NFT (if any) is reset to none.212	Transfer {213		#[indexed]214		from: address,215		#[indexed]216		to: address,217		#[indexed]218		token_id: uint256,219	},220	/// @dev This emits when the approved address for an NFT is changed or221	///  reaffirmed. The zero address indicates there is no approved address.222	///  When a Transfer event emits, this also indicates that the approved223	///  address for that NFT (if any) is reset to none.224	Approval {225		#[indexed]226		owner: address,227		#[indexed]228		approved: address,229		#[indexed]230		token_id: uint256,231	},232	/// @dev This emits when an operator is enabled or disabled for an owner.233	///  The operator can manage all NFTs of the owner.234	#[allow(dead_code)]235	ApprovalForAll {236		#[indexed]237		owner: address,238		#[indexed]239		operator: address,240		approved: bool,241	},242}243244#[derive(ToLog)]245pub enum ERC721UniqueMintableEvents {246	#[allow(dead_code)]247	MintingFinished {},248}249250/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension251/// @dev See https://eips.ethereum.org/EIPS/eip-721252#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]253impl<T: Config> NonfungibleHandle<T>254where255	T::AccountId: From<[u8; 32]>,256{257	/// @notice A descriptive name for a collection of NFTs in this contract258	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`259	#[solidity(hide, rename_selector = "name")]260	fn name_proxy(&self) -> Result<string> {261		self.name()262	}263264	/// @notice An abbreviated name for NFTs in this contract265	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`266	#[solidity(hide, rename_selector = "symbol")]267	fn symbol_proxy(&self) -> Result<string> {268		self.symbol()269	}270271	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.272	///273	/// @dev If the token has a `url` property and it is not empty, it is returned.274	///  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`.275	///  If the collection property `baseURI` is empty or absent, return "" (empty string)276	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix277	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).278	///279	/// @return token's const_metadata280	#[solidity(rename_selector = "tokenURI")]281	fn token_uri(&self, token_id: uint256) -> Result<string> {282		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;283284		match get_token_property(self, token_id_u32, &key::url()).as_deref() {285			Err(_) | Ok("") => (),286			Ok(url) => {287				return Ok(url.into());288			}289		};290291		let base_uri =292			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())293				.map(BoundedVec::into_inner)294				.map(string::from_utf8)295				.transpose()296				.map_err(|e| {297					Error::Revert(alloc::format!(298						"Can not convert value \"baseURI\" to string with error \"{}\"",299						e300					))301				})?;302303		let base_uri = match base_uri.as_deref() {304			None | Some("") => {305				return Ok("".into());306			}307			Some(base_uri) => base_uri.into(),308		};309310		Ok(311			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {312				Err(_) | Ok("") => base_uri,313				Ok(suffix) => base_uri + suffix,314			},315		)316	}317}318319/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension320/// @dev See https://eips.ethereum.org/EIPS/eip-721321#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]322impl<T: Config> NonfungibleHandle<T> {323	/// @notice Enumerate valid NFTs324	/// @param index A counter less than `totalSupply()`325	/// @return The token identifier for the `index`th NFT,326	///  (sort order not specified)327	fn token_by_index(&self, index: uint256) -> Result<uint256> {328		Ok(index)329	}330331	/// @dev Not implemented332	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {333		// TODO: Not implemetable334		Err("not implemented".into())335	}336337	/// @notice Count NFTs tracked by this contract338	/// @return A count of valid NFTs tracked by this contract, where each one of339	///  them has an assigned and queryable owner not equal to the zero address340	fn total_supply(&self) -> Result<uint256> {341		self.consume_store_reads(1)?;342		Ok(<Pallet<T>>::total_supply(self).into())343	}344}345346/// @title ERC-721 Non-Fungible Token Standard347/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md348#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]349impl<T: Config> NonfungibleHandle<T> {350	/// @notice Count all NFTs assigned to an owner351	/// @dev NFTs assigned to the zero address are considered invalid, and this352	///  function throws for queries about the zero address.353	/// @param owner An address for whom to query the balance354	/// @return The number of NFTs owned by `owner`, possibly zero355	fn balance_of(&self, owner: address) -> Result<uint256> {356		self.consume_store_reads(1)?;357		let owner = T::CrossAccountId::from_eth(owner);358		let balance = <AccountBalance<T>>::get((self.id, owner));359		Ok(balance.into())360	}361	/// @notice Find the owner of an NFT362	/// @dev NFTs assigned to zero address are considered invalid, and queries363	///  about them do throw.364	/// @param tokenId The identifier for an NFT365	/// @return The address of the owner of the NFT366	fn owner_of(&self, token_id: uint256) -> Result<address> {367		self.consume_store_reads(1)?;368		let token: TokenId = token_id.try_into()?;369		Ok(*<TokenData<T>>::get((self.id, token))370			.ok_or("token not found")?371			.owner372			.as_eth())373	}374	/// @dev Not implemented375	#[solidity(rename_selector = "safeTransferFrom")]376	fn safe_transfer_from_with_data(377		&mut self,378		_from: address,379		_to: address,380		_token_id: uint256,381		_data: bytes,382	) -> Result<void> {383		// TODO: Not implemetable384		Err("not implemented".into())385	}386	/// @dev Not implemented387	fn safe_transfer_from(388		&mut self,389		_from: address,390		_to: address,391		_token_id: uint256,392	) -> Result<void> {393		// TODO: Not implemetable394		Err("not implemented".into())395	}396397	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE398	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE399	///  THEY MAY BE PERMANENTLY LOST400	/// @dev Throws unless `msg.sender` is the current owner or an authorized401	///  operator for this NFT. Throws if `from` is not the current owner. Throws402	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.403	/// @param from The current owner of the NFT404	/// @param to The new owner405	/// @param tokenId The NFT to transfer406	#[weight(<SelfWeightOf<T>>::transfer_from())]407	fn transfer_from(408		&mut self,409		caller: caller,410		from: address,411		to: address,412		token_id: uint256,413	) -> Result<void> {414		let caller = T::CrossAccountId::from_eth(caller);415		let from = T::CrossAccountId::from_eth(from);416		let to = T::CrossAccountId::from_eth(to);417		let token = token_id.try_into()?;418		let budget = self419			.recorder420			.weight_calls_budget(<StructureWeight<T>>::find_parent());421422		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)423			.map_err(dispatch_to_evm::<T>)?;424		Ok(())425	}426427	/// @notice Set or reaffirm the approved address for an NFT428	/// @dev The zero address indicates there is no approved address.429	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized430	///  operator of the current owner.431	/// @param approved The new approved NFT controller432	/// @param tokenId The NFT to approve433	#[weight(<SelfWeightOf<T>>::approve())]434	fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {435		let caller = T::CrossAccountId::from_eth(caller);436		let approved = T::CrossAccountId::from_eth(approved);437		let token = token_id.try_into()?;438439		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))440			.map_err(dispatch_to_evm::<T>)?;441		Ok(())442	}443444	/// @dev Not implemented445	fn set_approval_for_all(446		&mut self,447		_caller: caller,448		_operator: address,449		_approved: bool,450	) -> Result<void> {451		// TODO: Not implemetable452		Err("not implemented".into())453	}454455	/// @dev Not implemented456	fn get_approved(&self, _token_id: uint256) -> Result<address> {457		// TODO: Not implemetable458		Err("not implemented".into())459	}460461	/// @dev Not implemented462	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {463		// TODO: Not implemetable464		Err("not implemented".into())465	}466}467468/// @title ERC721 Token that can be irreversibly burned (destroyed).469#[solidity_interface(name = ERC721Burnable)]470impl<T: Config> NonfungibleHandle<T> {471	/// @notice Burns a specific ERC721 token.472	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized473	///  operator of the current owner.474	/// @param tokenId The NFT to approve475	#[weight(<SelfWeightOf<T>>::burn_item())]476	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {477		let caller = T::CrossAccountId::from_eth(caller);478		let token = token_id.try_into()?;479480		<Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;481		Ok(())482	}483}484485/// @title ERC721 minting logic.486#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]487impl<T: Config> NonfungibleHandle<T> {488	fn minting_finished(&self) -> Result<bool> {489		Ok(false)490	}491492	/// @notice Function to mint token.493	/// @param to The new owner494	/// @return uint256 The id of the newly minted token495	#[weight(<SelfWeightOf<T>>::create_item())]496	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {497		let token_id: uint256 = <TokensMinted<T>>::get(self.id)498			.checked_add(1)499			.ok_or("item id overflow")?500			.into();501		self.mint_check_id(caller, to, token_id)?;502		Ok(token_id)503	}504505	/// @notice Function to mint token.506	/// @dev `tokenId` should be obtained with `nextTokenId` method,507	///  unlike standard, you can't specify it manually508	/// @param to The new owner509	/// @param tokenId ID of the minted NFT510	#[solidity(hide, rename_selector = "mint")]511	#[weight(<SelfWeightOf<T>>::create_item())]512	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {513		let caller = T::CrossAccountId::from_eth(caller);514		let to = T::CrossAccountId::from_eth(to);515		let token_id: u32 = token_id.try_into()?;516		let budget = self517			.recorder518			.weight_calls_budget(<StructureWeight<T>>::find_parent());519520		if <TokensMinted<T>>::get(self.id)521			.checked_add(1)522			.ok_or("item id overflow")?523			!= token_id524		{525			return Err("item id should be next".into());526		}527528		<Pallet<T>>::create_item(529			self,530			&caller,531			CreateItemData::<T> {532				properties: BoundedVec::default(),533				owner: to,534			},535			&budget,536		)537		.map_err(dispatch_to_evm::<T>)?;538539		Ok(true)540	}541542	/// @notice Function to mint token with the given tokenUri.543	/// @param to The new owner544	/// @param tokenUri Token URI that would be stored in the NFT properties545	/// @return uint256 The id of the newly minted token546	#[solidity(rename_selector = "mintWithTokenURI")]547	#[weight(<SelfWeightOf<T>>::create_item())]548	fn mint_with_token_uri(549		&mut self,550		caller: caller,551		to: address,552		token_uri: string,553	) -> Result<uint256> {554		let token_id: uint256 = <TokensMinted<T>>::get(self.id)555			.checked_add(1)556			.ok_or("item id overflow")?557			.into();558		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;559		Ok(token_id)560	}561562	/// @notice Function to mint token with the given tokenUri.563	/// @dev `tokenId` should be obtained with `nextTokenId` method,564	///  unlike standard, you can't specify it manually565	/// @param to The new owner566	/// @param tokenId ID of the minted NFT567	/// @param tokenUri Token URI that would be stored in the NFT properties568	#[solidity(hide, rename_selector = "mintWithTokenURI")]569	#[weight(<SelfWeightOf<T>>::create_item())]570	fn mint_with_token_uri_check_id(571		&mut self,572		caller: caller,573		to: address,574		token_id: uint256,575		token_uri: string,576	) -> Result<bool> {577		let key = key::url();578		let permission = get_token_permission::<T>(self.id, &key)?;579		if !permission.collection_admin {580			return Err("Operation is not allowed".into());581		}582583		let caller = T::CrossAccountId::from_eth(caller);584		let to = T::CrossAccountId::from_eth(to);585		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;586		let budget = self587			.recorder588			.weight_calls_budget(<StructureWeight<T>>::find_parent());589590		if <TokensMinted<T>>::get(self.id)591			.checked_add(1)592			.ok_or("item id overflow")?593			!= token_id594		{595			return Err("item id should be next".into());596		}597598		let mut properties = CollectionPropertiesVec::default();599		properties600			.try_push(Property {601				key,602				value: token_uri603					.into_bytes()604					.try_into()605					.map_err(|_| "token uri is too long")?,606			})607			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;608609		<Pallet<T>>::create_item(610			self,611			&caller,612			CreateItemData::<T> {613				properties,614				owner: to,615			},616			&budget,617		)618		.map_err(dispatch_to_evm::<T>)?;619		Ok(true)620	}621622	/// @dev Not implemented623	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {624		Err("not implementable".into())625	}626}627628fn get_token_property<T: Config>(629	collection: &CollectionHandle<T>,630	token_id: u32,631	key: &up_data_structs::PropertyKey,632) -> Result<string> {633	collection.consume_store_reads(1)?;634	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))635		.map_err(|_| Error::Revert("Token properties not found".into()))?;636	if let Some(property) = properties.get(key) {637		return Ok(string::from_utf8_lossy(property).into());638	}639640	Err("Property tokenURI not found".into())641}642643fn get_token_permission<T: Config>(644	collection_id: CollectionId,645	key: &PropertyKey,646) -> Result<PropertyPermission> {647	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)648		.map_err(|_| Error::Revert("No permissions for collection".into()))?;649	let a = token_property_permissions650		.get(key)651		.map(Clone::clone)652		.ok_or_else(|| {653			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();654			Error::Revert(alloc::format!("No permission for key {}", key))655		})?;656	Ok(a)657}658659/// @title Unique extensions for ERC721.660#[solidity_interface(name = ERC721UniqueExtensions)]661impl<T: Config> NonfungibleHandle<T>662where663	T::AccountId: From<[u8; 32]>,664{665	/// @notice A descriptive name for a collection of NFTs in this contract666	fn name(&self) -> Result<string> {667		Ok(decode_utf16(self.name.iter().copied())668			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))669			.collect::<string>())670	}671672	/// @notice An abbreviated name for NFTs in this contract673	fn symbol(&self) -> Result<string> {674		Ok(string::from_utf8_lossy(&self.token_prefix).into())675	}676677	/// @notice Set or reaffirm the approved address for an NFT678	/// @dev The zero address indicates there is no approved address.679	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized680	///  operator of the current owner.681	/// @param approved The new substrate address approved NFT controller682	/// @param tokenId The NFT to approve683	#[weight(<SelfWeightOf<T>>::approve())]684	fn approve_cross(685		&mut self,686		caller: caller,687		approved: EthCrossAccount,688		token_id: uint256,689	) -> Result<void> {690		let caller = T::CrossAccountId::from_eth(caller);691		let approved = approved.into_sub_cross_account::<T>()?;692		let token = token_id.try_into()?;693694		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))695			.map_err(dispatch_to_evm::<T>)?;696		Ok(())697	}698699	/// @notice Transfer ownership of an NFT700	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`701	///  is the zero address. Throws if `tokenId` is not a valid NFT.702	/// @param to The new owner703	/// @param tokenId The NFT to transfer704	#[weight(<SelfWeightOf<T>>::transfer())]705	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {706		let caller = T::CrossAccountId::from_eth(caller);707		let to = T::CrossAccountId::from_eth(to);708		let token = token_id.try_into()?;709		let budget = self710			.recorder711			.weight_calls_budget(<StructureWeight<T>>::find_parent());712713		<Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;714		Ok(())715	}716717	/// @notice Transfer ownership of an NFT from cross account address to cross account address718	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`719	///  is the zero address. Throws if `tokenId` is not a valid NFT.720	/// @param from Cross acccount address of current owner721	/// @param to Cross acccount address of new owner722	/// @param tokenId The NFT to transfer723	#[weight(<SelfWeightOf<T>>::transfer())]724	fn transfer_from_cross(725		&mut self,726		caller: caller,727		from: EthCrossAccount,728		to: EthCrossAccount,729		token_id: uint256,730	) -> Result<void> {731		let caller = T::CrossAccountId::from_eth(caller);732		let from = from.into_sub_cross_account::<T>()?;733		let to = to.into_sub_cross_account::<T>()?;734		let token_id = token_id.try_into()?;735		let budget = self736			.recorder737			.weight_calls_budget(<StructureWeight<T>>::find_parent());738		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)739			.map_err(dispatch_to_evm::<T>)?;740		Ok(())741	}742743	/// @notice Burns a specific ERC721 token.744	/// @dev Throws unless `msg.sender` is the current owner or an authorized745	///  operator for this NFT. Throws if `from` is not the current owner. Throws746	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.747	/// @param from The current owner of the NFT748	/// @param tokenId The NFT to transfer749	#[weight(<SelfWeightOf<T>>::burn_from())]750	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {751		let caller = T::CrossAccountId::from_eth(caller);752		let from = T::CrossAccountId::from_eth(from);753		let token = token_id.try_into()?;754		let budget = self755			.recorder756			.weight_calls_budget(<StructureWeight<T>>::find_parent());757758		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)759			.map_err(dispatch_to_evm::<T>)?;760		Ok(())761	}762763	/// @notice Burns a specific ERC721 token.764	/// @dev Throws unless `msg.sender` is the current owner or an authorized765	///  operator for this NFT. Throws if `from` is not the current owner. Throws766	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.767	/// @param from The current owner of the NFT768	/// @param tokenId The NFT to transfer769	#[weight(<SelfWeightOf<T>>::burn_from())]770	fn burn_from_cross(771		&mut self,772		caller: caller,773		from: EthCrossAccount,774		token_id: uint256,775	) -> Result<void> {776		let caller = T::CrossAccountId::from_eth(caller);777		let from = from.into_sub_cross_account::<T>()?;778		let token = token_id.try_into()?;779		let budget = self780			.recorder781			.weight_calls_budget(<StructureWeight<T>>::find_parent());782783		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)784			.map_err(dispatch_to_evm::<T>)?;785		Ok(())786	}787788	/// @notice Returns next free NFT ID.789	fn next_token_id(&self) -> Result<uint256> {790		self.consume_store_reads(1)?;791		Ok(<TokensMinted<T>>::get(self.id)792			.checked_add(1)793			.ok_or("item id overflow")?794			.into())795	}796797	/// @notice Function to mint multiple tokens.798	/// @dev `tokenIds` should be an array of consecutive numbers and first number799	///  should be obtained with `nextTokenId` method800	/// @param to The new owner801	/// @param tokenIds IDs of the minted NFTs802	#[solidity(hide)]803	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]804	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {805		let caller = T::CrossAccountId::from_eth(caller);806		let to = T::CrossAccountId::from_eth(to);807		let mut expected_index = <TokensMinted<T>>::get(self.id)808			.checked_add(1)809			.ok_or("item id overflow")?;810		let budget = self811			.recorder812			.weight_calls_budget(<StructureWeight<T>>::find_parent());813814		let total_tokens = token_ids.len();815		for id in token_ids.into_iter() {816			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;817			if id != expected_index {818				return Err("item id should be next".into());819			}820			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;821		}822		let data = (0..total_tokens)823			.map(|_| CreateItemData::<T> {824				properties: BoundedVec::default(),825				owner: to.clone(),826			})827			.collect();828829		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)830			.map_err(dispatch_to_evm::<T>)?;831		Ok(true)832	}833834	/// @notice Function to mint multiple tokens with the given tokenUris.835	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive836	///  numbers and first number should be obtained with `nextTokenId` method837	/// @param to The new owner838	/// @param tokens array of pairs of token ID and token URI for minted tokens839	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]840	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]841	fn mint_bulk_with_token_uri(842		&mut self,843		caller: caller,844		to: address,845		tokens: Vec<(uint256, string)>,846	) -> Result<bool> {847		let key = key::url();848		let caller = T::CrossAccountId::from_eth(caller);849		let to = T::CrossAccountId::from_eth(to);850		let mut expected_index = <TokensMinted<T>>::get(self.id)851			.checked_add(1)852			.ok_or("item id overflow")?;853		let budget = self854			.recorder855			.weight_calls_budget(<StructureWeight<T>>::find_parent());856857		let mut data = Vec::with_capacity(tokens.len());858		for (id, token_uri) in tokens {859			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;860			if id != expected_index {861				return Err("item id should be next".into());862			}863			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;864865			let mut properties = CollectionPropertiesVec::default();866			properties867				.try_push(Property {868					key: key.clone(),869					value: token_uri870						.into_bytes()871						.try_into()872						.map_err(|_| "token uri is too long")?,873				})874				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;875876			data.push(CreateItemData::<T> {877				properties,878				owner: to.clone(),879			});880		}881882		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)883			.map_err(dispatch_to_evm::<T>)?;884		Ok(true)885	}886}887888#[solidity_interface(889	name = UniqueNFT,890	is(891		ERC721,892		ERC721Enumerable,893		ERC721UniqueExtensions,894		ERC721UniqueMintable,895		ERC721Burnable,896		ERC721Metadata(if(this.flags.erc721metadata)),897		Collection(via(common_mut returns CollectionHandle<T>)),898		TokenProperties,899	)900)]901impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}902903// Not a tests, but code generators904generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);905generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);906907impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>908where909	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,910{911	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");912913	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {914		call::<T, UniqueNFTCall<T>, _, _>(handle, self)915	}916}