difftreelog
Merge pull request #778 from UniqueNetwork/fix/add-weight-to-nft-and-rft-evm-methods
in: master
2 files changed
pallets/nonfungible/src/erc.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Nonfungible Pallet EVM API18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Nonfungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.2122extern crate alloc;23use core::{24 char::{REPLACEMENT_CHARACTER, decode_utf16},25 convert::TryInto,26};27use evm_coder::{28 abi::AbiType, ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*,29 types::Property as PropertyStruct, weight,30};31use frame_support::BoundedVec;32use up_data_structs::{33 TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,34 CollectionPropertiesVec,35};36use pallet_evm_coder_substrate::dispatch_to_evm;37use sp_std::vec::Vec;38use pallet_common::{39 CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,40 erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},41 eth::EthCrossAccount,42};43use pallet_evm::{account::CrossAccountId, PrecompileHandle};44use pallet_evm_coder_substrate::call;45use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};46use sp_core::Get;4748use crate::{49 AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,50 SelfWeightOf, weights::WeightInfo, TokenProperties,51};5253/// @title A contract that allows to set and delete token properties and change token property permissions.54#[solidity_interface(name = TokenProperties)]55impl<T: Config> NonfungibleHandle<T> {56 /// @notice Set permissions for token property.57 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.58 /// @param key Property key.59 /// @param isMutable Permission to mutate property.60 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.61 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.62 fn set_token_property_permission(63 &mut self,64 caller: caller,65 key: string,66 is_mutable: bool,67 collection_admin: bool,68 token_owner: bool,69 ) -> Result<()> {70 let caller = T::CrossAccountId::from_eth(caller);71 <Pallet<T>>::set_property_permission(72 self,73 &caller,74 PropertyKeyPermission {75 key: <Vec<u8>>::from(key)76 .try_into()77 .map_err(|_| "too long key")?,78 permission: PropertyPermission {79 mutable: is_mutable,80 collection_admin,81 token_owner,82 },83 },84 )85 .map_err(dispatch_to_evm::<T>)86 }8788 /// @notice Set token property value.89 /// @dev Throws error if `msg.sender` has no permission to edit the property.90 /// @param tokenId ID of the token.91 /// @param key Property key.92 /// @param value Property value.93 #[solidity(hide)]94 fn set_property(95 &mut self,96 caller: caller,97 token_id: uint256,98 key: string,99 value: bytes,100 ) -> Result<()> {101 let caller = T::CrossAccountId::from_eth(caller);102 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;103 let key = <Vec<u8>>::from(key)104 .try_into()105 .map_err(|_| "key too long")?;106 let value = value.0.try_into().map_err(|_| "value too long")?;107108 let nesting_budget = self109 .recorder110 .weight_calls_budget(<StructureWeight<T>>::find_parent());111112 <Pallet<T>>::set_token_property(113 self,114 &caller,115 TokenId(token_id),116 Property { key, value },117 &nesting_budget,118 )119 .map_err(dispatch_to_evm::<T>)120 }121122 /// @notice Set token properties value.123 /// @dev Throws error if `msg.sender` has no permission to edit the property.124 /// @param tokenId ID of the token.125 /// @param properties settable properties126 #[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]127 fn set_properties(128 &mut self,129 caller: caller,130 token_id: uint256,131 properties: Vec<PropertyStruct>,132 ) -> Result<()> {133 let caller = T::CrossAccountId::from_eth(caller);134 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;135136 let nesting_budget = self137 .recorder138 .weight_calls_budget(<StructureWeight<T>>::find_parent());139140 let properties = properties141 .into_iter()142 .map(|PropertyStruct { key, value }| {143 let key = <Vec<u8>>::from(key)144 .try_into()145 .map_err(|_| "key too large")?;146147 let value = value.0.try_into().map_err(|_| "value too large")?;148149 Ok(Property { key, value })150 })151 .collect::<Result<Vec<_>>>()?;152153 <Pallet<T>>::set_token_properties(154 self,155 &caller,156 TokenId(token_id),157 properties.into_iter(),158 false,159 &nesting_budget,160 )161 .map_err(dispatch_to_evm::<T>)162 }163164 /// @notice Delete token property value.165 /// @dev Throws error if `msg.sender` has no permission to edit the property.166 /// @param tokenId ID of the token.167 /// @param key Property key.168 #[solidity(hide)]169 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {170 let caller = T::CrossAccountId::from_eth(caller);171 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;172 let key = <Vec<u8>>::from(key)173 .try_into()174 .map_err(|_| "key too long")?;175176 let nesting_budget = self177 .recorder178 .weight_calls_budget(<StructureWeight<T>>::find_parent());179180 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)181 .map_err(dispatch_to_evm::<T>)182 }183184 /// @notice Delete token properties value.185 /// @dev Throws error if `msg.sender` has no permission to edit the property.186 /// @param tokenId ID of the token.187 /// @param keys Properties key.188 fn delete_properties(189 &mut self,190 token_id: uint256,191 caller: caller,192 keys: Vec<string>,193 ) -> Result<()> {194 let caller = T::CrossAccountId::from_eth(caller);195 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;196 let keys = keys197 .into_iter()198 .map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))199 .collect::<Result<Vec<_>>>()?;200201 let nesting_budget = self202 .recorder203 .weight_calls_budget(<StructureWeight<T>>::find_parent());204205 <Pallet<T>>::delete_token_properties(206 self,207 &caller,208 TokenId(token_id),209 keys.into_iter(),210 &nesting_budget,211 )212 .map_err(dispatch_to_evm::<T>)213 }214215 /// @notice Get token property value.216 /// @dev Throws error if key not found217 /// @param tokenId ID of the token.218 /// @param key Property key.219 /// @return Property value bytes220 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {221 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;222 let key = <Vec<u8>>::from(key)223 .try_into()224 .map_err(|_| "key too long")?;225226 let props = <TokenProperties<T>>::get((self.id, token_id));227 let prop = props.get(&key).ok_or("key not found")?;228229 Ok(prop.to_vec().into())230 }231}232233#[derive(ToLog)]234pub enum ERC721Events {235 /// @dev This emits when ownership of any NFT changes by any mechanism.236 /// This event emits when NFTs are created (`from` == 0) and destroyed237 /// (`to` == 0). Exception: during contract creation, any number of NFTs238 /// may be created and assigned without emitting Transfer. At the time of239 /// any transfer, the approved address for that NFT (if any) is reset to none.240 Transfer {241 #[indexed]242 from: address,243 #[indexed]244 to: address,245 #[indexed]246 token_id: uint256,247 },248 /// @dev This emits when the approved address for an NFT is changed or249 /// reaffirmed. The zero address indicates there is no approved address.250 /// When a Transfer event emits, this also indicates that the approved251 /// address for that NFT (if any) is reset to none.252 Approval {253 #[indexed]254 owner: address,255 #[indexed]256 approved: address,257 #[indexed]258 token_id: uint256,259 },260 /// @dev This emits when an operator is enabled or disabled for an owner.261 /// The operator can manage all NFTs of the owner.262 #[allow(dead_code)]263 ApprovalForAll {264 #[indexed]265 owner: address,266 #[indexed]267 operator: address,268 approved: bool,269 },270}271272#[derive(ToLog)]273pub enum ERC721UniqueMintableEvents {274 #[allow(dead_code)]275 MintingFinished {},276}277278/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension279/// @dev See https://eips.ethereum.org/EIPS/eip-721280#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]281impl<T: Config> NonfungibleHandle<T>282where283 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,284{285 /// @notice A descriptive name for a collection of NFTs in this contract286 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`287 #[solidity(hide, rename_selector = "name")]288 fn name_proxy(&self) -> Result<string> {289 self.name()290 }291292 /// @notice An abbreviated name for NFTs in this contract293 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`294 #[solidity(hide, rename_selector = "symbol")]295 fn symbol_proxy(&self) -> Result<string> {296 self.symbol()297 }298299 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.300 ///301 /// @dev If the token has a `url` property and it is not empty, it is returned.302 /// 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`.303 /// If the collection property `baseURI` is empty or absent, return "" (empty string)304 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix305 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).306 ///307 /// @return token's const_metadata308 #[solidity(rename_selector = "tokenURI")]309 fn token_uri(&self, token_id: uint256) -> Result<string> {310 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;311312 match get_token_property(self, token_id_u32, &key::url()).as_deref() {313 Err(_) | Ok("") => (),314 Ok(url) => {315 return Ok(url.into());316 }317 };318319 let base_uri =320 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())321 .map(BoundedVec::into_inner)322 .map(string::from_utf8)323 .transpose()324 .map_err(|e| {325 Error::Revert(alloc::format!(326 "Can not convert value \"baseURI\" to string with error \"{}\"",327 e328 ))329 })?;330331 let base_uri = match base_uri.as_deref() {332 None | Some("") => {333 return Ok("".into());334 }335 Some(base_uri) => base_uri.into(),336 };337338 Ok(339 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {340 Err(_) | Ok("") => base_uri,341 Ok(suffix) => base_uri + suffix,342 },343 )344 }345}346347/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension348/// @dev See https://eips.ethereum.org/EIPS/eip-721349#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]350impl<T: Config> NonfungibleHandle<T> {351 /// @notice Enumerate valid NFTs352 /// @param index A counter less than `totalSupply()`353 /// @return The token identifier for the `index`th NFT,354 /// (sort order not specified)355 fn token_by_index(&self, index: uint256) -> Result<uint256> {356 Ok(index)357 }358359 /// @dev Not implemented360 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {361 // TODO: Not implemetable362 Err("not implemented".into())363 }364365 /// @notice Count NFTs tracked by this contract366 /// @return A count of valid NFTs tracked by this contract, where each one of367 /// them has an assigned and queryable owner not equal to the zero address368 fn total_supply(&self) -> Result<uint256> {369 self.consume_store_reads(1)?;370 Ok(<Pallet<T>>::total_supply(self).into())371 }372}373374/// @title ERC-721 Non-Fungible Token Standard375/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md376#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]377impl<T: Config> NonfungibleHandle<T> {378 /// @notice Count all NFTs assigned to an owner379 /// @dev NFTs assigned to the zero address are considered invalid, and this380 /// function throws for queries about the zero address.381 /// @param owner An address for whom to query the balance382 /// @return The number of NFTs owned by `owner`, possibly zero383 fn balance_of(&self, owner: address) -> Result<uint256> {384 self.consume_store_reads(1)?;385 let owner = T::CrossAccountId::from_eth(owner);386 let balance = <AccountBalance<T>>::get((self.id, owner));387 Ok(balance.into())388 }389 /// @notice Find the owner of an NFT390 /// @dev NFTs assigned to zero address are considered invalid, and queries391 /// about them do throw.392 /// @param tokenId The identifier for an NFT393 /// @return The address of the owner of the NFT394 fn owner_of(&self, token_id: uint256) -> Result<address> {395 self.consume_store_reads(1)?;396 let token: TokenId = token_id.try_into()?;397 Ok(*<TokenData<T>>::get((self.id, token))398 .ok_or("token not found")?399 .owner400 .as_eth())401 }402 /// @dev Not implemented403 #[solidity(rename_selector = "safeTransferFrom")]404 fn safe_transfer_from_with_data(405 &mut self,406 _from: address,407 _to: address,408 _token_id: uint256,409 _data: bytes,410 ) -> Result<void> {411 // TODO: Not implemetable412 Err("not implemented".into())413 }414 /// @dev Not implemented415 fn safe_transfer_from(416 &mut self,417 _from: address,418 _to: address,419 _token_id: uint256,420 ) -> Result<void> {421 // TODO: Not implemetable422 Err("not implemented".into())423 }424425 /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE426 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE427 /// THEY MAY BE PERMANENTLY LOST428 /// @dev Throws unless `msg.sender` is the current owner or an authorized429 /// operator for this NFT. Throws if `from` is not the current owner. Throws430 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.431 /// @param from The current owner of the NFT432 /// @param to The new owner433 /// @param tokenId The NFT to transfer434 #[weight(<SelfWeightOf<T>>::transfer_from())]435 fn transfer_from(436 &mut self,437 caller: caller,438 from: address,439 to: address,440 token_id: uint256,441 ) -> Result<void> {442 let caller = T::CrossAccountId::from_eth(caller);443 let from = T::CrossAccountId::from_eth(from);444 let to = T::CrossAccountId::from_eth(to);445 let token = token_id.try_into()?;446 let budget = self447 .recorder448 .weight_calls_budget(<StructureWeight<T>>::find_parent());449450 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)451 .map_err(dispatch_to_evm::<T>)?;452 Ok(())453 }454455 /// @notice Set or reaffirm the approved address for an NFT456 /// @dev The zero address indicates there is no approved address.457 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized458 /// operator of the current owner.459 /// @param approved The new approved NFT controller460 /// @param tokenId The NFT to approve461 #[weight(<SelfWeightOf<T>>::approve())]462 fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {463 let caller = T::CrossAccountId::from_eth(caller);464 let approved = T::CrossAccountId::from_eth(approved);465 let token = token_id.try_into()?;466467 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))468 .map_err(dispatch_to_evm::<T>)?;469 Ok(())470 }471472 /// @notice Sets or unsets the approval of a given operator.473 /// The `operator` is allowed to transfer all tokens of the `caller` on their behalf.474 /// @param operator Operator475 /// @param approved Should operator status be granted or revoked?476 #[weight(<SelfWeightOf<T>>::set_allowance_for_all())]477 fn set_approval_for_all(478 &mut self,479 caller: caller,480 operator: address,481 approved: bool,482 ) -> Result<void> {483 let caller = T::CrossAccountId::from_eth(caller);484 let operator = T::CrossAccountId::from_eth(operator);485486 <Pallet<T>>::set_allowance_for_all(self, &caller, &operator, approved)487 .map_err(dispatch_to_evm::<T>)?;488 Ok(())489 }490491 /// @dev Not implemented492 fn get_approved(&self, _token_id: uint256) -> Result<address> {493 // TODO: Not implemetable494 Err("not implemented".into())495 }496497 /// @notice Tells whether the given `owner` approves the `operator`.498 #[weight(<SelfWeightOf<T>>::allowance_for_all())]499 fn is_approved_for_all(&self, owner: address, operator: address) -> Result<bool> {500 let owner = T::CrossAccountId::from_eth(owner);501 let operator = T::CrossAccountId::from_eth(operator);502503 Ok(<Pallet<T>>::allowance_for_all(self, &owner, &operator))504 }505506 /// @notice Returns collection helper contract address507 fn collection_helper_address(&self) -> Result<address> {508 Ok(T::ContractAddress::get())509 }510}511512/// @title ERC721 Token that can be irreversibly burned (destroyed).513#[solidity_interface(name = ERC721Burnable)]514impl<T: Config> NonfungibleHandle<T> {515 /// @notice Burns a specific ERC721 token.516 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized517 /// operator of the current owner.518 /// @param tokenId The NFT to approve519 #[weight(<SelfWeightOf<T>>::burn_item())]520 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {521 let caller = T::CrossAccountId::from_eth(caller);522 let token = token_id.try_into()?;523524 <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;525 Ok(())526 }527}528529/// @title ERC721 minting logic.530#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]531impl<T: Config> NonfungibleHandle<T> {532 fn minting_finished(&self) -> Result<bool> {533 Ok(false)534 }535536 /// @notice Function to mint token.537 /// @param to The new owner538 /// @return uint256 The id of the newly minted token539 #[weight(<SelfWeightOf<T>>::create_item())]540 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {541 let token_id: uint256 = <TokensMinted<T>>::get(self.id)542 .checked_add(1)543 .ok_or("item id overflow")?544 .into();545 self.mint_check_id(caller, to, token_id)?;546 Ok(token_id)547 }548549 /// @notice Function to mint token.550 /// @dev `tokenId` should be obtained with `nextTokenId` method,551 /// unlike standard, you can't specify it manually552 /// @param to The new owner553 /// @param tokenId ID of the minted NFT554 #[solidity(hide, rename_selector = "mint")]555 #[weight(<SelfWeightOf<T>>::create_item())]556 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {557 let caller = T::CrossAccountId::from_eth(caller);558 let to = T::CrossAccountId::from_eth(to);559 let token_id: u32 = token_id.try_into()?;560 let budget = self561 .recorder562 .weight_calls_budget(<StructureWeight<T>>::find_parent());563564 if <TokensMinted<T>>::get(self.id)565 .checked_add(1)566 .ok_or("item id overflow")?567 != token_id568 {569 return Err("item id should be next".into());570 }571572 <Pallet<T>>::create_item(573 self,574 &caller,575 CreateItemData::<T> {576 properties: BoundedVec::default(),577 owner: to,578 },579 &budget,580 )581 .map_err(dispatch_to_evm::<T>)?;582583 Ok(true)584 }585586 /// @notice Function to mint token with the given tokenUri.587 /// @param to The new owner588 /// @param tokenUri Token URI that would be stored in the NFT properties589 /// @return uint256 The id of the newly minted token590 #[solidity(rename_selector = "mintWithTokenURI")]591 #[weight(<SelfWeightOf<T>>::create_item())]592 fn mint_with_token_uri(593 &mut self,594 caller: caller,595 to: address,596 token_uri: string,597 ) -> Result<uint256> {598 let token_id: uint256 = <TokensMinted<T>>::get(self.id)599 .checked_add(1)600 .ok_or("item id overflow")?601 .into();602 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;603 Ok(token_id)604 }605606 /// @notice Function to mint token with the given tokenUri.607 /// @dev `tokenId` should be obtained with `nextTokenId` method,608 /// unlike standard, you can't specify it manually609 /// @param to The new owner610 /// @param tokenId ID of the minted NFT611 /// @param tokenUri Token URI that would be stored in the NFT properties612 #[solidity(hide, rename_selector = "mintWithTokenURI")]613 #[weight(<SelfWeightOf<T>>::create_item())]614 fn mint_with_token_uri_check_id(615 &mut self,616 caller: caller,617 to: address,618 token_id: uint256,619 token_uri: string,620 ) -> Result<bool> {621 let key = key::url();622 let permission = get_token_permission::<T>(self.id, &key)?;623 if !permission.collection_admin {624 return Err("Operation is not allowed".into());625 }626627 let caller = T::CrossAccountId::from_eth(caller);628 let to = T::CrossAccountId::from_eth(to);629 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;630 let budget = self631 .recorder632 .weight_calls_budget(<StructureWeight<T>>::find_parent());633634 if <TokensMinted<T>>::get(self.id)635 .checked_add(1)636 .ok_or("item id overflow")?637 != token_id638 {639 return Err("item id should be next".into());640 }641642 let mut properties = CollectionPropertiesVec::default();643 properties644 .try_push(Property {645 key,646 value: token_uri647 .into_bytes()648 .try_into()649 .map_err(|_| "token uri is too long")?,650 })651 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;652653 <Pallet<T>>::create_item(654 self,655 &caller,656 CreateItemData::<T> {657 properties,658 owner: to,659 },660 &budget,661 )662 .map_err(dispatch_to_evm::<T>)?;663 Ok(true)664 }665666 /// @dev Not implemented667 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {668 Err("not implementable".into())669 }670}671672fn get_token_property<T: Config>(673 collection: &CollectionHandle<T>,674 token_id: u32,675 key: &up_data_structs::PropertyKey,676) -> Result<string> {677 collection.consume_store_reads(1)?;678 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))679 .map_err(|_| Error::Revert("Token properties not found".into()))?;680 if let Some(property) = properties.get(key) {681 return Ok(string::from_utf8_lossy(property).into());682 }683684 Err("Property tokenURI not found".into())685}686687fn get_token_permission<T: Config>(688 collection_id: CollectionId,689 key: &PropertyKey,690) -> Result<PropertyPermission> {691 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)692 .map_err(|_| Error::Revert("No permissions for collection".into()))?;693 let a = token_property_permissions694 .get(key)695 .map(Clone::clone)696 .ok_or_else(|| {697 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();698 Error::Revert(alloc::format!("No permission for key {}", key))699 })?;700 Ok(a)701}702703/// @title Unique extensions for ERC721.704#[solidity_interface(name = ERC721UniqueExtensions)]705impl<T: Config> NonfungibleHandle<T>706where707 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,708{709 /// @notice A descriptive name for a collection of NFTs in this contract710 fn name(&self) -> Result<string> {711 Ok(decode_utf16(self.name.iter().copied())712 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))713 .collect::<string>())714 }715716 /// @notice An abbreviated name for NFTs in this contract717 fn symbol(&self) -> Result<string> {718 Ok(string::from_utf8_lossy(&self.token_prefix).into())719 }720721 /// @notice A description for the collection.722 fn description(&self) -> Result<string> {723 Ok(decode_utf16(self.description.iter().copied())724 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))725 .collect::<string>())726 }727728 /// Returns the owner (in cross format) of the token.729 ///730 /// @param tokenId Id for the token.731 fn cross_owner_of(&self, token_id: uint256) -> Result<EthCrossAccount> {732 Self::token_owner(&self, token_id.try_into()?)733 .map(|o| EthCrossAccount::from_sub_cross_account::<T>(&o))734 .ok_or(Error::Revert("key too large".into()))735 }736737 /// Returns the token properties.738 ///739 /// @param tokenId Id for the token.740 /// @param keys Properties keys. Empty keys for all propertyes.741 /// @return Vector of properties key/value pairs.742 fn properties(&self, token_id: uint256, keys: Vec<string>) -> Result<Vec<PropertyStruct>> {743 let keys = keys744 .into_iter()745 .map(|key| {746 <Vec<u8>>::from(key)747 .try_into()748 .map_err(|_| Error::Revert("key too large".into()))749 })750 .collect::<Result<Vec<_>>>()?;751752 <Self as CommonCollectionOperations<T>>::token_properties(753 &self,754 token_id.try_into()?,755 if keys.is_empty() { None } else { Some(keys) },756 )757 .into_iter()758 .map(|p| {759 let key = string::from_utf8(p.key.to_vec())760 .map_err(|e| Error::Revert(alloc::format!("{}", e)))?;761 let value = bytes(p.value.to_vec());762 Ok(PropertyStruct { key, value })763 })764 .collect::<Result<Vec<_>>>()765 }766767 /// @notice Set or reaffirm the approved address for an NFT768 /// @dev The zero address indicates there is no approved address.769 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized770 /// operator of the current owner.771 /// @param approved The new substrate address approved NFT controller772 /// @param tokenId The NFT to approve773 #[weight(<SelfWeightOf<T>>::approve())]774 fn approve_cross(775 &mut self,776 caller: caller,777 approved: EthCrossAccount,778 token_id: uint256,779 ) -> Result<void> {780 let caller = T::CrossAccountId::from_eth(caller);781 let approved = approved.into_sub_cross_account::<T>()?;782 let token = token_id.try_into()?;783784 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))785 .map_err(dispatch_to_evm::<T>)?;786 Ok(())787 }788789 /// @notice Transfer ownership of an NFT790 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`791 /// is the zero address. Throws if `tokenId` is not a valid NFT.792 /// @param to The new owner793 /// @param tokenId The NFT to transfer794 #[weight(<SelfWeightOf<T>>::transfer())]795 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {796 let caller = T::CrossAccountId::from_eth(caller);797 let to = T::CrossAccountId::from_eth(to);798 let token = token_id.try_into()?;799 let budget = self800 .recorder801 .weight_calls_budget(<StructureWeight<T>>::find_parent());802803 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;804 Ok(())805 }806807 /// @notice Transfer ownership of an NFT808 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`809 /// is the zero address. Throws if `tokenId` is not a valid NFT.810 /// @param to The new owner811 /// @param tokenId The NFT to transfer812 #[weight(<SelfWeightOf<T>>::transfer())]813 fn transfer_cross(814 &mut self,815 caller: caller,816 to: EthCrossAccount,817 token_id: uint256,818 ) -> Result<void> {819 let caller = T::CrossAccountId::from_eth(caller);820 let to = to.into_sub_cross_account::<T>()?;821 let token = token_id.try_into()?;822 let budget = self823 .recorder824 .weight_calls_budget(<StructureWeight<T>>::find_parent());825826 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;827 Ok(())828 }829830 /// @notice Transfer ownership of an NFT from cross account address to cross account address831 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`832 /// is the zero address. Throws if `tokenId` is not a valid NFT.833 /// @param from Cross acccount address of current owner834 /// @param to Cross acccount address of new owner835 /// @param tokenId The NFT to transfer836 #[weight(<SelfWeightOf<T>>::transfer())]837 fn transfer_from_cross(838 &mut self,839 caller: caller,840 from: EthCrossAccount,841 to: EthCrossAccount,842 token_id: uint256,843 ) -> Result<void> {844 let caller = T::CrossAccountId::from_eth(caller);845 let from = from.into_sub_cross_account::<T>()?;846 let to = to.into_sub_cross_account::<T>()?;847 let token_id = token_id.try_into()?;848 let budget = self849 .recorder850 .weight_calls_budget(<StructureWeight<T>>::find_parent());851 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)852 .map_err(dispatch_to_evm::<T>)?;853 Ok(())854 }855856 /// @notice Burns a specific ERC721 token.857 /// @dev Throws unless `msg.sender` is the current owner or an authorized858 /// operator for this NFT. Throws if `from` is not the current owner. Throws859 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.860 /// @param from The current owner of the NFT861 /// @param tokenId The NFT to transfer862 #[solidity(hide)]863 #[weight(<SelfWeightOf<T>>::burn_from())]864 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {865 let caller = T::CrossAccountId::from_eth(caller);866 let from = T::CrossAccountId::from_eth(from);867 let token = token_id.try_into()?;868 let budget = self869 .recorder870 .weight_calls_budget(<StructureWeight<T>>::find_parent());871872 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)873 .map_err(dispatch_to_evm::<T>)?;874 Ok(())875 }876877 /// @notice Burns a specific ERC721 token.878 /// @dev Throws unless `msg.sender` is the current owner or an authorized879 /// operator for this NFT. Throws if `from` is not the current owner. Throws880 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.881 /// @param from The current owner of the NFT882 /// @param tokenId The NFT to transfer883 #[weight(<SelfWeightOf<T>>::burn_from())]884 fn burn_from_cross(885 &mut self,886 caller: caller,887 from: EthCrossAccount,888 token_id: uint256,889 ) -> Result<void> {890 let caller = T::CrossAccountId::from_eth(caller);891 let from = from.into_sub_cross_account::<T>()?;892 let token = token_id.try_into()?;893 let budget = self894 .recorder895 .weight_calls_budget(<StructureWeight<T>>::find_parent());896897 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)898 .map_err(dispatch_to_evm::<T>)?;899 Ok(())900 }901902 /// @notice Returns next free NFT ID.903 fn next_token_id(&self) -> Result<uint256> {904 self.consume_store_reads(1)?;905 Ok(<TokensMinted<T>>::get(self.id)906 .checked_add(1)907 .ok_or("item id overflow")?908 .into())909 }910911 /// @notice Function to mint multiple tokens.912 /// @dev `tokenIds` should be an array of consecutive numbers and first number913 /// should be obtained with `nextTokenId` method914 /// @param to The new owner915 /// @param tokenIds IDs of the minted NFTs916 #[solidity(hide)]917 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]918 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {919 let caller = T::CrossAccountId::from_eth(caller);920 let to = T::CrossAccountId::from_eth(to);921 let mut expected_index = <TokensMinted<T>>::get(self.id)922 .checked_add(1)923 .ok_or("item id overflow")?;924 let budget = self925 .recorder926 .weight_calls_budget(<StructureWeight<T>>::find_parent());927928 let total_tokens = token_ids.len();929 for id in token_ids.into_iter() {930 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;931 if id != expected_index {932 return Err("item id should be next".into());933 }934 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;935 }936 let data = (0..total_tokens)937 .map(|_| CreateItemData::<T> {938 properties: BoundedVec::default(),939 owner: to.clone(),940 })941 .collect();942943 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)944 .map_err(dispatch_to_evm::<T>)?;945 Ok(true)946 }947948 /// @notice Function to mint multiple tokens with the given tokenUris.949 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive950 /// numbers and first number should be obtained with `nextTokenId` method951 /// @param to The new owner952 /// @param tokens array of pairs of token ID and token URI for minted tokens953 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]954 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]955 fn mint_bulk_with_token_uri(956 &mut self,957 caller: caller,958 to: address,959 tokens: Vec<(uint256, string)>,960 ) -> Result<bool> {961 let key = key::url();962 let caller = T::CrossAccountId::from_eth(caller);963 let to = T::CrossAccountId::from_eth(to);964 let mut expected_index = <TokensMinted<T>>::get(self.id)965 .checked_add(1)966 .ok_or("item id overflow")?;967 let budget = self968 .recorder969 .weight_calls_budget(<StructureWeight<T>>::find_parent());970971 let mut data = Vec::with_capacity(tokens.len());972 for (id, token_uri) in tokens {973 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;974 if id != expected_index {975 return Err("item id should be next".into());976 }977 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;978979 let mut properties = CollectionPropertiesVec::default();980 properties981 .try_push(Property {982 key: key.clone(),983 value: token_uri984 .into_bytes()985 .try_into()986 .map_err(|_| "token uri is too long")?,987 })988 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;989990 data.push(CreateItemData::<T> {991 properties,992 owner: to.clone(),993 });994 }995996 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)997 .map_err(dispatch_to_evm::<T>)?;998 Ok(true)999 }1000}10011002#[solidity_interface(1003 name = UniqueNFT,1004 is(1005 ERC721,1006 ERC721Enumerable,1007 ERC721UniqueExtensions,1008 ERC721UniqueMintable,1009 ERC721Burnable,1010 ERC721Metadata(if(this.flags.erc721metadata)),1011 Collection(via(common_mut returns CollectionHandle<T>)),1012 TokenProperties,1013 )1014)]1015impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}10161017// Not a tests, but code generators1018generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);1019generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);10201021impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>1022where1023 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1024{1025 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");10261027 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {1028 call::<T, UniqueNFTCall<T>, _, _>(handle, self)1029 }1030}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # Nonfungible Pallet EVM API18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Nonfungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.2122extern crate alloc;23use core::{24 char::{REPLACEMENT_CHARACTER, decode_utf16},25 convert::TryInto,26};27use evm_coder::{28 abi::AbiType, ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*,29 types::Property as PropertyStruct, weight,30};31use frame_support::BoundedVec;32use up_data_structs::{33 TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,34 CollectionPropertiesVec,35};36use pallet_evm_coder_substrate::dispatch_to_evm;37use sp_std::vec::Vec;38use pallet_common::{39 CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,40 erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},41 eth::EthCrossAccount,42};43use pallet_evm::{account::CrossAccountId, PrecompileHandle};44use pallet_evm_coder_substrate::call;45use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};46use sp_core::Get;4748use crate::{49 AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,50 SelfWeightOf, weights::WeightInfo, TokenProperties,51};5253/// @title A contract that allows to set and delete token properties and change token property permissions.54#[solidity_interface(name = TokenProperties)]55impl<T: Config> NonfungibleHandle<T> {56 /// @notice Set permissions for token property.57 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.58 /// @param key Property key.59 /// @param isMutable Permission to mutate property.60 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.61 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.62 #[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]63 fn set_token_property_permission(64 &mut self,65 caller: caller,66 key: string,67 is_mutable: bool,68 collection_admin: bool,69 token_owner: bool,70 ) -> Result<()> {71 let caller = T::CrossAccountId::from_eth(caller);72 <Pallet<T>>::set_property_permission(73 self,74 &caller,75 PropertyKeyPermission {76 key: <Vec<u8>>::from(key)77 .try_into()78 .map_err(|_| "too long key")?,79 permission: PropertyPermission {80 mutable: is_mutable,81 collection_admin,82 token_owner,83 },84 },85 )86 .map_err(dispatch_to_evm::<T>)87 }8889 /// @notice Set token property value.90 /// @dev Throws error if `msg.sender` has no permission to edit the property.91 /// @param tokenId ID of the token.92 /// @param key Property key.93 /// @param value Property value.94 #[solidity(hide)]95 #[weight(<SelfWeightOf<T>>::set_token_properties(1))]96 fn set_property(97 &mut self,98 caller: caller,99 token_id: uint256,100 key: string,101 value: bytes,102 ) -> Result<()> {103 let caller = T::CrossAccountId::from_eth(caller);104 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;105 let key = <Vec<u8>>::from(key)106 .try_into()107 .map_err(|_| "key too long")?;108 let value = value.0.try_into().map_err(|_| "value too long")?;109110 let nesting_budget = self111 .recorder112 .weight_calls_budget(<StructureWeight<T>>::find_parent());113114 <Pallet<T>>::set_token_property(115 self,116 &caller,117 TokenId(token_id),118 Property { key, value },119 &nesting_budget,120 )121 .map_err(dispatch_to_evm::<T>)122 }123124 /// @notice Set token properties value.125 /// @dev Throws error if `msg.sender` has no permission to edit the property.126 /// @param tokenId ID of the token.127 /// @param properties settable properties128 #[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]129 fn set_properties(130 &mut self,131 caller: caller,132 token_id: uint256,133 properties: Vec<PropertyStruct>,134 ) -> Result<()> {135 let caller = T::CrossAccountId::from_eth(caller);136 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;137138 let nesting_budget = self139 .recorder140 .weight_calls_budget(<StructureWeight<T>>::find_parent());141142 let properties = properties143 .into_iter()144 .map(|PropertyStruct { key, value }| {145 let key = <Vec<u8>>::from(key)146 .try_into()147 .map_err(|_| "key too large")?;148149 let value = value.0.try_into().map_err(|_| "value too large")?;150151 Ok(Property { key, value })152 })153 .collect::<Result<Vec<_>>>()?;154155 <Pallet<T>>::set_token_properties(156 self,157 &caller,158 TokenId(token_id),159 properties.into_iter(),160 false,161 &nesting_budget,162 )163 .map_err(dispatch_to_evm::<T>)164 }165166 /// @notice Delete token property value.167 /// @dev Throws error if `msg.sender` has no permission to edit the property.168 /// @param tokenId ID of the token.169 /// @param key Property key.170 #[solidity(hide)]171 #[weight(<SelfWeightOf<T>>::delete_token_properties(1))]172 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {173 let caller = T::CrossAccountId::from_eth(caller);174 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;175 let key = <Vec<u8>>::from(key)176 .try_into()177 .map_err(|_| "key too long")?;178179 let nesting_budget = self180 .recorder181 .weight_calls_budget(<StructureWeight<T>>::find_parent());182183 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)184 .map_err(dispatch_to_evm::<T>)185 }186187 /// @notice Delete token properties value.188 /// @dev Throws error if `msg.sender` has no permission to edit the property.189 /// @param tokenId ID of the token.190 /// @param keys Properties key.191 #[weight(<SelfWeightOf<T>>::delete_token_properties(keys.len() as u32))]192 fn delete_properties(193 &mut self,194 token_id: uint256,195 caller: caller,196 keys: Vec<string>,197 ) -> Result<()> {198 let caller = T::CrossAccountId::from_eth(caller);199 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;200 let keys = keys201 .into_iter()202 .map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))203 .collect::<Result<Vec<_>>>()?;204205 let nesting_budget = self206 .recorder207 .weight_calls_budget(<StructureWeight<T>>::find_parent());208209 <Pallet<T>>::delete_token_properties(210 self,211 &caller,212 TokenId(token_id),213 keys.into_iter(),214 &nesting_budget,215 )216 .map_err(dispatch_to_evm::<T>)217 }218219 /// @notice Get token property value.220 /// @dev Throws error if key not found221 /// @param tokenId ID of the token.222 /// @param key Property key.223 /// @return Property value bytes224 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {225 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;226 let key = <Vec<u8>>::from(key)227 .try_into()228 .map_err(|_| "key too long")?;229230 let props = <TokenProperties<T>>::get((self.id, token_id));231 let prop = props.get(&key).ok_or("key not found")?;232233 Ok(prop.to_vec().into())234 }235}236237#[derive(ToLog)]238pub enum ERC721Events {239 /// @dev This emits when ownership of any NFT changes by any mechanism.240 /// This event emits when NFTs are created (`from` == 0) and destroyed241 /// (`to` == 0). Exception: during contract creation, any number of NFTs242 /// may be created and assigned without emitting Transfer. At the time of243 /// any transfer, the approved address for that NFT (if any) is reset to none.244 Transfer {245 #[indexed]246 from: address,247 #[indexed]248 to: address,249 #[indexed]250 token_id: uint256,251 },252 /// @dev This emits when the approved address for an NFT is changed or253 /// reaffirmed. The zero address indicates there is no approved address.254 /// When a Transfer event emits, this also indicates that the approved255 /// address for that NFT (if any) is reset to none.256 Approval {257 #[indexed]258 owner: address,259 #[indexed]260 approved: address,261 #[indexed]262 token_id: uint256,263 },264 /// @dev This emits when an operator is enabled or disabled for an owner.265 /// The operator can manage all NFTs of the owner.266 #[allow(dead_code)]267 ApprovalForAll {268 #[indexed]269 owner: address,270 #[indexed]271 operator: address,272 approved: bool,273 },274}275276#[derive(ToLog)]277pub enum ERC721UniqueMintableEvents {278 #[allow(dead_code)]279 MintingFinished {},280}281282/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension283/// @dev See https://eips.ethereum.org/EIPS/eip-721284#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]285impl<T: Config> NonfungibleHandle<T>286where287 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,288{289 /// @notice A descriptive name for a collection of NFTs in this contract290 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`291 #[solidity(hide, rename_selector = "name")]292 fn name_proxy(&self) -> Result<string> {293 self.name()294 }295296 /// @notice An abbreviated name for NFTs in this contract297 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`298 #[solidity(hide, rename_selector = "symbol")]299 fn symbol_proxy(&self) -> Result<string> {300 self.symbol()301 }302303 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.304 ///305 /// @dev If the token has a `url` property and it is not empty, it is returned.306 /// 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`.307 /// If the collection property `baseURI` is empty or absent, return "" (empty string)308 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix309 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).310 ///311 /// @return token's const_metadata312 #[solidity(rename_selector = "tokenURI")]313 fn token_uri(&self, token_id: uint256) -> Result<string> {314 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;315316 match get_token_property(self, token_id_u32, &key::url()).as_deref() {317 Err(_) | Ok("") => (),318 Ok(url) => {319 return Ok(url.into());320 }321 };322323 let base_uri =324 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())325 .map(BoundedVec::into_inner)326 .map(string::from_utf8)327 .transpose()328 .map_err(|e| {329 Error::Revert(alloc::format!(330 "Can not convert value \"baseURI\" to string with error \"{}\"",331 e332 ))333 })?;334335 let base_uri = match base_uri.as_deref() {336 None | Some("") => {337 return Ok("".into());338 }339 Some(base_uri) => base_uri.into(),340 };341342 Ok(343 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {344 Err(_) | Ok("") => base_uri,345 Ok(suffix) => base_uri + suffix,346 },347 )348 }349}350351/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension352/// @dev See https://eips.ethereum.org/EIPS/eip-721353#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]354impl<T: Config> NonfungibleHandle<T> {355 /// @notice Enumerate valid NFTs356 /// @param index A counter less than `totalSupply()`357 /// @return The token identifier for the `index`th NFT,358 /// (sort order not specified)359 fn token_by_index(&self, index: uint256) -> Result<uint256> {360 Ok(index)361 }362363 /// @dev Not implemented364 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {365 // TODO: Not implemetable366 Err("not implemented".into())367 }368369 /// @notice Count NFTs tracked by this contract370 /// @return A count of valid NFTs tracked by this contract, where each one of371 /// them has an assigned and queryable owner not equal to the zero address372 fn total_supply(&self) -> Result<uint256> {373 self.consume_store_reads(1)?;374 Ok(<Pallet<T>>::total_supply(self).into())375 }376}377378/// @title ERC-721 Non-Fungible Token Standard379/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md380#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]381impl<T: Config> NonfungibleHandle<T> {382 /// @notice Count all NFTs assigned to an owner383 /// @dev NFTs assigned to the zero address are considered invalid, and this384 /// function throws for queries about the zero address.385 /// @param owner An address for whom to query the balance386 /// @return The number of NFTs owned by `owner`, possibly zero387 fn balance_of(&self, owner: address) -> Result<uint256> {388 self.consume_store_reads(1)?;389 let owner = T::CrossAccountId::from_eth(owner);390 let balance = <AccountBalance<T>>::get((self.id, owner));391 Ok(balance.into())392 }393 /// @notice Find the owner of an NFT394 /// @dev NFTs assigned to zero address are considered invalid, and queries395 /// about them do throw.396 /// @param tokenId The identifier for an NFT397 /// @return The address of the owner of the NFT398 fn owner_of(&self, token_id: uint256) -> Result<address> {399 self.consume_store_reads(1)?;400 let token: TokenId = token_id.try_into()?;401 Ok(*<TokenData<T>>::get((self.id, token))402 .ok_or("token not found")?403 .owner404 .as_eth())405 }406 /// @dev Not implemented407 #[solidity(rename_selector = "safeTransferFrom")]408 fn safe_transfer_from_with_data(409 &mut self,410 _from: address,411 _to: address,412 _token_id: uint256,413 _data: bytes,414 ) -> Result<void> {415 // TODO: Not implemetable416 Err("not implemented".into())417 }418 /// @dev Not implemented419 fn safe_transfer_from(420 &mut self,421 _from: address,422 _to: address,423 _token_id: uint256,424 ) -> Result<void> {425 // TODO: Not implemetable426 Err("not implemented".into())427 }428429 /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE430 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE431 /// THEY MAY BE PERMANENTLY LOST432 /// @dev Throws unless `msg.sender` is the current owner or an authorized433 /// operator for this NFT. Throws if `from` is not the current owner. Throws434 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.435 /// @param from The current owner of the NFT436 /// @param to The new owner437 /// @param tokenId The NFT to transfer438 #[weight(<SelfWeightOf<T>>::transfer_from())]439 fn transfer_from(440 &mut self,441 caller: caller,442 from: address,443 to: address,444 token_id: uint256,445 ) -> Result<void> {446 let caller = T::CrossAccountId::from_eth(caller);447 let from = T::CrossAccountId::from_eth(from);448 let to = T::CrossAccountId::from_eth(to);449 let token = token_id.try_into()?;450 let budget = self451 .recorder452 .weight_calls_budget(<StructureWeight<T>>::find_parent());453454 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)455 .map_err(dispatch_to_evm::<T>)?;456 Ok(())457 }458459 /// @notice Set or reaffirm the approved address for an NFT460 /// @dev The zero address indicates there is no approved address.461 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized462 /// operator of the current owner.463 /// @param approved The new approved NFT controller464 /// @param tokenId The NFT to approve465 #[weight(<SelfWeightOf<T>>::approve())]466 fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {467 let caller = T::CrossAccountId::from_eth(caller);468 let approved = T::CrossAccountId::from_eth(approved);469 let token = token_id.try_into()?;470471 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))472 .map_err(dispatch_to_evm::<T>)?;473 Ok(())474 }475476 /// @notice Sets or unsets the approval of a given operator.477 /// The `operator` is allowed to transfer all tokens of the `caller` on their behalf.478 /// @param operator Operator479 /// @param approved Should operator status be granted or revoked?480 #[weight(<SelfWeightOf<T>>::set_allowance_for_all())]481 fn set_approval_for_all(482 &mut self,483 caller: caller,484 operator: address,485 approved: bool,486 ) -> Result<void> {487 let caller = T::CrossAccountId::from_eth(caller);488 let operator = T::CrossAccountId::from_eth(operator);489490 <Pallet<T>>::set_allowance_for_all(self, &caller, &operator, approved)491 .map_err(dispatch_to_evm::<T>)?;492 Ok(())493 }494495 /// @dev Not implemented496 fn get_approved(&self, _token_id: uint256) -> Result<address> {497 // TODO: Not implemetable498 Err("not implemented".into())499 }500501 /// @notice Tells whether the given `owner` approves the `operator`.502 #[weight(<SelfWeightOf<T>>::allowance_for_all())]503 fn is_approved_for_all(&self, owner: address, operator: address) -> Result<bool> {504 let owner = T::CrossAccountId::from_eth(owner);505 let operator = T::CrossAccountId::from_eth(operator);506507 Ok(<Pallet<T>>::allowance_for_all(self, &owner, &operator))508 }509510 /// @notice Returns collection helper contract address511 fn collection_helper_address(&self) -> Result<address> {512 Ok(T::ContractAddress::get())513 }514}515516/// @title ERC721 Token that can be irreversibly burned (destroyed).517#[solidity_interface(name = ERC721Burnable)]518impl<T: Config> NonfungibleHandle<T> {519 /// @notice Burns a specific ERC721 token.520 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized521 /// operator of the current owner.522 /// @param tokenId The NFT to approve523 #[weight(<SelfWeightOf<T>>::burn_item())]524 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {525 let caller = T::CrossAccountId::from_eth(caller);526 let token = token_id.try_into()?;527528 <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;529 Ok(())530 }531}532533/// @title ERC721 minting logic.534#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]535impl<T: Config> NonfungibleHandle<T> {536 fn minting_finished(&self) -> Result<bool> {537 Ok(false)538 }539540 /// @notice Function to mint token.541 /// @param to The new owner542 /// @return uint256 The id of the newly minted token543 #[weight(<SelfWeightOf<T>>::create_item())]544 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {545 let token_id: uint256 = <TokensMinted<T>>::get(self.id)546 .checked_add(1)547 .ok_or("item id overflow")?548 .into();549 self.mint_check_id(caller, to, token_id)?;550 Ok(token_id)551 }552553 /// @notice Function to mint token.554 /// @dev `tokenId` should be obtained with `nextTokenId` method,555 /// unlike standard, you can't specify it manually556 /// @param to The new owner557 /// @param tokenId ID of the minted NFT558 #[solidity(hide, rename_selector = "mint")]559 #[weight(<SelfWeightOf<T>>::create_item())]560 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {561 let caller = T::CrossAccountId::from_eth(caller);562 let to = T::CrossAccountId::from_eth(to);563 let token_id: u32 = token_id.try_into()?;564 let budget = self565 .recorder566 .weight_calls_budget(<StructureWeight<T>>::find_parent());567568 if <TokensMinted<T>>::get(self.id)569 .checked_add(1)570 .ok_or("item id overflow")?571 != token_id572 {573 return Err("item id should be next".into());574 }575576 <Pallet<T>>::create_item(577 self,578 &caller,579 CreateItemData::<T> {580 properties: BoundedVec::default(),581 owner: to,582 },583 &budget,584 )585 .map_err(dispatch_to_evm::<T>)?;586587 Ok(true)588 }589590 /// @notice Function to mint token with the given tokenUri.591 /// @param to The new owner592 /// @param tokenUri Token URI that would be stored in the NFT properties593 /// @return uint256 The id of the newly minted token594 #[solidity(rename_selector = "mintWithTokenURI")]595 #[weight(<SelfWeightOf<T>>::create_item())]596 fn mint_with_token_uri(597 &mut self,598 caller: caller,599 to: address,600 token_uri: string,601 ) -> Result<uint256> {602 let token_id: uint256 = <TokensMinted<T>>::get(self.id)603 .checked_add(1)604 .ok_or("item id overflow")?605 .into();606 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;607 Ok(token_id)608 }609610 /// @notice Function to mint token with the given tokenUri.611 /// @dev `tokenId` should be obtained with `nextTokenId` method,612 /// unlike standard, you can't specify it manually613 /// @param to The new owner614 /// @param tokenId ID of the minted NFT615 /// @param tokenUri Token URI that would be stored in the NFT properties616 #[solidity(hide, rename_selector = "mintWithTokenURI")]617 #[weight(<SelfWeightOf<T>>::create_item())]618 fn mint_with_token_uri_check_id(619 &mut self,620 caller: caller,621 to: address,622 token_id: uint256,623 token_uri: string,624 ) -> Result<bool> {625 let key = key::url();626 let permission = get_token_permission::<T>(self.id, &key)?;627 if !permission.collection_admin {628 return Err("Operation is not allowed".into());629 }630631 let caller = T::CrossAccountId::from_eth(caller);632 let to = T::CrossAccountId::from_eth(to);633 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;634 let budget = self635 .recorder636 .weight_calls_budget(<StructureWeight<T>>::find_parent());637638 if <TokensMinted<T>>::get(self.id)639 .checked_add(1)640 .ok_or("item id overflow")?641 != token_id642 {643 return Err("item id should be next".into());644 }645646 let mut properties = CollectionPropertiesVec::default();647 properties648 .try_push(Property {649 key,650 value: token_uri651 .into_bytes()652 .try_into()653 .map_err(|_| "token uri is too long")?,654 })655 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;656657 <Pallet<T>>::create_item(658 self,659 &caller,660 CreateItemData::<T> {661 properties,662 owner: to,663 },664 &budget,665 )666 .map_err(dispatch_to_evm::<T>)?;667 Ok(true)668 }669670 /// @dev Not implemented671 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {672 Err("not implementable".into())673 }674}675676fn get_token_property<T: Config>(677 collection: &CollectionHandle<T>,678 token_id: u32,679 key: &up_data_structs::PropertyKey,680) -> Result<string> {681 collection.consume_store_reads(1)?;682 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))683 .map_err(|_| Error::Revert("Token properties not found".into()))?;684 if let Some(property) = properties.get(key) {685 return Ok(string::from_utf8_lossy(property).into());686 }687688 Err("Property tokenURI not found".into())689}690691fn get_token_permission<T: Config>(692 collection_id: CollectionId,693 key: &PropertyKey,694) -> Result<PropertyPermission> {695 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)696 .map_err(|_| Error::Revert("No permissions for collection".into()))?;697 let a = token_property_permissions698 .get(key)699 .map(Clone::clone)700 .ok_or_else(|| {701 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();702 Error::Revert(alloc::format!("No permission for key {}", key))703 })?;704 Ok(a)705}706707/// @title Unique extensions for ERC721.708#[solidity_interface(name = ERC721UniqueExtensions)]709impl<T: Config> NonfungibleHandle<T>710where711 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,712{713 /// @notice A descriptive name for a collection of NFTs in this contract714 fn name(&self) -> Result<string> {715 Ok(decode_utf16(self.name.iter().copied())716 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))717 .collect::<string>())718 }719720 /// @notice An abbreviated name for NFTs in this contract721 fn symbol(&self) -> Result<string> {722 Ok(string::from_utf8_lossy(&self.token_prefix).into())723 }724725 /// @notice A description for the collection.726 fn description(&self) -> Result<string> {727 Ok(decode_utf16(self.description.iter().copied())728 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))729 .collect::<string>())730 }731732 /// Returns the owner (in cross format) of the token.733 ///734 /// @param tokenId Id for the token.735 fn cross_owner_of(&self, token_id: uint256) -> Result<EthCrossAccount> {736 Self::token_owner(&self, token_id.try_into()?)737 .map(|o| EthCrossAccount::from_sub_cross_account::<T>(&o))738 .ok_or(Error::Revert("key too large".into()))739 }740741 /// Returns the token properties.742 ///743 /// @param tokenId Id for the token.744 /// @param keys Properties keys. Empty keys for all propertyes.745 /// @return Vector of properties key/value pairs.746 fn properties(&self, token_id: uint256, keys: Vec<string>) -> Result<Vec<PropertyStruct>> {747 let keys = keys748 .into_iter()749 .map(|key| {750 <Vec<u8>>::from(key)751 .try_into()752 .map_err(|_| Error::Revert("key too large".into()))753 })754 .collect::<Result<Vec<_>>>()?;755756 <Self as CommonCollectionOperations<T>>::token_properties(757 &self,758 token_id.try_into()?,759 if keys.is_empty() { None } else { Some(keys) },760 )761 .into_iter()762 .map(|p| {763 let key = string::from_utf8(p.key.to_vec())764 .map_err(|e| Error::Revert(alloc::format!("{}", e)))?;765 let value = bytes(p.value.to_vec());766 Ok(PropertyStruct { key, value })767 })768 .collect::<Result<Vec<_>>>()769 }770771 /// @notice Set or reaffirm the approved address for an NFT772 /// @dev The zero address indicates there is no approved address.773 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized774 /// operator of the current owner.775 /// @param approved The new substrate address approved NFT controller776 /// @param tokenId The NFT to approve777 #[weight(<SelfWeightOf<T>>::approve())]778 fn approve_cross(779 &mut self,780 caller: caller,781 approved: EthCrossAccount,782 token_id: uint256,783 ) -> Result<void> {784 let caller = T::CrossAccountId::from_eth(caller);785 let approved = approved.into_sub_cross_account::<T>()?;786 let token = token_id.try_into()?;787788 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))789 .map_err(dispatch_to_evm::<T>)?;790 Ok(())791 }792793 /// @notice Transfer ownership of an NFT794 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`795 /// is the zero address. Throws if `tokenId` is not a valid NFT.796 /// @param to The new owner797 /// @param tokenId The NFT to transfer798 #[weight(<SelfWeightOf<T>>::transfer())]799 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {800 let caller = T::CrossAccountId::from_eth(caller);801 let to = T::CrossAccountId::from_eth(to);802 let token = token_id.try_into()?;803 let budget = self804 .recorder805 .weight_calls_budget(<StructureWeight<T>>::find_parent());806807 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;808 Ok(())809 }810811 /// @notice Transfer ownership of an NFT812 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`813 /// is the zero address. Throws if `tokenId` is not a valid NFT.814 /// @param to The new owner815 /// @param tokenId The NFT to transfer816 #[weight(<SelfWeightOf<T>>::transfer())]817 fn transfer_cross(818 &mut self,819 caller: caller,820 to: EthCrossAccount,821 token_id: uint256,822 ) -> Result<void> {823 let caller = T::CrossAccountId::from_eth(caller);824 let to = to.into_sub_cross_account::<T>()?;825 let token = token_id.try_into()?;826 let budget = self827 .recorder828 .weight_calls_budget(<StructureWeight<T>>::find_parent());829830 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;831 Ok(())832 }833834 /// @notice Transfer ownership of an NFT from cross account address to cross account address835 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`836 /// is the zero address. Throws if `tokenId` is not a valid NFT.837 /// @param from Cross acccount address of current owner838 /// @param to Cross acccount address of new owner839 /// @param tokenId The NFT to transfer840 #[weight(<SelfWeightOf<T>>::transfer())]841 fn transfer_from_cross(842 &mut self,843 caller: caller,844 from: EthCrossAccount,845 to: EthCrossAccount,846 token_id: uint256,847 ) -> Result<void> {848 let caller = T::CrossAccountId::from_eth(caller);849 let from = from.into_sub_cross_account::<T>()?;850 let to = to.into_sub_cross_account::<T>()?;851 let token_id = token_id.try_into()?;852 let budget = self853 .recorder854 .weight_calls_budget(<StructureWeight<T>>::find_parent());855 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)856 .map_err(dispatch_to_evm::<T>)?;857 Ok(())858 }859860 /// @notice Burns a specific ERC721 token.861 /// @dev Throws unless `msg.sender` is the current owner or an authorized862 /// operator for this NFT. Throws if `from` is not the current owner. Throws863 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.864 /// @param from The current owner of the NFT865 /// @param tokenId The NFT to transfer866 #[solidity(hide)]867 #[weight(<SelfWeightOf<T>>::burn_from())]868 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {869 let caller = T::CrossAccountId::from_eth(caller);870 let from = T::CrossAccountId::from_eth(from);871 let token = token_id.try_into()?;872 let budget = self873 .recorder874 .weight_calls_budget(<StructureWeight<T>>::find_parent());875876 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)877 .map_err(dispatch_to_evm::<T>)?;878 Ok(())879 }880881 /// @notice Burns a specific ERC721 token.882 /// @dev Throws unless `msg.sender` is the current owner or an authorized883 /// operator for this NFT. Throws if `from` is not the current owner. Throws884 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.885 /// @param from The current owner of the NFT886 /// @param tokenId The NFT to transfer887 #[weight(<SelfWeightOf<T>>::burn_from())]888 fn burn_from_cross(889 &mut self,890 caller: caller,891 from: EthCrossAccount,892 token_id: uint256,893 ) -> Result<void> {894 let caller = T::CrossAccountId::from_eth(caller);895 let from = from.into_sub_cross_account::<T>()?;896 let token = token_id.try_into()?;897 let budget = self898 .recorder899 .weight_calls_budget(<StructureWeight<T>>::find_parent());900901 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)902 .map_err(dispatch_to_evm::<T>)?;903 Ok(())904 }905906 /// @notice Returns next free NFT ID.907 fn next_token_id(&self) -> Result<uint256> {908 self.consume_store_reads(1)?;909 Ok(<TokensMinted<T>>::get(self.id)910 .checked_add(1)911 .ok_or("item id overflow")?912 .into())913 }914915 /// @notice Function to mint multiple tokens.916 /// @dev `tokenIds` should be an array of consecutive numbers and first number917 /// should be obtained with `nextTokenId` method918 /// @param to The new owner919 /// @param tokenIds IDs of the minted NFTs920 #[solidity(hide)]921 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]922 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {923 let caller = T::CrossAccountId::from_eth(caller);924 let to = T::CrossAccountId::from_eth(to);925 let mut expected_index = <TokensMinted<T>>::get(self.id)926 .checked_add(1)927 .ok_or("item id overflow")?;928 let budget = self929 .recorder930 .weight_calls_budget(<StructureWeight<T>>::find_parent());931932 let total_tokens = token_ids.len();933 for id in token_ids.into_iter() {934 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;935 if id != expected_index {936 return Err("item id should be next".into());937 }938 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;939 }940 let data = (0..total_tokens)941 .map(|_| CreateItemData::<T> {942 properties: BoundedVec::default(),943 owner: to.clone(),944 })945 .collect();946947 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)948 .map_err(dispatch_to_evm::<T>)?;949 Ok(true)950 }951952 /// @notice Function to mint multiple tokens with the given tokenUris.953 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive954 /// numbers and first number should be obtained with `nextTokenId` method955 /// @param to The new owner956 /// @param tokens array of pairs of token ID and token URI for minted tokens957 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]958 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]959 fn mint_bulk_with_token_uri(960 &mut self,961 caller: caller,962 to: address,963 tokens: Vec<(uint256, string)>,964 ) -> Result<bool> {965 let key = key::url();966 let caller = T::CrossAccountId::from_eth(caller);967 let to = T::CrossAccountId::from_eth(to);968 let mut expected_index = <TokensMinted<T>>::get(self.id)969 .checked_add(1)970 .ok_or("item id overflow")?;971 let budget = self972 .recorder973 .weight_calls_budget(<StructureWeight<T>>::find_parent());974975 let mut data = Vec::with_capacity(tokens.len());976 for (id, token_uri) in tokens {977 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;978 if id != expected_index {979 return Err("item id should be next".into());980 }981 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;982983 let mut properties = CollectionPropertiesVec::default();984 properties985 .try_push(Property {986 key: key.clone(),987 value: token_uri988 .into_bytes()989 .try_into()990 .map_err(|_| "token uri is too long")?,991 })992 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;993994 data.push(CreateItemData::<T> {995 properties,996 owner: to.clone(),997 });998 }9991000 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)1001 .map_err(dispatch_to_evm::<T>)?;1002 Ok(true)1003 }1004}10051006#[solidity_interface(1007 name = UniqueNFT,1008 is(1009 ERC721,1010 ERC721Enumerable,1011 ERC721UniqueExtensions,1012 ERC721UniqueMintable,1013 ERC721Burnable,1014 ERC721Metadata(if(this.flags.erc721metadata)),1015 Collection(via(common_mut returns CollectionHandle<T>)),1016 TokenProperties,1017 )1018)]1019impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}10201021// Not a tests, but code generators1022generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);1023generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);10241025impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>1026where1027 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1028{1029 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");10301031 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {1032 call::<T, UniqueNFTCall<T>, _, _>(handle, self)1033 }1034}pallets/refungible/src/erc.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -62,6 +62,7 @@
/// @param isMutable Permission to mutate property.
/// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.
/// @param tokenOwner Permission to mutate property by token owner if property is mutable.
+ #[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]
fn set_token_property_permission(
&mut self,
caller: caller,
@@ -94,6 +95,7 @@
/// @param key Property key.
/// @param value Property value.
#[solidity(hide)]
+ #[weight(<SelfWeightOf<T>>::set_token_properties(1))]
fn set_property(
&mut self,
caller: caller,
@@ -126,6 +128,7 @@
/// @dev Throws error if `msg.sender` has no permission to edit the property.
/// @param tokenId ID of the token.
/// @param properties settable properties
+ #[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]
fn set_properties(
&mut self,
caller: caller,
@@ -168,6 +171,7 @@
/// @param tokenId ID of the token.
/// @param key Property key.
#[solidity(hide)]
+ #[weight(<SelfWeightOf<T>>::delete_token_properties(1))]
fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {
let caller = T::CrossAccountId::from_eth(caller);
let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
@@ -187,6 +191,7 @@
/// @dev Throws error if `msg.sender` has no permission to edit the property.
/// @param tokenId ID of the token.
/// @param keys Properties key.
+ #[weight(<SelfWeightOf<T>>::delete_token_properties(keys.len() as u32))]
fn delete_properties(
&mut self,
token_id: uint256,