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

difftreelog

chore implement transfer and burn for ERC-721

Grigoriy Simonov2022-07-28parent: #7d1859c.patch.diff
in: master

9 files changed

modifiedMakefilediffbeforeafterboth
--- a/Makefile
+++ b/Makefile
@@ -41,10 +41,6 @@
 UniqueRefungible.sol:
 	PACKAGE=pallet-refungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
 	PACKAGE=pallet-refungible NAME=erc::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
-	
-UniqueRefungible.sol:
-	PACKAGE=pallet-refungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
-	PACKAGE=pallet-refungible NAME=erc::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
 
 UniqueRefungibleToken.sol:
 	PACKAGE=pallet-refungible NAME=erc_token::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -73,10 +69,6 @@
 UniqueRefungibleToken: UniqueRefungibleToken.sol
 	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungibleToken.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_TOKEN_EVM_ABI) ./.maintain/scripts/generate_abi.sh
-
-UniqueRefungible: UniqueRefungible.sol
-	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungible.raw ./.maintain/scripts/compile_stub.sh
-	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
 
 ContractHelpers: ContractHelpers.sol
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_STUBS)/ContractHelpers.raw ./.maintain/scripts/compile_stub.sh
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
before · pallets/refungible/src/erc.rs
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 alloc::string::ToString;25use core::{26	char::{REPLACEMENT_CHARACTER, decode_utf16},27	convert::TryInto,28};29use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};30use frame_support::{BoundedBTreeMap, BoundedVec};31use pallet_common::{32	CollectionHandle, CollectionPropertyPermissions,33	erc::{34		CommonEvmHandler, CollectionCall,35		static_property::{key, value as property_value},36	},37};38use pallet_evm::{account::CrossAccountId, PrecompileHandle};39use pallet_evm_coder_substrate::{call, dispatch_to_evm};40use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};41use sp_core::H160;42use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};43use up_data_structs::{44	CollectionId, CollectionPropertiesVec, Property, PropertyKey, PropertyKeyPermission,45	PropertyPermission, TokenId,46};4748use crate::{49	AccountBalance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,50	TokenProperties, TokensMinted, weights::WeightInfo,51};5253/// @title A contract that allows to set and delete token properties and change token property permissions.54#[solidity_interface(name = "TokenProperties")]55impl<T: Config> RefungibleHandle<T> {56	/// @notice Set permissions for token property.57	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.58	/// @param key Property key.59	/// @param is_mutable Permission to mutate property.60	/// @param collection_admin Permission to mutate property by collection admin if property is mutable.61	/// @param token_owner Permission to mutate property by token owner if property is mutable.62	fn set_token_property_permission(63		&mut self,64		caller: caller,65		key: string,66		is_mutable: bool,67		collection_admin: bool,68		token_owner: bool,69	) -> Result<()> {70		let caller = T::CrossAccountId::from_eth(caller);71		<Pallet<T>>::set_token_property_permissions(72			self,73			&caller,74			vec![PropertyKeyPermission {75				key: <Vec<u8>>::from(key)76					.try_into()77					.map_err(|_| "too long key")?,78				permission: PropertyPermission {79					mutable: is_mutable,80					collection_admin,81					token_owner,82				},83			}],84		)85		.map_err(dispatch_to_evm::<T>)86	}8788	/// @notice Set token property value.89	/// @dev Throws error if `msg.sender` has no permission to edit the property.90	/// @param tokenId ID of the token.91	/// @param key Property key.92	/// @param value Property value.93	fn set_property(94		&mut self,95		caller: caller,96		token_id: uint256,97		key: string,98		value: bytes,99	) -> Result<()> {100		let caller = T::CrossAccountId::from_eth(caller);101		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;102		let key = <Vec<u8>>::from(key)103			.try_into()104			.map_err(|_| "key too long")?;105		let value = value.try_into().map_err(|_| "value too long")?;106107		let nesting_budget = self108			.recorder109			.weight_calls_budget(<StructureWeight<T>>::find_parent());110111		<Pallet<T>>::set_token_property(112			self,113			&caller,114			TokenId(token_id),115			Property { key, value },116			&nesting_budget,117		)118		.map_err(dispatch_to_evm::<T>)119	}120121	/// @notice Delete token property value.122	/// @dev Throws error if `msg.sender` has no permission to edit the property.123	/// @param tokenId ID of the token.124	/// @param key Property key.125	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {126		let caller = T::CrossAccountId::from_eth(caller);127		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;128		let key = <Vec<u8>>::from(key)129			.try_into()130			.map_err(|_| "key too long")?;131132		let nesting_budget = self133			.recorder134			.weight_calls_budget(<StructureWeight<T>>::find_parent());135136		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)137			.map_err(dispatch_to_evm::<T>)138	}139140	/// @notice Get token property value.141	/// @dev Throws error if key not found142	/// @param tokenId ID of the token.143	/// @param key Property key.144	/// @return Property value bytes145	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {146		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;147		let key = <Vec<u8>>::from(key)148			.try_into()149			.map_err(|_| "key too long")?;150151		let props = <TokenProperties<T>>::get((self.id, token_id));152		let prop = props.get(&key).ok_or("key not found")?;153154		Ok(prop.to_vec())155	}156}157158#[derive(ToLog)]159pub enum ERC721Events {160	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed161	///  (`to` == 0). Exception: during contract creation, any number of RFTs162	///  may be created and assigned without emitting Transfer.163	Transfer {164		#[indexed]165		from: address,166		#[indexed]167		to: address,168		#[indexed]169		token_id: uint256,170	},171	/// @dev Not supported172	Approval {173		#[indexed]174		owner: address,175		#[indexed]176		approved: address,177		#[indexed]178		token_id: uint256,179	},180	/// @dev Not supported181	#[allow(dead_code)]182	ApprovalForAll {183		#[indexed]184		owner: address,185		#[indexed]186		operator: address,187		approved: bool,188	},189}190191#[derive(ToLog)]192pub enum ERC721MintableEvents {193	/// @dev Not supported194	#[allow(dead_code)]195	MintingFinished {},196}197198#[solidity_interface(name = "ERC721Metadata")]199impl<T: Config> RefungibleHandle<T> {200	/// @notice A descriptive name for a collection of RFTs in this contract201	fn name(&self) -> Result<string> {202		Ok(decode_utf16(self.name.iter().copied())203			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))204			.collect::<string>())205	}206207	/// @notice An abbreviated name for RFTs in this contract208	fn symbol(&self) -> Result<string> {209		Ok(string::from_utf8_lossy(&self.token_prefix).into())210	}211212	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.213	///214	/// @dev If the token has a `url` property and it is not empty, it is returned.215	///  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`.216	///  If the collection property `baseURI` is empty or absent, return "" (empty string)217	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix218	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).219	///220	/// @return token's const_metadata221	#[solidity(rename_selector = "tokenURI")]222	fn token_uri(&self, token_id: uint256) -> Result<string> {223		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;224225		if let Ok(url) = get_token_property(self, token_id_u32, &key::url()) {226			if !url.is_empty() {227				return Ok(url);228			}229		} else if !is_erc721_metadata_compatible::<T>(self.id) {230			return Err("tokenURI not set".into());231		}232233		if let Some(base_uri) =234			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())235		{236			if !base_uri.is_empty() {237				let base_uri = string::from_utf8(base_uri.into_inner()).map_err(|e| {238					Error::Revert(alloc::format!(239						"Can not convert value \"baseURI\" to string with error \"{}\"",240						e241					))242				})?;243				if let Ok(suffix) = get_token_property(self, token_id_u32, &key::suffix()) {244					if !suffix.is_empty() {245						return Ok(base_uri + suffix.as_str());246					}247				}248249				return Ok(base_uri + token_id.to_string().as_str());250			}251		}252253		Ok("".into())254	}255}256257/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension258/// @dev See https://eips.ethereum.org/EIPS/eip-721259#[solidity_interface(name = "ERC721Enumerable")]260impl<T: Config> RefungibleHandle<T> {261	/// @notice Enumerate valid RFTs262	/// @param index A counter less than `totalSupply()`263	/// @return The token identifier for the `index`th NFT,264	///  (sort order not specified)265	fn token_by_index(&self, index: uint256) -> Result<uint256> {266		Ok(index)267	}268269	/// Not implemented270	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {271		// TODO: Not implemetable272		Err("not implemented".into())273	}274275	/// @notice Count RFTs tracked by this contract276	/// @return A count of valid RFTs tracked by this contract, where each one of277	///  them has an assigned and queryable owner not equal to the zero address278	fn total_supply(&self) -> Result<uint256> {279		self.consume_store_reads(1)?;280		Ok(<Pallet<T>>::total_supply(self).into())281	}282}283284/// @title ERC-721 Non-Fungible Token Standard285/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md286#[solidity_interface(name = "ERC721", events(ERC721Events))]287impl<T: Config> RefungibleHandle<T> {288	/// @notice Count all RFTs assigned to an owner289	/// @dev RFTs assigned to the zero address are considered invalid, and this290	///  function throws for queries about the zero address.291	/// @param owner An address for whom to query the balance292	/// @return The number of RFTs owned by `owner`, possibly zero293	fn balance_of(&self, owner: address) -> Result<uint256> {294		self.consume_store_reads(1)?;295		let owner = T::CrossAccountId::from_eth(owner);296		let balance = <AccountBalance<T>>::get((self.id, owner));297		Ok(balance.into())298	}299300	fn owner_of(&self, token_id: uint256) -> Result<address> {301		self.consume_store_reads(2)?;302		let token = token_id.try_into()?;303		let owner = <Pallet<T>>::token_owner(self.id, token);304		Ok(owner305			.map(|address| *address.as_eth())306			.unwrap_or_else(|| H160::default()))307	}308309	/// @dev Not implemented310	fn safe_transfer_from_with_data(311		&mut self,312		_from: address,313		_to: address,314		_token_id: uint256,315		_data: bytes,316		_value: value,317	) -> Result<void> {318		// TODO: Not implemetable319		Err("not implemented".into())320	}321322	/// @dev Not implemented323	fn safe_transfer_from(324		&mut self,325		_from: address,326		_to: address,327		_token_id: uint256,328		_value: value,329	) -> Result<void> {330		// TODO: Not implemetable331		Err("not implemented".into())332	}333334	/// @dev Not implemented335	fn transfer_from(336		&mut self,337		_caller: caller,338		_from: address,339		_to: address,340		_token_id: uint256,341		_value: value,342	) -> Result<void> {343		Err("not implemented".into())344	}345346	/// @dev Not implemented347	fn approve(348		&mut self,349		_caller: caller,350		_approved: address,351		_token_id: uint256,352		_value: value,353	) -> Result<void> {354		Err("not implemented".into())355	}356357	/// @dev Not implemented358	fn set_approval_for_all(359		&mut self,360		_caller: caller,361		_operator: address,362		_approved: bool,363	) -> Result<void> {364		// TODO: Not implemetable365		Err("not implemented".into())366	}367368	/// @dev Not implemented369	fn get_approved(&self, _token_id: uint256) -> Result<address> {370		// TODO: Not implemetable371		Err("not implemented".into())372	}373374	/// @dev Not implemented375	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {376		// TODO: Not implemetable377		Err("not implemented".into())378	}379}380381/// @title ERC721 Token that can be irreversibly burned (destroyed).382#[solidity_interface(name = "ERC721Burnable")]383impl<T: Config> RefungibleHandle<T> {384	/// @dev Not implemented385	fn burn(&mut self, _caller: caller, _token_id: uint256, _value: value) -> Result<void> {386		Err("not implemented".into())387	}388}389390/// @title ERC721 minting logic.391#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]392impl<T: Config> RefungibleHandle<T> {393	fn minting_finished(&self) -> Result<bool> {394		Ok(false)395	}396397	/// @notice Function to mint token.398	/// @dev `tokenId` should be obtained with `nextTokenId` method,399	///  unlike standard, you can't specify it manually400	/// @param to The new owner401	/// @param tokenId ID of the minted RFT402	#[weight(<SelfWeightOf<T>>::create_item())]403	fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {404		let caller = T::CrossAccountId::from_eth(caller);405		let to = T::CrossAccountId::from_eth(to);406		let token_id: u32 = token_id.try_into()?;407		let budget = self408			.recorder409			.weight_calls_budget(<StructureWeight<T>>::find_parent());410411		if <TokensMinted<T>>::get(self.id)412			.checked_add(1)413			.ok_or("item id overflow")?414			!= token_id415		{416			return Err("item id should be next".into());417		}418419		let const_data = BoundedVec::default();420		let users = [(to.clone(), 1)]421			.into_iter()422			.collect::<BTreeMap<_, _>>()423			.try_into()424			.unwrap();425		<Pallet<T>>::create_item(426			self,427			&caller,428			CreateItemData::<T> {429				const_data,430				users,431				properties: CollectionPropertiesVec::default(),432			},433			&budget,434		)435		.map_err(dispatch_to_evm::<T>)?;436437		Ok(true)438	}439440	/// @notice Function to mint token with the given tokenUri.441	/// @dev `tokenId` should be obtained with `nextTokenId` method,442	///  unlike standard, you can't specify it manually443	/// @param to The new owner444	/// @param tokenId ID of the minted RFT445	/// @param tokenUri Token URI that would be stored in the RFT properties446	#[solidity(rename_selector = "mintWithTokenURI")]447	#[weight(<SelfWeightOf<T>>::create_item())]448	fn mint_with_token_uri(449		&mut self,450		caller: caller,451		to: address,452		token_id: uint256,453		token_uri: string,454	) -> Result<bool> {455		let key = key::url();456		let permission = get_token_permission::<T>(self.id, &key)?;457		if !permission.collection_admin {458			return Err("Operation is not allowed".into());459		}460461		let caller = T::CrossAccountId::from_eth(caller);462		let to = T::CrossAccountId::from_eth(to);463		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;464		let budget = self465			.recorder466			.weight_calls_budget(<StructureWeight<T>>::find_parent());467468		if <TokensMinted<T>>::get(self.id)469			.checked_add(1)470			.ok_or("item id overflow")?471			!= token_id472		{473			return Err("item id should be next".into());474		}475476		let mut properties = CollectionPropertiesVec::default();477		properties478			.try_push(Property {479				key,480				value: token_uri481					.into_bytes()482					.try_into()483					.map_err(|_| "token uri is too long")?,484			})485			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;486487		let const_data = BoundedVec::default();488		let users = [(to.clone(), 1)]489			.into_iter()490			.collect::<BTreeMap<_, _>>()491			.try_into()492			.unwrap();493		<Pallet<T>>::create_item(494			self,495			&caller,496			CreateItemData::<T> {497				const_data,498				users,499				properties,500			},501			&budget,502		)503		.map_err(dispatch_to_evm::<T>)?;504		Ok(true)505	}506507	/// @dev Not implemented508	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {509		Err("not implementable".into())510	}511}512513fn get_token_property<T: Config>(514	collection: &CollectionHandle<T>,515	token_id: u32,516	key: &up_data_structs::PropertyKey,517) -> Result<string> {518	collection.consume_store_reads(1)?;519	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))520		.map_err(|_| Error::Revert("Token properties not found".into()))?;521	if let Some(property) = properties.get(key) {522		return Ok(string::from_utf8_lossy(property).into());523	}524525	Err("Property tokenURI not found".into())526}527528fn is_erc721_metadata_compatible<T: Config>(collection_id: CollectionId) -> bool {529	if let Some(shema_name) =530		pallet_common::Pallet::<T>::get_collection_property(collection_id, &key::schema_name())531	{532		let shema_name = shema_name.into_inner();533		shema_name == property_value::ERC721_METADATA534	} else {535		false536	}537}538539fn get_token_permission<T: Config>(540	collection_id: CollectionId,541	key: &PropertyKey,542) -> Result<PropertyPermission> {543	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)544		.map_err(|_| Error::Revert("No permissions for collection".into()))?;545	let a = token_property_permissions546		.get(key)547		.map(Clone::clone)548		.ok_or_else(|| {549			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();550			Error::Revert(alloc::format!("No permission for key {}", key))551		})?;552	Ok(a)553}554555/// @title Unique extensions for ERC721.556#[solidity_interface(name = "ERC721UniqueExtensions")]557impl<T: Config> RefungibleHandle<T> {558	/// @notice Returns next free RFT ID.559	fn next_token_id(&self) -> Result<uint256> {560		self.consume_store_reads(1)?;561		Ok(<TokensMinted<T>>::get(self.id)562			.checked_add(1)563			.ok_or("item id overflow")?564			.into())565	}566567	/// @notice Function to mint multiple tokens.568	/// @dev `tokenIds` should be an array of consecutive numbers and first number569	///  should be obtained with `nextTokenId` method570	/// @param to The new owner571	/// @param tokenIds IDs of the minted RFTs572	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]573	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {574		let caller = T::CrossAccountId::from_eth(caller);575		let to = T::CrossAccountId::from_eth(to);576		let mut expected_index = <TokensMinted<T>>::get(self.id)577			.checked_add(1)578			.ok_or("item id overflow")?;579		let budget = self580			.recorder581			.weight_calls_budget(<StructureWeight<T>>::find_parent());582583		let total_tokens = token_ids.len();584		for id in token_ids.into_iter() {585			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;586			if id != expected_index {587				return Err("item id should be next".into());588			}589			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;590		}591		let const_data = BoundedVec::default();592		let users = [(to.clone(), 1)]593			.into_iter()594			.collect::<BTreeMap<_, _>>()595			.try_into()596			.unwrap();597		let create_item_data = CreateItemData::<T> {598			const_data,599			users,600			properties: CollectionPropertiesVec::default(),601		};602		let data = (0..total_tokens)603			.map(|_| create_item_data.clone())604			.collect();605606		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)607			.map_err(dispatch_to_evm::<T>)?;608		Ok(true)609	}610611	/// @notice Function to mint multiple tokens with the given tokenUris.612	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive613	///  numbers and first number should be obtained with `nextTokenId` method614	/// @param to The new owner615	/// @param tokens array of pairs of token ID and token URI for minted tokens616	#[solidity(rename_selector = "mintBulkWithTokenURI")]617	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]618	fn mint_bulk_with_token_uri(619		&mut self,620		caller: caller,621		to: address,622		tokens: Vec<(uint256, string)>,623	) -> Result<bool> {624		let key = key::url();625		let caller = T::CrossAccountId::from_eth(caller);626		let to = T::CrossAccountId::from_eth(to);627		let mut expected_index = <TokensMinted<T>>::get(self.id)628			.checked_add(1)629			.ok_or("item id overflow")?;630		let budget = self631			.recorder632			.weight_calls_budget(<StructureWeight<T>>::find_parent());633634		let mut data = Vec::with_capacity(tokens.len());635		let const_data = BoundedVec::default();636		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]637			.into_iter()638			.collect::<BTreeMap<_, _>>()639			.try_into()640			.unwrap();641		for (id, token_uri) in tokens {642			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;643			if id != expected_index {644				return Err("item id should be next".into());645			}646			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;647648			let mut properties = CollectionPropertiesVec::default();649			properties650				.try_push(Property {651					key: key.clone(),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 create_item_data = CreateItemData::<T> {660				const_data: const_data.clone(),661				users: users.clone(),662				properties,663			};664			data.push(create_item_data);665		}666667		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)668			.map_err(dispatch_to_evm::<T>)?;669		Ok(true)670	}671}672673#[solidity_interface(674	name = "UniqueRefungible",675	is(676		ERC721,677		ERC721Metadata,678		ERC721Enumerable,679		ERC721UniqueExtensions,680		ERC721Mintable,681		ERC721Burnable,682		via("CollectionHandle<T>", common_mut, Collection),683		TokenProperties,684	)685)]686impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}687688// Not a tests, but code generators689generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);690generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);691692impl<T: Config> CommonEvmHandler for RefungibleHandle<T>693where694	T::AccountId: From<[u8; 32]>,695{696	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");697	fn call(698		self,699		handle: &mut impl PrecompileHandle,700	) -> Option<pallet_common::erc::PrecompileResult> {701		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)702	}703}
after · pallets/refungible/src/erc.rs
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 alloc::string::ToString;25use core::{26	char::{REPLACEMENT_CHARACTER, decode_utf16},27	convert::TryInto,28};29use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};30use frame_support::{BoundedBTreeMap, BoundedVec};31use pallet_common::{32	CollectionHandle, CollectionPropertyPermissions,33	erc::{34		CommonEvmHandler, CollectionCall,35		static_property::{key, value as property_value},36	},37	eth::collection_id_to_address,38};39use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm, PrecompileHandle};40use pallet_evm_coder_substrate::{call, dispatch_to_evm};41use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};42use sp_core::H160;43use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};44use up_data_structs::{45	CollectionId, CollectionPropertiesVec, Property, PropertyKey, PropertyKeyPermission,46	PropertyPermission, TokenId,47};4849use crate::{50	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,51	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,52};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 is_mutable Permission to mutate property.61	/// @param collection_admin Permission to mutate property by collection admin if property is mutable.62	/// @param token_owner 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.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 Delete token property value.123	/// @dev Throws error if `msg.sender` has no permission to edit the property.124	/// @param tokenId ID of the token.125	/// @param key Property key.126	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {127		let caller = T::CrossAccountId::from_eth(caller);128		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;129		let key = <Vec<u8>>::from(key)130			.try_into()131			.map_err(|_| "key too long")?;132133		let nesting_budget = self134			.recorder135			.weight_calls_budget(<StructureWeight<T>>::find_parent());136137		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)138			.map_err(dispatch_to_evm::<T>)139	}140141	/// @notice Get token property value.142	/// @dev Throws error if key not found143	/// @param tokenId ID of the token.144	/// @param key Property key.145	/// @return Property value bytes146	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {147		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;148		let key = <Vec<u8>>::from(key)149			.try_into()150			.map_err(|_| "key too long")?;151152		let props = <TokenProperties<T>>::get((self.id, token_id));153		let prop = props.get(&key).ok_or("key not found")?;154155		Ok(prop.to_vec())156	}157}158159#[derive(ToLog)]160pub enum ERC721Events {161	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed162	///  (`to` == 0). Exception: during contract creation, any number of RFTs163	///  may be created and assigned without emitting Transfer.164	Transfer {165		#[indexed]166		from: address,167		#[indexed]168		to: address,169		#[indexed]170		token_id: uint256,171	},172	/// @dev Not supported173	Approval {174		#[indexed]175		owner: address,176		#[indexed]177		approved: address,178		#[indexed]179		token_id: uint256,180	},181	/// @dev Not supported182	#[allow(dead_code)]183	ApprovalForAll {184		#[indexed]185		owner: address,186		#[indexed]187		operator: address,188		approved: bool,189	},190}191192#[derive(ToLog)]193pub enum ERC721MintableEvents {194	/// @dev Not supported195	#[allow(dead_code)]196	MintingFinished {},197}198199#[solidity_interface(name = "ERC721Metadata")]200impl<T: Config> RefungibleHandle<T> {201	/// @notice A descriptive name for a collection of RFTs in this contract202	fn name(&self) -> Result<string> {203		Ok(decode_utf16(self.name.iter().copied())204			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))205			.collect::<string>())206	}207208	/// @notice An abbreviated name for RFTs in this contract209	fn symbol(&self) -> Result<string> {210		Ok(string::from_utf8_lossy(&self.token_prefix).into())211	}212213	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.214	///215	/// @dev If the token has a `url` property and it is not empty, it is returned.216	///  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`.217	///  If the collection property `baseURI` is empty or absent, return "" (empty string)218	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix219	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).220	///221	/// @return token's const_metadata222	#[solidity(rename_selector = "tokenURI")]223	fn token_uri(&self, token_id: uint256) -> Result<string> {224		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;225226		if let Ok(url) = get_token_property(self, token_id_u32, &key::url()) {227			if !url.is_empty() {228				return Ok(url);229			}230		} else if !is_erc721_metadata_compatible::<T>(self.id) {231			return Err("tokenURI not set".into());232		}233234		if let Some(base_uri) =235			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())236		{237			if !base_uri.is_empty() {238				let base_uri = string::from_utf8(base_uri.into_inner()).map_err(|e| {239					Error::Revert(alloc::format!(240						"Can not convert value \"baseURI\" to string with error \"{}\"",241						e242					))243				})?;244				if let Ok(suffix) = get_token_property(self, token_id_u32, &key::suffix()) {245					if !suffix.is_empty() {246						return Ok(base_uri + suffix.as_str());247					}248				}249250				return Ok(base_uri + token_id.to_string().as_str());251			}252		}253254		Ok("".into())255	}256}257258/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension259/// @dev See https://eips.ethereum.org/EIPS/eip-721260#[solidity_interface(name = "ERC721Enumerable")]261impl<T: Config> RefungibleHandle<T> {262	/// @notice Enumerate valid RFTs263	/// @param index A counter less than `totalSupply()`264	/// @return The token identifier for the `index`th NFT,265	///  (sort order not specified)266	fn token_by_index(&self, index: uint256) -> Result<uint256> {267		Ok(index)268	}269270	/// Not implemented271	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {272		// TODO: Not implemetable273		Err("not implemented".into())274	}275276	/// @notice Count RFTs tracked by this contract277	/// @return A count of valid RFTs tracked by this contract, where each one of278	///  them has an assigned and queryable owner not equal to the zero address279	fn total_supply(&self) -> Result<uint256> {280		self.consume_store_reads(1)?;281		Ok(<Pallet<T>>::total_supply(self).into())282	}283}284285/// @title ERC-721 Non-Fungible Token Standard286/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md287#[solidity_interface(name = "ERC721", events(ERC721Events))]288impl<T: Config> RefungibleHandle<T> {289	/// @notice Count all RFTs assigned to an owner290	/// @dev RFTs assigned to the zero address are considered invalid, and this291	///  function throws for queries about the zero address.292	/// @param owner An address for whom to query the balance293	/// @return The number of RFTs owned by `owner`, possibly zero294	fn balance_of(&self, owner: address) -> Result<uint256> {295		self.consume_store_reads(1)?;296		let owner = T::CrossAccountId::from_eth(owner);297		let balance = <AccountBalance<T>>::get((self.id, owner));298		Ok(balance.into())299	}300301	fn owner_of(&self, token_id: uint256) -> Result<address> {302		self.consume_store_reads(2)?;303		let token = token_id.try_into()?;304		let owner = <Pallet<T>>::token_owner(self.id, token);305		Ok(owner306			.map(|address| *address.as_eth())307			.unwrap_or_else(|| H160::default()))308	}309310	/// @dev Not implemented311	fn safe_transfer_from_with_data(312		&mut self,313		_from: address,314		_to: address,315		_token_id: uint256,316		_data: bytes,317		_value: value,318	) -> Result<void> {319		// TODO: Not implemetable320		Err("not implemented".into())321	}322323	/// @dev Not implemented324	fn safe_transfer_from(325		&mut self,326		_from: address,327		_to: address,328		_token_id: uint256,329		_value: value,330	) -> Result<void> {331		// TODO: Not implemetable332		Err("not implemented".into())333	}334335	/// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE336	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE337	///  THEY MAY BE PERMANENTLY LOST338	/// @dev Throws unless `msg.sender` is the current owner or an authorized339	///  operator for this RFT. Throws if `from` is not the current owner. Throws340	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.341	///  Throws if RFT pieces have multiple owners.342	/// @param from The current owner of the NFT343	/// @param to The new owner344	/// @param tokenId The NFT to transfer345	/// @param _value Not used for an NFT346	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]347	fn transfer_from(348		&mut self,349		caller: caller,350		from: address,351		to: address,352		token_id: uint256,353		_value: value,354	) -> Result<void> {355		let caller = T::CrossAccountId::from_eth(caller);356		let from = T::CrossAccountId::from_eth(from);357		let to = T::CrossAccountId::from_eth(to);358		let token = token_id.try_into()?;359		let budget = self360			.recorder361			.weight_calls_budget(<StructureWeight<T>>::find_parent());362363		let balance = balance(&self, token, &from)?;364		ensure_single_owner(&self, token, balance)?;365366		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)367			.map_err(dispatch_to_evm::<T>)?;368369		<PalletEvm<T>>::deposit_log(370			ERC721Events::Transfer {371				from: *from.as_eth(),372				to: *to.as_eth(),373				token_id: token_id.into(),374			}375			.to_log(collection_id_to_address(self.id)),376		);377		Ok(())378	}379380	/// @dev Not implemented381	fn approve(382		&mut self,383		_caller: caller,384		_approved: address,385		_token_id: uint256,386		_value: value,387	) -> Result<void> {388		Err("not implemented".into())389	}390391	/// @dev Not implemented392	fn set_approval_for_all(393		&mut self,394		_caller: caller,395		_operator: address,396		_approved: bool,397	) -> Result<void> {398		// TODO: Not implemetable399		Err("not implemented".into())400	}401402	/// @dev Not implemented403	fn get_approved(&self, _token_id: uint256) -> Result<address> {404		// TODO: Not implemetable405		Err("not implemented".into())406	}407408	/// @dev Not implemented409	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {410		// TODO: Not implemetable411		Err("not implemented".into())412	}413}414415/// Returns amount of pieces of `token` that `owner` have416fn balance<T: Config>(417	collection: &RefungibleHandle<T>,418	token: TokenId,419	owner: &T::CrossAccountId,420) -> Result<u128> {421	collection.consume_store_reads(1)?;422	let balance = <Balance<T>>::get((collection.id, token, &owner));423	Ok(balance)424}425426/// Throws if `owner_balance` is lower than total amount of `token` pieces427fn ensure_single_owner<T: Config>(428	collection: &RefungibleHandle<T>,429	token: TokenId,430	owner_balance: u128,431) -> Result<()> {432	collection.consume_store_reads(1)?;433	let total_supply = <TotalSupply<T>>::get((collection.id, token));434	if total_supply != owner_balance {435		return Err("token has multiple owners".into());436	}437	Ok(())438}439440/// @title ERC721 Token that can be irreversibly burned (destroyed).441#[solidity_interface(name = "ERC721Burnable")]442impl<T: Config> RefungibleHandle<T> {443	/// @notice Burns a specific ERC721 token.444	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized445	///  operator of the current owner.446	/// @param tokenId The RFT to approve447	#[weight(<SelfWeightOf<T>>::burn_item_fully())]448	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {449		let caller = T::CrossAccountId::from_eth(caller);450		let token = token_id.try_into()?;451452		let balance = balance(&self, token, &caller)?;453		ensure_single_owner(&self, token, balance)?;454455		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;456		Ok(())457	}458}459460/// @title ERC721 minting logic.461#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]462impl<T: Config> RefungibleHandle<T> {463	fn minting_finished(&self) -> Result<bool> {464		Ok(false)465	}466467	/// @notice Function to mint token.468	/// @dev `tokenId` should be obtained with `nextTokenId` method,469	///  unlike standard, you can't specify it manually470	/// @param to The new owner471	/// @param tokenId ID of the minted RFT472	#[weight(<SelfWeightOf<T>>::create_item())]473	fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {474		let caller = T::CrossAccountId::from_eth(caller);475		let to = T::CrossAccountId::from_eth(to);476		let token_id: u32 = token_id.try_into()?;477		let budget = self478			.recorder479			.weight_calls_budget(<StructureWeight<T>>::find_parent());480481		if <TokensMinted<T>>::get(self.id)482			.checked_add(1)483			.ok_or("item id overflow")?484			!= token_id485		{486			return Err("item id should be next".into());487		}488489		let const_data = BoundedVec::default();490		let users = [(to.clone(), 1)]491			.into_iter()492			.collect::<BTreeMap<_, _>>()493			.try_into()494			.unwrap();495		<Pallet<T>>::create_item(496			self,497			&caller,498			CreateItemData::<T> {499				const_data,500				users,501				properties: CollectionPropertiesVec::default(),502			},503			&budget,504		)505		.map_err(dispatch_to_evm::<T>)?;506507		Ok(true)508	}509510	/// @notice Function to mint token with the given tokenUri.511	/// @dev `tokenId` should be obtained with `nextTokenId` method,512	///  unlike standard, you can't specify it manually513	/// @param to The new owner514	/// @param tokenId ID of the minted RFT515	/// @param tokenUri Token URI that would be stored in the RFT properties516	#[solidity(rename_selector = "mintWithTokenURI")]517	#[weight(<SelfWeightOf<T>>::create_item())]518	fn mint_with_token_uri(519		&mut self,520		caller: caller,521		to: address,522		token_id: uint256,523		token_uri: string,524	) -> Result<bool> {525		let key = key::url();526		let permission = get_token_permission::<T>(self.id, &key)?;527		if !permission.collection_admin {528			return Err("Operation is not allowed".into());529		}530531		let caller = T::CrossAccountId::from_eth(caller);532		let to = T::CrossAccountId::from_eth(to);533		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;534		let budget = self535			.recorder536			.weight_calls_budget(<StructureWeight<T>>::find_parent());537538		if <TokensMinted<T>>::get(self.id)539			.checked_add(1)540			.ok_or("item id overflow")?541			!= token_id542		{543			return Err("item id should be next".into());544		}545546		let mut properties = CollectionPropertiesVec::default();547		properties548			.try_push(Property {549				key,550				value: token_uri551					.into_bytes()552					.try_into()553					.map_err(|_| "token uri is too long")?,554			})555			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;556557		let const_data = BoundedVec::default();558		let users = [(to.clone(), 1)]559			.into_iter()560			.collect::<BTreeMap<_, _>>()561			.try_into()562			.unwrap();563		<Pallet<T>>::create_item(564			self,565			&caller,566			CreateItemData::<T> {567				const_data,568				users,569				properties,570			},571			&budget,572		)573		.map_err(dispatch_to_evm::<T>)?;574		Ok(true)575	}576577	/// @dev Not implemented578	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {579		Err("not implementable".into())580	}581}582583fn get_token_property<T: Config>(584	collection: &CollectionHandle<T>,585	token_id: u32,586	key: &up_data_structs::PropertyKey,587) -> Result<string> {588	collection.consume_store_reads(1)?;589	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))590		.map_err(|_| Error::Revert("Token properties not found".into()))?;591	if let Some(property) = properties.get(key) {592		return Ok(string::from_utf8_lossy(property).into());593	}594595	Err("Property tokenURI not found".into())596}597598fn is_erc721_metadata_compatible<T: Config>(collection_id: CollectionId) -> bool {599	if let Some(shema_name) =600		pallet_common::Pallet::<T>::get_collection_property(collection_id, &key::schema_name())601	{602		let shema_name = shema_name.into_inner();603		shema_name == property_value::ERC721_METADATA604	} else {605		false606	}607}608609fn get_token_permission<T: Config>(610	collection_id: CollectionId,611	key: &PropertyKey,612) -> Result<PropertyPermission> {613	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)614		.map_err(|_| Error::Revert("No permissions for collection".into()))?;615	let a = token_property_permissions616		.get(key)617		.map(Clone::clone)618		.ok_or_else(|| {619			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();620			Error::Revert(alloc::format!("No permission for key {}", key))621		})?;622	Ok(a)623}624625/// @title Unique extensions for ERC721.626#[solidity_interface(name = "ERC721UniqueExtensions")]627impl<T: Config> RefungibleHandle<T> {628	/// @notice Transfer ownership of an RFT629	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`630	///  is the zero address. Throws if `tokenId` is not a valid RFT.631	///  Throws if RFT pieces have multiple owners.632	/// @param to The new owner633	/// @param tokenId The RFT to transfer634	/// @param _value Not used for an RFT635	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]636	fn transfer(637		&mut self,638		caller: caller,639		to: address,640		token_id: uint256,641		_value: value,642	) -> Result<void> {643		let caller = T::CrossAccountId::from_eth(caller);644		let to = T::CrossAccountId::from_eth(to);645		let token = token_id.try_into()?;646		let budget = self647			.recorder648			.weight_calls_budget(<StructureWeight<T>>::find_parent());649650		let balance = balance(&self, token, &caller)?;651		ensure_single_owner(&self, token, balance)?;652653		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)654			.map_err(dispatch_to_evm::<T>)?;655		<PalletEvm<T>>::deposit_log(656			ERC721Events::Transfer {657				from: *caller.as_eth(),658				to: *to.as_eth(),659				token_id: token_id.into(),660			}661			.to_log(collection_id_to_address(self.id)),662		);663		Ok(())664	}665666	/// @notice Burns a specific ERC721 token.667	/// @dev Throws unless `msg.sender` is the current owner or an authorized668	///  operator for this RFT. Throws if `from` is not the current owner. Throws669	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.670	///  Throws if RFT pieces have multiple owners.671	/// @param from The current owner of the RFT672	/// @param tokenId The RFT to transfer673	/// @param _value Not used for an RFT674	#[weight(<SelfWeightOf<T>>::burn_from())]675	fn burn_from(676		&mut self,677		caller: caller,678		from: address,679		token_id: uint256,680		_value: value,681	) -> Result<void> {682		let caller = T::CrossAccountId::from_eth(caller);683		let from = T::CrossAccountId::from_eth(from);684		let token = token_id.try_into()?;685		let budget = self686			.recorder687			.weight_calls_budget(<StructureWeight<T>>::find_parent());688689		let balance = balance(&self, token, &caller)?;690		ensure_single_owner(&self, token, balance)?;691692		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)693			.map_err(dispatch_to_evm::<T>)?;694		Ok(())695	}696697	/// @notice Returns next free RFT ID.698	fn next_token_id(&self) -> Result<uint256> {699		self.consume_store_reads(1)?;700		Ok(<TokensMinted<T>>::get(self.id)701			.checked_add(1)702			.ok_or("item id overflow")?703			.into())704	}705706	/// @notice Function to mint multiple tokens.707	/// @dev `tokenIds` should be an array of consecutive numbers and first number708	///  should be obtained with `nextTokenId` method709	/// @param to The new owner710	/// @param tokenIds IDs of the minted RFTs711	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]712	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {713		let caller = T::CrossAccountId::from_eth(caller);714		let to = T::CrossAccountId::from_eth(to);715		let mut expected_index = <TokensMinted<T>>::get(self.id)716			.checked_add(1)717			.ok_or("item id overflow")?;718		let budget = self719			.recorder720			.weight_calls_budget(<StructureWeight<T>>::find_parent());721722		let total_tokens = token_ids.len();723		for id in token_ids.into_iter() {724			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;725			if id != expected_index {726				return Err("item id should be next".into());727			}728			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;729		}730		let const_data = BoundedVec::default();731		let users = [(to.clone(), 1)]732			.into_iter()733			.collect::<BTreeMap<_, _>>()734			.try_into()735			.unwrap();736		let create_item_data = CreateItemData::<T> {737			const_data,738			users,739			properties: CollectionPropertiesVec::default(),740		};741		let data = (0..total_tokens)742			.map(|_| create_item_data.clone())743			.collect();744745		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)746			.map_err(dispatch_to_evm::<T>)?;747		Ok(true)748	}749750	/// @notice Function to mint multiple tokens with the given tokenUris.751	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive752	///  numbers and first number should be obtained with `nextTokenId` method753	/// @param to The new owner754	/// @param tokens array of pairs of token ID and token URI for minted tokens755	#[solidity(rename_selector = "mintBulkWithTokenURI")]756	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]757	fn mint_bulk_with_token_uri(758		&mut self,759		caller: caller,760		to: address,761		tokens: Vec<(uint256, string)>,762	) -> Result<bool> {763		let key = key::url();764		let caller = T::CrossAccountId::from_eth(caller);765		let to = T::CrossAccountId::from_eth(to);766		let mut expected_index = <TokensMinted<T>>::get(self.id)767			.checked_add(1)768			.ok_or("item id overflow")?;769		let budget = self770			.recorder771			.weight_calls_budget(<StructureWeight<T>>::find_parent());772773		let mut data = Vec::with_capacity(tokens.len());774		let const_data = BoundedVec::default();775		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]776			.into_iter()777			.collect::<BTreeMap<_, _>>()778			.try_into()779			.unwrap();780		for (id, token_uri) in tokens {781			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;782			if id != expected_index {783				return Err("item id should be next".into());784			}785			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;786787			let mut properties = CollectionPropertiesVec::default();788			properties789				.try_push(Property {790					key: key.clone(),791					value: token_uri792						.into_bytes()793						.try_into()794						.map_err(|_| "token uri is too long")?,795				})796				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;797798			let create_item_data = CreateItemData::<T> {799				const_data: const_data.clone(),800				users: users.clone(),801				properties,802			};803			data.push(create_item_data);804		}805806		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)807			.map_err(dispatch_to_evm::<T>)?;808		Ok(true)809	}810}811812#[solidity_interface(813	name = "UniqueRefungible",814	is(815		ERC721,816		ERC721Metadata,817		ERC721Enumerable,818		ERC721UniqueExtensions,819		ERC721Mintable,820		ERC721Burnable,821		via("CollectionHandle<T>", common_mut, Collection),822		TokenProperties,823	)824)]825impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}826827// Not a tests, but code generators828generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);829generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);830831impl<T: Config> CommonEvmHandler for RefungibleHandle<T>832where833	T::AccountId: From<[u8; 32]>,834{835	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");836	fn call(837		self,838		handle: &mut impl PrecompileHandle,839	) -> Option<pallet_common::erc::PrecompileResult> {840		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)841	}842}
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -454,6 +454,14 @@
 			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
 			Self::burn_token_unchecked(collection, token)?;
+			<PalletEvm<T>>::deposit_log(
+				ERC721Events::Transfer {
+					from: *owner.as_eth(),
+					to: H160::default(),
+					token_id: token.into(),
+				}
+				.to_log(collection_id_to_address(collection.id)),
+			);
 			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
 				collection.id,
 				token,
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -51,44 +51,15 @@
 	event MintingFinished();
 }
 
-// Selector: 0784ee64
-contract ERC721UniqueExtensions is Dummy, ERC165 {
-	// @notice Returns next free RFT ID.
-	//
-	// Selector: nextTokenId() 75794a3c
-	function nextTokenId() public view returns (uint256) {
-		require(false, stub_error);
-		dummy;
-		return 0;
-	}
-
-	// Selector: mintBulk(address,uint256[]) 44a9945e
-	function mintBulk(address to, uint256[] memory tokenIds)
-		public
-		returns (bool)
-	{
-		require(false, stub_error);
-		to;
-		tokenIds;
-		dummy = 0;
-		return false;
-	}
-
-	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
-	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
-		public
-		returns (bool)
-	{
-		require(false, stub_error);
-		to;
-		tokens;
-		dummy = 0;
-		return false;
-	}
-}
-
 // Selector: 41369377
 contract TokenProperties is Dummy, ERC165 {
+	// @notice Set permissions for token property.
+	// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// @param key Property key.
+	// @param is_mutable Permission to mutate property.
+	// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	// @param token_owner Permission to mutate property by token owner if property is mutable.
+	//
 	// Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa
 	function setTokenPropertyPermission(
 		string memory key,
@@ -104,6 +75,12 @@
 		dummy = 0;
 	}
 
+	// @notice Set token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @param value Property value.
+	//
 	// Selector: setProperty(uint256,string,bytes) 1752d67b
 	function setProperty(
 		uint256 tokenId,
@@ -117,6 +94,11 @@
 		dummy = 0;
 	}
 
+	// @notice Delete token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	//
 	// Selector: deleteProperty(uint256,string) 066111d1
 	function deleteProperty(uint256 tokenId, string memory key) public {
 		require(false, stub_error);
@@ -125,7 +107,11 @@
 		dummy = 0;
 	}
 
-	// Throws error if key not found
+	// @notice Get token property value.
+	// @dev Throws error if key not found
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @return Property value bytes
 	//
 	// Selector: property(uint256,string) 7228c327
 	function property(uint256 tokenId, string memory key)
@@ -143,7 +129,10 @@
 
 // Selector: 42966c68
 contract ERC721Burnable is Dummy, ERC165 {
-	// @dev Not implemented
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized
+	//  operator of the current owner.
+	// @param tokenId The RFT to approve
 	//
 	// Selector: burn(uint256) 42966c68
 	function burn(uint256 tokenId) public {
@@ -155,6 +144,12 @@
 
 // Selector: 58800161
 contract ERC721 is Dummy, ERC165, ERC721Events {
+	// @notice Count all RFTs assigned to an owner
+	// @dev RFTs assigned to the zero address are considered invalid, and this
+	//  function throws for queries about the zero address.
+	// @param owner An address for whom to query the balance
+	// @return The number of RFTs owned by `owner`, possibly zero
+	//
 	// Selector: balanceOf(address) 70a08231
 	function balanceOf(address owner) public view returns (uint256) {
 		require(false, stub_error);
@@ -203,7 +198,17 @@
 		dummy = 0;
 	}
 
-	// @dev Not implemented
+	// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE
+	//  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	//  THEY MAY BE PERMANENTLY LOST
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the NFT
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
 	//
 	// Selector: transferFrom(address,address,uint256) 23b872dd
 	function transferFrom(
@@ -266,6 +271,8 @@
 
 // Selector: 5b5e139f
 contract ERC721Metadata is Dummy, ERC165 {
+	// @notice A descriptive name for a collection of RFTs in this contract
+	//
 	// Selector: name() 06fdde03
 	function name() public view returns (string memory) {
 		require(false, stub_error);
@@ -273,6 +280,8 @@
 		return "";
 	}
 
+	// @notice An abbreviated name for RFTs in this contract
+	//
 	// Selector: symbol() 95d89b41
 	function symbol() public view returns (string memory) {
 		require(false, stub_error);
@@ -280,7 +289,15 @@
 		return "";
 	}
 
-	// Returns token's const_metadata
+	// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	//
+	// @dev If the token has a `url` property and it is not empty, it is returned.
+	//  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`.
+	//  If the collection property `baseURI` is empty or absent, return "" (empty string)
+	//  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix
+	//  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
+	//
+	// @return token's const_metadata
 	//
 	// Selector: tokenURI(uint256) c87b56dd
 	function tokenURI(uint256 tokenId) public view returns (string memory) {
@@ -300,8 +317,11 @@
 		return false;
 	}
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
 	//
 	// Selector: mint(address,uint256) 40c10f19
 	function mint(address to, uint256 tokenId) public returns (bool) {
@@ -312,8 +332,12 @@
 		return false;
 	}
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token with the given tokenUri.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
+	// @param tokenUri Token URI that would be stored in the RFT properties
 	//
 	// Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
 	function mintWithTokenURI(
@@ -341,6 +365,11 @@
 
 // Selector: 780e9d63
 contract ERC721Enumerable is Dummy, ERC165 {
+	// @notice Enumerate valid RFTs
+	// @param index A counter less than `totalSupply()`
+	// @return The token identifier for the `index`th NFT,
+	//  (sort order not specified)
+	//
 	// Selector: tokenByIndex(uint256) 4f6ccce7
 	function tokenByIndex(uint256 index) public view returns (uint256) {
 		require(false, stub_error);
@@ -364,6 +393,10 @@
 		return 0;
 	}
 
+	// @notice Count RFTs tracked by this contract
+	// @return A count of valid RFTs tracked by this contract, where each one of
+	//  them has an assigned and queryable owner not equal to the zero address
+	//
 	// Selector: totalSupply() 18160ddd
 	function totalSupply() public view returns (uint256) {
 		require(false, stub_error);
@@ -599,6 +632,87 @@
 	}
 }
 
+// Selector: d74d154f
+contract ERC721UniqueExtensions is Dummy, ERC165 {
+	// @notice Transfer ownership of an RFT
+	// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	//  is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param to The new owner
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 tokenId) public {
+		require(false, stub_error);
+		to;
+		tokenId;
+		dummy = 0;
+	}
+
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the RFT
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 tokenId) public {
+		require(false, stub_error);
+		from;
+		tokenId;
+		dummy = 0;
+	}
+
+	// @notice Returns next free RFT ID.
+	//
+	// Selector: nextTokenId() 75794a3c
+	function nextTokenId() public view returns (uint256) {
+		require(false, stub_error);
+		dummy;
+		return 0;
+	}
+
+	// @notice Function to mint multiple tokens.
+	// @dev `tokenIds` should be an array of consecutive numbers and first number
+	//  should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokenIds IDs of the minted RFTs
+	//
+	// Selector: mintBulk(address,uint256[]) 44a9945e
+	function mintBulk(address to, uint256[] memory tokenIds)
+		public
+		returns (bool)
+	{
+		require(false, stub_error);
+		to;
+		tokenIds;
+		dummy = 0;
+		return false;
+	}
+
+	// @notice Function to mint multiple tokens with the given tokenUris.
+	// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	//  numbers and first number should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokens array of pairs of token ID and token URI for minted tokens
+	//
+	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
+	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
+		public
+		returns (bool)
+	{
+		require(false, stub_error);
+		to;
+		tokens;
+		dummy = 0;
+		return false;
+	}
+}
+
 contract UniqueRefungible is
 	Dummy,
 	ERC165,
modifiedpallets/refungible/src/stubs/UniqueRefungibleToken.rawdiffbeforeafterboth

binary blob — no preview

modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -42,26 +42,15 @@
 	event MintingFinished();
 }
 
-// Selector: 0784ee64
-interface ERC721UniqueExtensions is Dummy, ERC165 {
-	// @notice Returns next free RFT ID.
-	//
-	// Selector: nextTokenId() 75794a3c
-	function nextTokenId() external view returns (uint256);
-
-	// Selector: mintBulk(address,uint256[]) 44a9945e
-	function mintBulk(address to, uint256[] memory tokenIds)
-		external
-		returns (bool);
-
-	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
-	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
-		external
-		returns (bool);
-}
-
 // Selector: 41369377
 interface TokenProperties is Dummy, ERC165 {
+	// @notice Set permissions for token property.
+	// @dev Throws error if `msg.sender` is not admin or owner of the collection.
+	// @param key Property key.
+	// @param is_mutable Permission to mutate property.
+	// @param collection_admin Permission to mutate property by collection admin if property is mutable.
+	// @param token_owner Permission to mutate property by token owner if property is mutable.
+	//
 	// Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa
 	function setTokenPropertyPermission(
 		string memory key,
@@ -70,6 +59,12 @@
 		bool tokenOwner
 	) external;
 
+	// @notice Set token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @param value Property value.
+	//
 	// Selector: setProperty(uint256,string,bytes) 1752d67b
 	function setProperty(
 		uint256 tokenId,
@@ -77,10 +72,19 @@
 		bytes memory value
 	) external;
 
+	// @notice Delete token property value.
+	// @dev Throws error if `msg.sender` has no permission to edit the property.
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	//
 	// Selector: deleteProperty(uint256,string) 066111d1
 	function deleteProperty(uint256 tokenId, string memory key) external;
 
-	// Throws error if key not found
+	// @notice Get token property value.
+	// @dev Throws error if key not found
+	// @param tokenId ID of the token.
+	// @param key Property key.
+	// @return Property value bytes
 	//
 	// Selector: property(uint256,string) 7228c327
 	function property(uint256 tokenId, string memory key)
@@ -91,7 +95,10 @@
 
 // Selector: 42966c68
 interface ERC721Burnable is Dummy, ERC165 {
-	// @dev Not implemented
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized
+	//  operator of the current owner.
+	// @param tokenId The RFT to approve
 	//
 	// Selector: burn(uint256) 42966c68
 	function burn(uint256 tokenId) external;
@@ -99,6 +106,12 @@
 
 // Selector: 58800161
 interface ERC721 is Dummy, ERC165, ERC721Events {
+	// @notice Count all RFTs assigned to an owner
+	// @dev RFTs assigned to the zero address are considered invalid, and this
+	//  function throws for queries about the zero address.
+	// @param owner An address for whom to query the balance
+	// @return The number of RFTs owned by `owner`, possibly zero
+	//
 	// Selector: balanceOf(address) 70a08231
 	function balanceOf(address owner) external view returns (uint256);
 
@@ -124,7 +137,17 @@
 		uint256 tokenId
 	) external;
 
-	// @dev Not implemented
+	// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE
+	//  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
+	//  THEY MAY BE PERMANENTLY LOST
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the NFT
+	// @param to The new owner
+	// @param tokenId The NFT to transfer
+	// @param _value Not used for an NFT
 	//
 	// Selector: transferFrom(address,address,uint256) 23b872dd
 	function transferFrom(
@@ -159,13 +182,25 @@
 
 // Selector: 5b5e139f
 interface ERC721Metadata is Dummy, ERC165 {
+	// @notice A descriptive name for a collection of RFTs in this contract
+	//
 	// Selector: name() 06fdde03
 	function name() external view returns (string memory);
 
+	// @notice An abbreviated name for RFTs in this contract
+	//
 	// Selector: symbol() 95d89b41
 	function symbol() external view returns (string memory);
 
-	// Returns token's const_metadata
+	// @notice A distinct Uniform Resource Identifier (URI) for a given asset.
+	//
+	// @dev If the token has a `url` property and it is not empty, it is returned.
+	//  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`.
+	//  If the collection property `baseURI` is empty or absent, return "" (empty string)
+	//  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix
+	//  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
+	//
+	// @return token's const_metadata
 	//
 	// Selector: tokenURI(uint256) c87b56dd
 	function tokenURI(uint256 tokenId) external view returns (string memory);
@@ -176,14 +211,21 @@
 	// Selector: mintingFinished() 05d2035b
 	function mintingFinished() external view returns (bool);
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
 	//
 	// Selector: mint(address,uint256) 40c10f19
 	function mint(address to, uint256 tokenId) external returns (bool);
 
-	// `token_id` should be obtained with `next_token_id` method,
-	// unlike standard, you can't specify it manually
+	// @notice Function to mint token with the given tokenUri.
+	// @dev `tokenId` should be obtained with `nextTokenId` method,
+	//  unlike standard, you can't specify it manually
+	// @param to The new owner
+	// @param tokenId ID of the minted RFT
+	// @param tokenUri Token URI that would be stored in the RFT properties
 	//
 	// Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
 	function mintWithTokenURI(
@@ -200,6 +242,11 @@
 
 // Selector: 780e9d63
 interface ERC721Enumerable is Dummy, ERC165 {
+	// @notice Enumerate valid RFTs
+	// @param index A counter less than `totalSupply()`
+	// @return The token identifier for the `index`th NFT,
+	//  (sort order not specified)
+	//
 	// Selector: tokenByIndex(uint256) 4f6ccce7
 	function tokenByIndex(uint256 index) external view returns (uint256);
 
@@ -211,6 +258,10 @@
 		view
 		returns (uint256);
 
+	// @notice Count RFTs tracked by this contract
+	// @return A count of valid RFTs tracked by this contract, where each one of
+	//  them has an assigned and queryable owner not equal to the zero address
+	//
 	// Selector: totalSupply() 18160ddd
 	function totalSupply() external view returns (uint256);
 }
@@ -363,6 +414,59 @@
 	function setCollectionMintMode(bool mode) external;
 }
 
+// Selector: d74d154f
+interface ERC721UniqueExtensions is Dummy, ERC165 {
+	// @notice Transfer ownership of an RFT
+	// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	//  is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param to The new owner
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: transfer(address,uint256) a9059cbb
+	function transfer(address to, uint256 tokenId) external;
+
+	// @notice Burns a specific ERC721 token.
+	// @dev Throws unless `msg.sender` is the current owner or an authorized
+	//  operator for this RFT. Throws if `from` is not the current owner. Throws
+	//  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
+	//  Throws if RFT pieces have multiple owners.
+	// @param from The current owner of the RFT
+	// @param tokenId The RFT to transfer
+	// @param _value Not used for an RFT
+	//
+	// Selector: burnFrom(address,uint256) 79cc6790
+	function burnFrom(address from, uint256 tokenId) external;
+
+	// @notice Returns next free RFT ID.
+	//
+	// Selector: nextTokenId() 75794a3c
+	function nextTokenId() external view returns (uint256);
+
+	// @notice Function to mint multiple tokens.
+	// @dev `tokenIds` should be an array of consecutive numbers and first number
+	//  should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokenIds IDs of the minted RFTs
+	//
+	// Selector: mintBulk(address,uint256[]) 44a9945e
+	function mintBulk(address to, uint256[] memory tokenIds)
+		external
+		returns (bool);
+
+	// @notice Function to mint multiple tokens with the given tokenUris.
+	// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
+	//  numbers and first number should be obtained with `nextTokenId` method
+	// @param to The new owner
+	// @param tokens array of pairs of token ID and token URI for minted tokens
+	//
+	// Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
+	function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
+		external
+		returns (bool);
+}
+
 interface UniqueRefungible is
 	Dummy,
 	ERC165,
modifiedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/reFungible.test.ts
+++ b/tests/src/eth/reFungible.test.ts
@@ -14,8 +14,8 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {createCollectionExpectSuccess} from '../util/helpers';
-import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, tokenIdToAddress} from './util/helpers';
+import {createCollectionExpectSuccess, UNIQUE} from '../util/helpers';
+import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, recordEthFee, tokenIdToAddress} from './util/helpers';
 import reFungibleAbi from './reFungibleAbi.json';
 import reFungibleTokenAbi from './reFungibleTokenAbi.json';
 import {expect} from 'chai';
@@ -26,7 +26,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
     const nextTokenId = await contract.methods.nextTokenId().call();
     await contract.methods.mint(caller, nextTokenId).send();
     const totalSupply = await contract.methods.totalSupply().call();
@@ -38,7 +38,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     {
       const nextTokenId = await contract.methods.nextTokenId().call();
@@ -63,7 +63,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     const tokenId = await contract.methods.nextTokenId().call();
     await contract.methods.mint(caller, tokenId).send();
@@ -79,7 +79,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     const tokenId = await contract.methods.nextTokenId().call();
     await contract.methods.mint(caller, tokenId).send();
@@ -105,7 +105,7 @@
     let result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
     const receiver = createEthAccount(web3);
-    const contract = evmCollection(web3, owner, collectionIdAddress);
+    const contract = evmCollection(web3, owner, collectionIdAddress, {type: 'ReFungible'});
     const nextTokenId = await contract.methods.nextTokenId().call();
 
     expect(nextTokenId).to.be.equal('1');
@@ -137,7 +137,7 @@
     const helper = evmCollectionHelpers(web3, caller);
     const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
     const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
 
     const receiver = createEthAccount(web3);
 
@@ -189,8 +189,148 @@
       expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');
     }
   });
+
+  itWeb3('Can perform burn()', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+    {
+      const result = await contract.methods.burn(tokenId).send();
+      const events = normalizeEvents(result.events);
+
+      expect(events).to.be.deep.equal([
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: caller,
+            to: '0x0000000000000000000000000000000000000000',
+            tokenId: tokenId.toString(),
+          },
+        },
+      ]);
+    }
+  });
+
+  itWeb3('Can perform transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+    {
+      const result = await contract.methods.transferFrom(caller, receiver, tokenId).send();
+      const events = normalizeEvents(result.events);
+      expect(events).to.include.deep.members([
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: caller,
+            to: receiver,
+            tokenId: tokenId.toString(),
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(1);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(caller).call();
+      expect(+balance).to.equal(0);
+    }
+  });
+
+  itWeb3('Can perform transfer()', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    {
+      const result = await contract.methods.transfer(receiver, tokenId).send();
+      const events = normalizeEvents(result.events);
+      expect(events).to.include.deep.members([
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: caller,
+            to: receiver,
+            tokenId: tokenId.toString(),
+          },
+        },
+      ]);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(caller).call();
+      expect(+balance).to.equal(0);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(1);
+    }
+  });
 });
 
+describe('RFT: Fees', () => {
+  itWeb3('transferFrom() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const cost = await recordEthFee(api, caller, () => contract.methods.transferFrom(caller, receiver, tokenId).send());
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+    expect(cost > 0n);
+  });
+
+  itWeb3('transfer() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, caller);
+    const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+    const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+    const contract = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const receiver = createEthAccount(web3);
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const cost = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send());
+    expect(cost < BigInt(0.2 * Number(UNIQUE)));
+    expect(cost > 0n);
+  });
+});
+
 describe('Common metadata', () => {
   itWeb3('Returns collection name', async ({api, web3, privateKeyWrapper}) => {
     const collection = await createCollectionExpectSuccess({
@@ -200,7 +340,7 @@
     const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
 
     const address = collectionIdToAddress(collection);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
     const name = await contract.methods.name().call();
 
     expect(name).to.equal('token name');
@@ -214,7 +354,7 @@
     const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
 
     const address = collectionIdToAddress(collection);
-    const contract = new web3.eth.Contract(reFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
     const symbol = await contract.methods.symbol().call();
 
     expect(symbol).to.equal('TOK');
modifiedtests/src/eth/reFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/reFungibleAbi.json
+++ b/tests/src/eth/reFungibleAbi.json
@@ -136,6 +136,16 @@
     "type": "function"
   },
   {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "burnFrom",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
     "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
     "name": "collectionProperty",
     "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
@@ -498,6 +508,16 @@
   },
   {
     "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "transfer",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
       { "internalType": "address", "name": "from", "type": "address" },
       { "internalType": "address", "name": "to", "type": "address" },
       { "internalType": "uint256", "name": "tokenId", "type": "uint256" }