git.delta.rocks / unique-network / refs/commits / 149c99dda705

difftreelog

Merge pull request #719 from UniqueNetwork/feature/transferCross

Yaroslav Bolyukin2022-11-21parents: #5f0218a #171847d.patch.diff
in: master

26 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6121,7 +6121,7 @@
 
 [[package]]
 name = "pallet-fungible"
-version = "0.1.6"
+version = "0.1.7"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -6376,7 +6376,7 @@
 
 [[package]]
 name = "pallet-nonfungible"
-version = "0.1.8"
+version = "0.1.9"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -6498,7 +6498,7 @@
 
 [[package]]
 name = "pallet-refungible"
-version = "0.2.7"
+version = "0.2.8"
 dependencies = [
  "derivative",
  "ethereum",
modifiedpallets/fungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/fungible/CHANGELOG.md
+++ b/pallets/fungible/CHANGELOG.md
@@ -2,22 +2,32 @@
 
 All notable changes to this project will be documented in this file.
 
+<!-- bureaucrate goes here -->
+
+## [0.1.7] - 2022-11-14
+
+### Changed
+
+- Added `transfer_cross` in eth functions.
+
 ## [0.1.6] - 2022-11-02
+
 ### Changed
- - Use named structure `EthCrossAccount` in eth functions.
 
+- Use named structure `EthCrossAccount` in eth functions.
+
 ## [0.1.5] - 2022-08-29
 
 ### Added
 
- - Implementation of `mint` and `mint_bulk` methods for ERC20 API.
+- Implementation of `mint` and `mint_bulk` methods for ERC20 API.
 
 ## [v0.1.4] - 2022-08-24
 
 ### Change
- - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
 
-<!-- bureaucrate goes here -->
+- Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
+
 ## [v0.1.3] 2022-08-16
 
 ### Other changes
@@ -41,11 +51,11 @@
 
 ### Fixed
 
- - Issue with ItemCreated event containing total supply of tokens instead minted amount
+- Issue with ItemCreated event containing total supply of tokens instead minted amount
 
 ## [0.1.1] - 2022-07-14
 
 ### Added
 
- - Implementation of 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.
+- Implementation of 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.
modifiedpallets/fungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/fungible/Cargo.toml
+++ b/pallets/fungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-fungible"
-version = "0.1.6"
+version = "0.1.7"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/fungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/fungible/src/erc.rs
+++ b/pallets/fungible/src/erc.rs
@@ -93,6 +93,7 @@
 		<Pallet<T>>::transfer(self, &caller, &to, amount, &budget).map_err(|_| "transfer error")?;
 		Ok(true)
 	}
+
 	#[weight(<SelfWeightOf<T>>::transfer_from())]
 	fn transfer_from(
 		&mut self,
@@ -238,6 +239,24 @@
 		Ok(true)
 	}
 
+	#[weight(<SelfWeightOf<T>>::transfer())]
+	fn transfer_cross(
+		&mut self,
+		caller: caller,
+		to: EthCrossAccount,
+		amount: uint256,
+	) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = to.into_sub_cross_account::<T>()?;
+		let amount = amount.try_into().map_err(|_| "amount overflow")?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::transfer(self, &caller, &to, amount, &budget).map_err(|_| "transfer error")?;
+		Ok(true)
+	}
+
 	#[weight(<SelfWeightOf<T>>::transfer_from())]
 	fn transfer_from_cross(
 		&mut self,
modifiedpallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth
--- a/pallets/fungible/src/stubs/UniqueFungible.sol
+++ b/pallets/fungible/src/stubs/UniqueFungible.sol
@@ -38,7 +38,7 @@
 	/// @param properties Vector of properties key/value pair.
 	/// @dev EVM selector for this function is: 0x50b26b2a,
 	///  or in textual repr: setCollectionProperties((string,bytes)[])
-	function setCollectionProperties(Tuple14[] memory properties) public {
+	function setCollectionProperties(Tuple15[] memory properties) public {
 		require(false, stub_error);
 		properties;
 		dummy = 0;
@@ -87,11 +87,11 @@
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
-	function collectionProperties(string[] memory keys) public view returns (Tuple14[] memory) {
+	function collectionProperties(string[] memory keys) public view returns (Tuple15[] memory) {
 		require(false, stub_error);
 		keys;
 		dummy;
-		return new Tuple14[](0);
+		return new Tuple15[](0);
 	}
 
 	/// Set the sponsor of the collection.
@@ -439,12 +439,12 @@
 }
 
 /// @dev anonymous struct
-struct Tuple14 {
+struct Tuple15 {
 	string field_0;
 	bytes field_1;
 }
 
-/// @dev the ERC-165 identifier for this interface is 0x032e5926
+/// @dev the ERC-165 identifier for this interface is 0x29f4dcd9
 contract ERC20UniqueExtensions is Dummy, ERC165 {
 	/// @dev EVM selector for this function is: 0x0ecd0ab0,
 	///  or in textual repr: approveCross((address,uint256),uint256)
@@ -497,6 +497,16 @@
 		return false;
 	}
 
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(EthCrossAccount memory to, uint256 amount) public returns (bool) {
+		require(false, stub_error);
+		to;
+		amount;
+		dummy = 0;
+		return false;
+	}
+
 	/// @dev EVM selector for this function is: 0xd5cf430b,
 	///  or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256)
 	function transferFromCross(
modifiedpallets/nonfungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/nonfungible/CHANGELOG.md
+++ b/pallets/nonfungible/CHANGELOG.md
@@ -4,6 +4,12 @@
 
 <!-- bureaucrate goes here -->
 
+## [0.1.9] - 2022-11-14
+
+### Changed
+
+- Added `transfer_cross` in eth functions.
+
 ## [v0.1.8] - 2022-11-11
 
 ### Changed
modifiedpallets/nonfungible/Cargo.tomldiffbeforeafterboth
--- a/pallets/nonfungible/Cargo.toml
+++ b/pallets/nonfungible/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "pallet-nonfungible"
-version = "0.1.8"
+version = "0.1.9"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -739,6 +739,29 @@
 		Ok(())
 	}
 
+	/// @notice Transfer ownership of an NFT
+	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	///  is the zero address. Throws if `tokenId` is not a valid NFT.
+	/// @param to The new owner
+	/// @param tokenId The NFT to transfer
+	#[weight(<SelfWeightOf<T>>::transfer())]
+	fn transfer_cross(
+		&mut self,
+		caller: caller,
+		to: EthCrossAccount,
+		token_id: uint256,
+	) -> Result<void> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let to = to.into_sub_cross_account::<T>()?;
+		let token = token_id.try_into()?;
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		<Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;
+		Ok(())
+	}
+
 	/// @notice Transfer ownership of an NFT from cross account address to cross account address
 	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
 	///  is the zero address. Throws if `tokenId` is not a valid NFT.
modifiedpallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth
--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -67,7 +67,7 @@
 	/// @param properties settable properties
 	/// @dev EVM selector for this function is: 0x14ed3a6e,
 	///  or in textual repr: setProperties(uint256,(string,bytes)[])
-	function setProperties(uint256 tokenId, Tuple21[] memory properties) public {
+	function setProperties(uint256 tokenId, Tuple22[] memory properties) public {
 		require(false, stub_error);
 		tokenId;
 		properties;
@@ -137,7 +137,7 @@
 	/// @param properties Vector of properties key/value pair.
 	/// @dev EVM selector for this function is: 0x50b26b2a,
 	///  or in textual repr: setCollectionProperties((string,bytes)[])
-	function setCollectionProperties(Tuple21[] memory properties) public {
+	function setCollectionProperties(Tuple22[] memory properties) public {
 		require(false, stub_error);
 		properties;
 		dummy = 0;
@@ -186,11 +186,11 @@
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
-	function collectionProperties(string[] memory keys) public view returns (Tuple21[] memory) {
+	function collectionProperties(string[] memory keys) public view returns (Tuple22[] memory) {
 		require(false, stub_error);
 		keys;
 		dummy;
-		return new Tuple21[](0);
+		return new Tuple22[](0);
 	}
 
 	/// Set the sponsor of the collection.
@@ -251,10 +251,10 @@
 	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.
 	/// @dev EVM selector for this function is: 0x6ec0a9f1,
 	///  or in textual repr: collectionSponsor()
-	function collectionSponsor() public view returns (Tuple24 memory) {
+	function collectionSponsor() public view returns (Tuple25 memory) {
 		require(false, stub_error);
 		dummy;
-		return Tuple24(0x0000000000000000000000000000000000000000, 0);
+		return Tuple25(0x0000000000000000000000000000000000000000, 0);
 	}
 
 	/// Set limits for the collection.
@@ -538,13 +538,13 @@
 }
 
 /// @dev anonymous struct
-struct Tuple24 {
+struct Tuple25 {
 	address field_0;
 	uint256 field_1;
 }
 
 /// @dev anonymous struct
-struct Tuple21 {
+struct Tuple22 {
 	string field_0;
 	bytes field_1;
 }
@@ -693,7 +693,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x244543ee
+/// @dev the ERC-165 identifier for this interface is 0x0e9fc611
 contract ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -742,6 +742,20 @@
 		dummy = 0;
 	}
 
+	/// @notice Transfer ownership of an NFT
+	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	///  is the zero address. Throws if `tokenId` is not a valid NFT.
+	/// @param to The new owner
+	/// @param tokenId The NFT to transfer
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(EthCrossAccount memory to, uint256 tokenId) public {
+		require(false, stub_error);
+		to;
+		tokenId;
+		dummy = 0;
+	}
+
 	/// @notice Transfer ownership of an NFT from cross account address to cross account address
 	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
 	///  is the zero address. Throws if `tokenId` is not a valid NFT.
@@ -822,7 +836,7 @@
 	// /// @param tokens array of pairs of token ID and token URI for minted tokens
 	// /// @dev EVM selector for this function is: 0x36543006,
 	// ///  or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[])
-	// function mintBulkWithTokenURI(address to, Tuple10[] memory tokens) public returns (bool) {
+	// function mintBulkWithTokenURI(address to, Tuple11[] memory tokens) public returns (bool) {
 	// 	require(false, stub_error);
 	// 	to;
 	// 	tokens;
@@ -833,7 +847,7 @@
 }
 
 /// @dev anonymous struct
-struct Tuple10 {
+struct Tuple11 {
 	uint256 field_0;
 	string field_1;
 }
modifiedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -4,6 +4,12 @@
 
 <!-- bureaucrate goes here -->
 
+## [0.2.8] - 2022-11-14
+
+### Changed
+
+- Added `transfer_cross` in eth functions.
+
 ## [v0.2.7] - 2022-11-11
 
 ### Changed
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.7"
+version = "0.2.8"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
before · pallets/refungible/src/erc.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Refungible Pallet EVM API for tokens18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.2122extern crate alloc;2324use core::{25	char::{REPLACEMENT_CHARACTER, decode_utf16},26	convert::TryInto,27};28use evm_coder::{29	abi::AbiType, ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*,30	weight,31};32use frame_support::{BoundedBTreeMap, BoundedVec};33use pallet_common::{34	CollectionHandle, CollectionPropertyPermissions,35	erc::{CommonEvmHandler, CollectionCall, static_property::key},36};37use pallet_evm::{account::CrossAccountId, PrecompileHandle};38use pallet_evm_coder_substrate::{call, dispatch_to_evm};39use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};40use sp_core::H160;41use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};42use up_data_structs::{43	CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,44	PropertyKeyPermission, PropertyPermission, TokenId,45};4647use crate::{48	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,49	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,50};5152pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5354/// @title A contract that allows to set and delete token properties and change token property permissions.55#[solidity_interface(name = TokenProperties)]56impl<T: Config> RefungibleHandle<T> {57	/// @notice Set permissions for token property.58	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.59	/// @param key Property key.60	/// @param isMutable Permission to mutate property.61	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.62	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.63	fn set_token_property_permission(64		&mut self,65		caller: caller,66		key: string,67		is_mutable: bool,68		collection_admin: bool,69		token_owner: bool,70	) -> Result<()> {71		let caller = T::CrossAccountId::from_eth(caller);72		<Pallet<T>>::set_token_property_permissions(73			self,74			&caller,75			vec![PropertyKeyPermission {76				key: <Vec<u8>>::from(key)77					.try_into()78					.map_err(|_| "too long key")?,79				permission: PropertyPermission {80					mutable: is_mutable,81					collection_admin,82					token_owner,83				},84			}],85		)86		.map_err(dispatch_to_evm::<T>)87	}8889	/// @notice Set token property value.90	/// @dev Throws error if `msg.sender` has no permission to edit the property.91	/// @param tokenId ID of the token.92	/// @param key Property key.93	/// @param value Property value.94	fn set_property(95		&mut self,96		caller: caller,97		token_id: uint256,98		key: string,99		value: bytes,100	) -> Result<()> {101		let caller = T::CrossAccountId::from_eth(caller);102		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;103		let key = <Vec<u8>>::from(key)104			.try_into()105			.map_err(|_| "key too long")?;106		let value = value.0.try_into().map_err(|_| "value too long")?;107108		let nesting_budget = self109			.recorder110			.weight_calls_budget(<StructureWeight<T>>::find_parent());111112		<Pallet<T>>::set_token_property(113			self,114			&caller,115			TokenId(token_id),116			Property { key, value },117			&nesting_budget,118		)119		.map_err(dispatch_to_evm::<T>)120	}121122	/// @notice Set token properties value.123	/// @dev Throws error if `msg.sender` has no permission to edit the property.124	/// @param tokenId ID of the token.125	/// @param properties settable properties126	fn set_properties(127		&mut self,128		caller: caller,129		token_id: uint256,130		properties: Vec<(string, bytes)>,131	) -> Result<()> {132		let caller = T::CrossAccountId::from_eth(caller);133		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;134135		let nesting_budget = self136			.recorder137			.weight_calls_budget(<StructureWeight<T>>::find_parent());138139		let properties = properties140			.into_iter()141			.map(|(key, value)| {142				let key = <Vec<u8>>::from(key)143					.try_into()144					.map_err(|_| "key too large")?;145146				let value = value.0.try_into().map_err(|_| "value too large")?;147148				Ok(Property { key, value })149			})150			.collect::<Result<Vec<_>>>()?;151152		<Pallet<T>>::set_token_properties(153			self,154			&caller,155			TokenId(token_id),156			properties.into_iter(),157			<Pallet<T>>::token_exists(&self, TokenId(token_id)),158			&nesting_budget,159		)160		.map_err(dispatch_to_evm::<T>)161	}162163	/// @notice Delete token property value.164	/// @dev Throws error if `msg.sender` has no permission to edit the property.165	/// @param tokenId ID of the token.166	/// @param key Property key.167	#[solidity(hide)]168	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {169		let caller = T::CrossAccountId::from_eth(caller);170		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;171		let key = <Vec<u8>>::from(key)172			.try_into()173			.map_err(|_| "key too long")?;174175		let nesting_budget = self176			.recorder177			.weight_calls_budget(<StructureWeight<T>>::find_parent());178179		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)180			.map_err(dispatch_to_evm::<T>)181	}182183	/// @notice Delete token properties value.184	/// @dev Throws error if `msg.sender` has no permission to edit the property.185	/// @param tokenId ID of the token.186	/// @param keys Properties key.187	fn delete_properties(188		&mut self,189		token_id: uint256,190		caller: caller,191		keys: Vec<string>,192	) -> Result<()> {193		let caller = T::CrossAccountId::from_eth(caller);194		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;195		let keys = keys196			.into_iter()197			.map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))198			.collect::<Result<Vec<_>>>()?;199200		let nesting_budget = self201			.recorder202			.weight_calls_budget(<StructureWeight<T>>::find_parent());203204		<Pallet<T>>::delete_token_properties(205			self,206			&caller,207			TokenId(token_id),208			keys.into_iter(),209			&nesting_budget,210		)211		.map_err(dispatch_to_evm::<T>)212	}213214	/// @notice Get token property value.215	/// @dev Throws error if key not found216	/// @param tokenId ID of the token.217	/// @param key Property key.218	/// @return Property value bytes219	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {220		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;221		let key = <Vec<u8>>::from(key)222			.try_into()223			.map_err(|_| "key too long")?;224225		let props = <TokenProperties<T>>::get((self.id, token_id));226		let prop = props.get(&key).ok_or("key not found")?;227228		Ok(prop.to_vec().into())229	}230}231232#[derive(ToLog)]233pub enum ERC721Events {234	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed235	///  (`to` == 0). Exception: during contract creation, any number of RFTs236	///  may be created and assigned without emitting Transfer.237	Transfer {238		#[indexed]239		from: address,240		#[indexed]241		to: address,242		#[indexed]243		token_id: uint256,244	},245	/// @dev Not supported246	Approval {247		#[indexed]248		owner: address,249		#[indexed]250		approved: address,251		#[indexed]252		token_id: uint256,253	},254	/// @dev Not supported255	#[allow(dead_code)]256	ApprovalForAll {257		#[indexed]258		owner: address,259		#[indexed]260		operator: address,261		approved: bool,262	},263}264265#[derive(ToLog)]266pub enum ERC721UniqueMintableEvents {267	/// @dev Not supported268	#[allow(dead_code)]269	MintingFinished {},270}271272#[solidity_interface(name = ERC721Metadata)]273impl<T: Config> RefungibleHandle<T>274where275	T::AccountId: From<[u8; 32]>,276{277	/// @notice A descriptive name for a collection of NFTs in this contract278	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`279	#[solidity(hide, rename_selector = "name")]280	fn name_proxy(&self) -> Result<string> {281		self.name()282	}283284	/// @notice An abbreviated name for NFTs in this contract285	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`286	#[solidity(hide, rename_selector = "symbol")]287	fn symbol_proxy(&self) -> Result<string> {288		self.symbol()289	}290291	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.292	///293	/// @dev If the token has a `url` property and it is not empty, it is returned.294	///  Else If the collection does not have a property with key `schemaName` or its value is not equal to `ERC721Metadata`, it return an error `tokenURI not set`.295	///  If the collection property `baseURI` is empty or absent, return "" (empty string)296	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix297	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).298	///299	/// @return token's const_metadata300	#[solidity(rename_selector = "tokenURI")]301	fn token_uri(&self, token_id: uint256) -> Result<string> {302		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;303304		match get_token_property(self, token_id_u32, &key::url()).as_deref() {305			Err(_) | Ok("") => (),306			Ok(url) => {307				return Ok(url.into());308			}309		};310311		let base_uri =312			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())313				.map(BoundedVec::into_inner)314				.map(string::from_utf8)315				.transpose()316				.map_err(|e| {317					Error::Revert(alloc::format!(318						"Can not convert value \"baseURI\" to string with error \"{}\"",319						e320					))321				})?;322323		let base_uri = match base_uri.as_deref() {324			None | Some("") => {325				return Ok("".into());326			}327			Some(base_uri) => base_uri.into(),328		};329330		Ok(331			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {332				Err(_) | Ok("") => base_uri,333				Ok(suffix) => base_uri + suffix,334			},335		)336	}337}338339/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension340/// @dev See https://eips.ethereum.org/EIPS/eip-721341#[solidity_interface(name = ERC721Enumerable)]342impl<T: Config> RefungibleHandle<T> {343	/// @notice Enumerate valid RFTs344	/// @param index A counter less than `totalSupply()`345	/// @return The token identifier for the `index`th NFT,346	///  (sort order not specified)347	fn token_by_index(&self, index: uint256) -> Result<uint256> {348		Ok(index)349	}350351	/// Not implemented352	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {353		// TODO: Not implemetable354		Err("not implemented".into())355	}356357	/// @notice Count RFTs tracked by this contract358	/// @return A count of valid RFTs tracked by this contract, where each one of359	///  them has an assigned and queryable owner not equal to the zero address360	fn total_supply(&self) -> Result<uint256> {361		self.consume_store_reads(1)?;362		Ok(<Pallet<T>>::total_supply(self).into())363	}364}365366/// @title ERC-721 Non-Fungible Token Standard367/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md368#[solidity_interface(name = ERC721, events(ERC721Events))]369impl<T: Config> RefungibleHandle<T> {370	/// @notice Count all RFTs assigned to an owner371	/// @dev RFTs assigned to the zero address are considered invalid, and this372	///  function throws for queries about the zero address.373	/// @param owner An address for whom to query the balance374	/// @return The number of RFTs owned by `owner`, possibly zero375	fn balance_of(&self, owner: address) -> Result<uint256> {376		self.consume_store_reads(1)?;377		let owner = T::CrossAccountId::from_eth(owner);378		let balance = <AccountBalance<T>>::get((self.id, owner));379		Ok(balance.into())380	}381382	/// @notice Find the owner of an RFT383	/// @dev RFTs assigned to zero address are considered invalid, and queries384	///  about them do throw.385	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for386	///  the tokens that are partially owned.387	/// @param tokenId The identifier for an RFT388	/// @return The address of the owner of the RFT389	fn owner_of(&self, token_id: uint256) -> Result<address> {390		self.consume_store_reads(2)?;391		let token = token_id.try_into()?;392		let owner = <Pallet<T>>::token_owner(self.id, token);393		Ok(owner394			.map(|address| *address.as_eth())395			.unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))396	}397398	/// @dev Not implemented399	fn safe_transfer_from_with_data(400		&mut self,401		_from: address,402		_to: address,403		_token_id: uint256,404		_data: bytes,405	) -> Result<void> {406		// TODO: Not implemetable407		Err("not implemented".into())408	}409410	/// @dev Not implemented411	fn safe_transfer_from(412		&mut self,413		_from: address,414		_to: address,415		_token_id: uint256,416	) -> Result<void> {417		// TODO: Not implemetable418		Err("not implemented".into())419	}420421	/// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE422	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE423	///  THEY MAY BE PERMANENTLY LOST424	/// @dev Throws unless `msg.sender` is the current owner or an authorized425	///  operator for this RFT. Throws if `from` is not the current owner. Throws426	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.427	///  Throws if RFT pieces have multiple owners.428	/// @param from The current owner of the NFT429	/// @param to The new owner430	/// @param tokenId The NFT to transfer431	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]432	fn transfer_from(433		&mut self,434		caller: caller,435		from: address,436		to: address,437		token_id: uint256,438	) -> Result<void> {439		let caller = T::CrossAccountId::from_eth(caller);440		let from = T::CrossAccountId::from_eth(from);441		let to = T::CrossAccountId::from_eth(to);442		let token = token_id.try_into()?;443		let budget = self444			.recorder445			.weight_calls_budget(<StructureWeight<T>>::find_parent());446447		let balance = balance(&self, token, &from)?;448		ensure_single_owner(&self, token, balance)?;449450		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)451			.map_err(dispatch_to_evm::<T>)?;452453		Ok(())454	}455456	/// @dev Not implemented457	fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {458		Err("not implemented".into())459	}460461	/// @dev Not implemented462	fn set_approval_for_all(463		&mut self,464		_caller: caller,465		_operator: address,466		_approved: bool,467	) -> Result<void> {468		// TODO: Not implemetable469		Err("not implemented".into())470	}471472	/// @dev Not implemented473	fn get_approved(&self, _token_id: uint256) -> Result<address> {474		// TODO: Not implemetable475		Err("not implemented".into())476	}477478	/// @dev Not implemented479	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {480		// TODO: Not implemetable481		Err("not implemented".into())482	}483}484485/// Returns amount of pieces of `token` that `owner` have486pub fn balance<T: Config>(487	collection: &RefungibleHandle<T>,488	token: TokenId,489	owner: &T::CrossAccountId,490) -> Result<u128> {491	collection.consume_store_reads(1)?;492	let balance = <Balance<T>>::get((collection.id, token, &owner));493	Ok(balance)494}495496/// Throws if `owner_balance` is lower than total amount of `token` pieces497pub fn ensure_single_owner<T: Config>(498	collection: &RefungibleHandle<T>,499	token: TokenId,500	owner_balance: u128,501) -> Result<()> {502	collection.consume_store_reads(1)?;503	let total_supply = <TotalSupply<T>>::get((collection.id, token));504	if total_supply != owner_balance {505		return Err("token has multiple owners".into());506	}507	Ok(())508}509510/// @title ERC721 Token that can be irreversibly burned (destroyed).511#[solidity_interface(name = ERC721Burnable)]512impl<T: Config> RefungibleHandle<T> {513	/// @notice Burns a specific ERC721 token.514	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized515	///  operator of the current owner.516	/// @param tokenId The RFT to approve517	#[weight(<SelfWeightOf<T>>::burn_item_fully())]518	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {519		let caller = T::CrossAccountId::from_eth(caller);520		let token = token_id.try_into()?;521522		let balance = balance(&self, token, &caller)?;523		ensure_single_owner(&self, token, balance)?;524525		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;526		Ok(())527	}528}529530/// @title ERC721 minting logic.531#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]532impl<T: Config> RefungibleHandle<T> {533	fn minting_finished(&self) -> Result<bool> {534		Ok(false)535	}536537	/// @notice Function to mint token.538	/// @param to The new owner539	/// @return uint256 The id of the newly minted token540	#[weight(<SelfWeightOf<T>>::create_item())]541	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {542		let token_id: uint256 = <TokensMinted<T>>::get(self.id)543			.checked_add(1)544			.ok_or("item id overflow")?545			.into();546		self.mint_check_id(caller, to, token_id)?;547		Ok(token_id)548	}549550	/// @notice Function to mint token.551	/// @dev `tokenId` should be obtained with `nextTokenId` method,552	///  unlike standard, you can't specify it manually553	/// @param to The new owner554	/// @param tokenId ID of the minted RFT555	#[solidity(hide, rename_selector = "mint")]556	#[weight(<SelfWeightOf<T>>::create_item())]557	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {558		let caller = T::CrossAccountId::from_eth(caller);559		let to = T::CrossAccountId::from_eth(to);560		let token_id: u32 = token_id.try_into()?;561		let budget = self562			.recorder563			.weight_calls_budget(<StructureWeight<T>>::find_parent());564565		if <TokensMinted<T>>::get(self.id)566			.checked_add(1)567			.ok_or("item id overflow")?568			!= token_id569		{570			return Err("item id should be next".into());571		}572573		let users = [(to.clone(), 1)]574			.into_iter()575			.collect::<BTreeMap<_, _>>()576			.try_into()577			.unwrap();578		<Pallet<T>>::create_item(579			self,580			&caller,581			CreateItemData::<T::CrossAccountId> {582				users,583				properties: CollectionPropertiesVec::default(),584			},585			&budget,586		)587		.map_err(dispatch_to_evm::<T>)?;588589		Ok(true)590	}591592	/// @notice Function to mint token with the given tokenUri.593	/// @param to The new owner594	/// @param tokenUri Token URI that would be stored in the NFT properties595	/// @return uint256 The id of the newly minted token596	#[solidity(rename_selector = "mintWithTokenURI")]597	#[weight(<SelfWeightOf<T>>::create_item())]598	fn mint_with_token_uri(599		&mut self,600		caller: caller,601		to: address,602		token_uri: string,603	) -> Result<uint256> {604		let token_id: uint256 = <TokensMinted<T>>::get(self.id)605			.checked_add(1)606			.ok_or("item id overflow")?607			.into();608		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;609		Ok(token_id)610	}611612	/// @notice Function to mint token with the given tokenUri.613	/// @dev `tokenId` should be obtained with `nextTokenId` method,614	///  unlike standard, you can't specify it manually615	/// @param to The new owner616	/// @param tokenId ID of the minted RFT617	/// @param tokenUri Token URI that would be stored in the RFT properties618	#[solidity(hide, rename_selector = "mintWithTokenURI")]619	#[weight(<SelfWeightOf<T>>::create_item())]620	fn mint_with_token_uri_check_id(621		&mut self,622		caller: caller,623		to: address,624		token_id: uint256,625		token_uri: string,626	) -> Result<bool> {627		let key = key::url();628		let permission = get_token_permission::<T>(self.id, &key)?;629		if !permission.collection_admin {630			return Err("Operation is not allowed".into());631		}632633		let caller = T::CrossAccountId::from_eth(caller);634		let to = T::CrossAccountId::from_eth(to);635		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;636		let budget = self637			.recorder638			.weight_calls_budget(<StructureWeight<T>>::find_parent());639640		if <TokensMinted<T>>::get(self.id)641			.checked_add(1)642			.ok_or("item id overflow")?643			!= token_id644		{645			return Err("item id should be next".into());646		}647648		let mut properties = CollectionPropertiesVec::default();649		properties650			.try_push(Property {651				key,652				value: token_uri653					.into_bytes()654					.try_into()655					.map_err(|_| "token uri is too long")?,656			})657			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;658659		let users = [(to.clone(), 1)]660			.into_iter()661			.collect::<BTreeMap<_, _>>()662			.try_into()663			.unwrap();664		<Pallet<T>>::create_item(665			self,666			&caller,667			CreateItemData::<T::CrossAccountId> { users, properties },668			&budget,669		)670		.map_err(dispatch_to_evm::<T>)?;671		Ok(true)672	}673674	/// @dev Not implemented675	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {676		Err("not implementable".into())677	}678}679680fn get_token_property<T: Config>(681	collection: &CollectionHandle<T>,682	token_id: u32,683	key: &up_data_structs::PropertyKey,684) -> Result<string> {685	collection.consume_store_reads(1)?;686	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))687		.map_err(|_| Error::Revert("Token properties not found".into()))?;688	if let Some(property) = properties.get(key) {689		return Ok(string::from_utf8_lossy(property).into());690	}691692	Err("Property tokenURI not found".into())693}694695fn get_token_permission<T: Config>(696	collection_id: CollectionId,697	key: &PropertyKey,698) -> Result<PropertyPermission> {699	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)700		.map_err(|_| Error::Revert("No permissions for collection".into()))?;701	let a = token_property_permissions702		.get(key)703		.map(Clone::clone)704		.ok_or_else(|| {705			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();706			Error::Revert(alloc::format!("No permission for key {}", key))707		})?;708	Ok(a)709}710711/// @title Unique extensions for ERC721.712#[solidity_interface(name = ERC721UniqueExtensions)]713impl<T: Config> RefungibleHandle<T>714where715	T::AccountId: From<[u8; 32]>,716{717	/// @notice A descriptive name for a collection of NFTs in this contract718	fn name(&self) -> Result<string> {719		Ok(decode_utf16(self.name.iter().copied())720			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))721			.collect::<string>())722	}723724	/// @notice An abbreviated name for NFTs in this contract725	fn symbol(&self) -> Result<string> {726		Ok(string::from_utf8_lossy(&self.token_prefix).into())727	}728729	/// @notice Transfer ownership of an RFT730	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`731	///  is the zero address. Throws if `tokenId` is not a valid RFT.732	///  Throws if RFT pieces have multiple owners.733	/// @param to The new owner734	/// @param tokenId The RFT to transfer735	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]736	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {737		let caller = T::CrossAccountId::from_eth(caller);738		let to = T::CrossAccountId::from_eth(to);739		let token = token_id.try_into()?;740		let budget = self741			.recorder742			.weight_calls_budget(<StructureWeight<T>>::find_parent());743744		let balance = balance(self, token, &caller)?;745		ensure_single_owner(self, token, balance)?;746747		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)748			.map_err(dispatch_to_evm::<T>)?;749		Ok(())750	}751752	/// @notice Transfer ownership of an RFT753	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`754	///  is the zero address. Throws if `tokenId` is not a valid RFT.755	///  Throws if RFT pieces have multiple owners.756	/// @param to The new owner757	/// @param tokenId The RFT to transfer758	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]759	fn transfer_from_cross(760		&mut self,761		caller: caller,762		from: EthCrossAccount,763		to: EthCrossAccount,764		token_id: uint256,765	) -> Result<void> {766		let caller = T::CrossAccountId::from_eth(caller);767		let from = from.into_sub_cross_account::<T>()?;768		let to = to.into_sub_cross_account::<T>()?;769		let token_id = token_id.try_into()?;770		let budget = self771			.recorder772			.weight_calls_budget(<StructureWeight<T>>::find_parent());773774		let balance = balance(self, token_id, &from)?;775		ensure_single_owner(self, token_id, balance)?;776777		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)778			.map_err(dispatch_to_evm::<T>)?;779		Ok(())780	}781782	/// @notice Burns a specific ERC721 token.783	/// @dev Throws unless `msg.sender` is the current owner or an authorized784	///  operator for this RFT. Throws if `from` is not the current owner. Throws785	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.786	///  Throws if RFT pieces have multiple owners.787	/// @param from The current owner of the RFT788	/// @param tokenId The RFT to transfer789	#[weight(<SelfWeightOf<T>>::burn_from())]790	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {791		let caller = T::CrossAccountId::from_eth(caller);792		let from = T::CrossAccountId::from_eth(from);793		let token = token_id.try_into()?;794		let budget = self795			.recorder796			.weight_calls_budget(<StructureWeight<T>>::find_parent());797798		let balance = balance(self, token, &from)?;799		ensure_single_owner(self, token, balance)?;800801		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)802			.map_err(dispatch_to_evm::<T>)?;803		Ok(())804	}805806	/// @notice Burns a specific ERC721 token.807	/// @dev Throws unless `msg.sender` is the current owner or an authorized808	///  operator for this RFT. Throws if `from` is not the current owner. Throws809	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.810	///  Throws if RFT pieces have multiple owners.811	/// @param from The current owner of the RFT812	/// @param tokenId The RFT to transfer813	#[weight(<SelfWeightOf<T>>::burn_from())]814	fn burn_from_cross(815		&mut self,816		caller: caller,817		from: EthCrossAccount,818		token_id: uint256,819	) -> Result<void> {820		let caller = T::CrossAccountId::from_eth(caller);821		let from = from.into_sub_cross_account::<T>()?;822		let token = token_id.try_into()?;823		let budget = self824			.recorder825			.weight_calls_budget(<StructureWeight<T>>::find_parent());826827		let balance = balance(self, token, &from)?;828		ensure_single_owner(self, token, balance)?;829830		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)831			.map_err(dispatch_to_evm::<T>)?;832		Ok(())833	}834835	/// @notice Returns next free RFT ID.836	fn next_token_id(&self) -> Result<uint256> {837		self.consume_store_reads(1)?;838		Ok(<TokensMinted<T>>::get(self.id)839			.checked_add(1)840			.ok_or("item id overflow")?841			.into())842	}843844	/// @notice Function to mint multiple tokens.845	/// @dev `tokenIds` should be an array of consecutive numbers and first number846	///  should be obtained with `nextTokenId` method847	/// @param to The new owner848	/// @param tokenIds IDs of the minted RFTs849	#[solidity(hide)]850	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]851	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {852		let caller = T::CrossAccountId::from_eth(caller);853		let to = T::CrossAccountId::from_eth(to);854		let mut expected_index = <TokensMinted<T>>::get(self.id)855			.checked_add(1)856			.ok_or("item id overflow")?;857		let budget = self858			.recorder859			.weight_calls_budget(<StructureWeight<T>>::find_parent());860861		let total_tokens = token_ids.len();862		for id in token_ids.into_iter() {863			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;864			if id != expected_index {865				return Err("item id should be next".into());866			}867			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;868		}869		let users = [(to.clone(), 1)]870			.into_iter()871			.collect::<BTreeMap<_, _>>()872			.try_into()873			.unwrap();874		let create_item_data = CreateItemData::<T::CrossAccountId> {875			users,876			properties: CollectionPropertiesVec::default(),877		};878		let data = (0..total_tokens)879			.map(|_| create_item_data.clone())880			.collect();881882		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)883			.map_err(dispatch_to_evm::<T>)?;884		Ok(true)885	}886887	/// @notice Function to mint multiple tokens with the given tokenUris.888	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive889	///  numbers and first number should be obtained with `nextTokenId` method890	/// @param to The new owner891	/// @param tokens array of pairs of token ID and token URI for minted tokens892	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]893	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]894	fn mint_bulk_with_token_uri(895		&mut self,896		caller: caller,897		to: address,898		tokens: Vec<(uint256, string)>,899	) -> Result<bool> {900		let key = key::url();901		let caller = T::CrossAccountId::from_eth(caller);902		let to = T::CrossAccountId::from_eth(to);903		let mut expected_index = <TokensMinted<T>>::get(self.id)904			.checked_add(1)905			.ok_or("item id overflow")?;906		let budget = self907			.recorder908			.weight_calls_budget(<StructureWeight<T>>::find_parent());909910		let mut data = Vec::with_capacity(tokens.len());911		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]912			.into_iter()913			.collect::<BTreeMap<_, _>>()914			.try_into()915			.unwrap();916		for (id, token_uri) in tokens {917			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;918			if id != expected_index {919				return Err("item id should be next".into());920			}921			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;922923			let mut properties = CollectionPropertiesVec::default();924			properties925				.try_push(Property {926					key: key.clone(),927					value: token_uri928						.into_bytes()929						.try_into()930						.map_err(|_| "token uri is too long")?,931				})932				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;933934			let create_item_data = CreateItemData::<T::CrossAccountId> {935				users: users.clone(),936				properties,937			};938			data.push(create_item_data);939		}940941		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)942			.map_err(dispatch_to_evm::<T>)?;943		Ok(true)944	}945946	/// Returns EVM address for refungible token947	///948	/// @param token ID of the token949	fn token_contract_address(&self, token: uint256) -> Result<address> {950		Ok(T::EvmTokenAddressMapping::token_to_address(951			self.id,952			token.try_into().map_err(|_| "token id overflow")?,953		))954	}955}956957#[solidity_interface(958	name = UniqueRefungible,959	is(960		ERC721,961		ERC721Enumerable,962		ERC721UniqueExtensions,963		ERC721UniqueMintable,964		ERC721Burnable,965		ERC721Metadata(if(this.flags.erc721metadata)),966		Collection(via(common_mut returns CollectionHandle<T>)),967		TokenProperties,968	)969)]970impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}971972// Not a tests, but code generators973generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);974generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);975976impl<T: Config> CommonEvmHandler for RefungibleHandle<T>977where978	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,979{980	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");981	fn call(982		self,983		handle: &mut impl PrecompileHandle,984	) -> Option<pallet_common::erc::PrecompileResult> {985		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)986	}987}
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -67,7 +67,7 @@
 	/// @param properties settable properties
 	/// @dev EVM selector for this function is: 0x14ed3a6e,
 	///  or in textual repr: setProperties(uint256,(string,bytes)[])
-	function setProperties(uint256 tokenId, Tuple20[] memory properties) public {
+	function setProperties(uint256 tokenId, Tuple21[] memory properties) public {
 		require(false, stub_error);
 		tokenId;
 		properties;
@@ -137,7 +137,7 @@
 	/// @param properties Vector of properties key/value pair.
 	/// @dev EVM selector for this function is: 0x50b26b2a,
 	///  or in textual repr: setCollectionProperties((string,bytes)[])
-	function setCollectionProperties(Tuple20[] memory properties) public {
+	function setCollectionProperties(Tuple21[] memory properties) public {
 		require(false, stub_error);
 		properties;
 		dummy = 0;
@@ -186,11 +186,11 @@
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
-	function collectionProperties(string[] memory keys) public view returns (Tuple20[] memory) {
+	function collectionProperties(string[] memory keys) public view returns (Tuple21[] memory) {
 		require(false, stub_error);
 		keys;
 		dummy;
-		return new Tuple20[](0);
+		return new Tuple21[](0);
 	}
 
 	/// Set the sponsor of the collection.
@@ -251,10 +251,10 @@
 	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.
 	/// @dev EVM selector for this function is: 0x6ec0a9f1,
 	///  or in textual repr: collectionSponsor()
-	function collectionSponsor() public view returns (Tuple23 memory) {
+	function collectionSponsor() public view returns (Tuple24 memory) {
 		require(false, stub_error);
 		dummy;
-		return Tuple23(0x0000000000000000000000000000000000000000, 0);
+		return Tuple24(0x0000000000000000000000000000000000000000, 0);
 	}
 
 	/// Set limits for the collection.
@@ -538,13 +538,13 @@
 }
 
 /// @dev anonymous struct
-struct Tuple23 {
+struct Tuple24 {
 	address field_0;
 	uint256 field_1;
 }
 
 /// @dev anonymous struct
-struct Tuple20 {
+struct Tuple21 {
 	string field_0;
 	bytes field_1;
 }
@@ -691,7 +691,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x81feb398
+/// @dev the ERC-165 identifier for this interface is 0xab243667
 contract ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -732,6 +732,21 @@
 	///  Throws if RFT pieces have multiple owners.
 	/// @param to The new owner
 	/// @param tokenId The RFT to transfer
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(EthCrossAccount memory to, uint256 tokenId) public {
+		require(false, stub_error);
+		to;
+		tokenId;
+		dummy = 0;
+	}
+
+	/// @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
 	/// @dev EVM selector for this function is: 0xd5cf430b,
 	///  or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256)
 	function transferFromCross(
@@ -809,7 +824,7 @@
 	// /// @param tokens array of pairs of token ID and token URI for minted tokens
 	// /// @dev EVM selector for this function is: 0x36543006,
 	// ///  or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[])
-	// function mintBulkWithTokenURI(address to, Tuple9[] memory tokens) public returns (bool) {
+	// function mintBulkWithTokenURI(address to, Tuple10[] memory tokens) public returns (bool) {
 	// 	require(false, stub_error);
 	// 	to;
 	// 	tokens;
@@ -831,7 +846,7 @@
 }
 
 /// @dev anonymous struct
-struct Tuple9 {
+struct Tuple10 {
 	uint256 field_0;
 	string field_1;
 }
modifiedtests/src/eth/api/UniqueFungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueFungible.sol
+++ b/tests/src/eth/api/UniqueFungible.sol
@@ -28,7 +28,7 @@
 	/// @param properties Vector of properties key/value pair.
 	/// @dev EVM selector for this function is: 0x50b26b2a,
 	///  or in textual repr: setCollectionProperties((string,bytes)[])
-	function setCollectionProperties(Tuple14[] memory properties) external;
+	function setCollectionProperties(Tuple15[] memory properties) external;
 
 	/// Delete collection property.
 	///
@@ -60,7 +60,7 @@
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
-	function collectionProperties(string[] memory keys) external view returns (Tuple14[] memory);
+	function collectionProperties(string[] memory keys) external view returns (Tuple15[] memory);
 
 	/// Set the sponsor of the collection.
 	///
@@ -287,12 +287,12 @@
 }
 
 /// @dev anonymous struct
-struct Tuple14 {
+struct Tuple15 {
 	string field_0;
 	bytes field_1;
 }
 
-/// @dev the ERC-165 identifier for this interface is 0x032e5926
+/// @dev the ERC-165 identifier for this interface is 0x29f4dcd9
 interface ERC20UniqueExtensions is Dummy, ERC165 {
 	/// @dev EVM selector for this function is: 0x0ecd0ab0,
 	///  or in textual repr: approveCross((address,uint256),uint256)
@@ -322,6 +322,10 @@
 	///  or in textual repr: mintBulk((address,uint256)[])
 	function mintBulk(Tuple8[] memory amounts) external returns (bool);
 
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(EthCrossAccount memory to, uint256 amount) external returns (bool);
+
 	/// @dev EVM selector for this function is: 0xd5cf430b,
 	///  or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256)
 	function transferFromCross(
modifiedtests/src/eth/api/UniqueNFT.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -49,7 +49,7 @@
 	/// @param properties settable properties
 	/// @dev EVM selector for this function is: 0x14ed3a6e,
 	///  or in textual repr: setProperties(uint256,(string,bytes)[])
-	function setProperties(uint256 tokenId, Tuple21[] memory properties) external;
+	function setProperties(uint256 tokenId, Tuple22[] memory properties) external;
 
 	// /// @notice Delete token property value.
 	// /// @dev Throws error if `msg.sender` has no permission to edit the property.
@@ -93,7 +93,7 @@
 	/// @param properties Vector of properties key/value pair.
 	/// @dev EVM selector for this function is: 0x50b26b2a,
 	///  or in textual repr: setCollectionProperties((string,bytes)[])
-	function setCollectionProperties(Tuple21[] memory properties) external;
+	function setCollectionProperties(Tuple22[] memory properties) external;
 
 	/// Delete collection property.
 	///
@@ -125,7 +125,7 @@
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
-	function collectionProperties(string[] memory keys) external view returns (Tuple21[] memory);
+	function collectionProperties(string[] memory keys) external view returns (Tuple22[] memory);
 
 	/// Set the sponsor of the collection.
 	///
@@ -167,7 +167,7 @@
 	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.
 	/// @dev EVM selector for this function is: 0x6ec0a9f1,
 	///  or in textual repr: collectionSponsor()
-	function collectionSponsor() external view returns (Tuple24 memory);
+	function collectionSponsor() external view returns (Tuple25 memory);
 
 	/// Set limits for the collection.
 	/// @dev Throws error if limit not found.
@@ -352,13 +352,13 @@
 }
 
 /// @dev anonymous struct
-struct Tuple24 {
+struct Tuple25 {
 	address field_0;
 	uint256 field_1;
 }
 
 /// @dev anonymous struct
-struct Tuple21 {
+struct Tuple22 {
 	string field_0;
 	bytes field_1;
 }
@@ -458,7 +458,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x244543ee
+/// @dev the ERC-165 identifier for this interface is 0x0e9fc611
 interface ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -489,6 +489,15 @@
 	///  or in textual repr: transfer(address,uint256)
 	function transfer(address to, uint256 tokenId) external;
 
+	/// @notice Transfer ownership of an NFT
+	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
+	///  is the zero address. Throws if `tokenId` is not a valid NFT.
+	/// @param to The new owner
+	/// @param tokenId The NFT to transfer
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(EthCrossAccount memory to, uint256 tokenId) external;
+
 	/// @notice Transfer ownership of an NFT from cross account address to cross account address
 	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`
 	///  is the zero address. Throws if `tokenId` is not a valid NFT.
@@ -543,12 +552,12 @@
 	// /// @param tokens array of pairs of token ID and token URI for minted tokens
 	// /// @dev EVM selector for this function is: 0x36543006,
 	// ///  or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[])
-	// function mintBulkWithTokenURI(address to, Tuple10[] memory tokens) external returns (bool);
+	// function mintBulkWithTokenURI(address to, Tuple11[] memory tokens) external returns (bool);
 
 }
 
 /// @dev anonymous struct
-struct Tuple10 {
+struct Tuple11 {
 	uint256 field_0;
 	string field_1;
 }
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -49,7 +49,7 @@
 	/// @param properties settable properties
 	/// @dev EVM selector for this function is: 0x14ed3a6e,
 	///  or in textual repr: setProperties(uint256,(string,bytes)[])
-	function setProperties(uint256 tokenId, Tuple20[] memory properties) external;
+	function setProperties(uint256 tokenId, Tuple21[] memory properties) external;
 
 	// /// @notice Delete token property value.
 	// /// @dev Throws error if `msg.sender` has no permission to edit the property.
@@ -93,7 +93,7 @@
 	/// @param properties Vector of properties key/value pair.
 	/// @dev EVM selector for this function is: 0x50b26b2a,
 	///  or in textual repr: setCollectionProperties((string,bytes)[])
-	function setCollectionProperties(Tuple20[] memory properties) external;
+	function setCollectionProperties(Tuple21[] memory properties) external;
 
 	/// Delete collection property.
 	///
@@ -125,7 +125,7 @@
 	/// @return Vector of properties key/value pairs.
 	/// @dev EVM selector for this function is: 0x285fb8e6,
 	///  or in textual repr: collectionProperties(string[])
-	function collectionProperties(string[] memory keys) external view returns (Tuple20[] memory);
+	function collectionProperties(string[] memory keys) external view returns (Tuple21[] memory);
 
 	/// Set the sponsor of the collection.
 	///
@@ -167,7 +167,7 @@
 	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.
 	/// @dev EVM selector for this function is: 0x6ec0a9f1,
 	///  or in textual repr: collectionSponsor()
-	function collectionSponsor() external view returns (Tuple23 memory);
+	function collectionSponsor() external view returns (Tuple24 memory);
 
 	/// Set limits for the collection.
 	/// @dev Throws error if limit not found.
@@ -352,13 +352,13 @@
 }
 
 /// @dev anonymous struct
-struct Tuple23 {
+struct Tuple24 {
 	address field_0;
 	uint256 field_1;
 }
 
 /// @dev anonymous struct
-struct Tuple20 {
+struct Tuple21 {
 	string field_0;
 	bytes field_1;
 }
@@ -456,7 +456,7 @@
 }
 
 /// @title Unique extensions for ERC721.
-/// @dev the ERC-165 identifier for this interface is 0x81feb398
+/// @dev the ERC-165 identifier for this interface is 0xab243667
 interface ERC721UniqueExtensions is Dummy, ERC165 {
 	/// @notice A descriptive name for a collection of NFTs in this contract
 	/// @dev EVM selector for this function is: 0x06fdde03,
@@ -484,6 +484,16 @@
 	///  Throws if RFT pieces have multiple owners.
 	/// @param to The new owner
 	/// @param tokenId The RFT to transfer
+	/// @dev EVM selector for this function is: 0x2ada85ff,
+	///  or in textual repr: transferCross((address,uint256),uint256)
+	function transferCross(EthCrossAccount memory to, uint256 tokenId) external;
+
+	/// @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
 	/// @dev EVM selector for this function is: 0xd5cf430b,
 	///  or in textual repr: transferFromCross((address,uint256),(address,uint256),uint256)
 	function transferFromCross(
@@ -535,7 +545,7 @@
 	// /// @param tokens array of pairs of token ID and token URI for minted tokens
 	// /// @dev EVM selector for this function is: 0x36543006,
 	// ///  or in textual repr: mintBulkWithTokenURI(address,(uint256,string)[])
-	// function mintBulkWithTokenURI(address to, Tuple9[] memory tokens) external returns (bool);
+	// function mintBulkWithTokenURI(address to, Tuple10[] memory tokens) external returns (bool);
 
 	/// Returns EVM address for refungible token
 	///
@@ -546,7 +556,7 @@
 }
 
 /// @dev anonymous struct
-struct Tuple9 {
+struct Tuple10 {
 	uint256 field_0;
 	string field_1;
 }
modifiedtests/src/eth/base.test.tsdiffbeforeafterboth
--- a/tests/src/eth/base.test.ts
+++ b/tests/src/eth/base.test.ts
@@ -117,7 +117,7 @@
   });
 
   itEth('ERC721UniqueExtensions support', async ({helper}) => {
-    await checkInterface(helper, '0x244543ee', true, true);
+    await checkInterface(helper, '0x0e9fc611', true, true);
   });
 
   itEth('ERC721Burnable - 0x42966c68 - support', async ({helper}) => {
modifiedtests/src/eth/fungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/fungible.test.ts
+++ b/tests/src/eth/fungible.test.ts
@@ -230,6 +230,59 @@
     }
   });
 
+  itEth('Can perform transferCross()', async ({helper}) => {
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const to = helper.ethCrossAccount.fromAddress(receiver);
+    const toSubstrate = helper.ethCrossAccount.fromKeyringPair(donor);
+    const collection = await helper.ft.mintCollection(alice);
+    await collection.mint(alice, 200n, {Ethereum: owner});
+
+    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+    const contract = helper.ethNativeContract.collection(collectionAddress, 'ft', owner);
+
+    {
+      const result = await contract.methods.transferCross(to, 50).send({from: owner});
+      
+      const event = result.events.Transfer;
+      expect(event.address).to.be.equal(collectionAddress);
+      expect(event.returnValues.from).to.be.equal(owner);
+      expect(event.returnValues.to).to.be.equal(receiver);
+      expect(event.returnValues.value).to.be.equal('50');
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(owner).call();
+      expect(+balance).to.equal(150);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(50);
+    }
+    
+    {
+      const result = await contract.methods.transferCross(toSubstrate, 50).send({from: owner});
+      
+      const event = result.events.Transfer;
+      expect(event.address).to.be.equal(collectionAddress);
+      expect(event.returnValues.from).to.be.equal(owner);
+      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(donor.address));
+      expect(event.returnValues.value).to.be.equal('50');
+    }
+
+    {
+      const balance = await collection.getBalance({Ethereum: owner});
+      expect(balance).to.equal(100n);
+    }
+
+    {
+      const balance = await collection.getBalance({Substrate: donor.address});
+      expect(balance).to.equal(50n);
+    }
+    
+  });
+  
   itEth('Can perform transfer()', async ({helper}) => {
     const owner = await helper.eth.createAccountWithBalance(donor);
     const receiver = await helper.eth.createAccountWithBalance(donor);
modifiedtests/src/eth/fungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/fungibleAbi.json
+++ b/tests/src/eth/fungibleAbi.json
@@ -239,7 +239,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple14[]",
+        "internalType": "struct Tuple15[]",
         "name": "",
         "type": "tuple[]"
       }
@@ -496,7 +496,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple14[]",
+        "internalType": "struct Tuple15[]",
         "name": "properties",
         "type": "tuple[]"
       }
@@ -594,6 +594,24 @@
   },
   {
     "inputs": [
+      {
+        "components": [
+          { "internalType": "address", "name": "eth", "type": "address" },
+          { "internalType": "uint256", "name": "sub", "type": "uint256" }
+        ],
+        "internalType": "struct EthCrossAccount",
+        "name": "to",
+        "type": "tuple"
+      },
+      { "internalType": "uint256", "name": "amount", "type": "uint256" }
+    ],
+    "name": "transferCross",
+    "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
       { "internalType": "address", "name": "from", "type": "address" },
       { "internalType": "address", "name": "to", "type": "address" },
       { "internalType": "uint256", "name": "amount", "type": "uint256" }
modifiedtests/src/eth/nonFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/nonFungible.test.ts
+++ b/tests/src/eth/nonFungible.test.ts
@@ -402,6 +402,60 @@
       expect(+balance).to.equal(1);
     }
   });
+  
+  itEth('Can perform transferCross()', async ({helper}) => {
+    const collection = await helper.nft.mintCollection(minter, {});
+    const owner = await helper.eth.createAccountWithBalance(donor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const to = helper.ethCrossAccount.fromAddress(receiver);
+    const toSubstrate = helper.ethCrossAccount.fromKeyringPair(minter);
+    
+    const {tokenId} = await collection.mintToken(minter, {Ethereum: owner});
+
+    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+    const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+
+    {
+      const result = await contract.methods.transferCross(to, tokenId).send({from: owner});
+
+      const event = result.events.Transfer;
+      expect(event.address).to.be.equal(collectionAddress);
+      expect(event.returnValues.from).to.be.equal(owner);
+      expect(event.returnValues.to).to.be.equal(receiver);
+      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(owner).call();
+      expect(+balance).to.equal(0);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(1);
+    }
+    
+    {
+      const substrateResult = await contract.methods.transferCross(toSubstrate, tokenId).send({from: receiver});
+      
+
+      const event = substrateResult.events.Transfer;
+      expect(event.address).to.be.equal(collectionAddress);
+      expect(event.returnValues.from).to.be.equal(receiver);
+      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));
+      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(0);
+    }
+
+    {
+      const balance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});
+      expect(balance).to.be.contain(tokenId);
+    }
+  });
 });
 
 describe('NFT: Fees', () => {
modifiedtests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/nonFungibleAbi.json
+++ b/tests/src/eth/nonFungibleAbi.json
@@ -269,7 +269,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple21[]",
+        "internalType": "struct Tuple22[]",
         "name": "",
         "type": "tuple[]"
       }
@@ -293,7 +293,7 @@
           { "internalType": "address", "name": "field_0", "type": "address" },
           { "internalType": "uint256", "name": "field_1", "type": "uint256" }
         ],
-        "internalType": "struct Tuple24",
+        "internalType": "struct Tuple25",
         "name": "",
         "type": "tuple"
       }
@@ -611,7 +611,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple21[]",
+        "internalType": "struct Tuple22[]",
         "name": "properties",
         "type": "tuple[]"
       }
@@ -682,7 +682,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple21[]",
+        "internalType": "struct Tuple22[]",
         "name": "properties",
         "type": "tuple[]"
       }
@@ -778,6 +778,24 @@
   },
   {
     "inputs": [
+      {
+        "components": [
+          { "internalType": "address", "name": "eth", "type": "address" },
+          { "internalType": "uint256", "name": "sub", "type": "uint256" }
+        ],
+        "internalType": "struct EthCrossAccount",
+        "name": "to",
+        "type": "tuple"
+      },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "transferCross",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
       { "internalType": "address", "name": "from", "type": "address" },
       { "internalType": "address", "name": "to", "type": "address" },
       { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
modifiedtests/src/eth/reFungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/reFungible.test.ts
+++ b/tests/src/eth/reFungible.test.ts
@@ -359,6 +359,59 @@
       expect(+balance).to.equal(1);
     }
   });
+  
+  itEth('Can perform transferCross()', async ({helper}) => {
+    const caller = await helper.eth.createAccountWithBalance(donor);
+    const receiver = await helper.eth.createAccountWithBalance(donor);
+    const to = helper.ethCrossAccount.fromAddress(receiver);
+    const toSubstrate = helper.ethCrossAccount.fromKeyringPair(minter);
+    const collection = await helper.rft.mintCollection(minter, {});
+    const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+    const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
+
+    const {tokenId} = await collection.mintToken(minter, 1n, {Ethereum: caller});
+
+    {
+      const result = await contract.methods.transferCross(to, tokenId).send({from: caller});
+
+      const event = result.events.Transfer;
+      expect(event.address).to.equal(collectionAddress);
+      expect(event.returnValues.from).to.equal(caller);
+      expect(event.returnValues.to).to.equal(receiver);
+      expect(event.returnValues.tokenId).to.equal(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);
+    }
+    
+    {
+      const substrateResult = await contract.methods.transferCross(toSubstrate, tokenId).send({from: receiver});
+      
+
+      const event = substrateResult.events.Transfer;
+      expect(event.address).to.be.equal(collectionAddress);
+      expect(event.returnValues.from).to.be.equal(receiver);
+      expect(event.returnValues.to).to.be.equal(helper.address.substrateToEth(minter.address));
+      expect(event.returnValues.tokenId).to.be.equal(`${tokenId}`);
+    }
+
+    {
+      const balance = await contract.methods.balanceOf(receiver).call();
+      expect(+balance).to.equal(0);
+    }
+
+    {
+      const balance = await helper.nft.getTokensByAddress(collection.collectionId, {Substrate: minter.address});
+      expect(balance).to.be.contain(tokenId);
+    }
+  });
 
   itEth('transfer event on transfer from partial ownership to full ownership', async ({helper}) => {
     const caller = await helper.eth.createAccountWithBalance(donor);
modifiedtests/src/eth/reFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/reFungibleAbi.json
+++ b/tests/src/eth/reFungibleAbi.json
@@ -251,7 +251,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple20[]",
+        "internalType": "struct Tuple21[]",
         "name": "",
         "type": "tuple[]"
       }
@@ -275,7 +275,7 @@
           { "internalType": "address", "name": "field_0", "type": "address" },
           { "internalType": "uint256", "name": "field_1", "type": "uint256" }
         ],
-        "internalType": "struct Tuple23",
+        "internalType": "struct Tuple24",
         "name": "",
         "type": "tuple"
       }
@@ -593,7 +593,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple20[]",
+        "internalType": "struct Tuple21[]",
         "name": "properties",
         "type": "tuple[]"
       }
@@ -664,7 +664,7 @@
           { "internalType": "string", "name": "field_0", "type": "string" },
           { "internalType": "bytes", "name": "field_1", "type": "bytes" }
         ],
-        "internalType": "struct Tuple20[]",
+        "internalType": "struct Tuple21[]",
         "name": "properties",
         "type": "tuple[]"
       }
@@ -769,6 +769,24 @@
   },
   {
     "inputs": [
+      {
+        "components": [
+          { "internalType": "address", "name": "eth", "type": "address" },
+          { "internalType": "uint256", "name": "sub", "type": "uint256" }
+        ],
+        "internalType": "struct EthCrossAccount",
+        "name": "to",
+        "type": "tuple"
+      },
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
+    ],
+    "name": "transferCross",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
       { "internalType": "address", "name": "from", "type": "address" },
       { "internalType": "address", "name": "to", "type": "address" },
       { "internalType": "uint256", "name": "tokenId", "type": "uint256" }