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

difftreelog

feature: Added `transfer_cross` in eth functions.

PraetorP2022-11-14parent: #20c695f.patch.diff
in: master

22 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.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
before · pallets/nonfungible/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//! # Nonfungible Pallet EVM API18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Nonfungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.2122extern crate alloc;23use core::{24	char::{REPLACEMENT_CHARACTER, decode_utf16},25	convert::TryInto,26};27use evm_coder::{28	abi::AbiType, ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*,29	weight,30};31use frame_support::BoundedVec;32use up_data_structs::{33	TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,34	CollectionPropertiesVec,35};36use pallet_evm_coder_substrate::dispatch_to_evm;37use sp_std::vec::Vec;38use pallet_common::{39	erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},40	CollectionHandle, CollectionPropertyPermissions,41};42use pallet_evm::{account::CrossAccountId, PrecompileHandle};43use pallet_evm_coder_substrate::call;44use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};4546use crate::{47	AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,48	SelfWeightOf, weights::WeightInfo, TokenProperties,49};5051/// @title A contract that allows to set and delete token properties and change token property permissions.52#[solidity_interface(name = TokenProperties)]53impl<T: Config> NonfungibleHandle<T> {54	/// @notice Set permissions for token property.55	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.56	/// @param key Property key.57	/// @param isMutable Permission to mutate property.58	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.59	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.60	fn set_token_property_permission(61		&mut self,62		caller: caller,63		key: string,64		is_mutable: bool,65		collection_admin: bool,66		token_owner: bool,67	) -> Result<()> {68		let caller = T::CrossAccountId::from_eth(caller);69		<Pallet<T>>::set_property_permission(70			self,71			&caller,72			PropertyKeyPermission {73				key: <Vec<u8>>::from(key)74					.try_into()75					.map_err(|_| "too long key")?,76				permission: PropertyPermission {77					mutable: is_mutable,78					collection_admin,79					token_owner,80				},81			},82		)83		.map_err(dispatch_to_evm::<T>)84	}8586	/// @notice Set token property value.87	/// @dev Throws error if `msg.sender` has no permission to edit the property.88	/// @param tokenId ID of the token.89	/// @param key Property key.90	/// @param value Property value.91	fn set_property(92		&mut self,93		caller: caller,94		token_id: uint256,95		key: string,96		value: bytes,97	) -> Result<()> {98		let caller = T::CrossAccountId::from_eth(caller);99		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;100		let key = <Vec<u8>>::from(key)101			.try_into()102			.map_err(|_| "key too long")?;103		let value = value.0.try_into().map_err(|_| "value too long")?;104105		let nesting_budget = self106			.recorder107			.weight_calls_budget(<StructureWeight<T>>::find_parent());108109		<Pallet<T>>::set_token_property(110			self,111			&caller,112			TokenId(token_id),113			Property { key, value },114			&nesting_budget,115		)116		.map_err(dispatch_to_evm::<T>)117	}118119	/// @notice Set token properties value.120	/// @dev Throws error if `msg.sender` has no permission to edit the property.121	/// @param tokenId ID of the token.122	/// @param properties settable properties123	#[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]124	fn set_properties(125		&mut self,126		caller: caller,127		token_id: uint256,128		properties: Vec<(string, bytes)>,129	) -> Result<()> {130		let caller = T::CrossAccountId::from_eth(caller);131		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;132133		let nesting_budget = self134			.recorder135			.weight_calls_budget(<StructureWeight<T>>::find_parent());136137		let properties = properties138			.into_iter()139			.map(|(key, value)| {140				let key = <Vec<u8>>::from(key)141					.try_into()142					.map_err(|_| "key too large")?;143144				let value = value.0.try_into().map_err(|_| "value too large")?;145146				Ok(Property { key, value })147			})148			.collect::<Result<Vec<_>>>()?;149150		<Pallet<T>>::set_token_properties(151			self,152			&caller,153			TokenId(token_id),154			properties.into_iter(),155			<Pallet<T>>::token_exists(&self, TokenId(token_id)),156			&nesting_budget,157		)158		.map_err(dispatch_to_evm::<T>)159	}160161	/// @notice Delete token property value.162	/// @dev Throws error if `msg.sender` has no permission to edit the property.163	/// @param tokenId ID of the token.164	/// @param key Property key.165	#[solidity(hide)]166	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {167		let caller = T::CrossAccountId::from_eth(caller);168		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;169		let key = <Vec<u8>>::from(key)170			.try_into()171			.map_err(|_| "key too long")?;172173		let nesting_budget = self174			.recorder175			.weight_calls_budget(<StructureWeight<T>>::find_parent());176177		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)178			.map_err(dispatch_to_evm::<T>)179	}180181	/// @notice Delete token properties value.182	/// @dev Throws error if `msg.sender` has no permission to edit the property.183	/// @param tokenId ID of the token.184	/// @param keys Properties key.185	fn delete_properties(186		&mut self,187		token_id: uint256,188		caller: caller,189		keys: Vec<string>,190	) -> Result<()> {191		let caller = T::CrossAccountId::from_eth(caller);192		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;193		let keys = keys194			.into_iter()195			.map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))196			.collect::<Result<Vec<_>>>()?;197198		let nesting_budget = self199			.recorder200			.weight_calls_budget(<StructureWeight<T>>::find_parent());201202		<Pallet<T>>::delete_token_properties(203			self,204			&caller,205			TokenId(token_id),206			keys.into_iter(),207			&nesting_budget,208		)209		.map_err(dispatch_to_evm::<T>)210	}211212	/// @notice Get token property value.213	/// @dev Throws error if key not found214	/// @param tokenId ID of the token.215	/// @param key Property key.216	/// @return Property value bytes217	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {218		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;219		let key = <Vec<u8>>::from(key)220			.try_into()221			.map_err(|_| "key too long")?;222223		let props = <TokenProperties<T>>::get((self.id, token_id));224		let prop = props.get(&key).ok_or("key not found")?;225226		Ok(prop.to_vec().into())227	}228}229230#[derive(ToLog)]231pub enum ERC721Events {232	/// @dev This emits when ownership of any NFT changes by any mechanism.233	///  This event emits when NFTs are created (`from` == 0) and destroyed234	///  (`to` == 0). Exception: during contract creation, any number of NFTs235	///  may be created and assigned without emitting Transfer. At the time of236	///  any transfer, the approved address for that NFT (if any) is reset to none.237	Transfer {238		#[indexed]239		from: address,240		#[indexed]241		to: address,242		#[indexed]243		token_id: uint256,244	},245	/// @dev This emits when the approved address for an NFT is changed or246	///  reaffirmed. The zero address indicates there is no approved address.247	///  When a Transfer event emits, this also indicates that the approved248	///  address for that NFT (if any) is reset to none.249	Approval {250		#[indexed]251		owner: address,252		#[indexed]253		approved: address,254		#[indexed]255		token_id: uint256,256	},257	/// @dev This emits when an operator is enabled or disabled for an owner.258	///  The operator can manage all NFTs of the owner.259	#[allow(dead_code)]260	ApprovalForAll {261		#[indexed]262		owner: address,263		#[indexed]264		operator: address,265		approved: bool,266	},267}268269#[derive(ToLog)]270pub enum ERC721UniqueMintableEvents {271	#[allow(dead_code)]272	MintingFinished {},273}274275/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension276/// @dev See https://eips.ethereum.org/EIPS/eip-721277#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]278impl<T: Config> NonfungibleHandle<T>279where280	T::AccountId: From<[u8; 32]>,281{282	/// @notice A descriptive name for a collection of NFTs in this contract283	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`284	#[solidity(hide, rename_selector = "name")]285	fn name_proxy(&self) -> Result<string> {286		self.name()287	}288289	/// @notice An abbreviated name for NFTs in this contract290	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`291	#[solidity(hide, rename_selector = "symbol")]292	fn symbol_proxy(&self) -> Result<string> {293		self.symbol()294	}295296	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.297	///298	/// @dev If the token has a `url` property and it is not empty, it is returned.299	///  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`.300	///  If the collection property `baseURI` is empty or absent, return "" (empty string)301	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix302	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).303	///304	/// @return token's const_metadata305	#[solidity(rename_selector = "tokenURI")]306	fn token_uri(&self, token_id: uint256) -> Result<string> {307		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;308309		match get_token_property(self, token_id_u32, &key::url()).as_deref() {310			Err(_) | Ok("") => (),311			Ok(url) => {312				return Ok(url.into());313			}314		};315316		let base_uri =317			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())318				.map(BoundedVec::into_inner)319				.map(string::from_utf8)320				.transpose()321				.map_err(|e| {322					Error::Revert(alloc::format!(323						"Can not convert value \"baseURI\" to string with error \"{}\"",324						e325					))326				})?;327328		let base_uri = match base_uri.as_deref() {329			None | Some("") => {330				return Ok("".into());331			}332			Some(base_uri) => base_uri.into(),333		};334335		Ok(336			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {337				Err(_) | Ok("") => base_uri,338				Ok(suffix) => base_uri + suffix,339			},340		)341	}342}343344/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension345/// @dev See https://eips.ethereum.org/EIPS/eip-721346#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]347impl<T: Config> NonfungibleHandle<T> {348	/// @notice Enumerate valid NFTs349	/// @param index A counter less than `totalSupply()`350	/// @return The token identifier for the `index`th NFT,351	///  (sort order not specified)352	fn token_by_index(&self, index: uint256) -> Result<uint256> {353		Ok(index)354	}355356	/// @dev Not implemented357	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {358		// TODO: Not implemetable359		Err("not implemented".into())360	}361362	/// @notice Count NFTs tracked by this contract363	/// @return A count of valid NFTs tracked by this contract, where each one of364	///  them has an assigned and queryable owner not equal to the zero address365	fn total_supply(&self) -> Result<uint256> {366		self.consume_store_reads(1)?;367		Ok(<Pallet<T>>::total_supply(self).into())368	}369}370371/// @title ERC-721 Non-Fungible Token Standard372/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md373#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]374impl<T: Config> NonfungibleHandle<T> {375	/// @notice Count all NFTs assigned to an owner376	/// @dev NFTs assigned to the zero address are considered invalid, and this377	///  function throws for queries about the zero address.378	/// @param owner An address for whom to query the balance379	/// @return The number of NFTs owned by `owner`, possibly zero380	fn balance_of(&self, owner: address) -> Result<uint256> {381		self.consume_store_reads(1)?;382		let owner = T::CrossAccountId::from_eth(owner);383		let balance = <AccountBalance<T>>::get((self.id, owner));384		Ok(balance.into())385	}386	/// @notice Find the owner of an NFT387	/// @dev NFTs assigned to zero address are considered invalid, and queries388	///  about them do throw.389	/// @param tokenId The identifier for an NFT390	/// @return The address of the owner of the NFT391	fn owner_of(&self, token_id: uint256) -> Result<address> {392		self.consume_store_reads(1)?;393		let token: TokenId = token_id.try_into()?;394		Ok(*<TokenData<T>>::get((self.id, token))395			.ok_or("token not found")?396			.owner397			.as_eth())398	}399	/// @dev Not implemented400	#[solidity(rename_selector = "safeTransferFrom")]401	fn safe_transfer_from_with_data(402		&mut self,403		_from: address,404		_to: address,405		_token_id: uint256,406		_data: bytes,407	) -> Result<void> {408		// TODO: Not implemetable409		Err("not implemented".into())410	}411	/// @dev Not implemented412	fn safe_transfer_from(413		&mut self,414		_from: address,415		_to: address,416		_token_id: uint256,417	) -> Result<void> {418		// TODO: Not implemetable419		Err("not implemented".into())420	}421422	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE423	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE424	///  THEY MAY BE PERMANENTLY LOST425	/// @dev Throws unless `msg.sender` is the current owner or an authorized426	///  operator for this NFT. Throws if `from` is not the current owner. Throws427	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.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())]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		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)448			.map_err(dispatch_to_evm::<T>)?;449		Ok(())450	}451452	/// @notice Set or reaffirm the approved address for an NFT453	/// @dev The zero address indicates there is no approved address.454	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized455	///  operator of the current owner.456	/// @param approved The new approved NFT controller457	/// @param tokenId The NFT to approve458	#[weight(<SelfWeightOf<T>>::approve())]459	fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {460		let caller = T::CrossAccountId::from_eth(caller);461		let approved = T::CrossAccountId::from_eth(approved);462		let token = token_id.try_into()?;463464		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))465			.map_err(dispatch_to_evm::<T>)?;466		Ok(())467	}468469	/// @dev Not implemented470	fn set_approval_for_all(471		&mut self,472		_caller: caller,473		_operator: address,474		_approved: bool,475	) -> Result<void> {476		// TODO: Not implemetable477		Err("not implemented".into())478	}479480	/// @dev Not implemented481	fn get_approved(&self, _token_id: uint256) -> Result<address> {482		// TODO: Not implemetable483		Err("not implemented".into())484	}485486	/// @dev Not implemented487	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {488		// TODO: Not implemetable489		Err("not implemented".into())490	}491}492493/// @title ERC721 Token that can be irreversibly burned (destroyed).494#[solidity_interface(name = ERC721Burnable)]495impl<T: Config> NonfungibleHandle<T> {496	/// @notice Burns a specific ERC721 token.497	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized498	///  operator of the current owner.499	/// @param tokenId The NFT to approve500	#[weight(<SelfWeightOf<T>>::burn_item())]501	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {502		let caller = T::CrossAccountId::from_eth(caller);503		let token = token_id.try_into()?;504505		<Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;506		Ok(())507	}508}509510/// @title ERC721 minting logic.511#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]512impl<T: Config> NonfungibleHandle<T> {513	fn minting_finished(&self) -> Result<bool> {514		Ok(false)515	}516517	/// @notice Function to mint token.518	/// @param to The new owner519	/// @return uint256 The id of the newly minted token520	#[weight(<SelfWeightOf<T>>::create_item())]521	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {522		let token_id: uint256 = <TokensMinted<T>>::get(self.id)523			.checked_add(1)524			.ok_or("item id overflow")?525			.into();526		self.mint_check_id(caller, to, token_id)?;527		Ok(token_id)528	}529530	/// @notice Function to mint token.531	/// @dev `tokenId` should be obtained with `nextTokenId` method,532	///  unlike standard, you can't specify it manually533	/// @param to The new owner534	/// @param tokenId ID of the minted NFT535	#[solidity(hide, rename_selector = "mint")]536	#[weight(<SelfWeightOf<T>>::create_item())]537	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {538		let caller = T::CrossAccountId::from_eth(caller);539		let to = T::CrossAccountId::from_eth(to);540		let token_id: u32 = token_id.try_into()?;541		let budget = self542			.recorder543			.weight_calls_budget(<StructureWeight<T>>::find_parent());544545		if <TokensMinted<T>>::get(self.id)546			.checked_add(1)547			.ok_or("item id overflow")?548			!= token_id549		{550			return Err("item id should be next".into());551		}552553		<Pallet<T>>::create_item(554			self,555			&caller,556			CreateItemData::<T> {557				properties: BoundedVec::default(),558				owner: to,559			},560			&budget,561		)562		.map_err(dispatch_to_evm::<T>)?;563564		Ok(true)565	}566567	/// @notice Function to mint token with the given tokenUri.568	/// @param to The new owner569	/// @param tokenUri Token URI that would be stored in the NFT properties570	/// @return uint256 The id of the newly minted token571	#[solidity(rename_selector = "mintWithTokenURI")]572	#[weight(<SelfWeightOf<T>>::create_item())]573	fn mint_with_token_uri(574		&mut self,575		caller: caller,576		to: address,577		token_uri: string,578	) -> Result<uint256> {579		let token_id: uint256 = <TokensMinted<T>>::get(self.id)580			.checked_add(1)581			.ok_or("item id overflow")?582			.into();583		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;584		Ok(token_id)585	}586587	/// @notice Function to mint token with the given tokenUri.588	/// @dev `tokenId` should be obtained with `nextTokenId` method,589	///  unlike standard, you can't specify it manually590	/// @param to The new owner591	/// @param tokenId ID of the minted NFT592	/// @param tokenUri Token URI that would be stored in the NFT properties593	#[solidity(hide, rename_selector = "mintWithTokenURI")]594	#[weight(<SelfWeightOf<T>>::create_item())]595	fn mint_with_token_uri_check_id(596		&mut self,597		caller: caller,598		to: address,599		token_id: uint256,600		token_uri: string,601	) -> Result<bool> {602		let key = key::url();603		let permission = get_token_permission::<T>(self.id, &key)?;604		if !permission.collection_admin {605			return Err("Operation is not allowed".into());606		}607608		let caller = T::CrossAccountId::from_eth(caller);609		let to = T::CrossAccountId::from_eth(to);610		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;611		let budget = self612			.recorder613			.weight_calls_budget(<StructureWeight<T>>::find_parent());614615		if <TokensMinted<T>>::get(self.id)616			.checked_add(1)617			.ok_or("item id overflow")?618			!= token_id619		{620			return Err("item id should be next".into());621		}622623		let mut properties = CollectionPropertiesVec::default();624		properties625			.try_push(Property {626				key,627				value: token_uri628					.into_bytes()629					.try_into()630					.map_err(|_| "token uri is too long")?,631			})632			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;633634		<Pallet<T>>::create_item(635			self,636			&caller,637			CreateItemData::<T> {638				properties,639				owner: to,640			},641			&budget,642		)643		.map_err(dispatch_to_evm::<T>)?;644		Ok(true)645	}646647	/// @dev Not implemented648	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {649		Err("not implementable".into())650	}651}652653fn get_token_property<T: Config>(654	collection: &CollectionHandle<T>,655	token_id: u32,656	key: &up_data_structs::PropertyKey,657) -> Result<string> {658	collection.consume_store_reads(1)?;659	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))660		.map_err(|_| Error::Revert("Token properties not found".into()))?;661	if let Some(property) = properties.get(key) {662		return Ok(string::from_utf8_lossy(property).into());663	}664665	Err("Property tokenURI not found".into())666}667668fn get_token_permission<T: Config>(669	collection_id: CollectionId,670	key: &PropertyKey,671) -> Result<PropertyPermission> {672	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)673		.map_err(|_| Error::Revert("No permissions for collection".into()))?;674	let a = token_property_permissions675		.get(key)676		.map(Clone::clone)677		.ok_or_else(|| {678			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();679			Error::Revert(alloc::format!("No permission for key {}", key))680		})?;681	Ok(a)682}683684/// @title Unique extensions for ERC721.685#[solidity_interface(name = ERC721UniqueExtensions)]686impl<T: Config> NonfungibleHandle<T>687where688	T::AccountId: From<[u8; 32]>,689{690	/// @notice A descriptive name for a collection of NFTs in this contract691	fn name(&self) -> Result<string> {692		Ok(decode_utf16(self.name.iter().copied())693			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))694			.collect::<string>())695	}696697	/// @notice An abbreviated name for NFTs in this contract698	fn symbol(&self) -> Result<string> {699		Ok(string::from_utf8_lossy(&self.token_prefix).into())700	}701702	/// @notice Set or reaffirm the approved address for an NFT703	/// @dev The zero address indicates there is no approved address.704	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized705	///  operator of the current owner.706	/// @param approved The new substrate address approved NFT controller707	/// @param tokenId The NFT to approve708	#[weight(<SelfWeightOf<T>>::approve())]709	fn approve_cross(710		&mut self,711		caller: caller,712		approved: EthCrossAccount,713		token_id: uint256,714	) -> Result<void> {715		let caller = T::CrossAccountId::from_eth(caller);716		let approved = approved.into_sub_cross_account::<T>()?;717		let token = token_id.try_into()?;718719		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))720			.map_err(dispatch_to_evm::<T>)?;721		Ok(())722	}723724	/// @notice Transfer ownership of an NFT725	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`726	///  is the zero address. Throws if `tokenId` is not a valid NFT.727	/// @param to The new owner728	/// @param tokenId The NFT to transfer729	#[weight(<SelfWeightOf<T>>::transfer())]730	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {731		let caller = T::CrossAccountId::from_eth(caller);732		let to = T::CrossAccountId::from_eth(to);733		let token = token_id.try_into()?;734		let budget = self735			.recorder736			.weight_calls_budget(<StructureWeight<T>>::find_parent());737738		<Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;739		Ok(())740	}741742	/// @notice Transfer ownership of an NFT from cross account address to cross account address743	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`744	///  is the zero address. Throws if `tokenId` is not a valid NFT.745	/// @param from Cross acccount address of current owner746	/// @param to Cross acccount address of new owner747	/// @param tokenId The NFT to transfer748	#[weight(<SelfWeightOf<T>>::transfer())]749	fn transfer_from_cross(750		&mut self,751		caller: caller,752		from: EthCrossAccount,753		to: EthCrossAccount,754		token_id: uint256,755	) -> Result<void> {756		let caller = T::CrossAccountId::from_eth(caller);757		let from = from.into_sub_cross_account::<T>()?;758		let to = to.into_sub_cross_account::<T>()?;759		let token_id = token_id.try_into()?;760		let budget = self761			.recorder762			.weight_calls_budget(<StructureWeight<T>>::find_parent());763		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)764			.map_err(dispatch_to_evm::<T>)?;765		Ok(())766	}767768	/// @notice Burns a specific ERC721 token.769	/// @dev Throws unless `msg.sender` is the current owner or an authorized770	///  operator for this NFT. Throws if `from` is not the current owner. Throws771	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.772	/// @param from The current owner of the NFT773	/// @param tokenId The NFT to transfer774	#[weight(<SelfWeightOf<T>>::burn_from())]775	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {776		let caller = T::CrossAccountId::from_eth(caller);777		let from = T::CrossAccountId::from_eth(from);778		let token = token_id.try_into()?;779		let budget = self780			.recorder781			.weight_calls_budget(<StructureWeight<T>>::find_parent());782783		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)784			.map_err(dispatch_to_evm::<T>)?;785		Ok(())786	}787788	/// @notice Burns a specific ERC721 token.789	/// @dev Throws unless `msg.sender` is the current owner or an authorized790	///  operator for this NFT. Throws if `from` is not the current owner. Throws791	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.792	/// @param from The current owner of the NFT793	/// @param tokenId The NFT to transfer794	#[weight(<SelfWeightOf<T>>::burn_from())]795	fn burn_from_cross(796		&mut self,797		caller: caller,798		from: EthCrossAccount,799		token_id: uint256,800	) -> Result<void> {801		let caller = T::CrossAccountId::from_eth(caller);802		let from = from.into_sub_cross_account::<T>()?;803		let token = token_id.try_into()?;804		let budget = self805			.recorder806			.weight_calls_budget(<StructureWeight<T>>::find_parent());807808		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)809			.map_err(dispatch_to_evm::<T>)?;810		Ok(())811	}812813	/// @notice Returns next free NFT ID.814	fn next_token_id(&self) -> Result<uint256> {815		self.consume_store_reads(1)?;816		Ok(<TokensMinted<T>>::get(self.id)817			.checked_add(1)818			.ok_or("item id overflow")?819			.into())820	}821822	/// @notice Function to mint multiple tokens.823	/// @dev `tokenIds` should be an array of consecutive numbers and first number824	///  should be obtained with `nextTokenId` method825	/// @param to The new owner826	/// @param tokenIds IDs of the minted NFTs827	#[solidity(hide)]828	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]829	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {830		let caller = T::CrossAccountId::from_eth(caller);831		let to = T::CrossAccountId::from_eth(to);832		let mut expected_index = <TokensMinted<T>>::get(self.id)833			.checked_add(1)834			.ok_or("item id overflow")?;835		let budget = self836			.recorder837			.weight_calls_budget(<StructureWeight<T>>::find_parent());838839		let total_tokens = token_ids.len();840		for id in token_ids.into_iter() {841			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;842			if id != expected_index {843				return Err("item id should be next".into());844			}845			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;846		}847		let data = (0..total_tokens)848			.map(|_| CreateItemData::<T> {849				properties: BoundedVec::default(),850				owner: to.clone(),851			})852			.collect();853854		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)855			.map_err(dispatch_to_evm::<T>)?;856		Ok(true)857	}858859	/// @notice Function to mint multiple tokens with the given tokenUris.860	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive861	///  numbers and first number should be obtained with `nextTokenId` method862	/// @param to The new owner863	/// @param tokens array of pairs of token ID and token URI for minted tokens864	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]865	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]866	fn mint_bulk_with_token_uri(867		&mut self,868		caller: caller,869		to: address,870		tokens: Vec<(uint256, string)>,871	) -> Result<bool> {872		let key = key::url();873		let caller = T::CrossAccountId::from_eth(caller);874		let to = T::CrossAccountId::from_eth(to);875		let mut expected_index = <TokensMinted<T>>::get(self.id)876			.checked_add(1)877			.ok_or("item id overflow")?;878		let budget = self879			.recorder880			.weight_calls_budget(<StructureWeight<T>>::find_parent());881882		let mut data = Vec::with_capacity(tokens.len());883		for (id, token_uri) in tokens {884			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;885			if id != expected_index {886				return Err("item id should be next".into());887			}888			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;889890			let mut properties = CollectionPropertiesVec::default();891			properties892				.try_push(Property {893					key: key.clone(),894					value: token_uri895						.into_bytes()896						.try_into()897						.map_err(|_| "token uri is too long")?,898				})899				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;900901			data.push(CreateItemData::<T> {902				properties,903				owner: to.clone(),904			});905		}906907		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)908			.map_err(dispatch_to_evm::<T>)?;909		Ok(true)910	}911}912913#[solidity_interface(914	name = UniqueNFT,915	is(916		ERC721,917		ERC721Enumerable,918		ERC721UniqueExtensions,919		ERC721UniqueMintable,920		ERC721Burnable,921		ERC721Metadata(if(this.flags.erc721metadata)),922		Collection(via(common_mut returns CollectionHandle<T>)),923		TokenProperties,924	)925)]926impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}927928// Not a tests, but code generators929generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);930generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);931932impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>933where934	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,935{936	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");937938	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {939		call::<T, UniqueNFTCall<T>, _, _>(handle, self)940	}941}
after · pallets/nonfungible/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//! # Nonfungible Pallet EVM API18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Nonfungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.2122extern crate alloc;23use core::{24	char::{REPLACEMENT_CHARACTER, decode_utf16},25	convert::TryInto,26};27use evm_coder::{28	abi::AbiType, ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*,29	weight,30};31use frame_support::BoundedVec;32use up_data_structs::{33	TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,34	CollectionPropertiesVec,35};36use pallet_evm_coder_substrate::dispatch_to_evm;37use sp_std::vec::Vec;38use pallet_common::{39	erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},40	CollectionHandle, CollectionPropertyPermissions,41};42use pallet_evm::{account::CrossAccountId, PrecompileHandle};43use pallet_evm_coder_substrate::call;44use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};4546use crate::{47	AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,48	SelfWeightOf, weights::WeightInfo, TokenProperties,49};5051/// @title A contract that allows to set and delete token properties and change token property permissions.52#[solidity_interface(name = TokenProperties)]53impl<T: Config> NonfungibleHandle<T> {54	/// @notice Set permissions for token property.55	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.56	/// @param key Property key.57	/// @param isMutable Permission to mutate property.58	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.59	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.60	fn set_token_property_permission(61		&mut self,62		caller: caller,63		key: string,64		is_mutable: bool,65		collection_admin: bool,66		token_owner: bool,67	) -> Result<()> {68		let caller = T::CrossAccountId::from_eth(caller);69		<Pallet<T>>::set_property_permission(70			self,71			&caller,72			PropertyKeyPermission {73				key: <Vec<u8>>::from(key)74					.try_into()75					.map_err(|_| "too long key")?,76				permission: PropertyPermission {77					mutable: is_mutable,78					collection_admin,79					token_owner,80				},81			},82		)83		.map_err(dispatch_to_evm::<T>)84	}8586	/// @notice Set token property value.87	/// @dev Throws error if `msg.sender` has no permission to edit the property.88	/// @param tokenId ID of the token.89	/// @param key Property key.90	/// @param value Property value.91	fn set_property(92		&mut self,93		caller: caller,94		token_id: uint256,95		key: string,96		value: bytes,97	) -> Result<()> {98		let caller = T::CrossAccountId::from_eth(caller);99		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;100		let key = <Vec<u8>>::from(key)101			.try_into()102			.map_err(|_| "key too long")?;103		let value = value.0.try_into().map_err(|_| "value too long")?;104105		let nesting_budget = self106			.recorder107			.weight_calls_budget(<StructureWeight<T>>::find_parent());108109		<Pallet<T>>::set_token_property(110			self,111			&caller,112			TokenId(token_id),113			Property { key, value },114			&nesting_budget,115		)116		.map_err(dispatch_to_evm::<T>)117	}118119	/// @notice Set token properties value.120	/// @dev Throws error if `msg.sender` has no permission to edit the property.121	/// @param tokenId ID of the token.122	/// @param properties settable properties123	#[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]124	fn set_properties(125		&mut self,126		caller: caller,127		token_id: uint256,128		properties: Vec<(string, bytes)>,129	) -> Result<()> {130		let caller = T::CrossAccountId::from_eth(caller);131		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;132133		let nesting_budget = self134			.recorder135			.weight_calls_budget(<StructureWeight<T>>::find_parent());136137		let properties = properties138			.into_iter()139			.map(|(key, value)| {140				let key = <Vec<u8>>::from(key)141					.try_into()142					.map_err(|_| "key too large")?;143144				let value = value.0.try_into().map_err(|_| "value too large")?;145146				Ok(Property { key, value })147			})148			.collect::<Result<Vec<_>>>()?;149150		<Pallet<T>>::set_token_properties(151			self,152			&caller,153			TokenId(token_id),154			properties.into_iter(),155			<Pallet<T>>::token_exists(&self, TokenId(token_id)),156			&nesting_budget,157		)158		.map_err(dispatch_to_evm::<T>)159	}160161	/// @notice Delete token property value.162	/// @dev Throws error if `msg.sender` has no permission to edit the property.163	/// @param tokenId ID of the token.164	/// @param key Property key.165	#[solidity(hide)]166	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {167		let caller = T::CrossAccountId::from_eth(caller);168		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;169		let key = <Vec<u8>>::from(key)170			.try_into()171			.map_err(|_| "key too long")?;172173		let nesting_budget = self174			.recorder175			.weight_calls_budget(<StructureWeight<T>>::find_parent());176177		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)178			.map_err(dispatch_to_evm::<T>)179	}180181	/// @notice Delete token properties value.182	/// @dev Throws error if `msg.sender` has no permission to edit the property.183	/// @param tokenId ID of the token.184	/// @param keys Properties key.185	fn delete_properties(186		&mut self,187		token_id: uint256,188		caller: caller,189		keys: Vec<string>,190	) -> Result<()> {191		let caller = T::CrossAccountId::from_eth(caller);192		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;193		let keys = keys194			.into_iter()195			.map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))196			.collect::<Result<Vec<_>>>()?;197198		let nesting_budget = self199			.recorder200			.weight_calls_budget(<StructureWeight<T>>::find_parent());201202		<Pallet<T>>::delete_token_properties(203			self,204			&caller,205			TokenId(token_id),206			keys.into_iter(),207			&nesting_budget,208		)209		.map_err(dispatch_to_evm::<T>)210	}211212	/// @notice Get token property value.213	/// @dev Throws error if key not found214	/// @param tokenId ID of the token.215	/// @param key Property key.216	/// @return Property value bytes217	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {218		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;219		let key = <Vec<u8>>::from(key)220			.try_into()221			.map_err(|_| "key too long")?;222223		let props = <TokenProperties<T>>::get((self.id, token_id));224		let prop = props.get(&key).ok_or("key not found")?;225226		Ok(prop.to_vec().into())227	}228}229230#[derive(ToLog)]231pub enum ERC721Events {232	/// @dev This emits when ownership of any NFT changes by any mechanism.233	///  This event emits when NFTs are created (`from` == 0) and destroyed234	///  (`to` == 0). Exception: during contract creation, any number of NFTs235	///  may be created and assigned without emitting Transfer. At the time of236	///  any transfer, the approved address for that NFT (if any) is reset to none.237	Transfer {238		#[indexed]239		from: address,240		#[indexed]241		to: address,242		#[indexed]243		token_id: uint256,244	},245	/// @dev This emits when the approved address for an NFT is changed or246	///  reaffirmed. The zero address indicates there is no approved address.247	///  When a Transfer event emits, this also indicates that the approved248	///  address for that NFT (if any) is reset to none.249	Approval {250		#[indexed]251		owner: address,252		#[indexed]253		approved: address,254		#[indexed]255		token_id: uint256,256	},257	/// @dev This emits when an operator is enabled or disabled for an owner.258	///  The operator can manage all NFTs of the owner.259	#[allow(dead_code)]260	ApprovalForAll {261		#[indexed]262		owner: address,263		#[indexed]264		operator: address,265		approved: bool,266	},267}268269#[derive(ToLog)]270pub enum ERC721UniqueMintableEvents {271	#[allow(dead_code)]272	MintingFinished {},273}274275/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension276/// @dev See https://eips.ethereum.org/EIPS/eip-721277#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]278impl<T: Config> NonfungibleHandle<T>279where280	T::AccountId: From<[u8; 32]>,281{282	/// @notice A descriptive name for a collection of NFTs in this contract283	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`284	#[solidity(hide, rename_selector = "name")]285	fn name_proxy(&self) -> Result<string> {286		self.name()287	}288289	/// @notice An abbreviated name for NFTs in this contract290	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`291	#[solidity(hide, rename_selector = "symbol")]292	fn symbol_proxy(&self) -> Result<string> {293		self.symbol()294	}295296	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.297	///298	/// @dev If the token has a `url` property and it is not empty, it is returned.299	///  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`.300	///  If the collection property `baseURI` is empty or absent, return "" (empty string)301	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix302	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).303	///304	/// @return token's const_metadata305	#[solidity(rename_selector = "tokenURI")]306	fn token_uri(&self, token_id: uint256) -> Result<string> {307		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;308309		match get_token_property(self, token_id_u32, &key::url()).as_deref() {310			Err(_) | Ok("") => (),311			Ok(url) => {312				return Ok(url.into());313			}314		};315316		let base_uri =317			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())318				.map(BoundedVec::into_inner)319				.map(string::from_utf8)320				.transpose()321				.map_err(|e| {322					Error::Revert(alloc::format!(323						"Can not convert value \"baseURI\" to string with error \"{}\"",324						e325					))326				})?;327328		let base_uri = match base_uri.as_deref() {329			None | Some("") => {330				return Ok("".into());331			}332			Some(base_uri) => base_uri.into(),333		};334335		Ok(336			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {337				Err(_) | Ok("") => base_uri,338				Ok(suffix) => base_uri + suffix,339			},340		)341	}342}343344/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension345/// @dev See https://eips.ethereum.org/EIPS/eip-721346#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]347impl<T: Config> NonfungibleHandle<T> {348	/// @notice Enumerate valid NFTs349	/// @param index A counter less than `totalSupply()`350	/// @return The token identifier for the `index`th NFT,351	///  (sort order not specified)352	fn token_by_index(&self, index: uint256) -> Result<uint256> {353		Ok(index)354	}355356	/// @dev Not implemented357	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {358		// TODO: Not implemetable359		Err("not implemented".into())360	}361362	/// @notice Count NFTs tracked by this contract363	/// @return A count of valid NFTs tracked by this contract, where each one of364	///  them has an assigned and queryable owner not equal to the zero address365	fn total_supply(&self) -> Result<uint256> {366		self.consume_store_reads(1)?;367		Ok(<Pallet<T>>::total_supply(self).into())368	}369}370371/// @title ERC-721 Non-Fungible Token Standard372/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md373#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]374impl<T: Config> NonfungibleHandle<T> {375	/// @notice Count all NFTs assigned to an owner376	/// @dev NFTs assigned to the zero address are considered invalid, and this377	///  function throws for queries about the zero address.378	/// @param owner An address for whom to query the balance379	/// @return The number of NFTs owned by `owner`, possibly zero380	fn balance_of(&self, owner: address) -> Result<uint256> {381		self.consume_store_reads(1)?;382		let owner = T::CrossAccountId::from_eth(owner);383		let balance = <AccountBalance<T>>::get((self.id, owner));384		Ok(balance.into())385	}386	/// @notice Find the owner of an NFT387	/// @dev NFTs assigned to zero address are considered invalid, and queries388	///  about them do throw.389	/// @param tokenId The identifier for an NFT390	/// @return The address of the owner of the NFT391	fn owner_of(&self, token_id: uint256) -> Result<address> {392		self.consume_store_reads(1)?;393		let token: TokenId = token_id.try_into()?;394		Ok(*<TokenData<T>>::get((self.id, token))395			.ok_or("token not found")?396			.owner397			.as_eth())398	}399	/// @dev Not implemented400	#[solidity(rename_selector = "safeTransferFrom")]401	fn safe_transfer_from_with_data(402		&mut self,403		_from: address,404		_to: address,405		_token_id: uint256,406		_data: bytes,407	) -> Result<void> {408		// TODO: Not implemetable409		Err("not implemented".into())410	}411	/// @dev Not implemented412	fn safe_transfer_from(413		&mut self,414		_from: address,415		_to: address,416		_token_id: uint256,417	) -> Result<void> {418		// TODO: Not implemetable419		Err("not implemented".into())420	}421422	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE423	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE424	///  THEY MAY BE PERMANENTLY LOST425	/// @dev Throws unless `msg.sender` is the current owner or an authorized426	///  operator for this NFT. Throws if `from` is not the current owner. Throws427	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.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())]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		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)448			.map_err(dispatch_to_evm::<T>)?;449		Ok(())450	}451452	/// @notice Set or reaffirm the approved address for an NFT453	/// @dev The zero address indicates there is no approved address.454	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized455	///  operator of the current owner.456	/// @param approved The new approved NFT controller457	/// @param tokenId The NFT to approve458	#[weight(<SelfWeightOf<T>>::approve())]459	fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {460		let caller = T::CrossAccountId::from_eth(caller);461		let approved = T::CrossAccountId::from_eth(approved);462		let token = token_id.try_into()?;463464		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))465			.map_err(dispatch_to_evm::<T>)?;466		Ok(())467	}468469	/// @dev Not implemented470	fn set_approval_for_all(471		&mut self,472		_caller: caller,473		_operator: address,474		_approved: bool,475	) -> Result<void> {476		// TODO: Not implemetable477		Err("not implemented".into())478	}479480	/// @dev Not implemented481	fn get_approved(&self, _token_id: uint256) -> Result<address> {482		// TODO: Not implemetable483		Err("not implemented".into())484	}485486	/// @dev Not implemented487	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {488		// TODO: Not implemetable489		Err("not implemented".into())490	}491}492493/// @title ERC721 Token that can be irreversibly burned (destroyed).494#[solidity_interface(name = ERC721Burnable)]495impl<T: Config> NonfungibleHandle<T> {496	/// @notice Burns a specific ERC721 token.497	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized498	///  operator of the current owner.499	/// @param tokenId The NFT to approve500	#[weight(<SelfWeightOf<T>>::burn_item())]501	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {502		let caller = T::CrossAccountId::from_eth(caller);503		let token = token_id.try_into()?;504505		<Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;506		Ok(())507	}508}509510/// @title ERC721 minting logic.511#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]512impl<T: Config> NonfungibleHandle<T> {513	fn minting_finished(&self) -> Result<bool> {514		Ok(false)515	}516517	/// @notice Function to mint token.518	/// @param to The new owner519	/// @return uint256 The id of the newly minted token520	#[weight(<SelfWeightOf<T>>::create_item())]521	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {522		let token_id: uint256 = <TokensMinted<T>>::get(self.id)523			.checked_add(1)524			.ok_or("item id overflow")?525			.into();526		self.mint_check_id(caller, to, token_id)?;527		Ok(token_id)528	}529530	/// @notice Function to mint token.531	/// @dev `tokenId` should be obtained with `nextTokenId` method,532	///  unlike standard, you can't specify it manually533	/// @param to The new owner534	/// @param tokenId ID of the minted NFT535	#[solidity(hide, rename_selector = "mint")]536	#[weight(<SelfWeightOf<T>>::create_item())]537	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {538		let caller = T::CrossAccountId::from_eth(caller);539		let to = T::CrossAccountId::from_eth(to);540		let token_id: u32 = token_id.try_into()?;541		let budget = self542			.recorder543			.weight_calls_budget(<StructureWeight<T>>::find_parent());544545		if <TokensMinted<T>>::get(self.id)546			.checked_add(1)547			.ok_or("item id overflow")?548			!= token_id549		{550			return Err("item id should be next".into());551		}552553		<Pallet<T>>::create_item(554			self,555			&caller,556			CreateItemData::<T> {557				properties: BoundedVec::default(),558				owner: to,559			},560			&budget,561		)562		.map_err(dispatch_to_evm::<T>)?;563564		Ok(true)565	}566567	/// @notice Function to mint token with the given tokenUri.568	/// @param to The new owner569	/// @param tokenUri Token URI that would be stored in the NFT properties570	/// @return uint256 The id of the newly minted token571	#[solidity(rename_selector = "mintWithTokenURI")]572	#[weight(<SelfWeightOf<T>>::create_item())]573	fn mint_with_token_uri(574		&mut self,575		caller: caller,576		to: address,577		token_uri: string,578	) -> Result<uint256> {579		let token_id: uint256 = <TokensMinted<T>>::get(self.id)580			.checked_add(1)581			.ok_or("item id overflow")?582			.into();583		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;584		Ok(token_id)585	}586587	/// @notice Function to mint token with the given tokenUri.588	/// @dev `tokenId` should be obtained with `nextTokenId` method,589	///  unlike standard, you can't specify it manually590	/// @param to The new owner591	/// @param tokenId ID of the minted NFT592	/// @param tokenUri Token URI that would be stored in the NFT properties593	#[solidity(hide, rename_selector = "mintWithTokenURI")]594	#[weight(<SelfWeightOf<T>>::create_item())]595	fn mint_with_token_uri_check_id(596		&mut self,597		caller: caller,598		to: address,599		token_id: uint256,600		token_uri: string,601	) -> Result<bool> {602		let key = key::url();603		let permission = get_token_permission::<T>(self.id, &key)?;604		if !permission.collection_admin {605			return Err("Operation is not allowed".into());606		}607608		let caller = T::CrossAccountId::from_eth(caller);609		let to = T::CrossAccountId::from_eth(to);610		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;611		let budget = self612			.recorder613			.weight_calls_budget(<StructureWeight<T>>::find_parent());614615		if <TokensMinted<T>>::get(self.id)616			.checked_add(1)617			.ok_or("item id overflow")?618			!= token_id619		{620			return Err("item id should be next".into());621		}622623		let mut properties = CollectionPropertiesVec::default();624		properties625			.try_push(Property {626				key,627				value: token_uri628					.into_bytes()629					.try_into()630					.map_err(|_| "token uri is too long")?,631			})632			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;633634		<Pallet<T>>::create_item(635			self,636			&caller,637			CreateItemData::<T> {638				properties,639				owner: to,640			},641			&budget,642		)643		.map_err(dispatch_to_evm::<T>)?;644		Ok(true)645	}646647	/// @dev Not implemented648	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {649		Err("not implementable".into())650	}651}652653fn get_token_property<T: Config>(654	collection: &CollectionHandle<T>,655	token_id: u32,656	key: &up_data_structs::PropertyKey,657) -> Result<string> {658	collection.consume_store_reads(1)?;659	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))660		.map_err(|_| Error::Revert("Token properties not found".into()))?;661	if let Some(property) = properties.get(key) {662		return Ok(string::from_utf8_lossy(property).into());663	}664665	Err("Property tokenURI not found".into())666}667668fn get_token_permission<T: Config>(669	collection_id: CollectionId,670	key: &PropertyKey,671) -> Result<PropertyPermission> {672	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)673		.map_err(|_| Error::Revert("No permissions for collection".into()))?;674	let a = token_property_permissions675		.get(key)676		.map(Clone::clone)677		.ok_or_else(|| {678			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();679			Error::Revert(alloc::format!("No permission for key {}", key))680		})?;681	Ok(a)682}683684/// @title Unique extensions for ERC721.685#[solidity_interface(name = ERC721UniqueExtensions)]686impl<T: Config> NonfungibleHandle<T>687where688	T::AccountId: From<[u8; 32]>,689{690	/// @notice A descriptive name for a collection of NFTs in this contract691	fn name(&self) -> Result<string> {692		Ok(decode_utf16(self.name.iter().copied())693			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))694			.collect::<string>())695	}696697	/// @notice An abbreviated name for NFTs in this contract698	fn symbol(&self) -> Result<string> {699		Ok(string::from_utf8_lossy(&self.token_prefix).into())700	}701702	/// @notice Set or reaffirm the approved address for an NFT703	/// @dev The zero address indicates there is no approved address.704	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized705	///  operator of the current owner.706	/// @param approved The new substrate address approved NFT controller707	/// @param tokenId The NFT to approve708	#[weight(<SelfWeightOf<T>>::approve())]709	fn approve_cross(710		&mut self,711		caller: caller,712		approved: EthCrossAccount,713		token_id: uint256,714	) -> Result<void> {715		let caller = T::CrossAccountId::from_eth(caller);716		let approved = approved.into_sub_cross_account::<T>()?;717		let token = token_id.try_into()?;718719		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))720			.map_err(dispatch_to_evm::<T>)?;721		Ok(())722	}723724	/// @notice Transfer ownership of an NFT725	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`726	///  is the zero address. Throws if `tokenId` is not a valid NFT.727	/// @param to The new owner728	/// @param tokenId The NFT to transfer729	#[weight(<SelfWeightOf<T>>::transfer())]730	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {731		let caller = T::CrossAccountId::from_eth(caller);732		let to = T::CrossAccountId::from_eth(to);733		let token = token_id.try_into()?;734		let budget = self735			.recorder736			.weight_calls_budget(<StructureWeight<T>>::find_parent());737738		<Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;739		Ok(())740	}741	742	/// @notice Transfer ownership of an NFT743	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`744	///  is the zero address. Throws if `tokenId` is not a valid NFT.745	/// @param to The new owner746	/// @param tokenId The NFT to transfer747	#[weight(<SelfWeightOf<T>>::transfer())]748	fn transfer_cross(&mut self, caller: caller, to: EthCrossAccount, token_id: uint256) -> Result<void> {749		let caller = T::CrossAccountId::from_eth(caller);750		let to = to.into_sub_cross_account::<T>()?;751		let token = token_id.try_into()?;752		let budget = self753			.recorder754			.weight_calls_budget(<StructureWeight<T>>::find_parent());755756		<Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;757		Ok(())758	}759	760	/// @notice Transfer ownership of an NFT from cross account address to cross account address761	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`762	///  is the zero address. Throws if `tokenId` is not a valid NFT.763	/// @param from Cross acccount address of current owner764	/// @param to Cross acccount address of new owner765	/// @param tokenId The NFT to transfer766	#[weight(<SelfWeightOf<T>>::transfer())]767	fn transfer_from_cross(768		&mut self,769		caller: caller,770		from: EthCrossAccount,771		to: EthCrossAccount,772		token_id: uint256,773	) -> Result<void> {774		let caller = T::CrossAccountId::from_eth(caller);775		let from = from.into_sub_cross_account::<T>()?;776		let to = to.into_sub_cross_account::<T>()?;777		let token_id = token_id.try_into()?;778		let budget = self779			.recorder780			.weight_calls_budget(<StructureWeight<T>>::find_parent());781		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)782			.map_err(dispatch_to_evm::<T>)?;783		Ok(())784	}785786	/// @notice Burns a specific ERC721 token.787	/// @dev Throws unless `msg.sender` is the current owner or an authorized788	///  operator for this NFT. Throws if `from` is not the current owner. Throws789	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.790	/// @param from The current owner of the NFT791	/// @param tokenId The NFT to transfer792	#[weight(<SelfWeightOf<T>>::burn_from())]793	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {794		let caller = T::CrossAccountId::from_eth(caller);795		let from = T::CrossAccountId::from_eth(from);796		let token = token_id.try_into()?;797		let budget = self798			.recorder799			.weight_calls_budget(<StructureWeight<T>>::find_parent());800801		<Pallet<T>>::burn_from(self, &caller, &from, token, &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 NFT. Throws if `from` is not the current owner. Throws809	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.810	/// @param from The current owner of the NFT811	/// @param tokenId The NFT to transfer812	#[weight(<SelfWeightOf<T>>::burn_from())]813	fn burn_from_cross(814		&mut self,815		caller: caller,816		from: EthCrossAccount,817		token_id: uint256,818	) -> Result<void> {819		let caller = T::CrossAccountId::from_eth(caller);820		let from = from.into_sub_cross_account::<T>()?;821		let token = token_id.try_into()?;822		let budget = self823			.recorder824			.weight_calls_budget(<StructureWeight<T>>::find_parent());825826		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)827			.map_err(dispatch_to_evm::<T>)?;828		Ok(())829	}830831	/// @notice Returns next free NFT ID.832	fn next_token_id(&self) -> Result<uint256> {833		self.consume_store_reads(1)?;834		Ok(<TokensMinted<T>>::get(self.id)835			.checked_add(1)836			.ok_or("item id overflow")?837			.into())838	}839840	/// @notice Function to mint multiple tokens.841	/// @dev `tokenIds` should be an array of consecutive numbers and first number842	///  should be obtained with `nextTokenId` method843	/// @param to The new owner844	/// @param tokenIds IDs of the minted NFTs845	#[solidity(hide)]846	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]847	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {848		let caller = T::CrossAccountId::from_eth(caller);849		let to = T::CrossAccountId::from_eth(to);850		let mut expected_index = <TokensMinted<T>>::get(self.id)851			.checked_add(1)852			.ok_or("item id overflow")?;853		let budget = self854			.recorder855			.weight_calls_budget(<StructureWeight<T>>::find_parent());856857		let total_tokens = token_ids.len();858		for id in token_ids.into_iter() {859			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;860			if id != expected_index {861				return Err("item id should be next".into());862			}863			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;864		}865		let data = (0..total_tokens)866			.map(|_| CreateItemData::<T> {867				properties: BoundedVec::default(),868				owner: to.clone(),869			})870			.collect();871872		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)873			.map_err(dispatch_to_evm::<T>)?;874		Ok(true)875	}876877	/// @notice Function to mint multiple tokens with the given tokenUris.878	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive879	///  numbers and first number should be obtained with `nextTokenId` method880	/// @param to The new owner881	/// @param tokens array of pairs of token ID and token URI for minted tokens882	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]883	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]884	fn mint_bulk_with_token_uri(885		&mut self,886		caller: caller,887		to: address,888		tokens: Vec<(uint256, string)>,889	) -> Result<bool> {890		let key = key::url();891		let caller = T::CrossAccountId::from_eth(caller);892		let to = T::CrossAccountId::from_eth(to);893		let mut expected_index = <TokensMinted<T>>::get(self.id)894			.checked_add(1)895			.ok_or("item id overflow")?;896		let budget = self897			.recorder898			.weight_calls_budget(<StructureWeight<T>>::find_parent());899900		let mut data = Vec::with_capacity(tokens.len());901		for (id, token_uri) in tokens {902			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;903			if id != expected_index {904				return Err("item id should be next".into());905			}906			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;907908			let mut properties = CollectionPropertiesVec::default();909			properties910				.try_push(Property {911					key: key.clone(),912					value: token_uri913						.into_bytes()914						.try_into()915						.map_err(|_| "token uri is too long")?,916				})917				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;918919			data.push(CreateItemData::<T> {920				properties,921				owner: to.clone(),922			});923		}924925		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)926			.map_err(dispatch_to_evm::<T>)?;927		Ok(true)928	}929}930931#[solidity_interface(932	name = UniqueNFT,933	is(934		ERC721,935		ERC721Enumerable,936		ERC721UniqueExtensions,937		ERC721UniqueMintable,938		ERC721Burnable,939		ERC721Metadata(if(this.flags.erc721metadata)),940		Collection(via(common_mut returns CollectionHandle<T>)),941		TokenProperties,942	)943)]944impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}945946// Not a tests, but code generators947generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);948generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);949950impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>951where952	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,953{954	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");955956	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {957		call::<T, UniqueNFTCall<T>, _, _>(handle, self)958	}959}
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
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -756,6 +756,29 @@
 	/// @param to The new owner
 	/// @param tokenId The RFT to transfer
 	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]
+	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());
+
+		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 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
+	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]
 	fn transfer_from_cross(
 		&mut self,
 		caller: caller,
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/fungible.test.tsdiffbeforeafterboth
--- a/tests/src/eth/fungible.test.ts
+++ b/tests/src/eth/fungible.test.ts
@@ -230,6 +230,37 @@
     }
   });
 
+  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 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);
+    }
+  });
+  
   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,37 @@
       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 = helper.eth.createAccount();
+    const to = helper.ethCrossAccount.fromAddress(receiver);
+    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);
+    }
+  });
 });
 
 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,37 @@
       expect(+balance).to.equal(1);
     }
   });
+  
+  itEth('Can perform transferCross()', async ({helper}) => {
+    const caller = await helper.eth.createAccountWithBalance(donor);
+    const receiver = helper.eth.createAccount();
+    const to = helper.ethCrossAccount.fromAddress(receiver);
+    const {collectionAddress} = await helper.eth.createRFTCollection(caller, 'Transferry', '6', '6');
+    const contract = helper.ethNativeContract.collection(collectionAddress, 'rft', caller);
+
+    const result = await contract.methods.mint(caller).send();
+    const tokenId = result.events.Transfer.returnValues.tokenId;
+
+    {
+      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);
+    }
+  });
 
   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" }