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.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -114,6 +114,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/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.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//! # Refungible Pallet EVM API for tokens18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.2122extern crate alloc;2324use core::{25 char::{REPLACEMENT_CHARACTER, decode_utf16},26 convert::TryInto,27};28use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};29use frame_support::{BoundedBTreeMap, BoundedVec};30use pallet_common::{31 CollectionHandle, CollectionPropertyPermissions,32 erc::{CommonEvmHandler, CollectionCall, static_property::key},33 eth::convert_tuple_to_cross_account,34};35use pallet_evm::{account::CrossAccountId, PrecompileHandle};36use pallet_evm_coder_substrate::{call, dispatch_to_evm};37use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};38use sp_core::H160;39use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};40use up_data_structs::{41 CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,42 PropertyKeyPermission, PropertyPermission, TokenId,43};4445use crate::{46 AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,47 TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,48};4950pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5152/// @title A contract that allows to set and delete token properties and change token property permissions.53#[solidity_interface(name = TokenProperties)]54impl<T: Config> RefungibleHandle<T> {55 /// @notice Set permissions for token property.56 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.57 /// @param key Property key.58 /// @param isMutable Permission to mutate property.59 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.60 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.61 fn set_token_property_permission(62 &mut self,63 caller: caller,64 key: string,65 is_mutable: bool,66 collection_admin: bool,67 token_owner: bool,68 ) -> Result<()> {69 let caller = T::CrossAccountId::from_eth(caller);70 <Pallet<T>>::set_token_property_permissions(71 self,72 &caller,73 vec![PropertyKeyPermission {74 key: <Vec<u8>>::from(key)75 .try_into()76 .map_err(|_| "too long key")?,77 permission: PropertyPermission {78 mutable: is_mutable,79 collection_admin,80 token_owner,81 },82 }],83 )84 .map_err(dispatch_to_evm::<T>)85 }8687 /// @notice Set token property value.88 /// @dev Throws error if `msg.sender` has no permission to edit the property.89 /// @param tokenId ID of the token.90 /// @param key Property key.91 /// @param value Property value.92 fn set_property(93 &mut self,94 caller: caller,95 token_id: uint256,96 key: string,97 value: bytes,98 ) -> Result<()> {99 let caller = T::CrossAccountId::from_eth(caller);100 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;101 let key = <Vec<u8>>::from(key)102 .try_into()103 .map_err(|_| "key too long")?;104 let value = value.0.try_into().map_err(|_| "value too long")?;105106 let nesting_budget = self107 .recorder108 .weight_calls_budget(<StructureWeight<T>>::find_parent());109110 <Pallet<T>>::set_token_property(111 self,112 &caller,113 TokenId(token_id),114 Property { key, value },115 &nesting_budget,116 )117 .map_err(dispatch_to_evm::<T>)118 }119120 /// @notice Delete token property value.121 /// @dev Throws error if `msg.sender` has no permission to edit the property.122 /// @param tokenId ID of the token.123 /// @param key Property key.124 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {125 let caller = T::CrossAccountId::from_eth(caller);126 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;127 let key = <Vec<u8>>::from(key)128 .try_into()129 .map_err(|_| "key too long")?;130131 let nesting_budget = self132 .recorder133 .weight_calls_budget(<StructureWeight<T>>::find_parent());134135 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)136 .map_err(dispatch_to_evm::<T>)137 }138139 /// @notice Get token property value.140 /// @dev Throws error if key not found141 /// @param tokenId ID of the token.142 /// @param key Property key.143 /// @return Property value bytes144 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {145 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;146 let key = <Vec<u8>>::from(key)147 .try_into()148 .map_err(|_| "key too long")?;149150 let props = <TokenProperties<T>>::get((self.id, token_id));151 let prop = props.get(&key).ok_or("key not found")?;152153 Ok(prop.to_vec().into())154 }155}156157#[derive(ToLog)]158pub enum ERC721Events {159 /// @dev This event emits when NFTs are created (`from` == 0) and destroyed160 /// (`to` == 0). Exception: during contract creation, any number of RFTs161 /// may be created and assigned without emitting Transfer.162 Transfer {163 #[indexed]164 from: address,165 #[indexed]166 to: address,167 #[indexed]168 token_id: uint256,169 },170 /// @dev Not supported171 Approval {172 #[indexed]173 owner: address,174 #[indexed]175 approved: address,176 #[indexed]177 token_id: uint256,178 },179 /// @dev Not supported180 #[allow(dead_code)]181 ApprovalForAll {182 #[indexed]183 owner: address,184 #[indexed]185 operator: address,186 approved: bool,187 },188}189190#[derive(ToLog)]191pub enum ERC721UniqueMintableEvents {192 /// @dev Not supported193 #[allow(dead_code)]194 MintingFinished {},195}196197#[solidity_interface(name = ERC721Metadata)]198impl<T: Config> RefungibleHandle<T>199where200 T::AccountId: From<[u8; 32]>,201{202 /// @notice A descriptive name for a collection of NFTs in this contract203 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`204 #[solidity(hide, rename_selector = "name")]205 fn name_proxy(&self) -> Result<string> {206 self.name()207 }208209 /// @notice An abbreviated name for NFTs in this contract210 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`211 #[solidity(hide, rename_selector = "symbol")]212 fn symbol_proxy(&self) -> Result<string> {213 self.symbol()214 }215216 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.217 ///218 /// @dev If the token has a `url` property and it is not empty, it is returned.219 /// 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`.220 /// If the collection property `baseURI` is empty or absent, return "" (empty string)221 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix222 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).223 ///224 /// @return token's const_metadata225 #[solidity(rename_selector = "tokenURI")]226 fn token_uri(&self, token_id: uint256) -> Result<string> {227 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;228229 match get_token_property(self, token_id_u32, &key::url()).as_deref() {230 Err(_) | Ok("") => (),231 Ok(url) => {232 return Ok(url.into());233 }234 };235236 let base_uri =237 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())238 .map(BoundedVec::into_inner)239 .map(string::from_utf8)240 .transpose()241 .map_err(|e| {242 Error::Revert(alloc::format!(243 "Can not convert value \"baseURI\" to string with error \"{}\"",244 e245 ))246 })?;247248 let base_uri = match base_uri.as_deref() {249 None | Some("") => {250 return Ok("".into());251 }252 Some(base_uri) => base_uri.into(),253 };254255 Ok(256 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {257 Err(_) | Ok("") => base_uri,258 Ok(suffix) => base_uri + suffix,259 },260 )261 }262}263264/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension265/// @dev See https://eips.ethereum.org/EIPS/eip-721266#[solidity_interface(name = ERC721Enumerable)]267impl<T: Config> RefungibleHandle<T> {268 /// @notice Enumerate valid RFTs269 /// @param index A counter less than `totalSupply()`270 /// @return The token identifier for the `index`th NFT,271 /// (sort order not specified)272 fn token_by_index(&self, index: uint256) -> Result<uint256> {273 Ok(index)274 }275276 /// Not implemented277 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {278 // TODO: Not implemetable279 Err("not implemented".into())280 }281282 /// @notice Count RFTs tracked by this contract283 /// @return A count of valid RFTs tracked by this contract, where each one of284 /// them has an assigned and queryable owner not equal to the zero address285 fn total_supply(&self) -> Result<uint256> {286 self.consume_store_reads(1)?;287 Ok(<Pallet<T>>::total_supply(self).into())288 }289}290291/// @title ERC-721 Non-Fungible Token Standard292/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md293#[solidity_interface(name = ERC721, events(ERC721Events))]294impl<T: Config> RefungibleHandle<T> {295 /// @notice Count all RFTs assigned to an owner296 /// @dev RFTs assigned to the zero address are considered invalid, and this297 /// function throws for queries about the zero address.298 /// @param owner An address for whom to query the balance299 /// @return The number of RFTs owned by `owner`, possibly zero300 fn balance_of(&self, owner: address) -> Result<uint256> {301 self.consume_store_reads(1)?;302 let owner = T::CrossAccountId::from_eth(owner);303 let balance = <AccountBalance<T>>::get((self.id, owner));304 Ok(balance.into())305 }306307 /// @notice Find the owner of an RFT308 /// @dev RFTs assigned to zero address are considered invalid, and queries309 /// about them do throw.310 /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for311 /// the tokens that are partially owned.312 /// @param tokenId The identifier for an RFT313 /// @return The address of the owner of the RFT314 fn owner_of(&self, token_id: uint256) -> Result<address> {315 self.consume_store_reads(2)?;316 let token = token_id.try_into()?;317 let owner = <Pallet<T>>::token_owner(self.id, token);318 Ok(owner319 .map(|address| *address.as_eth())320 .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))321 }322323 /// @dev Not implemented324 fn safe_transfer_from_with_data(325 &mut self,326 _from: address,327 _to: address,328 _token_id: uint256,329 _data: bytes,330 ) -> Result<void> {331 // TODO: Not implemetable332 Err("not implemented".into())333 }334335 /// @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 RFT -- 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 RFT. Throws if `from` is not the current owner. Throws351 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.352 /// Throws if RFT pieces have multiple owners.353 /// @param from The current owner of the NFT354 /// @param to The new owner355 /// @param tokenId The NFT to transfer356 #[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]357 fn transfer_from(358 &mut self,359 caller: caller,360 from: address,361 to: address,362 token_id: uint256,363 ) -> Result<void> {364 let caller = T::CrossAccountId::from_eth(caller);365 let from = T::CrossAccountId::from_eth(from);366 let to = T::CrossAccountId::from_eth(to);367 let token = token_id.try_into()?;368 let budget = self369 .recorder370 .weight_calls_budget(<StructureWeight<T>>::find_parent());371372 let balance = balance(&self, token, &from)?;373 ensure_single_owner(&self, token, balance)?;374375 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)376 .map_err(dispatch_to_evm::<T>)?;377378 Ok(())379 }380381 /// @dev Not implemented382 fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {383 Err("not implemented".into())384 }385386 /// @dev Not implemented387 fn set_approval_for_all(388 &mut self,389 _caller: caller,390 _operator: address,391 _approved: bool,392 ) -> Result<void> {393 // TODO: Not implemetable394 Err("not implemented".into())395 }396397 /// @dev Not implemented398 fn get_approved(&self, _token_id: uint256) -> Result<address> {399 // TODO: Not implemetable400 Err("not implemented".into())401 }402403 /// @dev Not implemented404 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {405 // TODO: Not implemetable406 Err("not implemented".into())407 }408}409410/// Returns amount of pieces of `token` that `owner` have411pub fn balance<T: Config>(412 collection: &RefungibleHandle<T>,413 token: TokenId,414 owner: &T::CrossAccountId,415) -> Result<u128> {416 collection.consume_store_reads(1)?;417 let balance = <Balance<T>>::get((collection.id, token, &owner));418 Ok(balance)419}420421/// Throws if `owner_balance` is lower than total amount of `token` pieces422pub fn ensure_single_owner<T: Config>(423 collection: &RefungibleHandle<T>,424 token: TokenId,425 owner_balance: u128,426) -> Result<()> {427 collection.consume_store_reads(1)?;428 let total_supply = <TotalSupply<T>>::get((collection.id, token));429 if total_supply != owner_balance {430 return Err("token has multiple owners".into());431 }432 Ok(())433}434435/// @title ERC721 Token that can be irreversibly burned (destroyed).436#[solidity_interface(name = ERC721Burnable)]437impl<T: Config> RefungibleHandle<T> {438 /// @notice Burns a specific ERC721 token.439 /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized440 /// operator of the current owner.441 /// @param tokenId The RFT to approve442 #[weight(<SelfWeightOf<T>>::burn_item_fully())]443 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {444 let caller = T::CrossAccountId::from_eth(caller);445 let token = token_id.try_into()?;446447 let balance = balance(&self, token, &caller)?;448 ensure_single_owner(&self, token, balance)?;449450 <Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;451 Ok(())452 }453}454455/// @title ERC721 minting logic.456#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]457impl<T: Config> RefungibleHandle<T> {458 fn minting_finished(&self) -> Result<bool> {459 Ok(false)460 }461462 /// @notice Function to mint token.463 /// @param to The new owner464 /// @return uint256 The id of the newly minted token465 #[weight(<SelfWeightOf<T>>::create_item())]466 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {467 let token_id: uint256 = <TokensMinted<T>>::get(self.id)468 .checked_add(1)469 .ok_or("item id overflow")?470 .into();471 self.mint_check_id(caller, to, token_id)?;472 Ok(token_id)473 }474475 /// @notice Function to mint token.476 /// @dev `tokenId` should be obtained with `nextTokenId` method,477 /// unlike standard, you can't specify it manually478 /// @param to The new owner479 /// @param tokenId ID of the minted RFT480 #[solidity(hide, rename_selector = "mint")]481 #[weight(<SelfWeightOf<T>>::create_item())]482 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {483 let caller = T::CrossAccountId::from_eth(caller);484 let to = T::CrossAccountId::from_eth(to);485 let token_id: u32 = token_id.try_into()?;486 let budget = self487 .recorder488 .weight_calls_budget(<StructureWeight<T>>::find_parent());489490 if <TokensMinted<T>>::get(self.id)491 .checked_add(1)492 .ok_or("item id overflow")?493 != token_id494 {495 return Err("item id should be next".into());496 }497498 let users = [(to.clone(), 1)]499 .into_iter()500 .collect::<BTreeMap<_, _>>()501 .try_into()502 .unwrap();503 <Pallet<T>>::create_item(504 self,505 &caller,506 CreateItemData::<T::CrossAccountId> {507 users,508 properties: CollectionPropertiesVec::default(),509 },510 &budget,511 )512 .map_err(dispatch_to_evm::<T>)?;513514 Ok(true)515 }516517 /// @notice Function to mint token with the given tokenUri.518 /// @param to The new owner519 /// @param tokenUri Token URI that would be stored in the NFT properties520 /// @return uint256 The id of the newly minted token521 #[solidity(rename_selector = "mintWithTokenURI")]522 #[weight(<SelfWeightOf<T>>::create_item())]523 fn mint_with_token_uri(524 &mut self,525 caller: caller,526 to: address,527 token_uri: string,528 ) -> Result<uint256> {529 let token_id: uint256 = <TokensMinted<T>>::get(self.id)530 .checked_add(1)531 .ok_or("item id overflow")?532 .into();533 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;534 Ok(token_id)535 }536537 /// @notice Function to mint token with the given tokenUri.538 /// @dev `tokenId` should be obtained with `nextTokenId` method,539 /// unlike standard, you can't specify it manually540 /// @param to The new owner541 /// @param tokenId ID of the minted RFT542 /// @param tokenUri Token URI that would be stored in the RFT properties543 #[solidity(hide, rename_selector = "mintWithTokenURI")]544 #[weight(<SelfWeightOf<T>>::create_item())]545 fn mint_with_token_uri_check_id(546 &mut self,547 caller: caller,548 to: address,549 token_id: uint256,550 token_uri: string,551 ) -> Result<bool> {552 let key = key::url();553 let permission = get_token_permission::<T>(self.id, &key)?;554 if !permission.collection_admin {555 return Err("Operation is not allowed".into());556 }557558 let caller = T::CrossAccountId::from_eth(caller);559 let to = T::CrossAccountId::from_eth(to);560 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;561 let budget = self562 .recorder563 .weight_calls_budget(<StructureWeight<T>>::find_parent());564565 if <TokensMinted<T>>::get(self.id)566 .checked_add(1)567 .ok_or("item id overflow")?568 != token_id569 {570 return Err("item id should be next".into());571 }572573 let mut properties = CollectionPropertiesVec::default();574 properties575 .try_push(Property {576 key,577 value: token_uri578 .into_bytes()579 .try_into()580 .map_err(|_| "token uri is too long")?,581 })582 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;583584 let users = [(to.clone(), 1)]585 .into_iter()586 .collect::<BTreeMap<_, _>>()587 .try_into()588 .unwrap();589 <Pallet<T>>::create_item(590 self,591 &caller,592 CreateItemData::<T::CrossAccountId> { users, properties },593 &budget,594 )595 .map_err(dispatch_to_evm::<T>)?;596 Ok(true)597 }598599 /// @dev Not implemented600 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {601 Err("not implementable".into())602 }603}604605fn get_token_property<T: Config>(606 collection: &CollectionHandle<T>,607 token_id: u32,608 key: &up_data_structs::PropertyKey,609) -> Result<string> {610 collection.consume_store_reads(1)?;611 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))612 .map_err(|_| Error::Revert("Token properties not found".into()))?;613 if let Some(property) = properties.get(key) {614 return Ok(string::from_utf8_lossy(property).into());615 }616617 Err("Property tokenURI not found".into())618}619620fn get_token_permission<T: Config>(621 collection_id: CollectionId,622 key: &PropertyKey,623) -> Result<PropertyPermission> {624 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)625 .map_err(|_| Error::Revert("No permissions for collection".into()))?;626 let a = token_property_permissions627 .get(key)628 .map(Clone::clone)629 .ok_or_else(|| {630 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();631 Error::Revert(alloc::format!("No permission for key {}", key))632 })?;633 Ok(a)634}635636/// @title Unique extensions for ERC721.637#[solidity_interface(name = ERC721UniqueExtensions)]638impl<T: Config> RefungibleHandle<T>639where640 T::AccountId: From<[u8; 32]>,641{642 /// @notice A descriptive name for a collection of NFTs in this contract643 fn name(&self) -> Result<string> {644 Ok(decode_utf16(self.name.iter().copied())645 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))646 .collect::<string>())647 }648649 /// @notice An abbreviated name for NFTs in this contract650 fn symbol(&self) -> Result<string> {651 Ok(string::from_utf8_lossy(&self.token_prefix).into())652 }653654 /// @notice Transfer ownership of an RFT655 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`656 /// is the zero address. Throws if `tokenId` is not a valid RFT.657 /// Throws if RFT pieces have multiple owners.658 /// @param to The new owner659 /// @param tokenId The RFT to transfer660 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]661 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {662 let caller = T::CrossAccountId::from_eth(caller);663 let to = T::CrossAccountId::from_eth(to);664 let token = token_id.try_into()?;665 let budget = self666 .recorder667 .weight_calls_budget(<StructureWeight<T>>::find_parent());668669 let balance = balance(self, token, &caller)?;670 ensure_single_owner(self, token, balance)?;671672 <Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)673 .map_err(dispatch_to_evm::<T>)?;674 Ok(())675 }676677 /// @notice Transfer ownership of an RFT678 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`679 /// is the zero address. Throws if `tokenId` is not a valid RFT.680 /// Throws if RFT pieces have multiple owners.681 /// @param to The new owner682 /// @param tokenId The RFT to transfer683 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]684 fn transfer_from_cross(685 &mut self,686 caller: caller,687 from: (address, uint256),688 to: (address, uint256),689 token_id: uint256,690 ) -> Result<void> {691 let caller = T::CrossAccountId::from_eth(caller);692 let from = convert_tuple_to_cross_account::<T>(from)?;693 let to = convert_tuple_to_cross_account::<T>(to)?;694 let token_id = token_id.try_into()?;695 let budget = self696 .recorder697 .weight_calls_budget(<StructureWeight<T>>::find_parent());698699 let balance = balance(self, token_id, &from)?;700 ensure_single_owner(self, token_id, balance)?;701702 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)703 .map_err(dispatch_to_evm::<T>)?;704 Ok(())705 }706707 /// @notice Burns a specific ERC721 token.708 /// @dev Throws unless `msg.sender` is the current owner or an authorized709 /// operator for this RFT. Throws if `from` is not the current owner. Throws710 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.711 /// Throws if RFT pieces have multiple owners.712 /// @param from The current owner of the RFT713 /// @param tokenId The RFT to transfer714 #[weight(<SelfWeightOf<T>>::burn_from())]715 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {716 let caller = T::CrossAccountId::from_eth(caller);717 let from = T::CrossAccountId::from_eth(from);718 let token = token_id.try_into()?;719 let budget = self720 .recorder721 .weight_calls_budget(<StructureWeight<T>>::find_parent());722723 let balance = balance(self, token, &from)?;724 ensure_single_owner(self, token, balance)?;725726 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)727 .map_err(dispatch_to_evm::<T>)?;728 Ok(())729 }730731 /// @notice Burns a specific ERC721 token.732 /// @dev Throws unless `msg.sender` is the current owner or an authorized733 /// operator for this RFT. Throws if `from` is not the current owner. Throws734 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.735 /// Throws if RFT pieces have multiple owners.736 /// @param from The current owner of the RFT737 /// @param tokenId The RFT to transfer738 #[weight(<SelfWeightOf<T>>::burn_from())]739 fn burn_from_cross(740 &mut self,741 caller: caller,742 from: (address, uint256),743 token_id: uint256,744 ) -> Result<void> {745 let caller = T::CrossAccountId::from_eth(caller);746 let from = convert_tuple_to_cross_account::<T>(from)?;747 let token = token_id.try_into()?;748 let budget = self749 .recorder750 .weight_calls_budget(<StructureWeight<T>>::find_parent());751752 let balance = balance(self, token, &from)?;753 ensure_single_owner(self, token, balance)?;754755 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)756 .map_err(dispatch_to_evm::<T>)?;757 Ok(())758 }759760 /// @notice Returns next free RFT ID.761 fn next_token_id(&self) -> Result<uint256> {762 self.consume_store_reads(1)?;763 Ok(<TokensMinted<T>>::get(self.id)764 .checked_add(1)765 .ok_or("item id overflow")?766 .into())767 }768769 /// @notice Function to mint multiple tokens.770 /// @dev `tokenIds` should be an array of consecutive numbers and first number771 /// should be obtained with `nextTokenId` method772 /// @param to The new owner773 /// @param tokenIds IDs of the minted RFTs774 #[solidity(hide)]775 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]776 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {777 let caller = T::CrossAccountId::from_eth(caller);778 let to = T::CrossAccountId::from_eth(to);779 let mut expected_index = <TokensMinted<T>>::get(self.id)780 .checked_add(1)781 .ok_or("item id overflow")?;782 let budget = self783 .recorder784 .weight_calls_budget(<StructureWeight<T>>::find_parent());785786 let total_tokens = token_ids.len();787 for id in token_ids.into_iter() {788 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;789 if id != expected_index {790 return Err("item id should be next".into());791 }792 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;793 }794 let users = [(to.clone(), 1)]795 .into_iter()796 .collect::<BTreeMap<_, _>>()797 .try_into()798 .unwrap();799 let create_item_data = CreateItemData::<T::CrossAccountId> {800 users,801 properties: CollectionPropertiesVec::default(),802 };803 let data = (0..total_tokens)804 .map(|_| create_item_data.clone())805 .collect();806807 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)808 .map_err(dispatch_to_evm::<T>)?;809 Ok(true)810 }811812 /// @notice Function to mint multiple tokens with the given tokenUris.813 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive814 /// numbers and first number should be obtained with `nextTokenId` method815 /// @param to The new owner816 /// @param tokens array of pairs of token ID and token URI for minted tokens817 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]818 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]819 fn mint_bulk_with_token_uri(820 &mut self,821 caller: caller,822 to: address,823 tokens: Vec<(uint256, string)>,824 ) -> Result<bool> {825 let key = key::url();826 let caller = T::CrossAccountId::from_eth(caller);827 let to = T::CrossAccountId::from_eth(to);828 let mut expected_index = <TokensMinted<T>>::get(self.id)829 .checked_add(1)830 .ok_or("item id overflow")?;831 let budget = self832 .recorder833 .weight_calls_budget(<StructureWeight<T>>::find_parent());834835 let mut data = Vec::with_capacity(tokens.len());836 let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]837 .into_iter()838 .collect::<BTreeMap<_, _>>()839 .try_into()840 .unwrap();841 for (id, token_uri) in tokens {842 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;843 if id != expected_index {844 return Err("item id should be next".into());845 }846 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;847848 let mut properties = CollectionPropertiesVec::default();849 properties850 .try_push(Property {851 key: key.clone(),852 value: token_uri853 .into_bytes()854 .try_into()855 .map_err(|_| "token uri is too long")?,856 })857 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;858859 let create_item_data = CreateItemData::<T::CrossAccountId> {860 users: users.clone(),861 properties,862 };863 data.push(create_item_data);864 }865866 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)867 .map_err(dispatch_to_evm::<T>)?;868 Ok(true)869 }870871 /// Returns EVM address for refungible token872 ///873 /// @param token ID of the token874 fn token_contract_address(&self, token: uint256) -> Result<address> {875 Ok(T::EvmTokenAddressMapping::token_to_address(876 self.id,877 token.try_into().map_err(|_| "token id overflow")?,878 ))879 }880}881882#[solidity_interface(883 name = UniqueRefungible,884 is(885 ERC721,886 ERC721Enumerable,887 ERC721UniqueExtensions,888 ERC721UniqueMintable,889 ERC721Burnable,890 ERC721Metadata(if(this.flags.erc721metadata)),891 Collection(via(common_mut returns CollectionHandle<T>)),892 TokenProperties,893 )894)]895impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}896897// Not a tests, but code generators898generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);899generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);900901impl<T: Config> CommonEvmHandler for RefungibleHandle<T>902where903 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,904{905 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");906 fn call(907 self,908 handle: &mut impl PrecompileHandle,909 ) -> Option<pallet_common::erc::PrecompileResult> {910 call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)911 }912}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//! # Refungible Pallet EVM API for tokens18//!19//! Provides ERC-721 standart support implementation and EVM API for unique extensions for Refungible Pallet.20//! Method implementations are mostly doing parameter conversion and calling Refungible Pallet methods.2122extern crate alloc;2324use core::{25 char::{REPLACEMENT_CHARACTER, decode_utf16},26 convert::TryInto,27};28use evm_coder::{ToLog, execution::*, generate_stubgen, solidity, solidity_interface, types::*, weight};29use frame_support::{BoundedBTreeMap, BoundedVec};30use pallet_common::{31 CollectionHandle, CollectionPropertyPermissions,32 erc::{CommonEvmHandler, CollectionCall, static_property::key},33 eth::convert_tuple_to_cross_account,34};35use pallet_evm::{account::CrossAccountId, PrecompileHandle};36use pallet_evm_coder_substrate::{call, dispatch_to_evm};37use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};38use sp_core::H160;39use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};40use up_data_structs::{41 CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,42 PropertyKeyPermission, PropertyPermission, TokenId,43};4445use crate::{46 AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,47 TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,48};4950pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5152/// @title A contract that allows to set and delete token properties and change token property permissions.53#[solidity_interface(name = TokenProperties)]54impl<T: Config> RefungibleHandle<T> {55 /// @notice Set permissions for token property.56 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.57 /// @param key Property key.58 /// @param isMutable Permission to mutate property.59 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.60 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.61 fn set_token_property_permission(62 &mut self,63 caller: caller,64 key: string,65 is_mutable: bool,66 collection_admin: bool,67 token_owner: bool,68 ) -> Result<()> {69 let caller = T::CrossAccountId::from_eth(caller);70 <Pallet<T>>::set_token_property_permissions(71 self,72 &caller,73 vec![PropertyKeyPermission {74 key: <Vec<u8>>::from(key)75 .try_into()76 .map_err(|_| "too long key")?,77 permission: PropertyPermission {78 mutable: is_mutable,79 collection_admin,80 token_owner,81 },82 }],83 )84 .map_err(dispatch_to_evm::<T>)85 }8687 /// @notice Set token property value.88 /// @dev Throws error if `msg.sender` has no permission to edit the property.89 /// @param tokenId ID of the token.90 /// @param key Property key.91 /// @param value Property value.92 fn set_property(93 &mut self,94 caller: caller,95 token_id: uint256,96 key: string,97 value: bytes,98 ) -> Result<()> {99 let caller = T::CrossAccountId::from_eth(caller);100 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;101 let key = <Vec<u8>>::from(key)102 .try_into()103 .map_err(|_| "key too long")?;104 let value = value.0.try_into().map_err(|_| "value too long")?;105106 let nesting_budget = self107 .recorder108 .weight_calls_budget(<StructureWeight<T>>::find_parent());109110 <Pallet<T>>::set_token_property(111 self,112 &caller,113 TokenId(token_id),114 Property { key, value },115 &nesting_budget,116 )117 .map_err(dispatch_to_evm::<T>)118 }119120 /// @notice Set token properties value.121 /// @dev Throws error if `msg.sender` has no permission to edit the property.122 /// @param tokenId ID of the token.123 /// @param properties settable properties124 fn set_properties(125 &mut self,126 caller: caller,127 token_id: uint256,128 properties: Vec<(string, bytes)>,129 ) -> Result<()> {130 let caller = T::CrossAccountId::from_eth(caller);131 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;132133 let nesting_budget = self134 .recorder135 .weight_calls_budget(<StructureWeight<T>>::find_parent());136137 let properties = properties138 .into_iter()139 .map(|(key, value)| {140 let key = <Vec<u8>>::from(key)141 .try_into()142 .map_err(|_| "key too large")?;143144 let value = value.0.try_into().map_err(|_| "value too large")?;145146 Ok(Property { key, value })147 })148 .collect::<Result<Vec<_>>>()?;149150 <Pallet<T>>::set_token_properties(151 self,152 &caller,153 TokenId(token_id),154 properties.into_iter(),155 <Pallet<T>>::token_exists(&self, TokenId(token_id)),156 &nesting_budget,157 )158 .map_err(dispatch_to_evm::<T>)159 }160161 /// @notice Delete token property value.162 /// @dev Throws error if `msg.sender` has no permission to edit the property.163 /// @param tokenId ID of the token.164 /// @param key Property key.165 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {166 let caller = T::CrossAccountId::from_eth(caller);167 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;168 let key = <Vec<u8>>::from(key)169 .try_into()170 .map_err(|_| "key too long")?;171172 let nesting_budget = self173 .recorder174 .weight_calls_budget(<StructureWeight<T>>::find_parent());175176 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)177 .map_err(dispatch_to_evm::<T>)178 }179180 /// @notice Get token property value.181 /// @dev Throws error if key not found182 /// @param tokenId ID of the token.183 /// @param key Property key.184 /// @return Property value bytes185 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {186 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;187 let key = <Vec<u8>>::from(key)188 .try_into()189 .map_err(|_| "key too long")?;190191 let props = <TokenProperties<T>>::get((self.id, token_id));192 let prop = props.get(&key).ok_or("key not found")?;193194 Ok(prop.to_vec().into())195 }196}197198#[derive(ToLog)]199pub enum ERC721Events {200 /// @dev This event emits when NFTs are created (`from` == 0) and destroyed201 /// (`to` == 0). Exception: during contract creation, any number of RFTs202 /// may be created and assigned without emitting Transfer.203 Transfer {204 #[indexed]205 from: address,206 #[indexed]207 to: address,208 #[indexed]209 token_id: uint256,210 },211 /// @dev Not supported212 Approval {213 #[indexed]214 owner: address,215 #[indexed]216 approved: address,217 #[indexed]218 token_id: uint256,219 },220 /// @dev Not supported221 #[allow(dead_code)]222 ApprovalForAll {223 #[indexed]224 owner: address,225 #[indexed]226 operator: address,227 approved: bool,228 },229}230231#[derive(ToLog)]232pub enum ERC721UniqueMintableEvents {233 /// @dev Not supported234 #[allow(dead_code)]235 MintingFinished {},236}237238#[solidity_interface(name = ERC721Metadata)]239impl<T: Config> RefungibleHandle<T>240where241 T::AccountId: From<[u8; 32]>,242{243 /// @notice A descriptive name for a collection of NFTs in this contract244 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`245 #[solidity(hide, rename_selector = "name")]246 fn name_proxy(&self) -> Result<string> {247 self.name()248 }249250 /// @notice An abbreviated name for NFTs in this contract251 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`252 #[solidity(hide, rename_selector = "symbol")]253 fn symbol_proxy(&self) -> Result<string> {254 self.symbol()255 }256257 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.258 ///259 /// @dev If the token has a `url` property and it is not empty, it is returned.260 /// 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`.261 /// If the collection property `baseURI` is empty or absent, return "" (empty string)262 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix263 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).264 ///265 /// @return token's const_metadata266 #[solidity(rename_selector = "tokenURI")]267 fn token_uri(&self, token_id: uint256) -> Result<string> {268 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;269270 match get_token_property(self, token_id_u32, &key::url()).as_deref() {271 Err(_) | Ok("") => (),272 Ok(url) => {273 return Ok(url.into());274 }275 };276277 let base_uri =278 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())279 .map(BoundedVec::into_inner)280 .map(string::from_utf8)281 .transpose()282 .map_err(|e| {283 Error::Revert(alloc::format!(284 "Can not convert value \"baseURI\" to string with error \"{}\"",285 e286 ))287 })?;288289 let base_uri = match base_uri.as_deref() {290 None | Some("") => {291 return Ok("".into());292 }293 Some(base_uri) => base_uri.into(),294 };295296 Ok(297 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {298 Err(_) | Ok("") => base_uri,299 Ok(suffix) => base_uri + suffix,300 },301 )302 }303}304305/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension306/// @dev See https://eips.ethereum.org/EIPS/eip-721307#[solidity_interface(name = ERC721Enumerable)]308impl<T: Config> RefungibleHandle<T> {309 /// @notice Enumerate valid RFTs310 /// @param index A counter less than `totalSupply()`311 /// @return The token identifier for the `index`th NFT,312 /// (sort order not specified)313 fn token_by_index(&self, index: uint256) -> Result<uint256> {314 Ok(index)315 }316317 /// Not implemented318 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {319 // TODO: Not implemetable320 Err("not implemented".into())321 }322323 /// @notice Count RFTs tracked by this contract324 /// @return A count of valid RFTs tracked by this contract, where each one of325 /// them has an assigned and queryable owner not equal to the zero address326 fn total_supply(&self) -> Result<uint256> {327 self.consume_store_reads(1)?;328 Ok(<Pallet<T>>::total_supply(self).into())329 }330}331332/// @title ERC-721 Non-Fungible Token Standard333/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md334#[solidity_interface(name = ERC721, events(ERC721Events))]335impl<T: Config> RefungibleHandle<T> {336 /// @notice Count all RFTs assigned to an owner337 /// @dev RFTs assigned to the zero address are considered invalid, and this338 /// function throws for queries about the zero address.339 /// @param owner An address for whom to query the balance340 /// @return The number of RFTs owned by `owner`, possibly zero341 fn balance_of(&self, owner: address) -> Result<uint256> {342 self.consume_store_reads(1)?;343 let owner = T::CrossAccountId::from_eth(owner);344 let balance = <AccountBalance<T>>::get((self.id, owner));345 Ok(balance.into())346 }347348 /// @notice Find the owner of an RFT349 /// @dev RFTs assigned to zero address are considered invalid, and queries350 /// about them do throw.351 /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for352 /// the tokens that are partially owned.353 /// @param tokenId The identifier for an RFT354 /// @return The address of the owner of the RFT355 fn owner_of(&self, token_id: uint256) -> Result<address> {356 self.consume_store_reads(2)?;357 let token = token_id.try_into()?;358 let owner = <Pallet<T>>::token_owner(self.id, token);359 Ok(owner360 .map(|address| *address.as_eth())361 .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))362 }363364 /// @dev Not implemented365 fn safe_transfer_from_with_data(366 &mut self,367 _from: address,368 _to: address,369 _token_id: uint256,370 _data: bytes,371 ) -> Result<void> {372 // TODO: Not implemetable373 Err("not implemented".into())374 }375376 /// @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 RFT -- 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 RFT. Throws if `from` is not the current owner. Throws392 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.393 /// Throws if RFT pieces have multiple owners.394 /// @param from The current owner of the NFT395 /// @param to The new owner396 /// @param tokenId The NFT to transfer397 #[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]398 fn transfer_from(399 &mut self,400 caller: caller,401 from: address,402 to: address,403 token_id: uint256,404 ) -> Result<void> {405 let caller = T::CrossAccountId::from_eth(caller);406 let from = T::CrossAccountId::from_eth(from);407 let to = T::CrossAccountId::from_eth(to);408 let token = token_id.try_into()?;409 let budget = self410 .recorder411 .weight_calls_budget(<StructureWeight<T>>::find_parent());412413 let balance = balance(&self, token, &from)?;414 ensure_single_owner(&self, token, balance)?;415416 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)417 .map_err(dispatch_to_evm::<T>)?;418419 Ok(())420 }421422 /// @dev Not implemented423 fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {424 Err("not implemented".into())425 }426427 /// @dev Not implemented428 fn set_approval_for_all(429 &mut self,430 _caller: caller,431 _operator: address,432 _approved: bool,433 ) -> Result<void> {434 // TODO: Not implemetable435 Err("not implemented".into())436 }437438 /// @dev Not implemented439 fn get_approved(&self, _token_id: uint256) -> Result<address> {440 // TODO: Not implemetable441 Err("not implemented".into())442 }443444 /// @dev Not implemented445 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {446 // TODO: Not implemetable447 Err("not implemented".into())448 }449}450451/// Returns amount of pieces of `token` that `owner` have452pub fn balance<T: Config>(453 collection: &RefungibleHandle<T>,454 token: TokenId,455 owner: &T::CrossAccountId,456) -> Result<u128> {457 collection.consume_store_reads(1)?;458 let balance = <Balance<T>>::get((collection.id, token, &owner));459 Ok(balance)460}461462/// Throws if `owner_balance` is lower than total amount of `token` pieces463pub fn ensure_single_owner<T: Config>(464 collection: &RefungibleHandle<T>,465 token: TokenId,466 owner_balance: u128,467) -> Result<()> {468 collection.consume_store_reads(1)?;469 let total_supply = <TotalSupply<T>>::get((collection.id, token));470 if total_supply != owner_balance {471 return Err("token has multiple owners".into());472 }473 Ok(())474}475476/// @title ERC721 Token that can be irreversibly burned (destroyed).477#[solidity_interface(name = ERC721Burnable)]478impl<T: Config> RefungibleHandle<T> {479 /// @notice Burns a specific ERC721 token.480 /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized481 /// operator of the current owner.482 /// @param tokenId The RFT to approve483 #[weight(<SelfWeightOf<T>>::burn_item_fully())]484 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {485 let caller = T::CrossAccountId::from_eth(caller);486 let token = token_id.try_into()?;487488 let balance = balance(&self, token, &caller)?;489 ensure_single_owner(&self, token, balance)?;490491 <Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;492 Ok(())493 }494}495496/// @title ERC721 minting logic.497#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]498impl<T: Config> RefungibleHandle<T> {499 fn minting_finished(&self) -> Result<bool> {500 Ok(false)501 }502503 /// @notice Function to mint token.504 /// @param to The new owner505 /// @return uint256 The id of the newly minted token506 #[weight(<SelfWeightOf<T>>::create_item())]507 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {508 let token_id: uint256 = <TokensMinted<T>>::get(self.id)509 .checked_add(1)510 .ok_or("item id overflow")?511 .into();512 self.mint_check_id(caller, to, token_id)?;513 Ok(token_id)514 }515516 /// @notice Function to mint token.517 /// @dev `tokenId` should be obtained with `nextTokenId` method,518 /// unlike standard, you can't specify it manually519 /// @param to The new owner520 /// @param tokenId ID of the minted RFT521 #[solidity(hide, rename_selector = "mint")]522 #[weight(<SelfWeightOf<T>>::create_item())]523 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {524 let caller = T::CrossAccountId::from_eth(caller);525 let to = T::CrossAccountId::from_eth(to);526 let token_id: u32 = token_id.try_into()?;527 let budget = self528 .recorder529 .weight_calls_budget(<StructureWeight<T>>::find_parent());530531 if <TokensMinted<T>>::get(self.id)532 .checked_add(1)533 .ok_or("item id overflow")?534 != token_id535 {536 return Err("item id should be next".into());537 }538539 let users = [(to.clone(), 1)]540 .into_iter()541 .collect::<BTreeMap<_, _>>()542 .try_into()543 .unwrap();544 <Pallet<T>>::create_item(545 self,546 &caller,547 CreateItemData::<T::CrossAccountId> {548 users,549 properties: CollectionPropertiesVec::default(),550 },551 &budget,552 )553 .map_err(dispatch_to_evm::<T>)?;554555 Ok(true)556 }557558 /// @notice Function to mint token with the given tokenUri.559 /// @param to The new owner560 /// @param tokenUri Token URI that would be stored in the NFT properties561 /// @return uint256 The id of the newly minted token562 #[solidity(rename_selector = "mintWithTokenURI")]563 #[weight(<SelfWeightOf<T>>::create_item())]564 fn mint_with_token_uri(565 &mut self,566 caller: caller,567 to: address,568 token_uri: string,569 ) -> Result<uint256> {570 let token_id: uint256 = <TokensMinted<T>>::get(self.id)571 .checked_add(1)572 .ok_or("item id overflow")?573 .into();574 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;575 Ok(token_id)576 }577578 /// @notice Function to mint token with the given tokenUri.579 /// @dev `tokenId` should be obtained with `nextTokenId` method,580 /// unlike standard, you can't specify it manually581 /// @param to The new owner582 /// @param tokenId ID of the minted RFT583 /// @param tokenUri Token URI that would be stored in the RFT properties584 #[solidity(hide, rename_selector = "mintWithTokenURI")]585 #[weight(<SelfWeightOf<T>>::create_item())]586 fn mint_with_token_uri_check_id(587 &mut self,588 caller: caller,589 to: address,590 token_id: uint256,591 token_uri: string,592 ) -> Result<bool> {593 let key = key::url();594 let permission = get_token_permission::<T>(self.id, &key)?;595 if !permission.collection_admin {596 return Err("Operation is not allowed".into());597 }598599 let caller = T::CrossAccountId::from_eth(caller);600 let to = T::CrossAccountId::from_eth(to);601 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;602 let budget = self603 .recorder604 .weight_calls_budget(<StructureWeight<T>>::find_parent());605606 if <TokensMinted<T>>::get(self.id)607 .checked_add(1)608 .ok_or("item id overflow")?609 != token_id610 {611 return Err("item id should be next".into());612 }613614 let mut properties = CollectionPropertiesVec::default();615 properties616 .try_push(Property {617 key,618 value: token_uri619 .into_bytes()620 .try_into()621 .map_err(|_| "token uri is too long")?,622 })623 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;624625 let users = [(to.clone(), 1)]626 .into_iter()627 .collect::<BTreeMap<_, _>>()628 .try_into()629 .unwrap();630 <Pallet<T>>::create_item(631 self,632 &caller,633 CreateItemData::<T::CrossAccountId> { users, properties },634 &budget,635 )636 .map_err(dispatch_to_evm::<T>)?;637 Ok(true)638 }639640 /// @dev Not implemented641 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {642 Err("not implementable".into())643 }644}645646fn get_token_property<T: Config>(647 collection: &CollectionHandle<T>,648 token_id: u32,649 key: &up_data_structs::PropertyKey,650) -> Result<string> {651 collection.consume_store_reads(1)?;652 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))653 .map_err(|_| Error::Revert("Token properties not found".into()))?;654 if let Some(property) = properties.get(key) {655 return Ok(string::from_utf8_lossy(property).into());656 }657658 Err("Property tokenURI not found".into())659}660661fn get_token_permission<T: Config>(662 collection_id: CollectionId,663 key: &PropertyKey,664) -> Result<PropertyPermission> {665 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)666 .map_err(|_| Error::Revert("No permissions for collection".into()))?;667 let a = token_property_permissions668 .get(key)669 .map(Clone::clone)670 .ok_or_else(|| {671 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();672 Error::Revert(alloc::format!("No permission for key {}", key))673 })?;674 Ok(a)675}676677/// @title Unique extensions for ERC721.678#[solidity_interface(name = ERC721UniqueExtensions)]679impl<T: Config> RefungibleHandle<T>680where681 T::AccountId: From<[u8; 32]>,682{683 /// @notice A descriptive name for a collection of NFTs in this contract684 fn name(&self) -> Result<string> {685 Ok(decode_utf16(self.name.iter().copied())686 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))687 .collect::<string>())688 }689690 /// @notice An abbreviated name for NFTs in this contract691 fn symbol(&self) -> Result<string> {692 Ok(string::from_utf8_lossy(&self.token_prefix).into())693 }694695 /// @notice Transfer ownership of an RFT696 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`697 /// is the zero address. Throws if `tokenId` is not a valid RFT.698 /// Throws if RFT pieces have multiple owners.699 /// @param to The new owner700 /// @param tokenId The RFT to transfer701 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]702 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {703 let caller = T::CrossAccountId::from_eth(caller);704 let to = T::CrossAccountId::from_eth(to);705 let token = token_id.try_into()?;706 let budget = self707 .recorder708 .weight_calls_budget(<StructureWeight<T>>::find_parent());709710 let balance = balance(self, token, &caller)?;711 ensure_single_owner(self, token, balance)?;712713 <Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)714 .map_err(dispatch_to_evm::<T>)?;715 Ok(())716 }717718 /// @notice Transfer ownership of an RFT719 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`720 /// is the zero address. Throws if `tokenId` is not a valid RFT.721 /// Throws if RFT pieces have multiple owners.722 /// @param to The new owner723 /// @param tokenId The RFT to transfer724 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]725 fn transfer_from_cross(726 &mut self,727 caller: caller,728 from: (address, uint256),729 to: (address, uint256),730 token_id: uint256,731 ) -> Result<void> {732 let caller = T::CrossAccountId::from_eth(caller);733 let from = convert_tuple_to_cross_account::<T>(from)?;734 let to = convert_tuple_to_cross_account::<T>(to)?;735 let token_id = token_id.try_into()?;736 let budget = self737 .recorder738 .weight_calls_budget(<StructureWeight<T>>::find_parent());739740 let balance = balance(self, token_id, &from)?;741 ensure_single_owner(self, token_id, balance)?;742743 Pallet::<T>::transfer_from(self, &caller, &from, &to, token_id, balance, &budget)744 .map_err(dispatch_to_evm::<T>)?;745 Ok(())746 }747748 /// @notice Burns a specific ERC721 token.749 /// @dev Throws unless `msg.sender` is the current owner or an authorized750 /// operator for this RFT. Throws if `from` is not the current owner. Throws751 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.752 /// Throws if RFT pieces have multiple owners.753 /// @param from The current owner of the RFT754 /// @param tokenId The RFT to transfer755 #[weight(<SelfWeightOf<T>>::burn_from())]756 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {757 let caller = T::CrossAccountId::from_eth(caller);758 let from = T::CrossAccountId::from_eth(from);759 let token = token_id.try_into()?;760 let budget = self761 .recorder762 .weight_calls_budget(<StructureWeight<T>>::find_parent());763764 let balance = balance(self, token, &from)?;765 ensure_single_owner(self, token, balance)?;766767 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)768 .map_err(dispatch_to_evm::<T>)?;769 Ok(())770 }771772 /// @notice Burns a specific ERC721 token.773 /// @dev Throws unless `msg.sender` is the current owner or an authorized774 /// operator for this RFT. Throws if `from` is not the current owner. Throws775 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.776 /// Throws if RFT pieces have multiple owners.777 /// @param from The current owner of the RFT778 /// @param tokenId The RFT to transfer779 #[weight(<SelfWeightOf<T>>::burn_from())]780 fn burn_from_cross(781 &mut self,782 caller: caller,783 from: (address, uint256),784 token_id: uint256,785 ) -> Result<void> {786 let caller = T::CrossAccountId::from_eth(caller);787 let from = convert_tuple_to_cross_account::<T>(from)?;788 let token = token_id.try_into()?;789 let budget = self790 .recorder791 .weight_calls_budget(<StructureWeight<T>>::find_parent());792793 let balance = balance(self, token, &from)?;794 ensure_single_owner(self, token, balance)?;795796 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)797 .map_err(dispatch_to_evm::<T>)?;798 Ok(())799 }800801 /// @notice Returns next free RFT ID.802 fn next_token_id(&self) -> Result<uint256> {803 self.consume_store_reads(1)?;804 Ok(<TokensMinted<T>>::get(self.id)805 .checked_add(1)806 .ok_or("item id overflow")?807 .into())808 }809810 /// @notice Function to mint multiple tokens.811 /// @dev `tokenIds` should be an array of consecutive numbers and first number812 /// should be obtained with `nextTokenId` method813 /// @param to The new owner814 /// @param tokenIds IDs of the minted RFTs815 #[solidity(hide)]816 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]817 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {818 let caller = T::CrossAccountId::from_eth(caller);819 let to = T::CrossAccountId::from_eth(to);820 let mut expected_index = <TokensMinted<T>>::get(self.id)821 .checked_add(1)822 .ok_or("item id overflow")?;823 let budget = self824 .recorder825 .weight_calls_budget(<StructureWeight<T>>::find_parent());826827 let total_tokens = token_ids.len();828 for id in token_ids.into_iter() {829 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;830 if id != expected_index {831 return Err("item id should be next".into());832 }833 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;834 }835 let users = [(to.clone(), 1)]836 .into_iter()837 .collect::<BTreeMap<_, _>>()838 .try_into()839 .unwrap();840 let create_item_data = CreateItemData::<T::CrossAccountId> {841 users,842 properties: CollectionPropertiesVec::default(),843 };844 let data = (0..total_tokens)845 .map(|_| create_item_data.clone())846 .collect();847848 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)849 .map_err(dispatch_to_evm::<T>)?;850 Ok(true)851 }852853 /// @notice Function to mint multiple tokens with the given tokenUris.854 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive855 /// numbers and first number should be obtained with `nextTokenId` method856 /// @param to The new owner857 /// @param tokens array of pairs of token ID and token URI for minted tokens858 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]859 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]860 fn mint_bulk_with_token_uri(861 &mut self,862 caller: caller,863 to: address,864 tokens: Vec<(uint256, string)>,865 ) -> Result<bool> {866 let key = key::url();867 let caller = T::CrossAccountId::from_eth(caller);868 let to = T::CrossAccountId::from_eth(to);869 let mut expected_index = <TokensMinted<T>>::get(self.id)870 .checked_add(1)871 .ok_or("item id overflow")?;872 let budget = self873 .recorder874 .weight_calls_budget(<StructureWeight<T>>::find_parent());875876 let mut data = Vec::with_capacity(tokens.len());877 let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]878 .into_iter()879 .collect::<BTreeMap<_, _>>()880 .try_into()881 .unwrap();882 for (id, token_uri) in tokens {883 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;884 if id != expected_index {885 return Err("item id should be next".into());886 }887 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;888889 let mut properties = CollectionPropertiesVec::default();890 properties891 .try_push(Property {892 key: key.clone(),893 value: token_uri894 .into_bytes()895 .try_into()896 .map_err(|_| "token uri is too long")?,897 })898 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;899900 let create_item_data = CreateItemData::<T::CrossAccountId> {901 users: users.clone(),902 properties,903 };904 data.push(create_item_data);905 }906907 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)908 .map_err(dispatch_to_evm::<T>)?;909 Ok(true)910 }911912 /// Returns EVM address for refungible token913 ///914 /// @param token ID of the token915 fn token_contract_address(&self, token: uint256) -> Result<address> {916 Ok(T::EvmTokenAddressMapping::token_to_address(917 self.id,918 token.try_into().map_err(|_| "token id overflow")?,919 ))920 }921}922923#[solidity_interface(924 name = UniqueRefungible,925 is(926 ERC721,927 ERC721Enumerable,928 ERC721UniqueExtensions,929 ERC721UniqueMintable,930 ERC721Burnable,931 ERC721Metadata(if(this.flags.erc721metadata)),932 Collection(via(common_mut returns CollectionHandle<T>)),933 TokenProperties,934 )935)]936impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}937938// Not a tests, but code generators939generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);940generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);941942impl<T: Config> CommonEvmHandler for RefungibleHandle<T>943where944 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,945{946 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");947 fn call(948 self,949 handle: &mut impl PrecompileHandle,950 ) -> Option<pallet_common::erc::PrecompileResult> {951 call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)952 }953}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);