git.delta.rocks / unique-network / refs/commits / 57a093883bee

difftreelog

Added `set_properties` method for `TokenProperties` interface.

PraetorP2022-10-24parent: #7125f4f.patch.diff
in: master

19 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6358,7 +6358,7 @@
 
 [[package]]
 name = "pallet-nonfungible"
-version = "0.1.5"
+version = "0.1.6"
 dependencies = [
  "ethereum",
  "evm-coder",
@@ -6480,7 +6480,7 @@
 
 [[package]]
 name = "pallet-refungible"
-version = "0.2.4"
+version = "0.2.5"
 dependencies = [
  "derivative",
  "ethereum",
modifiedpallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth
--- a/pallets/fungible/src/stubs/UniqueFungible.sol
+++ b/pallets/fungible/src/stubs/UniqueFungible.sol
@@ -385,7 +385,7 @@
 
 	/// Get collection owner.
 	///
-	/// @return Tuble with sponsor address and his substrate mirror.
+	/// @return Tuple with sponsor address and his substrate mirror.
 	/// If address is canonical then substrate mirror is zero and vice versa.
 	/// @dev EVM selector for this function is: 0xdf727d3b,
 	///  or in textual repr: collectionOwner()
modifiedpallets/nonfungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/nonfungible/CHANGELOG.md
+++ b/pallets/nonfungible/CHANGELOG.md
@@ -2,12 +2,20 @@
 
 All notable changes to this project will be documented in this file.
 
+<!-- bureaucrate goes here -->
+
+## [v0.1.6] - 2022-20-10
+
+### Change
+
+- Added `set_properties` method for `TokenProperties` interface.
+
 ## [v0.1.5] - 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.4] 2022-08-16
 
 ### Other changes
@@ -28,7 +36,9 @@
 - build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b
 
 ## [0.1.2] - 2022-07-25
+
 ### Changed
+
 - New `token_uri` retrieval logic:
 
       If the collection has a `url` property and it is not empty, it is returned.
@@ -39,8 +49,9 @@
       otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
 
 ## [0.1.1] - 2022-07-14
+
 ### Added
 
 - Implementation of RPC method `token_owners`.
-   For reasons of compatibility with this pallet, returns only one owner if token exists.
-   This was an internal request to improve the web interface and support fractionalization event.
+  For reasons of compatibility with this pallet, returns only one owner if token exists.
+  This was an internal request to improve the web interface and support fractionalization event.
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.5"
+version = "0.1.6"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/nonfungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -114,6 +114,47 @@
 		.map_err(dispatch_to_evm::<T>)
 	}
 
+	/// @notice Set token properties value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param properties settable properties
+	fn set_properties(
+		&mut self,
+		caller: caller,
+		token_id: uint256,
+		properties: Vec<(string, bytes)>,
+	) -> Result<()> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+
+		let nesting_budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+		let properties = properties
+			.into_iter()
+			.map(|(key, value)| {
+				let key = <Vec<u8>>::from(key)
+					.try_into()
+					.map_err(|_| "key too large")?;
+
+				let value = value.0.try_into().map_err(|_| "value too large")?;
+
+				Ok(Property { key, value })
+			})
+			.collect::<Result<Vec<_>>>()?;
+
+		<Pallet<T>>::set_token_properties(
+			self,
+			&caller,
+			TokenId(token_id),
+			properties.into_iter(),
+			<Pallet<T>>::token_exists(&self, TokenId(token_id)),
+			&nesting_budget,
+		)
+		.map_err(dispatch_to_evm::<T>)
+	}
+
 	/// @notice Delete token property value.
 	/// @dev Throws error if `msg.sender` has no permission to edit the property.
 	/// @param tokenId ID of the token.
modifiedpallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth
--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -18,7 +18,7 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
 contract TokenProperties is Dummy, ERC165 {
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -61,6 +61,19 @@
 		dummy = 0;
 	}
 
+	/// @notice Set token properties value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param properties settable properties
+	/// @dev EVM selector for this function is: 0x14ed3a6e,
+	///  or in textual repr: setProperties(uint256,(string,bytes)[])
+	function setProperties(uint256 tokenId, Tuple19[] memory properties) public {
+		require(false, stub_error);
+		tokenId;
+		properties;
+		dummy = 0;
+	}
+
 	/// @notice Delete token property value.
 	/// @dev Throws error if `msg.sender` has no permission to edit the property.
 	/// @param tokenId ID of the token.
@@ -458,7 +471,7 @@
 
 	/// Get collection owner.
 	///
-	/// @return Tuble with sponsor address and his substrate mirror.
+	/// @return Tuple with sponsor address and his substrate mirror.
 	/// If address is canonical then substrate mirror is zero and vice versa.
 	/// @dev EVM selector for this function is: 0xdf727d3b,
 	///  or in textual repr: collectionOwner()
modifiedpallets/refungible/CHANGELOG.mddiffbeforeafterboth
--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -2,12 +2,20 @@
 
 All notable changes to this project will be documented in this file.
 
+## [v0.2.5] - 2022-20-10
+
+### Change
+
+- Added `set_properties` method for `TokenProperties` interface.
+
 ## [v0.2.4] - 2022-08-24
 
 ### Change
- - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
 
+- Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
+
 <!-- bureaucrate goes here -->
+
 ## [v0.2.3] 2022-08-16
 
 ### Other changes
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.4"
+version = "0.2.5"
 license = "GPLv3"
 edition = "2021"
 
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
before · pallets/refungible/src/erc.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Refungible Pallet EVM API for tokens18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.2122extern crate alloc;2324use core::{25	char::{REPLACEMENT_CHARACTER, decode_utf16},26	convert::TryInto,27};28use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};29use frame_support::{BoundedBTreeMap, BoundedVec};30use pallet_common::{31	CollectionHandle, CollectionPropertyPermissions,32	erc::{CommonEvmHandler, CollectionCall, static_property::key},33	eth::convert_tuple_to_cross_account,34};35use pallet_evm::{account::CrossAccountId, PrecompileHandle};36use pallet_evm_coder_substrate::{call, dispatch_to_evm};37use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};38use sp_core::H160;39use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};40use up_data_structs::{41	CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,42	PropertyKeyPermission, PropertyPermission, TokenId,43};4445use crate::{46	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,47	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,48};4950pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5152/// @title A contract that allows to set and delete token properties and change token property permissions.53#[solidity_interface(name = TokenProperties)]54impl<T: Config> RefungibleHandle<T> {55	/// @notice Set permissions for token property.56	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.57	/// @param key Property key.58	/// @param isMutable Permission to mutate property.59	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.60	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.61	fn set_token_property_permission(62		&mut self,63		caller: caller,64		key: string,65		is_mutable: bool,66		collection_admin: bool,67		token_owner: bool,68	) -> Result<()> {69		let caller = T::CrossAccountId::from_eth(caller);70		<Pallet<T>>::set_token_property_permissions(71			self,72			&caller,73			vec![PropertyKeyPermission {74				key: <Vec<u8>>::from(key)75					.try_into()76					.map_err(|_| "too long key")?,77				permission: PropertyPermission {78					mutable: is_mutable,79					collection_admin,80					token_owner,81				},82			}],83		)84		.map_err(dispatch_to_evm::<T>)85	}8687	/// @notice Set token property value.88	/// @dev Throws error if `msg.sender` has no permission to edit the property.89	/// @param tokenId ID of the token.90	/// @param key Property key.91	/// @param value Property value.92	fn set_property(93		&mut self,94		caller: caller,95		token_id: uint256,96		key: string,97		value: bytes,98	) -> Result<()> {99		let caller = T::CrossAccountId::from_eth(caller);100		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;101		let key = <Vec<u8>>::from(key)102			.try_into()103			.map_err(|_| "key too long")?;104		let value = value.0.try_into().map_err(|_| "value too long")?;105106		let nesting_budget = self107			.recorder108			.weight_calls_budget(<StructureWeight<T>>::find_parent());109110		<Pallet<T>>::set_token_property(111			self,112			&caller,113			TokenId(token_id),114			Property { key, value },115			&nesting_budget,116		)117		.map_err(dispatch_to_evm::<T>)118	}119120	/// @notice Delete token property value.121	/// @dev Throws error if `msg.sender` has no permission to edit the property.122	/// @param tokenId ID of the token.123	/// @param key Property key.124	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {125		let caller = T::CrossAccountId::from_eth(caller);126		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;127		let key = <Vec<u8>>::from(key)128			.try_into()129			.map_err(|_| "key too long")?;130131		let nesting_budget = self132			.recorder133			.weight_calls_budget(<StructureWeight<T>>::find_parent());134135		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)136			.map_err(dispatch_to_evm::<T>)137	}138139	/// @notice Get token property value.140	/// @dev Throws error if key not found141	/// @param tokenId ID of the token.142	/// @param key Property key.143	/// @return Property value bytes144	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {145		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;146		let key = <Vec<u8>>::from(key)147			.try_into()148			.map_err(|_| "key too long")?;149150		let props = <TokenProperties<T>>::get((self.id, token_id));151		let prop = props.get(&key).ok_or("key not found")?;152153		Ok(prop.to_vec().into())154	}155}156157#[derive(ToLog)]158pub enum ERC721Events {159	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed160	///  (`to` == 0). Exception: during contract creation, any number of RFTs161	///  may be created and assigned without emitting Transfer.162	Transfer {163		#[indexed]164		from: address,165		#[indexed]166		to: address,167		#[indexed]168		token_id: uint256,169	},170	/// @dev Not supported171	Approval {172		#[indexed]173		owner: address,174		#[indexed]175		approved: address,176		#[indexed]177		token_id: uint256,178	},179	/// @dev Not supported180	#[allow(dead_code)]181	ApprovalForAll {182		#[indexed]183		owner: address,184		#[indexed]185		operator: address,186		approved: bool,187	},188}189190#[derive(ToLog)]191pub enum ERC721UniqueMintableEvents {192	/// @dev Not supported193	#[allow(dead_code)]194	MintingFinished {},195}196197#[solidity_interface(name = ERC721Metadata)]198impl<T: Config> RefungibleHandle<T>199where200	T::AccountId: From<[u8; 32]>,201{202	/// @notice A descriptive name for a collection of NFTs in this contract203	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`204	#[solidity(hide, rename_selector = "name")]205	fn name_proxy(&self) -> Result<string> {206		self.name()207	}208209	/// @notice An abbreviated name for NFTs in this contract210	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`211	#[solidity(hide, rename_selector = "symbol")]212	fn symbol_proxy(&self) -> Result<string> {213		self.symbol()214	}215216	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.217	///218	/// @dev If the token has a `url` property and it is not empty, it is returned.219	///  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`.220	///  If the collection property `baseURI` is empty or absent, return "" (empty string)221	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix222	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).223	///224	/// @return token's const_metadata225	#[solidity(rename_selector = "tokenURI")]226	fn token_uri(&self, token_id: uint256) -> Result<string> {227		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;228229		match get_token_property(self, token_id_u32, &key::url()).as_deref() {230			Err(_) | Ok("") => (),231			Ok(url) => {232				return Ok(url.into());233			}234		};235236		let base_uri =237			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())238				.map(BoundedVec::into_inner)239				.map(string::from_utf8)240				.transpose()241				.map_err(|e| {242					Error::Revert(alloc::format!(243						"Can not convert value \"baseURI\" to string with error \"{}\"",244						e245					))246				})?;247248		let base_uri = match base_uri.as_deref() {249			None | Some("") => {250				return Ok("".into());251			}252			Some(base_uri) => base_uri.into(),253		};254255		Ok(256			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {257				Err(_) | Ok("") => base_uri,258				Ok(suffix) => base_uri + suffix,259			},260		)261	}262}263264/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension265/// @dev See https://eips.ethereum.org/EIPS/eip-721266#[solidity_interface(name = ERC721Enumerable)]267impl<T: Config> RefungibleHandle<T> {268	/// @notice Enumerate valid RFTs269	/// @param index A counter less than `totalSupply()`270	/// @return The token identifier for the `index`th NFT,271	///  (sort order not specified)272	fn token_by_index(&self, index: uint256) -> Result<uint256> {273		Ok(index)274	}275276	/// Not implemented277	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {278		// TODO: Not implemetable279		Err("not implemented".into())280	}281282	/// @notice Count RFTs tracked by this contract283	/// @return A count of valid RFTs tracked by this contract, where each one of284	///  them has an assigned and queryable owner not equal to the zero address285	fn total_supply(&self) -> Result<uint256> {286		self.consume_store_reads(1)?;287		Ok(<Pallet<T>>::total_supply(self).into())288	}289}290291/// @title ERC-721 Non-Fungible Token Standard292/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md293#[solidity_interface(name = ERC721, events(ERC721Events))]294impl<T: Config> RefungibleHandle<T> {295	/// @notice Count all RFTs assigned to an owner296	/// @dev RFTs assigned to the zero address are considered invalid, and this297	///  function throws for queries about the zero address.298	/// @param owner An address for whom to query the balance299	/// @return The number of RFTs owned by `owner`, possibly zero300	fn balance_of(&self, owner: address) -> Result<uint256> {301		self.consume_store_reads(1)?;302		let owner = T::CrossAccountId::from_eth(owner);303		let balance = <AccountBalance<T>>::get((self.id, owner));304		Ok(balance.into())305	}306307	/// @notice Find the owner of an RFT308	/// @dev RFTs assigned to zero address are considered invalid, and queries309	///  about them do throw.310	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for311	///  the tokens that are partially owned.312	/// @param tokenId The identifier for an RFT313	/// @return The address of the owner of the RFT314	fn owner_of(&self, token_id: uint256) -> Result<address> {315		self.consume_store_reads(2)?;316		let token = token_id.try_into()?;317		let owner = <Pallet<T>>::token_owner(self.id, token);318		Ok(owner319			.map(|address| *address.as_eth())320			.unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))321	}322323	/// @dev Not implemented324	fn safe_transfer_from_with_data(325		&mut self,326		_from: address,327		_to: address,328		_token_id: uint256,329		_data: bytes,330	) -> Result<void> {331		// TODO: Not implemetable332		Err("not implemented".into())333	}334335	/// @dev Not implemented336	fn safe_transfer_from(337		&mut self,338		_from: address,339		_to: address,340		_token_id: uint256,341	) -> Result<void> {342		// TODO: Not implemetable343		Err("not implemented".into())344	}345346	/// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE347	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE348	///  THEY MAY BE PERMANENTLY LOST349	/// @dev Throws unless `msg.sender` is the current owner or an authorized350	///  operator for this RFT. Throws if `from` is not the current owner. Throws351	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.352	///  Throws if RFT pieces have multiple owners.353	/// @param from The current owner of the NFT354	/// @param to The new owner355	/// @param tokenId The NFT to transfer356	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]357	fn transfer_from(358		&mut self,359		caller: caller,360		from: address,361		to: address,362		token_id: uint256,363	) -> Result<void> {364		let caller = T::CrossAccountId::from_eth(caller);365		let from = T::CrossAccountId::from_eth(from);366		let to = T::CrossAccountId::from_eth(to);367		let token = token_id.try_into()?;368		let budget = self369			.recorder370			.weight_calls_budget(<StructureWeight<T>>::find_parent());371372		let balance = balance(&self, token, &from)?;373		ensure_single_owner(&self, token, balance)?;374375		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)376			.map_err(dispatch_to_evm::<T>)?;377378		Ok(())379	}380381	/// @dev Not implemented382	fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {383		Err("not implemented".into())384	}385386	/// @dev Not implemented387	fn set_approval_for_all(388		&mut self,389		_caller: caller,390		_operator: address,391		_approved: bool,392	) -> Result<void> {393		// TODO: Not implemetable394		Err("not implemented".into())395	}396397	/// @dev Not implemented398	fn get_approved(&self, _token_id: uint256) -> Result<address> {399		// TODO: Not implemetable400		Err("not implemented".into())401	}402403	/// @dev Not implemented404	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {405		// TODO: Not implemetable406		Err("not implemented".into())407	}408}409410/// Returns amount of pieces of `token` that `owner` have411pub fn balance<T: Config>(412	collection: &RefungibleHandle<T>,413	token: TokenId,414	owner: &T::CrossAccountId,415) -> Result<u128> {416	collection.consume_store_reads(1)?;417	let balance = <Balance<T>>::get((collection.id, token, &owner));418	Ok(balance)419}420421/// Throws if `owner_balance` is lower than total amount of `token` pieces422pub fn ensure_single_owner<T: Config>(423	collection: &RefungibleHandle<T>,424	token: TokenId,425	owner_balance: u128,426) -> Result<()> {427	collection.consume_store_reads(1)?;428	let total_supply = <TotalSupply<T>>::get((collection.id, token));429	if total_supply != owner_balance {430		return Err("token has multiple owners".into());431	}432	Ok(())433}434435/// @title ERC721 Token that can be irreversibly burned (destroyed).436#[solidity_interface(name = ERC721Burnable)]437impl<T: Config> RefungibleHandle<T> {438	/// @notice Burns a specific ERC721 token.439	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized440	///  operator of the current owner.441	/// @param tokenId The RFT to approve442	#[weight(<SelfWeightOf<T>>::burn_item_fully())]443	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {444		let caller = T::CrossAccountId::from_eth(caller);445		let token = token_id.try_into()?;446447		let balance = balance(&self, token, &caller)?;448		ensure_single_owner(&self, token, balance)?;449450		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;451		Ok(())452	}453}454455/// @title ERC721 minting logic.456#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]457impl<T: Config> RefungibleHandle<T> {458	fn minting_finished(&self) -> Result<bool> {459		Ok(false)460	}461462	/// @notice Function to mint token.463	/// @param to The new owner464	/// @return uint256 The id of the newly minted token465	#[weight(<SelfWeightOf<T>>::create_item())]466	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {467		let token_id: uint256 = <TokensMinted<T>>::get(self.id)468			.checked_add(1)469			.ok_or("item id overflow")?470			.into();471		self.mint_check_id(caller, to, token_id)?;472		Ok(token_id)473	}474475	/// @notice Function to mint token.476	/// @dev `tokenId` should be obtained with `nextTokenId` method,477	///  unlike standard, you can't specify it manually478	/// @param to The new owner479	/// @param tokenId ID of the minted RFT480	#[solidity(hide, rename_selector = "mint")]481	#[weight(<SelfWeightOf<T>>::create_item())]482	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {483		let caller = T::CrossAccountId::from_eth(caller);484		let to = T::CrossAccountId::from_eth(to);485		let token_id: u32 = token_id.try_into()?;486		let budget = self487			.recorder488			.weight_calls_budget(<StructureWeight<T>>::find_parent());489490		if <TokensMinted<T>>::get(self.id)491			.checked_add(1)492			.ok_or("item id overflow")?493			!= token_id494		{495			return Err("item id should be next".into());496		}497498		let users = [(to.clone(), 1)]499			.into_iter()500			.collect::<BTreeMap<_, _>>()501			.try_into()502			.unwrap();503		<Pallet<T>>::create_item(504			self,505			&caller,506			CreateItemData::<T::CrossAccountId> {507				users,508				properties: CollectionPropertiesVec::default(),509			},510			&budget,511		)512		.map_err(dispatch_to_evm::<T>)?;513514		Ok(true)515	}516517	/// @notice Function to mint token with the given tokenUri.518	/// @param to The new owner519	/// @param tokenUri Token URI that would be stored in the NFT properties520	/// @return uint256 The id of the newly minted token521	#[solidity(rename_selector = "mintWithTokenURI")]522	#[weight(<SelfWeightOf<T>>::create_item())]523	fn mint_with_token_uri(524		&mut self,525		caller: caller,526		to: address,527		token_uri: string,528	) -> Result<uint256> {529		let token_id: uint256 = <TokensMinted<T>>::get(self.id)530			.checked_add(1)531			.ok_or("item id overflow")?532			.into();533		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;534		Ok(token_id)535	}536537	/// @notice Function to mint token with the given tokenUri.538	/// @dev `tokenId` should be obtained with `nextTokenId` method,539	///  unlike standard, you can't specify it manually540	/// @param to The new owner541	/// @param tokenId ID of the minted RFT542	/// @param tokenUri Token URI that would be stored in the RFT properties543	#[solidity(hide, rename_selector = "mintWithTokenURI")]544	#[weight(<SelfWeightOf<T>>::create_item())]545	fn mint_with_token_uri_check_id(546		&mut self,547		caller: caller,548		to: address,549		token_id: uint256,550		token_uri: string,551	) -> Result<bool> {552		let key = key::url();553		let permission = get_token_permission::<T>(self.id, &key)?;554		if !permission.collection_admin {555			return Err("Operation is not allowed".into());556		}557558		let caller = T::CrossAccountId::from_eth(caller);559		let to = T::CrossAccountId::from_eth(to);560		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;561		let budget = self562			.recorder563			.weight_calls_budget(<StructureWeight<T>>::find_parent());564565		if <TokensMinted<T>>::get(self.id)566			.checked_add(1)567			.ok_or("item id overflow")?568			!= token_id569		{570			return Err("item id should be next".into());571		}572573		let mut properties = CollectionPropertiesVec::default();574		properties575			.try_push(Property {576				key,577				value: token_uri578					.into_bytes()579					.try_into()580					.map_err(|_| "token uri is too long")?,581			})582			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;583584		let users = [(to.clone(), 1)]585			.into_iter()586			.collect::<BTreeMap<_, _>>()587			.try_into()588			.unwrap();589		<Pallet<T>>::create_item(590			self,591			&caller,592			CreateItemData::<T::CrossAccountId> { users, properties },593			&budget,594		)595		.map_err(dispatch_to_evm::<T>)?;596		Ok(true)597	}598599	/// @dev Not implemented600	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {601		Err("not implementable".into())602	}603}604605fn get_token_property<T: Config>(606	collection: &CollectionHandle<T>,607	token_id: u32,608	key: &up_data_structs::PropertyKey,609) -> Result<string> {610	collection.consume_store_reads(1)?;611	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))612		.map_err(|_| Error::Revert("Token properties not found".into()))?;613	if let Some(property) = properties.get(key) {614		return Ok(string::from_utf8_lossy(property).into());615	}616617	Err("Property tokenURI not found".into())618}619620fn get_token_permission<T: Config>(621	collection_id: CollectionId,622	key: &PropertyKey,623) -> Result<PropertyPermission> {624	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)625		.map_err(|_| Error::Revert("No permissions for collection".into()))?;626	let a = token_property_permissions627		.get(key)628		.map(Clone::clone)629		.ok_or_else(|| {630			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();631			Error::Revert(alloc::format!("No permission for key {}", key))632		})?;633	Ok(a)634}635636/// @title Unique extensions for ERC721.637#[solidity_interface(name = ERC721UniqueExtensions)]638impl<T: Config> RefungibleHandle<T>639where640	T::AccountId: From<[u8; 32]>,641{642	/// @notice A descriptive name for a collection of NFTs in this contract643	fn name(&self) -> Result<string> {644		Ok(decode_utf16(self.name.iter().copied())645			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))646			.collect::<string>())647	}648649	/// @notice An abbreviated name for NFTs in this contract650	fn symbol(&self) -> Result<string> {651		Ok(string::from_utf8_lossy(&self.token_prefix).into())652	}653654	/// @notice Transfer ownership of an RFT655	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`656	///  is the zero address. Throws if `tokenId` is not a valid RFT.657	///  Throws if RFT pieces have multiple owners.658	/// @param to The new owner659	/// @param tokenId The RFT to transfer660	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]661	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {662		let caller = T::CrossAccountId::from_eth(caller);663		let to = T::CrossAccountId::from_eth(to);664		let token = token_id.try_into()?;665		let budget = self666			.recorder667			.weight_calls_budget(<StructureWeight<T>>::find_parent());668669		let balance = balance(self, token, &caller)?;670		ensure_single_owner(self, token, balance)?;671672		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)673			.map_err(dispatch_to_evm::<T>)?;674		Ok(())675	}676677	/// @notice Transfer ownership of an RFT678	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`679	///  is the zero address. Throws if `tokenId` is not a valid RFT.680	///  Throws if RFT pieces have multiple owners.681	/// @param to The new owner682	/// @param tokenId The RFT to transfer683	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]684	fn transfer_from_cross(685		&mut self,686		caller: caller,687		from: (address, uint256),688		to: (address, uint256),689		token_id: uint256,690	) -> Result<void> {691		let caller = T::CrossAccountId::from_eth(caller);692		let from = convert_tuple_to_cross_account::<T>(from)?;693		let to = convert_tuple_to_cross_account::<T>(to)?;694		let token_id = token_id.try_into()?;695		let budget = self696			.recorder697			.weight_calls_budget(<StructureWeight<T>>::find_parent());698699		let balance = balance(self, token_id, &from)?;700		ensure_single_owner(self, token_id, balance)?;701702		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)703			.map_err(dispatch_to_evm::<T>)?;704		Ok(())705	}706707	/// @notice Burns a specific ERC721 token.708	/// @dev Throws unless `msg.sender` is the current owner or an authorized709	///  operator for this RFT. Throws if `from` is not the current owner. Throws710	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.711	///  Throws if RFT pieces have multiple owners.712	/// @param from The current owner of the RFT713	/// @param tokenId The RFT to transfer714	#[weight(<SelfWeightOf<T>>::burn_from())]715	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {716		let caller = T::CrossAccountId::from_eth(caller);717		let from = T::CrossAccountId::from_eth(from);718		let token = token_id.try_into()?;719		let budget = self720			.recorder721			.weight_calls_budget(<StructureWeight<T>>::find_parent());722723		let balance = balance(self, token, &from)?;724		ensure_single_owner(self, token, balance)?;725726		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)727			.map_err(dispatch_to_evm::<T>)?;728		Ok(())729	}730731	/// @notice Burns a specific ERC721 token.732	/// @dev Throws unless `msg.sender` is the current owner or an authorized733	///  operator for this RFT. Throws if `from` is not the current owner. Throws734	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.735	///  Throws if RFT pieces have multiple owners.736	/// @param from The current owner of the RFT737	/// @param tokenId The RFT to transfer738	#[weight(<SelfWeightOf<T>>::burn_from())]739	fn burn_from_cross(740		&mut self,741		caller: caller,742		from: (address, uint256),743		token_id: uint256,744	) -> Result<void> {745		let caller = T::CrossAccountId::from_eth(caller);746		let from = convert_tuple_to_cross_account::<T>(from)?;747		let token = token_id.try_into()?;748		let budget = self749			.recorder750			.weight_calls_budget(<StructureWeight<T>>::find_parent());751752		let balance = balance(self, token, &from)?;753		ensure_single_owner(self, token, balance)?;754755		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)756			.map_err(dispatch_to_evm::<T>)?;757		Ok(())758	}759760	/// @notice Returns next free RFT ID.761	fn next_token_id(&self) -> Result<uint256> {762		self.consume_store_reads(1)?;763		Ok(<TokensMinted<T>>::get(self.id)764			.checked_add(1)765			.ok_or("item id overflow")?766			.into())767	}768769	/// @notice Function to mint multiple tokens.770	/// @dev `tokenIds` should be an array of consecutive numbers and first number771	///  should be obtained with `nextTokenId` method772	/// @param to The new owner773	/// @param tokenIds IDs of the minted RFTs774	#[solidity(hide)]775	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]776	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {777		let caller = T::CrossAccountId::from_eth(caller);778		let to = T::CrossAccountId::from_eth(to);779		let mut expected_index = <TokensMinted<T>>::get(self.id)780			.checked_add(1)781			.ok_or("item id overflow")?;782		let budget = self783			.recorder784			.weight_calls_budget(<StructureWeight<T>>::find_parent());785786		let total_tokens = token_ids.len();787		for id in token_ids.into_iter() {788			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;789			if id != expected_index {790				return Err("item id should be next".into());791			}792			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;793		}794		let users = [(to.clone(), 1)]795			.into_iter()796			.collect::<BTreeMap<_, _>>()797			.try_into()798			.unwrap();799		let create_item_data = CreateItemData::<T::CrossAccountId> {800			users,801			properties: CollectionPropertiesVec::default(),802		};803		let data = (0..total_tokens)804			.map(|_| create_item_data.clone())805			.collect();806807		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)808			.map_err(dispatch_to_evm::<T>)?;809		Ok(true)810	}811812	/// @notice Function to mint multiple tokens with the given tokenUris.813	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive814	///  numbers and first number should be obtained with `nextTokenId` method815	/// @param to The new owner816	/// @param tokens array of pairs of token ID and token URI for minted tokens817	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]818	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]819	fn mint_bulk_with_token_uri(820		&mut self,821		caller: caller,822		to: address,823		tokens: Vec<(uint256, string)>,824	) -> Result<bool> {825		let key = key::url();826		let caller = T::CrossAccountId::from_eth(caller);827		let to = T::CrossAccountId::from_eth(to);828		let mut expected_index = <TokensMinted<T>>::get(self.id)829			.checked_add(1)830			.ok_or("item id overflow")?;831		let budget = self832			.recorder833			.weight_calls_budget(<StructureWeight<T>>::find_parent());834835		let mut data = Vec::with_capacity(tokens.len());836		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]837			.into_iter()838			.collect::<BTreeMap<_, _>>()839			.try_into()840			.unwrap();841		for (id, token_uri) in tokens {842			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;843			if id != expected_index {844				return Err("item id should be next".into());845			}846			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;847848			let mut properties = CollectionPropertiesVec::default();849			properties850				.try_push(Property {851					key: key.clone(),852					value: token_uri853						.into_bytes()854						.try_into()855						.map_err(|_| "token uri is too long")?,856				})857				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;858859			let create_item_data = CreateItemData::<T::CrossAccountId> {860				users: users.clone(),861				properties,862			};863			data.push(create_item_data);864		}865866		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)867			.map_err(dispatch_to_evm::<T>)?;868		Ok(true)869	}870871	/// Returns EVM address for refungible token872	///873	/// @param token ID of the token874	fn token_contract_address(&self, token: uint256) -> Result<address> {875		Ok(T::EvmTokenAddressMapping::token_to_address(876			self.id,877			token.try_into().map_err(|_| "token id overflow")?,878		))879	}880}881882#[solidity_interface(883	name = UniqueRefungible,884	is(885		ERC721,886		ERC721Enumerable,887		ERC721UniqueExtensions,888		ERC721UniqueMintable,889		ERC721Burnable,890		ERC721Metadata(if(this.flags.erc721metadata)),891		Collection(via(common_mut returns CollectionHandle<T>)),892		TokenProperties,893	)894)]895impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}896897// Not a tests, but code generators898generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);899generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);900901impl<T: Config> CommonEvmHandler for RefungibleHandle<T>902where903	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,904{905	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");906	fn call(907		self,908		handle: &mut impl PrecompileHandle,909	) -> Option<pallet_common::erc::PrecompileResult> {910		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)911	}912}
after · pallets/refungible/src/erc.rs
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Refungible Pallet EVM API for tokens18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.2122extern crate alloc;2324use core::{25	char::{REPLACEMENT_CHARACTER, decode_utf16},26	convert::TryInto,27};28use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};29use frame_support::{BoundedBTreeMap, BoundedVec};30use pallet_common::{31	CollectionHandle, CollectionPropertyPermissions,32	erc::{CommonEvmHandler, CollectionCall, static_property::key},33	eth::convert_tuple_to_cross_account,34};35use pallet_evm::{account::CrossAccountId, PrecompileHandle};36use pallet_evm_coder_substrate::{call, dispatch_to_evm};37use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};38use sp_core::H160;39use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};40use up_data_structs::{41	CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,42	PropertyKeyPermission, PropertyPermission, TokenId,43};4445use crate::{46	AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,47	TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,48};4950pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5152/// @title A contract that allows to set and delete token properties and change token property permissions.53#[solidity_interface(name = TokenProperties)]54impl<T: Config> RefungibleHandle<T> {55	/// @notice Set permissions for token property.56	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.57	/// @param key Property key.58	/// @param isMutable Permission to mutate property.59	/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.60	/// @param tokenOwner Permission to mutate property by token owner if property is mutable.61	fn set_token_property_permission(62		&mut self,63		caller: caller,64		key: string,65		is_mutable: bool,66		collection_admin: bool,67		token_owner: bool,68	) -> Result<()> {69		let caller = T::CrossAccountId::from_eth(caller);70		<Pallet<T>>::set_token_property_permissions(71			self,72			&caller,73			vec![PropertyKeyPermission {74				key: <Vec<u8>>::from(key)75					.try_into()76					.map_err(|_| "too long key")?,77				permission: PropertyPermission {78					mutable: is_mutable,79					collection_admin,80					token_owner,81				},82			}],83		)84		.map_err(dispatch_to_evm::<T>)85	}8687	/// @notice Set token property value.88	/// @dev Throws error if `msg.sender` has no permission to edit the property.89	/// @param tokenId ID of the token.90	/// @param key Property key.91	/// @param value Property value.92	fn set_property(93		&mut self,94		caller: caller,95		token_id: uint256,96		key: string,97		value: bytes,98	) -> Result<()> {99		let caller = T::CrossAccountId::from_eth(caller);100		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;101		let key = <Vec<u8>>::from(key)102			.try_into()103			.map_err(|_| "key too long")?;104		let value = value.0.try_into().map_err(|_| "value too long")?;105106		let nesting_budget = self107			.recorder108			.weight_calls_budget(<StructureWeight<T>>::find_parent());109110		<Pallet<T>>::set_token_property(111			self,112			&caller,113			TokenId(token_id),114			Property { key, value },115			&nesting_budget,116		)117		.map_err(dispatch_to_evm::<T>)118	}119120	/// @notice Set token properties value.121	/// @dev Throws error if `msg.sender` has no permission to edit the property.122	/// @param tokenId ID of the token.123	/// @param properties settable properties124	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	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {166		let caller = T::CrossAccountId::from_eth(caller);167		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;168		let key = <Vec<u8>>::from(key)169			.try_into()170			.map_err(|_| "key too long")?;171172		let nesting_budget = self173			.recorder174			.weight_calls_budget(<StructureWeight<T>>::find_parent());175176		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)177			.map_err(dispatch_to_evm::<T>)178	}179180	/// @notice Get token property value.181	/// @dev Throws error if key not found182	/// @param tokenId ID of the token.183	/// @param key Property key.184	/// @return Property value bytes185	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {186		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;187		let key = <Vec<u8>>::from(key)188			.try_into()189			.map_err(|_| "key too long")?;190191		let props = <TokenProperties<T>>::get((self.id, token_id));192		let prop = props.get(&key).ok_or("key not found")?;193194		Ok(prop.to_vec().into())195	}196}197198#[derive(ToLog)]199pub enum ERC721Events {200	/// @dev This event emits when NFTs are created (`from` == 0) and destroyed201	///  (`to` == 0). Exception: during contract creation, any number of RFTs202	///  may be created and assigned without emitting Transfer.203	Transfer {204		#[indexed]205		from: address,206		#[indexed]207		to: address,208		#[indexed]209		token_id: uint256,210	},211	/// @dev Not supported212	Approval {213		#[indexed]214		owner: address,215		#[indexed]216		approved: address,217		#[indexed]218		token_id: uint256,219	},220	/// @dev Not supported221	#[allow(dead_code)]222	ApprovalForAll {223		#[indexed]224		owner: address,225		#[indexed]226		operator: address,227		approved: bool,228	},229}230231#[derive(ToLog)]232pub enum ERC721UniqueMintableEvents {233	/// @dev Not supported234	#[allow(dead_code)]235	MintingFinished {},236}237238#[solidity_interface(name = ERC721Metadata)]239impl<T: Config> RefungibleHandle<T>240where241	T::AccountId: From<[u8; 32]>,242{243	/// @notice A descriptive name for a collection of NFTs in this contract244	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`245	#[solidity(hide, rename_selector = "name")]246	fn name_proxy(&self) -> Result<string> {247		self.name()248	}249250	/// @notice An abbreviated name for NFTs in this contract251	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`252	#[solidity(hide, rename_selector = "symbol")]253	fn symbol_proxy(&self) -> Result<string> {254		self.symbol()255	}256257	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.258	///259	/// @dev If the token has a `url` property and it is not empty, it is returned.260	///  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`.261	///  If the collection property `baseURI` is empty or absent, return "" (empty string)262	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix263	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).264	///265	/// @return token's const_metadata266	#[solidity(rename_selector = "tokenURI")]267	fn token_uri(&self, token_id: uint256) -> Result<string> {268		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;269270		match get_token_property(self, token_id_u32, &key::url()).as_deref() {271			Err(_) | Ok("") => (),272			Ok(url) => {273				return Ok(url.into());274			}275		};276277		let base_uri =278			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())279				.map(BoundedVec::into_inner)280				.map(string::from_utf8)281				.transpose()282				.map_err(|e| {283					Error::Revert(alloc::format!(284						"Can not convert value \"baseURI\" to string with error \"{}\"",285						e286					))287				})?;288289		let base_uri = match base_uri.as_deref() {290			None | Some("") => {291				return Ok("".into());292			}293			Some(base_uri) => base_uri.into(),294		};295296		Ok(297			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {298				Err(_) | Ok("") => base_uri,299				Ok(suffix) => base_uri + suffix,300			},301		)302	}303}304305/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension306/// @dev See https://eips.ethereum.org/EIPS/eip-721307#[solidity_interface(name = ERC721Enumerable)]308impl<T: Config> RefungibleHandle<T> {309	/// @notice Enumerate valid RFTs310	/// @param index A counter less than `totalSupply()`311	/// @return The token identifier for the `index`th NFT,312	///  (sort order not specified)313	fn token_by_index(&self, index: uint256) -> Result<uint256> {314		Ok(index)315	}316317	/// Not implemented318	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {319		// TODO: Not implemetable320		Err("not implemented".into())321	}322323	/// @notice Count RFTs tracked by this contract324	/// @return A count of valid RFTs tracked by this contract, where each one of325	///  them has an assigned and queryable owner not equal to the zero address326	fn total_supply(&self) -> Result<uint256> {327		self.consume_store_reads(1)?;328		Ok(<Pallet<T>>::total_supply(self).into())329	}330}331332/// @title ERC-721 Non-Fungible Token Standard333/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md334#[solidity_interface(name = ERC721, events(ERC721Events))]335impl<T: Config> RefungibleHandle<T> {336	/// @notice Count all RFTs assigned to an owner337	/// @dev RFTs assigned to the zero address are considered invalid, and this338	///  function throws for queries about the zero address.339	/// @param owner An address for whom to query the balance340	/// @return The number of RFTs owned by `owner`, possibly zero341	fn balance_of(&self, owner: address) -> Result<uint256> {342		self.consume_store_reads(1)?;343		let owner = T::CrossAccountId::from_eth(owner);344		let balance = <AccountBalance<T>>::get((self.id, owner));345		Ok(balance.into())346	}347348	/// @notice Find the owner of an RFT349	/// @dev RFTs assigned to zero address are considered invalid, and queries350	///  about them do throw.351	///  Returns special 0xffffffffffffffffffffffffffffffffffffffff address for352	///  the tokens that are partially owned.353	/// @param tokenId The identifier for an RFT354	/// @return The address of the owner of the RFT355	fn owner_of(&self, token_id: uint256) -> Result<address> {356		self.consume_store_reads(2)?;357		let token = token_id.try_into()?;358		let owner = <Pallet<T>>::token_owner(self.id, token);359		Ok(owner360			.map(|address| *address.as_eth())361			.unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))362	}363364	/// @dev Not implemented365	fn safe_transfer_from_with_data(366		&mut self,367		_from: address,368		_to: address,369		_token_id: uint256,370		_data: bytes,371	) -> Result<void> {372		// TODO: Not implemetable373		Err("not implemented".into())374	}375376	/// @dev Not implemented377	fn safe_transfer_from(378		&mut self,379		_from: address,380		_to: address,381		_token_id: uint256,382	) -> Result<void> {383		// TODO: Not implemetable384		Err("not implemented".into())385	}386387	/// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE388	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE389	///  THEY MAY BE PERMANENTLY LOST390	/// @dev Throws unless `msg.sender` is the current owner or an authorized391	///  operator for this RFT. Throws if `from` is not the current owner. Throws392	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.393	///  Throws if RFT pieces have multiple owners.394	/// @param from The current owner of the NFT395	/// @param to The new owner396	/// @param tokenId The NFT to transfer397	#[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]398	fn transfer_from(399		&mut self,400		caller: caller,401		from: address,402		to: address,403		token_id: uint256,404	) -> Result<void> {405		let caller = T::CrossAccountId::from_eth(caller);406		let from = T::CrossAccountId::from_eth(from);407		let to = T::CrossAccountId::from_eth(to);408		let token = token_id.try_into()?;409		let budget = self410			.recorder411			.weight_calls_budget(<StructureWeight<T>>::find_parent());412413		let balance = balance(&self, token, &from)?;414		ensure_single_owner(&self, token, balance)?;415416		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)417			.map_err(dispatch_to_evm::<T>)?;418419		Ok(())420	}421422	/// @dev Not implemented423	fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {424		Err("not implemented".into())425	}426427	/// @dev Not implemented428	fn set_approval_for_all(429		&mut self,430		_caller: caller,431		_operator: address,432		_approved: bool,433	) -> Result<void> {434		// TODO: Not implemetable435		Err("not implemented".into())436	}437438	/// @dev Not implemented439	fn get_approved(&self, _token_id: uint256) -> Result<address> {440		// TODO: Not implemetable441		Err("not implemented".into())442	}443444	/// @dev Not implemented445	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {446		// TODO: Not implemetable447		Err("not implemented".into())448	}449}450451/// Returns amount of pieces of `token` that `owner` have452pub fn balance<T: Config>(453	collection: &RefungibleHandle<T>,454	token: TokenId,455	owner: &T::CrossAccountId,456) -> Result<u128> {457	collection.consume_store_reads(1)?;458	let balance = <Balance<T>>::get((collection.id, token, &owner));459	Ok(balance)460}461462/// Throws if `owner_balance` is lower than total amount of `token` pieces463pub fn ensure_single_owner<T: Config>(464	collection: &RefungibleHandle<T>,465	token: TokenId,466	owner_balance: u128,467) -> Result<()> {468	collection.consume_store_reads(1)?;469	let total_supply = <TotalSupply<T>>::get((collection.id, token));470	if total_supply != owner_balance {471		return Err("token has multiple owners".into());472	}473	Ok(())474}475476/// @title ERC721 Token that can be irreversibly burned (destroyed).477#[solidity_interface(name = ERC721Burnable)]478impl<T: Config> RefungibleHandle<T> {479	/// @notice Burns a specific ERC721 token.480	/// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized481	///  operator of the current owner.482	/// @param tokenId The RFT to approve483	#[weight(<SelfWeightOf<T>>::burn_item_fully())]484	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {485		let caller = T::CrossAccountId::from_eth(caller);486		let token = token_id.try_into()?;487488		let balance = balance(&self, token, &caller)?;489		ensure_single_owner(&self, token, balance)?;490491		<Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;492		Ok(())493	}494}495496/// @title ERC721 minting logic.497#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]498impl<T: Config> RefungibleHandle<T> {499	fn minting_finished(&self) -> Result<bool> {500		Ok(false)501	}502503	/// @notice Function to mint token.504	/// @param to The new owner505	/// @return uint256 The id of the newly minted token506	#[weight(<SelfWeightOf<T>>::create_item())]507	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {508		let token_id: uint256 = <TokensMinted<T>>::get(self.id)509			.checked_add(1)510			.ok_or("item id overflow")?511			.into();512		self.mint_check_id(caller, to, token_id)?;513		Ok(token_id)514	}515516	/// @notice Function to mint token.517	/// @dev `tokenId` should be obtained with `nextTokenId` method,518	///  unlike standard, you can't specify it manually519	/// @param to The new owner520	/// @param tokenId ID of the minted RFT521	#[solidity(hide, rename_selector = "mint")]522	#[weight(<SelfWeightOf<T>>::create_item())]523	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {524		let caller = T::CrossAccountId::from_eth(caller);525		let to = T::CrossAccountId::from_eth(to);526		let token_id: u32 = token_id.try_into()?;527		let budget = self528			.recorder529			.weight_calls_budget(<StructureWeight<T>>::find_parent());530531		if <TokensMinted<T>>::get(self.id)532			.checked_add(1)533			.ok_or("item id overflow")?534			!= token_id535		{536			return Err("item id should be next".into());537		}538539		let users = [(to.clone(), 1)]540			.into_iter()541			.collect::<BTreeMap<_, _>>()542			.try_into()543			.unwrap();544		<Pallet<T>>::create_item(545			self,546			&caller,547			CreateItemData::<T::CrossAccountId> {548				users,549				properties: CollectionPropertiesVec::default(),550			},551			&budget,552		)553		.map_err(dispatch_to_evm::<T>)?;554555		Ok(true)556	}557558	/// @notice Function to mint token with the given tokenUri.559	/// @param to The new owner560	/// @param tokenUri Token URI that would be stored in the NFT properties561	/// @return uint256 The id of the newly minted token562	#[solidity(rename_selector = "mintWithTokenURI")]563	#[weight(<SelfWeightOf<T>>::create_item())]564	fn mint_with_token_uri(565		&mut self,566		caller: caller,567		to: address,568		token_uri: string,569	) -> Result<uint256> {570		let token_id: uint256 = <TokensMinted<T>>::get(self.id)571			.checked_add(1)572			.ok_or("item id overflow")?573			.into();574		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;575		Ok(token_id)576	}577578	/// @notice Function to mint token with the given tokenUri.579	/// @dev `tokenId` should be obtained with `nextTokenId` method,580	///  unlike standard, you can't specify it manually581	/// @param to The new owner582	/// @param tokenId ID of the minted RFT583	/// @param tokenUri Token URI that would be stored in the RFT properties584	#[solidity(hide, rename_selector = "mintWithTokenURI")]585	#[weight(<SelfWeightOf<T>>::create_item())]586	fn mint_with_token_uri_check_id(587		&mut self,588		caller: caller,589		to: address,590		token_id: uint256,591		token_uri: string,592	) -> Result<bool> {593		let key = key::url();594		let permission = get_token_permission::<T>(self.id, &key)?;595		if !permission.collection_admin {596			return Err("Operation is not allowed".into());597		}598599		let caller = T::CrossAccountId::from_eth(caller);600		let to = T::CrossAccountId::from_eth(to);601		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;602		let budget = self603			.recorder604			.weight_calls_budget(<StructureWeight<T>>::find_parent());605606		if <TokensMinted<T>>::get(self.id)607			.checked_add(1)608			.ok_or("item id overflow")?609			!= token_id610		{611			return Err("item id should be next".into());612		}613614		let mut properties = CollectionPropertiesVec::default();615		properties616			.try_push(Property {617				key,618				value: token_uri619					.into_bytes()620					.try_into()621					.map_err(|_| "token uri is too long")?,622			})623			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;624625		let users = [(to.clone(), 1)]626			.into_iter()627			.collect::<BTreeMap<_, _>>()628			.try_into()629			.unwrap();630		<Pallet<T>>::create_item(631			self,632			&caller,633			CreateItemData::<T::CrossAccountId> { users, properties },634			&budget,635		)636		.map_err(dispatch_to_evm::<T>)?;637		Ok(true)638	}639640	/// @dev Not implemented641	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {642		Err("not implementable".into())643	}644}645646fn get_token_property<T: Config>(647	collection: &CollectionHandle<T>,648	token_id: u32,649	key: &up_data_structs::PropertyKey,650) -> Result<string> {651	collection.consume_store_reads(1)?;652	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))653		.map_err(|_| Error::Revert("Token properties not found".into()))?;654	if let Some(property) = properties.get(key) {655		return Ok(string::from_utf8_lossy(property).into());656	}657658	Err("Property tokenURI not found".into())659}660661fn get_token_permission<T: Config>(662	collection_id: CollectionId,663	key: &PropertyKey,664) -> Result<PropertyPermission> {665	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)666		.map_err(|_| Error::Revert("No permissions for collection".into()))?;667	let a = token_property_permissions668		.get(key)669		.map(Clone::clone)670		.ok_or_else(|| {671			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();672			Error::Revert(alloc::format!("No permission for key {}", key))673		})?;674	Ok(a)675}676677/// @title Unique extensions for ERC721.678#[solidity_interface(name = ERC721UniqueExtensions)]679impl<T: Config> RefungibleHandle<T>680where681	T::AccountId: From<[u8; 32]>,682{683	/// @notice A descriptive name for a collection of NFTs in this contract684	fn name(&self) -> Result<string> {685		Ok(decode_utf16(self.name.iter().copied())686			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))687			.collect::<string>())688	}689690	/// @notice An abbreviated name for NFTs in this contract691	fn symbol(&self) -> Result<string> {692		Ok(string::from_utf8_lossy(&self.token_prefix).into())693	}694695	/// @notice Transfer ownership of an RFT696	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`697	///  is the zero address. Throws if `tokenId` is not a valid RFT.698	///  Throws if RFT pieces have multiple owners.699	/// @param to The new owner700	/// @param tokenId The RFT to transfer701	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]702	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {703		let caller = T::CrossAccountId::from_eth(caller);704		let to = T::CrossAccountId::from_eth(to);705		let token = token_id.try_into()?;706		let budget = self707			.recorder708			.weight_calls_budget(<StructureWeight<T>>::find_parent());709710		let balance = balance(self, token, &caller)?;711		ensure_single_owner(self, token, balance)?;712713		<Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)714			.map_err(dispatch_to_evm::<T>)?;715		Ok(())716	}717718	/// @notice Transfer ownership of an RFT719	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`720	///  is the zero address. Throws if `tokenId` is not a valid RFT.721	///  Throws if RFT pieces have multiple owners.722	/// @param to The new owner723	/// @param tokenId The RFT to transfer724	#[weight(<SelfWeightOf<T>>::transfer_creating_removing())]725	fn transfer_from_cross(726		&mut self,727		caller: caller,728		from: (address, uint256),729		to: (address, uint256),730		token_id: uint256,731	) -> Result<void> {732		let caller = T::CrossAccountId::from_eth(caller);733		let from = convert_tuple_to_cross_account::<T>(from)?;734		let to = convert_tuple_to_cross_account::<T>(to)?;735		let token_id = token_id.try_into()?;736		let budget = self737			.recorder738			.weight_calls_budget(<StructureWeight<T>>::find_parent());739740		let balance = balance(self, token_id, &from)?;741		ensure_single_owner(self, token_id, balance)?;742743		Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)744			.map_err(dispatch_to_evm::<T>)?;745		Ok(())746	}747748	/// @notice Burns a specific ERC721 token.749	/// @dev Throws unless `msg.sender` is the current owner or an authorized750	///  operator for this RFT. Throws if `from` is not the current owner. Throws751	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.752	///  Throws if RFT pieces have multiple owners.753	/// @param from The current owner of the RFT754	/// @param tokenId The RFT to transfer755	#[weight(<SelfWeightOf<T>>::burn_from())]756	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {757		let caller = T::CrossAccountId::from_eth(caller);758		let from = T::CrossAccountId::from_eth(from);759		let token = token_id.try_into()?;760		let budget = self761			.recorder762			.weight_calls_budget(<StructureWeight<T>>::find_parent());763764		let balance = balance(self, token, &from)?;765		ensure_single_owner(self, token, balance)?;766767		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)768			.map_err(dispatch_to_evm::<T>)?;769		Ok(())770	}771772	/// @notice Burns a specific ERC721 token.773	/// @dev Throws unless `msg.sender` is the current owner or an authorized774	///  operator for this RFT. Throws if `from` is not the current owner. Throws775	///  if `to` is the zero address. Throws if `tokenId` is not a valid RFT.776	///  Throws if RFT pieces have multiple owners.777	/// @param from The current owner of the RFT778	/// @param tokenId The RFT to transfer779	#[weight(<SelfWeightOf<T>>::burn_from())]780	fn burn_from_cross(781		&mut self,782		caller: caller,783		from: (address, uint256),784		token_id: uint256,785	) -> Result<void> {786		let caller = T::CrossAccountId::from_eth(caller);787		let from = convert_tuple_to_cross_account::<T>(from)?;788		let token = token_id.try_into()?;789		let budget = self790			.recorder791			.weight_calls_budget(<StructureWeight<T>>::find_parent());792793		let balance = balance(self, token, &from)?;794		ensure_single_owner(self, token, balance)?;795796		<Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)797			.map_err(dispatch_to_evm::<T>)?;798		Ok(())799	}800801	/// @notice Returns next free RFT ID.802	fn next_token_id(&self) -> Result<uint256> {803		self.consume_store_reads(1)?;804		Ok(<TokensMinted<T>>::get(self.id)805			.checked_add(1)806			.ok_or("item id overflow")?807			.into())808	}809810	/// @notice Function to mint multiple tokens.811	/// @dev `tokenIds` should be an array of consecutive numbers and first number812	///  should be obtained with `nextTokenId` method813	/// @param to The new owner814	/// @param tokenIds IDs of the minted RFTs815	#[solidity(hide)]816	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]817	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {818		let caller = T::CrossAccountId::from_eth(caller);819		let to = T::CrossAccountId::from_eth(to);820		let mut expected_index = <TokensMinted<T>>::get(self.id)821			.checked_add(1)822			.ok_or("item id overflow")?;823		let budget = self824			.recorder825			.weight_calls_budget(<StructureWeight<T>>::find_parent());826827		let total_tokens = token_ids.len();828		for id in token_ids.into_iter() {829			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;830			if id != expected_index {831				return Err("item id should be next".into());832			}833			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;834		}835		let users = [(to.clone(), 1)]836			.into_iter()837			.collect::<BTreeMap<_, _>>()838			.try_into()839			.unwrap();840		let create_item_data = CreateItemData::<T::CrossAccountId> {841			users,842			properties: CollectionPropertiesVec::default(),843		};844		let data = (0..total_tokens)845			.map(|_| create_item_data.clone())846			.collect();847848		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)849			.map_err(dispatch_to_evm::<T>)?;850		Ok(true)851	}852853	/// @notice Function to mint multiple tokens with the given tokenUris.854	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive855	///  numbers and first number should be obtained with `nextTokenId` method856	/// @param to The new owner857	/// @param tokens array of pairs of token ID and token URI for minted tokens858	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]859	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]860	fn mint_bulk_with_token_uri(861		&mut self,862		caller: caller,863		to: address,864		tokens: Vec<(uint256, string)>,865	) -> Result<bool> {866		let key = key::url();867		let caller = T::CrossAccountId::from_eth(caller);868		let to = T::CrossAccountId::from_eth(to);869		let mut expected_index = <TokensMinted<T>>::get(self.id)870			.checked_add(1)871			.ok_or("item id overflow")?;872		let budget = self873			.recorder874			.weight_calls_budget(<StructureWeight<T>>::find_parent());875876		let mut data = Vec::with_capacity(tokens.len());877		let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]878			.into_iter()879			.collect::<BTreeMap<_, _>>()880			.try_into()881			.unwrap();882		for (id, token_uri) in tokens {883			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;884			if id != expected_index {885				return Err("item id should be next".into());886			}887			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;888889			let mut properties = CollectionPropertiesVec::default();890			properties891				.try_push(Property {892					key: key.clone(),893					value: token_uri894						.into_bytes()895						.try_into()896						.map_err(|_| "token uri is too long")?,897				})898				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;899900			let create_item_data = CreateItemData::<T::CrossAccountId> {901				users: users.clone(),902				properties,903			};904			data.push(create_item_data);905		}906907		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)908			.map_err(dispatch_to_evm::<T>)?;909		Ok(true)910	}911912	/// Returns EVM address for refungible token913	///914	/// @param token ID of the token915	fn token_contract_address(&self, token: uint256) -> Result<address> {916		Ok(T::EvmTokenAddressMapping::token_to_address(917			self.id,918			token.try_into().map_err(|_| "token id overflow")?,919		))920	}921}922923#[solidity_interface(924	name = UniqueRefungible,925	is(926		ERC721,927		ERC721Enumerable,928		ERC721UniqueExtensions,929		ERC721UniqueMintable,930		ERC721Burnable,931		ERC721Metadata(if(this.flags.erc721metadata)),932		Collection(via(common_mut returns CollectionHandle<T>)),933		TokenProperties,934	)935)]936impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}937938// Not a tests, but code generators939generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);940generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);941942impl<T: Config> CommonEvmHandler for RefungibleHandle<T>943where944	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,945{946	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");947	fn call(948		self,949		handle: &mut impl PrecompileHandle,950	) -> Option<pallet_common::erc::PrecompileResult> {951		call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)952	}953}
modifiedpallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth
--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -18,7 +18,7 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
 contract TokenProperties is Dummy, ERC165 {
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -61,6 +61,19 @@
 		dummy = 0;
 	}
 
+	/// @notice Set token properties value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param properties settable properties
+	/// @dev EVM selector for this function is: 0x14ed3a6e,
+	///  or in textual repr: setProperties(uint256,(string,bytes)[])
+	function setProperties(uint256 tokenId, Tuple19[] memory properties) public {
+		require(false, stub_error);
+		tokenId;
+		properties;
+		dummy = 0;
+	}
+
 	/// @notice Delete token property value.
 	/// @dev Throws error if `msg.sender` has no permission to edit the property.
 	/// @param tokenId ID of the token.
@@ -458,7 +471,7 @@
 
 	/// Get collection owner.
 	///
-	/// @return Tuble with sponsor address and his substrate mirror.
+	/// @return Tuple with sponsor address and his substrate mirror.
 	/// If address is canonical then substrate mirror is zero and vice versa.
 	/// @dev EVM selector for this function is: 0xdf727d3b,
 	///  or in textual repr: collectionOwner()
modifiedtests/src/eth/api/UniqueFungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueFungible.sol
+++ b/tests/src/eth/api/UniqueFungible.sol
@@ -249,7 +249,7 @@
 
 	/// Get collection owner.
 	///
-	/// @return Tuble with sponsor address and his substrate mirror.
+	/// @return Tuple with sponsor address and his substrate mirror.
 	/// If address is canonical then substrate mirror is zero and vice versa.
 	/// @dev EVM selector for this function is: 0xdf727d3b,
 	///  or in textual repr: collectionOwner()
modifiedtests/src/eth/api/UniqueNFT.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -13,7 +13,7 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
 interface TokenProperties is Dummy, ERC165 {
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -43,6 +43,14 @@
 		bytes memory value
 	) external;
 
+	/// @notice Set token properties value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param properties settable properties
+	/// @dev EVM selector for this function is: 0x14ed3a6e,
+	///  or in textual repr: setProperties(uint256,(string,bytes)[])
+	function setProperties(uint256 tokenId, Tuple19[] memory properties) external;
+
 	/// @notice Delete token property value.
 	/// @dev Throws error if `msg.sender` has no permission to edit the property.
 	/// @param tokenId ID of the token.
@@ -298,7 +306,7 @@
 
 	/// Get collection owner.
 	///
-	/// @return Tuble with sponsor address and his substrate mirror.
+	/// @return Tuple with sponsor address and his substrate mirror.
 	/// If address is canonical then substrate mirror is zero and vice versa.
 	/// @dev EVM selector for this function is: 0xdf727d3b,
 	///  or in textual repr: collectionOwner()
modifiedtests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth
--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -13,7 +13,7 @@
 }
 
 /// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
 interface TokenProperties is Dummy, ERC165 {
 	/// @notice Set permissions for token property.
 	/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -43,6 +43,14 @@
 		bytes memory value
 	) external;
 
+	/// @notice Set token properties value.
+	/// @dev Throws error if `msg.sender` has no permission to edit the property.
+	/// @param tokenId ID of the token.
+	/// @param properties settable properties
+	/// @dev EVM selector for this function is: 0x14ed3a6e,
+	///  or in textual repr: setProperties(uint256,(string,bytes)[])
+	function setProperties(uint256 tokenId, Tuple19[] memory properties) external;
+
 	/// @notice Delete token property value.
 	/// @dev Throws error if `msg.sender` has no permission to edit the property.
 	/// @param tokenId ID of the token.
@@ -298,7 +306,7 @@
 
 	/// Get collection owner.
 	///
-	/// @return Tuble with sponsor address and his substrate mirror.
+	/// @return Tuple with sponsor address and his substrate mirror.
 	/// If address is canonical then substrate mirror is zero and vice versa.
 	/// @dev EVM selector for this function is: 0xdf727d3b,
 	///  or in textual repr: collectionOwner()
modifiedtests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/nonFungibleAbi.json
+++ b/tests/src/eth/nonFungibleAbi.json
@@ -677,6 +677,24 @@
   {
     "inputs": [
       { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      {
+        "components": [
+          { "internalType": "string", "name": "field_0", "type": "string" },
+          { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+        ],
+        "internalType": "struct Tuple19[]",
+        "name": "properties",
+        "type": "tuple[]"
+      }
+    ],
+    "name": "setProperties",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
       { "internalType": "string", "name": "key", "type": "string" },
       { "internalType": "bytes", "name": "value", "type": "bytes" }
     ],
modifiedtests/src/eth/reFungibleAbi.jsondiffbeforeafterboth
--- a/tests/src/eth/reFungibleAbi.json
+++ b/tests/src/eth/reFungibleAbi.json
@@ -659,6 +659,24 @@
   {
     "inputs": [
       { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+      {
+        "components": [
+          { "internalType": "string", "name": "field_0", "type": "string" },
+          { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+        ],
+        "internalType": "struct Tuple19[]",
+        "name": "properties",
+        "type": "tuple[]"
+      }
+    ],
+    "name": "setProperties",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
       { "internalType": "string", "name": "key", "type": "string" },
       { "internalType": "bytes", "name": "value", "type": "bytes" }
     ],
modifiedtests/src/eth/tokenProperties.test.tsdiffbeforeafterboth
--- a/tests/src/eth/tokenProperties.test.ts
+++ b/tests/src/eth/tokenProperties.test.ts
@@ -16,6 +16,7 @@
 
 import {itEth, usingEthPlaygrounds, expect} from './util';
 import {IKeyringPair} from '@polkadot/types/types';
+import {ITokenPropertyPermission} from '../util/playgrounds/types';
 
 describe('EVM token properties', () => {
   let donor: IKeyringPair;
@@ -68,6 +69,64 @@
     const [{value}] = await token.getProperties(['testKey']);
     expect(value).to.equal('testValue');
   });
+  
+  itEth('Can be multiple set for NFT ', async({helper}) => {
+    const caller = await helper.eth.createAccountWithBalance(donor);
+    
+    const properties = Array(5).fill(0).map((_, i) => { return {field_0: `key_${i}`, field_1: Buffer.from(`value_${i}`)}; });
+    const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.field_0, permission: {tokenOwner: true,
+      collectionAdmin: true,
+      mutable: true}}; });
+    
+    const collection = await helper.nft.mintCollection(alice, {
+      tokenPrefix: 'ethp',
+      tokenPropertyPermissions: permissions,
+    });
+    
+    const token = await collection.mintToken(alice);
+    
+    const valuesBefore = await token.getProperties(properties.map(p => p.field_0));
+    expect(valuesBefore).to.be.deep.equal([]);
+    
+    await collection.addAdmin(alice, {Ethereum: caller});
+
+    const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+    const contract = helper.ethNativeContract.collection(address, 'nft', caller);
+
+    await contract.methods.setProperties(token.tokenId, properties).send({from: caller});
+
+    const values = await token.getProperties(properties.map(p => p.field_0));
+    expect(values).to.be.deep.equal(properties.map(p => { return {key: p.field_0, value: p.field_1.toString()}; }));
+  });
+  
+  itEth('Can be multiple set for RFT ', async({helper}) => {
+    const caller = await helper.eth.createAccountWithBalance(donor);
+    
+    const properties = Array(5).fill(0).map((_, i) => { return {field_0: `key_${i}`, field_1: Buffer.from(`value_${i}`)}; });
+    const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.field_0, permission: {tokenOwner: true,
+      collectionAdmin: true,
+      mutable: true}}; });
+    
+    const collection = await helper.rft.mintCollection(alice, {
+      tokenPrefix: 'ethp',
+      tokenPropertyPermissions: permissions,
+    });
+        
+    const token = await collection.mintToken(alice);
+    
+    const valuesBefore = await token.getProperties(properties.map(p => p.field_0));
+    expect(valuesBefore).to.be.deep.equal([]);
+    
+    await collection.addAdmin(alice, {Ethereum: caller});
+
+    const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+    const contract = helper.ethNativeContract.collection(address, 'rft', caller);
+
+    await contract.methods.setProperties(token.tokenId, properties).send({from: caller});
+
+    const values = await token.getProperties(properties.map(p => p.field_0));
+    expect(values).to.be.deep.equal(properties.map(p => { return {key: p.field_0, value: p.field_1.toString()}; }));
+  });
 
   itEth('Can be deleted', async({helper}) => {
     const caller = await helper.eth.createAccountWithBalance(donor);