git.delta.rocks / unique-network / refs/commits / 7bd00bbda0e0

difftreelog

source

pallets/refungible/src/erc.rs29.8 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Refungible Pallet EVM API for tokens18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.2122extern crate alloc;2324use core::{25	char::{REPLACEMENT_CHARACTER, decode_utf16},26	convert::TryInto,27};28use evm_coder::{29	abi::AbiType, ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*,30	weight,31};32use frame_support::{BoundedBTreeMap, BoundedVec};33use pallet_common::{34	CollectionHandle, CollectionPropertyPermissions,35	erc::{CommonEvmHandler, CollectionCall, static_property::key},36};37use pallet_evm::{account::CrossAccountId, PrecompileHandle};38use pallet_evm_coder_substrate::{call, dispatch_to_evm};39use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};40use sp_core::H160;41use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};42use up_data_structs::{43	CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,44	PropertyKeyPermission, PropertyPermission, TokenId,45};4647use crate::{48	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,49	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,50};5152pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5354/// @title A contract that allows to set and delete token properties and change token property permissions.55#[solidity_interface(name = TokenProperties)]56impl<T: Config> RefungibleHandle<T> {57	/// @notice Set permissions for token property.58	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.59	/// @param key Property key.60	/// @param isMutable Permission to mutate property.61	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.62	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.63	fn set_token_property_permission(64		&mut self,65		caller: caller,66		key: string,67		is_mutable: bool,68		collection_admin: bool,69		token_owner: bool,70	) -> Result<()> {71		let caller = T::CrossAccountId::from_eth(caller);72		<Pallet<T>>::set_token_property_permissions(73			self,74			&caller,75			vec![PropertyKeyPermission {76				key: <Vec<u8>>::from(key)77					.try_into()78					.map_err(|_| "too long key")?,79				permission: PropertyPermission {80					mutable: is_mutable,81					collection_admin,82					token_owner,83				},84			}],85		)86		.map_err(dispatch_to_evm::<T>)87	}8889	/// @notice Set token property value.90	/// @dev Throws error if `msg.sender` has no permission to edit the property.91	/// @param tokenId ID of the token.92	/// @param key Property key.93	/// @param value Property value.94	fn set_property(95		&mut self,96		caller: caller,97		token_id: uint256,98		key: string,99		value: bytes,100	) -> Result<()> {101		let caller = T::CrossAccountId::from_eth(caller);102		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;103		let key = <Vec<u8>>::from(key)104			.try_into()105			.map_err(|_| "key too long")?;106		let value = value.0.try_into().map_err(|_| "value too long")?;107108		let nesting_budget = self109			.recorder110			.weight_calls_budget(<StructureWeight<T>>::find_parent());111112		<Pallet<T>>::set_token_property(113			self,114			&caller,115			TokenId(token_id),116			Property { key, value },117			&nesting_budget,118		)119		.map_err(dispatch_to_evm::<T>)120	}121122	/// @notice Set token properties value.123	/// @dev Throws error if `msg.sender` has no permission to edit the property.124	/// @param tokenId ID of the token.125	/// @param properties settable properties126	fn set_properties(127		&mut self,128		caller: caller,129		token_id: uint256,130		properties: Vec<(string, bytes)>,131	) -> Result<()> {132		let caller = T::CrossAccountId::from_eth(caller);133		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;134135		let nesting_budget = self136			.recorder137			.weight_calls_budget(<StructureWeight<T>>::find_parent());138139		let properties = properties140			.into_iter()141			.map(|(key, value)| {142				let key = <Vec<u8>>::from(key)143					.try_into()144					.map_err(|_| "key too large")?;145146				let value = value.0.try_into().map_err(|_| "value too large")?;147148				Ok(Property { key, value })149			})150			.collect::<Result<Vec<_>>>()?;151152		<Pallet<T>>::set_token_properties(153			self,154			&caller,155			TokenId(token_id),156			properties.into_iter(),157			<Pallet<T>>::token_exists(&self, TokenId(token_id)),158			&nesting_budget,159		)160		.map_err(dispatch_to_evm::<T>)161	}162163	/// @notice Delete token property value.164	/// @dev Throws error if `msg.sender` has no permission to edit the property.165	/// @param tokenId ID of the token.166	/// @param key Property key.167	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {168		let caller = T::CrossAccountId::from_eth(caller);169		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;170		let key = <Vec<u8>>::from(key)171			.try_into()172			.map_err(|_| "key too long")?;173174		let nesting_budget = self175			.recorder176			.weight_calls_budget(<StructureWeight<T>>::find_parent());177178		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)179			.map_err(dispatch_to_evm::<T>)180	}181182	/// @notice Get token property value.183	/// @dev Throws error if key not found184	/// @param tokenId ID of the token.185	/// @param key Property key.186	/// @return Property value bytes187	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {188		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;189		let key = <Vec<u8>>::from(key)190			.try_into()191			.map_err(|_| "key too long")?;192193		let props = <TokenProperties<T>>::get((self.id, token_id));194		let prop = props.get(&key).ok_or("key not found")?;195196		Ok(prop.to_vec().into())197	}198}199200#[derive(ToLog)]201pub enum ERC721Events {202	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed203	///  (`to` == 0). Exception: during contract creation, any number of RFTs204	///  may be created and assigned without emitting Transfer.205	Transfer {206		#[indexed]207		from: address,208		#[indexed]209		to: address,210		#[indexed]211		token_id: uint256,212	},213	/// @dev Not supported214	Approval {215		#[indexed]216		owner: address,217		#[indexed]218		approved: address,219		#[indexed]220		token_id: uint256,221	},222	/// @dev Not supported223	#[allow(dead_code)]224	ApprovalForAll {225		#[indexed]226		owner: address,227		#[indexed]228		operator: address,229		approved: bool,230	},231}232233#[derive(ToLog)]234pub enum ERC721UniqueMintableEvents {235	/// @dev Not supported236	#[allow(dead_code)]237	MintingFinished {},238}239240#[solidity_interface(name = ERC721Metadata)]241impl<T: Config> RefungibleHandle<T>242where243	T::AccountId: From<[u8; 32]>,244{245	/// @notice A descriptive name for a collection of NFTs in this contract246	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`247	#[solidity(hide, rename_selector = "name")]248	fn name_proxy(&self) -> Result<string> {249		self.name()250	}251252	/// @notice An abbreviated name for NFTs in this contract253	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`254	#[solidity(hide, rename_selector = "symbol")]255	fn symbol_proxy(&self) -> Result<string> {256		self.symbol()257	}258259	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.260	///261	/// @dev If the token has a `url` property and it is not empty, it is returned.262	///  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`.263	///  If the collection property `baseURI` is empty or absent, return "" (empty string)264	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix265	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).266	///267	/// @return token's const_metadata268	#[solidity(rename_selector = "tokenURI")]269	fn token_uri(&self, token_id: uint256) -> Result<string> {270		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;271272		match get_token_property(self, token_id_u32, &key::url()).as_deref() {273			Err(_) | Ok("") => (),274			Ok(url) => {275				return Ok(url.into());276			}277		};278279		let base_uri =280			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())281				.map(BoundedVec::into_inner)282				.map(string::from_utf8)283				.transpose()284				.map_err(|e| {285					Error::Revert(alloc::format!(286						"Can not convert value \"baseURI\" to string with error \"{}\"",287						e288					))289				})?;290291		let base_uri = match base_uri.as_deref() {292			None | Some("") => {293				return Ok("".into());294			}295			Some(base_uri) => base_uri.into(),296		};297298		Ok(299			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {300				Err(_) | Ok("") => base_uri,301				Ok(suffix) => base_uri + suffix,302			},303		)304	}305}306307/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension308/// @dev See https://eips.ethereum.org/EIPS/eip-721309#[solidity_interface(name = ERC721Enumerable)]310impl<T: Config> RefungibleHandle<T> {311	/// @notice Enumerate valid RFTs312	/// @param index A counter less than `totalSupply()`313	/// @return The token identifier for the `index`th NFT,314	///  (sort order not specified)315	fn token_by_index(&self, index: uint256) -> Result<uint256> {316		Ok(index)317	}318319	/// Not implemented320	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {321		// TODO: Not implemetable322		Err("not implemented".into())323	}324325	/// @notice Count RFTs tracked by this contract326	/// @return A count of valid RFTs tracked by this contract, where each one of327	///  them has an assigned and queryable owner not equal to the zero address328	fn total_supply(&self) -> Result<uint256> {329		self.consume_store_reads(1)?;330		Ok(<Pallet<T>>::total_supply(self).into())331	}332}333334/// @title ERC-721 Non-Fungible Token Standard335/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md336#[solidity_interface(name = ERC721, events(ERC721Events))]337impl<T: Config> RefungibleHandle<T> {338	/// @notice Count all RFTs assigned to an owner339	/// @dev RFTs assigned to the zero address are considered invalid, and this340	///  function throws for queries about the zero address.341	/// @param owner An address for whom to query the balance342	/// @return The number of RFTs owned by `owner`, possibly zero343	fn balance_of(&self, owner: address) -> Result<uint256> {344		self.consume_store_reads(1)?;345		let owner = T::CrossAccountId::from_eth(owner);346		let balance = <AccountBalance<T>>::get((self.id, owner));347		Ok(balance.into())348	}349350	/// @notice Find the owner of an RFT351	/// @dev RFTs assigned to zero address are considered invalid, and queries352	///  about them do throw.353	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for354	///  the tokens that are partially owned.355	/// @param tokenId The identifier for an RFT356	/// @return The address of the owner of the RFT357	fn owner_of(&self, token_id: uint256) -> Result<address> {358		self.consume_store_reads(2)?;359		let token = token_id.try_into()?;360		let owner = <Pallet<T>>::token_owner(self.id, token);361		Ok(owner362			.map(|address| *address.as_eth())363			.unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))364	}365366	/// @dev Not implemented367	fn safe_transfer_from_with_data(368		&mut self,369		_from: address,370		_to: address,371		_token_id: uint256,372		_data: bytes,373	) -> Result<void> {374		// TODO: Not implemetable375		Err("not implemented".into())376	}377378	/// @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 RFT -- 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 RFT. Throws if `from` is not the current owner. Throws394	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.395	///  Throws if RFT pieces have multiple owners.396	/// @param from The current owner of the NFT397	/// @param to The new owner398	/// @param tokenId The NFT to transfer399	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]400	fn transfer_from(401		&mut self,402		caller: caller,403		from: address,404		to: address,405		token_id: uint256,406	) -> Result<void> {407		let caller = T::CrossAccountId::from_eth(caller);408		let from = T::CrossAccountId::from_eth(from);409		let to = T::CrossAccountId::from_eth(to);410		let token = token_id.try_into()?;411		let budget = self412			.recorder413			.weight_calls_budget(<StructureWeight<T>>::find_parent());414415		let balance = balance(&self, token, &from)?;416		ensure_single_owner(&self, token, balance)?;417418		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)419			.map_err(dispatch_to_evm::<T>)?;420421		Ok(())422	}423424	/// @dev Not implemented425	fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {426		Err("not implemented".into())427	}428429	/// @dev Not implemented430	fn set_approval_for_all(431		&mut self,432		_caller: caller,433		_operator: address,434		_approved: bool,435	) -> Result<void> {436		// TODO: Not implemetable437		Err("not implemented".into())438	}439440	/// @dev Not implemented441	fn get_approved(&self, _token_id: uint256) -> Result<address> {442		// TODO: Not implemetable443		Err("not implemented".into())444	}445446	/// @dev Not implemented447	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {448		// TODO: Not implemetable449		Err("not implemented".into())450	}451}452453/// Returns amount of pieces of `token` that `owner` have454pub fn balance<T: Config>(455	collection: &RefungibleHandle<T>,456	token: TokenId,457	owner: &T::CrossAccountId,458) -> Result<u128> {459	collection.consume_store_reads(1)?;460	let balance = <Balance<T>>::get((collection.id, token, &owner));461	Ok(balance)462}463464/// Throws if `owner_balance` is lower than total amount of `token` pieces465pub fn ensure_single_owner<T: Config>(466	collection: &RefungibleHandle<T>,467	token: TokenId,468	owner_balance: u128,469) -> Result<()> {470	collection.consume_store_reads(1)?;471	let total_supply = <TotalSupply<T>>::get((collection.id, token));472	if total_supply != owner_balance {473		return Err("token has multiple owners".into());474	}475	Ok(())476}477478/// @title ERC721 Token that can be irreversibly burned (destroyed).479#[solidity_interface(name = ERC721Burnable)]480impl<T: Config> RefungibleHandle<T> {481	/// @notice Burns a specific ERC721 token.482	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized483	///  operator of the current owner.484	/// @param tokenId The RFT to approve485	#[weight(<SelfWeightOf<T>>::burn_item_fully())]486	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {487		let caller = T::CrossAccountId::from_eth(caller);488		let token = token_id.try_into()?;489490		let balance = balance(&self, token, &caller)?;491		ensure_single_owner(&self, token, balance)?;492493		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;494		Ok(())495	}496}497498/// @title ERC721 minting logic.499#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]500impl<T: Config> RefungibleHandle<T> {501	fn minting_finished(&self) -> Result<bool> {502		Ok(false)503	}504505	/// @notice Function to mint token.506	/// @param to The new owner507	/// @return uint256 The id of the newly minted token508	#[weight(<SelfWeightOf<T>>::create_item())]509	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {510		let token_id: uint256 = <TokensMinted<T>>::get(self.id)511			.checked_add(1)512			.ok_or("item id overflow")?513			.into();514		self.mint_check_id(caller, to, token_id)?;515		Ok(token_id)516	}517518	/// @notice Function to mint token.519	/// @dev `tokenId` should be obtained with `nextTokenId` method,520	///  unlike standard, you can't specify it manually521	/// @param to The new owner522	/// @param tokenId ID of the minted RFT523	#[solidity(hide, rename_selector = "mint")]524	#[weight(<SelfWeightOf<T>>::create_item())]525	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {526		let caller = T::CrossAccountId::from_eth(caller);527		let to = T::CrossAccountId::from_eth(to);528		let token_id: u32 = token_id.try_into()?;529		let budget = self530			.recorder531			.weight_calls_budget(<StructureWeight<T>>::find_parent());532533		if <TokensMinted<T>>::get(self.id)534			.checked_add(1)535			.ok_or("item id overflow")?536			!= token_id537		{538			return Err("item id should be next".into());539		}540541		let users = [(to.clone(), 1)]542			.into_iter()543			.collect::<BTreeMap<_, _>>()544			.try_into()545			.unwrap();546		<Pallet<T>>::create_item(547			self,548			&caller,549			CreateItemData::<T::CrossAccountId> {550				users,551				properties: CollectionPropertiesVec::default(),552			},553			&budget,554		)555		.map_err(dispatch_to_evm::<T>)?;556557		Ok(true)558	}559560	/// @notice Function to mint token with the given tokenUri.561	/// @param to The new owner562	/// @param tokenUri Token URI that would be stored in the NFT properties563	/// @return uint256 The id of the newly minted token564	#[solidity(rename_selector = "mintWithTokenURI")]565	#[weight(<SelfWeightOf<T>>::create_item())]566	fn mint_with_token_uri(567		&mut self,568		caller: caller,569		to: address,570		token_uri: string,571	) -> Result<uint256> {572		let token_id: uint256 = <TokensMinted<T>>::get(self.id)573			.checked_add(1)574			.ok_or("item id overflow")?575			.into();576		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;577		Ok(token_id)578	}579580	/// @notice Function to mint token with the given tokenUri.581	/// @dev `tokenId` should be obtained with `nextTokenId` method,582	///  unlike standard, you can't specify it manually583	/// @param to The new owner584	/// @param tokenId ID of the minted RFT585	/// @param tokenUri Token URI that would be stored in the RFT properties586	#[solidity(hide, rename_selector = "mintWithTokenURI")]587	#[weight(<SelfWeightOf<T>>::create_item())]588	fn mint_with_token_uri_check_id(589		&mut self,590		caller: caller,591		to: address,592		token_id: uint256,593		token_uri: string,594	) -> Result<bool> {595		let key = key::url();596		let permission = get_token_permission::<T>(self.id, &key)?;597		if !permission.collection_admin {598			return Err("Operation is not allowed".into());599		}600601		let caller = T::CrossAccountId::from_eth(caller);602		let to = T::CrossAccountId::from_eth(to);603		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;604		let budget = self605			.recorder606			.weight_calls_budget(<StructureWeight<T>>::find_parent());607608		if <TokensMinted<T>>::get(self.id)609			.checked_add(1)610			.ok_or("item id overflow")?611			!= token_id612		{613			return Err("item id should be next".into());614		}615616		let mut properties = CollectionPropertiesVec::default();617		properties618			.try_push(Property {619				key,620				value: token_uri621					.into_bytes()622					.try_into()623					.map_err(|_| "token uri is too long")?,624			})625			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;626627		let users = [(to.clone(), 1)]628			.into_iter()629			.collect::<BTreeMap<_, _>>()630			.try_into()631			.unwrap();632		<Pallet<T>>::create_item(633			self,634			&caller,635			CreateItemData::<T::CrossAccountId> { users, properties },636			&budget,637		)638		.map_err(dispatch_to_evm::<T>)?;639		Ok(true)640	}641642	/// @dev Not implemented643	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {644		Err("not implementable".into())645	}646}647648fn get_token_property<T: Config>(649	collection: &CollectionHandle<T>,650	token_id: u32,651	key: &up_data_structs::PropertyKey,652) -> Result<string> {653	collection.consume_store_reads(1)?;654	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))655		.map_err(|_| Error::Revert("Token properties not found".into()))?;656	if let Some(property) = properties.get(key) {657		return Ok(string::from_utf8_lossy(property).into());658	}659660	Err("Property tokenURI not found".into())661}662663fn get_token_permission<T: Config>(664	collection_id: CollectionId,665	key: &PropertyKey,666) -> Result<PropertyPermission> {667	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)668		.map_err(|_| Error::Revert("No permissions for collection".into()))?;669	let a = token_property_permissions670		.get(key)671		.map(Clone::clone)672		.ok_or_else(|| {673			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();674			Error::Revert(alloc::format!("No permission for key {}", key))675		})?;676	Ok(a)677}678679/// @title Unique extensions for ERC721.680#[solidity_interface(name = ERC721UniqueExtensions)]681impl<T: Config> RefungibleHandle<T>682where683	T::AccountId: From<[u8; 32]>,684{685	/// @notice A descriptive name for a collection of NFTs in this contract686	fn name(&self) -> Result<string> {687		Ok(decode_utf16(self.name.iter().copied())688			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))689			.collect::<string>())690	}691692	/// @notice An abbreviated name for NFTs in this contract693	fn symbol(&self) -> Result<string> {694		Ok(string::from_utf8_lossy(&self.token_prefix).into())695	}696697	/// @notice Transfer ownership of an RFT698	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`699	///  is the zero address. Throws if `tokenId` is not a valid RFT.700	///  Throws if RFT pieces have multiple owners.701	/// @param to The new owner702	/// @param tokenId The RFT to transfer703	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]704	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {705		let caller = T::CrossAccountId::from_eth(caller);706		let to = T::CrossAccountId::from_eth(to);707		let token = token_id.try_into()?;708		let budget = self709			.recorder710			.weight_calls_budget(<StructureWeight<T>>::find_parent());711712		let balance = balance(self, token, &caller)?;713		ensure_single_owner(self, token, balance)?;714715		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)716			.map_err(dispatch_to_evm::<T>)?;717		Ok(())718	}719720	/// @notice Transfer ownership of an RFT721	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`722	///  is the zero address. Throws if `tokenId` is not a valid RFT.723	///  Throws if RFT pieces have multiple owners.724	/// @param to The new owner725	/// @param tokenId The RFT to transfer726	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]727	fn transfer_from_cross(728		&mut self,729		caller: caller,730		from: EthCrossAccount,731		to: EthCrossAccount,732		token_id: uint256,733	) -> Result<void> {734		let caller = T::CrossAccountId::from_eth(caller);735		let from = from.into_sub_cross_account::<T>()?;736		let to = to.into_sub_cross_account::<T>()?;737		let token_id = token_id.try_into()?;738		let budget = self739			.recorder740			.weight_calls_budget(<StructureWeight<T>>::find_parent());741742		let balance = balance(self, token_id, &from)?;743		ensure_single_owner(self, token_id, balance)?;744745		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)746			.map_err(dispatch_to_evm::<T>)?;747		Ok(())748	}749750	/// @notice Burns a specific ERC721 token.751	/// @dev Throws unless `msg.sender` is the current owner or an authorized752	///  operator for this RFT. Throws if `from` is not the current owner. Throws753	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.754	///  Throws if RFT pieces have multiple owners.755	/// @param from The current owner of the RFT756	/// @param tokenId The RFT to transfer757	#[weight(<SelfWeightOf<T>>::burn_from())]758	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {759		let caller = T::CrossAccountId::from_eth(caller);760		let from = T::CrossAccountId::from_eth(from);761		let token = token_id.try_into()?;762		let budget = self763			.recorder764			.weight_calls_budget(<StructureWeight<T>>::find_parent());765766		let balance = balance(self, token, &from)?;767		ensure_single_owner(self, token, balance)?;768769		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)770			.map_err(dispatch_to_evm::<T>)?;771		Ok(())772	}773774	/// @notice Burns a specific ERC721 token.775	/// @dev Throws unless `msg.sender` is the current owner or an authorized776	///  operator for this RFT. Throws if `from` is not the current owner. Throws777	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.778	///  Throws if RFT pieces have multiple owners.779	/// @param from The current owner of the RFT780	/// @param tokenId The RFT to transfer781	#[weight(<SelfWeightOf<T>>::burn_from())]782	fn burn_from_cross(783		&mut self,784		caller: caller,785		from: EthCrossAccount,786		token_id: uint256,787	) -> Result<void> {788		let caller = T::CrossAccountId::from_eth(caller);789		let from = from.into_sub_cross_account::<T>()?;790		let token = token_id.try_into()?;791		let budget = self792			.recorder793			.weight_calls_budget(<StructureWeight<T>>::find_parent());794795		let balance = balance(self, token, &from)?;796		ensure_single_owner(self, token, balance)?;797798		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)799			.map_err(dispatch_to_evm::<T>)?;800		Ok(())801	}802803	/// @notice Returns next free RFT ID.804	fn next_token_id(&self) -> Result<uint256> {805		self.consume_store_reads(1)?;806		Ok(<TokensMinted<T>>::get(self.id)807			.checked_add(1)808			.ok_or("item id overflow")?809			.into())810	}811812	/// @notice Function to mint multiple tokens.813	/// @dev `tokenIds` should be an array of consecutive numbers and first number814	///  should be obtained with `nextTokenId` method815	/// @param to The new owner816	/// @param tokenIds IDs of the minted RFTs817	#[solidity(hide)]818	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]819	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {820		let caller = T::CrossAccountId::from_eth(caller);821		let to = T::CrossAccountId::from_eth(to);822		let mut expected_index = <TokensMinted<T>>::get(self.id)823			.checked_add(1)824			.ok_or("item id overflow")?;825		let budget = self826			.recorder827			.weight_calls_budget(<StructureWeight<T>>::find_parent());828829		let total_tokens = token_ids.len();830		for id in token_ids.into_iter() {831			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;832			if id != expected_index {833				return Err("item id should be next".into());834			}835			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;836		}837		let users = [(to.clone(), 1)]838			.into_iter()839			.collect::<BTreeMap<_, _>>()840			.try_into()841			.unwrap();842		let create_item_data = CreateItemData::<T::CrossAccountId> {843			users,844			properties: CollectionPropertiesVec::default(),845		};846		let data = (0..total_tokens)847			.map(|_| create_item_data.clone())848			.collect();849850		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)851			.map_err(dispatch_to_evm::<T>)?;852		Ok(true)853	}854855	/// @notice Function to mint multiple tokens with the given tokenUris.856	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive857	///  numbers and first number should be obtained with `nextTokenId` method858	/// @param to The new owner859	/// @param tokens array of pairs of token ID and token URI for minted tokens860	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]861	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]862	fn mint_bulk_with_token_uri(863		&mut self,864		caller: caller,865		to: address,866		tokens: Vec<(uint256, string)>,867	) -> Result<bool> {868		let key = key::url();869		let caller = T::CrossAccountId::from_eth(caller);870		let to = T::CrossAccountId::from_eth(to);871		let mut expected_index = <TokensMinted<T>>::get(self.id)872			.checked_add(1)873			.ok_or("item id overflow")?;874		let budget = self875			.recorder876			.weight_calls_budget(<StructureWeight<T>>::find_parent());877878		let mut data = Vec::with_capacity(tokens.len());879		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]880			.into_iter()881			.collect::<BTreeMap<_, _>>()882			.try_into()883			.unwrap();884		for (id, token_uri) in tokens {885			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;886			if id != expected_index {887				return Err("item id should be next".into());888			}889			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;890891			let mut properties = CollectionPropertiesVec::default();892			properties893				.try_push(Property {894					key: key.clone(),895					value: token_uri896						.into_bytes()897						.try_into()898						.map_err(|_| "token uri is too long")?,899				})900				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;901902			let create_item_data = CreateItemData::<T::CrossAccountId> {903				users: users.clone(),904				properties,905			};906			data.push(create_item_data);907		}908909		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)910			.map_err(dispatch_to_evm::<T>)?;911		Ok(true)912	}913914	/// Returns EVM address for refungible token915	///916	/// @param token ID of the token917	fn token_contract_address(&self, token: uint256) -> Result<address> {918		Ok(T::EvmTokenAddressMapping::token_to_address(919			self.id,920			token.try_into().map_err(|_| "token id overflow")?,921		))922	}923}924925#[solidity_interface(926	name = UniqueRefungible,927	is(928		ERC721,929		ERC721Enumerable,930		ERC721UniqueExtensions,931		ERC721UniqueMintable,932		ERC721Burnable,933		ERC721Metadata(if(this.flags.erc721metadata)),934		Collection(via(common_mut returns CollectionHandle<T>)),935		TokenProperties,936	)937)]938impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}939940// Not a tests, but code generators941generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);942generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);943944impl<T: Config> CommonEvmHandler for RefungibleHandle<T>945where946	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,947{948	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");949	fn call(950		self,951		handle: &mut impl PrecompileHandle,952	) -> Option<pallet_common::erc::PrecompileResult> {953		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)954	}955}