difftreelog
Added `set_properties` method for `TokenProperties` interface.
in: master
19 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6358,7 +6358,7 @@
[[package]]
name = "pallet-nonfungible"
-version = "0.1.5"
+version = "0.1.6"
dependencies = [
"ethereum",
"evm-coder",
@@ -6480,7 +6480,7 @@
[[package]]
name = "pallet-refungible"
-version = "0.2.4"
+version = "0.2.5"
dependencies = [
"derivative",
"ethereum",
pallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterbothbinary blob — no preview
pallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth--- a/pallets/fungible/src/stubs/UniqueFungible.sol
+++ b/pallets/fungible/src/stubs/UniqueFungible.sol
@@ -385,7 +385,7 @@
/// Get collection owner.
///
- /// @return Tuble with sponsor address and his substrate mirror.
+ /// @return Tuple with sponsor address and his substrate mirror.
/// If address is canonical then substrate mirror is zero and vice versa.
/// @dev EVM selector for this function is: 0xdf727d3b,
/// or in textual repr: collectionOwner()
pallets/nonfungible/CHANGELOG.mddiffbeforeafterboth--- a/pallets/nonfungible/CHANGELOG.md
+++ b/pallets/nonfungible/CHANGELOG.md
@@ -2,12 +2,20 @@
All notable changes to this project will be documented in this file.
+<!-- bureaucrate goes here -->
+
+## [v0.1.6] - 2022-20-10
+
+### Change
+
+- Added `set_properties` method for `TokenProperties` interface.
+
## [v0.1.5] - 2022-08-24
### Change
- - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
-<!-- bureaucrate goes here -->
+- Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
+
## [v0.1.4] 2022-08-16
### Other changes
@@ -28,7 +36,9 @@
- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b
## [0.1.2] - 2022-07-25
+
### Changed
+
- New `token_uri` retrieval logic:
If the collection has a `url` property and it is not empty, it is returned.
@@ -39,8 +49,9 @@
otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).
## [0.1.1] - 2022-07-14
+
### Added
- Implementation of RPC method `token_owners`.
- For reasons of compatibility with this pallet, returns only one owner if token exists.
- This was an internal request to improve the web interface and support fractionalization event.
+ For reasons of compatibility with this pallet, returns only one owner if token exists.
+ This was an internal request to improve the web interface and support fractionalization event.
pallets/nonfungible/Cargo.tomldiffbeforeafterboth--- a/pallets/nonfungible/Cargo.toml
+++ b/pallets/nonfungible/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pallet-nonfungible"
-version = "0.1.5"
+version = "0.1.6"
license = "GPLv3"
edition = "2021"
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 Delete token property value.118 /// @dev Throws error if `msg.sender` has no permission to edit the property.119 /// @param tokenId ID of the token.120 /// @param key Property key.121 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {122 let caller = T::CrossAccountId::from_eth(caller);123 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;124 let key = <Vec<u8>>::from(key)125 .try_into()126 .map_err(|_| "key too long")?;127128 let nesting_budget = self129 .recorder130 .weight_calls_budget(<StructureWeight<T>>::find_parent());131132 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)133 .map_err(dispatch_to_evm::<T>)134 }135136 /// @notice Get token property value.137 /// @dev Throws error if key not found138 /// @param tokenId ID of the token.139 /// @param key Property key.140 /// @return Property value bytes141 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {142 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;143 let key = <Vec<u8>>::from(key)144 .try_into()145 .map_err(|_| "key too long")?;146147 let props = <TokenProperties<T>>::get((self.id, token_id));148 let prop = props.get(&key).ok_or("key not found")?;149150 Ok(prop.to_vec().into())151 }152}153154#[derive(ToLog)]155pub enum ERC721Events {156 /// @dev This emits when ownership of any NFT changes by any mechanism.157 /// This event emits when NFTs are created (`from` == 0) and destroyed158 /// (`to` == 0). Exception: during contract creation, any number of NFTs159 /// may be created and assigned without emitting Transfer. At the time of160 /// any transfer, the approved address for that NFT (if any) is reset to none.161 Transfer {162 #[indexed]163 from: address,164 #[indexed]165 to: address,166 #[indexed]167 token_id: uint256,168 },169 /// @dev This emits when the approved address for an NFT is changed or170 /// reaffirmed. The zero address indicates there is no approved address.171 /// When a Transfer event emits, this also indicates that the approved172 /// address for that NFT (if any) is reset to none.173 Approval {174 #[indexed]175 owner: address,176 #[indexed]177 approved: address,178 #[indexed]179 token_id: uint256,180 },181 /// @dev This emits when an operator is enabled or disabled for an owner.182 /// The operator can manage all NFTs of the owner.183 #[allow(dead_code)]184 ApprovalForAll {185 #[indexed]186 owner: address,187 #[indexed]188 operator: address,189 approved: bool,190 },191}192193#[derive(ToLog)]194pub enum ERC721UniqueMintableEvents {195 #[allow(dead_code)]196 MintingFinished {},197}198199/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension200/// @dev See https://eips.ethereum.org/EIPS/eip-721201#[solidity_interface(name = ERC721Metadata, expect_selector = 0x5b5e139f)]202impl<T: Config> NonfungibleHandle<T>203where204 T::AccountId: From<[u8; 32]>,205{206 /// @notice A descriptive name for a collection of NFTs in this contract207 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`208 #[solidity(hide, rename_selector = "name")]209 fn name_proxy(&self) -> Result<string> {210 self.name()211 }212213 /// @notice An abbreviated name for NFTs in this contract214 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`215 #[solidity(hide, rename_selector = "symbol")]216 fn symbol_proxy(&self) -> Result<string> {217 self.symbol()218 }219220 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.221 ///222 /// @dev If the token has a `url` property and it is not empty, it is returned.223 /// 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`.224 /// If the collection property `baseURI` is empty or absent, return "" (empty string)225 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix226 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).227 ///228 /// @return token's const_metadata229 #[solidity(rename_selector = "tokenURI")]230 fn token_uri(&self, token_id: uint256) -> Result<string> {231 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;232233 match get_token_property(self, token_id_u32, &key::url()).as_deref() {234 Err(_) | Ok("") => (),235 Ok(url) => {236 return Ok(url.into());237 }238 };239240 let base_uri =241 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())242 .map(BoundedVec::into_inner)243 .map(string::from_utf8)244 .transpose()245 .map_err(|e| {246 Error::Revert(alloc::format!(247 "Can not convert value \"baseURI\" to string with error \"{}\"",248 e249 ))250 })?;251252 let base_uri = match base_uri.as_deref() {253 None | Some("") => {254 return Ok("".into());255 }256 Some(base_uri) => base_uri.into(),257 };258259 Ok(260 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {261 Err(_) | Ok("") => base_uri,262 Ok(suffix) => base_uri + suffix,263 },264 )265 }266}267268/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension269/// @dev See https://eips.ethereum.org/EIPS/eip-721270#[solidity_interface(name = ERC721Enumerable, expect_selector = 0x780e9d63)]271impl<T: Config> NonfungibleHandle<T> {272 /// @notice Enumerate valid NFTs273 /// @param index A counter less than `totalSupply()`274 /// @return The token identifier for the `index`th NFT,275 /// (sort order not specified)276 fn token_by_index(&self, index: uint256) -> Result<uint256> {277 Ok(index)278 }279280 /// @dev Not implemented281 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {282 // TODO: Not implemetable283 Err("not implemented".into())284 }285286 /// @notice Count NFTs tracked by this contract287 /// @return A count of valid NFTs tracked by this contract, where each one of288 /// them has an assigned and queryable owner not equal to the zero address289 fn total_supply(&self) -> Result<uint256> {290 self.consume_store_reads(1)?;291 Ok(<Pallet<T>>::total_supply(self).into())292 }293}294295/// @title ERC-721 Non-Fungible Token Standard296/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md297#[solidity_interface(name = ERC721, events(ERC721Events), expect_selector = 0x80ac58cd)]298impl<T: Config> NonfungibleHandle<T> {299 /// @notice Count all NFTs assigned to an owner300 /// @dev NFTs assigned to the zero address are considered invalid, and this301 /// function throws for queries about the zero address.302 /// @param owner An address for whom to query the balance303 /// @return The number of NFTs owned by `owner`, possibly zero304 fn balance_of(&self, owner: address) -> Result<uint256> {305 self.consume_store_reads(1)?;306 let owner = T::CrossAccountId::from_eth(owner);307 let balance = <AccountBalance<T>>::get((self.id, owner));308 Ok(balance.into())309 }310 /// @notice Find the owner of an NFT311 /// @dev NFTs assigned to zero address are considered invalid, and queries312 /// about them do throw.313 /// @param tokenId The identifier for an NFT314 /// @return The address of the owner of the NFT315 fn owner_of(&self, token_id: uint256) -> Result<address> {316 self.consume_store_reads(1)?;317 let token: TokenId = token_id.try_into()?;318 Ok(*<TokenData<T>>::get((self.id, token))319 .ok_or("token not found")?320 .owner321 .as_eth())322 }323 /// @dev Not implemented324 #[solidity(rename_selector = "safeTransferFrom")]325 fn safe_transfer_from_with_data(326 &mut self,327 _from: address,328 _to: address,329 _token_id: uint256,330 _data: bytes,331 ) -> Result<void> {332 // TODO: Not implemetable333 Err("not implemented".into())334 }335 /// @dev Not implemented336 fn safe_transfer_from(337 &mut self,338 _from: address,339 _to: address,340 _token_id: uint256,341 ) -> Result<void> {342 // TODO: Not implemetable343 Err("not implemented".into())344 }345346 /// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE347 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE348 /// THEY MAY BE PERMANENTLY LOST349 /// @dev Throws unless `msg.sender` is the current owner or an authorized350 /// operator for this NFT. Throws if `from` is not the current owner. Throws351 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.352 /// @param from The current owner of the NFT353 /// @param to The new owner354 /// @param tokenId The NFT to transfer355 #[weight(<SelfWeightOf<T>>::transfer_from())]356 fn transfer_from(357 &mut self,358 caller: caller,359 from: address,360 to: address,361 token_id: uint256,362 ) -> Result<void> {363 let caller = T::CrossAccountId::from_eth(caller);364 let from = T::CrossAccountId::from_eth(from);365 let to = T::CrossAccountId::from_eth(to);366 let token = token_id.try_into()?;367 let budget = self368 .recorder369 .weight_calls_budget(<StructureWeight<T>>::find_parent());370371 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, &budget)372 .map_err(dispatch_to_evm::<T>)?;373 Ok(())374 }375376 /// @notice Set or reaffirm the approved address for an NFT377 /// @dev The zero address indicates there is no approved address.378 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized379 /// operator of the current owner.380 /// @param approved The new approved NFT controller381 /// @param tokenId The NFT to approve382 #[weight(<SelfWeightOf<T>>::approve())]383 fn approve(&mut self, caller: caller, approved: address, token_id: uint256) -> Result<void> {384 let caller = T::CrossAccountId::from_eth(caller);385 let approved = T::CrossAccountId::from_eth(approved);386 let token = token_id.try_into()?;387388 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))389 .map_err(dispatch_to_evm::<T>)?;390 Ok(())391 }392393 /// @dev Not implemented394 fn set_approval_for_all(395 &mut self,396 _caller: caller,397 _operator: address,398 _approved: bool,399 ) -> Result<void> {400 // TODO: Not implemetable401 Err("not implemented".into())402 }403404 /// @dev Not implemented405 fn get_approved(&self, _token_id: uint256) -> Result<address> {406 // TODO: Not implemetable407 Err("not implemented".into())408 }409410 /// @dev Not implemented411 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {412 // TODO: Not implemetable413 Err("not implemented".into())414 }415}416417/// @title ERC721 Token that can be irreversibly burned (destroyed).418#[solidity_interface(name = ERC721Burnable)]419impl<T: Config> NonfungibleHandle<T> {420 /// @notice Burns a specific ERC721 token.421 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized422 /// operator of the current owner.423 /// @param tokenId The NFT to approve424 #[weight(<SelfWeightOf<T>>::burn_item())]425 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {426 let caller = T::CrossAccountId::from_eth(caller);427 let token = token_id.try_into()?;428429 <Pallet<T>>::burn(self, &caller, token).map_err(dispatch_to_evm::<T>)?;430 Ok(())431 }432}433434/// @title ERC721 minting logic.435#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]436impl<T: Config> NonfungibleHandle<T> {437 fn minting_finished(&self) -> Result<bool> {438 Ok(false)439 }440441 /// @notice Function to mint token.442 /// @param to The new owner443 /// @return uint256 The id of the newly minted token444 #[weight(<SelfWeightOf<T>>::create_item())]445 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {446 let token_id: uint256 = <TokensMinted<T>>::get(self.id)447 .checked_add(1)448 .ok_or("item id overflow")?449 .into();450 self.mint_check_id(caller, to, token_id)?;451 Ok(token_id)452 }453454 /// @notice Function to mint token.455 /// @dev `tokenId` should be obtained with `nextTokenId` method,456 /// unlike standard, you can't specify it manually457 /// @param to The new owner458 /// @param tokenId ID of the minted NFT459 #[solidity(hide, rename_selector = "mint")]460 #[weight(<SelfWeightOf<T>>::create_item())]461 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {462 let caller = T::CrossAccountId::from_eth(caller);463 let to = T::CrossAccountId::from_eth(to);464 let token_id: u32 = token_id.try_into()?;465 let budget = self466 .recorder467 .weight_calls_budget(<StructureWeight<T>>::find_parent());468469 if <TokensMinted<T>>::get(self.id)470 .checked_add(1)471 .ok_or("item id overflow")?472 != token_id473 {474 return Err("item id should be next".into());475 }476477 <Pallet<T>>::create_item(478 self,479 &caller,480 CreateItemData::<T> {481 properties: BoundedVec::default(),482 owner: to,483 },484 &budget,485 )486 .map_err(dispatch_to_evm::<T>)?;487488 Ok(true)489 }490491 /// @notice Function to mint token with the given tokenUri.492 /// @param to The new owner493 /// @param tokenUri Token URI that would be stored in the NFT properties494 /// @return uint256 The id of the newly minted token495 #[solidity(rename_selector = "mintWithTokenURI")]496 #[weight(<SelfWeightOf<T>>::create_item())]497 fn mint_with_token_uri(498 &mut self,499 caller: caller,500 to: address,501 token_uri: string,502 ) -> Result<uint256> {503 let token_id: uint256 = <TokensMinted<T>>::get(self.id)504 .checked_add(1)505 .ok_or("item id overflow")?506 .into();507 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;508 Ok(token_id)509 }510511 /// @notice Function to mint token with the given tokenUri.512 /// @dev `tokenId` should be obtained with `nextTokenId` method,513 /// unlike standard, you can't specify it manually514 /// @param to The new owner515 /// @param tokenId ID of the minted NFT516 /// @param tokenUri Token URI that would be stored in the NFT properties517 #[solidity(hide, rename_selector = "mintWithTokenURI")]518 #[weight(<SelfWeightOf<T>>::create_item())]519 fn mint_with_token_uri_check_id(520 &mut self,521 caller: caller,522 to: address,523 token_id: uint256,524 token_uri: string,525 ) -> Result<bool> {526 let key = key::url();527 let permission = get_token_permission::<T>(self.id, &key)?;528 if !permission.collection_admin {529 return Err("Operation is not allowed".into());530 }531532 let caller = T::CrossAccountId::from_eth(caller);533 let to = T::CrossAccountId::from_eth(to);534 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;535 let budget = self536 .recorder537 .weight_calls_budget(<StructureWeight<T>>::find_parent());538539 if <TokensMinted<T>>::get(self.id)540 .checked_add(1)541 .ok_or("item id overflow")?542 != token_id543 {544 return Err("item id should be next".into());545 }546547 let mut properties = CollectionPropertiesVec::default();548 properties549 .try_push(Property {550 key,551 value: token_uri552 .into_bytes()553 .try_into()554 .map_err(|_| "token uri is too long")?,555 })556 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;557558 <Pallet<T>>::create_item(559 self,560 &caller,561 CreateItemData::<T> {562 properties,563 owner: to,564 },565 &budget,566 )567 .map_err(dispatch_to_evm::<T>)?;568 Ok(true)569 }570571 /// @dev Not implemented572 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {573 Err("not implementable".into())574 }575}576577fn get_token_property<T: Config>(578 collection: &CollectionHandle<T>,579 token_id: u32,580 key: &up_data_structs::PropertyKey,581) -> Result<string> {582 collection.consume_store_reads(1)?;583 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))584 .map_err(|_| Error::Revert("Token properties not found".into()))?;585 if let Some(property) = properties.get(key) {586 return Ok(string::from_utf8_lossy(property).into());587 }588589 Err("Property tokenURI not found".into())590}591592fn get_token_permission<T: Config>(593 collection_id: CollectionId,594 key: &PropertyKey,595) -> Result<PropertyPermission> {596 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)597 .map_err(|_| Error::Revert("No permissions for collection".into()))?;598 let a = token_property_permissions599 .get(key)600 .map(Clone::clone)601 .ok_or_else(|| {602 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();603 Error::Revert(alloc::format!("No permission for key {}", key))604 })?;605 Ok(a)606}607608/// @title Unique extensions for ERC721.609#[solidity_interface(name = ERC721UniqueExtensions)]610impl<T: Config> NonfungibleHandle<T>611where612 T::AccountId: From<[u8; 32]>,613{614 /// @notice A descriptive name for a collection of NFTs in this contract615 fn name(&self) -> Result<string> {616 Ok(decode_utf16(self.name.iter().copied())617 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))618 .collect::<string>())619 }620621 /// @notice An abbreviated name for NFTs in this contract622 fn symbol(&self) -> Result<string> {623 Ok(string::from_utf8_lossy(&self.token_prefix).into())624 }625626 /// @notice Set or reaffirm the approved address for an NFT627 /// @dev The zero address indicates there is no approved address.628 /// @dev Throws unless `msg.sender` is the current NFT owner, or an authorized629 /// operator of the current owner.630 /// @param approved The new substrate address approved NFT controller631 /// @param tokenId The NFT to approve632 #[weight(<SelfWeightOf<T>>::approve())]633 fn approve_cross(634 &mut self,635 caller: caller,636 approved: (address, uint256),637 token_id: uint256,638 ) -> Result<void> {639 let caller = T::CrossAccountId::from_eth(caller);640 let approved = convert_tuple_to_cross_account::<T>(approved)?;641 let token = token_id.try_into()?;642643 <Pallet<T>>::set_allowance(self, &caller, token, Some(&approved))644 .map_err(dispatch_to_evm::<T>)?;645 Ok(())646 }647648 /// @notice Transfer ownership of an NFT649 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`650 /// is the zero address. Throws if `tokenId` is not a valid NFT.651 /// @param to The new owner652 /// @param tokenId The NFT to transfer653 #[weight(<SelfWeightOf<T>>::transfer())]654 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {655 let caller = T::CrossAccountId::from_eth(caller);656 let to = T::CrossAccountId::from_eth(to);657 let token = token_id.try_into()?;658 let budget = self659 .recorder660 .weight_calls_budget(<StructureWeight<T>>::find_parent());661662 <Pallet<T>>::transfer(self, &caller, &to, token, &budget).map_err(dispatch_to_evm::<T>)?;663 Ok(())664 }665666 /// @notice Transfer ownership of an NFT from cross account address to cross account address667 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`668 /// is the zero address. Throws if `tokenId` is not a valid NFT.669 /// @param from Cross acccount address of current owner670 /// @param to Cross acccount address of new owner671 /// @param tokenId The NFT to transfer672 #[weight(<SelfWeightOf<T>>::transfer())]673 fn transfer_from_cross(674 &mut self,675 caller: caller,676 from: (address, uint256),677 to: (address, uint256),678 token_id: uint256,679 ) -> Result<void> {680 let caller = T::CrossAccountId::from_eth(caller);681 let from = convert_tuple_to_cross_account::<T>(from)?;682 let to = convert_tuple_to_cross_account::<T>(to)?;683 let token_id = token_id.try_into()?;684 let budget = self685 .recorder686 .weight_calls_budget(<StructureWeight<T>>::find_parent());687 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, &budget)688 .map_err(dispatch_to_evm::<T>)?;689 Ok(())690 }691692 /// @notice Burns a specific ERC721 token.693 /// @dev Throws unless `msg.sender` is the current owner or an authorized694 /// operator for this NFT. Throws if `from` is not the current owner. Throws695 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.696 /// @param from The current owner of the NFT697 /// @param tokenId The NFT to transfer698 #[weight(<SelfWeightOf<T>>::burn_from())]699 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {700 let caller = T::CrossAccountId::from_eth(caller);701 let from = T::CrossAccountId::from_eth(from);702 let token = token_id.try_into()?;703 let budget = self704 .recorder705 .weight_calls_budget(<StructureWeight<T>>::find_parent());706707 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)708 .map_err(dispatch_to_evm::<T>)?;709 Ok(())710 }711712 /// @notice Burns a specific ERC721 token.713 /// @dev Throws unless `msg.sender` is the current owner or an authorized714 /// operator for this NFT. Throws if `from` is not the current owner. Throws715 /// if `to` is the zero address. Throws if `tokenId` is not a valid NFT.716 /// @param from The current owner of the NFT717 /// @param tokenId The NFT to transfer718 #[weight(<SelfWeightOf<T>>::burn_from())]719 fn burn_from_cross(720 &mut self,721 caller: caller,722 from: (address, uint256),723 token_id: uint256,724 ) -> Result<void> {725 let caller = T::CrossAccountId::from_eth(caller);726 let from = convert_tuple_to_cross_account::<T>(from)?;727 let token = token_id.try_into()?;728 let budget = self729 .recorder730 .weight_calls_budget(<StructureWeight<T>>::find_parent());731732 <Pallet<T>>::burn_from(self, &caller, &from, token, &budget)733 .map_err(dispatch_to_evm::<T>)?;734 Ok(())735 }736737 /// @notice Returns next free NFT ID.738 fn next_token_id(&self) -> Result<uint256> {739 self.consume_store_reads(1)?;740 Ok(<TokensMinted<T>>::get(self.id)741 .checked_add(1)742 .ok_or("item id overflow")?743 .into())744 }745746 /// @notice Function to mint multiple tokens.747 /// @dev `tokenIds` should be an array of consecutive numbers and first number748 /// should be obtained with `nextTokenId` method749 /// @param to The new owner750 /// @param tokenIds IDs of the minted NFTs751 #[solidity(hide)]752 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]753 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {754 let caller = T::CrossAccountId::from_eth(caller);755 let to = T::CrossAccountId::from_eth(to);756 let mut expected_index = <TokensMinted<T>>::get(self.id)757 .checked_add(1)758 .ok_or("item id overflow")?;759 let budget = self760 .recorder761 .weight_calls_budget(<StructureWeight<T>>::find_parent());762763 let total_tokens = token_ids.len();764 for id in token_ids.into_iter() {765 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;766 if id != expected_index {767 return Err("item id should be next".into());768 }769 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;770 }771 let data = (0..total_tokens)772 .map(|_| CreateItemData::<T> {773 properties: BoundedVec::default(),774 owner: to.clone(),775 })776 .collect();777778 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)779 .map_err(dispatch_to_evm::<T>)?;780 Ok(true)781 }782783 /// @notice Function to mint multiple tokens with the given tokenUris.784 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive785 /// numbers and first number should be obtained with `nextTokenId` method786 /// @param to The new owner787 /// @param tokens array of pairs of token ID and token URI for minted tokens788 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]789 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]790 fn mint_bulk_with_token_uri(791 &mut self,792 caller: caller,793 to: address,794 tokens: Vec<(uint256, string)>,795 ) -> Result<bool> {796 let key = key::url();797 let caller = T::CrossAccountId::from_eth(caller);798 let to = T::CrossAccountId::from_eth(to);799 let mut expected_index = <TokensMinted<T>>::get(self.id)800 .checked_add(1)801 .ok_or("item id overflow")?;802 let budget = self803 .recorder804 .weight_calls_budget(<StructureWeight<T>>::find_parent());805806 let mut data = Vec::with_capacity(tokens.len());807 for (id, token_uri) in tokens {808 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;809 if id != expected_index {810 return Err("item id should be next".into());811 }812 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;813814 let mut properties = CollectionPropertiesVec::default();815 properties816 .try_push(Property {817 key: key.clone(),818 value: token_uri819 .into_bytes()820 .try_into()821 .map_err(|_| "token uri is too long")?,822 })823 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;824825 data.push(CreateItemData::<T> {826 properties,827 owner: to.clone(),828 });829 }830831 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)832 .map_err(dispatch_to_evm::<T>)?;833 Ok(true)834 }835}836837#[solidity_interface(838 name = UniqueNFT,839 is(840 ERC721,841 ERC721Enumerable,842 ERC721UniqueExtensions,843 ERC721UniqueMintable,844 ERC721Burnable,845 ERC721Metadata(if(this.flags.erc721metadata)),846 Collection(via(common_mut returns CollectionHandle<T>)),847 TokenProperties,848 )849)]850impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}851852// Not a tests, but code generators853generate_stubgen!(gen_impl, UniqueNFTCall<()>, true);854generate_stubgen!(gen_iface, UniqueNFTCall<()>, false);855856impl<T: Config> CommonEvmHandler for NonfungibleHandle<T>857where858 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,859{860 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueNFT.raw");861862 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {863 call::<T, UniqueNFTCall<T>, _, _>(handle, self)864 }865}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 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}pallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterbothbinary blob — no preview
pallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -18,7 +18,7 @@
}
/// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
contract TokenProperties is Dummy, ERC165 {
/// @notice Set permissions for token property.
/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -61,6 +61,19 @@
dummy = 0;
}
+ /// @notice Set token properties value.
+ /// @dev Throws error if `msg.sender` has no permission to edit the property.
+ /// @param tokenId ID of the token.
+ /// @param properties settable properties
+ /// @dev EVM selector for this function is: 0x14ed3a6e,
+ /// or in textual repr: setProperties(uint256,(string,bytes)[])
+ function setProperties(uint256 tokenId, Tuple19[] memory properties) public {
+ require(false, stub_error);
+ tokenId;
+ properties;
+ dummy = 0;
+ }
+
/// @notice Delete token property value.
/// @dev Throws error if `msg.sender` has no permission to edit the property.
/// @param tokenId ID of the token.
@@ -458,7 +471,7 @@
/// Get collection owner.
///
- /// @return Tuble with sponsor address and his substrate mirror.
+ /// @return Tuple with sponsor address and his substrate mirror.
/// If address is canonical then substrate mirror is zero and vice versa.
/// @dev EVM selector for this function is: 0xdf727d3b,
/// or in textual repr: collectionOwner()
pallets/refungible/CHANGELOG.mddiffbeforeafterboth--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -2,12 +2,20 @@
All notable changes to this project will be documented in this file.
+## [v0.2.5] - 2022-20-10
+
+### Change
+
+- Added `set_properties` method for `TokenProperties` interface.
+
## [v0.2.4] - 2022-08-24
### Change
- - Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
+- Add bound `AsRef<[u8; 32]>` to `T::CrossAccountId`.
+
<!-- bureaucrate goes here -->
+
## [v0.2.3] 2022-08-16
### Other changes
pallets/refungible/Cargo.tomldiffbeforeafterboth--- a/pallets/refungible/Cargo.toml
+++ b/pallets/refungible/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pallet-refungible"
-version = "0.2.4"
+version = "0.2.5"
license = "GPLv3"
edition = "2021"
pallets/refungible/src/erc.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -117,6 +117,47 @@
.map_err(dispatch_to_evm::<T>)
}
+ /// @notice Set token properties value.
+ /// @dev Throws error if `msg.sender` has no permission to edit the property.
+ /// @param tokenId ID of the token.
+ /// @param properties settable properties
+ fn set_properties(
+ &mut self,
+ caller: caller,
+ token_id: uint256,
+ properties: Vec<(string, bytes)>,
+ ) -> Result<()> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;
+
+ let nesting_budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ let properties = properties
+ .into_iter()
+ .map(|(key, value)| {
+ let key = <Vec<u8>>::from(key)
+ .try_into()
+ .map_err(|_| "key too large")?;
+
+ let value = value.0.try_into().map_err(|_| "value too large")?;
+
+ Ok(Property { key, value })
+ })
+ .collect::<Result<Vec<_>>>()?;
+
+ <Pallet<T>>::set_token_properties(
+ self,
+ &caller,
+ TokenId(token_id),
+ properties.into_iter(),
+ <Pallet<T>>::token_exists(&self, TokenId(token_id)),
+ &nesting_budget,
+ )
+ .map_err(dispatch_to_evm::<T>)
+ }
+
/// @notice Delete token property value.
/// @dev Throws error if `msg.sender` has no permission to edit the property.
/// @param tokenId ID of the token.
pallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterbothbinary blob — no preview
pallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -18,7 +18,7 @@
}
/// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
contract TokenProperties is Dummy, ERC165 {
/// @notice Set permissions for token property.
/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -61,6 +61,19 @@
dummy = 0;
}
+ /// @notice Set token properties value.
+ /// @dev Throws error if `msg.sender` has no permission to edit the property.
+ /// @param tokenId ID of the token.
+ /// @param properties settable properties
+ /// @dev EVM selector for this function is: 0x14ed3a6e,
+ /// or in textual repr: setProperties(uint256,(string,bytes)[])
+ function setProperties(uint256 tokenId, Tuple19[] memory properties) public {
+ require(false, stub_error);
+ tokenId;
+ properties;
+ dummy = 0;
+ }
+
/// @notice Delete token property value.
/// @dev Throws error if `msg.sender` has no permission to edit the property.
/// @param tokenId ID of the token.
@@ -458,7 +471,7 @@
/// Get collection owner.
///
- /// @return Tuble with sponsor address and his substrate mirror.
+ /// @return Tuple with sponsor address and his substrate mirror.
/// If address is canonical then substrate mirror is zero and vice versa.
/// @dev EVM selector for this function is: 0xdf727d3b,
/// or in textual repr: collectionOwner()
tests/src/eth/api/UniqueFungible.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueFungible.sol
+++ b/tests/src/eth/api/UniqueFungible.sol
@@ -249,7 +249,7 @@
/// Get collection owner.
///
- /// @return Tuble with sponsor address and his substrate mirror.
+ /// @return Tuple with sponsor address and his substrate mirror.
/// If address is canonical then substrate mirror is zero and vice versa.
/// @dev EVM selector for this function is: 0xdf727d3b,
/// or in textual repr: collectionOwner()
tests/src/eth/api/UniqueNFT.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -13,7 +13,7 @@
}
/// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
interface TokenProperties is Dummy, ERC165 {
/// @notice Set permissions for token property.
/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -43,6 +43,14 @@
bytes memory value
) external;
+ /// @notice Set token properties value.
+ /// @dev Throws error if `msg.sender` has no permission to edit the property.
+ /// @param tokenId ID of the token.
+ /// @param properties settable properties
+ /// @dev EVM selector for this function is: 0x14ed3a6e,
+ /// or in textual repr: setProperties(uint256,(string,bytes)[])
+ function setProperties(uint256 tokenId, Tuple19[] memory properties) external;
+
/// @notice Delete token property value.
/// @dev Throws error if `msg.sender` has no permission to edit the property.
/// @param tokenId ID of the token.
@@ -298,7 +306,7 @@
/// Get collection owner.
///
- /// @return Tuble with sponsor address and his substrate mirror.
+ /// @return Tuple with sponsor address and his substrate mirror.
/// If address is canonical then substrate mirror is zero and vice versa.
/// @dev EVM selector for this function is: 0xdf727d3b,
/// or in textual repr: collectionOwner()
tests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -13,7 +13,7 @@
}
/// @title A contract that allows to set and delete token properties and change token property permissions.
-/// @dev the ERC-165 identifier for this interface is 0x41369377
+/// @dev the ERC-165 identifier for this interface is 0x55dba919
interface TokenProperties is Dummy, ERC165 {
/// @notice Set permissions for token property.
/// @dev Throws error if `msg.sender` is not admin or owner of the collection.
@@ -43,6 +43,14 @@
bytes memory value
) external;
+ /// @notice Set token properties value.
+ /// @dev Throws error if `msg.sender` has no permission to edit the property.
+ /// @param tokenId ID of the token.
+ /// @param properties settable properties
+ /// @dev EVM selector for this function is: 0x14ed3a6e,
+ /// or in textual repr: setProperties(uint256,(string,bytes)[])
+ function setProperties(uint256 tokenId, Tuple19[] memory properties) external;
+
/// @notice Delete token property value.
/// @dev Throws error if `msg.sender` has no permission to edit the property.
/// @param tokenId ID of the token.
@@ -298,7 +306,7 @@
/// Get collection owner.
///
- /// @return Tuble with sponsor address and his substrate mirror.
+ /// @return Tuple with sponsor address and his substrate mirror.
/// If address is canonical then substrate mirror is zero and vice versa.
/// @dev EVM selector for this function is: 0xdf727d3b,
/// or in textual repr: collectionOwner()
tests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth--- a/tests/src/eth/nonFungibleAbi.json
+++ b/tests/src/eth/nonFungibleAbi.json
@@ -677,6 +677,24 @@
{
"inputs": [
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple19[]",
+ "name": "properties",
+ "type": "tuple[]"
+ }
+ ],
+ "name": "setProperties",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
{ "internalType": "string", "name": "key", "type": "string" },
{ "internalType": "bytes", "name": "value", "type": "bytes" }
],
tests/src/eth/reFungibleAbi.jsondiffbeforeafterboth--- a/tests/src/eth/reFungibleAbi.json
+++ b/tests/src/eth/reFungibleAbi.json
@@ -659,6 +659,24 @@
{
"inputs": [
{ "internalType": "uint256", "name": "tokenId", "type": "uint256" },
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple19[]",
+ "name": "properties",
+ "type": "tuple[]"
+ }
+ ],
+ "name": "setProperties",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
{ "internalType": "string", "name": "key", "type": "string" },
{ "internalType": "bytes", "name": "value", "type": "bytes" }
],
tests/src/eth/tokenProperties.test.tsdiffbeforeafterboth--- a/tests/src/eth/tokenProperties.test.ts
+++ b/tests/src/eth/tokenProperties.test.ts
@@ -16,6 +16,7 @@
import {itEth, usingEthPlaygrounds, expect} from './util';
import {IKeyringPair} from '@polkadot/types/types';
+import {ITokenPropertyPermission} from '../util/playgrounds/types';
describe('EVM token properties', () => {
let donor: IKeyringPair;
@@ -68,6 +69,64 @@
const [{value}] = await token.getProperties(['testKey']);
expect(value).to.equal('testValue');
});
+
+ itEth('Can be multiple set for NFT ', async({helper}) => {
+ const caller = await helper.eth.createAccountWithBalance(donor);
+
+ const properties = Array(5).fill(0).map((_, i) => { return {field_0: `key_${i}`, field_1: Buffer.from(`value_${i}`)}; });
+ const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.field_0, permission: {tokenOwner: true,
+ collectionAdmin: true,
+ mutable: true}}; });
+
+ const collection = await helper.nft.mintCollection(alice, {
+ tokenPrefix: 'ethp',
+ tokenPropertyPermissions: permissions,
+ });
+
+ const token = await collection.mintToken(alice);
+
+ const valuesBefore = await token.getProperties(properties.map(p => p.field_0));
+ expect(valuesBefore).to.be.deep.equal([]);
+
+ await collection.addAdmin(alice, {Ethereum: caller});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'nft', caller);
+
+ await contract.methods.setProperties(token.tokenId, properties).send({from: caller});
+
+ const values = await token.getProperties(properties.map(p => p.field_0));
+ expect(values).to.be.deep.equal(properties.map(p => { return {key: p.field_0, value: p.field_1.toString()}; }));
+ });
+
+ itEth('Can be multiple set for RFT ', async({helper}) => {
+ const caller = await helper.eth.createAccountWithBalance(donor);
+
+ const properties = Array(5).fill(0).map((_, i) => { return {field_0: `key_${i}`, field_1: Buffer.from(`value_${i}`)}; });
+ const permissions: ITokenPropertyPermission[] = properties.map(p => { return {key: p.field_0, permission: {tokenOwner: true,
+ collectionAdmin: true,
+ mutable: true}}; });
+
+ const collection = await helper.rft.mintCollection(alice, {
+ tokenPrefix: 'ethp',
+ tokenPropertyPermissions: permissions,
+ });
+
+ const token = await collection.mintToken(alice);
+
+ const valuesBefore = await token.getProperties(properties.map(p => p.field_0));
+ expect(valuesBefore).to.be.deep.equal([]);
+
+ await collection.addAdmin(alice, {Ethereum: caller});
+
+ const address = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(address, 'rft', caller);
+
+ await contract.methods.setProperties(token.tokenId, properties).send({from: caller});
+
+ const values = await token.getProperties(properties.map(p => p.field_0));
+ expect(values).to.be.deep.equal(properties.map(p => { return {key: p.field_0, value: p.field_1.toString()}; }));
+ });
itEth('Can be deleted', async({helper}) => {
const caller = await helper.eth.createAccountWithBalance(donor);