difftreelog
chore fix weight for `set_properties`
in: master
1 file 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::{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::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},37 CollectionHandle, CollectionPropertyPermissions,38 eth::convert_tuple_to_cross_account,39};40use pallet_evm::{account::CrossAccountId, PrecompileHandle};41use pallet_evm_coder_substrate::call;42use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};4344use crate::{45 AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,46 SelfWeightOf, weights::WeightInfo, TokenProperties,47};4849/// @title A contract that allows to set and delete token properties and change token property permissions.50#[solidity_interface(name = TokenProperties)]51impl<T: Config> NonfungibleHandle<T> {52 /// @notice Set permissions for token property.53 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.54 /// @param key Property key.55 /// @param isMutable Permission to mutate property.56 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.57 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.58 fn set_token_property_permission(59 &mut self,60 caller: caller,61 key: string,62 is_mutable: bool,63 collection_admin: bool,64 token_owner: bool,65 ) -> Result<()> {66 let caller = T::CrossAccountId::from_eth(caller);67 <Pallet<T>>::set_property_permission(68 self,69 &caller,70 PropertyKeyPermission {71 key: <Vec<u8>>::from(key)72 .try_into()73 .map_err(|_| "too long key")?,74 permission: PropertyPermission {75 mutable: is_mutable,76 collection_admin,77 token_owner,78 },79 },80 )81 .map_err(dispatch_to_evm::<T>)82 }8384 /// @notice Set token property value.85 /// @dev Throws error if `msg.sender` has no permission to edit the property.86 /// @param tokenId ID of the token.87 /// @param key Property key.88 /// @param value Property value.89 fn set_property(90 &mut self,91 caller: caller,92 token_id: uint256,93 key: string,94 value: bytes,95 ) -> Result<()> {96 let caller = T::CrossAccountId::from_eth(caller);97 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;98 let key = <Vec<u8>>::from(key)99 .try_into()100 .map_err(|_| "key too long")?;101 let value = value.0.try_into().map_err(|_| "value too long")?;102103 let nesting_budget = self104 .recorder105 .weight_calls_budget(<StructureWeight<T>>::find_parent());106107 <Pallet<T>>::set_token_property(108 self,109 &caller,110 TokenId(token_id),111 Property { key, value },112 &nesting_budget,113 )114 .map_err(dispatch_to_evm::<T>)115 }116117 /// @notice Set token properties value.118 /// @dev Throws error if `msg.sender` has no permission to edit the property.119 /// @param tokenId ID of the token.120 /// @param properties settable properties121 fn set_properties(122 &mut self,123 caller: caller,124 token_id: uint256,125 properties: Vec<(string, bytes)>,126 ) -> Result<()> {127 let caller = T::CrossAccountId::from_eth(caller);128 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;129130 let nesting_budget = self131 .recorder132 .weight_calls_budget(<StructureWeight<T>>::find_parent());133134 let properties = properties135 .into_iter()136 .map(|(key, value)| {137 let key = <Vec<u8>>::from(key)138 .try_into()139 .map_err(|_| "key too large")?;140141 let value = value.0.try_into().map_err(|_| "value too large")?;142143 Ok(Property { key, value })144 })145 .collect::<Result<Vec<_>>>()?;146147 <Pallet<T>>::set_token_properties(148 self,149 &caller,150 TokenId(token_id),151 properties.into_iter(),152 <Pallet<T>>::token_exists(&self, TokenId(token_id)),153 &nesting_budget,154 )155 .map_err(dispatch_to_evm::<T>)156 }157158 /// @notice Delete token property value.159 /// @dev Throws error if `msg.sender` has no permission to edit the property.160 /// @param tokenId ID of the token.161 /// @param key Property key.162 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {163 let caller = T::CrossAccountId::from_eth(caller);164 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;165 let key = <Vec<u8>>::from(key)166 .try_into()167 .map_err(|_| "key too long")?;168169 let nesting_budget = self170 .recorder171 .weight_calls_budget(<StructureWeight<T>>::find_parent());172173 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)174 .map_err(dispatch_to_evm::<T>)175 }176177 /// @notice Get token property value.178 /// @dev Throws error if key not found179 /// @param tokenId ID of the token.180 /// @param key Property key.181 /// @return Property value bytes182 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {183 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;184 let key = <Vec<u8>>::from(key)185 .try_into()186 .map_err(|_| "key too long")?;187188 let props = <TokenProperties<T>>::get((self.id, token_id));189 let prop = props.get(&key).ok_or("key not found")?;190191 Ok(prop.to_vec().into())192 }193}194195#[derive(ToLog)]196pub enum ERC721Events {197 /// @dev This emits when ownership of any NFT changes by any mechanism.198 /// This event emits when NFTs are created (`from` == 0) and destroyed199 /// (`to` == 0). Exception: during contract creation, any number of NFTs200 /// may be created and assigned without emitting Transfer. At the time of201 /// any transfer, the approved address for that NFT (if any) is reset to none.202 Transfer {203 #[indexed]204 from: address,205 #[indexed]206 to: address,207 #[indexed]208 token_id: uint256,209 },210 /// @dev This emits when the approved address for an NFT is changed or211 /// reaffirmed. The zero address indicates there is no approved address.212 /// When a Transfer event emits, this also indicates that the approved213 /// address for that NFT (if any) is reset to none.214 Approval {215 #[indexed]216 owner: address,217 #[indexed]218 approved: address,219 #[indexed]220 token_id: uint256,221 },222 /// @dev This emits when an operator is enabled or disabled for an owner.223 /// The operator can manage all NFTs of the owner.224 #[allow(dead_code)]225 ApprovalForAll {226 #[indexed]227 owner: address,228 #[indexed]229 operator: address,230 approved: bool,231 },232}233234#[derive(ToLog)]235pub enum ERC721UniqueMintableEvents {236 #[allow(dead_code)]237 MintingFinished {},238}239240/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension241/// @dev See https://eips.ethereum.org/EIPS/eip-721242#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]243impl<T: Config> NonfungibleHandle<T>244where245 T::AccountId: From<[u8; 32]>,246{247 /// @notice A descriptive name for a collection of NFTs in this contract248 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`249 #[solidity(hide, rename_selector = "name")]250 fn name_proxy(&self) -> Result<string> {251 self.name()252 }253254 /// @notice An abbreviated name for NFTs in this contract255 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`256 #[solidity(hide, rename_selector = "symbol")]257 fn symbol_proxy(&self) -> Result<string> {258 self.symbol()259 }260261 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.262 ///263 /// @dev If the token has a `url` property and it is not empty, it is returned.264 /// 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`.265 /// If the collection property `baseURI` is empty or absent, return "" (empty string)266 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix267 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).268 ///269 /// @return token's const_metadata270 #[solidity(rename_selector = "tokenURI")]271 fn token_uri(&self, token_id: uint256) -> Result<string> {272 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;273274 match get_token_property(self, token_id_u32, &key::url()).as_deref() {275 Err(_) | Ok("") => (),276 Ok(url) => {277 return Ok(url.into());278 }279 };280281 let base_uri =282 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())283 .map(BoundedVec::into_inner)284 .map(string::from_utf8)285 .transpose()286 .map_err(|e| {287 Error::Revert(alloc::format!(288 "Can not convert value \"baseURI\" to string with error \"{}\"",289 e290 ))291 })?;292293 let base_uri = match base_uri.as_deref() {294 None | Some("") => {295 return Ok("".into());296 }297 Some(base_uri) => base_uri.into(),298 };299300 Ok(301 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {302 Err(_) | Ok("") => base_uri,303 Ok(suffix) => base_uri + suffix,304 },305 )306 }307}308309/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension310/// @dev See https://eips.ethereum.org/EIPS/eip-721311#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]312impl<T: Config> NonfungibleHandle<T> {313 /// @notice Enumerate valid NFTs314 /// @param index A counter less than `totalSupply()`315 /// @return The token identifier for the `index`th NFT,316 /// (sort order not specified)317 fn token_by_index(&self, index: uint256) -> Result<uint256> {318 Ok(index)319 }320321 /// @dev Not implemented322 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {323 // TODO: Not implemetable324 Err("not implemented".into())325 }326327 /// @notice Count NFTs tracked by this contract328 /// @return A count of valid NFTs tracked by this contract, where each one of329 /// them has an assigned and queryable owner not equal to the zero address330 fn total_supply(&self) -> Result<uint256> {331 self.consume_store_reads(1)?;332 Ok(<Pallet<T>>::total_supply(self).into())333 }334}335336/// @title ERC-721 Non-Fungible Token Standard337/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md338#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]339impl<T: Config> NonfungibleHandle<T> {340 /// @notice Count all NFTs assigned to an owner341 /// @dev NFTs assigned to the zero address are considered invalid, and this342 /// function throws for queries about the zero address.343 /// @param owner An address for whom to query the balance344 /// @return The number of NFTs owned by `owner`, possibly zero345 fn balance_of(&self, owner: address) -> Result<uint256> {346 self.consume_store_reads(1)?;347 let owner = T::CrossAccountId::from_eth(owner);348 let balance = <AccountBalance<T>>::get((self.id, owner));349 Ok(balance.into())350 }351 /// @notice Find the owner of an NFT352 /// @dev NFTs assigned to zero address are considered invalid, and queries353 /// about them do throw.354 /// @param tokenId The identifier for an NFT355 /// @return The address of the owner of the NFT356 fn owner_of(&self, token_id: uint256) -> Result<address> {357 self.consume_store_reads(1)?;358 let token: TokenId = token_id.try_into()?;359 Ok(*<TokenData<T>>::get((self.id, token))360 .ok_or("token not found")?361 .owner362 .as_eth())363 }364 /// @dev Not implemented365 #[solidity(rename_selector = "safeTransferFrom")]366 fn safe_transfer_from_with_data(367 &mut self,368 _from: address,369 _to: address,370 _token_id: uint256,371 _data: bytes,372 ) -> Result<void> {373 // TODO: Not implemetable374 Err("not implemented".into())375 }376 /// @dev Not implemented377 fn safe_transfer_from(378 &mut self,379 _from: address,380 _to: address,381 _token_id: uint256,382 ) -> Result<void> {383 // TODO: Not implemetable384 Err("not implemented".into())385 }386387 /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE388 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE389 /// THEY MAY BE PERMANENTLY LOST390 /// @dev Throws unless `msg.sender` is the current owner or an authorized391 /// operator for this NFT. Throws if `from` is not the current owner. Throws392 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.393 /// @param from The current owner of the NFT394 /// @param to The new owner395 /// @param tokenId The NFT to transfer396 #[weight(<SelfWeightOf<T>>::transfer_from())]397 fn transfer_from(398 &mut self,399 caller: caller,400 from: address,401 to: address,402 token_id: uint256,403 ) -> Result<void> {404 let caller = T::CrossAccountId::from_eth(caller);405 let from = T::CrossAccountId::from_eth(from);406 let to = T::CrossAccountId::from_eth(to);407 let token = token_id.try_into()?;408 let budget = self409 .recorder410 .weight_calls_budget(<StructureWeight<T>>::find_parent());411412 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)413 .map_err(dispatch_to_evm::<T>)?;414 Ok(())415 }416417 /// @notice Set or reaffirm the approved address for an NFT418 /// @dev The zero address indicates there is no approved address.419 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized420 /// operator of the current owner.421 /// @param approved The new approved NFT controller422 /// @param tokenId The NFT to approve423 #[weight(<SelfWeightOf<T>>::approve())]424 fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {425 let caller = T::CrossAccountId::from_eth(caller);426 let approved = T::CrossAccountId::from_eth(approved);427 let token = token_id.try_into()?;428429 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))430 .map_err(dispatch_to_evm::<T>)?;431 Ok(())432 }433434 /// @dev Not implemented435 fn set_approval_for_all(436 &mut self,437 _caller: caller,438 _operator: address,439 _approved: bool,440 ) -> Result<void> {441 // TODO: Not implemetable442 Err("not implemented".into())443 }444445 /// @dev Not implemented446 fn get_approved(&self, _token_id: uint256) -> Result<address> {447 // TODO: Not implemetable448 Err("not implemented".into())449 }450451 /// @dev Not implemented452 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {453 // TODO: Not implemetable454 Err("not implemented".into())455 }456}457458/// @title ERC721 Token that can be irreversibly burned (destroyed).459#[solidity_interface(name = ERC721Burnable)]460impl<T: Config> NonfungibleHandle<T> {461 /// @notice Burns a specific ERC721 token.462 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized463 /// operator of the current owner.464 /// @param tokenId The NFT to approve465 #[weight(<SelfWeightOf<T>>::burn_item())]466 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {467 let caller = T::CrossAccountId::from_eth(caller);468 let token = token_id.try_into()?;469470 <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;471 Ok(())472 }473}474475/// @title ERC721 minting logic.476#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]477impl<T: Config> NonfungibleHandle<T> {478 fn minting_finished(&self) -> Result<bool> {479 Ok(false)480 }481482 /// @notice Function to mint token.483 /// @param to The new owner484 /// @return uint256 The id of the newly minted token485 #[weight(<SelfWeightOf<T>>::create_item())]486 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {487 let token_id: uint256 = <TokensMinted<T>>::get(self.id)488 .checked_add(1)489 .ok_or("item id overflow")?490 .into();491 self.mint_check_id(caller, to, token_id)?;492 Ok(token_id)493 }494495 /// @notice Function to mint token.496 /// @dev `tokenId` should be obtained with `nextTokenId` method,497 /// unlike standard, you can't specify it manually498 /// @param to The new owner499 /// @param tokenId ID of the minted NFT500 #[solidity(hide, rename_selector = "mint")]501 #[weight(<SelfWeightOf<T>>::create_item())]502 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {503 let caller = T::CrossAccountId::from_eth(caller);504 let to = T::CrossAccountId::from_eth(to);505 let token_id: u32 = token_id.try_into()?;506 let budget = self507 .recorder508 .weight_calls_budget(<StructureWeight<T>>::find_parent());509510 if <TokensMinted<T>>::get(self.id)511 .checked_add(1)512 .ok_or("item id overflow")?513 != token_id514 {515 return Err("item id should be next".into());516 }517518 <Pallet<T>>::create_item(519 self,520 &caller,521 CreateItemData::<T> {522 properties: BoundedVec::default(),523 owner: to,524 },525 &budget,526 )527 .map_err(dispatch_to_evm::<T>)?;528529 Ok(true)530 }531532 /// @notice Function to mint token with the given tokenUri.533 /// @param to The new owner534 /// @param tokenUri Token URI that would be stored in the NFT properties535 /// @return uint256 The id of the newly minted token536 #[solidity(rename_selector = "mintWithTokenURI")]537 #[weight(<SelfWeightOf<T>>::create_item())]538 fn mint_with_token_uri(539 &mut self,540 caller: caller,541 to: address,542 token_uri: string,543 ) -> Result<uint256> {544 let token_id: uint256 = <TokensMinted<T>>::get(self.id)545 .checked_add(1)546 .ok_or("item id overflow")?547 .into();548 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;549 Ok(token_id)550 }551552 /// @notice Function to mint token with the given tokenUri.553 /// @dev `tokenId` should be obtained with `nextTokenId` method,554 /// unlike standard, you can't specify it manually555 /// @param to The new owner556 /// @param tokenId ID of the minted NFT557 /// @param tokenUri Token URI that would be stored in the NFT properties558 #[solidity(hide, rename_selector = "mintWithTokenURI")]559 #[weight(<SelfWeightOf<T>>::create_item())]560 fn mint_with_token_uri_check_id(561 &mut self,562 caller: caller,563 to: address,564 token_id: uint256,565 token_uri: string,566 ) -> Result<bool> {567 let key = key::url();568 let permission = get_token_permission::<T>(self.id, &key)?;569 if !permission.collection_admin {570 return Err("Operation is not allowed".into());571 }572573 let caller = T::CrossAccountId::from_eth(caller);574 let to = T::CrossAccountId::from_eth(to);575 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;576 let budget = self577 .recorder578 .weight_calls_budget(<StructureWeight<T>>::find_parent());579580 if <TokensMinted<T>>::get(self.id)581 .checked_add(1)582 .ok_or("item id overflow")?583 != token_id584 {585 return Err("item id should be next".into());586 }587588 let mut properties = CollectionPropertiesVec::default();589 properties590 .try_push(Property {591 key,592 value: token_uri593 .into_bytes()594 .try_into()595 .map_err(|_| "token uri is too long")?,596 })597 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;598599 <Pallet<T>>::create_item(600 self,601 &caller,602 CreateItemData::<T> {603 properties,604 owner: to,605 },606 &budget,607 )608 .map_err(dispatch_to_evm::<T>)?;609 Ok(true)610 }611612 /// @dev Not implemented613 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {614 Err("not implementable".into())615 }616}617618fn get_token_property<T: Config>(619 collection: &CollectionHandle<T>,620 token_id: u32,621 key: &up_data_structs::PropertyKey,622) -> Result<string> {623 collection.consume_store_reads(1)?;624 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))625 .map_err(|_| Error::Revert("Token properties not found".into()))?;626 if let Some(property) = properties.get(key) {627 return Ok(string::from_utf8_lossy(property).into());628 }629630 Err("Property tokenURI not found".into())631}632633fn get_token_permission<T: Config>(634 collection_id: CollectionId,635 key: &PropertyKey,636) -> Result<PropertyPermission> {637 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)638 .map_err(|_| Error::Revert("No permissions for collection".into()))?;639 let a = token_property_permissions640 .get(key)641 .map(Clone::clone)642 .ok_or_else(|| {643 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();644 Error::Revert(alloc::format!("No permission for key {}", key))645 })?;646 Ok(a)647}648649/// @title Unique extensions for ERC721.650#[solidity_interface(name = ERC721UniqueExtensions)]651impl<T: Config> NonfungibleHandle<T>652where653 T::AccountId: From<[u8; 32]>,654{655 /// @notice A descriptive name for a collection of NFTs in this contract656 fn name(&self) -> Result<string> {657 Ok(decode_utf16(self.name.iter().copied())658 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))659 .collect::<string>())660 }661662 /// @notice An abbreviated name for NFTs in this contract663 fn symbol(&self) -> Result<string> {664 Ok(string::from_utf8_lossy(&self.token_prefix).into())665 }666667 /// @notice Set or reaffirm the approved address for an NFT668 /// @dev The zero address indicates there is no approved address.669 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized670 /// operator of the current owner.671 /// @param approved The new substrate address approved NFT controller672 /// @param tokenId The NFT to approve673 #[weight(<SelfWeightOf<T>>::approve())]674 fn approve_cross(675 &mut self,676 caller: caller,677 approved: (address, uint256),678 token_id: uint256,679 ) -> Result<void> {680 let caller = T::CrossAccountId::from_eth(caller);681 let approved = convert_tuple_to_cross_account::<T>(approved)?;682 let token = token_id.try_into()?;683684 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))685 .map_err(dispatch_to_evm::<T>)?;686 Ok(())687 }688689 /// @notice Transfer ownership of an NFT690 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`691 /// is the zero address. Throws if `tokenId` is not a valid NFT.692 /// @param to The new owner693 /// @param tokenId The NFT to transfer694 #[weight(<SelfWeightOf<T>>::transfer())]695 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {696 let caller = T::CrossAccountId::from_eth(caller);697 let to = T::CrossAccountId::from_eth(to);698 let token = token_id.try_into()?;699 let budget = self700 .recorder701 .weight_calls_budget(<StructureWeight<T>>::find_parent());702703 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;704 Ok(())705 }706707 /// @notice Transfer ownership of an NFT from cross account address to cross account address708 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`709 /// is the zero address. Throws if `tokenId` is not a valid NFT.710 /// @param from Cross acccount address of current owner711 /// @param to Cross acccount address of new owner712 /// @param tokenId The NFT to transfer713 #[weight(<SelfWeightOf<T>>::transfer())]714 fn transfer_from_cross(715 &mut self,716 caller: caller,717 from: (address, uint256),718 to: (address, uint256),719 token_id: uint256,720 ) -> Result<void> {721 let caller = T::CrossAccountId::from_eth(caller);722 let from = convert_tuple_to_cross_account::<T>(from)?;723 let to = convert_tuple_to_cross_account::<T>(to)?;724 let token_id = token_id.try_into()?;725 let budget = self726 .recorder727 .weight_calls_budget(<StructureWeight<T>>::find_parent());728 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)729 .map_err(dispatch_to_evm::<T>)?;730 Ok(())731 }732733 /// @notice Burns a specific ERC721 token.734 /// @dev Throws unless `msg.sender` is the current owner or an authorized735 /// operator for this NFT. Throws if `from` is not the current owner. Throws736 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.737 /// @param from The current owner of the NFT738 /// @param tokenId The NFT to transfer739 #[weight(<SelfWeightOf<T>>::burn_from())]740 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {741 let caller = T::CrossAccountId::from_eth(caller);742 let from = T::CrossAccountId::from_eth(from);743 let token = token_id.try_into()?;744 let budget = self745 .recorder746 .weight_calls_budget(<StructureWeight<T>>::find_parent());747748 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)749 .map_err(dispatch_to_evm::<T>)?;750 Ok(())751 }752753 /// @notice Burns a specific ERC721 token.754 /// @dev Throws unless `msg.sender` is the current owner or an authorized755 /// operator for this NFT. Throws if `from` is not the current owner. Throws756 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.757 /// @param from The current owner of the NFT758 /// @param tokenId The NFT to transfer759 #[weight(<SelfWeightOf<T>>::burn_from())]760 fn burn_from_cross(761 &mut self,762 caller: caller,763 from: (address, uint256),764 token_id: uint256,765 ) -> Result<void> {766 let caller = T::CrossAccountId::from_eth(caller);767 let from = convert_tuple_to_cross_account::<T>(from)?;768 let token = token_id.try_into()?;769 let budget = self770 .recorder771 .weight_calls_budget(<StructureWeight<T>>::find_parent());772773 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)774 .map_err(dispatch_to_evm::<T>)?;775 Ok(())776 }777778 /// @notice Returns next free NFT ID.779 fn next_token_id(&self) -> Result<uint256> {780 self.consume_store_reads(1)?;781 Ok(<TokensMinted<T>>::get(self.id)782 .checked_add(1)783 .ok_or("item id overflow")?784 .into())785 }786787 /// @notice Function to mint multiple tokens.788 /// @dev `tokenIds` should be an array of consecutive numbers and first number789 /// should be obtained with `nextTokenId` method790 /// @param to The new owner791 /// @param tokenIds IDs of the minted NFTs792 #[solidity(hide)]793 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]794 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {795 let caller = T::CrossAccountId::from_eth(caller);796 let to = T::CrossAccountId::from_eth(to);797 let mut expected_index = <TokensMinted<T>>::get(self.id)798 .checked_add(1)799 .ok_or("item id overflow")?;800 let budget = self801 .recorder802 .weight_calls_budget(<StructureWeight<T>>::find_parent());803804 let total_tokens = token_ids.len();805 for id in token_ids.into_iter() {806 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;807 if id != expected_index {808 return Err("item id should be next".into());809 }810 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;811 }812 let data = (0..total_tokens)813 .map(|_| CreateItemData::<T> {814 properties: BoundedVec::default(),815 owner: to.clone(),816 })817 .collect();818819 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)820 .map_err(dispatch_to_evm::<T>)?;821 Ok(true)822 }823824 /// @notice Function to mint multiple tokens with the given tokenUris.825 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive826 /// numbers and first number should be obtained with `nextTokenId` method827 /// @param to The new owner828 /// @param tokens array of pairs of token ID and token URI for minted tokens829 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]830 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]831 fn mint_bulk_with_token_uri(832 &mut self,833 caller: caller,834 to: address,835 tokens: Vec<(uint256, string)>,836 ) -> Result<bool> {837 let key = key::url();838 let caller = T::CrossAccountId::from_eth(caller);839 let to = T::CrossAccountId::from_eth(to);840 let mut expected_index = <TokensMinted<T>>::get(self.id)841 .checked_add(1)842 .ok_or("item id overflow")?;843 let budget = self844 .recorder845 .weight_calls_budget(<StructureWeight<T>>::find_parent());846847 let mut data = Vec::with_capacity(tokens.len());848 for (id, token_uri) in tokens {849 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;850 if id != expected_index {851 return Err("item id should be next".into());852 }853 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;854855 let mut properties = CollectionPropertiesVec::default();856 properties857 .try_push(Property {858 key: key.clone(),859 value: token_uri860 .into_bytes()861 .try_into()862 .map_err(|_| "token uri is too long")?,863 })864 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;865866 data.push(CreateItemData::<T> {867 properties,868 owner: to.clone(),869 });870 }871872 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)873 .map_err(dispatch_to_evm::<T>)?;874 Ok(true)875 }876}877878#[solidity_interface(879 name = UniqueNFT,880 is(881 ERC721,882 ERC721Enumerable,883 ERC721UniqueExtensions,884 ERC721UniqueMintable,885 ERC721Burnable,886 ERC721Metadata(if(this.flags.erc721metadata)),887 Collection(via(common_mut returns CollectionHandle<T>)),888 TokenProperties,889 )890)]891impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}892893// Not a tests, but code generators894generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);895generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);896897impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>898where899 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,900{901 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");902903 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {904 call::<T, UniqueNFTCall<T>, _, _>(handle, self)905 }906}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::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},37 CollectionHandle, CollectionPropertyPermissions,38 eth::convert_tuple_to_cross_account,39};40use pallet_evm::{account::CrossAccountId, PrecompileHandle};41use pallet_evm_coder_substrate::call;42use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};4344use crate::{45 AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,46 SelfWeightOf, weights::WeightInfo, TokenProperties,47};4849/// @title A contract that allows to set and delete token properties and change token property permissions.50#[solidity_interface(name = TokenProperties)]51impl<T: Config> NonfungibleHandle<T> {52 /// @notice Set permissions for token property.53 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.54 /// @param key Property key.55 /// @param isMutable Permission to mutate property.56 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.57 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.58 fn set_token_property_permission(59 &mut self,60 caller: caller,61 key: string,62 is_mutable: bool,63 collection_admin: bool,64 token_owner: bool,65 ) -> Result<()> {66 let caller = T::CrossAccountId::from_eth(caller);67 <Pallet<T>>::set_property_permission(68 self,69 &caller,70 PropertyKeyPermission {71 key: <Vec<u8>>::from(key)72 .try_into()73 .map_err(|_| "too long key")?,74 permission: PropertyPermission {75 mutable: is_mutable,76 collection_admin,77 token_owner,78 },79 },80 )81 .map_err(dispatch_to_evm::<T>)82 }8384 /// @notice Set token property value.85 /// @dev Throws error if `msg.sender` has no permission to edit the property.86 /// @param tokenId ID of the token.87 /// @param key Property key.88 /// @param value Property value.89 fn set_property(90 &mut self,91 caller: caller,92 token_id: uint256,93 key: string,94 value: bytes,95 ) -> Result<()> {96 let caller = T::CrossAccountId::from_eth(caller);97 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;98 let key = <Vec<u8>>::from(key)99 .try_into()100 .map_err(|_| "key too long")?;101 let value = value.0.try_into().map_err(|_| "value too long")?;102103 let nesting_budget = self104 .recorder105 .weight_calls_budget(<StructureWeight<T>>::find_parent());106107 <Pallet<T>>::set_token_property(108 self,109 &caller,110 TokenId(token_id),111 Property { key, value },112 &nesting_budget,113 )114 .map_err(dispatch_to_evm::<T>)115 }116117 /// @notice Set token properties value.118 /// @dev Throws error if `msg.sender` has no permission to edit the property.119 /// @param tokenId ID of the token.120 /// @param properties settable properties121 #[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]122 fn set_properties(123 &mut self,124 caller: caller,125 token_id: uint256,126 properties: Vec<(string, bytes)>,127 ) -> Result<()> {128 let caller = T::CrossAccountId::from_eth(caller);129 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;130131 let nesting_budget = self132 .recorder133 .weight_calls_budget(<StructureWeight<T>>::find_parent());134135 let properties = properties136 .into_iter()137 .map(|(key, value)| {138 let key = <Vec<u8>>::from(key)139 .try_into()140 .map_err(|_| "key too large")?;141142 let value = value.0.try_into().map_err(|_| "value too large")?;143144 Ok(Property { key, value })145 })146 .collect::<Result<Vec<_>>>()?;147148 <Pallet<T>>::set_token_properties(149 self,150 &caller,151 TokenId(token_id),152 properties.into_iter(),153 <Pallet<T>>::token_exists(&self, TokenId(token_id)),154 &nesting_budget,155 )156 .map_err(dispatch_to_evm::<T>)157 }158159 /// @notice Delete token property value.160 /// @dev Throws error if `msg.sender` has no permission to edit the property.161 /// @param tokenId ID of the token.162 /// @param key Property key.163 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {164 let caller = T::CrossAccountId::from_eth(caller);165 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;166 let key = <Vec<u8>>::from(key)167 .try_into()168 .map_err(|_| "key too long")?;169170 let nesting_budget = self171 .recorder172 .weight_calls_budget(<StructureWeight<T>>::find_parent());173174 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)175 .map_err(dispatch_to_evm::<T>)176 }177178 /// @notice Get token property value.179 /// @dev Throws error if key not found180 /// @param tokenId ID of the token.181 /// @param key Property key.182 /// @return Property value bytes183 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {184 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;185 let key = <Vec<u8>>::from(key)186 .try_into()187 .map_err(|_| "key too long")?;188189 let props = <TokenProperties<T>>::get((self.id, token_id));190 let prop = props.get(&key).ok_or("key not found")?;191192 Ok(prop.to_vec().into())193 }194}195196#[derive(ToLog)]197pub enum ERC721Events {198 /// @dev This emits when ownership of any NFT changes by any mechanism.199 /// This event emits when NFTs are created (`from` == 0) and destroyed200 /// (`to` == 0). Exception: during contract creation, any number of NFTs201 /// may be created and assigned without emitting Transfer. At the time of202 /// any transfer, the approved address for that NFT (if any) is reset to none.203 Transfer {204 #[indexed]205 from: address,206 #[indexed]207 to: address,208 #[indexed]209 token_id: uint256,210 },211 /// @dev This emits when the approved address for an NFT is changed or212 /// reaffirmed. The zero address indicates there is no approved address.213 /// When a Transfer event emits, this also indicates that the approved214 /// address for that NFT (if any) is reset to none.215 Approval {216 #[indexed]217 owner: address,218 #[indexed]219 approved: address,220 #[indexed]221 token_id: uint256,222 },223 /// @dev This emits when an operator is enabled or disabled for an owner.224 /// The operator can manage all NFTs of the owner.225 #[allow(dead_code)]226 ApprovalForAll {227 #[indexed]228 owner: address,229 #[indexed]230 operator: address,231 approved: bool,232 },233}234235#[derive(ToLog)]236pub enum ERC721UniqueMintableEvents {237 #[allow(dead_code)]238 MintingFinished {},239}240241/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension242/// @dev See https://eips.ethereum.org/EIPS/eip-721243#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]244impl<T: Config> NonfungibleHandle<T>245where246 T::AccountId: From<[u8; 32]>,247{248 /// @notice A descriptive name for a collection of NFTs in this contract249 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`250 #[solidity(hide, rename_selector = "name")]251 fn name_proxy(&self) -> Result<string> {252 self.name()253 }254255 /// @notice An abbreviated name for NFTs in this contract256 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`257 #[solidity(hide, rename_selector = "symbol")]258 fn symbol_proxy(&self) -> Result<string> {259 self.symbol()260 }261262 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.263 ///264 /// @dev If the token has a `url` property and it is not empty, it is returned.265 /// 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`.266 /// If the collection property `baseURI` is empty or absent, return "" (empty string)267 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix268 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).269 ///270 /// @return token's const_metadata271 #[solidity(rename_selector = "tokenURI")]272 fn token_uri(&self, token_id: uint256) -> Result<string> {273 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;274275 match get_token_property(self, token_id_u32, &key::url()).as_deref() {276 Err(_) | Ok("") => (),277 Ok(url) => {278 return Ok(url.into());279 }280 };281282 let base_uri =283 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())284 .map(BoundedVec::into_inner)285 .map(string::from_utf8)286 .transpose()287 .map_err(|e| {288 Error::Revert(alloc::format!(289 "Can not convert value \"baseURI\" to string with error \"{}\"",290 e291 ))292 })?;293294 let base_uri = match base_uri.as_deref() {295 None | Some("") => {296 return Ok("".into());297 }298 Some(base_uri) => base_uri.into(),299 };300301 Ok(302 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {303 Err(_) | Ok("") => base_uri,304 Ok(suffix) => base_uri + suffix,305 },306 )307 }308}309310/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension311/// @dev See https://eips.ethereum.org/EIPS/eip-721312#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]313impl<T: Config> NonfungibleHandle<T> {314 /// @notice Enumerate valid NFTs315 /// @param index A counter less than `totalSupply()`316 /// @return The token identifier for the `index`th NFT,317 /// (sort order not specified)318 fn token_by_index(&self, index: uint256) -> Result<uint256> {319 Ok(index)320 }321322 /// @dev Not implemented323 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {324 // TODO: Not implemetable325 Err("not implemented".into())326 }327328 /// @notice Count NFTs tracked by this contract329 /// @return A count of valid NFTs tracked by this contract, where each one of330 /// them has an assigned and queryable owner not equal to the zero address331 fn total_supply(&self) -> Result<uint256> {332 self.consume_store_reads(1)?;333 Ok(<Pallet<T>>::total_supply(self).into())334 }335}336337/// @title ERC-721 Non-Fungible Token Standard338/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md339#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]340impl<T: Config> NonfungibleHandle<T> {341 /// @notice Count all NFTs assigned to an owner342 /// @dev NFTs assigned to the zero address are considered invalid, and this343 /// function throws for queries about the zero address.344 /// @param owner An address for whom to query the balance345 /// @return The number of NFTs owned by `owner`, possibly zero346 fn balance_of(&self, owner: address) -> Result<uint256> {347 self.consume_store_reads(1)?;348 let owner = T::CrossAccountId::from_eth(owner);349 let balance = <AccountBalance<T>>::get((self.id, owner));350 Ok(balance.into())351 }352 /// @notice Find the owner of an NFT353 /// @dev NFTs assigned to zero address are considered invalid, and queries354 /// about them do throw.355 /// @param tokenId The identifier for an NFT356 /// @return The address of the owner of the NFT357 fn owner_of(&self, token_id: uint256) -> Result<address> {358 self.consume_store_reads(1)?;359 let token: TokenId = token_id.try_into()?;360 Ok(*<TokenData<T>>::get((self.id, token))361 .ok_or("token not found")?362 .owner363 .as_eth())364 }365 /// @dev Not implemented366 #[solidity(rename_selector = "safeTransferFrom")]367 fn safe_transfer_from_with_data(368 &mut self,369 _from: address,370 _to: address,371 _token_id: uint256,372 _data: bytes,373 ) -> Result<void> {374 // TODO: Not implemetable375 Err("not implemented".into())376 }377 /// @dev Not implemented378 fn safe_transfer_from(379 &mut self,380 _from: address,381 _to: address,382 _token_id: uint256,383 ) -> Result<void> {384 // TODO: Not implemetable385 Err("not implemented".into())386 }387388 /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE389 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE390 /// THEY MAY BE PERMANENTLY LOST391 /// @dev Throws unless `msg.sender` is the current owner or an authorized392 /// operator for this NFT. Throws if `from` is not the current owner. Throws393 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.394 /// @param from The current owner of the NFT395 /// @param to The new owner396 /// @param tokenId The NFT to transfer397 #[weight(<SelfWeightOf<T>>::transfer_from())]398 fn transfer_from(399 &mut self,400 caller: caller,401 from: address,402 to: address,403 token_id: uint256,404 ) -> Result<void> {405 let caller = T::CrossAccountId::from_eth(caller);406 let from = T::CrossAccountId::from_eth(from);407 let to = T::CrossAccountId::from_eth(to);408 let token = token_id.try_into()?;409 let budget = self410 .recorder411 .weight_calls_budget(<StructureWeight<T>>::find_parent());412413 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)414 .map_err(dispatch_to_evm::<T>)?;415 Ok(())416 }417418 /// @notice Set or reaffirm the approved address for an NFT419 /// @dev The zero address indicates there is no approved address.420 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized421 /// operator of the current owner.422 /// @param approved The new approved NFT controller423 /// @param tokenId The NFT to approve424 #[weight(<SelfWeightOf<T>>::approve())]425 fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {426 let caller = T::CrossAccountId::from_eth(caller);427 let approved = T::CrossAccountId::from_eth(approved);428 let token = token_id.try_into()?;429430 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))431 .map_err(dispatch_to_evm::<T>)?;432 Ok(())433 }434435 /// @dev Not implemented436 fn set_approval_for_all(437 &mut self,438 _caller: caller,439 _operator: address,440 _approved: bool,441 ) -> Result<void> {442 // TODO: Not implemetable443 Err("not implemented".into())444 }445446 /// @dev Not implemented447 fn get_approved(&self, _token_id: uint256) -> Result<address> {448 // TODO: Not implemetable449 Err("not implemented".into())450 }451452 /// @dev Not implemented453 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {454 // TODO: Not implemetable455 Err("not implemented".into())456 }457}458459/// @title ERC721 Token that can be irreversibly burned (destroyed).460#[solidity_interface(name = ERC721Burnable)]461impl<T: Config> NonfungibleHandle<T> {462 /// @notice Burns a specific ERC721 token.463 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized464 /// operator of the current owner.465 /// @param tokenId The NFT to approve466 #[weight(<SelfWeightOf<T>>::burn_item())]467 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {468 let caller = T::CrossAccountId::from_eth(caller);469 let token = token_id.try_into()?;470471 <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;472 Ok(())473 }474}475476/// @title ERC721 minting logic.477#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]478impl<T: Config> NonfungibleHandle<T> {479 fn minting_finished(&self) -> Result<bool> {480 Ok(false)481 }482483 /// @notice Function to mint token.484 /// @param to The new owner485 /// @return uint256 The id of the newly minted token486 #[weight(<SelfWeightOf<T>>::create_item())]487 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {488 let token_id: uint256 = <TokensMinted<T>>::get(self.id)489 .checked_add(1)490 .ok_or("item id overflow")?491 .into();492 self.mint_check_id(caller, to, token_id)?;493 Ok(token_id)494 }495496 /// @notice Function to mint token.497 /// @dev `tokenId` should be obtained with `nextTokenId` method,498 /// unlike standard, you can't specify it manually499 /// @param to The new owner500 /// @param tokenId ID of the minted NFT501 #[solidity(hide, rename_selector = "mint")]502 #[weight(<SelfWeightOf<T>>::create_item())]503 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {504 let caller = T::CrossAccountId::from_eth(caller);505 let to = T::CrossAccountId::from_eth(to);506 let token_id: u32 = token_id.try_into()?;507 let budget = self508 .recorder509 .weight_calls_budget(<StructureWeight<T>>::find_parent());510511 if <TokensMinted<T>>::get(self.id)512 .checked_add(1)513 .ok_or("item id overflow")?514 != token_id515 {516 return Err("item id should be next".into());517 }518519 <Pallet<T>>::create_item(520 self,521 &caller,522 CreateItemData::<T> {523 properties: BoundedVec::default(),524 owner: to,525 },526 &budget,527 )528 .map_err(dispatch_to_evm::<T>)?;529530 Ok(true)531 }532533 /// @notice Function to mint token with the given tokenUri.534 /// @param to The new owner535 /// @param tokenUri Token URI that would be stored in the NFT properties536 /// @return uint256 The id of the newly minted token537 #[solidity(rename_selector = "mintWithTokenURI")]538 #[weight(<SelfWeightOf<T>>::create_item())]539 fn mint_with_token_uri(540 &mut self,541 caller: caller,542 to: address,543 token_uri: string,544 ) -> 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_with_token_uri_check_id(caller, to, token_id, token_uri)?;550 Ok(token_id)551 }552553 /// @notice Function to mint token with the given tokenUri.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 /// @param tokenUri Token URI that would be stored in the NFT properties559 #[solidity(hide, rename_selector = "mintWithTokenURI")]560 #[weight(<SelfWeightOf<T>>::create_item())]561 fn mint_with_token_uri_check_id(562 &mut self,563 caller: caller,564 to: address,565 token_id: uint256,566 token_uri: string,567 ) -> Result<bool> {568 let key = key::url();569 let permission = get_token_permission::<T>(self.id, &key)?;570 if !permission.collection_admin {571 return Err("Operation is not allowed".into());572 }573574 let caller = T::CrossAccountId::from_eth(caller);575 let to = T::CrossAccountId::from_eth(to);576 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;577 let budget = self578 .recorder579 .weight_calls_budget(<StructureWeight<T>>::find_parent());580581 if <TokensMinted<T>>::get(self.id)582 .checked_add(1)583 .ok_or("item id overflow")?584 != token_id585 {586 return Err("item id should be next".into());587 }588589 let mut properties = CollectionPropertiesVec::default();590 properties591 .try_push(Property {592 key,593 value: token_uri594 .into_bytes()595 .try_into()596 .map_err(|_| "token uri is too long")?,597 })598 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;599600 <Pallet<T>>::create_item(601 self,602 &caller,603 CreateItemData::<T> {604 properties,605 owner: to,606 },607 &budget,608 )609 .map_err(dispatch_to_evm::<T>)?;610 Ok(true)611 }612613 /// @dev Not implemented614 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {615 Err("not implementable".into())616 }617}618619fn get_token_property<T: Config>(620 collection: &CollectionHandle<T>,621 token_id: u32,622 key: &up_data_structs::PropertyKey,623) -> Result<string> {624 collection.consume_store_reads(1)?;625 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))626 .map_err(|_| Error::Revert("Token properties not found".into()))?;627 if let Some(property) = properties.get(key) {628 return Ok(string::from_utf8_lossy(property).into());629 }630631 Err("Property tokenURI not found".into())632}633634fn get_token_permission<T: Config>(635 collection_id: CollectionId,636 key: &PropertyKey,637) -> Result<PropertyPermission> {638 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)639 .map_err(|_| Error::Revert("No permissions for collection".into()))?;640 let a = token_property_permissions641 .get(key)642 .map(Clone::clone)643 .ok_or_else(|| {644 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();645 Error::Revert(alloc::format!("No permission for key {}", key))646 })?;647 Ok(a)648}649650/// @title Unique extensions for ERC721.651#[solidity_interface(name = ERC721UniqueExtensions)]652impl<T: Config> NonfungibleHandle<T>653where654 T::AccountId: From<[u8; 32]>,655{656 /// @notice A descriptive name for a collection of NFTs in this contract657 fn name(&self) -> Result<string> {658 Ok(decode_utf16(self.name.iter().copied())659 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))660 .collect::<string>())661 }662663 /// @notice An abbreviated name for NFTs in this contract664 fn symbol(&self) -> Result<string> {665 Ok(string::from_utf8_lossy(&self.token_prefix).into())666 }667668 /// @notice Set or reaffirm the approved address for an NFT669 /// @dev The zero address indicates there is no approved address.670 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized671 /// operator of the current owner.672 /// @param approved The new substrate address approved NFT controller673 /// @param tokenId The NFT to approve674 #[weight(<SelfWeightOf<T>>::approve())]675 fn approve_cross(676 &mut self,677 caller: caller,678 approved: (address, uint256),679 token_id: uint256,680 ) -> Result<void> {681 let caller = T::CrossAccountId::from_eth(caller);682 let approved = convert_tuple_to_cross_account::<T>(approved)?;683 let token = token_id.try_into()?;684685 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))686 .map_err(dispatch_to_evm::<T>)?;687 Ok(())688 }689690 /// @notice Transfer ownership of an NFT691 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`692 /// is the zero address. Throws if `tokenId` is not a valid NFT.693 /// @param to The new owner694 /// @param tokenId The NFT to transfer695 #[weight(<SelfWeightOf<T>>::transfer())]696 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {697 let caller = T::CrossAccountId::from_eth(caller);698 let to = T::CrossAccountId::from_eth(to);699 let token = token_id.try_into()?;700 let budget = self701 .recorder702 .weight_calls_budget(<StructureWeight<T>>::find_parent());703704 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;705 Ok(())706 }707708 /// @notice Transfer ownership of an NFT from cross account address to cross account address709 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`710 /// is the zero address. Throws if `tokenId` is not a valid NFT.711 /// @param from Cross acccount address of current owner712 /// @param to Cross acccount address of new owner713 /// @param tokenId The NFT to transfer714 #[weight(<SelfWeightOf<T>>::transfer())]715 fn transfer_from_cross(716 &mut self,717 caller: caller,718 from: (address, uint256),719 to: (address, uint256),720 token_id: uint256,721 ) -> Result<void> {722 let caller = T::CrossAccountId::from_eth(caller);723 let from = convert_tuple_to_cross_account::<T>(from)?;724 let to = convert_tuple_to_cross_account::<T>(to)?;725 let token_id = token_id.try_into()?;726 let budget = self727 .recorder728 .weight_calls_budget(<StructureWeight<T>>::find_parent());729 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)730 .map_err(dispatch_to_evm::<T>)?;731 Ok(())732 }733734 /// @notice Burns a specific ERC721 token.735 /// @dev Throws unless `msg.sender` is the current owner or an authorized736 /// operator for this NFT. Throws if `from` is not the current owner. Throws737 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.738 /// @param from The current owner of the NFT739 /// @param tokenId The NFT to transfer740 #[weight(<SelfWeightOf<T>>::burn_from())]741 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {742 let caller = T::CrossAccountId::from_eth(caller);743 let from = T::CrossAccountId::from_eth(from);744 let token = token_id.try_into()?;745 let budget = self746 .recorder747 .weight_calls_budget(<StructureWeight<T>>::find_parent());748749 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)750 .map_err(dispatch_to_evm::<T>)?;751 Ok(())752 }753754 /// @notice Burns a specific ERC721 token.755 /// @dev Throws unless `msg.sender` is the current owner or an authorized756 /// operator for this NFT. Throws if `from` is not the current owner. Throws757 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.758 /// @param from The current owner of the NFT759 /// @param tokenId The NFT to transfer760 #[weight(<SelfWeightOf<T>>::burn_from())]761 fn burn_from_cross(762 &mut self,763 caller: caller,764 from: (address, uint256),765 token_id: uint256,766 ) -> Result<void> {767 let caller = T::CrossAccountId::from_eth(caller);768 let from = convert_tuple_to_cross_account::<T>(from)?;769 let token = token_id.try_into()?;770 let budget = self771 .recorder772 .weight_calls_budget(<StructureWeight<T>>::find_parent());773774 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)775 .map_err(dispatch_to_evm::<T>)?;776 Ok(())777 }778779 /// @notice Returns next free NFT ID.780 fn next_token_id(&self) -> Result<uint256> {781 self.consume_store_reads(1)?;782 Ok(<TokensMinted<T>>::get(self.id)783 .checked_add(1)784 .ok_or("item id overflow")?785 .into())786 }787788 /// @notice Function to mint multiple tokens.789 /// @dev `tokenIds` should be an array of consecutive numbers and first number790 /// should be obtained with `nextTokenId` method791 /// @param to The new owner792 /// @param tokenIds IDs of the minted NFTs793 #[solidity(hide)]794 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]795 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {796 let caller = T::CrossAccountId::from_eth(caller);797 let to = T::CrossAccountId::from_eth(to);798 let mut expected_index = <TokensMinted<T>>::get(self.id)799 .checked_add(1)800 .ok_or("item id overflow")?;801 let budget = self802 .recorder803 .weight_calls_budget(<StructureWeight<T>>::find_parent());804805 let total_tokens = token_ids.len();806 for id in token_ids.into_iter() {807 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;808 if id != expected_index {809 return Err("item id should be next".into());810 }811 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;812 }813 let data = (0..total_tokens)814 .map(|_| CreateItemData::<T> {815 properties: BoundedVec::default(),816 owner: to.clone(),817 })818 .collect();819820 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)821 .map_err(dispatch_to_evm::<T>)?;822 Ok(true)823 }824825 /// @notice Function to mint multiple tokens with the given tokenUris.826 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive827 /// numbers and first number should be obtained with `nextTokenId` method828 /// @param to The new owner829 /// @param tokens array of pairs of token ID and token URI for minted tokens830 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]831 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]832 fn mint_bulk_with_token_uri(833 &mut self,834 caller: caller,835 to: address,836 tokens: Vec<(uint256, string)>,837 ) -> Result<bool> {838 let key = key::url();839 let caller = T::CrossAccountId::from_eth(caller);840 let to = T::CrossAccountId::from_eth(to);841 let mut expected_index = <TokensMinted<T>>::get(self.id)842 .checked_add(1)843 .ok_or("item id overflow")?;844 let budget = self845 .recorder846 .weight_calls_budget(<StructureWeight<T>>::find_parent());847848 let mut data = Vec::with_capacity(tokens.len());849 for (id, token_uri) in tokens {850 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;851 if id != expected_index {852 return Err("item id should be next".into());853 }854 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;855856 let mut properties = CollectionPropertiesVec::default();857 properties858 .try_push(Property {859 key: key.clone(),860 value: token_uri861 .into_bytes()862 .try_into()863 .map_err(|_| "token uri is too long")?,864 })865 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;866867 data.push(CreateItemData::<T> {868 properties,869 owner: to.clone(),870 });871 }872873 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)874 .map_err(dispatch_to_evm::<T>)?;875 Ok(true)876 }877}878879#[solidity_interface(880 name = UniqueNFT,881 is(882 ERC721,883 ERC721Enumerable,884 ERC721UniqueExtensions,885 ERC721UniqueMintable,886 ERC721Burnable,887 ERC721Metadata(if(this.flags.erc721metadata)),888 Collection(via(common_mut returns CollectionHandle<T>)),889 TokenProperties,890 )891)]892impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}893894// Not a tests, but code generators895generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);896generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);897898impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>899where900 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,901{902 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");903904 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {905 call::<T, UniqueNFTCall<T>, _, _>(handle, self)906 }907}