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

difftreelog

source

pallets/nonfungible/src/erc.rs36.7 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::CommonWeights, weights::WeightInfo, AccountBalance, Config, CreateItemData,53	NonfungibleHandle, Pallet, SelfWeightOf, TokenData, TokenProperties, TokensMinted,54};5556/// Nft events.57#[derive(ToLog)]58pub enum ERC721TokenEvent {59	/// The token has been changed.60	TokenChanged {61		/// Token ID.62		#[indexed]63		token_id: U256,64	},65}6667/// Token minting parameters68#[derive(AbiCoder, Default, Debug)]69pub struct MintTokenData {70	/// Minted token owner71	pub owner: eth::CrossAddress,72	/// Minted token properties73	pub properties: Vec<eth::Property>,74}7576frontier_contract! {77	macro_rules! NonfungibleHandle_result {...}78	impl<T: Config> Contract for NonfungibleHandle<T> {...}79}8081fn nesting_budget<T: Config>(recorder: &SubstrateRecorder<T>) -> impl Budget + '_ {82	recorder.weight_calls_budget(<StructureWeight<T>>::find_parent())83}8485/// @title A contract that allows to set and delete token properties and change token property permissions.86#[solidity_interface(name = TokenProperties, events(ERC721TokenEvent), enum(derive(PreDispatch)), enum_attr(weight))]87impl<T: Config> NonfungibleHandle<T> {88	/// @notice Set permissions for token property.89	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.90	/// @param key Property key.91	/// @param isMutable Permission to mutate property.92	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.93	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.94	#[solidity(hide)]95	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]96	fn set_token_property_permission(97		&mut self,98		caller: Caller,99		key: String,100		is_mutable: bool,101		collection_admin: bool,102		token_owner: bool,103	) -> Result<()> {104		let caller = T::CrossAccountId::from_eth(caller);105		<Pallet<T>>::set_token_property_permissions(106			self,107			&caller,108			vec![PropertyKeyPermission {109				key: <Vec<u8>>::from(key)110					.try_into()111					.map_err(|_| "too long key")?,112				permission: PropertyPermission {113					mutable: is_mutable,114					collection_admin,115					token_owner,116				},117			}],118		)119		.map_err(dispatch_to_evm::<T>)120	}121122	/// @notice Set permissions for token property.123	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.124	/// @param permissions Permissions for keys.125	#[weight(<SelfWeightOf<T>>::set_token_property_permissions(permissions.len() as u32))]126	fn set_token_property_permissions(127		&mut self,128		caller: Caller,129		permissions: Vec<eth::TokenPropertyPermission>,130	) -> Result<()> {131		let caller = T::CrossAccountId::from_eth(caller);132		let perms = eth::TokenPropertyPermission::into_property_key_permissions(permissions)?;133134		<Pallet<T>>::set_token_property_permissions(self, &caller, perms)135			.map_err(dispatch_to_evm::<T>)136	}137138	/// @notice Get permissions for token properties.139	fn token_property_permissions(&self) -> Result<Vec<eth::TokenPropertyPermission>> {140		let perms = <Pallet<T>>::token_property_permission(self.id);141		Ok(perms142			.into_iter()143			.map(eth::TokenPropertyPermission::from)144			.collect())145	}146147	/// @notice Set token property value.148	/// @dev Throws error if `msg.sender` has no permission to edit the property.149	/// @param tokenId ID of the token.150	/// @param key Property key.151	/// @param value Property value.152	#[solidity(hide)]153	#[weight(<CommonWeights<T>>::set_token_properties(1))]154	fn set_property(155		&mut self,156		caller: Caller,157		token_id: U256,158		key: String,159		value: Bytes,160	) -> Result<()> {161		let caller = T::CrossAccountId::from_eth(caller);162		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;163		let key = <Vec<u8>>::from(key)164			.try_into()165			.map_err(|_| "key too long")?;166		let value = value.0.try_into().map_err(|_| "value too long")?;167168		<Pallet<T>>::set_token_property(169			self,170			&caller,171			TokenId(token_id),172			Property { key, value },173			&nesting_budget(&self.recorder),174		)175		.map_err(dispatch_to_evm::<T>)176	}177178	/// @notice Set token properties value.179	/// @dev Throws error if `msg.sender` has no permission to edit the property.180	/// @param tokenId ID of the token.181	/// @param properties settable properties182	#[weight(<CommonWeights<T>>::set_token_properties(properties.len() as u32))]183	fn set_properties(184		&mut self,185		caller: Caller,186		token_id: U256,187		properties: Vec<eth::Property>,188	) -> Result<()> {189		let caller = T::CrossAccountId::from_eth(caller);190		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;191192		let properties = properties193			.into_iter()194			.map(eth::Property::try_into)195			.collect::<Result<Vec<_>>>()?;196197		<Pallet<T>>::set_token_properties(198			self,199			&caller,200			TokenId(token_id),201			properties.into_iter(),202			&nesting_budget(&self.recorder),203		)204		.map_err(dispatch_to_evm::<T>)205	}206207	/// @notice Delete token property value.208	/// @dev Throws error if `msg.sender` has no permission to edit the property.209	/// @param tokenId ID of the token.210	/// @param key Property key.211	#[solidity(hide)]212	#[weight(<CommonWeights<T>>::delete_token_properties(1))]213	fn delete_property(&mut self, token_id: U256, caller: Caller, key: String) -> Result<()> {214		let caller = T::CrossAccountId::from_eth(caller);215		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;216		let key = <Vec<u8>>::from(key)217			.try_into()218			.map_err(|_| "key too long")?;219220		<Pallet<T>>::delete_token_property(221			self,222			&caller,223			TokenId(token_id),224			key,225			&nesting_budget(&self.recorder),226		)227		.map_err(dispatch_to_evm::<T>)228	}229230	/// @notice Delete token properties value.231	/// @dev Throws error if `msg.sender` has no permission to edit the property.232	/// @param tokenId ID of the token.233	/// @param keys Properties key.234	#[weight(<CommonWeights<T>>::delete_token_properties(keys.len() as u32))]235	fn delete_properties(236		&mut self,237		token_id: U256,238		caller: Caller,239		keys: Vec<String>,240	) -> Result<()> {241		let caller = T::CrossAccountId::from_eth(caller);242		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;243		let keys = keys244			.into_iter()245			.map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))246			.collect::<Result<Vec<_>>>()?;247248		<Pallet<T>>::delete_token_properties(249			self,250			&caller,251			TokenId(token_id),252			keys.into_iter(),253			&nesting_budget(&self.recorder),254		)255		.map_err(dispatch_to_evm::<T>)256	}257258	/// @notice Get token property value.259	/// @dev Throws error if key not found260	/// @param tokenId ID of the token.261	/// @param key Property key.262	/// @return Property value bytes263	fn property(&self, token_id: U256, key: String) -> Result<Bytes> {264		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;265		let key = <Vec<u8>>::from(key)266			.try_into()267			.map_err(|_| "key too long")?;268269		let props =270			<TokenProperties<T>>::get((self.id, token_id)).ok_or("token properties not found")?;271		let prop = props.get(&key).ok_or("key not found")?;272273		Ok(prop.to_vec().into())274	}275}276277#[derive(ToLog)]278pub enum ERC721Events {279	/// @dev This emits when ownership of any NFT changes by any mechanism.280	///  This event emits when NFTs are created (`from` == 0) and destroyed281	///  (`to` == 0). Exception: during contract creation, any number of NFTs282	///  may be created and assigned without emitting Transfer. At the time of283	///  any transfer, the approved address for that NFT (if any) is reset to none.284	Transfer {285		#[indexed]286		from: Address,287		#[indexed]288		to: Address,289		#[indexed]290		token_id: U256,291	},292	/// @dev This emits when the approved address for an NFT is changed or293	///  reaffirmed. The zero address indicates there is no approved address.294	///  When a Transfer event emits, this also indicates that the approved295	///  address for that NFT (if any) is reset to none.296	Approval {297		#[indexed]298		owner: Address,299		#[indexed]300		approved: Address,301		#[indexed]302		token_id: U256,303	},304	/// @dev This emits when an operator is enabled or disabled for an owner.305	///  The operator can manage all NFTs of the owner.306	#[allow(dead_code)]307	ApprovalForAll {308		#[indexed]309		owner: Address,310		#[indexed]311		operator: Address,312		approved: bool,313	},314}315316/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension317/// @dev See https://eips.ethereum.org/EIPS/eip-721318#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f, enum(derive(PreDispatch)), enum_attr(weight))]319impl<T: Config> NonfungibleHandle<T>320where321	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,322{323	/// @notice A descriptive name for a collection of NFTs in this contract324	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`325	#[solidity(hide, rename_selector = "name")]326	fn name_proxy(&self) -> String {327		self.name()328	}329330	/// @notice An abbreviated name for NFTs in this contract331	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`332	#[solidity(hide, rename_selector = "symbol")]333	fn symbol_proxy(&self) -> String {334		self.symbol()335	}336337	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.338	///339	/// @dev If the token has a `url` property and it is not empty, it is returned.340	///  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`.341	///  If the collection property `baseURI` is empty or absent, return "" (empty string)342	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix343	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).344	///345	/// @return token's const_metadata346	#[solidity(rename_selector = "tokenURI")]347	fn token_uri(&self, token_id: U256) -> Result<String> {348		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;349350		match get_token_property(self, token_id_u32, &key::url()).as_deref() {351			Err(_) | Ok("") => (),352			Ok(url) => {353				return Ok(url.into());354			}355		};356357		let base_uri =358			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())359				.map(BoundedVec::into_inner)360				.map(String::from_utf8)361				.transpose()362				.map_err(|e| {363					Error::Revert(alloc::format!(364						"can not convert value \"baseURI\" to string with error \"{e}\""365					))366				})?;367368		let base_uri = match base_uri.as_deref() {369			None | Some("") => {370				return Ok("".into());371			}372			Some(base_uri) => base_uri.into(),373		};374375		Ok(376			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {377				Err(_) | Ok("") => base_uri,378				Ok(suffix) => base_uri + suffix,379			},380		)381	}382}383384/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension385/// @dev See https://eips.ethereum.org/EIPS/eip-721386#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63, enum(derive(PreDispatch)), enum_attr(weight))]387impl<T: Config> NonfungibleHandle<T> {388	/// @notice Enumerate valid NFTs389	/// @param index A counter less than `totalSupply()`390	/// @return The token identifier for the `index`th NFT,391	///  (sort order not specified)392	fn token_by_index(&self, index: U256) -> U256 {393		index394	}395396	/// @dev Not implemented397	fn token_of_owner_by_index(&self, _owner: Address, _index: U256) -> Result<U256> {398		// TODO: Not implemetable399		Err("not implemented".into())400	}401402	/// @notice Count NFTs tracked by this contract403	/// @return A count of valid NFTs tracked by this contract, where each one of404	///  them has an assigned and queryable owner not equal to the zero address405	fn total_supply(&self) -> Result<U256> {406		self.consume_store_reads(1)?;407		Ok(<Pallet<T>>::total_supply(self).into())408	}409}410411/// @title ERC-721 Non-Fungible Token Standard412/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md413#[solidity_interface(name = ERC721, events(ERC721Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x80ac58cd)]414impl<T: Config> NonfungibleHandle<T> {415	/// @notice Count all NFTs assigned to an owner416	/// @dev NFTs assigned to the zero address are considered invalid, and this417	///  function throws for queries about the zero address.418	/// @param owner An address for whom to query the balance419	/// @return The number of NFTs owned by `owner`, possibly zero420	fn balance_of(&self, owner: Address) -> Result<U256> {421		self.consume_store_reads(1)?;422		let owner = T::CrossAccountId::from_eth(owner);423		let balance = <AccountBalance<T>>::get((self.id, owner));424		Ok(balance.into())425	}426	/// @notice Find the owner of an NFT427	/// @dev NFTs assigned to zero address are considered invalid, and queries428	///  about them do throw.429	/// @param tokenId The identifier for an NFT430	/// @return The address of the owner of the NFT431	fn owner_of(&self, token_id: U256) -> Result<Address> {432		self.consume_store_reads(1)?;433		let token: TokenId = token_id.try_into()?;434		Ok(*<TokenData<T>>::get((self.id, token))435			.ok_or("token not found")?436			.owner437			.as_eth())438	}439	/// @dev Not implemented440	#[solidity(rename_selector = "safeTransferFrom")]441	fn safe_transfer_from_with_data(442		&mut self,443		_from: Address,444		_to: Address,445		_token_id: U256,446		_data: Bytes,447	) -> Result<()> {448		// TODO: Not implemetable449		Err("not implemented".into())450	}451	/// @dev Not implemented452	fn safe_transfer_from(&mut self, _from: Address, _to: Address, _token_id: U256) -> Result<()> {453		// TODO: Not implemetable454		Err("not implemented".into())455	}456457	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE458	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE459	///  THEY MAY BE PERMANENTLY LOST460	/// @dev Throws unless `msg.sender` is the current owner or an authorized461	///  operator for this NFT. Throws if `from` is not the current owner. Throws462	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.463	/// @param from The current owner of the NFT464	/// @param to The new owner465	/// @param tokenId The NFT to transfer466	#[weight(<CommonWeights<T>>::transfer_from())]467	fn transfer_from(468		&mut self,469		caller: Caller,470		from: Address,471		to: Address,472		token_id: U256,473	) -> Result<()> {474		let caller = T::CrossAccountId::from_eth(caller);475		let from = T::CrossAccountId::from_eth(from);476		let to = T::CrossAccountId::from_eth(to);477		let token = token_id.try_into()?;478479		<Pallet<T>>::transfer_from(480			self,481			&caller,482			&from,483			&to,484			token,485			&nesting_budget(&self.recorder),486		)487		.map_err(|e| dispatch_to_evm::<T>(e.error))?;488		Ok(())489	}490491	/// @notice Set or reaffirm the approved address for an NFT492	/// @dev The zero address indicates there is no approved address.493	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized494	///  operator of the current owner.495	/// @param approved The new approved NFT controller496	/// @param tokenId The NFT to approve497	#[weight(<SelfWeightOf<T>>::approve())]498	fn approve(&mut self, caller: Caller, approved: Address, token_id: U256) -> Result<()> {499		let caller = T::CrossAccountId::from_eth(caller);500		let approved = T::CrossAccountId::from_eth(approved);501		let token = token_id.try_into()?;502503		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))504			.map_err(dispatch_to_evm::<T>)?;505		Ok(())506	}507508	/// @notice Sets or unsets the approval of a given operator.509	/// The `operator` is allowed to transfer all tokens of the `caller` on their behalf.510	/// @param operator Operator511	/// @param approved Should operator status be granted or revoked?512	#[weight(<SelfWeightOf<T>>::set_allowance_for_all())]513	fn set_approval_for_all(514		&mut self,515		caller: Caller,516		operator: Address,517		approved: bool,518	) -> Result<()> {519		let caller = T::CrossAccountId::from_eth(caller);520		let operator = T::CrossAccountId::from_eth(operator);521522		<Pallet<T>>::set_allowance_for_all(self, &caller, &operator, approved)523			.map_err(dispatch_to_evm::<T>)?;524		Ok(())525	}526527	/// @notice Get the approved address for a single NFT528	/// @dev Throws if `tokenId` is not a valid NFT529	/// @param tokenId The NFT to find the approved address for530	/// @return The approved address for this NFT, or the zero address if there is none531	fn get_approved(&self, token_id: U256) -> Result<Address> {532		let token_id = token_id.try_into()?;533		let operator = <Pallet<T>>::get_allowance(self, token_id).map_err(dispatch_to_evm::<T>)?;534		Ok(if let Some(operator) = operator {535			*operator.as_eth()536		} else {537			Address::zero()538		})539	}540541	/// @notice Tells whether the given `owner` approves the `operator`.542	#[weight(<SelfWeightOf<T>>::allowance_for_all())]543	fn is_approved_for_all(&self, owner: Address, operator: Address) -> Result<bool> {544		let owner = T::CrossAccountId::from_eth(owner);545		let operator = T::CrossAccountId::from_eth(operator);546547		Ok(<Pallet<T>>::allowance_for_all(self, &owner, &operator))548	}549}550551/// @title ERC721 Token that can be irreversibly burned (destroyed).552#[solidity_interface(name = ERC721Burnable, enum(derive(PreDispatch)), enum_attr(weight))]553impl<T: Config> NonfungibleHandle<T> {554	/// @notice Burns a specific ERC721 token.555	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized556	///  operator of the current owner.557	/// @param tokenId The NFT to approve558	#[weight(<SelfWeightOf<T>>::burn_item())]559	fn burn(&mut self, caller: Caller, token_id: U256) -> Result<()> {560		let caller = T::CrossAccountId::from_eth(caller);561		let token = token_id.try_into()?;562563		<Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;564		Ok(())565	}566}567568/// @title ERC721 minting logic.569#[solidity_interface(name = ERC721UniqueMintable, enum(derive(PreDispatch)), enum_attr(weight))]570impl<T: Config> NonfungibleHandle<T> {571	/// @notice Function to mint a token.572	/// @param to The new owner573	/// @return uint256 The id of the newly minted token574	#[weight(<SelfWeightOf<T>>::create_item())]575	fn mint(&mut self, caller: Caller, to: Address) -> Result<U256> {576		let token_id: U256 = <TokensMinted<T>>::get(self.id)577			.checked_add(1)578			.ok_or("item id overflow")?579			.into();580		self.mint_check_id(caller, to, token_id)?;581		Ok(token_id)582	}583584	/// @notice Function to mint a token.585	/// @dev `tokenId` should be obtained with `nextTokenId` method,586	///  unlike standard, you can't specify it manually587	/// @param to The new owner588	/// @param tokenId ID of the minted NFT589	#[solidity(hide, rename_selector = "mint")]590	#[weight(<SelfWeightOf<T>>::create_item())]591	fn mint_check_id(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<bool> {592		let caller = T::CrossAccountId::from_eth(caller);593		let to = T::CrossAccountId::from_eth(to);594		let token_id: u32 = token_id.try_into()?;595596		if <TokensMinted<T>>::get(self.id)597			.checked_add(1)598			.ok_or("item id overflow")?599			!= token_id600		{601			return Err("item id should be next".into());602		}603604		<Pallet<T>>::create_item(605			self,606			&caller,607			CreateItemData::<T> {608				properties: BoundedVec::default(),609				owner: to,610			},611			&nesting_budget(&self.recorder),612		)613		.map_err(dispatch_to_evm::<T>)?;614615		Ok(true)616	}617618	/// @notice Function to mint token with the given tokenUri.619	/// @param to The new owner620	/// @param tokenUri Token URI that would be stored in the NFT properties621	/// @return uint256 The id of the newly minted token622	#[solidity(rename_selector = "mintWithTokenURI")]623	#[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]624	fn mint_with_token_uri(625		&mut self,626		caller: Caller,627		to: Address,628		token_uri: String,629	) -> Result<U256> {630		let token_id: U256 = <TokensMinted<T>>::get(self.id)631			.checked_add(1)632			.ok_or("item id overflow")?633			.into();634		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;635		Ok(token_id)636	}637638	/// @notice Function to mint token with the given tokenUri.639	/// @dev `tokenId` should be obtained with `nextTokenId` method,640	///  unlike standard, you can't specify it manually641	/// @param to The new owner642	/// @param tokenId ID of the minted NFT643	/// @param tokenUri Token URI that would be stored in the NFT properties644	#[solidity(hide, rename_selector = "mintWithTokenURI")]645	#[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]646	fn mint_with_token_uri_check_id(647		&mut self,648		caller: Caller,649		to: Address,650		token_id: U256,651		token_uri: String,652	) -> Result<bool> {653		let key = key::url();654		let permission = get_token_permission::<T>(self.id, &key)?;655		if !permission.collection_admin {656			return Err("operation is not allowed".into());657		}658659		let caller = T::CrossAccountId::from_eth(caller);660		let to = T::CrossAccountId::from_eth(to);661		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;662663		if <TokensMinted<T>>::get(self.id)664			.checked_add(1)665			.ok_or("item id overflow")?666			!= token_id667		{668			return Err("item id should be next".into());669		}670671		let mut properties = CollectionPropertiesVec::default();672		properties673			.try_push(Property {674				key,675				value: token_uri676					.into_bytes()677					.try_into()678					.map_err(|_| "token uri is too long")?,679			})680			.map_err(|e| Error::Revert(alloc::format!("can't add property: {e:?}")))?;681682		<Pallet<T>>::create_item(683			self,684			&caller,685			CreateItemData::<T> {686				properties,687				owner: to,688			},689			&nesting_budget(&self.recorder),690		)691		.map_err(dispatch_to_evm::<T>)?;692		Ok(true)693	}694}695696fn get_token_property<T: Config>(697	collection: &CollectionHandle<T>,698	token_id: u32,699	key: &up_data_structs::PropertyKey,700) -> Result<String> {701	collection.consume_store_reads(1)?;702	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))703		.map_err(|_| Error::Revert("token properties not found".into()))?;704	if let Some(property) = properties.get(key) {705		return Ok(String::from_utf8_lossy(property).into());706	}707708	Err("property tokenURI not found".into())709}710711fn get_token_permission<T: Config>(712	collection_id: CollectionId,713	key: &PropertyKey,714) -> Result<PropertyPermission> {715	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)716		.map_err(|_| Error::Revert("no permissions for collection".into()))?;717	let a = token_property_permissions718		.get(key)719		.map(Clone::clone)720		.ok_or_else(|| {721			let key = String::from_utf8(key.clone().into_inner()).unwrap_or_default();722			Error::Revert(alloc::format!("no permission for key {key}"))723		})?;724	Ok(a)725}726727/// @title Unique extensions for ERC721.728#[solidity_interface(name = ERC721UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))]729impl<T: Config> NonfungibleHandle<T>730where731	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,732{733	/// @notice A descriptive name for a collection of NFTs in this contract734	fn name(&self) -> String {735		decode_utf16(self.name.iter().copied())736			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))737			.collect::<String>()738	}739740	/// @notice An abbreviated name for NFTs in this contract741	fn symbol(&self) -> String {742		String::from_utf8_lossy(&self.token_prefix).into()743	}744745	/// @notice A description for the collection.746	fn description(&self) -> String {747		decode_utf16(self.description.iter().copied())748			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))749			.collect::<String>()750	}751752	/// Returns the owner (in cross format) of the token.753	///754	/// @param tokenId Id for the token.755	#[solidity(hide)]756	fn cross_owner_of(&self, token_id: U256) -> Result<eth::CrossAddress> {757		Self::owner_of_cross(self, token_id)758	}759760	/// Returns the owner (in cross format) of the token.761	///762	/// @param tokenId Id for the token.763	fn owner_of_cross(&self, token_id: U256) -> Result<eth::CrossAddress> {764		Self::token_owner(self, token_id.try_into()?)765			.map(|o| eth::CrossAddress::from_sub_cross_account::<T>(&o))766			.map_err(|_| Error::Revert("token not found".into()))767	}768769	/// @notice Count all NFTs assigned to an owner770	/// @param owner An cross address for whom to query the balance771	/// @return The number of NFTs owned by `owner`, possibly zero772	fn balance_of_cross(&self, owner: eth::CrossAddress) -> Result<U256> {773		self.consume_store_reads(1)?;774		let balance = <AccountBalance<T>>::get((self.id, owner.into_sub_cross_account::<T>()?));775		Ok(balance.into())776	}777778	/// Returns the token properties.779	///780	/// @param tokenId Id for the token.781	/// @param keys Properties keys. Empty keys for all propertyes.782	/// @return Vector of properties key/value pairs.783	fn properties(&self, token_id: U256, keys: Vec<String>) -> Result<Vec<eth::Property>> {784		let keys = keys785			.into_iter()786			.map(|key| {787				<Vec<u8>>::from(key)788					.try_into()789					.map_err(|_| Error::Revert("key too large".into()))790			})791			.collect::<Result<Vec<_>>>()?;792793		<Self as CommonCollectionOperations<T>>::token_properties(794			self,795			token_id.try_into()?,796			if keys.is_empty() { None } else { Some(keys) },797		)798		.into_iter()799		.map(eth::Property::try_from)800		.collect::<Result<Vec<_>>>()801	}802803	/// @notice Set or reaffirm the approved address for an NFT804	/// @dev The zero address indicates there is no approved address.805	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized806	///  operator of the current owner.807	/// @param approved The new substrate address approved NFT controller808	/// @param tokenId The NFT to approve809	#[weight(<SelfWeightOf<T>>::approve())]810	fn approve_cross(811		&mut self,812		caller: Caller,813		approved: eth::CrossAddress,814		token_id: U256,815	) -> Result<()> {816		let caller = T::CrossAccountId::from_eth(caller);817		let approved = approved.into_sub_cross_account::<T>()?;818		let token = token_id.try_into()?;819820		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))821			.map_err(dispatch_to_evm::<T>)?;822		Ok(())823	}824825	/// @notice Transfer ownership of an NFT826	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`827	///  is the zero address. Throws if `tokenId` is not a valid NFT.828	/// @param to The new owner829	/// @param tokenId The NFT to transfer830	#[weight(<CommonWeights<T>>::transfer())]831	fn transfer(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<()> {832		let caller = T::CrossAccountId::from_eth(caller);833		let to = T::CrossAccountId::from_eth(to);834		let token = token_id.try_into()?;835836		<Pallet<T>>::transfer(self, &caller, &to, token, &nesting_budget(&self.recorder))837			.map_err(|e| dispatch_to_evm::<T>(e.error))?;838		Ok(())839	}840841	/// @notice Transfer ownership of an NFT842	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`843	///  is the zero address. Throws if `tokenId` is not a valid NFT.844	/// @param to The new owner845	/// @param tokenId The NFT to transfer846	#[weight(<CommonWeights<T>>::transfer())]847	fn transfer_cross(848		&mut self,849		caller: Caller,850		to: eth::CrossAddress,851		token_id: U256,852	) -> Result<()> {853		let caller = T::CrossAccountId::from_eth(caller);854		let to = to.into_sub_cross_account::<T>()?;855		let token = token_id.try_into()?;856857		<Pallet<T>>::transfer(self, &caller, &to, token, &nesting_budget(&self.recorder))858			.map_err(|e| dispatch_to_evm::<T>(e.error))?;859		Ok(())860	}861862	/// @notice Transfer ownership of an NFT from cross account address to cross account address863	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`864	///  is the zero address. Throws if `tokenId` is not a valid NFT.865	/// @param from Cross acccount address of current owner866	/// @param to Cross acccount address of new owner867	/// @param tokenId The NFT to transfer868	#[weight(<CommonWeights<T>>::transfer_from())]869	fn transfer_from_cross(870		&mut self,871		caller: Caller,872		from: eth::CrossAddress,873		to: eth::CrossAddress,874		token_id: U256,875	) -> Result<()> {876		let caller = T::CrossAccountId::from_eth(caller);877		let from = from.into_sub_cross_account::<T>()?;878		let to = to.into_sub_cross_account::<T>()?;879		let token_id = token_id.try_into()?;880881		Pallet::<T>::transfer_from(882			self,883			&caller,884			&from,885			&to,886			token_id,887			&nesting_budget(&self.recorder),888		)889		.map_err(|e| dispatch_to_evm::<T>(e.error))?;890		Ok(())891	}892893	/// @notice Burns a specific ERC721 token.894	/// @dev Throws unless `msg.sender` is the current owner or an authorized895	///  operator for this NFT. Throws if `from` is not the current owner. Throws896	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.897	/// @param from The current owner of the NFT898	/// @param tokenId The NFT to transfer899	#[solidity(hide)]900	#[weight(<SelfWeightOf<T>>::burn_from())]901	fn burn_from(&mut self, caller: Caller, from: Address, token_id: U256) -> Result<()> {902		let caller = T::CrossAccountId::from_eth(caller);903		let from = T::CrossAccountId::from_eth(from);904		let token = token_id.try_into()?;905906		<Pallet<T>>::burn_from(self, &caller, &from, token, &nesting_budget(&self.recorder))907			.map_err(dispatch_to_evm::<T>)?;908		Ok(())909	}910911	/// @notice Burns a specific ERC721 token.912	/// @dev Throws unless `msg.sender` is the current owner or an authorized913	///  operator for this NFT. Throws if `from` is not the current owner. Throws914	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.915	/// @param from The current owner of the NFT916	/// @param tokenId The NFT to transfer917	#[weight(<SelfWeightOf<T>>::burn_from())]918	fn burn_from_cross(919		&mut self,920		caller: Caller,921		from: eth::CrossAddress,922		token_id: U256,923	) -> Result<()> {924		let caller = T::CrossAccountId::from_eth(caller);925		let from = from.into_sub_cross_account::<T>()?;926		let token = token_id.try_into()?;927928		<Pallet<T>>::burn_from(self, &caller, &from, token, &nesting_budget(&self.recorder))929			.map_err(dispatch_to_evm::<T>)?;930		Ok(())931	}932933	/// @notice Returns next free NFT ID.934	fn next_token_id(&self) -> Result<U256> {935		self.consume_store_reads(1)?;936		Ok(<Pallet<T>>::next_token_id(self)937			.map_err(dispatch_to_evm::<T>)?938			.into())939	}940941	/// @notice Function to mint multiple tokens.942	/// @dev `tokenIds` should be an array of consecutive numbers and first number943	///  should be obtained with `nextTokenId` method944	/// @param to The new owner945	/// @param tokenIds IDs of the minted NFTs946	#[solidity(hide)]947	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]948	fn mint_bulk(&mut self, caller: Caller, to: Address, token_ids: Vec<U256>) -> Result<bool> {949		let caller = T::CrossAccountId::from_eth(caller);950		let to = T::CrossAccountId::from_eth(to);951		let mut expected_index = <TokensMinted<T>>::get(self.id)952			.checked_add(1)953			.ok_or("item id overflow")?;954955		let total_tokens = token_ids.len();956		for id in token_ids.into_iter() {957			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;958			if id != expected_index {959				return Err("item id should be next".into());960			}961			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;962		}963		let data = (0..total_tokens)964			.map(|_| CreateItemData::<T> {965				properties: BoundedVec::default(),966				owner: to.clone(),967			})968			.collect();969970		<Pallet<T>>::create_multiple_items(self, &caller, data, &nesting_budget(&self.recorder))971			.map_err(dispatch_to_evm::<T>)?;972		Ok(true)973	}974975	/// @notice Function to mint a token.976	/// @param data Array of pairs of token owner and token's properties for minted token977	#[weight(<SelfWeightOf<T>>::create_multiple_items(data.len() as u32) + <SelfWeightOf<T>>::set_token_properties(data.len() as u32))]978	fn mint_bulk_cross(&mut self, caller: Caller, data: Vec<MintTokenData>) -> Result<bool> {979		let caller = T::CrossAccountId::from_eth(caller);980981		let mut create_nft_data = Vec::with_capacity(data.len());982		for MintTokenData { owner, properties } in data {983			let owner = owner.into_sub_cross_account::<T>()?;984			create_nft_data.push(CreateItemData::<T> {985				properties: properties986					.into_iter()987					.map(|property| property.try_into())988					.collect::<Result<Vec<_>>>()?989					.try_into()990					.map_err(|_| "too many properties")?,991				owner,992			});993		}994995		<Pallet<T>>::create_multiple_items(996			self,997			&caller,998			create_nft_data,999			&nesting_budget(&self.recorder),1000		)1001		.map_err(dispatch_to_evm::<T>)?;1002		Ok(true)1003	}10041005	/// @notice Function to mint multiple tokens with the given tokenUris.1006	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive1007	///  numbers and first number should be obtained with `nextTokenId` method1008	/// @param to The new owner1009	/// @param tokens array of pairs of token ID and token URI for minted tokens1010	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]1011	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32) + <SelfWeightOf<T>>::set_token_properties(tokens.len() as u32))]1012	fn mint_bulk_with_token_uri(1013		&mut self,1014		caller: Caller,1015		to: Address,1016		tokens: Vec<TokenUri>,1017	) -> Result<bool> {1018		let key = key::url();1019		let caller = T::CrossAccountId::from_eth(caller);1020		let to = T::CrossAccountId::from_eth(to);1021		let mut expected_index = <TokensMinted<T>>::get(self.id)1022			.checked_add(1)1023			.ok_or("item id overflow")?;10241025		let mut data = Vec::with_capacity(tokens.len());1026		for TokenUri { id, uri } in tokens {1027			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;1028			if id != expected_index {1029				return Err("item id should be next".into());1030			}1031			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;10321033			let mut properties = CollectionPropertiesVec::default();1034			properties1035				.try_push(Property {1036					key: key.clone(),1037					value: uri1038						.into_bytes()1039						.try_into()1040						.map_err(|_| "token uri is too long")?,1041				})1042				.map_err(|e| Error::Revert(alloc::format!("can't add property: {e:?}")))?;10431044			data.push(CreateItemData::<T> {1045				properties,1046				owner: to.clone(),1047			});1048		}10491050		<Pallet<T>>::create_multiple_items(self, &caller, data, &nesting_budget(&self.recorder))1051			.map_err(dispatch_to_evm::<T>)?;1052		Ok(true)1053	}10541055	/// @notice Function to mint a token.1056	/// @param to The new owner crossAccountId1057	/// @param properties Properties of minted token1058	/// @return uint256 The id of the newly minted token1059	#[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]1060	fn mint_cross(1061		&mut self,1062		caller: Caller,1063		to: eth::CrossAddress,1064		properties: Vec<eth::Property>,1065	) -> Result<U256> {1066		let token_id = <TokensMinted<T>>::get(self.id)1067			.checked_add(1)1068			.ok_or("item id overflow")?;10691070		let to = to.into_sub_cross_account::<T>()?;10711072		let properties = properties1073			.into_iter()1074			.map(eth::Property::try_into)1075			.collect::<Result<Vec<_>>>()?1076			.try_into()1077			.map_err(|_| Error::Revert("too many properties".to_string()))?;10781079		let caller = T::CrossAccountId::from_eth(caller);10801081		<Pallet<T>>::create_item(1082			self,1083			&caller,1084			CreateItemData::<T> {1085				properties,1086				owner: to,1087			},1088			&nesting_budget(&self.recorder),1089		)1090		.map_err(dispatch_to_evm::<T>)?;10911092		Ok(token_id.into())1093	}10941095	/// @notice Returns collection helper contract address1096	fn collection_helper_address(&self) -> Address {1097		T::ContractAddress::get()1098	}1099}11001101#[solidity_interface(1102	name = UniqueNFT,1103	is(1104		ERC721,1105		ERC721Enumerable,1106		ERC721UniqueExtensions,1107		ERC721UniqueMintable,1108		ERC721Burnable,1109		ERC721Metadata(if(this.flags.erc721metadata)),1110		Collection(via(common_mut returns CollectionHandle<T>)),1111		TokenProperties,1112	),1113	enum(derive(PreDispatch)),1114)]1115impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}11161117// Not a tests, but code generators1118generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);1119generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);11201121impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>1122where1123	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1124{1125	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");11261127	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {1128		call::<T, UniqueNFTCall<T>, _, _>(handle, self)1129	}1130}