git.delta.rocks / unique-network / refs/commits / 503c92cd6363

difftreelog

feat remove schemaName/schemaVersion

Yaroslav Bolyukin2022-10-14parent: #9d4aa0e.patch.diff
in: master

4 files changed

modifiedpallets/common/src/erc.rsdiffbeforeafterboth
--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -660,16 +660,6 @@
 	pub mod key {
 		use super::*;
 
-		/// Key "schemaName".
-		pub fn schema_name() -> up_data_structs::PropertyKey {
-			property_key_from_bytes(b"schemaName").expect(EXPECT_CONVERT_ERROR)
-		}
-
-		/// Key "schemaVersion".
-		pub fn schema_version() -> up_data_structs::PropertyKey {
-			property_key_from_bytes(b"schemaVersion").expect(EXPECT_CONVERT_ERROR)
-		}
-
 		/// Key "baseURI".
 		pub fn base_uri() -> up_data_structs::PropertyKey {
 			property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)
@@ -688,32 +678,6 @@
 		/// Key "parentNft".
 		pub fn parent_nft() -> up_data_structs::PropertyKey {
 			property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)
-		}
-
-		/// Key "ERC721Metadata".
-		pub fn erc721_metadata() -> up_data_structs::PropertyKey {
-			property_key_from_bytes(b"ERC721Metadata").expect(EXPECT_CONVERT_ERROR)
-		}
-	}
-
-	/// Values.
-	pub mod value {
-		use super::*;
-
-		/// Value "Schema version".
-		pub const SCHEMA_VERSION: &[u8] = b"1.0.0";
-
-		/// Value "ERC721Metadata".
-		pub const ERC721_METADATA: &[u8] = b"ERC721Metadata";
-
-		/// Value for [`ERC721_METADATA`].
-		pub fn erc721() -> up_data_structs::PropertyValue {
-			property_value_from_bytes(ERC721_METADATA).expect(EXPECT_CONVERT_ERROR)
-		}
-
-		/// Value for [`SCHEMA_VERSION`].
-		pub fn schema_version() -> up_data_structs::PropertyValue {
-			property_value_from_bytes(SCHEMA_VERSION).expect(EXPECT_CONVERT_ERROR)
 		}
 	}
 
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::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};28use frame_support::BoundedVec;29use up_data_structs::{30	TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,31	CollectionPropertiesVec,32};33use pallet_evm_coder_substrate::dispatch_to_evm;34use sp_std::vec::Vec;35use pallet_common::{36	erc::{37		CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key,38		static_property::value,39	},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.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 Delete token property value.120	/// @dev Throws error if `msg.sender` has no permission to edit the property.121	/// @param tokenId ID of the token.122	/// @param key Property key.123	fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {124		let caller = T::CrossAccountId::from_eth(caller);125		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;126		let key = <Vec<u8>>::from(key)127			.try_into()128			.map_err(|_| "key too long")?;129130		let nesting_budget = self131			.recorder132			.weight_calls_budget(<StructureWeight<T>>::find_parent());133134		<Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)135			.map_err(dispatch_to_evm::<T>)136	}137138	/// @notice Get token property value.139	/// @dev Throws error if key not found140	/// @param tokenId ID of the token.141	/// @param key Property key.142	/// @return Property value bytes143	fn property(&self, token_id: uint256, key: string) -> Result<bytes> {144		let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;145		let key = <Vec<u8>>::from(key)146			.try_into()147			.map_err(|_| "key too long")?;148149		let props = <TokenProperties<T>>::get((self.id, token_id));150		let prop = props.get(&key).ok_or("key not found")?;151152		Ok(prop.to_vec())153	}154}155156#[derive(ToLog)]157pub enum ERC721Events {158	/// @dev This emits when ownership of any NFT changes by any mechanism.159	///  This event emits when NFTs are created (`from` == 0) and destroyed160	///  (`to` == 0). Exception: during contract creation, any number of NFTs161	///  may be created and assigned without emitting Transfer. At the time of162	///  any transfer, the approved address for that NFT (if any) is reset to none.163	Transfer {164		#[indexed]165		from: address,166		#[indexed]167		to: address,168		#[indexed]169		token_id: uint256,170	},171	/// @dev This emits when the approved address for an NFT is changed or172	///  reaffirmed. The zero address indicates there is no approved address.173	///  When a Transfer event emits, this also indicates that the approved174	///  address for that NFT (if any) is reset to none.175	Approval {176		#[indexed]177		owner: address,178		#[indexed]179		approved: address,180		#[indexed]181		token_id: uint256,182	},183	/// @dev This emits when an operator is enabled or disabled for an owner.184	///  The operator can manage all NFTs of the owner.185	#[allow(dead_code)]186	ApprovalForAll {187		#[indexed]188		owner: address,189		#[indexed]190		operator: address,191		approved: bool,192	},193}194195#[derive(ToLog)]196pub enum ERC721UniqueMintableEvents {197	#[allow(dead_code)]198	MintingFinished {},199}200201/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension202/// @dev See https://eips.ethereum.org/EIPS/eip-721203#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]204impl<T: Config> NonfungibleHandle<T> {205	/// @notice A descriptive name for a collection of NFTs in this contract206	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`207	#[solidity(hide, rename_selector = "name")]208	fn name_proxy(&self) -> Result<string> {209		self.name()210	}211212	/// @notice An abbreviated name for NFTs in this contract213	/// @dev real implementation of this function lies in `ERC721UniqueExtensions`214	#[solidity(hide, rename_selector = "symbol")]215	fn symbol_proxy(&self) -> Result<string> {216		self.symbol()217	}218219	/// @notice A distinct Uniform Resource Identifier (URI) for a given asset.220	///221	/// @dev If the token has a `url` property and it is not empty, it is returned.222	///  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`.223	///  If the collection property `baseURI` is empty or absent, return "" (empty string)224	///  otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix225	///  otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).226	///227	/// @return token's const_metadata228	#[solidity(rename_selector = "tokenURI")]229	fn token_uri(&self, token_id: uint256) -> Result<string> {230		let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;231232		match get_token_property(self, token_id_u32, &key::url()).as_deref() {233			Err(_) | Ok("") => (),234			Ok(url) => {235				return Ok(url.into());236			}237		};238239		let base_uri =240			pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())241				.map(BoundedVec::into_inner)242				.map(string::from_utf8)243				.transpose()244				.map_err(|e| {245					Error::Revert(alloc::format!(246						"Can not convert value \"baseURI\" to string with error \"{}\"",247						e248					))249				})?;250251		let base_uri = match base_uri.as_deref() {252			None | Some("") => {253				return Ok("".into());254			}255			Some(base_uri) => base_uri.into(),256		};257258		Ok(259			match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {260				Err(_) | Ok("") => base_uri,261				Ok(suffix) => base_uri + suffix,262			},263		)264	}265}266267/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension268/// @dev See https://eips.ethereum.org/EIPS/eip-721269#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]270impl<T: Config> NonfungibleHandle<T> {271	/// @notice Enumerate valid NFTs272	/// @param index A counter less than `totalSupply()`273	/// @return The token identifier for the `index`th NFT,274	///  (sort order not specified)275	fn token_by_index(&self, index: uint256) -> Result<uint256> {276		Ok(index)277	}278279	/// @dev Not implemented280	fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {281		// TODO: Not implemetable282		Err("not implemented".into())283	}284285	/// @notice Count NFTs tracked by this contract286	/// @return A count of valid NFTs tracked by this contract, where each one of287	///  them has an assigned and queryable owner not equal to the zero address288	fn total_supply(&self) -> Result<uint256> {289		self.consume_store_reads(1)?;290		Ok(<Pallet<T>>::total_supply(self).into())291	}292}293294/// @title ERC-721 Non-Fungible Token Standard295/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md296#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]297impl<T: Config> NonfungibleHandle<T> {298	/// @notice Count all NFTs assigned to an owner299	/// @dev NFTs assigned to the zero address are considered invalid, and this300	///  function throws for queries about the zero address.301	/// @param owner An address for whom to query the balance302	/// @return The number of NFTs owned by `owner`, possibly zero303	fn balance_of(&self, owner: address) -> Result<uint256> {304		self.consume_store_reads(1)?;305		let owner = T::CrossAccountId::from_eth(owner);306		let balance = <AccountBalance<T>>::get((self.id, owner));307		Ok(balance.into())308	}309	/// @notice Find the owner of an NFT310	/// @dev NFTs assigned to zero address are considered invalid, and queries311	///  about them do throw.312	/// @param tokenId The identifier for an NFT313	/// @return The address of the owner of the NFT314	fn owner_of(&self, token_id: uint256) -> Result<address> {315		self.consume_store_reads(1)?;316		let token: TokenId = token_id.try_into()?;317		Ok(*<TokenData<T>>::get((self.id, token))318			.ok_or("token not found")?319			.owner320			.as_eth())321	}322	/// @dev Not implemented323	#[solidity(rename_selector = "safeTransferFrom")]324	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	}334	/// @dev Not implemented335	fn safe_transfer_from(336		&mut self,337		_from: address,338		_to: address,339		_token_id: uint256,340	) -> Result<void> {341		// TODO: Not implemetable342		Err("not implemented".into())343	}344345	/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE346	///  TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE347	///  THEY MAY BE PERMANENTLY LOST348	/// @dev Throws unless `msg.sender` is the current owner or an authorized349	///  operator for this NFT. Throws if `from` is not the current owner. Throws350	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.351	/// @param from The current owner of the NFT352	/// @param to The new owner353	/// @param tokenId The NFT to transfer354	#[weight(<SelfWeightOf<T>>::transfer_from())]355	fn transfer_from(356		&mut self,357		caller: caller,358		from: address,359		to: address,360		token_id: uint256,361	) -> Result<void> {362		let caller = T::CrossAccountId::from_eth(caller);363		let from = T::CrossAccountId::from_eth(from);364		let to = T::CrossAccountId::from_eth(to);365		let token = token_id.try_into()?;366		let budget = self367			.recorder368			.weight_calls_budget(<StructureWeight<T>>::find_parent());369370		<Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)371			.map_err(dispatch_to_evm::<T>)?;372		Ok(())373	}374375	/// @notice Set or reaffirm the approved address for an NFT376	/// @dev The zero address indicates there is no approved address.377	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized378	///  operator of the current owner.379	/// @param approved The new approved NFT controller380	/// @param tokenId The NFT to approve381	#[weight(<SelfWeightOf<T>>::approve())]382	fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {383		let caller = T::CrossAccountId::from_eth(caller);384		let approved = T::CrossAccountId::from_eth(approved);385		let token = token_id.try_into()?;386387		<Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))388			.map_err(dispatch_to_evm::<T>)?;389		Ok(())390	}391392	/// @dev Not implemented393	fn set_approval_for_all(394		&mut self,395		_caller: caller,396		_operator: address,397		_approved: bool,398	) -> Result<void> {399		// TODO: Not implemetable400		Err("not implemented".into())401	}402403	/// @dev Not implemented404	fn get_approved(&self, _token_id: uint256) -> Result<address> {405		// TODO: Not implemetable406		Err("not implemented".into())407	}408409	/// @dev Not implemented410	fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {411		// TODO: Not implemetable412		Err("not implemented".into())413	}414}415416/// @title ERC721 Token that can be irreversibly burned (destroyed).417#[solidity_interface(name = ERC721Burnable)]418impl<T: Config> NonfungibleHandle<T> {419	/// @notice Burns a specific ERC721 token.420	/// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized421	///  operator of the current owner.422	/// @param tokenId The NFT to approve423	#[weight(<SelfWeightOf<T>>::burn_item())]424	fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {425		let caller = T::CrossAccountId::from_eth(caller);426		let token = token_id.try_into()?;427428		<Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;429		Ok(())430	}431}432433/// @title ERC721 minting logic.434#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]435impl<T: Config> NonfungibleHandle<T> {436	fn minting_finished(&self) -> Result<bool> {437		Ok(false)438	}439440	/// @notice Function to mint token.441	/// @param to The new owner442	/// @return uint256 The id of the newly minted token443	#[weight(<SelfWeightOf<T>>::create_item())]444	fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {445		let token_id: uint256 = <TokensMinted<T>>::get(self.id)446			.checked_add(1)447			.ok_or("item id overflow")?448			.into();449		self.mint_check_id(caller, to, token_id)?;450		Ok(token_id)451	}452453	/// @notice Function to mint token.454	/// @dev `tokenId` should be obtained with `nextTokenId` method,455	///  unlike standard, you can't specify it manually456	/// @param to The new owner457	/// @param tokenId ID of the minted NFT458	#[solidity(hide, rename_selector = "mint")]459	#[weight(<SelfWeightOf<T>>::create_item())]460	fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {461		let caller = T::CrossAccountId::from_eth(caller);462		let to = T::CrossAccountId::from_eth(to);463		let token_id: u32 = token_id.try_into()?;464		let budget = self465			.recorder466			.weight_calls_budget(<StructureWeight<T>>::find_parent());467468		if <TokensMinted<T>>::get(self.id)469			.checked_add(1)470			.ok_or("item id overflow")?471			!= token_id472		{473			return Err("item id should be next".into());474		}475476		<Pallet<T>>::create_item(477			self,478			&caller,479			CreateItemData::<T> {480				properties: BoundedVec::default(),481				owner: to,482			},483			&budget,484		)485		.map_err(dispatch_to_evm::<T>)?;486487		Ok(true)488	}489490	/// @notice Function to mint token with the given tokenUri.491	/// @param to The new owner492	/// @param tokenUri Token URI that would be stored in the NFT properties493	/// @return uint256 The id of the newly minted token494	#[solidity(rename_selector = "mintWithTokenURI")]495	#[weight(<SelfWeightOf<T>>::create_item())]496	fn mint_with_token_uri(497		&mut self,498		caller: caller,499		to: address,500		token_uri: string,501	) -> Result<uint256> {502		let token_id: uint256 = <TokensMinted<T>>::get(self.id)503			.checked_add(1)504			.ok_or("item id overflow")?505			.into();506		self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;507		Ok(token_id)508	}509510	/// @notice Function to mint token with the given tokenUri.511	/// @dev `tokenId` should be obtained with `nextTokenId` method,512	///  unlike standard, you can't specify it manually513	/// @param to The new owner514	/// @param tokenId ID of the minted NFT515	/// @param tokenUri Token URI that would be stored in the NFT properties516	#[solidity(hide, rename_selector = "mintWithTokenURI")]517	#[weight(<SelfWeightOf<T>>::create_item())]518	fn mint_with_token_uri_check_id(519		&mut self,520		caller: caller,521		to: address,522		token_id: uint256,523		token_uri: string,524	) -> Result<bool> {525		let key = key::url();526		let permission = get_token_permission::<T>(self.id, &key)?;527		if !permission.collection_admin {528			return Err("Operation is not allowed".into());529		}530531		let caller = T::CrossAccountId::from_eth(caller);532		let to = T::CrossAccountId::from_eth(to);533		let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;534		let budget = self535			.recorder536			.weight_calls_budget(<StructureWeight<T>>::find_parent());537538		if <TokensMinted<T>>::get(self.id)539			.checked_add(1)540			.ok_or("item id overflow")?541			!= token_id542		{543			return Err("item id should be next".into());544		}545546		let mut properties = CollectionPropertiesVec::default();547		properties548			.try_push(Property {549				key,550				value: token_uri551					.into_bytes()552					.try_into()553					.map_err(|_| "token uri is too long")?,554			})555			.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;556557		<Pallet<T>>::create_item(558			self,559			&caller,560			CreateItemData::<T> {561				properties,562				owner: to,563			},564			&budget,565		)566		.map_err(dispatch_to_evm::<T>)?;567		Ok(true)568	}569570	/// @dev Not implemented571	fn finish_minting(&mut self, _caller: caller) -> Result<bool> {572		Err("not implementable".into())573	}574}575576fn get_token_property<T: Config>(577	collection: &CollectionHandle<T>,578	token_id: u32,579	key: &up_data_structs::PropertyKey,580) -> Result<string> {581	collection.consume_store_reads(1)?;582	let properties = <TokenProperties<T>>::try_get((collection.id, token_id))583		.map_err(|_| Error::Revert("Token properties not found".into()))?;584	if let Some(property) = properties.get(key) {585		return Ok(string::from_utf8_lossy(property).into());586	}587588	Err("Property tokenURI not found".into())589}590591fn get_token_permission<T: Config>(592	collection_id: CollectionId,593	key: &PropertyKey,594) -> Result<PropertyPermission> {595	let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)596		.map_err(|_| Error::Revert("No permissions for collection".into()))?;597	let a = token_property_permissions598		.get(key)599		.map(Clone::clone)600		.ok_or_else(|| {601			let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();602			Error::Revert(alloc::format!("No permission for key {}", key))603		})?;604	Ok(a)605}606607/// @title Unique extensions for ERC721.608#[solidity_interface(name = ERC721UniqueExtensions)]609impl<T: Config> NonfungibleHandle<T> {610	/// @notice A descriptive name for a collection of NFTs in this contract611	fn name(&self) -> Result<string> {612		Ok(decode_utf16(self.name.iter().copied())613			.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))614			.collect::<string>())615	}616617	/// @notice An abbreviated name for NFTs in this contract618	fn symbol(&self) -> Result<string> {619		Ok(string::from_utf8_lossy(&self.token_prefix).into())620	}621622	/// @notice Transfer ownership of an NFT623	/// @dev Throws unless `msg.sender` is the current owner. Throws if `to`624	///  is the zero address. Throws if `tokenId` is not a valid NFT.625	/// @param to The new owner626	/// @param tokenId The NFT to transfer627	#[weight(<SelfWeightOf<T>>::transfer())]628	fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {629		let caller = T::CrossAccountId::from_eth(caller);630		let to = T::CrossAccountId::from_eth(to);631		let token = token_id.try_into()?;632		let budget = self633			.recorder634			.weight_calls_budget(<StructureWeight<T>>::find_parent());635636		<Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;637		Ok(())638	}639640	/// @notice Burns a specific ERC721 token.641	/// @dev Throws unless `msg.sender` is the current owner or an authorized642	///  operator for this NFT. Throws if `from` is not the current owner. Throws643	///  if `to` is the zero address. Throws if `tokenId` is not a valid NFT.644	/// @param from The current owner of the NFT645	/// @param tokenId The NFT to transfer646	#[weight(<SelfWeightOf<T>>::burn_from())]647	fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {648		let caller = T::CrossAccountId::from_eth(caller);649		let from = T::CrossAccountId::from_eth(from);650		let token = token_id.try_into()?;651		let budget = self652			.recorder653			.weight_calls_budget(<StructureWeight<T>>::find_parent());654655		<Pallet<T>>::burn_from(self, &caller, &from, token, &budget)656			.map_err(dispatch_to_evm::<T>)?;657		Ok(())658	}659660	/// @notice Returns next free NFT ID.661	fn next_token_id(&self) -> Result<uint256> {662		self.consume_store_reads(1)?;663		Ok(<TokensMinted<T>>::get(self.id)664			.checked_add(1)665			.ok_or("item id overflow")?666			.into())667	}668669	/// @notice Function to mint multiple tokens.670	/// @dev `tokenIds` should be an array of consecutive numbers and first number671	///  should be obtained with `nextTokenId` method672	/// @param to The new owner673	/// @param tokenIds IDs of the minted NFTs674	#[solidity(hide)]675	#[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]676	fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {677		let caller = T::CrossAccountId::from_eth(caller);678		let to = T::CrossAccountId::from_eth(to);679		let mut expected_index = <TokensMinted<T>>::get(self.id)680			.checked_add(1)681			.ok_or("item id overflow")?;682		let budget = self683			.recorder684			.weight_calls_budget(<StructureWeight<T>>::find_parent());685686		let total_tokens = token_ids.len();687		for id in token_ids.into_iter() {688			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;689			if id != expected_index {690				return Err("item id should be next".into());691			}692			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;693		}694		let data = (0..total_tokens)695			.map(|_| CreateItemData::<T> {696				properties: BoundedVec::default(),697				owner: to.clone(),698			})699			.collect();700701		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)702			.map_err(dispatch_to_evm::<T>)?;703		Ok(true)704	}705706	/// @notice Function to mint multiple tokens with the given tokenUris.707	/// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive708	///  numbers and first number should be obtained with `nextTokenId` method709	/// @param to The new owner710	/// @param tokens array of pairs of token ID and token URI for minted tokens711	#[solidity(hide, rename_selector = "mintBulkWithTokenURI")]712	#[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]713	fn mint_bulk_with_token_uri(714		&mut self,715		caller: caller,716		to: address,717		tokens: Vec<(uint256, string)>,718	) -> Result<bool> {719		let key = key::url();720		let caller = T::CrossAccountId::from_eth(caller);721		let to = T::CrossAccountId::from_eth(to);722		let mut expected_index = <TokensMinted<T>>::get(self.id)723			.checked_add(1)724			.ok_or("item id overflow")?;725		let budget = self726			.recorder727			.weight_calls_budget(<StructureWeight<T>>::find_parent());728729		let mut data = Vec::with_capacity(tokens.len());730		for (id, token_uri) in tokens {731			let id: u32 = id.try_into().map_err(|_| "token id overflow")?;732			if id != expected_index {733				return Err("item id should be next".into());734			}735			expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;736737			let mut properties = CollectionPropertiesVec::default();738			properties739				.try_push(Property {740					key: key.clone(),741					value: token_uri742						.into_bytes()743						.try_into()744						.map_err(|_| "token uri is too long")?,745				})746				.map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;747748			data.push(CreateItemData::<T> {749				properties,750				owner: to.clone(),751			});752		}753754		<Pallet<T>>::create_multiple_items(self, &caller, data, &budget)755			.map_err(dispatch_to_evm::<T>)?;756		Ok(true)757	}758}759760#[solidity_interface(761	name = UniqueNFT,762	is(763		ERC721,764		ERC721Enumerable,765		ERC721UniqueExtensions,766		ERC721UniqueMintable,767		ERC721Burnable,768		ERC721Metadata(if(this.flags.erc721metadata)),769		Collection(via(common_mut returns CollectionHandle<T>)),770		TokenProperties,771	)772)]773impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}774775// Not a tests, but code generators776generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);777generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);778779impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>780where781	T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,782{783	const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");784785	fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {786		call::<T, UniqueNFTCall<T>, _, _>(handle, self)787	}788}
modifiedpallets/refungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -29,7 +29,7 @@
 use frame_support::{BoundedBTreeMap, BoundedVec};
 use pallet_common::{
 	CollectionHandle, CollectionPropertyPermissions,
-	erc::{CommonEvmHandler, CollectionCall, static_property::key, static_property::value},
+	erc::{CommonEvmHandler, CollectionCall, static_property::key},
 };
 use pallet_evm::{account::CrossAccountId, PrecompileHandle};
 use pallet_evm_coder_substrate::{call, dispatch_to_evm};
modifiedpallets/unique/src/eth/mod.rsdiffbeforeafterboth
--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -25,7 +25,7 @@
 	dispatch::CollectionDispatch,
 	erc::{
 		CollectionHelpersEvents,
-		static_property::{key, value as property_value},
+		static_property::{key},
 	},
 	Pallet as PalletCommon,
 };
@@ -125,26 +125,13 @@
 	} else {
 		up_data_structs::CollectionPropertiesPermissionsVec::default()
 	};
-	let properties = if add_properties {
-		let mut properties = vec![
-			up_data_structs::Property {
-				key: key::schema_name(),
-				value: property_value::erc721(),
-			},
-			up_data_structs::Property {
-				key: key::schema_version(),
-				value: property_value::schema_version(),
-			},
-		];
-		if !base_uri_value.is_empty() {
-			properties.push(up_data_structs::Property {
-				key: key::base_uri(),
-				value: base_uri_value,
-			})
-		}
-		properties
-			.try_into()
-			.map_err(|e| Error::Revert(format!("{:?}", e)))?
+	let properties = if add_properties && !base_uri_value.is_empty() {
+		vec![up_data_structs::Property {
+			key: key::base_uri(),
+			value: base_uri_value,
+		}]
+		.try_into()
+		.expect("limit >= 1")
 	} else {
 		up_data_structs::CollectionPropertiesVec::default()
 	};
@@ -404,32 +391,20 @@
 		}
 
 		let all_properties = <pallet_common::CollectionProperties<T>>::get(collection.id);
-		let mut new_properties = vec![];
-		if all_properties.get(&key::schema_name()).is_none() {
-			self.recorder().consume_sstore()?;
-			new_properties.push(up_data_structs::Property {
-				key: key::schema_name(),
-				value: property_value::erc721(),
-			});
-			new_properties.push(up_data_structs::Property {
-				key: key::schema_version(),
-				value: property_value::schema_version(),
-			});
-		}
 		if all_properties.get(&key::base_uri()).is_none() && !base_uri.is_empty() {
-			new_properties.push(up_data_structs::Property {
-				key: key::base_uri(),
-				value: base_uri
-					.into_bytes()
-					.try_into()
-					.map_err(|_| "base uri is too large")?,
-			});
-		}
-
-		if !new_properties.is_empty() {
 			self.recorder().consume_sstore()?;
-			<PalletCommon<T>>::set_collection_properties(&collection, &caller, new_properties)
-				.map_err(dispatch_to_evm::<T>)?;
+			<PalletCommon<T>>::set_collection_properties(
+				&collection,
+				&caller,
+				vec![up_data_structs::Property {
+					key: key::base_uri(),
+					value: base_uri
+						.into_bytes()
+						.try_into()
+						.map_err(|_| "base uri is too large")?,
+				}],
+			)
+			.map_err(dispatch_to_evm::<T>)?;
 		}
 
 		self.recorder().consume_sstore()?;