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

difftreelog

source

pallets/refungible/src/erc.rs31.6 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	#[solidity(hide)]168	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {169		let caller = T::CrossAccountId::from_eth(caller);170		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;171		let key = <Vec<u8>>::from(key)172			.try_into()173			.map_err(|_| "key too long")?;174175		let nesting_budget = self176			.recorder177			.weight_calls_budget(<StructureWeight<T>>::find_parent());178179		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)180			.map_err(dispatch_to_evm::<T>)181	}182183	/// @notice Delete token properties value.184	/// @dev Throws error if `msg.sender` has no permission to edit the property.185	/// @param tokenId ID of the token.186	/// @param keys Properties key.187	fn delete_properties(188		&mut self,189		token_id: uint256,190		caller: caller,191		keys: Vec<string>,192	) -> Result<()> {193		let caller = T::CrossAccountId::from_eth(caller);194		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;195		let keys = keys196			.into_iter()197			.map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))198			.collect::<Result<Vec<_>>>()?;199200		let nesting_budget = self201			.recorder202			.weight_calls_budget(<StructureWeight<T>>::find_parent());203204		<Pallet<T>>::delete_token_properties(205			self,206			&caller,207			TokenId(token_id),208			keys.into_iter(),209			&nesting_budget,210		)211		.map_err(dispatch_to_evm::<T>)212	}213214	/// @notice Get token property value.215	/// @dev Throws error if key not found216	/// @param tokenId ID of the token.217	/// @param key Property key.218	/// @return Property value bytes219	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {220		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;221		let key = <Vec<u8>>::from(key)222			.try_into()223			.map_err(|_| "key too long")?;224225		let props = <TokenProperties<T>>::get((self.id, token_id));226		let prop = props.get(&key).ok_or("key not found")?;227228		Ok(prop.to_vec().into())229	}230}231232#[derive(ToLog)]233pub enum ERC721Events {234	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed235	///  (`to` == 0). Exception: during contract creation, any number of RFTs236	///  may be created and assigned without emitting Transfer.237	Transfer {238		#[indexed]239		from: address,240		#[indexed]241		to: address,242		#[indexed]243		token_id: uint256,244	},245	/// @dev Not supported246	Approval {247		#[indexed]248		owner: address,249		#[indexed]250		approved: address,251		#[indexed]252		token_id: uint256,253	},254	/// @dev Not supported255	#[allow(dead_code)]256	ApprovalForAll {257		#[indexed]258		owner: address,259		#[indexed]260		operator: address,261		approved: bool,262	},263}264265#[derive(ToLog)]266pub enum ERC721UniqueMintableEvents {267	/// @dev Not supported268	#[allow(dead_code)]269	MintingFinished {},270}271272#[solidity_interface(name = ERC721Metadata)]273impl<T: Config> RefungibleHandle<T>274where275	T::AccountId: From<[u8; 32]>,276{277	/// @notice A descriptive name for a collection of NFTs in this contract278	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`279	#[solidity(hide, rename_selector = "name")]280	fn name_proxy(&self) -> Result<string> {281		self.name()282	}283284	/// @notice An abbreviated name for NFTs in this contract285	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`286	#[solidity(hide, rename_selector = "symbol")]287	fn symbol_proxy(&self) -> Result<string> {288		self.symbol()289	}290291	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.292	///293	/// @dev If the token has a `url` property and it is not empty, it is returned.294	///  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`.295	///  If the collection property `baseURI` is empty or absent, return "" (empty string)296	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix297	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).298	///299	/// @return token's const_metadata300	#[solidity(rename_selector = "tokenURI")]301	fn token_uri(&self, token_id: uint256) -> Result<string> {302		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;303304		match get_token_property(self, token_id_u32, &key::url()).as_deref() {305			Err(_) | Ok("") => (),306			Ok(url) => {307				return Ok(url.into());308			}309		};310311		let base_uri =312			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())313				.map(BoundedVec::into_inner)314				.map(string::from_utf8)315				.transpose()316				.map_err(|e| {317					Error::Revert(alloc::format!(318						"Can not convert value \"baseURI\" to string with error \"{}\"",319						e320					))321				})?;322323		let base_uri = match base_uri.as_deref() {324			None | Some("") => {325				return Ok("".into());326			}327			Some(base_uri) => base_uri.into(),328		};329330		Ok(331			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {332				Err(_) | Ok("") => base_uri,333				Ok(suffix) => base_uri + suffix,334			},335		)336	}337}338339/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension340/// @dev See https://eips.ethereum.org/EIPS/eip-721341#[solidity_interface(name = ERC721Enumerable)]342impl<T: Config> RefungibleHandle<T> {343	/// @notice Enumerate valid RFTs344	/// @param index A counter less than `totalSupply()`345	/// @return The token identifier for the `index`th NFT,346	///  (sort order not specified)347	fn token_by_index(&self, index: uint256) -> Result<uint256> {348		Ok(index)349	}350351	/// Not implemented352	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {353		// TODO: Not implemetable354		Err("not implemented".into())355	}356357	/// @notice Count RFTs tracked by this contract358	/// @return A count of valid RFTs tracked by this contract, where each one of359	///  them has an assigned and queryable owner not equal to the zero address360	fn total_supply(&self) -> Result<uint256> {361		self.consume_store_reads(1)?;362		Ok(<Pallet<T>>::total_supply(self).into())363	}364}365366/// @title ERC-721 Non-Fungible Token Standard367/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md368#[solidity_interface(name = ERC721, events(ERC721Events))]369impl<T: Config> RefungibleHandle<T> {370	/// @notice Count all RFTs assigned to an owner371	/// @dev RFTs assigned to the zero address are considered invalid, and this372	///  function throws for queries about the zero address.373	/// @param owner An address for whom to query the balance374	/// @return The number of RFTs owned by `owner`, possibly zero375	fn balance_of(&self, owner: address) -> Result<uint256> {376		self.consume_store_reads(1)?;377		let owner = T::CrossAccountId::from_eth(owner);378		let balance = <AccountBalance<T>>::get((self.id, owner));379		Ok(balance.into())380	}381382	/// @notice Find the owner of an RFT383	/// @dev RFTs assigned to zero address are considered invalid, and queries384	///  about them do throw.385	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for386	///  the tokens that are partially owned.387	/// @param tokenId The identifier for an RFT388	/// @return The address of the owner of the RFT389	fn owner_of(&self, token_id: uint256) -> Result<address> {390		self.consume_store_reads(2)?;391		let token = token_id.try_into()?;392		let owner = <Pallet<T>>::token_owner(self.id, token);393		Ok(owner394			.map(|address| *address.as_eth())395			.unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))396	}397398	/// @dev Not implemented399	fn safe_transfer_from_with_data(400		&mut self,401		_from: address,402		_to: address,403		_token_id: uint256,404		_data: bytes,405	) -> Result<void> {406		// TODO: Not implemetable407		Err("not implemented".into())408	}409410	/// @dev Not implemented411	fn safe_transfer_from(412		&mut self,413		_from: address,414		_to: address,415		_token_id: uint256,416	) -> Result<void> {417		// TODO: Not implemetable418		Err("not implemented".into())419	}420421	/// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE422	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE423	///  THEY MAY BE PERMANENTLY LOST424	/// @dev Throws unless `msg.sender` is the current owner or an authorized425	///  operator for this RFT. Throws if `from` is not the current owner. Throws426	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.427	///  Throws if RFT pieces have multiple owners.428	/// @param from The current owner of the NFT429	/// @param to The new owner430	/// @param tokenId The NFT to transfer431	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]432	fn transfer_from(433		&mut self,434		caller: caller,435		from: address,436		to: address,437		token_id: uint256,438	) -> Result<void> {439		let caller = T::CrossAccountId::from_eth(caller);440		let from = T::CrossAccountId::from_eth(from);441		let to = T::CrossAccountId::from_eth(to);442		let token = token_id.try_into()?;443		let budget = self444			.recorder445			.weight_calls_budget(<StructureWeight<T>>::find_parent());446447		let balance = balance(&self, token, &from)?;448		ensure_single_owner(&self, token, balance)?;449450		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)451			.map_err(dispatch_to_evm::<T>)?;452453		Ok(())454	}455456	/// @dev Not implemented457	fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {458		Err("not implemented".into())459	}460461	/// @dev Not implemented462	fn set_approval_for_all(463		&mut self,464		_caller: caller,465		_operator: address,466		_approved: bool,467	) -> Result<void> {468		// TODO: Not implemetable469		Err("not implemented".into())470	}471472	/// @dev Not implemented473	fn get_approved(&self, _token_id: uint256) -> Result<address> {474		// TODO: Not implemetable475		Err("not implemented".into())476	}477478	/// @dev Not implemented479	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {480		// TODO: Not implemetable481		Err("not implemented".into())482	}483}484485/// Returns amount of pieces of `token` that `owner` have486pub fn balance<T: Config>(487	collection: &RefungibleHandle<T>,488	token: TokenId,489	owner: &T::CrossAccountId,490) -> Result<u128> {491	collection.consume_store_reads(1)?;492	let balance = <Balance<T>>::get((collection.id, token, &owner));493	Ok(balance)494}495496/// Throws if `owner_balance` is lower than total amount of `token` pieces497pub fn ensure_single_owner<T: Config>(498	collection: &RefungibleHandle<T>,499	token: TokenId,500	owner_balance: u128,501) -> Result<()> {502	collection.consume_store_reads(1)?;503	let total_supply = <TotalSupply<T>>::get((collection.id, token));504	if total_supply != owner_balance {505		return Err("token has multiple owners".into());506	}507	Ok(())508}509510/// @title ERC721 Token that can be irreversibly burned (destroyed).511#[solidity_interface(name = ERC721Burnable)]512impl<T: Config> RefungibleHandle<T> {513	/// @notice Burns a specific ERC721 token.514	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized515	///  operator of the current owner.516	/// @param tokenId The RFT to approve517	#[weight(<SelfWeightOf<T>>::burn_item_fully())]518	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {519		let caller = T::CrossAccountId::from_eth(caller);520		let token = token_id.try_into()?;521522		let balance = balance(&self, token, &caller)?;523		ensure_single_owner(&self, token, balance)?;524525		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;526		Ok(())527	}528}529530/// @title ERC721 minting logic.531#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]532impl<T: Config> RefungibleHandle<T> {533	fn minting_finished(&self) -> Result<bool> {534		Ok(false)535	}536537	/// @notice Function to mint token.538	/// @param to The new owner539	/// @return uint256 The id of the newly minted token540	#[weight(<SelfWeightOf<T>>::create_item())]541	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {542		let token_id: uint256 = <TokensMinted<T>>::get(self.id)543			.checked_add(1)544			.ok_or("item id overflow")?545			.into();546		self.mint_check_id(caller, to, token_id)?;547		Ok(token_id)548	}549550	/// @notice Function to mint token.551	/// @dev `tokenId` should be obtained with `nextTokenId` method,552	///  unlike standard, you can't specify it manually553	/// @param to The new owner554	/// @param tokenId ID of the minted RFT555	#[solidity(hide, rename_selector = "mint")]556	#[weight(<SelfWeightOf<T>>::create_item())]557	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {558		let caller = T::CrossAccountId::from_eth(caller);559		let to = T::CrossAccountId::from_eth(to);560		let token_id: u32 = token_id.try_into()?;561		let budget = self562			.recorder563			.weight_calls_budget(<StructureWeight<T>>::find_parent());564565		if <TokensMinted<T>>::get(self.id)566			.checked_add(1)567			.ok_or("item id overflow")?568			!= token_id569		{570			return Err("item id should be next".into());571		}572573		let users = [(to.clone(), 1)]574			.into_iter()575			.collect::<BTreeMap<_, _>>()576			.try_into()577			.unwrap();578		<Pallet<T>>::create_item(579			self,580			&caller,581			CreateItemData::<T::CrossAccountId> {582				users,583				properties: CollectionPropertiesVec::default(),584			},585			&budget,586		)587		.map_err(dispatch_to_evm::<T>)?;588589		Ok(true)590	}591592	/// @notice Function to mint token with the given tokenUri.593	/// @param to The new owner594	/// @param tokenUri Token URI that would be stored in the NFT properties595	/// @return uint256 The id of the newly minted token596	#[solidity(rename_selector = "mintWithTokenURI")]597	#[weight(<SelfWeightOf<T>>::create_item())]598	fn mint_with_token_uri(599		&mut self,600		caller: caller,601		to: address,602		token_uri: string,603	) -> Result<uint256> {604		let token_id: uint256 = <TokensMinted<T>>::get(self.id)605			.checked_add(1)606			.ok_or("item id overflow")?607			.into();608		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;609		Ok(token_id)610	}611612	/// @notice Function to mint token with the given tokenUri.613	/// @dev `tokenId` should be obtained with `nextTokenId` method,614	///  unlike standard, you can't specify it manually615	/// @param to The new owner616	/// @param tokenId ID of the minted RFT617	/// @param tokenUri Token URI that would be stored in the RFT properties618	#[solidity(hide, rename_selector = "mintWithTokenURI")]619	#[weight(<SelfWeightOf<T>>::create_item())]620	fn mint_with_token_uri_check_id(621		&mut self,622		caller: caller,623		to: address,624		token_id: uint256,625		token_uri: string,626	) -> Result<bool> {627		let key = key::url();628		let permission = get_token_permission::<T>(self.id, &key)?;629		if !permission.collection_admin {630			return Err("Operation is not allowed".into());631		}632633		let caller = T::CrossAccountId::from_eth(caller);634		let to = T::CrossAccountId::from_eth(to);635		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;636		let budget = self637			.recorder638			.weight_calls_budget(<StructureWeight<T>>::find_parent());639640		if <TokensMinted<T>>::get(self.id)641			.checked_add(1)642			.ok_or("item id overflow")?643			!= token_id644		{645			return Err("item id should be next".into());646		}647648		let mut properties = CollectionPropertiesVec::default();649		properties650			.try_push(Property {651				key,652				value: token_uri653					.into_bytes()654					.try_into()655					.map_err(|_| "token uri is too long")?,656			})657			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;658659		let users = [(to.clone(), 1)]660			.into_iter()661			.collect::<BTreeMap<_, _>>()662			.try_into()663			.unwrap();664		<Pallet<T>>::create_item(665			self,666			&caller,667			CreateItemData::<T::CrossAccountId> { users, properties },668			&budget,669		)670		.map_err(dispatch_to_evm::<T>)?;671		Ok(true)672	}673674	/// @dev Not implemented675	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {676		Err("not implementable".into())677	}678}679680fn get_token_property<T: Config>(681	collection: &CollectionHandle<T>,682	token_id: u32,683	key: &up_data_structs::PropertyKey,684) -> Result<string> {685	collection.consume_store_reads(1)?;686	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))687		.map_err(|_| Error::Revert("Token properties not found".into()))?;688	if let Some(property) = properties.get(key) {689		return Ok(string::from_utf8_lossy(property).into());690	}691692	Err("Property tokenURI not found".into())693}694695fn get_token_permission<T: Config>(696	collection_id: CollectionId,697	key: &PropertyKey,698) -> Result<PropertyPermission> {699	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)700		.map_err(|_| Error::Revert("No permissions for collection".into()))?;701	let a = token_property_permissions702		.get(key)703		.map(Clone::clone)704		.ok_or_else(|| {705			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();706			Error::Revert(alloc::format!("No permission for key {}", key))707		})?;708	Ok(a)709}710711/// @title Unique extensions for ERC721.712#[solidity_interface(name = ERC721UniqueExtensions)]713impl<T: Config> RefungibleHandle<T>714where715	T::AccountId: From<[u8; 32]>,716{717	/// @notice A descriptive name for a collection of NFTs in this contract718	fn name(&self) -> Result<string> {719		Ok(decode_utf16(self.name.iter().copied())720			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))721			.collect::<string>())722	}723724	/// @notice An abbreviated name for NFTs in this contract725	fn symbol(&self) -> Result<string> {726		Ok(string::from_utf8_lossy(&self.token_prefix).into())727	}728729	/// @notice Transfer ownership of an RFT730	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`731	///  is the zero address. Throws if `tokenId` is not a valid RFT.732	///  Throws if RFT pieces have multiple owners.733	/// @param to The new owner734	/// @param tokenId The RFT to transfer735	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]736	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {737		let caller = T::CrossAccountId::from_eth(caller);738		let to = T::CrossAccountId::from_eth(to);739		let token = token_id.try_into()?;740		let budget = self741			.recorder742			.weight_calls_budget(<StructureWeight<T>>::find_parent());743744		let balance = balance(self, token, &caller)?;745		ensure_single_owner(self, token, balance)?;746747		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)748			.map_err(dispatch_to_evm::<T>)?;749		Ok(())750	}751752	/// @notice Transfer ownership of an RFT753	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`754	///  is the zero address. Throws if `tokenId` is not a valid RFT.755	///  Throws if RFT pieces have multiple owners.756	/// @param to The new owner757	/// @param tokenId The RFT to transfer758	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]759	fn transfer_cross(&mut self, caller: caller, to: EthCrossAccount, token_id: uint256) -> Result<void> {760		let caller = T::CrossAccountId::from_eth(caller);761		let to = to.into_sub_cross_account::<T>()?;762		let token = token_id.try_into()?;763		let budget = self764			.recorder765			.weight_calls_budget(<StructureWeight<T>>::find_parent());766767		let balance = balance(self, token, &caller)?;768		ensure_single_owner(self, token, balance)?;769770		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)771			.map_err(dispatch_to_evm::<T>)?;772		Ok(())773	}774775	/// @notice Transfer ownership of an RFT776	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`777	///  is the zero address. Throws if `tokenId` is not a valid RFT.778	///  Throws if RFT pieces have multiple owners.779	/// @param to The new owner780	/// @param tokenId The RFT to transfer781	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]782	fn transfer_from_cross(783		&mut self,784		caller: caller,785		from: EthCrossAccount,786		to: EthCrossAccount,787		token_id: uint256,788	) -> Result<void> {789		let caller = T::CrossAccountId::from_eth(caller);790		let from = from.into_sub_cross_account::<T>()?;791		let to = to.into_sub_cross_account::<T>()?;792		let token_id = token_id.try_into()?;793		let budget = self794			.recorder795			.weight_calls_budget(<StructureWeight<T>>::find_parent());796797		let balance = balance(self, token_id, &from)?;798		ensure_single_owner(self, token_id, balance)?;799800		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)801			.map_err(dispatch_to_evm::<T>)?;802		Ok(())803	}804805	/// @notice Burns a specific ERC721 token.806	/// @dev Throws unless `msg.sender` is the current owner or an authorized807	///  operator for this RFT. Throws if `from` is not the current owner. Throws808	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.809	///  Throws if RFT pieces have multiple owners.810	/// @param from The current owner of the RFT811	/// @param tokenId The RFT to transfer812	#[weight(<SelfWeightOf<T>>::burn_from())]813	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {814		let caller = T::CrossAccountId::from_eth(caller);815		let from = T::CrossAccountId::from_eth(from);816		let token = token_id.try_into()?;817		let budget = self818			.recorder819			.weight_calls_budget(<StructureWeight<T>>::find_parent());820821		let balance = balance(self, token, &from)?;822		ensure_single_owner(self, token, balance)?;823824		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)825			.map_err(dispatch_to_evm::<T>)?;826		Ok(())827	}828829	/// @notice Burns a specific ERC721 token.830	/// @dev Throws unless `msg.sender` is the current owner or an authorized831	///  operator for this RFT. Throws if `from` is not the current owner. Throws832	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.833	///  Throws if RFT pieces have multiple owners.834	/// @param from The current owner of the RFT835	/// @param tokenId The RFT to transfer836	#[weight(<SelfWeightOf<T>>::burn_from())]837	fn burn_from_cross(838		&mut self,839		caller: caller,840		from: EthCrossAccount,841		token_id: uint256,842	) -> Result<void> {843		let caller = T::CrossAccountId::from_eth(caller);844		let from = from.into_sub_cross_account::<T>()?;845		let token = token_id.try_into()?;846		let budget = self847			.recorder848			.weight_calls_budget(<StructureWeight<T>>::find_parent());849850		let balance = balance(self, token, &from)?;851		ensure_single_owner(self, token, balance)?;852853		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)854			.map_err(dispatch_to_evm::<T>)?;855		Ok(())856	}857858	/// @notice Returns next free RFT ID.859	fn next_token_id(&self) -> Result<uint256> {860		self.consume_store_reads(1)?;861		Ok(<TokensMinted<T>>::get(self.id)862			.checked_add(1)863			.ok_or("item id overflow")?864			.into())865	}866867	/// @notice Function to mint multiple tokens.868	/// @dev `tokenIds` should be an array of consecutive numbers and first number869	///  should be obtained with `nextTokenId` method870	/// @param to The new owner871	/// @param tokenIds IDs of the minted RFTs872	#[solidity(hide)]873	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]874	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {875		let caller = T::CrossAccountId::from_eth(caller);876		let to = T::CrossAccountId::from_eth(to);877		let mut expected_index = <TokensMinted<T>>::get(self.id)878			.checked_add(1)879			.ok_or("item id overflow")?;880		let budget = self881			.recorder882			.weight_calls_budget(<StructureWeight<T>>::find_parent());883884		let total_tokens = token_ids.len();885		for id in token_ids.into_iter() {886			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;887			if id != expected_index {888				return Err("item id should be next".into());889			}890			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;891		}892		let users = [(to.clone(), 1)]893			.into_iter()894			.collect::<BTreeMap<_, _>>()895			.try_into()896			.unwrap();897		let create_item_data = CreateItemData::<T::CrossAccountId> {898			users,899			properties: CollectionPropertiesVec::default(),900		};901		let data = (0..total_tokens)902			.map(|_| create_item_data.clone())903			.collect();904905		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)906			.map_err(dispatch_to_evm::<T>)?;907		Ok(true)908	}909910	/// @notice Function to mint multiple tokens with the given tokenUris.911	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive912	///  numbers and first number should be obtained with `nextTokenId` method913	/// @param to The new owner914	/// @param tokens array of pairs of token ID and token URI for minted tokens915	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]916	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]917	fn mint_bulk_with_token_uri(918		&mut self,919		caller: caller,920		to: address,921		tokens: Vec<(uint256, string)>,922	) -> Result<bool> {923		let key = key::url();924		let caller = T::CrossAccountId::from_eth(caller);925		let to = T::CrossAccountId::from_eth(to);926		let mut expected_index = <TokensMinted<T>>::get(self.id)927			.checked_add(1)928			.ok_or("item id overflow")?;929		let budget = self930			.recorder931			.weight_calls_budget(<StructureWeight<T>>::find_parent());932933		let mut data = Vec::with_capacity(tokens.len());934		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]935			.into_iter()936			.collect::<BTreeMap<_, _>>()937			.try_into()938			.unwrap();939		for (id, token_uri) in tokens {940			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;941			if id != expected_index {942				return Err("item id should be next".into());943			}944			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;945946			let mut properties = CollectionPropertiesVec::default();947			properties948				.try_push(Property {949					key: key.clone(),950					value: token_uri951						.into_bytes()952						.try_into()953						.map_err(|_| "token uri is too long")?,954				})955				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;956957			let create_item_data = CreateItemData::<T::CrossAccountId> {958				users: users.clone(),959				properties,960			};961			data.push(create_item_data);962		}963964		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)965			.map_err(dispatch_to_evm::<T>)?;966		Ok(true)967	}968969	/// Returns EVM address for refungible token970	///971	/// @param token ID of the token972	fn token_contract_address(&self, token: uint256) -> Result<address> {973		Ok(T::EvmTokenAddressMapping::token_to_address(974			self.id,975			token.try_into().map_err(|_| "token id overflow")?,976		))977	}978}979980#[solidity_interface(981	name = UniqueRefungible,982	is(983		ERC721,984		ERC721Enumerable,985		ERC721UniqueExtensions,986		ERC721UniqueMintable,987		ERC721Burnable,988		ERC721Metadata(if(this.flags.erc721metadata)),989		Collection(via(common_mut returns CollectionHandle<T>)),990		TokenProperties,991	)992)]993impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}994995// Not a tests, but code generators996generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);997generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);998999impl<T: Config> CommonEvmHandler for RefungibleHandle<T>1000where1001	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1002{1003	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");1004	fn call(1005		self,1006		handle: &mut impl PrecompileHandle,1007	) -> Option<pallet_common::erc::PrecompileResult> {1008		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)1009	}1010}