git.delta.rocks / unique-network / refs/commits / 3203150b5c74

difftreelog

Merge remote-tracking branch 'origin/feature/erc-721-for-refungible' into develop

Yaroslav Bolyukin2022-08-04parents: #f5cb854 #708f63e.patch.diff
in: master

13 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6311,7 +6311,7 @@
 
 [[package]]
 name = "pallet-refungible"
-version = "0.2.0"
+version = "0.2.1"
 dependencies = [
  "ethereum",
  "evm-coder",
modifiedMakefilediffbeforeafterboth
--- a/Makefile
+++ b/Makefile
@@ -15,8 +15,9 @@
 NONFUNGIBLE_EVM_STUBS=./pallets/nonfungible/src/stubs
 NONFUNGIBLE_EVM_ABI=./tests/src/eth/nonFungibleAbi.json
 
-RENFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json
-RENFUNGIBLE_TOKEN_EVM_ABI=./tests/src/eth/reFungibleTokenAbi.json
+REFUNGIBLE_EVM_STUBS=./pallets/refungible/src/stubs
+REFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json
+REFUNGIBLE_TOKEN_EVM_ABI=./tests/src/eth/reFungibleTokenAbi.json
 
 CONTRACT_HELPERS_STUBS=./pallets/evm-contract-helpers/src/stubs/
 CONTRACT_HELPERS_ABI=./tests/src/eth/util/contractHelpersAbi.json
@@ -36,7 +37,7 @@
 UniqueNFT.sol:
 	PACKAGE=pallet-nonfungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
 	PACKAGE=pallet-nonfungible NAME=erc::gen_impl OUTPUT=$(NONFUNGIBLE_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
@@ -61,14 +62,14 @@
 	INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/UniqueNFT.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
 
-UniqueRefungibleToken: UniqueRefungibleToken.sol
-	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungibleToken.raw ./.maintain/scripts/compile_stub.sh
-	INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(RENFUNGIBLE_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
 
+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
+
 ContractHelpers: ContractHelpers.sol
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_STUBS)/ContractHelpers.raw ./.maintain/scripts/compile_stub.sh
 	INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_ABI) ./.maintain/scripts/generate_abi.sh
modifiedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -2,8 +2,18 @@
 
 All notable changes to this project will be documented in this file.
 
+## [v0.2.1] - 2022-07-27
+
+### New features
+
+Implementation of ERC-721 EVM API ([#452](https://github.com/UniqueNetwork/unique-chain/pull/452))
+
 ## [v0.2.0] - 2022-08-01
+
 ### Deprecated
+
+`const_data` field is removed
+
 - `ItemData`
 - `TokenData`
 
@@ -15,11 +25,13 @@
 test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
 
 ## [v0.1.1] - 2022-07-14
-### Added
+
+### Added features
+
 - Support for properties for RFT collections and tokens.
 
 ### Other changes
 
 - feat: RPC method `token_owners` returning 10 owners in no particular order.
 
-This was an internal request to improve the web interface and support fractionalization event. 
+This was an internal request to improve the web interface and support fractionalization event.
modifiedpallets/refungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/refungible/Cargo.toml
+++ b/pallets/refungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-refungible"
-version = "0.2.0"
+version = "0.2.1"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -14,19 +14,797 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
+//! # Refungible Pallet EVM API for tokens
+//!
+//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.
+//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.
+
 extern crate alloc;
-use evm_coder::{generate_stubgen, solidity_interface, types::*};
 
-use pallet_common::{CollectionHandle, erc::CollectionCall, erc::CommonEvmHandler};
+use alloc::string::ToString;
+use core::{
+	char::{REPLACEMENT_CHARACTER, decode_utf16},
+	convert::TryInto,
+};
+use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};
+use frame_support::{BoundedBTreeMap, BoundedVec};
+use pallet_common::{
+	CollectionHandle, CollectionPropertyPermissions,
+	erc::{
+		CommonEvmHandler, CollectionCall,
+		static_property::{key, value as property_value},
+	},
+};
+use pallet_evm::{account::CrossAccountId, PrecompileHandle};
+use pallet_evm_coder_substrate::{call, dispatch_to_evm};
+use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
+use sp_core::H160;
+use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};
+use up_data_structs::{
+	CollectionId, CollectionPropertiesVec, Property, PropertyKey, PropertyKeyPermission,
+	PropertyPermission, TokenId,
+};
+
+use crate::{
+	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,
+	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,
+};
+
+pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);
+
+/// @title A contract that allows to set and delete token properties and change token property permissions.
+#[solidity_interface(name = "TokenProperties")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @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.
+	fn set_token_property_permission(
+		&mut self,
+		caller: caller,
+		key: string,
+		is_mutable: bool,
+		collection_admin: bool,
+		token_owner: bool,
+	) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		<Pallet<T>>::set_token_property_permissions(
+			self,
+			&caller,
+			vec![PropertyKeyPermission {
+				key: <Vec<u8>>::from(key)
+					.try_into()
+					.map_err(|_| "too long key")?,
+				permission: PropertyPermission {
+					mutable: is_mutable,
+					collection_admin,
+					token_owner,
+				},
+			}],
+		)
+		.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @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.
+	fn set_property(
+		&mut self,
+		caller: caller,
+		token_id: uint256,
+		key: string,
+		value: bytes,
+	) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+		let key = <Vec<u8>>::from(key)
+			.try_into()
+			.map_err(|_| "key too long")?;
+		let value = value.try_into().map_err(|_| "value too long")?;
+
+		let nesting_budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::set_token_property(
+			self,
+			&caller,
+			TokenId(token_id),
+			Property { key, value },
+			&nesting_budget,
+		)
+		.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @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.
+	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+		let key = <Vec<u8>>::from(key)
+			.try_into()
+			.map_err(|_| "key too long")?;
+
+		let nesting_budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)
+			.map_err(dispatch_to_evm::<T>)
+	}
+
+	/// @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
+	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+		let key = <Vec<u8>>::from(key)
+			.try_into()
+			.map_err(|_| "key too long")?;
+
+		let props = <TokenProperties<T>>::get((self.id, token_id));
+		let prop = props.get(&key).ok_or("key not found")?;
+
+		Ok(prop.to_vec())
+	}
+}
+
+#[derive(ToLog)]
+pub enum ERC721Events {
+	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed
+	///  (`to` == 0). Exception: during contract creation, any number of RFTs
+	///  may be created and assigned without emitting Transfer.
+	Transfer {
+		#[indexed]
+		from: address,
+		#[indexed]
+		to: address,
+		#[indexed]
+		token_id: uint256,
+	},
+	/// @dev Not supported
+	Approval {
+		#[indexed]
+		owner: address,
+		#[indexed]
+		approved: address,
+		#[indexed]
+		token_id: uint256,
+	},
+	/// @dev Not supported
+	#[allow(dead_code)]
+	ApprovalForAll {
+		#[indexed]
+		owner: address,
+		#[indexed]
+		operator: address,
+		approved: bool,
+	},
+}
+
+#[derive(ToLog)]
+pub enum ERC721MintableEvents {
+	/// @dev Not supported
+	#[allow(dead_code)]
+	MintingFinished {},
+}
+
+#[solidity_interface(name = "ERC721Metadata")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice A descriptive name for a collection of RFTs in this contract
+	fn name(&self) -> Result<string> {
+		Ok(decode_utf16(self.name.iter().copied())
+			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
+			.collect::<string>())
+	}
+
+	/// @notice An abbreviated name for RFTs in this contract
+	fn symbol(&self) -> Result<string> {
+		Ok(string::from_utf8_lossy(&self.token_prefix).into())
+	}
+
+	/// @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
+	#[solidity(rename_selector = "tokenURI")]
+	fn token_uri(&self, token_id: uint256) -> Result<string> {
+		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+
+		if let Ok(url) = get_token_property(self, token_id_u32, &key::url()) {
+			if !url.is_empty() {
+				return Ok(url);
+			}
+		} else if !is_erc721_metadata_compatible::<T>(self.id) {
+			return Err("tokenURI not set".into());
+		}
+
+		if let Some(base_uri) =
+			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())
+		{
+			if !base_uri.is_empty() {
+				let base_uri = string::from_utf8(base_uri.into_inner()).map_err(|e| {
+					Error::Revert(alloc::format!(
+						"Can not convert value \"baseURI\" to string with error \"{}\"",
+						e
+					))
+				})?;
+				if let Ok(suffix) = get_token_property(self, token_id_u32, &key::suffix()) {
+					if !suffix.is_empty() {
+						return Ok(base_uri + suffix.as_str());
+					}
+				}
+
+				return Ok(base_uri + token_id.to_string().as_str());
+			}
+		}
+
+		Ok("".into())
+	}
+}
+
+/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
+/// @dev See https://eips.ethereum.org/EIPS/eip-721
+#[solidity_interface(name = "ERC721Enumerable")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @notice Enumerate valid RFTs
+	/// @param index A counter less than `totalSupply()`
+	/// @return The token identifier for the `index`th NFT,
+	///  (sort order not specified)
+	fn token_by_index(&self, index: uint256) -> Result<uint256> {
+		Ok(index)
+	}
+
+	/// Not implemented
+	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @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
+	fn total_supply(&self) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		Ok(<Pallet<T>>::total_supply(self).into())
+	}
+}
+
+/// @title ERC-721 Non-Fungible Token Standard
+/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
+#[solidity_interface(name = "ERC721", events(ERC721Events))]
+impl<T: Config> RefungibleHandle<T> {
+	/// @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
+	fn balance_of(&self, owner: address) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		let owner = T::CrossAccountId::from_eth(owner);
+		let balance = <AccountBalance<T>>::get((self.id, owner));
+		Ok(balance.into())
+	}
+
+	/// @notice Find the owner of an RFT
+	/// @dev RFTs assigned to zero address are considered invalid, and queries
+	///  about them do throw.
+	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for
+	///  the tokens that are partially owned.
+	/// @param tokenId The identifier for an RFT
+	/// @return The address of the owner of the RFT
+	fn owner_of(&self, token_id: uint256) -> Result<address> {
+		self.consume_store_reads(2)?;
+		let token = token_id.try_into()?;
+		let owner = <Pallet<T>>::token_owner(self.id, token);
+		Ok(owner
+			.map(|address| *address.as_eth())
+			.unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))
+	}
+
+	/// @dev Not implemented
+	fn safe_transfer_from_with_data(
+		&mut self,
+		_from: address,
+		_to: address,
+		_token_id: uint256,
+		_data: bytes,
+		_value: value,
+	) -> Result<void> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn safe_transfer_from(
+		&mut self,
+		_from: address,
+		_to: address,
+		_token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @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
+	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]
+	fn transfer_from(
+		&mut self,
+		caller: caller,
+		from: address,
+		to: address,
+		token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let to = T::CrossAccountId::from_eth(to);
+		let token = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let balance = balance(&self, token, &from)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+
+		Ok(())
+	}
+
+	/// @dev Not implemented
+	fn approve(
+		&mut self,
+		_caller: caller,
+		_approved: address,
+		_token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn set_approval_for_all(
+		&mut self,
+		_caller: caller,
+		_operator: address,
+		_approved: bool,
+	) -> Result<void> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn get_approved(&self, _token_id: uint256) -> Result<address> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+
+	/// @dev Not implemented
+	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {
+		// TODO: Not implemetable
+		Err("not implemented".into())
+	}
+}
+
+/// Returns amount of pieces of `token` that `owner` have
+fn balance<T: Config>(
+	collection: &RefungibleHandle<T>,
+	token: TokenId,
+	owner: &T::CrossAccountId,
+) -> Result<u128> {
+	collection.consume_store_reads(1)?;
+	let balance = <Balance<T>>::get((collection.id, token, &owner));
+	Ok(balance)
+}
+
+/// Throws if `owner_balance` is lower than total amount of `token` pieces
+fn ensure_single_owner<T: Config>(
+	collection: &RefungibleHandle<T>,
+	token: TokenId,
+	owner_balance: u128,
+) -> Result<()> {
+	collection.consume_store_reads(1)?;
+	let total_supply = <TotalSupply<T>>::get((collection.id, token));
+	if total_supply != owner_balance {
+		return Err("token has multiple owners".into());
+	}
+	Ok(())
+}
+
+/// @title ERC721 Token that can be irreversibly burned (destroyed).
+#[solidity_interface(name = "ERC721Burnable")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @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
+	#[weight(<SelfWeightOf<T>>::burn_item_fully())]
+	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token = token_id.try_into()?;
+
+		let balance = balance(&self, token, &caller)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;
+		Ok(())
+	}
+}
+
+/// @title ERC721 minting logic.
+#[solidity_interface(name = "ERC721Mintable", events(ERC721MintableEvents))]
+impl<T: Config> RefungibleHandle<T> {
+	fn minting_finished(&self) -> Result<bool> {
+		Ok(false)
+	}
+
+	/// @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
+	#[weight(<SelfWeightOf<T>>::create_item())]
+	fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token_id: u32 = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		if <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			!= token_id
+		{
+			return Err("item id should be next".into());
+		}
+
+		let users = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		<Pallet<T>>::create_item(
+			self,
+			&caller,
+			CreateItemData::<T> {
+				users,
+				properties: CollectionPropertiesVec::default(),
+			},
+			&budget,
+		)
+		.map_err(dispatch_to_evm::<T>)?;
+
+		Ok(true)
+	}
+
+	/// @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
+	#[solidity(rename_selector = "mintWithTokenURI")]
+	#[weight(<SelfWeightOf<T>>::create_item())]
+	fn mint_with_token_uri(
+		&mut self,
+		caller: caller,
+		to: address,
+		token_id: uint256,
+		token_uri: string,
+	) -> Result<bool> {
+		let key = key::url();
+		let permission = get_token_permission::<T>(self.id, &key)?;
+		if !permission.collection_admin {
+			return Err("Operation is not allowed".into());
+		}
+
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
 
-use pallet_evm::PrecompileHandle;
-use pallet_evm_coder_substrate::call;
+		if <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			!= token_id
+		{
+			return Err("item id should be next".into());
+		}
 
-use crate::{Config, RefungibleHandle};
+		let mut properties = CollectionPropertiesVec::default();
+		properties
+			.try_push(Property {
+				key,
+				value: token_uri
+					.into_bytes()
+					.try_into()
+					.map_err(|_| "token uri is too long")?,
+			})
+			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;
 
+		let users = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		<Pallet<T>>::create_item(
+			self,
+			&caller,
+			CreateItemData::<T> {
+				users,
+				properties,
+			},
+			&budget,
+		)
+		.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @dev Not implemented
+	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {
+		Err("not implementable".into())
+	}
+}
+
+fn get_token_property<T: Config>(
+	collection: &CollectionHandle<T>,
+	token_id: u32,
+	key: &up_data_structs::PropertyKey,
+) -> Result<string> {
+	collection.consume_store_reads(1)?;
+	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))
+		.map_err(|_| Error::Revert("Token properties not found".into()))?;
+	if let Some(property) = properties.get(key) {
+		return Ok(string::from_utf8_lossy(property).into());
+	}
+
+	Err("Property tokenURI not found".into())
+}
+
+fn is_erc721_metadata_compatible<T: Config>(collection_id: CollectionId) -> bool {
+	if let Some(shema_name) =
+		pallet_common::Pallet::<T>::get_collection_property(collection_id, &key::schema_name())
+	{
+		let shema_name = shema_name.into_inner();
+		shema_name == property_value::ERC721_METADATA
+	} else {
+		false
+	}
+}
+
+fn get_token_permission<T: Config>(
+	collection_id: CollectionId,
+	key: &PropertyKey,
+) -> Result<PropertyPermission> {
+	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)
+		.map_err(|_| Error::Revert("No permissions for collection".into()))?;
+	let a = token_property_permissions
+		.get(key)
+		.map(Clone::clone)
+		.ok_or_else(|| {
+			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();
+			Error::Revert(alloc::format!("No permission for key {}", key))
+		})?;
+	Ok(a)
+}
+
+/// @title Unique extensions for ERC721.
+#[solidity_interface(name = "ERC721UniqueExtensions")]
+impl<T: Config> RefungibleHandle<T> {
+	/// @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
+	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]
+	fn transfer(
+		&mut self,
+		caller: caller,
+		to: address,
+		token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let token = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let balance = balance(&self, token, &caller)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(())
+	}
+
+	/// @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
+	#[weight(<SelfWeightOf<T>>::burn_from())]
+	fn burn_from(
+		&mut self,
+		caller: caller,
+		from: address,
+		token_id: uint256,
+		_value: value,
+	) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let from = T::CrossAccountId::from_eth(from);
+		let token = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let balance = balance(&self, token, &caller)?;
+		ensure_single_owner(&self, token, balance)?;
+
+		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(())
+	}
+
+	/// @notice Returns next free RFT ID.
+	fn next_token_id(&self) -> Result<uint256> {
+		self.consume_store_reads(1)?;
+		Ok(<TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?
+			.into())
+	}
+
+	/// @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
+	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]
+	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let mut expected_index = <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let total_tokens = token_ids.len();
+		for id in token_ids.into_iter() {
+			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+			if id != expected_index {
+				return Err("item id should be next".into());
+			}
+			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+		}
+		let users = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		let create_item_data = CreateItemData::<T> {
+			users,
+			properties: CollectionPropertiesVec::default(),
+		};
+		let data = (0..total_tokens)
+			.map(|_| create_item_data.clone())
+			.collect();
+
+		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+
+	/// @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
+	#[solidity(rename_selector = "mintBulkWithTokenURI")]
+	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]
+	fn mint_bulk_with_token_uri(
+		&mut self,
+		caller: caller,
+		to: address,
+		tokens: Vec<(uint256, string)>,
+	) -> Result<bool> {
+		let key = key::url();
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = T::CrossAccountId::from_eth(to);
+		let mut expected_index = <TokensMinted<T>>::get(self.id)
+			.checked_add(1)
+			.ok_or("item id overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let mut data = Vec::with_capacity(tokens.len());
+		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]
+			.into_iter()
+			.collect::<BTreeMap<_, _>>()
+			.try_into()
+			.unwrap();
+		for (id, token_uri) in tokens {
+			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;
+			if id != expected_index {
+				return Err("item id should be next".into());
+			}
+			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;
+
+			let mut properties = CollectionPropertiesVec::default();
+			properties
+				.try_push(Property {
+					key: key.clone(),
+					value: token_uri
+						.into_bytes()
+						.try_into()
+						.map_err(|_| "token uri is too long")?,
+				})
+				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;
+
+			let create_item_data = CreateItemData::<T> {
+				users: users.clone(),
+				properties,
+			};
+			data.push(create_item_data);
+		}
+
+		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+}
+
 #[solidity_interface(
 	name = "UniqueRefungible",
-	is(via("CollectionHandle<T>", common_mut, Collection),)
+	is(
+		ERC721,
+		ERC721Metadata,
+		ERC721Enumerable,
+		ERC721UniqueExtensions,
+		ERC721Mintable,
+		ERC721Burnable,
+		via("CollectionHandle<T>", common_mut, Collection),
+		TokenProperties,
+	)
 )]
 impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}
 
modifiedpallets/refungible/src/lib.rsdiffbeforeafterboth
--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -88,6 +88,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use crate::erc_token::ERC20Events;
+use crate::erc::ERC721Events;
 
 use codec::{Encode, Decode, MaxEncodedLen};
 use core::ops::Deref;
@@ -96,7 +97,8 @@
 use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
 use pallet_evm_coder_substrate::WithRecorder;
 use pallet_common::{
-	CommonCollectionOperations, Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,
+	CommonCollectionOperations, Error as CommonError, Event as CommonEvent,
+	eth::collection_id_to_address, Pallet as PalletCommon,
 };
 use pallet_structure::Pallet as PalletStructure;
 use scale_info::TypeInfo;
@@ -117,6 +119,9 @@
 pub mod erc;
 pub mod erc_token;
 pub mod weights;
+
+pub type CreateItemData<T> =
+	CreateRefungibleExData<<T as pallet_evm::account::Config>::CrossAccountId>;
 pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;
 
 /// Token data, stored independently from other data used to describe it
@@ -396,6 +401,7 @@
 
 	pub fn burn_token_unchecked(
 		collection: &RefungibleHandle<T>,
+		owner: &T::CrossAccountId,
 		token_id: TokenId,
 	) -> DispatchResult {
 		let burnt = <TokensBurnt<T>>::get(collection.id)
@@ -407,7 +413,15 @@
 		<TotalSupply<T>>::remove((collection.id, token_id));
 		<Balance<T>>::remove_prefix((collection.id, token_id), None);
 		<Allowance<T>>::remove_prefix((collection.id, token_id), None);
-		// TODO: ERC721 transfer event
+
+		<PalletEvm<T>>::deposit_log(
+			ERC721Events::Transfer {
+				from: *owner.as_eth(),
+				to: H160::default(),
+				token_id: token_id.into(),
+			}
+			.to_log(collection_id_to_address(collection.id)),
+		);
 		Ok(())
 	}
 
@@ -449,7 +463,15 @@
 			<Owned<T>>::remove((collection.id, owner, token));
 			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
-			Self::burn_token_unchecked(collection, token)?;
+			Self::burn_token_unchecked(collection, owner, token)?;
+			<PalletEvm<T>>::deposit_log(
+				ERC20Events::Transfer {
+					from: *owner.as_eth(),
+					to: H160::default(),
+					value: amount.into(),
+				}
+				.to_log(collection_id_to_address(collection.id)),
+			);
 			<PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(
 				collection.id,
 				token,
@@ -478,6 +500,17 @@
 			<PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);
 			<Balance<T>>::remove((collection.id, token, owner));
 			<AccountBalance<T>>::insert((collection.id, owner), account_balance);
+
+			if let Some(user) = Self::token_owner(collection.id, token) {
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						to: *user.as_eth(),
+						token_id: token.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			}
 		} else {
 			<Balance<T>>::insert((collection.id, token, owner), balance);
 		}
@@ -695,12 +728,13 @@
 		}
 		<PalletCommon<T>>::ensure_correct_receiver(to)?;
 
-		let balance_from = <Balance<T>>::get((collection.id, token, from))
+		let initial_balance_from = <Balance<T>>::get((collection.id, token, from));
+		let updated_balance_from = initial_balance_from
 			.checked_sub(amount)
 			.ok_or(<CommonError<T>>::TokenValueTooLow)?;
 		let mut create_target = false;
 		let from_to_differ = from != to;
-		let balance_to = if from != to {
+		let updated_balance_to = if from != to {
 			let old_balance = <Balance<T>>::get((collection.id, token, to));
 			if old_balance == 0 {
 				create_target = true;
@@ -714,7 +748,7 @@
 			None
 		};
 
-		let account_balance_from = if balance_from == 0 {
+		let account_balance_from = if updated_balance_from == 0 {
 			Some(
 				<AccountBalance<T>>::get((collection.id, from))
 					.checked_sub(1)
@@ -750,15 +784,15 @@
 			nesting_budget,
 		)?;
 
-		if let Some(balance_to) = balance_to {
+		if let Some(updated_balance_to) = updated_balance_to {
 			// from != to
-			if balance_from == 0 {
+			if updated_balance_from == 0 {
 				<Balance<T>>::remove((collection.id, token, from));
 				<PalletStructure<T>>::unnest_if_nested(from, collection.id, token);
 			} else {
-				<Balance<T>>::insert((collection.id, token, from), balance_from);
+				<Balance<T>>::insert((collection.id, token, from), updated_balance_from);
 			}
-			<Balance<T>>::insert((collection.id, token, to), balance_to);
+			<Balance<T>>::insert((collection.id, token, to), updated_balance_to);
 			if let Some(account_balance_from) = account_balance_from {
 				<AccountBalance<T>>::insert((collection.id, from), account_balance_from);
 				<Owned<T>>::remove((collection.id, from, token));
@@ -780,6 +814,7 @@
 				token,
 			)),
 		);
+
 		<PalletCommon<T>>::deposit_event(CommonEvent::Transfer(
 			collection.id,
 			token,
@@ -787,6 +822,46 @@
 			to.clone(),
 			amount,
 		));
+
+		let total_supply = <TotalSupply<T>>::get((collection.id, token));
+
+		if amount == total_supply {
+			// if token was fully owned by `from` and will be fully owned by `to` after transfer
+			<PalletEvm<T>>::deposit_log(
+				ERC721Events::Transfer {
+					from: *from.as_eth(),
+					to: *to.as_eth(),
+					token_id: token.into(),
+				}
+				.to_log(collection_id_to_address(collection.id)),
+			);
+		} else if let Some(updated_balance_to) = updated_balance_to {
+			// if `from` not equals `to`. This condition is needed to avoid sending event
+			// when `from` fully owns token and sends part of token pieces to itself.
+			if initial_balance_from == total_supply {
+				// if token was fully owned by `from` and will be only partially owned by `to`
+				// and `from` after transfer
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: *from.as_eth(),
+						to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						token_id: token.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			} else if updated_balance_to == total_supply {
+				// if token was partially owned by `from` and will be fully owned by `to` after transfer
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						to: *to.as_eth(),
+						token_id: token.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			}
+		}
+
 		Ok(())
 	}
 
@@ -798,7 +873,7 @@
 	pub fn create_multiple_items(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
-		data: Vec<CreateRefungibleExData<T::CrossAccountId>>,
+		data: Vec<CreateItemData<T>>,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
 		if !collection.is_owner_or_admin(sender) {
@@ -920,11 +995,35 @@
 		for (i, token) in data.into_iter().enumerate() {
 			let token_id = first_token_id + i as u32 + 1;
 
-			for (user, amount) in token.users.into_iter() {
-				if amount == 0 {
-					continue;
-				}
+			let receivers = token
+				.users
+				.into_iter()
+				.filter(|(_, amount)| *amount > 0)
+				.collect::<Vec<_>>();
+
+			if let [(user, _)] = receivers.as_slice() {
+				// if there is exactly one receiver
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: H160::default(),
+						to: *user.as_eth(),
+						token_id: token_id.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			} else if let [_, ..] = receivers.as_slice() {
+				// if there is more than one receiver
+				<PalletEvm<T>>::deposit_log(
+					ERC721Events::Transfer {
+						from: H160::default(),
+						to: erc::ADDRESS_FOR_PARTIALLY_OWNED_TOKENS,
+						token_id: token_id.into(),
+					}
+					.to_log(collection_id_to_address(collection.id)),
+				);
+			}
 
+			for (user, amount) in receivers.into_iter() {
 				<PalletEvm<T>>::deposit_log(
 					ERC20Events::Transfer {
 						from: H160::default(),
@@ -1114,7 +1213,7 @@
 	pub fn create_item(
 		collection: &RefungibleHandle<T>,
 		sender: &T::CrossAccountId,
-		data: CreateRefungibleExData<T::CrossAccountId>,
+		data: CreateItemData<T>,
 		nesting_budget: &dyn Budget,
 	) -> DispatchResult {
 		Self::create_multiple_items(collection, sender, vec![data], nesting_budget)
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
33
4pragma solidity >=0.8.0 <0.9.0;4pragma solidity >=0.8.0 <0.9.0;
5
6// Anonymous struct
7struct Tuple0 {
8 uint256 field_0;
9 string field_1;
10}
511
6// Common stubs holder12// Common stubs holder
7contract Dummy {13contract Dummy {
21 }27 }
22}28}
29
30// Inline
31contract ERC721Events {
32 event Transfer(
33 address indexed from,
34 address indexed to,
35 uint256 indexed tokenId
36 );
37 event Approval(
38 address indexed owner,
39 address indexed approved,
40 uint256 indexed tokenId
41 );
42 event ApprovalForAll(
43 address indexed owner,
44 address indexed operator,
45 bool approved
46 );
47}
48
49// Inline
50contract ERC721MintableEvents {
51 event MintingFinished();
52}
53
54// Selector: 41369377
55contract TokenProperties is Dummy, ERC165 {
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 //
63 // Selector: setTokenPropertyPermission(string,bool,bool,bool) 222d97fa
64 function setTokenPropertyPermission(
65 string memory key,
66 bool isMutable,
67 bool collectionAdmin,
68 bool tokenOwner
69 ) public {
70 require(false, stub_error);
71 key;
72 isMutable;
73 collectionAdmin;
74 tokenOwner;
75 dummy = 0;
76 }
77
78 // @notice Set token property value.
79 // @dev Throws error if `msg.sender` has no permission to edit the property.
80 // @param tokenId ID of the token.
81 // @param key Property key.
82 // @param value Property value.
83 //
84 // Selector: setProperty(uint256,string,bytes) 1752d67b
85 function setProperty(
86 uint256 tokenId,
87 string memory key,
88 bytes memory value
89 ) public {
90 require(false, stub_error);
91 tokenId;
92 key;
93 value;
94 dummy = 0;
95 }
96
97 // @notice Delete token property value.
98 // @dev Throws error if `msg.sender` has no permission to edit the property.
99 // @param tokenId ID of the token.
100 // @param key Property key.
101 //
102 // Selector: deleteProperty(uint256,string) 066111d1
103 function deleteProperty(uint256 tokenId, string memory key) public {
104 require(false, stub_error);
105 tokenId;
106 key;
107 dummy = 0;
108 }
109
110 // @notice Get token property value.
111 // @dev Throws error if key not found
112 // @param tokenId ID of the token.
113 // @param key Property key.
114 // @return Property value bytes
115 //
116 // Selector: property(uint256,string) 7228c327
117 function property(uint256 tokenId, string memory key)
118 public
119 view
120 returns (bytes memory)
121 {
122 require(false, stub_error);
123 tokenId;
124 key;
125 dummy;
126 return hex"";
127 }
128}
129
130// Selector: 42966c68
131contract ERC721Burnable is Dummy, ERC165 {
132 // @notice Burns a specific ERC721 token.
133 // @dev Throws unless `msg.sender` is the current RFT owner, or an authorized
134 // operator of the current owner.
135 // @param tokenId The RFT to approve
136 //
137 // Selector: burn(uint256) 42966c68
138 function burn(uint256 tokenId) public {
139 require(false, stub_error);
140 tokenId;
141 dummy = 0;
142 }
143}
144
145// Selector: 58800161
146contract ERC721 is Dummy, ERC165, ERC721Events {
147 // @notice Count all RFTs assigned to an owner
148 // @dev RFTs assigned to the zero address are considered invalid, and this
149 // function throws for queries about the zero address.
150 // @param owner An address for whom to query the balance
151 // @return The number of RFTs owned by `owner`, possibly zero
152 //
153 // Selector: balanceOf(address) 70a08231
154 function balanceOf(address owner) public view returns (uint256) {
155 require(false, stub_error);
156 owner;
157 dummy;
158 return 0;
159 }
160
161 // @notice Find the owner of an RFT
162 // @dev RFTs assigned to zero address are considered invalid, and queries
163 // about them do throw.
164 // Returns special 0xffffffffffffffffffffffffffffffffffffffff address for
165 // the tokens that are partially owned.
166 // @param tokenId The identifier for an RFT
167 // @return The address of the owner of the RFT
168 //
169 // Selector: ownerOf(uint256) 6352211e
170 function ownerOf(uint256 tokenId) public view returns (address) {
171 require(false, stub_error);
172 tokenId;
173 dummy;
174 return 0x0000000000000000000000000000000000000000;
175 }
176
177 // @dev Not implemented
178 //
179 // Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
180 function safeTransferFromWithData(
181 address from,
182 address to,
183 uint256 tokenId,
184 bytes memory data
185 ) public {
186 require(false, stub_error);
187 from;
188 to;
189 tokenId;
190 data;
191 dummy = 0;
192 }
193
194 // @dev Not implemented
195 //
196 // Selector: safeTransferFrom(address,address,uint256) 42842e0e
197 function safeTransferFrom(
198 address from,
199 address to,
200 uint256 tokenId
201 ) public {
202 require(false, stub_error);
203 from;
204 to;
205 tokenId;
206 dummy = 0;
207 }
208
209 // @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE
210 // TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE
211 // THEY MAY BE PERMANENTLY LOST
212 // @dev Throws unless `msg.sender` is the current owner or an authorized
213 // operator for this RFT. Throws if `from` is not the current owner. Throws
214 // if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
215 // Throws if RFT pieces have multiple owners.
216 // @param from The current owner of the NFT
217 // @param to The new owner
218 // @param tokenId The NFT to transfer
219 // @param _value Not used for an NFT
220 //
221 // Selector: transferFrom(address,address,uint256) 23b872dd
222 function transferFrom(
223 address from,
224 address to,
225 uint256 tokenId
226 ) public {
227 require(false, stub_error);
228 from;
229 to;
230 tokenId;
231 dummy = 0;
232 }
233
234 // @dev Not implemented
235 //
236 // Selector: approve(address,uint256) 095ea7b3
237 function approve(address approved, uint256 tokenId) public {
238 require(false, stub_error);
239 approved;
240 tokenId;
241 dummy = 0;
242 }
243
244 // @dev Not implemented
245 //
246 // Selector: setApprovalForAll(address,bool) a22cb465
247 function setApprovalForAll(address operator, bool approved) public {
248 require(false, stub_error);
249 operator;
250 approved;
251 dummy = 0;
252 }
253
254 // @dev Not implemented
255 //
256 // Selector: getApproved(uint256) 081812fc
257 function getApproved(uint256 tokenId) public view returns (address) {
258 require(false, stub_error);
259 tokenId;
260 dummy;
261 return 0x0000000000000000000000000000000000000000;
262 }
263
264 // @dev Not implemented
265 //
266 // Selector: isApprovedForAll(address,address) e985e9c5
267 function isApprovedForAll(address owner, address operator)
268 public
269 view
270 returns (address)
271 {
272 require(false, stub_error);
273 owner;
274 operator;
275 dummy;
276 return 0x0000000000000000000000000000000000000000;
277 }
278}
279
280// Selector: 5b5e139f
281contract ERC721Metadata is Dummy, ERC165 {
282 // @notice A descriptive name for a collection of RFTs in this contract
283 //
284 // Selector: name() 06fdde03
285 function name() public view returns (string memory) {
286 require(false, stub_error);
287 dummy;
288 return "";
289 }
290
291 // @notice An abbreviated name for RFTs in this contract
292 //
293 // Selector: symbol() 95d89b41
294 function symbol() public view returns (string memory) {
295 require(false, stub_error);
296 dummy;
297 return "";
298 }
299
300 // @notice A distinct Uniform Resource Identifier (URI) for a given asset.
301 //
302 // @dev If the token has a `url` property and it is not empty, it is returned.
303 // 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`.
304 // If the collection property `baseURI` is empty or absent, return "" (empty string)
305 // otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix
306 // otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
307 //
308 // @return token's const_metadata
309 //
310 // Selector: tokenURI(uint256) c87b56dd
311 function tokenURI(uint256 tokenId) public view returns (string memory) {
312 require(false, stub_error);
313 tokenId;
314 dummy;
315 return "";
316 }
317}
318
319// Selector: 68ccfe89
320contract ERC721Mintable is Dummy, ERC165, ERC721MintableEvents {
321 // Selector: mintingFinished() 05d2035b
322 function mintingFinished() public view returns (bool) {
323 require(false, stub_error);
324 dummy;
325 return false;
326 }
327
328 // @notice Function to mint token.
329 // @dev `tokenId` should be obtained with `nextTokenId` method,
330 // unlike standard, you can't specify it manually
331 // @param to The new owner
332 // @param tokenId ID of the minted RFT
333 //
334 // Selector: mint(address,uint256) 40c10f19
335 function mint(address to, uint256 tokenId) public returns (bool) {
336 require(false, stub_error);
337 to;
338 tokenId;
339 dummy = 0;
340 return false;
341 }
342
343 // @notice Function to mint token with the given tokenUri.
344 // @dev `tokenId` should be obtained with `nextTokenId` method,
345 // unlike standard, you can't specify it manually
346 // @param to The new owner
347 // @param tokenId ID of the minted RFT
348 // @param tokenUri Token URI that would be stored in the RFT properties
349 //
350 // Selector: mintWithTokenURI(address,uint256,string) 50bb4e7f
351 function mintWithTokenURI(
352 address to,
353 uint256 tokenId,
354 string memory tokenUri
355 ) public returns (bool) {
356 require(false, stub_error);
357 to;
358 tokenId;
359 tokenUri;
360 dummy = 0;
361 return false;
362 }
363
364 // @dev Not implemented
365 //
366 // Selector: finishMinting() 7d64bcb4
367 function finishMinting() public returns (bool) {
368 require(false, stub_error);
369 dummy = 0;
370 return false;
371 }
372}
373
374// Selector: 780e9d63
375contract ERC721Enumerable is Dummy, ERC165 {
376 // @notice Enumerate valid RFTs
377 // @param index A counter less than `totalSupply()`
378 // @return The token identifier for the `index`th NFT,
379 // (sort order not specified)
380 //
381 // Selector: tokenByIndex(uint256) 4f6ccce7
382 function tokenByIndex(uint256 index) public view returns (uint256) {
383 require(false, stub_error);
384 index;
385 dummy;
386 return 0;
387 }
388
389 // Not implemented
390 //
391 // Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
392 function tokenOfOwnerByIndex(address owner, uint256 index)
393 public
394 view
395 returns (uint256)
396 {
397 require(false, stub_error);
398 owner;
399 index;
400 dummy;
401 return 0;
402 }
403
404 // @notice Count RFTs tracked by this contract
405 // @return A count of valid RFTs tracked by this contract, where each one of
406 // them has an assigned and queryable owner not equal to the zero address
407 //
408 // Selector: totalSupply() 18160ddd
409 function totalSupply() public view returns (uint256) {
410 require(false, stub_error);
411 dummy;
412 return 0;
413 }
414}
23415
24// Selector: 7d9262e6416// Selector: 7d9262e6
25contract Collection is Dummy, ERC165 {417contract Collection is Dummy, ERC165 {
248 }640 }
249}641}
642
643// Selector: d74d154f
644contract ERC721UniqueExtensions is Dummy, ERC165 {
645 // @notice Transfer ownership of an RFT
646 // @dev Throws unless `msg.sender` is the current owner. Throws if `to`
647 // is the zero address. Throws if `tokenId` is not a valid RFT.
648 // Throws if RFT pieces have multiple owners.
649 // @param to The new owner
650 // @param tokenId The RFT to transfer
651 // @param _value Not used for an RFT
652 //
653 // Selector: transfer(address,uint256) a9059cbb
654 function transfer(address to, uint256 tokenId) public {
655 require(false, stub_error);
656 to;
657 tokenId;
658 dummy = 0;
659 }
660
661 // @notice Burns a specific ERC721 token.
662 // @dev Throws unless `msg.sender` is the current owner or an authorized
663 // operator for this RFT. Throws if `from` is not the current owner. Throws
664 // if `to` is the zero address. Throws if `tokenId` is not a valid RFT.
665 // Throws if RFT pieces have multiple owners.
666 // @param from The current owner of the RFT
667 // @param tokenId The RFT to transfer
668 // @param _value Not used for an RFT
669 //
670 // Selector: burnFrom(address,uint256) 79cc6790
671 function burnFrom(address from, uint256 tokenId) public {
672 require(false, stub_error);
673 from;
674 tokenId;
675 dummy = 0;
676 }
677
678 // @notice Returns next free RFT ID.
679 //
680 // Selector: nextTokenId() 75794a3c
681 function nextTokenId() public view returns (uint256) {
682 require(false, stub_error);
683 dummy;
684 return 0;
685 }
686
687 // @notice Function to mint multiple tokens.
688 // @dev `tokenIds` should be an array of consecutive numbers and first number
689 // should be obtained with `nextTokenId` method
690 // @param to The new owner
691 // @param tokenIds IDs of the minted RFTs
692 //
693 // Selector: mintBulk(address,uint256[]) 44a9945e
694 function mintBulk(address to, uint256[] memory tokenIds)
695 public
696 returns (bool)
697 {
698 require(false, stub_error);
699 to;
700 tokenIds;
701 dummy = 0;
702 return false;
703 }
704
705 // @notice Function to mint multiple tokens with the given tokenUris.
706 // @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive
707 // numbers and first number should be obtained with `nextTokenId` method
708 // @param to The new owner
709 // @param tokens array of pairs of token ID and token URI for minted tokens
710 //
711 // Selector: mintBulkWithTokenURI(address,(uint256,string)[]) 36543006
712 function mintBulkWithTokenURI(address to, Tuple0[] memory tokens)
713 public
714 returns (bool)
715 {
716 require(false, stub_error);
717 to;
718 tokens;
719 dummy = 0;
720 return false;
721 }
722}
250723
251contract UniqueRefungible is Dummy, ERC165, Collection {}724contract UniqueRefungible is
725 Dummy,
726 ERC165,
727 ERC721,
728 ERC721Metadata,
729 ERC721Enumerable,
730 ERC721UniqueExtensions,
731 ERC721Mintable,
732 ERC721Burnable,
733 Collection,
734 TokenProperties
735{}
252736
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -3,6 +3,12 @@
 
 pragma solidity >=0.8.0 <0.9.0;
 
+// Anonymous struct
+struct Tuple0 {
+	uint256 field_0;
+	string field_1;
+}
+
 // Common stubs holder
 interface Dummy {
 
@@ -12,6 +18,262 @@
 	function supportsInterface(bytes4 interfaceID) external view returns (bool);
 }
 
+// Inline
+interface ERC721Events {
+	event Transfer(
+		address indexed from,
+		address indexed to,
+		uint256 indexed tokenId
+	);
+	event Approval(
+		address indexed owner,
+		address indexed approved,
+		uint256 indexed tokenId
+	);
+	event ApprovalForAll(
+		address indexed owner,
+		address indexed operator,
+		bool approved
+	);
+}
+
+// Inline
+interface ERC721MintableEvents {
+	event MintingFinished();
+}
+
+// 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,
+		bool isMutable,
+		bool collectionAdmin,
+		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,
+		string memory key,
+		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;
+
+	// @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)
+		external
+		view
+		returns (bytes memory);
+}
+
+// Selector: 42966c68
+interface ERC721Burnable is Dummy, ERC165 {
+	// @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;
+}
+
+// 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);
+
+	// @notice Find the owner of an RFT
+	// @dev RFTs assigned to zero address are considered invalid, and queries
+	//  about them do throw.
+	//  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for
+	//  the tokens that are partially owned.
+	// @param tokenId The identifier for an RFT
+	// @return The address of the owner of the RFT
+	//
+	// Selector: ownerOf(uint256) 6352211e
+	function ownerOf(uint256 tokenId) external view returns (address);
+
+	// @dev Not implemented
+	//
+	// Selector: safeTransferFromWithData(address,address,uint256,bytes) 60a11672
+	function safeTransferFromWithData(
+		address from,
+		address to,
+		uint256 tokenId,
+		bytes memory data
+	) external;
+
+	// @dev Not implemented
+	//
+	// Selector: safeTransferFrom(address,address,uint256) 42842e0e
+	function safeTransferFrom(
+		address from,
+		address to,
+		uint256 tokenId
+	) external;
+
+	// @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(
+		address from,
+		address to,
+		uint256 tokenId
+	) external;
+
+	// @dev Not implemented
+	//
+	// Selector: approve(address,uint256) 095ea7b3
+	function approve(address approved, uint256 tokenId) external;
+
+	// @dev Not implemented
+	//
+	// Selector: setApprovalForAll(address,bool) a22cb465
+	function setApprovalForAll(address operator, bool approved) external;
+
+	// @dev Not implemented
+	//
+	// Selector: getApproved(uint256) 081812fc
+	function getApproved(uint256 tokenId) external view returns (address);
+
+	// @dev Not implemented
+	//
+	// Selector: isApprovedForAll(address,address) e985e9c5
+	function isApprovedForAll(address owner, address operator)
+		external
+		view
+		returns (address);
+}
+
+// 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);
+
+	// @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);
+}
+
+// Selector: 68ccfe89
+interface ERC721Mintable is Dummy, ERC165, ERC721MintableEvents {
+	// Selector: mintingFinished() 05d2035b
+	function mintingFinished() external view returns (bool);
+
+	// @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);
+
+	// @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(
+		address to,
+		uint256 tokenId,
+		string memory tokenUri
+	) external returns (bool);
+
+	// @dev Not implemented
+	//
+	// Selector: finishMinting() 7d64bcb4
+	function finishMinting() external returns (bool);
+}
+
+// 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);
+
+	// Not implemented
+	//
+	// Selector: tokenOfOwnerByIndex(address,uint256) 2f745c59
+	function tokenOfOwnerByIndex(address owner, uint256 index)
+		external
+		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);
+}
+
 // Selector: 7d9262e6
 interface Collection is Dummy, ERC165 {
 	// Set collection property.
@@ -160,4 +422,68 @@
 	function setCollectionMintMode(bool mode) external;
 }
 
-interface UniqueRefungible is Dummy, ERC165, Collection {}
+// 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,
+	ERC721,
+	ERC721Metadata,
+	ERC721Enumerable,
+	ERC721UniqueExtensions,
+	ERC721Mintable,
+	ERC721Burnable,
+	Collection,
+	TokenProperties
+{}
addedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungible.test.ts
@@ -0,0 +1,465 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// 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, UNIQUE} from '../util/helpers';
+import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, recordEthFee, recordEvents, tokenIdToAddress} from './util/helpers';
+import reFungibleTokenAbi from './reFungibleTokenAbi.json';
+import {expect} from 'chai';
+
+describe('Refungible: Information getting', () => {
+  itWeb3('totalSupply', async ({api, web3, 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 nextTokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, nextTokenId).send();
+    const totalSupply = await contract.methods.totalSupply().call();
+    expect(totalSupply).to.equal('1');
+  });
+
+  itWeb3('balanceOf', async ({api, web3, 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 nextTokenId = await contract.methods.nextTokenId().call();
+      await contract.methods.mint(caller, nextTokenId).send();
+    }
+    {
+      const nextTokenId = await contract.methods.nextTokenId().call();
+      await contract.methods.mint(caller, nextTokenId).send();
+    }
+    {
+      const nextTokenId = await contract.methods.nextTokenId().call();
+      await contract.methods.mint(caller, nextTokenId).send();
+    }
+
+    const balance = await contract.methods.balanceOf(caller).call();
+
+    expect(balance).to.equal('3');
+  });
+
+  itWeb3('ownerOf', async ({api, web3, 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 owner = await contract.methods.ownerOf(tokenId).call();
+
+    expect(owner).to.equal(caller);
+  });
+
+  itWeb3('ownerOf after burn', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    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 = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    await tokenContract.methods.burnFrom(caller, 1).send();
+
+    const owner = await contract.methods.ownerOf(tokenId).call();
+
+    expect(owner).to.equal(receiver);
+  });
+
+  itWeb3('ownerOf for partial ownership', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    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 = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    const owner = await contract.methods.ownerOf(tokenId).call();
+
+    expect(owner).to.equal('0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF');
+  });
+});
+
+describe('Refungible: Plain calls', () => {
+  itWeb3('Can perform mint()', async ({web3, api, privateKeyWrapper}) => {
+    const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const helper = evmCollectionHelpers(web3, owner);
+    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, {type: 'ReFungible'});
+    const nextTokenId = await contract.methods.nextTokenId().call();
+
+    expect(nextTokenId).to.be.equal('1');
+    result = await contract.methods.mintWithTokenURI(
+      receiver,
+      nextTokenId,
+      'Test URI',
+    ).send();
+
+    const events = normalizeEvents(result.events);
+
+    expect(events).to.include.deep.members([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: '0x0000000000000000000000000000000000000000',
+          to: receiver,
+          tokenId: nextTokenId,
+        },
+      },
+    ]);
+
+    expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+  });
+
+  itWeb3('Can perform mintBulk()', 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 nextTokenId = await contract.methods.nextTokenId().call();
+      expect(nextTokenId).to.be.equal('1');
+      const result = await contract.methods.mintBulkWithTokenURI(
+        receiver,
+        [
+          [nextTokenId, 'Test URI 0'],
+          [+nextTokenId + 1, 'Test URI 1'],
+          [+nextTokenId + 2, 'Test URI 2'],
+        ],
+      ).send();
+      const events = normalizeEvents(result.events);
+
+      expect(events).to.include.deep.members([
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: '0x0000000000000000000000000000000000000000',
+            to: receiver,
+            tokenId: nextTokenId,
+          },
+        },
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: '0x0000000000000000000000000000000000000000',
+            to: receiver,
+            tokenId: String(+nextTokenId + 1),
+          },
+        },
+        {
+          address: collectionIdAddress,
+          event: 'Transfer',
+          args: {
+            from: '0x0000000000000000000000000000000000000000',
+            to: receiver,
+            tokenId: String(+nextTokenId + 2),
+          },
+        },
+      ]);
+
+      expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');
+      expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');
+      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.include.deep.members([
+        {
+          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, collectionId} = 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 address = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    await tokenContract.methods.repartition(15).send();
+
+    {
+      const erc20Events = await recordEvents(tokenContract, async () => {
+        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(),
+            },
+          },
+        ]);
+      });
+      
+      expect(erc20Events).to.include.deep.members([
+        {
+          address,
+          event: 'Transfer',
+          args: {
+            from: caller,
+            to: receiver,
+            value: '15',
+          },
+        },
+      ]);
+    }
+
+    {
+      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);
+    }
+  });
+
+  itWeb3('transfer event on transfer from partial ownership to full ownership', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    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 = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    const events =  await recordEvents(contract, async () => 
+      await tokenContract.methods.transfer(receiver, 1).send());
+    expect(events).to.deep.equal([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF',
+          to: receiver,
+          tokenId: tokenId.toString(),
+        },
+      },
+    ]);
+  });
+
+  itWeb3('transfer event on transfer from full ownership to partial ownership', async ({api, web3, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = createEthAccount(web3);
+    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 = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+    const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+    await tokenContract.methods.repartition(2).send();
+    
+    const events =  await recordEvents(contract, async () => 
+      await tokenContract.methods.transfer(receiver, 1).send());
+      
+    expect(events).to.deep.equal([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: caller,
+          to: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF',
+          tokenId: tokenId.toString(),
+        },
+      },
+    ]);
+  });
+});
+
+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({
+      name: 'token name',
+      mode: {type: 'ReFungible'},
+    });
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const address = collectionIdToAddress(collection);
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
+    const name = await contract.methods.name().call();
+
+    expect(name).to.equal('token name');
+  });
+
+  itWeb3('Returns symbol name', async ({api, web3, privateKeyWrapper}) => {
+    const collection = await createCollectionExpectSuccess({
+      tokenPrefix: 'TOK',
+      mode: {type: 'ReFungible'},
+    });
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+    const address = collectionIdToAddress(collection);
+    const contract = evmCollection(web3, caller, address, {type: 'ReFungible'});
+    const symbol = await contract.methods.symbol().call();
+
+    expect(symbol).to.equal('TOK');
+  });
+});
\ No newline at end of file
addedtests/src/eth/reFungibleAbi.jsondiffbeforeafterboth
--- /dev/null
+++ b/tests/src/eth/reFungibleAbi.json
@@ -0,0 +1,530 @@
+[
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "owner",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "approved",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "tokenId",
+        "type": "uint256"
+      }
+    ],
+    "name": "Approval",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "owner",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "operator",
+        "type": "address"
+      },
+      {
+        "indexed": false,
+        "internalType": "bool",
+        "name": "approved",
+        "type": "bool"
+      }
+    ],
+    "name": "ApprovalForAll",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [],
+    "name": "MintingFinished",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "from",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "to",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "tokenId",
+        "type": "uint256"
+      }
+    ],
+    "name": "Transfer",
+    "type": "event"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "newAdmin", "type": "address" }
+    ],
+    "name": "addCollectionAdmin",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "newAdmin", "type": "uint256" }
+    ],
+    "name": "addCollectionAdminSubstrate",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "user", "type": "address" }
+    ],
+    "name": "addToCollectionAllowList",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "approved", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "approve",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" }
+    ],
+    "name": "balanceOf",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "burn",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "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" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "confirmCollectionSponsorship",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "contractAddress",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
+    "name": "deleteCollectionProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "key", "type": "string" }
+    ],
+    "name": "deleteProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "finishMinting",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "getApproved",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" },
+      { "internalType": "address", "name": "operator", "type": "address" }
+    ],
+    "name": "isApprovedForAll",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "mint",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256[]", "name": "tokenIds", "type": "uint256[]" }
+    ],
+    "name": "mintBulk",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      {
+        "components": [
+          { "internalType": "uint256", "name": "field_0", "type": "uint256" },
+          { "internalType": "string", "name": "field_1", "type": "string" }
+        ],
+        "internalType": "struct Tuple0[]",
+        "name": "tokens",
+        "type": "tuple[]"
+      }
+    ],
+    "name": "mintBulkWithTokenURI",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "tokenUri", "type": "string" }
+    ],
+    "name": "mintWithTokenURI",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "mintingFinished",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "name",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "nextTokenId",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "ownerOf",
+    "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "key", "type": "string" }
+    ],
+    "name": "property",
+    "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "admin", "type": "address" }
+    ],
+    "name": "removeCollectionAdmin",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "admin", "type": "uint256" }
+    ],
+    "name": "removeCollectionAdminSubstrate",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "user", "type": "address" }
+    ],
+    "name": "removeFromCollectionAllowList",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "safeTransferFrom",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "from", "type": "address" },
+      { "internalType": "address", "name": "to", "type": "address" },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "bytes", "name": "data", "type": "bytes" }
+    ],
+    "name": "safeTransferFromWithData",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "operator", "type": "address" },
+      { "internalType": "bool", "name": "approved", "type": "bool" }
+    ],
+    "name": "setApprovalForAll",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "uint8", "name": "mode", "type": "uint8" }],
+    "name": "setCollectionAccess",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "limit", "type": "string" },
+      { "internalType": "uint32", "name": "value", "type": "uint32" }
+    ],
+    "name": "setCollectionLimit",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "limit", "type": "string" },
+      { "internalType": "bool", "name": "value", "type": "bool" }
+    ],
+    "name": "setCollectionLimit",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "bool", "name": "mode", "type": "bool" }],
+    "name": "setCollectionMintMode",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [{ "internalType": "bool", "name": "enable", "type": "bool" }],
+    "name": "setCollectionNesting",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "bool", "name": "enable", "type": "bool" },
+      {
+        "internalType": "address[]",
+        "name": "collections",
+        "type": "address[]"
+      }
+    ],
+    "name": "setCollectionNesting",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "key", "type": "string" },
+      { "internalType": "bytes", "name": "value", "type": "bytes" }
+    ],
+    "name": "setCollectionProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "sponsor", "type": "address" }
+    ],
+    "name": "setCollectionSponsor",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      { "internalType": "string", "name": "key", "type": "string" },
+      { "internalType": "bytes", "name": "value", "type": "bytes" }
+    ],
+    "name": "setProperty",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "string", "name": "key", "type": "string" },
+      { "internalType": "bool", "name": "isMutable", "type": "bool" },
+      { "internalType": "bool", "name": "collectionAdmin", "type": "bool" },
+      { "internalType": "bool", "name": "tokenOwner", "type": "bool" }
+    ],
+    "name": "setTokenPropertyPermission",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }
+    ],
+    "name": "supportsInterface",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "symbol",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "index", "type": "uint256" }
+    ],
+    "name": "tokenByIndex",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "address", "name": "owner", "type": "address" },
+      { "internalType": "uint256", "name": "index", "type": "uint256" }
+    ],
+    "name": "tokenOfOwnerByIndex",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "tokenURI",
+    "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "totalSupply",
+    "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "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" }
+    ],
+    "name": "transferFrom",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]
modifiedtests/src/eth/reFungibleToken.test.tsdiffbeforeafterboth
--- a/tests/src/eth/reFungibleToken.test.ts
+++ b/tests/src/eth/reFungibleToken.test.ts
@@ -74,7 +74,7 @@
 });
 
 // FIXME: Need erc721 for ReFubgible.
-describe.skip('Check ERC721 token URI for ReFungible', () => {
+describe('Check ERC721 token URI for ReFungible', () => {
   itWeb3('Empty tokenURI', async ({web3, api, privateKeyWrapper}) => {
     const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
     const helper = evmCollectionHelpers(web3, owner);
@@ -277,7 +277,7 @@
     {
       const result = await contract.methods.transferFrom(owner, receiver, 49).send({from: spender});
       const events = normalizeEvents(result.events);
-      expect(events).to.be.deep.equal([
+      expect(events).to.include.deep.members([
         {
           address,
           event: 'Transfer',
@@ -329,7 +329,7 @@
     {
       const result = await contract.methods.transfer(receiver, 50).send({from: owner});
       const events = normalizeEvents(result.events);
-      expect(events).to.be.deep.equal([
+      expect(events).to.include.deep.members([
         {
           address,
           event: 'Transfer',
@@ -442,6 +442,38 @@
       },
     ]);
   });
+
+  itWeb3('Receiving Transfer event on burning into full ownership', async ({web3, api, privateKeyWrapper}) => {
+    const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    const receiver = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+    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 = evmCollection(web3, caller, collectionIdAddress, {type: 'ReFungible'});
+
+    const tokenId = await contract.methods.nextTokenId().call();
+    await contract.methods.mint(caller, tokenId).send();
+
+    const address = tokenIdToAddress(collectionId, tokenId);
+
+    const tokenContract =  new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+    await tokenContract.methods.repartition(2).send();
+    await tokenContract.methods.transfer(receiver, 1).send();
+
+    const events =  await recordEvents(contract, async () => 
+      await tokenContract.methods.burnFrom(caller, 1).send());
+    expect(events).to.deep.equal([
+      {
+        address: collectionIdAddress,
+        event: 'Transfer',
+        args: {
+          from: '0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF',
+          to: receiver,
+          tokenId,
+        },
+      },
+    ]);
+  });
 });
 
 describe('Refungible: Fees', () => {
modifiedtests/src/eth/util/helpers.tsdiffbeforeafterboth
--- a/tests/src/eth/util/helpers.ts
+++ b/tests/src/eth/util/helpers.ts
@@ -31,7 +31,7 @@
 import collectionHelpersAbi from '../collectionHelpersAbi.json';
 import fungibleAbi from '../fungibleAbi.json';
 import nonFungibleAbi from '../nonFungibleAbi.json';
-import refungibleAbi from '../refungibleAbi.json';
+import refungibleAbi from '../reFungibleAbi.json';
 import contractHelpersAbi from './contractHelpersAbi.json';
 
 export const GAS_ARGS = {gas: 2500000};