difftreelog
doc
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::{abi::AbiType, ToLog, generate_stubgen, solidity_interface, types::*};28use frame_support::BoundedVec;29use up_data_structs::{30 TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,31 CollectionPropertiesVec,32};33use pallet_evm_coder_substrate::{34 dispatch_to_evm, frontier_contract,35 execution::{Result, PreDispatch, Error},36};37use sp_std::{vec::Vec, vec};38use pallet_common::{39 CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,40 erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},41 eth::{self, TokenUri},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::{U256, Get};4748use crate::{49 AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,50 TokenProperties, SelfWeightOf, weights::WeightInfo,51};5253/// Nft events.54#[derive(ToLog)]55pub enum ERC721TokenEvent {56 /// The token has been changed.57 TokenChanged {58 /// Token ID.59 #[indexed]60 token_id: U256,61 },62}6364frontier_contract! {65 macro_rules! NonfungibleHandle_result {...}66 impl<T: Config> Contract for NonfungibleHandle<T> {...}67}6869/// @title A contract that allows to set and delete token properties and change token property permissions.70#[solidity_interface(name = TokenProperties, events(ERC721TokenEvent), enum(derive(PreDispatch)), enum_attr(weight))]71impl<T: Config> NonfungibleHandle<T> {72 /// @notice Set permissions for token property.73 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.74 /// @param key Property key.75 /// @param isMutable Permission to mutate property.76 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.77 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.78 #[solidity(hide)]79 #[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]80 fn set_token_property_permission(81 &mut self,82 caller: Caller,83 key: String,84 is_mutable: bool,85 collection_admin: bool,86 token_owner: bool,87 ) -> Result<()> {88 let caller = T::CrossAccountId::from_eth(caller);89 <Pallet<T>>::set_token_property_permissions(90 self,91 &caller,92 vec![PropertyKeyPermission {93 key: <Vec<u8>>::from(key)94 .try_into()95 .map_err(|_| "too long key")?,96 permission: PropertyPermission {97 mutable: is_mutable,98 collection_admin,99 token_owner,100 },101 }],102 )103 .map_err(dispatch_to_evm::<T>)104 }105106 /// @notice Set permissions for token property.107 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.108 /// @param permissions Permissions for keys.109 #[weight(<SelfWeightOf<T>>::set_token_property_permissions(permissions.len() as u32))]110 fn set_token_property_permissions(111 &mut self,112 caller: Caller,113 permissions: Vec<eth::TokenPropertyPermission>,114 ) -> Result<()> {115 let caller = T::CrossAccountId::from_eth(caller);116 let perms = eth::TokenPropertyPermission::into_property_key_permissions(permissions)?;117118 <Pallet<T>>::set_token_property_permissions(self, &caller, perms)119 .map_err(dispatch_to_evm::<T>)120 }121122 /// @notice Get permissions for token properties.123 fn token_property_permissions(&self) -> Result<Vec<eth::TokenPropertyPermission>> {124 let perms = <Pallet<T>>::token_property_permission(self.id);125 Ok(perms126 .into_iter()127 .map(eth::TokenPropertyPermission::from)128 .collect())129 }130131 /// @notice Set token property value.132 /// @dev Throws error if `msg.sender` has no permission to edit the property.133 /// @param tokenId ID of the token.134 /// @param key Property key.135 /// @param value Property value.136 #[solidity(hide)]137 #[weight(<SelfWeightOf<T>>::set_token_properties(1))]138 fn set_property(139 &mut self,140 caller: Caller,141 token_id: U256,142 key: String,143 value: Bytes,144 ) -> Result<()> {145 let caller = T::CrossAccountId::from_eth(caller);146 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;147 let key = <Vec<u8>>::from(key)148 .try_into()149 .map_err(|_| "key too long")?;150 let value = value.0.try_into().map_err(|_| "value too long")?;151152 let nesting_budget = self153 .recorder154 .weight_calls_budget(<StructureWeight<T>>::find_parent());155156 <Pallet<T>>::set_token_property(157 self,158 &caller,159 TokenId(token_id),160 Property { key, value },161 &nesting_budget,162 )163 .map_err(dispatch_to_evm::<T>)164 }165166 /// @notice Set token properties value.167 /// @dev Throws error if `msg.sender` has no permission to edit the property.168 /// @param tokenId ID of the token.169 /// @param properties settable properties170 #[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]171 fn set_properties(172 &mut self,173 caller: Caller,174 token_id: U256,175 properties: Vec<eth::Property>,176 ) -> Result<()> {177 let caller = T::CrossAccountId::from_eth(caller);178 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;179180 let nesting_budget = self181 .recorder182 .weight_calls_budget(<StructureWeight<T>>::find_parent());183184 let properties = properties185 .into_iter()186 .map(eth::Property::try_into)187 .collect::<Result<Vec<_>>>()?;188189 <Pallet<T>>::set_token_properties(190 self,191 &caller,192 TokenId(token_id),193 properties.into_iter(),194 false,195 &nesting_budget,196 )197 .map_err(dispatch_to_evm::<T>)198 }199200 /// @notice Delete token property value.201 /// @dev Throws error if `msg.sender` has no permission to edit the property.202 /// @param tokenId ID of the token.203 /// @param key Property key.204 #[solidity(hide)]205 #[weight(<SelfWeightOf<T>>::delete_token_properties(1))]206 fn delete_property(&mut self, token_id: U256, caller: Caller, key: String) -> Result<()> {207 let caller = T::CrossAccountId::from_eth(caller);208 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;209 let key = <Vec<u8>>::from(key)210 .try_into()211 .map_err(|_| "key too long")?;212213 let nesting_budget = self214 .recorder215 .weight_calls_budget(<StructureWeight<T>>::find_parent());216217 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)218 .map_err(dispatch_to_evm::<T>)219 }220221 /// @notice Delete token properties value.222 /// @dev Throws error if `msg.sender` has no permission to edit the property.223 /// @param tokenId ID of the token.224 /// @param keys Properties key.225 #[weight(<SelfWeightOf<T>>::delete_token_properties(keys.len() as u32))]226 fn delete_properties(227 &mut self,228 token_id: U256,229 caller: Caller,230 keys: Vec<String>,231 ) -> Result<()> {232 let caller = T::CrossAccountId::from_eth(caller);233 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;234 let keys = keys235 .into_iter()236 .map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))237 .collect::<Result<Vec<_>>>()?;238239 let nesting_budget = self240 .recorder241 .weight_calls_budget(<StructureWeight<T>>::find_parent());242243 <Pallet<T>>::delete_token_properties(244 self,245 &caller,246 TokenId(token_id),247 keys.into_iter(),248 &nesting_budget,249 )250 .map_err(dispatch_to_evm::<T>)251 }252253 /// @notice Get token property value.254 /// @dev Throws error if key not found255 /// @param tokenId ID of the token.256 /// @param key Property key.257 /// @return Property value bytes258 fn property(&self, token_id: U256, key: String) -> Result<Bytes> {259 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;260 let key = <Vec<u8>>::from(key)261 .try_into()262 .map_err(|_| "key too long")?;263264 let props = <TokenProperties<T>>::get((self.id, token_id));265 let prop = props.get(&key).ok_or("key not found")?;266267 Ok(prop.to_vec().into())268 }269}270271#[derive(ToLog)]272pub enum ERC721Events {273 /// @dev This emits when ownership of any NFT changes by any mechanism.274 /// This event emits when NFTs are created (`from` == 0) and destroyed275 /// (`to` == 0). Exception: during contract creation, any number of NFTs276 /// may be created and assigned without emitting Transfer. At the time of277 /// any transfer, the approved address for that NFT (if any) is reset to none.278 Transfer {279 #[indexed]280 from: Address,281 #[indexed]282 to: Address,283 #[indexed]284 token_id: U256,285 },286 /// @dev This emits when the approved address for an NFT is changed or287 /// reaffirmed. The zero address indicates there is no approved address.288 /// When a Transfer event emits, this also indicates that the approved289 /// address for that NFT (if any) is reset to none.290 Approval {291 #[indexed]292 owner: Address,293 #[indexed]294 approved: Address,295 #[indexed]296 token_id: U256,297 },298 /// @dev This emits when an operator is enabled or disabled for an owner.299 /// The operator can manage all NFTs of the owner.300 #[allow(dead_code)]301 ApprovalForAll {302 #[indexed]303 owner: Address,304 #[indexed]305 operator: Address,306 approved: bool,307 },308}309310/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension311/// @dev See https://eips.ethereum.org/EIPS/eip-721312#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f, enum(derive(PreDispatch)), enum_attr(weight))]313impl<T: Config> NonfungibleHandle<T>314where315 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,316{317 /// @notice A descriptive name for a collection of NFTs in this contract318 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`319 #[solidity(hide, rename_selector = "name")]320 fn name_proxy(&self) -> String {321 self.name()322 }323324 /// @notice An abbreviated name for NFTs in this contract325 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`326 #[solidity(hide, rename_selector = "symbol")]327 fn symbol_proxy(&self) -> String {328 self.symbol()329 }330331 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.332 ///333 /// @dev If the token has a `url` property and it is not empty, it is returned.334 /// 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`.335 /// If the collection property `baseURI` is empty or absent, return "" (empty string)336 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix337 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).338 ///339 /// @return token's const_metadata340 #[solidity(rename_selector = "tokenURI")]341 fn token_uri(&self, token_id: U256) -> Result<String> {342 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;343344 match get_token_property(self, token_id_u32, &key::url()).as_deref() {345 Err(_) | Ok("") => (),346 Ok(url) => {347 return Ok(url.into());348 }349 };350351 let base_uri =352 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())353 .map(BoundedVec::into_inner)354 .map(String::from_utf8)355 .transpose()356 .map_err(|e| {357 Error::Revert(alloc::format!(358 "Can not convert value \"baseURI\" to string with error \"{}\"",359 e360 ))361 })?;362363 let base_uri = match base_uri.as_deref() {364 None | Some("") => {365 return Ok("".into());366 }367 Some(base_uri) => base_uri.into(),368 };369370 Ok(371 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {372 Err(_) | Ok("") => base_uri,373 Ok(suffix) => base_uri + suffix,374 },375 )376 }377}378379/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension380/// @dev See https://eips.ethereum.org/EIPS/eip-721381#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63, enum(derive(PreDispatch)), enum_attr(weight))]382impl<T: Config> NonfungibleHandle<T> {383 /// @notice Enumerate valid NFTs384 /// @param index A counter less than `totalSupply()`385 /// @return The token identifier for the `index`th NFT,386 /// (sort order not specified)387 fn token_by_index(&self, index: U256) -> U256 {388 index389 }390391 /// @dev Not implemented392 fn token_of_owner_by_index(&self, _owner: Address, _index: U256) -> Result<U256> {393 // TODO: Not implemetable394 Err("not implemented".into())395 }396397 /// @notice Count NFTs tracked by this contract398 /// @return A count of valid NFTs tracked by this contract, where each one of399 /// them has an assigned and queryable owner not equal to the zero address400 fn total_supply(&self) -> Result<U256> {401 self.consume_store_reads(1)?;402 Ok(<Pallet<T>>::total_supply(self).into())403 }404}405406/// @title ERC-721 Non-Fungible Token Standard407/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md408#[solidity_interface(name = ERC721, events(ERC721Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x80ac58cd)]409impl<T: Config> NonfungibleHandle<T> {410 /// @notice Count all NFTs assigned to an owner411 /// @dev NFTs assigned to the zero address are considered invalid, and this412 /// function throws for queries about the zero address.413 /// @param owner An address for whom to query the balance414 /// @return The number of NFTs owned by `owner`, possibly zero415 fn balance_of(&self, owner: Address) -> Result<U256> {416 self.consume_store_reads(1)?;417 let owner = T::CrossAccountId::from_eth(owner);418 let balance = <AccountBalance<T>>::get((self.id, owner));419 Ok(balance.into())420 }421 /// @notice Find the owner of an NFT422 /// @dev NFTs assigned to zero address are considered invalid, and queries423 /// about them do throw.424 /// @param tokenId The identifier for an NFT425 /// @return The address of the owner of the NFT426 fn owner_of(&self, token_id: U256) -> Result<Address> {427 self.consume_store_reads(1)?;428 let token: TokenId = token_id.try_into()?;429 Ok(*<TokenData<T>>::get((self.id, token))430 .ok_or("token not found")?431 .owner432 .as_eth())433 }434 /// @dev Not implemented435 #[solidity(rename_selector = "safeTransferFrom")]436 fn safe_transfer_from_with_data(437 &mut self,438 _from: Address,439 _to: Address,440 _token_id: U256,441 _data: Bytes,442 ) -> Result<()> {443 // TODO: Not implemetable444 Err("not implemented".into())445 }446 /// @dev Not implemented447 fn safe_transfer_from(&mut self, _from: Address, _to: Address, _token_id: U256) -> Result<()> {448 // TODO: Not implemetable449 Err("not implemented".into())450 }451452 /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE453 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE454 /// THEY MAY BE PERMANENTLY LOST455 /// @dev Throws unless `msg.sender` is the current owner or an authorized456 /// operator for this NFT. Throws if `from` is not the current owner. Throws457 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.458 /// @param from The current owner of the NFT459 /// @param to The new owner460 /// @param tokenId The NFT to transfer461 #[weight(<SelfWeightOf<T>>::transfer_from())]462 fn transfer_from(463 &mut self,464 caller: Caller,465 from: Address,466 to: Address,467 token_id: U256,468 ) -> Result<()> {469 let caller = T::CrossAccountId::from_eth(caller);470 let from = T::CrossAccountId::from_eth(from);471 let to = T::CrossAccountId::from_eth(to);472 let token = token_id.try_into()?;473 let budget = self474 .recorder475 .weight_calls_budget(<StructureWeight<T>>::find_parent());476477 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)478 .map_err(dispatch_to_evm::<T>)?;479 Ok(())480 }481482 /// @notice Set or reaffirm the approved address for an NFT483 /// @dev The zero address indicates there is no approved address.484 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized485 /// operator of the current owner.486 /// @param approved The new approved NFT controller487 /// @param tokenId The NFT to approve488 #[weight(<SelfWeightOf<T>>::approve())]489 fn approve(&mut self, caller: Caller, approved: Address, token_id: U256) -> Result<()> {490 let caller = T::CrossAccountId::from_eth(caller);491 let approved = T::CrossAccountId::from_eth(approved);492 let token = token_id.try_into()?;493494 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))495 .map_err(dispatch_to_evm::<T>)?;496 Ok(())497 }498499 /// @notice Sets or unsets the approval of a given operator.500 /// The `operator` is allowed to transfer all tokens of the `caller` on their behalf.501 /// @param operator Operator502 /// @param approved Should operator status be granted or revoked?503 #[weight(<SelfWeightOf<T>>::set_allowance_for_all())]504 fn set_approval_for_all(505 &mut self,506 caller: Caller,507 operator: Address,508 approved: bool,509 ) -> Result<()> {510 let caller = T::CrossAccountId::from_eth(caller);511 let operator = T::CrossAccountId::from_eth(operator);512513 <Pallet<T>>::set_allowance_for_all(self, &caller, &operator, approved)514 .map_err(dispatch_to_evm::<T>)?;515 Ok(())516 }517518 /// @dev Not implemented519 fn get_approved(&self, token_id: U256) -> Result<Address> {520 let token = token_id.try_into()?;521 let operator = <Pallet<T>>::get_allowance(self, token).map_err(dispatch_to_evm::<T>)?;522 Ok(if let Some(operator) = operator {523 *operator.as_eth()524 } else {525 Address::zero()526 })527 }528529 /// @notice Tells whether the given `owner` approves the `operator`.530 #[weight(<SelfWeightOf<T>>::allowance_for_all())]531 fn is_approved_for_all(&self, owner: Address, operator: Address) -> Result<bool> {532 let owner = T::CrossAccountId::from_eth(owner);533 let operator = T::CrossAccountId::from_eth(operator);534535 Ok(<Pallet<T>>::allowance_for_all(self, &owner, &operator))536 }537}538539/// @title ERC721 Token that can be irreversibly burned (destroyed).540#[solidity_interface(name = ERC721Burnable, enum(derive(PreDispatch)), enum_attr(weight))]541impl<T: Config> NonfungibleHandle<T> {542 /// @notice Burns a specific ERC721 token.543 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized544 /// operator of the current owner.545 /// @param tokenId The NFT to approve546 #[weight(<SelfWeightOf<T>>::burn_item())]547 fn burn(&mut self, caller: Caller, token_id: U256) -> Result<()> {548 let caller = T::CrossAccountId::from_eth(caller);549 let token = token_id.try_into()?;550551 <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;552 Ok(())553 }554}555556/// @title ERC721 minting logic.557#[solidity_interface(name = ERC721UniqueMintable, enum(derive(PreDispatch)), enum_attr(weight))]558impl<T: Config> NonfungibleHandle<T> {559 /// @notice Function to mint a token.560 /// @param to The new owner561 /// @return uint256 The id of the newly minted token562 #[weight(<SelfWeightOf<T>>::create_item())]563 fn mint(&mut self, caller: Caller, to: Address) -> Result<U256> {564 let token_id: U256 = <TokensMinted<T>>::get(self.id)565 .checked_add(1)566 .ok_or("item id overflow")?567 .into();568 self.mint_check_id(caller, to, token_id)?;569 Ok(token_id)570 }571572 /// @notice Function to mint a token.573 /// @dev `tokenId` should be obtained with `nextTokenId` method,574 /// unlike standard, you can't specify it manually575 /// @param to The new owner576 /// @param tokenId ID of the minted NFT577 #[solidity(hide, rename_selector = "mint")]578 #[weight(<SelfWeightOf<T>>::create_item())]579 fn mint_check_id(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<bool> {580 let caller = T::CrossAccountId::from_eth(caller);581 let to = T::CrossAccountId::from_eth(to);582 let token_id: u32 = token_id.try_into()?;583 let budget = self584 .recorder585 .weight_calls_budget(<StructureWeight<T>>::find_parent());586587 if <TokensMinted<T>>::get(self.id)588 .checked_add(1)589 .ok_or("item id overflow")?590 != token_id591 {592 return Err("item id should be next".into());593 }594595 <Pallet<T>>::create_item(596 self,597 &caller,598 CreateItemData::<T> {599 properties: BoundedVec::default(),600 owner: to,601 },602 &budget,603 )604 .map_err(dispatch_to_evm::<T>)?;605606 Ok(true)607 }608609 /// @notice Function to mint token with the given tokenUri.610 /// @param to The new owner611 /// @param tokenUri Token URI that would be stored in the NFT properties612 /// @return uint256 The id of the newly minted token613 #[solidity(rename_selector = "mintWithTokenURI")]614 #[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]615 fn mint_with_token_uri(616 &mut self,617 caller: Caller,618 to: Address,619 token_uri: String,620 ) -> Result<U256> {621 let token_id: U256 = <TokensMinted<T>>::get(self.id)622 .checked_add(1)623 .ok_or("item id overflow")?624 .into();625 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;626 Ok(token_id)627 }628629 /// @notice Function to mint token with the given tokenUri.630 /// @dev `tokenId` should be obtained with `nextTokenId` method,631 /// unlike standard, you can't specify it manually632 /// @param to The new owner633 /// @param tokenId ID of the minted NFT634 /// @param tokenUri Token URI that would be stored in the NFT properties635 #[solidity(hide, rename_selector = "mintWithTokenURI")]636 #[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]637 fn mint_with_token_uri_check_id(638 &mut self,639 caller: Caller,640 to: Address,641 token_id: U256,642 token_uri: String,643 ) -> Result<bool> {644 let key = key::url();645 let permission = get_token_permission::<T>(self.id, &key)?;646 if !permission.collection_admin {647 return Err("Operation is not allowed".into());648 }649650 let caller = T::CrossAccountId::from_eth(caller);651 let to = T::CrossAccountId::from_eth(to);652 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;653 let budget = self654 .recorder655 .weight_calls_budget(<StructureWeight<T>>::find_parent());656657 if <TokensMinted<T>>::get(self.id)658 .checked_add(1)659 .ok_or("item id overflow")?660 != token_id661 {662 return Err("item id should be next".into());663 }664665 let mut properties = CollectionPropertiesVec::default();666 properties667 .try_push(Property {668 key,669 value: token_uri670 .into_bytes()671 .try_into()672 .map_err(|_| "token uri is too long")?,673 })674 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;675676 <Pallet<T>>::create_item(677 self,678 &caller,679 CreateItemData::<T> {680 properties,681 owner: to,682 },683 &budget,684 )685 .map_err(dispatch_to_evm::<T>)?;686 Ok(true)687 }688}689690fn get_token_property<T: Config>(691 collection: &CollectionHandle<T>,692 token_id: u32,693 key: &up_data_structs::PropertyKey,694) -> Result<String> {695 collection.consume_store_reads(1)?;696 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))697 .map_err(|_| Error::Revert("Token properties not found".into()))?;698 if let Some(property) = properties.get(key) {699 return Ok(String::from_utf8_lossy(property).into());700 }701702 Err("Property tokenURI not found".into())703}704705fn get_token_permission<T: Config>(706 collection_id: CollectionId,707 key: &PropertyKey,708) -> Result<PropertyPermission> {709 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)710 .map_err(|_| Error::Revert("No permissions for collection".into()))?;711 let a = token_property_permissions712 .get(key)713 .map(Clone::clone)714 .ok_or_else(|| {715 let key = String::from_utf8(key.clone().into_inner()).unwrap_or_default();716 Error::Revert(alloc::format!("No permission for key {}", key))717 })?;718 Ok(a)719}720721/// @title Unique extensions for ERC721.722#[solidity_interface(name = ERC721UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))]723impl<T: Config> NonfungibleHandle<T>724where725 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,726{727 /// @notice A descriptive name for a collection of NFTs in this contract728 fn name(&self) -> String {729 decode_utf16(self.name.iter().copied())730 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))731 .collect::<String>()732 }733734 /// @notice An abbreviated name for NFTs in this contract735 fn symbol(&self) -> String {736 String::from_utf8_lossy(&self.token_prefix).into()737 }738739 /// @notice A description for the collection.740 fn description(&self) -> String {741 decode_utf16(self.description.iter().copied())742 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))743 .collect::<String>()744 }745746 /// Returns the owner (in cross format) of the token.747 ///748 /// @param tokenId Id for the token.749 fn cross_owner_of(&self, token_id: U256) -> Result<eth::CrossAddress> {750 Self::token_owner(&self, token_id.try_into()?)751 .map(|o| eth::CrossAddress::from_sub_cross_account::<T>(&o))752 .map_err(|_| Error::Revert("token not found".into()))753 }754755 /// Returns the token properties.756 ///757 /// @param tokenId Id for the token.758 /// @param keys Properties keys. Empty keys for all propertyes.759 /// @return Vector of properties key/value pairs.760 fn properties(&self, token_id: U256, keys: Vec<String>) -> Result<Vec<eth::Property>> {761 let keys = keys762 .into_iter()763 .map(|key| {764 <Vec<u8>>::from(key)765 .try_into()766 .map_err(|_| Error::Revert("key too large".into()))767 })768 .collect::<Result<Vec<_>>>()?;769770 <Self as CommonCollectionOperations<T>>::token_properties(771 &self,772 token_id.try_into()?,773 if keys.is_empty() { None } else { Some(keys) },774 )775 .into_iter()776 .map(eth::Property::try_from)777 .collect::<Result<Vec<_>>>()778 }779780 /// @notice Set or reaffirm the approved address for an NFT781 /// @dev The zero address indicates there is no approved address.782 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized783 /// operator of the current owner.784 /// @param approved The new substrate address approved NFT controller785 /// @param tokenId The NFT to approve786 #[weight(<SelfWeightOf<T>>::approve())]787 fn approve_cross(788 &mut self,789 caller: Caller,790 approved: eth::CrossAddress,791 token_id: U256,792 ) -> Result<()> {793 let caller = T::CrossAccountId::from_eth(caller);794 let approved = approved.into_sub_cross_account::<T>()?;795 let token = token_id.try_into()?;796797 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))798 .map_err(dispatch_to_evm::<T>)?;799 Ok(())800 }801802 /// @notice Transfer ownership of an NFT803 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`804 /// is the zero address. Throws if `tokenId` is not a valid NFT.805 /// @param to The new owner806 /// @param tokenId The NFT to transfer807 #[weight(<SelfWeightOf<T>>::transfer())]808 fn transfer(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<()> {809 let caller = T::CrossAccountId::from_eth(caller);810 let to = T::CrossAccountId::from_eth(to);811 let token = token_id.try_into()?;812 let budget = self813 .recorder814 .weight_calls_budget(<StructureWeight<T>>::find_parent());815816 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;817 Ok(())818 }819820 /// @notice Transfer ownership of an NFT821 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`822 /// is the zero address. Throws if `tokenId` is not a valid NFT.823 /// @param to The new owner824 /// @param tokenId The NFT to transfer825 #[weight(<SelfWeightOf<T>>::transfer())]826 fn transfer_cross(827 &mut self,828 caller: Caller,829 to: eth::CrossAddress,830 token_id: U256,831 ) -> Result<()> {832 let caller = T::CrossAccountId::from_eth(caller);833 let to = to.into_sub_cross_account::<T>()?;834 let token = token_id.try_into()?;835 let budget = self836 .recorder837 .weight_calls_budget(<StructureWeight<T>>::find_parent());838839 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;840 Ok(())841 }842843 /// @notice Transfer ownership of an NFT from cross account address to cross account address844 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`845 /// is the zero address. Throws if `tokenId` is not a valid NFT.846 /// @param from Cross acccount address of current owner847 /// @param to Cross acccount address of new owner848 /// @param tokenId The NFT to transfer849 #[weight(<SelfWeightOf<T>>::transfer())]850 fn transfer_from_cross(851 &mut self,852 caller: Caller,853 from: eth::CrossAddress,854 to: eth::CrossAddress,855 token_id: U256,856 ) -> Result<()> {857 let caller = T::CrossAccountId::from_eth(caller);858 let from = from.into_sub_cross_account::<T>()?;859 let to = to.into_sub_cross_account::<T>()?;860 let token_id = token_id.try_into()?;861 let budget = self862 .recorder863 .weight_calls_budget(<StructureWeight<T>>::find_parent());864 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)865 .map_err(dispatch_to_evm::<T>)?;866 Ok(())867 }868869 /// @notice Burns a specific ERC721 token.870 /// @dev Throws unless `msg.sender` is the current owner or an authorized871 /// operator for this NFT. Throws if `from` is not the current owner. Throws872 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.873 /// @param from The current owner of the NFT874 /// @param tokenId The NFT to transfer875 #[solidity(hide)]876 #[weight(<SelfWeightOf<T>>::burn_from())]877 fn burn_from(&mut self, caller: Caller, from: Address, token_id: U256) -> Result<()> {878 let caller = T::CrossAccountId::from_eth(caller);879 let from = T::CrossAccountId::from_eth(from);880 let token = token_id.try_into()?;881 let budget = self882 .recorder883 .weight_calls_budget(<StructureWeight<T>>::find_parent());884885 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)886 .map_err(dispatch_to_evm::<T>)?;887 Ok(())888 }889890 /// @notice Burns a specific ERC721 token.891 /// @dev Throws unless `msg.sender` is the current owner or an authorized892 /// operator for this NFT. Throws if `from` is not the current owner. Throws893 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.894 /// @param from The current owner of the NFT895 /// @param tokenId The NFT to transfer896 #[weight(<SelfWeightOf<T>>::burn_from())]897 fn burn_from_cross(898 &mut self,899 caller: Caller,900 from: eth::CrossAddress,901 token_id: U256,902 ) -> Result<()> {903 let caller = T::CrossAccountId::from_eth(caller);904 let from = from.into_sub_cross_account::<T>()?;905 let token = token_id.try_into()?;906 let budget = self907 .recorder908 .weight_calls_budget(<StructureWeight<T>>::find_parent());909910 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)911 .map_err(dispatch_to_evm::<T>)?;912 Ok(())913 }914915 /// @notice Returns next free NFT ID.916 fn next_token_id(&self) -> Result<U256> {917 self.consume_store_reads(1)?;918 Ok(<TokensMinted<T>>::get(self.id)919 .checked_add(1)920 .ok_or("item id overflow")?921 .into())922 }923924 /// @notice Function to mint multiple tokens.925 /// @dev `tokenIds` should be an array of consecutive numbers and first number926 /// should be obtained with `nextTokenId` method927 /// @param to The new owner928 /// @param tokenIds IDs of the minted NFTs929 #[solidity(hide)]930 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]931 fn mint_bulk(&mut self, caller: Caller, to: Address, token_ids: Vec<U256>) -> Result<bool> {932 let caller = T::CrossAccountId::from_eth(caller);933 let to = T::CrossAccountId::from_eth(to);934 let mut expected_index = <TokensMinted<T>>::get(self.id)935 .checked_add(1)936 .ok_or("item id overflow")?;937 let budget = self938 .recorder939 .weight_calls_budget(<StructureWeight<T>>::find_parent());940941 let total_tokens = token_ids.len();942 for id in token_ids.into_iter() {943 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;944 if id != expected_index {945 return Err("item id should be next".into());946 }947 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;948 }949 let data = (0..total_tokens)950 .map(|_| CreateItemData::<T> {951 properties: BoundedVec::default(),952 owner: to.clone(),953 })954 .collect();955956 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)957 .map_err(dispatch_to_evm::<T>)?;958 Ok(true)959 }960961 /// @notice Function to mint multiple tokens with the given tokenUris.962 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive963 /// numbers and first number should be obtained with `nextTokenId` method964 /// @param to The new owner965 /// @param tokens array of pairs of token ID and token URI for minted tokens966 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]967 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32) + <SelfWeightOf<T>>::set_token_properties(tokens.len() as u32))]968 fn mint_bulk_with_token_uri(969 &mut self,970 caller: Caller,971 to: Address,972 tokens: Vec<TokenUri>,973 ) -> Result<bool> {974 let key = key::url();975 let caller = T::CrossAccountId::from_eth(caller);976 let to = T::CrossAccountId::from_eth(to);977 let mut expected_index = <TokensMinted<T>>::get(self.id)978 .checked_add(1)979 .ok_or("item id overflow")?;980 let budget = self981 .recorder982 .weight_calls_budget(<StructureWeight<T>>::find_parent());983984 let mut data = Vec::with_capacity(tokens.len());985 for TokenUri { id, uri } in tokens {986 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;987 if id != expected_index {988 return Err("item id should be next".into());989 }990 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;991992 let mut properties = CollectionPropertiesVec::default();993 properties994 .try_push(Property {995 key: key.clone(),996 value: uri997 .into_bytes()998 .try_into()999 .map_err(|_| "token uri is too long")?,1000 })1001 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;10021003 data.push(CreateItemData::<T> {1004 properties,1005 owner: to.clone(),1006 });1007 }10081009 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)1010 .map_err(dispatch_to_evm::<T>)?;1011 Ok(true)1012 }10131014 /// @notice Function to mint a token.1015 /// @param to The new owner crossAccountId1016 /// @param properties Properties of minted token1017 /// @return uint256 The id of the newly minted token1018 #[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]1019 fn mint_cross(1020 &mut self,1021 caller: Caller,1022 to: eth::CrossAddress,1023 properties: Vec<eth::Property>,1024 ) -> Result<U256> {1025 let token_id = <TokensMinted<T>>::get(self.id)1026 .checked_add(1)1027 .ok_or("item id overflow")?;10281029 let to = to.into_sub_cross_account::<T>()?;10301031 let properties = properties1032 .into_iter()1033 .map(eth::Property::try_into)1034 .collect::<Result<Vec<_>>>()?1035 .try_into()1036 .map_err(|_| Error::Revert(alloc::format!("too many properties")))?;10371038 let caller = T::CrossAccountId::from_eth(caller);10391040 let budget = self1041 .recorder1042 .weight_calls_budget(<StructureWeight<T>>::find_parent());10431044 <Pallet<T>>::create_item(1045 self,1046 &caller,1047 CreateItemData::<T> {1048 properties,1049 owner: to,1050 },1051 &budget,1052 )1053 .map_err(dispatch_to_evm::<T>)?;10541055 Ok(token_id.into())1056 }10571058 /// @notice Returns collection helper contract address1059 fn collection_helper_address(&self) -> Address {1060 T::ContractAddress::get()1061 }1062}10631064#[solidity_interface(1065 name = UniqueNFT,1066 is(1067 ERC721,1068 ERC721Enumerable,1069 ERC721UniqueExtensions,1070 ERC721UniqueMintable,1071 ERC721Burnable,1072 ERC721Metadata(if(this.flags.erc721metadata)),1073 Collection(via(common_mut returns CollectionHandle<T>)),1074 TokenProperties,1075 ),1076 enum(derive(PreDispatch)),1077)]1078impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}10791080// Not a tests, but code generators1081generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);1082generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);10831084impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>1085where1086 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1087{1088 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");10891090 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {1091 call::<T, UniqueNFTCall<T>, _, _>(handle, self)1092 }1093}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::{abi::AbiType, ToLog, generate_stubgen, solidity_interface, types::*};28use frame_support::BoundedVec;29use up_data_structs::{30 TokenId, PropertyPermission, PropertyKeyPermission, Property, CollectionId, PropertyKey,31 CollectionPropertiesVec,32};33use pallet_evm_coder_substrate::{34 dispatch_to_evm, frontier_contract,35 execution::{Result, PreDispatch, Error},36};37use sp_std::{vec::Vec, vec};38use pallet_common::{39 CollectionHandle, CollectionPropertyPermissions, CommonCollectionOperations,40 erc::{CommonEvmHandler, PrecompileResult, CollectionCall, static_property::key},41 eth::{self, TokenUri},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::{U256, Get};4748use crate::{49 AccountBalance, Config, CreateItemData, NonfungibleHandle, Pallet, TokenData, TokensMinted,50 TokenProperties, SelfWeightOf, weights::WeightInfo,51};5253/// Nft events.54#[derive(ToLog)]55pub enum ERC721TokenEvent {56 /// The token has been changed.57 TokenChanged {58 /// Token ID.59 #[indexed]60 token_id: U256,61 },62}6364frontier_contract! {65 macro_rules! NonfungibleHandle_result {...}66 impl<T: Config> Contract for NonfungibleHandle<T> {...}67}6869/// @title A contract that allows to set and delete token properties and change token property permissions.70#[solidity_interface(name = TokenProperties, events(ERC721TokenEvent), enum(derive(PreDispatch)), enum_attr(weight))]71impl<T: Config> NonfungibleHandle<T> {72 /// @notice Set permissions for token property.73 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.74 /// @param key Property key.75 /// @param isMutable Permission to mutate property.76 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.77 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.78 #[solidity(hide)]79 #[weight(<SelfWeightOf<T>>::set_token_property_permissions(1))]80 fn set_token_property_permission(81 &mut self,82 caller: Caller,83 key: String,84 is_mutable: bool,85 collection_admin: bool,86 token_owner: bool,87 ) -> Result<()> {88 let caller = T::CrossAccountId::from_eth(caller);89 <Pallet<T>>::set_token_property_permissions(90 self,91 &caller,92 vec![PropertyKeyPermission {93 key: <Vec<u8>>::from(key)94 .try_into()95 .map_err(|_| "too long key")?,96 permission: PropertyPermission {97 mutable: is_mutable,98 collection_admin,99 token_owner,100 },101 }],102 )103 .map_err(dispatch_to_evm::<T>)104 }105106 /// @notice Set permissions for token property.107 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.108 /// @param permissions Permissions for keys.109 #[weight(<SelfWeightOf<T>>::set_token_property_permissions(permissions.len() as u32))]110 fn set_token_property_permissions(111 &mut self,112 caller: Caller,113 permissions: Vec<eth::TokenPropertyPermission>,114 ) -> Result<()> {115 let caller = T::CrossAccountId::from_eth(caller);116 let perms = eth::TokenPropertyPermission::into_property_key_permissions(permissions)?;117118 <Pallet<T>>::set_token_property_permissions(self, &caller, perms)119 .map_err(dispatch_to_evm::<T>)120 }121122 /// @notice Get permissions for token properties.123 fn token_property_permissions(&self) -> Result<Vec<eth::TokenPropertyPermission>> {124 let perms = <Pallet<T>>::token_property_permission(self.id);125 Ok(perms126 .into_iter()127 .map(eth::TokenPropertyPermission::from)128 .collect())129 }130131 /// @notice Set token property value.132 /// @dev Throws error if `msg.sender` has no permission to edit the property.133 /// @param tokenId ID of the token.134 /// @param key Property key.135 /// @param value Property value.136 #[solidity(hide)]137 #[weight(<SelfWeightOf<T>>::set_token_properties(1))]138 fn set_property(139 &mut self,140 caller: Caller,141 token_id: U256,142 key: String,143 value: Bytes,144 ) -> Result<()> {145 let caller = T::CrossAccountId::from_eth(caller);146 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;147 let key = <Vec<u8>>::from(key)148 .try_into()149 .map_err(|_| "key too long")?;150 let value = value.0.try_into().map_err(|_| "value too long")?;151152 let nesting_budget = self153 .recorder154 .weight_calls_budget(<StructureWeight<T>>::find_parent());155156 <Pallet<T>>::set_token_property(157 self,158 &caller,159 TokenId(token_id),160 Property { key, value },161 &nesting_budget,162 )163 .map_err(dispatch_to_evm::<T>)164 }165166 /// @notice Set token properties value.167 /// @dev Throws error if `msg.sender` has no permission to edit the property.168 /// @param tokenId ID of the token.169 /// @param properties settable properties170 #[weight(<SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]171 fn set_properties(172 &mut self,173 caller: Caller,174 token_id: U256,175 properties: Vec<eth::Property>,176 ) -> Result<()> {177 let caller = T::CrossAccountId::from_eth(caller);178 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;179180 let nesting_budget = self181 .recorder182 .weight_calls_budget(<StructureWeight<T>>::find_parent());183184 let properties = properties185 .into_iter()186 .map(eth::Property::try_into)187 .collect::<Result<Vec<_>>>()?;188189 <Pallet<T>>::set_token_properties(190 self,191 &caller,192 TokenId(token_id),193 properties.into_iter(),194 false,195 &nesting_budget,196 )197 .map_err(dispatch_to_evm::<T>)198 }199200 /// @notice Delete token property value.201 /// @dev Throws error if `msg.sender` has no permission to edit the property.202 /// @param tokenId ID of the token.203 /// @param key Property key.204 #[solidity(hide)]205 #[weight(<SelfWeightOf<T>>::delete_token_properties(1))]206 fn delete_property(&mut self, token_id: U256, caller: Caller, key: String) -> Result<()> {207 let caller = T::CrossAccountId::from_eth(caller);208 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;209 let key = <Vec<u8>>::from(key)210 .try_into()211 .map_err(|_| "key too long")?;212213 let nesting_budget = self214 .recorder215 .weight_calls_budget(<StructureWeight<T>>::find_parent());216217 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)218 .map_err(dispatch_to_evm::<T>)219 }220221 /// @notice Delete token properties value.222 /// @dev Throws error if `msg.sender` has no permission to edit the property.223 /// @param tokenId ID of the token.224 /// @param keys Properties key.225 #[weight(<SelfWeightOf<T>>::delete_token_properties(keys.len() as u32))]226 fn delete_properties(227 &mut self,228 token_id: U256,229 caller: Caller,230 keys: Vec<String>,231 ) -> Result<()> {232 let caller = T::CrossAccountId::from_eth(caller);233 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;234 let keys = keys235 .into_iter()236 .map(|k| Ok(<Vec<u8>>::from(k).try_into().map_err(|_| "key too long")?))237 .collect::<Result<Vec<_>>>()?;238239 let nesting_budget = self240 .recorder241 .weight_calls_budget(<StructureWeight<T>>::find_parent());242243 <Pallet<T>>::delete_token_properties(244 self,245 &caller,246 TokenId(token_id),247 keys.into_iter(),248 &nesting_budget,249 )250 .map_err(dispatch_to_evm::<T>)251 }252253 /// @notice Get token property value.254 /// @dev Throws error if key not found255 /// @param tokenId ID of the token.256 /// @param key Property key.257 /// @return Property value bytes258 fn property(&self, token_id: U256, key: String) -> Result<Bytes> {259 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;260 let key = <Vec<u8>>::from(key)261 .try_into()262 .map_err(|_| "key too long")?;263264 let props = <TokenProperties<T>>::get((self.id, token_id));265 let prop = props.get(&key).ok_or("key not found")?;266267 Ok(prop.to_vec().into())268 }269}270271#[derive(ToLog)]272pub enum ERC721Events {273 /// @dev This emits when ownership of any NFT changes by any mechanism.274 /// This event emits when NFTs are created (`from` == 0) and destroyed275 /// (`to` == 0). Exception: during contract creation, any number of NFTs276 /// may be created and assigned without emitting Transfer. At the time of277 /// any transfer, the approved address for that NFT (if any) is reset to none.278 Transfer {279 #[indexed]280 from: Address,281 #[indexed]282 to: Address,283 #[indexed]284 token_id: U256,285 },286 /// @dev This emits when the approved address for an NFT is changed or287 /// reaffirmed. The zero address indicates there is no approved address.288 /// When a Transfer event emits, this also indicates that the approved289 /// address for that NFT (if any) is reset to none.290 Approval {291 #[indexed]292 owner: Address,293 #[indexed]294 approved: Address,295 #[indexed]296 token_id: U256,297 },298 /// @dev This emits when an operator is enabled or disabled for an owner.299 /// The operator can manage all NFTs of the owner.300 #[allow(dead_code)]301 ApprovalForAll {302 #[indexed]303 owner: Address,304 #[indexed]305 operator: Address,306 approved: bool,307 },308}309310/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension311/// @dev See https://eips.ethereum.org/EIPS/eip-721312#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f, enum(derive(PreDispatch)), enum_attr(weight))]313impl<T: Config> NonfungibleHandle<T>314where315 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,316{317 /// @notice A descriptive name for a collection of NFTs in this contract318 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`319 #[solidity(hide, rename_selector = "name")]320 fn name_proxy(&self) -> String {321 self.name()322 }323324 /// @notice An abbreviated name for NFTs in this contract325 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`326 #[solidity(hide, rename_selector = "symbol")]327 fn symbol_proxy(&self) -> String {328 self.symbol()329 }330331 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.332 ///333 /// @dev If the token has a `url` property and it is not empty, it is returned.334 /// 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`.335 /// If the collection property `baseURI` is empty or absent, return "" (empty string)336 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix337 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).338 ///339 /// @return token's const_metadata340 #[solidity(rename_selector = "tokenURI")]341 fn token_uri(&self, token_id: U256) -> Result<String> {342 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;343344 match get_token_property(self, token_id_u32, &key::url()).as_deref() {345 Err(_) | Ok("") => (),346 Ok(url) => {347 return Ok(url.into());348 }349 };350351 let base_uri =352 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())353 .map(BoundedVec::into_inner)354 .map(String::from_utf8)355 .transpose()356 .map_err(|e| {357 Error::Revert(alloc::format!(358 "Can not convert value \"baseURI\" to string with error \"{}\"",359 e360 ))361 })?;362363 let base_uri = match base_uri.as_deref() {364 None | Some("") => {365 return Ok("".into());366 }367 Some(base_uri) => base_uri.into(),368 };369370 Ok(371 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {372 Err(_) | Ok("") => base_uri,373 Ok(suffix) => base_uri + suffix,374 },375 )376 }377}378379/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension380/// @dev See https://eips.ethereum.org/EIPS/eip-721381#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63, enum(derive(PreDispatch)), enum_attr(weight))]382impl<T: Config> NonfungibleHandle<T> {383 /// @notice Enumerate valid NFTs384 /// @param index A counter less than `totalSupply()`385 /// @return The token identifier for the `index`th NFT,386 /// (sort order not specified)387 fn token_by_index(&self, index: U256) -> U256 {388 index389 }390391 /// @dev Not implemented392 fn token_of_owner_by_index(&self, _owner: Address, _index: U256) -> Result<U256> {393 // TODO: Not implemetable394 Err("not implemented".into())395 }396397 /// @notice Count NFTs tracked by this contract398 /// @return A count of valid NFTs tracked by this contract, where each one of399 /// them has an assigned and queryable owner not equal to the zero address400 fn total_supply(&self) -> Result<U256> {401 self.consume_store_reads(1)?;402 Ok(<Pallet<T>>::total_supply(self).into())403 }404}405406/// @title ERC-721 Non-Fungible Token Standard407/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md408#[solidity_interface(name = ERC721, events(ERC721Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x80ac58cd)]409impl<T: Config> NonfungibleHandle<T> {410 /// @notice Count all NFTs assigned to an owner411 /// @dev NFTs assigned to the zero address are considered invalid, and this412 /// function throws for queries about the zero address.413 /// @param owner An address for whom to query the balance414 /// @return The number of NFTs owned by `owner`, possibly zero415 fn balance_of(&self, owner: Address) -> Result<U256> {416 self.consume_store_reads(1)?;417 let owner = T::CrossAccountId::from_eth(owner);418 let balance = <AccountBalance<T>>::get((self.id, owner));419 Ok(balance.into())420 }421 /// @notice Find the owner of an NFT422 /// @dev NFTs assigned to zero address are considered invalid, and queries423 /// about them do throw.424 /// @param tokenId The identifier for an NFT425 /// @return The address of the owner of the NFT426 fn owner_of(&self, token_id: U256) -> Result<Address> {427 self.consume_store_reads(1)?;428 let token: TokenId = token_id.try_into()?;429 Ok(*<TokenData<T>>::get((self.id, token))430 .ok_or("token not found")?431 .owner432 .as_eth())433 }434 /// @dev Not implemented435 #[solidity(rename_selector = "safeTransferFrom")]436 fn safe_transfer_from_with_data(437 &mut self,438 _from: Address,439 _to: Address,440 _token_id: U256,441 _data: Bytes,442 ) -> Result<()> {443 // TODO: Not implemetable444 Err("not implemented".into())445 }446 /// @dev Not implemented447 fn safe_transfer_from(&mut self, _from: Address, _to: Address, _token_id: U256) -> Result<()> {448 // TODO: Not implemetable449 Err("not implemented".into())450 }451452 /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE453 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE454 /// THEY MAY BE PERMANENTLY LOST455 /// @dev Throws unless `msg.sender` is the current owner or an authorized456 /// operator for this NFT. Throws if `from` is not the current owner. Throws457 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.458 /// @param from The current owner of the NFT459 /// @param to The new owner460 /// @param tokenId The NFT to transfer461 #[weight(<SelfWeightOf<T>>::transfer_from())]462 fn transfer_from(463 &mut self,464 caller: Caller,465 from: Address,466 to: Address,467 token_id: U256,468 ) -> Result<()> {469 let caller = T::CrossAccountId::from_eth(caller);470 let from = T::CrossAccountId::from_eth(from);471 let to = T::CrossAccountId::from_eth(to);472 let token = token_id.try_into()?;473 let budget = self474 .recorder475 .weight_calls_budget(<StructureWeight<T>>::find_parent());476477 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)478 .map_err(dispatch_to_evm::<T>)?;479 Ok(())480 }481482 /// @notice Set or reaffirm the approved address for an NFT483 /// @dev The zero address indicates there is no approved address.484 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized485 /// operator of the current owner.486 /// @param approved The new approved NFT controller487 /// @param tokenId The NFT to approve488 #[weight(<SelfWeightOf<T>>::approve())]489 fn approve(&mut self, caller: Caller, approved: Address, token_id: U256) -> Result<()> {490 let caller = T::CrossAccountId::from_eth(caller);491 let approved = T::CrossAccountId::from_eth(approved);492 let token = token_id.try_into()?;493494 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))495 .map_err(dispatch_to_evm::<T>)?;496 Ok(())497 }498499 /// @notice Sets or unsets the approval of a given operator.500 /// The `operator` is allowed to transfer all tokens of the `caller` on their behalf.501 /// @param operator Operator502 /// @param approved Should operator status be granted or revoked?503 #[weight(<SelfWeightOf<T>>::set_allowance_for_all())]504 fn set_approval_for_all(505 &mut self,506 caller: Caller,507 operator: Address,508 approved: bool,509 ) -> Result<()> {510 let caller = T::CrossAccountId::from_eth(caller);511 let operator = T::CrossAccountId::from_eth(operator);512513 <Pallet<T>>::set_allowance_for_all(self, &caller, &operator, approved)514 .map_err(dispatch_to_evm::<T>)?;515 Ok(())516 }517518 /// @notice Get the approved address for a single NFT519 /// @dev Throws if `_tokenId` is not a valid NFT520 /// @param _tokenId The NFT to find the approved address for521 /// @return The approved address for this NFT, or the zero address if there is none522 fn get_approved(&self, token_id: U256) -> Result<Address> {523 let token = token_id.try_into()?;524 let operator = <Pallet<T>>::get_allowance(self, token).map_err(dispatch_to_evm::<T>)?;525 Ok(if let Some(operator) = operator {526 *operator.as_eth()527 } else {528 Address::zero()529 })530 }531532 /// @notice Tells whether the given `owner` approves the `operator`.533 #[weight(<SelfWeightOf<T>>::allowance_for_all())]534 fn is_approved_for_all(&self, owner: Address, operator: Address) -> Result<bool> {535 let owner = T::CrossAccountId::from_eth(owner);536 let operator = T::CrossAccountId::from_eth(operator);537538 Ok(<Pallet<T>>::allowance_for_all(self, &owner, &operator))539 }540}541542/// @title ERC721 Token that can be irreversibly burned (destroyed).543#[solidity_interface(name = ERC721Burnable, enum(derive(PreDispatch)), enum_attr(weight))]544impl<T: Config> NonfungibleHandle<T> {545 /// @notice Burns a specific ERC721 token.546 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized547 /// operator of the current owner.548 /// @param tokenId The NFT to approve549 #[weight(<SelfWeightOf<T>>::burn_item())]550 fn burn(&mut self, caller: Caller, token_id: U256) -> Result<()> {551 let caller = T::CrossAccountId::from_eth(caller);552 let token = token_id.try_into()?;553554 <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;555 Ok(())556 }557}558559/// @title ERC721 minting logic.560#[solidity_interface(name = ERC721UniqueMintable, enum(derive(PreDispatch)), enum_attr(weight))]561impl<T: Config> NonfungibleHandle<T> {562 /// @notice Function to mint a token.563 /// @param to The new owner564 /// @return uint256 The id of the newly minted token565 #[weight(<SelfWeightOf<T>>::create_item())]566 fn mint(&mut self, caller: Caller, to: Address) -> Result<U256> {567 let token_id: U256 = <TokensMinted<T>>::get(self.id)568 .checked_add(1)569 .ok_or("item id overflow")?570 .into();571 self.mint_check_id(caller, to, token_id)?;572 Ok(token_id)573 }574575 /// @notice Function to mint a token.576 /// @dev `tokenId` should be obtained with `nextTokenId` method,577 /// unlike standard, you can't specify it manually578 /// @param to The new owner579 /// @param tokenId ID of the minted NFT580 #[solidity(hide, rename_selector = "mint")]581 #[weight(<SelfWeightOf<T>>::create_item())]582 fn mint_check_id(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<bool> {583 let caller = T::CrossAccountId::from_eth(caller);584 let to = T::CrossAccountId::from_eth(to);585 let token_id: u32 = token_id.try_into()?;586 let budget = self587 .recorder588 .weight_calls_budget(<StructureWeight<T>>::find_parent());589590 if <TokensMinted<T>>::get(self.id)591 .checked_add(1)592 .ok_or("item id overflow")?593 != token_id594 {595 return Err("item id should be next".into());596 }597598 <Pallet<T>>::create_item(599 self,600 &caller,601 CreateItemData::<T> {602 properties: BoundedVec::default(),603 owner: to,604 },605 &budget,606 )607 .map_err(dispatch_to_evm::<T>)?;608609 Ok(true)610 }611612 /// @notice Function to mint token with the given tokenUri.613 /// @param to The new owner614 /// @param tokenUri Token URI that would be stored in the NFT properties615 /// @return uint256 The id of the newly minted token616 #[solidity(rename_selector = "mintWithTokenURI")]617 #[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]618 fn mint_with_token_uri(619 &mut self,620 caller: Caller,621 to: Address,622 token_uri: String,623 ) -> Result<U256> {624 let token_id: U256 = <TokensMinted<T>>::get(self.id)625 .checked_add(1)626 .ok_or("item id overflow")?627 .into();628 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;629 Ok(token_id)630 }631632 /// @notice Function to mint token with the given tokenUri.633 /// @dev `tokenId` should be obtained with `nextTokenId` method,634 /// unlike standard, you can't specify it manually635 /// @param to The new owner636 /// @param tokenId ID of the minted NFT637 /// @param tokenUri Token URI that would be stored in the NFT properties638 #[solidity(hide, rename_selector = "mintWithTokenURI")]639 #[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(1))]640 fn mint_with_token_uri_check_id(641 &mut self,642 caller: Caller,643 to: Address,644 token_id: U256,645 token_uri: String,646 ) -> Result<bool> {647 let key = key::url();648 let permission = get_token_permission::<T>(self.id, &key)?;649 if !permission.collection_admin {650 return Err("Operation is not allowed".into());651 }652653 let caller = T::CrossAccountId::from_eth(caller);654 let to = T::CrossAccountId::from_eth(to);655 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;656 let budget = self657 .recorder658 .weight_calls_budget(<StructureWeight<T>>::find_parent());659660 if <TokensMinted<T>>::get(self.id)661 .checked_add(1)662 .ok_or("item id overflow")?663 != token_id664 {665 return Err("item id should be next".into());666 }667668 let mut properties = CollectionPropertiesVec::default();669 properties670 .try_push(Property {671 key,672 value: token_uri673 .into_bytes()674 .try_into()675 .map_err(|_| "token uri is too long")?,676 })677 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;678679 <Pallet<T>>::create_item(680 self,681 &caller,682 CreateItemData::<T> {683 properties,684 owner: to,685 },686 &budget,687 )688 .map_err(dispatch_to_evm::<T>)?;689 Ok(true)690 }691}692693fn get_token_property<T: Config>(694 collection: &CollectionHandle<T>,695 token_id: u32,696 key: &up_data_structs::PropertyKey,697) -> Result<String> {698 collection.consume_store_reads(1)?;699 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))700 .map_err(|_| Error::Revert("Token properties not found".into()))?;701 if let Some(property) = properties.get(key) {702 return Ok(String::from_utf8_lossy(property).into());703 }704705 Err("Property tokenURI not found".into())706}707708fn get_token_permission<T: Config>(709 collection_id: CollectionId,710 key: &PropertyKey,711) -> Result<PropertyPermission> {712 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)713 .map_err(|_| Error::Revert("No permissions for collection".into()))?;714 let a = token_property_permissions715 .get(key)716 .map(Clone::clone)717 .ok_or_else(|| {718 let key = String::from_utf8(key.clone().into_inner()).unwrap_or_default();719 Error::Revert(alloc::format!("No permission for key {}", key))720 })?;721 Ok(a)722}723724/// @title Unique extensions for ERC721.725#[solidity_interface(name = ERC721UniqueExtensions, enum(derive(PreDispatch)), enum_attr(weight))]726impl<T: Config> NonfungibleHandle<T>727where728 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,729{730 /// @notice A descriptive name for a collection of NFTs in this contract731 fn name(&self) -> String {732 decode_utf16(self.name.iter().copied())733 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))734 .collect::<String>()735 }736737 /// @notice An abbreviated name for NFTs in this contract738 fn symbol(&self) -> String {739 String::from_utf8_lossy(&self.token_prefix).into()740 }741742 /// @notice A description for the collection.743 fn description(&self) -> String {744 decode_utf16(self.description.iter().copied())745 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))746 .collect::<String>()747 }748749 /// Returns the owner (in cross format) of the token.750 ///751 /// @param tokenId Id for the token.752 fn cross_owner_of(&self, token_id: U256) -> Result<eth::CrossAddress> {753 Self::token_owner(&self, token_id.try_into()?)754 .map(|o| eth::CrossAddress::from_sub_cross_account::<T>(&o))755 .map_err(|_| Error::Revert("token not found".into()))756 }757758 /// Returns the token properties.759 ///760 /// @param tokenId Id for the token.761 /// @param keys Properties keys. Empty keys for all propertyes.762 /// @return Vector of properties key/value pairs.763 fn properties(&self, token_id: U256, keys: Vec<String>) -> Result<Vec<eth::Property>> {764 let keys = keys765 .into_iter()766 .map(|key| {767 <Vec<u8>>::from(key)768 .try_into()769 .map_err(|_| Error::Revert("key too large".into()))770 })771 .collect::<Result<Vec<_>>>()?;772773 <Self as CommonCollectionOperations<T>>::token_properties(774 &self,775 token_id.try_into()?,776 if keys.is_empty() { None } else { Some(keys) },777 )778 .into_iter()779 .map(eth::Property::try_from)780 .collect::<Result<Vec<_>>>()781 }782783 /// @notice Set or reaffirm the approved address for an NFT784 /// @dev The zero address indicates there is no approved address.785 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized786 /// operator of the current owner.787 /// @param approved The new substrate address approved NFT controller788 /// @param tokenId The NFT to approve789 #[weight(<SelfWeightOf<T>>::approve())]790 fn approve_cross(791 &mut self,792 caller: Caller,793 approved: eth::CrossAddress,794 token_id: U256,795 ) -> Result<()> {796 let caller = T::CrossAccountId::from_eth(caller);797 let approved = approved.into_sub_cross_account::<T>()?;798 let token = token_id.try_into()?;799800 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))801 .map_err(dispatch_to_evm::<T>)?;802 Ok(())803 }804805 /// @notice Transfer ownership of an NFT806 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`807 /// is the zero address. Throws if `tokenId` is not a valid NFT.808 /// @param to The new owner809 /// @param tokenId The NFT to transfer810 #[weight(<SelfWeightOf<T>>::transfer())]811 fn transfer(&mut self, caller: Caller, to: Address, token_id: U256) -> Result<()> {812 let caller = T::CrossAccountId::from_eth(caller);813 let to = T::CrossAccountId::from_eth(to);814 let token = token_id.try_into()?;815 let budget = self816 .recorder817 .weight_calls_budget(<StructureWeight<T>>::find_parent());818819 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;820 Ok(())821 }822823 /// @notice Transfer ownership of an NFT824 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`825 /// is the zero address. Throws if `tokenId` is not a valid NFT.826 /// @param to The new owner827 /// @param tokenId The NFT to transfer828 #[weight(<SelfWeightOf<T>>::transfer())]829 fn transfer_cross(830 &mut self,831 caller: Caller,832 to: eth::CrossAddress,833 token_id: U256,834 ) -> Result<()> {835 let caller = T::CrossAccountId::from_eth(caller);836 let to = to.into_sub_cross_account::<T>()?;837 let token = token_id.try_into()?;838 let budget = self839 .recorder840 .weight_calls_budget(<StructureWeight<T>>::find_parent());841842 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;843 Ok(())844 }845846 /// @notice Transfer ownership of an NFT from cross account address to cross account address847 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`848 /// is the zero address. Throws if `tokenId` is not a valid NFT.849 /// @param from Cross acccount address of current owner850 /// @param to Cross acccount address of new owner851 /// @param tokenId The NFT to transfer852 #[weight(<SelfWeightOf<T>>::transfer())]853 fn transfer_from_cross(854 &mut self,855 caller: Caller,856 from: eth::CrossAddress,857 to: eth::CrossAddress,858 token_id: U256,859 ) -> Result<()> {860 let caller = T::CrossAccountId::from_eth(caller);861 let from = from.into_sub_cross_account::<T>()?;862 let to = to.into_sub_cross_account::<T>()?;863 let token_id = token_id.try_into()?;864 let budget = self865 .recorder866 .weight_calls_budget(<StructureWeight<T>>::find_parent());867 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)868 .map_err(dispatch_to_evm::<T>)?;869 Ok(())870 }871872 /// @notice Burns a specific ERC721 token.873 /// @dev Throws unless `msg.sender` is the current owner or an authorized874 /// operator for this NFT. Throws if `from` is not the current owner. Throws875 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.876 /// @param from The current owner of the NFT877 /// @param tokenId The NFT to transfer878 #[solidity(hide)]879 #[weight(<SelfWeightOf<T>>::burn_from())]880 fn burn_from(&mut self, caller: Caller, from: Address, token_id: U256) -> Result<()> {881 let caller = T::CrossAccountId::from_eth(caller);882 let from = T::CrossAccountId::from_eth(from);883 let token = token_id.try_into()?;884 let budget = self885 .recorder886 .weight_calls_budget(<StructureWeight<T>>::find_parent());887888 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)889 .map_err(dispatch_to_evm::<T>)?;890 Ok(())891 }892893 /// @notice Burns a specific ERC721 token.894 /// @dev Throws unless `msg.sender` is the current owner or an authorized895 /// operator for this NFT. Throws if `from` is not the current owner. Throws896 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.897 /// @param from The current owner of the NFT898 /// @param tokenId The NFT to transfer899 #[weight(<SelfWeightOf<T>>::burn_from())]900 fn burn_from_cross(901 &mut self,902 caller: Caller,903 from: eth::CrossAddress,904 token_id: U256,905 ) -> Result<()> {906 let caller = T::CrossAccountId::from_eth(caller);907 let from = from.into_sub_cross_account::<T>()?;908 let token = token_id.try_into()?;909 let budget = self910 .recorder911 .weight_calls_budget(<StructureWeight<T>>::find_parent());912913 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)914 .map_err(dispatch_to_evm::<T>)?;915 Ok(())916 }917918 /// @notice Returns next free NFT ID.919 fn next_token_id(&self) -> Result<U256> {920 self.consume_store_reads(1)?;921 Ok(<TokensMinted<T>>::get(self.id)922 .checked_add(1)923 .ok_or("item id overflow")?924 .into())925 }926927 /// @notice Function to mint multiple tokens.928 /// @dev `tokenIds` should be an array of consecutive numbers and first number929 /// should be obtained with `nextTokenId` method930 /// @param to The new owner931 /// @param tokenIds IDs of the minted NFTs932 #[solidity(hide)]933 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]934 fn mint_bulk(&mut self, caller: Caller, to: Address, token_ids: Vec<U256>) -> Result<bool> {935 let caller = T::CrossAccountId::from_eth(caller);936 let to = T::CrossAccountId::from_eth(to);937 let mut expected_index = <TokensMinted<T>>::get(self.id)938 .checked_add(1)939 .ok_or("item id overflow")?;940 let budget = self941 .recorder942 .weight_calls_budget(<StructureWeight<T>>::find_parent());943944 let total_tokens = token_ids.len();945 for id in token_ids.into_iter() {946 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;947 if id != expected_index {948 return Err("item id should be next".into());949 }950 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;951 }952 let data = (0..total_tokens)953 .map(|_| CreateItemData::<T> {954 properties: BoundedVec::default(),955 owner: to.clone(),956 })957 .collect();958959 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)960 .map_err(dispatch_to_evm::<T>)?;961 Ok(true)962 }963964 /// @notice Function to mint multiple tokens with the given tokenUris.965 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive966 /// numbers and first number should be obtained with `nextTokenId` method967 /// @param to The new owner968 /// @param tokens array of pairs of token ID and token URI for minted tokens969 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]970 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32) + <SelfWeightOf<T>>::set_token_properties(tokens.len() as u32))]971 fn mint_bulk_with_token_uri(972 &mut self,973 caller: Caller,974 to: Address,975 tokens: Vec<TokenUri>,976 ) -> Result<bool> {977 let key = key::url();978 let caller = T::CrossAccountId::from_eth(caller);979 let to = T::CrossAccountId::from_eth(to);980 let mut expected_index = <TokensMinted<T>>::get(self.id)981 .checked_add(1)982 .ok_or("item id overflow")?;983 let budget = self984 .recorder985 .weight_calls_budget(<StructureWeight<T>>::find_parent());986987 let mut data = Vec::with_capacity(tokens.len());988 for TokenUri { id, uri } in tokens {989 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;990 if id != expected_index {991 return Err("item id should be next".into());992 }993 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;994995 let mut properties = CollectionPropertiesVec::default();996 properties997 .try_push(Property {998 key: key.clone(),999 value: uri1000 .into_bytes()1001 .try_into()1002 .map_err(|_| "token uri is too long")?,1003 })1004 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;10051006 data.push(CreateItemData::<T> {1007 properties,1008 owner: to.clone(),1009 });1010 }10111012 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)1013 .map_err(dispatch_to_evm::<T>)?;1014 Ok(true)1015 }10161017 /// @notice Function to mint a token.1018 /// @param to The new owner crossAccountId1019 /// @param properties Properties of minted token1020 /// @return uint256 The id of the newly minted token1021 #[weight(<SelfWeightOf<T>>::create_item() + <SelfWeightOf<T>>::set_token_properties(properties.len() as u32))]1022 fn mint_cross(1023 &mut self,1024 caller: Caller,1025 to: eth::CrossAddress,1026 properties: Vec<eth::Property>,1027 ) -> Result<U256> {1028 let token_id = <TokensMinted<T>>::get(self.id)1029 .checked_add(1)1030 .ok_or("item id overflow")?;10311032 let to = to.into_sub_cross_account::<T>()?;10331034 let properties = properties1035 .into_iter()1036 .map(eth::Property::try_into)1037 .collect::<Result<Vec<_>>>()?1038 .try_into()1039 .map_err(|_| Error::Revert(alloc::format!("too many properties")))?;10401041 let caller = T::CrossAccountId::from_eth(caller);10421043 let budget = self1044 .recorder1045 .weight_calls_budget(<StructureWeight<T>>::find_parent());10461047 <Pallet<T>>::create_item(1048 self,1049 &caller,1050 CreateItemData::<T> {1051 properties,1052 owner: to,1053 },1054 &budget,1055 )1056 .map_err(dispatch_to_evm::<T>)?;10571058 Ok(token_id.into())1059 }10601061 /// @notice Returns collection helper contract address1062 fn collection_helper_address(&self) -> Address {1063 T::ContractAddress::get()1064 }1065}10661067#[solidity_interface(1068 name = UniqueNFT,1069 is(1070 ERC721,1071 ERC721Enumerable,1072 ERC721UniqueExtensions,1073 ERC721UniqueMintable,1074 ERC721Burnable,1075 ERC721Metadata(if(this.flags.erc721metadata)),1076 Collection(via(common_mut returns CollectionHandle<T>)),1077 TokenProperties,1078 ),1079 enum(derive(PreDispatch)),1080)]1081impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}10821083// Not a tests, but code generators1084generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);1085generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);10861087impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>1088where1089 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,1090{1091 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");10921093 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {1094 call::<T, UniqueNFTCall<T>, _, _>(handle, self)1095 }1096}