git.delta.rocks / unique-network / refs/commits / 01896baccffe

difftreelog

source

pallets/nonfungible/src/erc.rs29.3 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::{format, string::ToString};24use core::{25	char::{REPLACEMENT_CHARACTER, decode_utf16},26	convert::TryInto,27};28use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};29use frame_support::BoundedVec;30use up_data_structs::{31	TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,32	CollectionPropertiesVec,33};34use pallet_evm_coder_substrate::dispatch_to_evm;35use sp_std::vec::Vec;36use pallet_common::{37	erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},38	CollectionHandle, CollectionPropertyPermissions,39	eth::convert_tuple_to_cross_account,40};41use pallet_evm::{account::CrossAccountId, PrecompileHandle};42use pallet_evm_coder_substrate::call;43use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};4445use crate::{46	AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,47	SelfWeightOf, weights::WeightInfo, TokenProperties,48};4950/// @title A contract that allows to set and delete token properties and change token property permissions.51#[solidity_interface(name = TokenProperties)]52impl<T: Config> NonfungibleHandle<T> {53	/// @notice Set permissions for token property.54	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.55	/// @param key Property key.56	/// @param isMutable Permission to mutate property.57	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.58	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.59	fn set_token_property_permission(60		&mut self,61		caller: caller,62		key: string,63		is_mutable: bool,64		collection_admin: bool,65		token_owner: bool,66	) -> Result<()> {67		let caller = T::CrossAccountId::from_eth(caller);68		<Pallet<T>>::set_property_permission(69			self,70			&caller,71			PropertyKeyPermission {72				key: <Vec<u8>>::from(key)73					.try_into()74					.map_err(|_| "too long key")?,75				permission: PropertyPermission {76					mutable: is_mutable,77					collection_admin,78					token_owner,79				},80			},81		)82		.map_err(dispatch_to_evm::<T>)83	}8485	/// @notice Set token property value.86	/// @dev Throws error if `msg.sender` has no permission to edit the property.87	/// @param tokenId ID of the token.88	/// @param key Property key.89	/// @param value Property value.90	fn set_property(91		&mut self,92		caller: caller,93		token_id: uint256,94		key: string,95		value: bytes,96	) -> Result<()> {97		let caller = T::CrossAccountId::from_eth(caller);98		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;99		let key = <Vec<u8>>::from(key)100			.try_into()101			.map_err(|_| "key too long")?;102		let value = value.0.try_into().map_err(|_| "value too long")?;103104		let nesting_budget = self105			.recorder106			.weight_calls_budget(<StructureWeight<T>>::find_parent());107108		<Pallet<T>>::set_token_property(109			self,110			&caller,111			TokenId(token_id),112			Property { key, value },113			&nesting_budget,114		)115		.map_err(dispatch_to_evm::<T>)116	}117118	/// @notice Set token properties value.119	/// @dev Throws error if `msg.sender` has no permission to edit the property.120	/// @param tokenId ID of the token.121	/// @param properties settable properties122	#[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]123	fn set_properties(124		&mut self,125		caller: caller,126		token_id: uint256,127		properties: Vec<(string, bytes)>,128	) -> Result<()> {129		let caller = T::CrossAccountId::from_eth(caller);130		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;131132		let nesting_budget = self133			.recorder134			.weight_calls_budget(<StructureWeight<T>>::find_parent());135136		let properties = properties137			.into_iter()138			.map(|(key, value)| {139				let key = <Vec<u8>>::from(key)140					.try_into()141					.map_err(|_| "key too large")?;142143				let value = value.0.try_into().map_err(|_| "value too large")?;144145				Ok(Property { key, value })146			})147			.collect::<Result<Vec<_>>>()?;148149		<Pallet<T>>::set_token_properties(150			self,151			&caller,152			TokenId(token_id),153			properties.into_iter(),154			<Pallet<T>>::token_exists(&self, TokenId(token_id)),155			&nesting_budget,156		)157		.map_err(dispatch_to_evm::<T>)158	}159160	/// @notice Delete token property value.161	/// @dev Throws error if `msg.sender` has no permission to edit the property.162	/// @param tokenId ID of the token.163	/// @param key Property key.164	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {165		let caller = T::CrossAccountId::from_eth(caller);166		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;167		let key = <Vec<u8>>::from(key)168			.try_into()169			.map_err(|_| "key too long")?;170171		let nesting_budget = self172			.recorder173			.weight_calls_budget(<StructureWeight<T>>::find_parent());174175		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)176			.map_err(dispatch_to_evm::<T>)177	}178179	/// @notice Get token property value.180	/// @dev Throws error if key not found181	/// @param tokenId ID of the token.182	/// @param key Property key.183	/// @return Property value bytes184	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {185		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;186		let key = <Vec<u8>>::from(key)187			.try_into()188			.map_err(|_| "key too long")?;189190		let props = <TokenProperties<T>>::get((self.id, token_id));191		let prop = props.get(&key).ok_or("key not found")?;192193		Ok(prop.to_vec().into())194	}195}196197#[derive(ToLog)]198pub enum ERC721Events {199	/// @dev This emits when ownership of any NFT changes by any mechanism.200	///  This event emits when NFTs are created (`from` == 0) and destroyed201	///  (`to` == 0). Exception: during contract creation, any number of NFTs202	///  may be created and assigned without emitting Transfer. At the time of203	///  any transfer, the approved address for that NFT (if any) is reset to none.204	Transfer {205		#[indexed]206		from: address,207		#[indexed]208		to: address,209		#[indexed]210		token_id: uint256,211	},212	/// @dev This emits when the approved address for an NFT is changed or213	///  reaffirmed. The zero address indicates there is no approved address.214	///  When a Transfer event emits, this also indicates that the approved215	///  address for that NFT (if any) is reset to none.216	Approval {217		#[indexed]218		owner: address,219		#[indexed]220		approved: address,221		#[indexed]222		token_id: uint256,223	},224	/// @dev This emits when an operator is enabled or disabled for an owner.225	///  The operator can manage all NFTs of the owner.226	#[allow(dead_code)]227	ApprovalForAll {228		#[indexed]229		owner: address,230		#[indexed]231		operator: address,232		approved: bool,233	},234}235236#[derive(ToLog)]237pub enum ERC721UniqueMintableEvents {238	#[allow(dead_code)]239	MintingFinished {},240}241242/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension243/// @dev See https://eips.ethereum.org/EIPS/eip-721244#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]245impl<T: Config> NonfungibleHandle<T>246where247	T::AccountId: From<[u8; 32]>,248{249	/// @notice A descriptive name for a collection of NFTs in this contract250	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`251	#[solidity(hide, rename_selector = "name")]252	fn name_proxy(&self) -> Result<string> {253		self.name()254	}255256	/// @notice An abbreviated name for NFTs in this contract257	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`258	#[solidity(hide, rename_selector = "symbol")]259	fn symbol_proxy(&self) -> Result<string> {260		self.symbol()261	}262263	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.264	///265	/// @dev If the token has a `url` property and it is not empty, it is returned.266	///  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`.267	///  If the collection property `baseURI` is empty or absent, return "" (empty string)268	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix269	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).270	///271	/// @return token's const_metadata272	#[solidity(rename_selector = "tokenURI")]273	fn token_uri(&self, token_id: uint256) -> Result<string> {274		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;275276		match get_token_property(self, token_id_u32, &key::url()).as_deref() {277			Err(_) | Ok("") => (),278			Ok(url) => {279				return Ok(url.into());280			}281		};282283		let base_uri =284			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())285				.map(BoundedVec::into_inner)286				.map(string::from_utf8)287				.transpose()288				.map_err(|e| {289					Error::Revert(alloc::format!(290						"Can not convert value \"baseURI\" to string with error \"{}\"",291						e292					))293				})?;294295		let base_uri = match base_uri.as_deref() {296			None | Some("") => {297				return Ok("".into());298			}299			Some(base_uri) => base_uri.into(),300		};301302		Ok(303			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {304				Err(_) | Ok("") => base_uri,305				Ok(suffix) => base_uri + suffix,306			},307		)308	}309}310311/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension312/// @dev See https://eips.ethereum.org/EIPS/eip-721313#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]314impl<T: Config> NonfungibleHandle<T> {315	/// @notice Enumerate valid NFTs316	/// @param index A counter less than `totalSupply()`317	/// @return The token identifier for the `index`th NFT,318	///  (sort order not specified)319	fn token_by_index(&self, index: uint256) -> Result<uint256> {320		Ok(index)321	}322323	/// @dev Not implemented324	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {325		// TODO: Not implemetable326		Err("not implemented".into())327	}328329	/// @notice Count NFTs tracked by this contract330	/// @return A count of valid NFTs tracked by this contract, where each one of331	///  them has an assigned and queryable owner not equal to the zero address332	fn total_supply(&self) -> Result<uint256> {333		self.consume_store_reads(1)?;334		Ok(<Pallet<T>>::total_supply(self).into())335	}336}337338/// @title ERC-721 Non-Fungible Token Standard339/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md340#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]341impl<T: Config> NonfungibleHandle<T> {342	/// @notice Count all NFTs assigned to an owner343	/// @dev NFTs assigned to the zero address are considered invalid, and this344	///  function throws for queries about the zero address.345	/// @param owner An address for whom to query the balance346	/// @return The number of NFTs owned by `owner`, possibly zero347	fn balance_of(&self, owner: address) -> Result<uint256> {348		self.consume_store_reads(1)?;349		let owner = T::CrossAccountId::from_eth(owner);350		let balance = <AccountBalance<T>>::get((self.id, owner));351		Ok(balance.into())352	}353	/// @notice Find the owner of an NFT354	/// @dev NFTs assigned to zero address are considered invalid, and queries355	///  about them do throw.356	/// @param tokenId The identifier for an NFT357	/// @return The address of the owner of the NFT358	fn owner_of(&self, token_id: uint256) -> Result<address> {359		self.consume_store_reads(1)?;360		let token: TokenId = token_id.try_into()?;361		Ok(*<TokenData<T>>::get((self.id, token))362			.ok_or("token not found")?363			.owner364			.as_eth())365	}366	/// @dev Not implemented367	#[solidity(rename_selector = "safeTransferFrom")]368	fn safe_transfer_from_with_data(369		&mut self,370		_from: address,371		_to: address,372		_token_id: uint256,373		_data: bytes,374	) -> Result<void> {375		// TODO: Not implemetable376		Err("not implemented".into())377	}378	/// @dev Not implemented379	fn safe_transfer_from(380		&mut self,381		_from: address,382		_to: address,383		_token_id: uint256,384	) -> Result<void> {385		// TODO: Not implemetable386		Err("not implemented".into())387	}388389	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE390	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE391	///  THEY MAY BE PERMANENTLY LOST392	/// @dev Throws unless `msg.sender` is the current owner or an authorized393	///  operator for this NFT. Throws if `from` is not the current owner. Throws394	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.395	/// @param from The current owner of the NFT396	/// @param to The new owner397	/// @param tokenId The NFT to transfer398	#[weight(<SelfWeightOf<T>>::transfer_from())]399	fn transfer_from(400		&mut self,401		caller: caller,402		from: address,403		to: address,404		token_id: uint256,405	) -> Result<void> {406		let caller = T::CrossAccountId::from_eth(caller);407		let from = T::CrossAccountId::from_eth(from);408		let to = T::CrossAccountId::from_eth(to);409		let token = token_id.try_into()?;410		let budget = self411			.recorder412			.weight_calls_budget(<StructureWeight<T>>::find_parent());413414		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)415			.map_err(dispatch_to_evm::<T>)?;416		Ok(())417	}418419	/// @notice Set or reaffirm the approved address for an NFT420	/// @dev The zero address indicates there is no approved address.421	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized422	///  operator of the current owner.423	/// @param approved The new approved NFT controller424	/// @param tokenId The NFT to approve425	#[weight(<SelfWeightOf<T>>::approve())]426	fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {427		let caller = T::CrossAccountId::from_eth(caller);428		let approved = T::CrossAccountId::from_eth(approved);429		let token = token_id.try_into()?;430431		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))432			.map_err(dispatch_to_evm::<T>)?;433		Ok(())434	}435436	/// @dev Not implemented437	fn set_approval_for_all(438		&mut self,439		_caller: caller,440		_operator: address,441		_approved: bool,442	) -> Result<void> {443		// TODO: Not implemetable444		Err("not implemented".into())445	}446447	/// @dev Not implemented448	fn get_approved(&self, _token_id: uint256) -> Result<address> {449		// TODO: Not implemetable450		Err("not implemented".into())451	}452453	/// @dev Not implemented454	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {455		// TODO: Not implemetable456		Err("not implemented".into())457	}458}459460/// @title ERC721 Token that can be irreversibly burned (destroyed).461#[solidity_interface(name = ERC721Burnable)]462impl<T: Config> NonfungibleHandle<T> {463	/// @notice Burns a specific ERC721 token.464	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized465	///  operator of the current owner.466	/// @param tokenId The NFT to approve467	#[weight(<SelfWeightOf<T>>::burn_item())]468	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {469		let caller = T::CrossAccountId::from_eth(caller);470		let token = token_id.try_into()?;471472		<Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;473		Ok(())474	}475}476477/// @title ERC721 minting logic.478#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]479impl<T: Config> NonfungibleHandle<T> {480	fn minting_finished(&self) -> Result<bool> {481		Ok(false)482	}483484	/// @notice Function to mint token.485	/// @param to The new owner486	/// @return uint256 The id of the newly minted token487	#[weight(<SelfWeightOf<T>>::create_item())]488	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {489		let token_id: uint256 = <TokensMinted<T>>::get(self.id)490			.checked_add(1)491			.ok_or("item id overflow")?492			.into();493		self.mint_check_id(caller, to, token_id)?;494		Ok(token_id)495	}496497	/// @notice Function to mint token.498	/// @dev `tokenId` should be obtained with `nextTokenId` method,499	///  unlike standard, you can't specify it manually500	/// @param to The new owner501	/// @param tokenId ID of the minted NFT502	#[solidity(hide, rename_selector = "mint")]503	#[weight(<SelfWeightOf<T>>::create_item())]504	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {505		let caller = T::CrossAccountId::from_eth(caller);506		let to = T::CrossAccountId::from_eth(to);507		let token_id: u32 = token_id.try_into()?;508		let budget = self509			.recorder510			.weight_calls_budget(<StructureWeight<T>>::find_parent());511512		if <TokensMinted<T>>::get(self.id)513			.checked_add(1)514			.ok_or("item id overflow")?515			!= token_id516		{517			return Err("item id should be next".into());518		}519520		<Pallet<T>>::create_item(521			self,522			&caller,523			CreateItemData::<T> {524				properties: BoundedVec::default(),525				owner: to,526			},527			&budget,528		)529		.map_err(dispatch_to_evm::<T>)?;530531		Ok(true)532	}533534	/// @notice Function to mint token with the given tokenUri.535	/// @param to The new owner536	/// @param tokenUri Token URI that would be stored in the NFT properties537	/// @return uint256 The id of the newly minted token538	#[solidity(rename_selector = "mintWithTokenURI")]539	#[weight(<SelfWeightOf<T>>::create_item())]540	fn mint_with_token_uri(541		&mut self,542		caller: caller,543		to: address,544		token_uri: string,545	) -> Result<uint256> {546		let token_id: uint256 = <TokensMinted<T>>::get(self.id)547			.checked_add(1)548			.ok_or("item id overflow")?549			.into();550		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;551		Ok(token_id)552	}553554	/// @notice Function to mint token with the given tokenUri.555	/// @dev `tokenId` should be obtained with `nextTokenId` method,556	///  unlike standard, you can't specify it manually557	/// @param to The new owner558	/// @param tokenId ID of the minted NFT559	/// @param tokenUri Token URI that would be stored in the NFT properties560	#[solidity(hide, rename_selector = "mintWithTokenURI")]561	#[weight(<SelfWeightOf<T>>::create_item())]562	fn mint_with_token_uri_check_id(563		&mut self,564		caller: caller,565		to: address,566		token_id: uint256,567		token_uri: string,568	) -> Result<bool> {569		let key = key::url();570		let permission = get_token_permission::<T>(self.id, &key)?;571		if !permission.collection_admin {572			return Err("Operation is not allowed".into());573		}574575		let caller = T::CrossAccountId::from_eth(caller);576		let to = T::CrossAccountId::from_eth(to);577		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;578		let budget = self579			.recorder580			.weight_calls_budget(<StructureWeight<T>>::find_parent());581582		if <TokensMinted<T>>::get(self.id)583			.checked_add(1)584			.ok_or("item id overflow")?585			!= token_id586		{587			return Err("item id should be next".into());588		}589590		let mut properties = CollectionPropertiesVec::default();591		properties592			.try_push(Property {593				key,594				value: token_uri595					.into_bytes()596					.try_into()597					.map_err(|_| "token uri is too long")?,598			})599			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;600601		<Pallet<T>>::create_item(602			self,603			&caller,604			CreateItemData::<T> {605				properties,606				owner: to,607			},608			&budget,609		)610		.map_err(dispatch_to_evm::<T>)?;611		Ok(true)612	}613614	/// @dev Not implemented615	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {616		Err("not implementable".into())617	}618}619620fn get_token_property<T: Config>(621	collection: &CollectionHandle<T>,622	token_id: u32,623	key: &up_data_structs::PropertyKey,624) -> Result<string> {625	collection.consume_store_reads(1)?;626	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))627		.map_err(|_| Error::Revert("Token properties not found".into()))?;628	if let Some(property) = properties.get(key) {629		return Ok(string::from_utf8_lossy(property).into());630	}631632	Err("Property tokenURI not found".into())633}634635fn get_token_permission<T: Config>(636	collection_id: CollectionId,637	key: &PropertyKey,638) -> Result<PropertyPermission> {639	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)640		.map_err(|_| Error::Revert("No permissions for collection".into()))?;641	let a = token_property_permissions642		.get(key)643		.map(Clone::clone)644		.ok_or_else(|| {645			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();646			Error::Revert(alloc::format!("No permission for key {}", key))647		})?;648	Ok(a)649}650651/// @title Unique extensions for ERC721.652#[solidity_interface(name = ERC721UniqueExtensions)]653impl<T: Config> NonfungibleHandle<T>654where655	T::AccountId: From<[u8; 32]>,656{657	/// @notice A descriptive name for a collection of NFTs in this contract658	fn name(&self) -> Result<string> {659		Ok(decode_utf16(self.name.iter().copied())660			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))661			.collect::<string>())662	}663664	/// @notice An abbreviated name for NFTs in this contract665	fn symbol(&self) -> Result<string> {666		Ok(string::from_utf8_lossy(&self.token_prefix).into())667	}668669	/// @notice Set or reaffirm the approved address for an NFT670	/// @dev The zero address indicates there is no approved address.671	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized672	///  operator of the current owner.673	/// @param approved The new substrate address approved NFT controller674	/// @param tokenId The NFT to approve675	#[weight(<SelfWeightOf<T>>::approve())]676	fn approve_cross(677		&mut self,678		caller: caller,679		approved: (address, uint256),680		token_id: uint256,681	) -> Result<void> {682		let caller = T::CrossAccountId::from_eth(caller);683		let approved = convert_tuple_to_cross_account::<T>(approved)?;684		let token = token_id.try_into()?;685686		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))687			.map_err(dispatch_to_evm::<T>)?;688		Ok(())689	}690691	/// @notice Transfer ownership of an NFT692	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`693	///  is the zero address. Throws if `tokenId` is not a valid NFT.694	/// @param to The new owner695	/// @param tokenId The NFT to transfer696	#[weight(<SelfWeightOf<T>>::transfer())]697	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {698		let caller = T::CrossAccountId::from_eth(caller);699		let to = T::CrossAccountId::from_eth(to);700		let token = token_id.try_into()?;701		let budget = self702			.recorder703			.weight_calls_budget(<StructureWeight<T>>::find_parent());704705		<Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;706		Ok(())707	}708709	/// @notice Transfer ownership of an NFT from cross account address to cross account address710	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`711	///  is the zero address. Throws if `tokenId` is not a valid NFT.712	/// @param from Cross acccount address of current owner713	/// @param to Cross acccount address of new owner714	/// @param tokenId The NFT to transfer715	#[weight(<SelfWeightOf<T>>::transfer())]716	fn transfer_from_cross(717		&mut self,718		caller: caller,719		from: EthCrossAccount,720		to: EthCrossAccount,721		token_id: uint256,722	) -> Result<void> {723		let caller = T::CrossAccountId::from_eth(caller);724		let from = from.into_sub_cross_account::<T>()?;725		let to = to.into_sub_cross_account::<T>()?;726		let token_id = token_id.try_into()?;727		let budget = self728			.recorder729			.weight_calls_budget(<StructureWeight<T>>::find_parent());730		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)731			.map_err(dispatch_to_evm::<T>)?;732		Ok(())733	}734735	/// @notice Burns a specific ERC721 token.736	/// @dev Throws unless `msg.sender` is the current owner or an authorized737	///  operator for this NFT. Throws if `from` is not the current owner. Throws738	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.739	/// @param from The current owner of the NFT740	/// @param tokenId The NFT to transfer741	#[weight(<SelfWeightOf<T>>::burn_from())]742	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {743		let caller = T::CrossAccountId::from_eth(caller);744		let from = T::CrossAccountId::from_eth(from);745		let token = token_id.try_into()?;746		let budget = self747			.recorder748			.weight_calls_budget(<StructureWeight<T>>::find_parent());749750		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)751			.map_err(dispatch_to_evm::<T>)?;752		Ok(())753	}754755	/// @notice Burns a specific ERC721 token.756	/// @dev Throws unless `msg.sender` is the current owner or an authorized757	///  operator for this NFT. Throws if `from` is not the current owner. Throws758	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.759	/// @param from The current owner of the NFT760	/// @param tokenId The NFT to transfer761	#[weight(<SelfWeightOf<T>>::burn_from())]762	fn burn_from_cross(763		&mut self,764		caller: caller,765		from: (address, uint256),766		token_id: uint256,767	) -> Result<void> {768		let caller = T::CrossAccountId::from_eth(caller);769		let from = convert_tuple_to_cross_account::<T>(from)?;770		let token = token_id.try_into()?;771		let budget = self772			.recorder773			.weight_calls_budget(<StructureWeight<T>>::find_parent());774775		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)776			.map_err(dispatch_to_evm::<T>)?;777		Ok(())778	}779780	/// @notice Returns next free NFT ID.781	fn next_token_id(&self) -> Result<uint256> {782		self.consume_store_reads(1)?;783		Ok(<TokensMinted<T>>::get(self.id)784			.checked_add(1)785			.ok_or("item id overflow")?786			.into())787	}788789	/// @notice Function to mint multiple tokens.790	/// @dev `tokenIds` should be an array of consecutive numbers and first number791	///  should be obtained with `nextTokenId` method792	/// @param to The new owner793	/// @param tokenIds IDs of the minted NFTs794	#[solidity(hide)]795	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]796	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {797		let caller = T::CrossAccountId::from_eth(caller);798		let to = T::CrossAccountId::from_eth(to);799		let mut expected_index = <TokensMinted<T>>::get(self.id)800			.checked_add(1)801			.ok_or("item id overflow")?;802		let budget = self803			.recorder804			.weight_calls_budget(<StructureWeight<T>>::find_parent());805806		let total_tokens = token_ids.len();807		for id in token_ids.into_iter() {808			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;809			if id != expected_index {810				return Err("item id should be next".into());811			}812			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;813		}814		let data = (0..total_tokens)815			.map(|_| CreateItemData::<T> {816				properties: BoundedVec::default(),817				owner: to.clone(),818			})819			.collect();820821		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)822			.map_err(dispatch_to_evm::<T>)?;823		Ok(true)824	}825826	/// @notice Function to mint multiple tokens with the given tokenUris.827	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive828	///  numbers and first number should be obtained with `nextTokenId` method829	/// @param to The new owner830	/// @param tokens array of pairs of token ID and token URI for minted tokens831	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]832	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]833	fn mint_bulk_with_token_uri(834		&mut self,835		caller: caller,836		to: address,837		tokens: Vec<(uint256, string)>,838	) -> Result<bool> {839		let key = key::url();840		let caller = T::CrossAccountId::from_eth(caller);841		let to = T::CrossAccountId::from_eth(to);842		let mut expected_index = <TokensMinted<T>>::get(self.id)843			.checked_add(1)844			.ok_or("item id overflow")?;845		let budget = self846			.recorder847			.weight_calls_budget(<StructureWeight<T>>::find_parent());848849		let mut data = Vec::with_capacity(tokens.len());850		for (id, token_uri) in tokens {851			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;852			if id != expected_index {853				return Err("item id should be next".into());854			}855			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;856857			let mut properties = CollectionPropertiesVec::default();858			properties859				.try_push(Property {860					key: key.clone(),861					value: token_uri862						.into_bytes()863						.try_into()864						.map_err(|_| "token uri is too long")?,865				})866				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;867868			data.push(CreateItemData::<T> {869				properties,870				owner: to.clone(),871			});872		}873874		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)875			.map_err(dispatch_to_evm::<T>)?;876		Ok(true)877	}878}879880#[solidity_interface(881	name = UniqueNFT,882	is(883		ERC721,884		ERC721Enumerable,885		ERC721UniqueExtensions,886		ERC721UniqueMintable,887		ERC721Burnable,888		ERC721Metadata(if(this.flags.erc721metadata)),889		Collection(via(common_mut returns CollectionHandle<T>)),890		TokenProperties,891	)892)]893impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}894895// Not a tests, but code generators896generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);897generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);898899impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>900where901	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,902{903	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");904905	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {906		call::<T, UniqueNFTCall<T>, _, _>(handle, self)907	}908}