difftreelog
refactor move erc721metadata to flags
in: master
6 files changed
pallets/common/src/erc.rsdiffbeforeafterboth--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -706,9 +706,6 @@
/// Value "ERC721Metadata".
pub const ERC721_METADATA: &[u8] = b"ERC721Metadata";
- /// Value "1" ERC721 metadata supported.
- pub const ERC721_METADATA_SUPPORTED: &[u8] = b"1";
-
/// Value for [`ERC721_METADATA`].
pub fn erc721() -> up_data_structs::PropertyValue {
property_value_from_bytes(ERC721_METADATA).expect(EXPECT_CONVERT_ERROR)
@@ -717,11 +714,6 @@
/// Value for [`SCHEMA_VERSION`].
pub fn schema_version() -> up_data_structs::PropertyValue {
property_value_from_bytes(SCHEMA_VERSION).expect(EXPECT_CONVERT_ERROR)
- }
-
- /// Value for [`ERC721_METADATA_SUPPORTED`].
- pub fn erc721_metadata_supported() -> up_data_structs::PropertyValue {
- property_value_from_bytes(ERC721_METADATA_SUPPORTED).expect(EXPECT_CONVERT_ERROR)
}
}
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -71,6 +71,7 @@
Collection,
RpcCollection,
CollectionFlags,
+ RpcCollectionFlags,
CollectionId,
CreateItemData,
MAX_TOKEN_PREFIX_LENGTH,
@@ -824,7 +825,11 @@
token_property_permissions,
properties,
read_only: flags.external,
- foreign: flags.foreign,
+
+ flags: RpcCollectionFlags {
+ foreign: flags.foreign,
+ erc721metadata: flags.erc721metadata,
+ },
})
}
}
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -708,22 +708,6 @@
}
}
-impl<T: Config> NonfungibleHandle<T> {
- pub fn supports_metadata(&self) -> bool {
- let has_metadata_support_enabled = if let Some(erc721_metadata) =
- pallet_common::Pallet::<T>::get_collection_property(self.id, &key::erc721_metadata())
- {
- *erc721_metadata.into_inner() == *value::ERC721_METADATA_SUPPORTED
- } else {
- false
- };
-
- let has_url_property_permissions = get_token_permission::<T>(self.id, &key::url()).is_ok();
-
- has_metadata_support_enabled && has_url_property_permissions
- }
-}
-
#[solidity_interface(
name = UniqueNFT,
is(
@@ -732,9 +716,9 @@
ERC721UniqueExtensions,
ERC721Mintable,
ERC721Burnable,
+ ERC721Metadata(if(this.flags.erc721metadata)),
Collection(via(common_mut returns CollectionHandle<T>)),
TokenProperties,
- ERC721Metadata(if(this.supports_metadata())),
)
)]
impl<T: Config> NonfungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}
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, static_property::value},33};34use pallet_evm::{account::CrossAccountId, PrecompileHandle};35use pallet_evm_coder_substrate::{call, dispatch_to_evm};36use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};37use sp_core::H160;38use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};39use up_data_structs::{40 CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,41 PropertyKeyPermission, PropertyPermission, TokenId,42};4344use crate::{45 AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,46 TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,47};4849pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5051/// @title A contract that allows to set and delete token properties and change token property permissions.52#[solidity_interface(name = TokenProperties)]53impl<T: Config> RefungibleHandle<T> {54 /// @notice Set permissions for token property.55 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.56 /// @param key Property key.57 /// @param isMutable Permission to mutate property.58 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.59 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.60 fn set_token_property_permission(61 &mut self,62 caller: caller,63 key: string,64 is_mutable: bool,65 collection_admin: bool,66 token_owner: bool,67 ) -> Result<()> {68 let caller = T::CrossAccountId::from_eth(caller);69 <Pallet<T>>::set_token_property_permissions(70 self,71 &caller,72 vec![PropertyKeyPermission {73 key: <Vec<u8>>::from(key)74 .try_into()75 .map_err(|_| "too long key")?,76 permission: PropertyPermission {77 mutable: is_mutable,78 collection_admin,79 token_owner,80 },81 }],82 )83 .map_err(dispatch_to_evm::<T>)84 }8586 /// @notice Set token property value.87 /// @dev Throws error if `msg.sender` has no permission to edit the property.88 /// @param tokenId ID of the token.89 /// @param key Property key.90 /// @param value Property value.91 fn set_property(92 &mut self,93 caller: caller,94 token_id: uint256,95 key: string,96 value: bytes,97 ) -> Result<()> {98 let caller = T::CrossAccountId::from_eth(caller);99 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;100 let key = <Vec<u8>>::from(key)101 .try_into()102 .map_err(|_| "key too long")?;103 let value = value.try_into().map_err(|_| "value too long")?;104105 let nesting_budget = self106 .recorder107 .weight_calls_budget(<StructureWeight<T>>::find_parent());108109 <Pallet<T>>::set_token_property(110 self,111 &caller,112 TokenId(token_id),113 Property { key, value },114 &nesting_budget,115 )116 .map_err(dispatch_to_evm::<T>)117 }118119 /// @notice Delete token property value.120 /// @dev Throws error if `msg.sender` has no permission to edit the property.121 /// @param tokenId ID of the token.122 /// @param key Property key.123 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {124 let caller = T::CrossAccountId::from_eth(caller);125 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;126 let key = <Vec<u8>>::from(key)127 .try_into()128 .map_err(|_| "key too long")?;129130 let nesting_budget = self131 .recorder132 .weight_calls_budget(<StructureWeight<T>>::find_parent());133134 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)135 .map_err(dispatch_to_evm::<T>)136 }137138 /// @notice Get token property value.139 /// @dev Throws error if key not found140 /// @param tokenId ID of the token.141 /// @param key Property key.142 /// @return Property value bytes143 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {144 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;145 let key = <Vec<u8>>::from(key)146 .try_into()147 .map_err(|_| "key too long")?;148149 let props = <TokenProperties<T>>::get((self.id, token_id));150 let prop = props.get(&key).ok_or("key not found")?;151152 Ok(prop.to_vec())153 }154}155156#[derive(ToLog)]157pub enum ERC721Events {158 /// @dev This event emits when NFTs are created (`from` == 0) and destroyed159 /// (`to` == 0). Exception: during contract creation, any number of RFTs160 /// may be created and assigned without emitting Transfer.161 Transfer {162 #[indexed]163 from: address,164 #[indexed]165 to: address,166 #[indexed]167 token_id: uint256,168 },169 /// @dev Not supported170 Approval {171 #[indexed]172 owner: address,173 #[indexed]174 approved: address,175 #[indexed]176 token_id: uint256,177 },178 /// @dev Not supported179 #[allow(dead_code)]180 ApprovalForAll {181 #[indexed]182 owner: address,183 #[indexed]184 operator: address,185 approved: bool,186 },187}188189#[derive(ToLog)]190pub enum ERC721MintableEvents {191 /// @dev Not supported192 #[allow(dead_code)]193 MintingFinished {},194}195196#[solidity_interface(name = ERC721Metadata)]197impl<T: Config> RefungibleHandle<T> {198 /// @notice A descriptive name for a collection of RFTs in this contract199 fn name(&self) -> Result<string> {200 Ok(decode_utf16(self.name.iter().copied())201 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))202 .collect::<string>())203 }204205 /// @notice An abbreviated name for RFTs in this contract206 fn symbol(&self) -> Result<string> {207 Ok(string::from_utf8_lossy(&self.token_prefix).into())208 }209210 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.211 ///212 /// @dev If the token has a `url` property and it is not empty, it is returned.213 /// 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`.214 /// If the collection property `baseURI` is empty or absent, return "" (empty string)215 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix216 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).217 ///218 /// @return token's const_metadata219 #[solidity(rename_selector = "tokenURI")]220 fn token_uri(&self, token_id: uint256) -> Result<string> {221 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;222223 match get_token_property(self, token_id_u32, &key::url()).as_deref() {224 Err(_) | Ok("") => (),225 Ok(url) => {226 return Ok(url.into());227 }228 };229230 let base_uri =231 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())232 .map(BoundedVec::into_inner)233 .map(string::from_utf8)234 .transpose()235 .map_err(|e| {236 Error::Revert(alloc::format!(237 "Can not convert value \"baseURI\" to string with error \"{}\"",238 e239 ))240 })?;241242 let base_uri = match base_uri.as_deref() {243 None | Some("") => {244 return Ok("".into());245 }246 Some(base_uri) => base_uri.into(),247 };248249 Ok(250 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {251 Err(_) | Ok("") => base_uri,252 Ok(suffix) => base_uri + suffix,253 },254 )255 }256}257258/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension259/// @dev See https://eips.ethereum.org/EIPS/eip-721260#[solidity_interface(name = ERC721Enumerable)]261impl<T: Config> RefungibleHandle<T> {262 /// @notice Enumerate valid RFTs263 /// @param index A counter less than `totalSupply()`264 /// @return The token identifier for the `index`th NFT,265 /// (sort order not specified)266 fn token_by_index(&self, index: uint256) -> Result<uint256> {267 Ok(index)268 }269270 /// Not implemented271 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {272 // TODO: Not implemetable273 Err("not implemented".into())274 }275276 /// @notice Count RFTs tracked by this contract277 /// @return A count of valid RFTs tracked by this contract, where each one of278 /// them has an assigned and queryable owner not equal to the zero address279 fn total_supply(&self) -> Result<uint256> {280 self.consume_store_reads(1)?;281 Ok(<Pallet<T>>::total_supply(self).into())282 }283}284285/// @title ERC-721 Non-Fungible Token Standard286/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md287#[solidity_interface(name = ERC721, events(ERC721Events))]288impl<T: Config> RefungibleHandle<T> {289 /// @notice Count all RFTs assigned to an owner290 /// @dev RFTs assigned to the zero address are considered invalid, and this291 /// function throws for queries about the zero address.292 /// @param owner An address for whom to query the balance293 /// @return The number of RFTs owned by `owner`, possibly zero294 fn balance_of(&self, owner: address) -> Result<uint256> {295 self.consume_store_reads(1)?;296 let owner = T::CrossAccountId::from_eth(owner);297 let balance = <AccountBalance<T>>::get((self.id, owner));298 Ok(balance.into())299 }300301 /// @notice Find the owner of an RFT302 /// @dev RFTs assigned to zero address are considered invalid, and queries303 /// about them do throw.304 /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for305 /// the tokens that are partially owned.306 /// @param tokenId The identifier for an RFT307 /// @return The address of the owner of the RFT308 fn owner_of(&self, token_id: uint256) -> Result<address> {309 self.consume_store_reads(2)?;310 let token = token_id.try_into()?;311 let owner = <Pallet<T>>::token_owner(self.id, token);312 Ok(owner313 .map(|address| *address.as_eth())314 .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))315 }316317 /// @dev Not implemented318 fn safe_transfer_from_with_data(319 &mut self,320 _from: address,321 _to: address,322 _token_id: uint256,323 _data: bytes,324 ) -> Result<void> {325 // TODO: Not implemetable326 Err("not implemented".into())327 }328329 /// @dev Not implemented330 fn safe_transfer_from(331 &mut self,332 _from: address,333 _to: address,334 _token_id: uint256,335 ) -> Result<void> {336 // TODO: Not implemetable337 Err("not implemented".into())338 }339340 /// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE341 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE342 /// THEY MAY BE PERMANENTLY LOST343 /// @dev Throws unless `msg.sender` is the current owner or an authorized344 /// operator for this RFT. Throws if `from` is not the current owner. Throws345 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.346 /// Throws if RFT pieces have multiple owners.347 /// @param from The current owner of the NFT348 /// @param to The new owner349 /// @param tokenId The NFT to transfer350 #[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]351 fn transfer_from(352 &mut self,353 caller: caller,354 from: address,355 to: address,356 token_id: uint256,357 ) -> Result<void> {358 let caller = T::CrossAccountId::from_eth(caller);359 let from = T::CrossAccountId::from_eth(from);360 let to = T::CrossAccountId::from_eth(to);361 let token = token_id.try_into()?;362 let budget = self363 .recorder364 .weight_calls_budget(<StructureWeight<T>>::find_parent());365366 let balance = balance(&self, token, &from)?;367 ensure_single_owner(&self, token, balance)?;368369 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)370 .map_err(dispatch_to_evm::<T>)?;371372 Ok(())373 }374375 /// @dev Not implemented376 fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {377 Err("not implemented".into())378 }379380 /// @dev Not implemented381 fn set_approval_for_all(382 &mut self,383 _caller: caller,384 _operator: address,385 _approved: bool,386 ) -> Result<void> {387 // TODO: Not implemetable388 Err("not implemented".into())389 }390391 /// @dev Not implemented392 fn get_approved(&self, _token_id: uint256) -> Result<address> {393 // TODO: Not implemetable394 Err("not implemented".into())395 }396397 /// @dev Not implemented398 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {399 // TODO: Not implemetable400 Err("not implemented".into())401 }402}403404/// Returns amount of pieces of `token` that `owner` have405pub fn balance<T: Config>(406 collection: &RefungibleHandle<T>,407 token: TokenId,408 owner: &T::CrossAccountId,409) -> Result<u128> {410 collection.consume_store_reads(1)?;411 let balance = <Balance<T>>::get((collection.id, token, &owner));412 Ok(balance)413}414415/// Throws if `owner_balance` is lower than total amount of `token` pieces416pub fn ensure_single_owner<T: Config>(417 collection: &RefungibleHandle<T>,418 token: TokenId,419 owner_balance: u128,420) -> Result<()> {421 collection.consume_store_reads(1)?;422 let total_supply = <TotalSupply<T>>::get((collection.id, token));423 if total_supply != owner_balance {424 return Err("token has multiple owners".into());425 }426 Ok(())427}428429/// @title ERC721 Token that can be irreversibly burned (destroyed).430#[solidity_interface(name = ERC721Burnable)]431impl<T: Config> RefungibleHandle<T> {432 /// @notice Burns a specific ERC721 token.433 /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized434 /// operator of the current owner.435 /// @param tokenId The RFT to approve436 #[weight(<SelfWeightOf<T>>::burn_item_fully())]437 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {438 let caller = T::CrossAccountId::from_eth(caller);439 let token = token_id.try_into()?;440441 let balance = balance(&self, token, &caller)?;442 ensure_single_owner(&self, token, balance)?;443444 <Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;445 Ok(())446 }447}448449/// @title ERC721 minting logic.450#[solidity_interface(name = ERC721Mintable, events(ERC721MintableEvents))]451impl<T: Config> RefungibleHandle<T> {452 fn minting_finished(&self) -> Result<bool> {453 Ok(false)454 }455456 /// @notice Function to mint token.457 /// @dev `tokenId` should be obtained with `nextTokenId` method,458 /// unlike standard, you can't specify it manually459 /// @param to The new owner460 /// @param tokenId ID of the minted RFT461 #[weight(<SelfWeightOf<T>>::create_item())]462 fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {463 let caller = T::CrossAccountId::from_eth(caller);464 let to = T::CrossAccountId::from_eth(to);465 let token_id: u32 = token_id.try_into()?;466 let budget = self467 .recorder468 .weight_calls_budget(<StructureWeight<T>>::find_parent());469470 if <TokensMinted<T>>::get(self.id)471 .checked_add(1)472 .ok_or("item id overflow")?473 != token_id474 {475 return Err("item id should be next".into());476 }477478 let users = [(to.clone(), 1)]479 .into_iter()480 .collect::<BTreeMap<_, _>>()481 .try_into()482 .unwrap();483 <Pallet<T>>::create_item(484 self,485 &caller,486 CreateItemData::<T::CrossAccountId> {487 users,488 properties: CollectionPropertiesVec::default(),489 },490 &budget,491 )492 .map_err(dispatch_to_evm::<T>)?;493494 Ok(true)495 }496497 /// @notice Function to mint token with the given tokenUri.498 /// @dev `tokenId` should be obtained with `nextTokenId` method,499 /// unlike standard, you can't specify it manually500 /// @param to The new owner501 /// @param tokenId ID of the minted RFT502 /// @param tokenUri Token URI that would be stored in the RFT properties503 #[solidity(rename_selector = "mintWithTokenURI")]504 #[weight(<SelfWeightOf<T>>::create_item())]505 fn mint_with_token_uri(506 &mut self,507 caller: caller,508 to: address,509 token_id: uint256,510 token_uri: string,511 ) -> Result<bool> {512 let key = key::url();513 let permission = get_token_permission::<T>(self.id, &key)?;514 if !permission.collection_admin {515 return Err("Operation is not allowed".into());516 }517518 let caller = T::CrossAccountId::from_eth(caller);519 let to = T::CrossAccountId::from_eth(to);520 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;521 let budget = self522 .recorder523 .weight_calls_budget(<StructureWeight<T>>::find_parent());524525 if <TokensMinted<T>>::get(self.id)526 .checked_add(1)527 .ok_or("item id overflow")?528 != token_id529 {530 return Err("item id should be next".into());531 }532533 let mut properties = CollectionPropertiesVec::default();534 properties535 .try_push(Property {536 key,537 value: token_uri538 .into_bytes()539 .try_into()540 .map_err(|_| "token uri is too long")?,541 })542 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;543544 let users = [(to.clone(), 1)]545 .into_iter()546 .collect::<BTreeMap<_, _>>()547 .try_into()548 .unwrap();549 <Pallet<T>>::create_item(550 self,551 &caller,552 CreateItemData::<T::CrossAccountId> { users, properties },553 &budget,554 )555 .map_err(dispatch_to_evm::<T>)?;556 Ok(true)557 }558559 /// @dev Not implemented560 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {561 Err("not implementable".into())562 }563}564565fn get_token_property<T: Config>(566 collection: &CollectionHandle<T>,567 token_id: u32,568 key: &up_data_structs::PropertyKey,569) -> Result<string> {570 collection.consume_store_reads(1)?;571 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))572 .map_err(|_| Error::Revert("Token properties not found".into()))?;573 if let Some(property) = properties.get(key) {574 return Ok(string::from_utf8_lossy(property).into());575 }576577 Err("Property tokenURI not found".into())578}579580fn get_token_permission<T: Config>(581 collection_id: CollectionId,582 key: &PropertyKey,583) -> Result<PropertyPermission> {584 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)585 .map_err(|_| Error::Revert("No permissions for collection".into()))?;586 let a = token_property_permissions587 .get(key)588 .map(Clone::clone)589 .ok_or_else(|| {590 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();591 Error::Revert(alloc::format!("No permission for key {}", key))592 })?;593 Ok(a)594}595596/// @title Unique extensions for ERC721.597#[solidity_interface(name = ERC721UniqueExtensions)]598impl<T: Config> RefungibleHandle<T> {599 /// @notice Transfer ownership of an RFT600 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`601 /// is the zero address. Throws if `tokenId` is not a valid RFT.602 /// Throws if RFT pieces have multiple owners.603 /// @param to The new owner604 /// @param tokenId The RFT to transfer605 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]606 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {607 let caller = T::CrossAccountId::from_eth(caller);608 let to = T::CrossAccountId::from_eth(to);609 let token = token_id.try_into()?;610 let budget = self611 .recorder612 .weight_calls_budget(<StructureWeight<T>>::find_parent());613614 let balance = balance(&self, token, &caller)?;615 ensure_single_owner(&self, token, balance)?;616617 <Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)618 .map_err(dispatch_to_evm::<T>)?;619 Ok(())620 }621622 /// @notice Burns a specific ERC721 token.623 /// @dev Throws unless `msg.sender` is the current owner or an authorized624 /// operator for this RFT. Throws if `from` is not the current owner. Throws625 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.626 /// Throws if RFT pieces have multiple owners.627 /// @param from The current owner of the RFT628 /// @param tokenId The RFT to transfer629 #[weight(<SelfWeightOf<T>>::burn_from())]630 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {631 let caller = T::CrossAccountId::from_eth(caller);632 let from = T::CrossAccountId::from_eth(from);633 let token = token_id.try_into()?;634 let budget = self635 .recorder636 .weight_calls_budget(<StructureWeight<T>>::find_parent());637638 let balance = balance(&self, token, &caller)?;639 ensure_single_owner(&self, token, balance)?;640641 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)642 .map_err(dispatch_to_evm::<T>)?;643 Ok(())644 }645646 /// @notice Returns next free RFT ID.647 fn next_token_id(&self) -> Result<uint256> {648 self.consume_store_reads(1)?;649 Ok(<TokensMinted<T>>::get(self.id)650 .checked_add(1)651 .ok_or("item id overflow")?652 .into())653 }654655 /// @notice Function to mint multiple tokens.656 /// @dev `tokenIds` should be an array of consecutive numbers and first number657 /// should be obtained with `nextTokenId` method658 /// @param to The new owner659 /// @param tokenIds IDs of the minted RFTs660 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]661 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {662 let caller = T::CrossAccountId::from_eth(caller);663 let to = T::CrossAccountId::from_eth(to);664 let mut expected_index = <TokensMinted<T>>::get(self.id)665 .checked_add(1)666 .ok_or("item id overflow")?;667 let budget = self668 .recorder669 .weight_calls_budget(<StructureWeight<T>>::find_parent());670671 let total_tokens = token_ids.len();672 for id in token_ids.into_iter() {673 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;674 if id != expected_index {675 return Err("item id should be next".into());676 }677 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;678 }679 let users = [(to.clone(), 1)]680 .into_iter()681 .collect::<BTreeMap<_, _>>()682 .try_into()683 .unwrap();684 let create_item_data = CreateItemData::<T::CrossAccountId> {685 users,686 properties: CollectionPropertiesVec::default(),687 };688 let data = (0..total_tokens)689 .map(|_| create_item_data.clone())690 .collect();691692 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)693 .map_err(dispatch_to_evm::<T>)?;694 Ok(true)695 }696697 /// @notice Function to mint multiple tokens with the given tokenUris.698 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive699 /// numbers and first number should be obtained with `nextTokenId` method700 /// @param to The new owner701 /// @param tokens array of pairs of token ID and token URI for minted tokens702 #[solidity(rename_selector = "mintBulkWithTokenURI")]703 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]704 fn mint_bulk_with_token_uri(705 &mut self,706 caller: caller,707 to: address,708 tokens: Vec<(uint256, string)>,709 ) -> Result<bool> {710 let key = key::url();711 let caller = T::CrossAccountId::from_eth(caller);712 let to = T::CrossAccountId::from_eth(to);713 let mut expected_index = <TokensMinted<T>>::get(self.id)714 .checked_add(1)715 .ok_or("item id overflow")?;716 let budget = self717 .recorder718 .weight_calls_budget(<StructureWeight<T>>::find_parent());719720 let mut data = Vec::with_capacity(tokens.len());721 let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]722 .into_iter()723 .collect::<BTreeMap<_, _>>()724 .try_into()725 .unwrap();726 for (id, token_uri) in tokens {727 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;728 if id != expected_index {729 return Err("item id should be next".into());730 }731 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;732733 let mut properties = CollectionPropertiesVec::default();734 properties735 .try_push(Property {736 key: key.clone(),737 value: token_uri738 .into_bytes()739 .try_into()740 .map_err(|_| "token uri is too long")?,741 })742 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;743744 let create_item_data = CreateItemData::<T::CrossAccountId> {745 users: users.clone(),746 properties,747 };748 data.push(create_item_data);749 }750751 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)752 .map_err(dispatch_to_evm::<T>)?;753 Ok(true)754 }755756 /// Returns EVM address for refungible token757 ///758 /// @param token ID of the token759 fn token_contract_address(&self, token: uint256) -> Result<address> {760 Ok(T::EvmTokenAddressMapping::token_to_address(761 self.id,762 token.try_into().map_err(|_| "token id overflow")?,763 ))764 }765}766767impl<T: Config> RefungibleHandle<T> {768 pub fn supports_metadata(&self) -> bool {769 let has_metadata_support_enabled = if let Some(erc721_metadata) =770 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::erc721_metadata())771 {772 *erc721_metadata.into_inner() == *value::ERC721_METADATA_SUPPORTED773 } else {774 false775 };776777 let has_url_property_permissions = get_token_permission::<T>(self.id, &key::url()).is_ok();778779 has_metadata_support_enabled && has_url_property_permissions780 }781}782783#[solidity_interface(784 name = UniqueRefungible,785 is(786 ERC721,787 ERC721Enumerable,788 ERC721UniqueExtensions,789 ERC721Mintable,790 ERC721Burnable,791 Collection(via(common_mut returns CollectionHandle<T>)),792 TokenProperties,793 ERC721Metadata(if(this.supports_metadata())),794 )795)]796impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}797798// Not a tests, but code generators799generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);800generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);801802impl<T: Config> CommonEvmHandler for RefungibleHandle<T>803where804 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,805{806 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");807 fn call(808 self,809 handle: &mut impl PrecompileHandle,810 ) -> Option<pallet_common::erc::PrecompileResult> {811 call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)812 }813}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, static_property::value},33};34use pallet_evm::{account::CrossAccountId, PrecompileHandle};35use pallet_evm_coder_substrate::{call, dispatch_to_evm};36use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};37use sp_core::H160;38use sp_std::{collections::btree_map::BTreeMap, vec::Vec, vec};39use up_data_structs::{40 CollectionId, CollectionPropertiesVec, mapping::TokenAddressMapping, Property, PropertyKey,41 PropertyKeyPermission, PropertyPermission, TokenId,42};4344use crate::{45 AccountBalance, Balance, Config, CreateItemData, Pallet, RefungibleHandle, SelfWeightOf,46 TokenProperties, TokensMinted, TotalSupply, weights::WeightInfo,47};4849pub const ADDRESS_FOR_PARTIALLY_OWNED_TOKENS: H160 = H160::repeat_byte(0xff);5051/// @title A contract that allows to set and delete token properties and change token property permissions.52#[solidity_interface(name = TokenProperties)]53impl<T: Config> RefungibleHandle<T> {54 /// @notice Set permissions for token property.55 /// @dev Throws error if `msg.sender` is not admin or owner of the collection.56 /// @param key Property key.57 /// @param isMutable Permission to mutate property.58 /// @param collectionAdmin Permission to mutate property by collection admin if property is mutable.59 /// @param tokenOwner Permission to mutate property by token owner if property is mutable.60 fn set_token_property_permission(61 &mut self,62 caller: caller,63 key: string,64 is_mutable: bool,65 collection_admin: bool,66 token_owner: bool,67 ) -> Result<()> {68 let caller = T::CrossAccountId::from_eth(caller);69 <Pallet<T>>::set_token_property_permissions(70 self,71 &caller,72 vec![PropertyKeyPermission {73 key: <Vec<u8>>::from(key)74 .try_into()75 .map_err(|_| "too long key")?,76 permission: PropertyPermission {77 mutable: is_mutable,78 collection_admin,79 token_owner,80 },81 }],82 )83 .map_err(dispatch_to_evm::<T>)84 }8586 /// @notice Set token property value.87 /// @dev Throws error if `msg.sender` has no permission to edit the property.88 /// @param tokenId ID of the token.89 /// @param key Property key.90 /// @param value Property value.91 fn set_property(92 &mut self,93 caller: caller,94 token_id: uint256,95 key: string,96 value: bytes,97 ) -> Result<()> {98 let caller = T::CrossAccountId::from_eth(caller);99 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;100 let key = <Vec<u8>>::from(key)101 .try_into()102 .map_err(|_| "key too long")?;103 let value = value.try_into().map_err(|_| "value too long")?;104105 let nesting_budget = self106 .recorder107 .weight_calls_budget(<StructureWeight<T>>::find_parent());108109 <Pallet<T>>::set_token_property(110 self,111 &caller,112 TokenId(token_id),113 Property { key, value },114 &nesting_budget,115 )116 .map_err(dispatch_to_evm::<T>)117 }118119 /// @notice Delete token property value.120 /// @dev Throws error if `msg.sender` has no permission to edit the property.121 /// @param tokenId ID of the token.122 /// @param key Property key.123 fn delete_property(&mut self, token_id: uint256, caller: caller, key: string) -> Result<()> {124 let caller = T::CrossAccountId::from_eth(caller);125 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;126 let key = <Vec<u8>>::from(key)127 .try_into()128 .map_err(|_| "key too long")?;129130 let nesting_budget = self131 .recorder132 .weight_calls_budget(<StructureWeight<T>>::find_parent());133134 <Pallet<T>>::delete_token_property(self, &caller, TokenId(token_id), key, &nesting_budget)135 .map_err(dispatch_to_evm::<T>)136 }137138 /// @notice Get token property value.139 /// @dev Throws error if key not found140 /// @param tokenId ID of the token.141 /// @param key Property key.142 /// @return Property value bytes143 fn property(&self, token_id: uint256, key: string) -> Result<bytes> {144 let token_id: u32 = token_id.try_into().map_err(|_| "token id overflow")?;145 let key = <Vec<u8>>::from(key)146 .try_into()147 .map_err(|_| "key too long")?;148149 let props = <TokenProperties<T>>::get((self.id, token_id));150 let prop = props.get(&key).ok_or("key not found")?;151152 Ok(prop.to_vec())153 }154}155156#[derive(ToLog)]157pub enum ERC721Events {158 /// @dev This event emits when NFTs are created (`from` == 0) and destroyed159 /// (`to` == 0). Exception: during contract creation, any number of RFTs160 /// may be created and assigned without emitting Transfer.161 Transfer {162 #[indexed]163 from: address,164 #[indexed]165 to: address,166 #[indexed]167 token_id: uint256,168 },169 /// @dev Not supported170 Approval {171 #[indexed]172 owner: address,173 #[indexed]174 approved: address,175 #[indexed]176 token_id: uint256,177 },178 /// @dev Not supported179 #[allow(dead_code)]180 ApprovalForAll {181 #[indexed]182 owner: address,183 #[indexed]184 operator: address,185 approved: bool,186 },187}188189#[derive(ToLog)]190pub enum ERC721MintableEvents {191 /// @dev Not supported192 #[allow(dead_code)]193 MintingFinished {},194}195196#[solidity_interface(name = ERC721Metadata)]197impl<T: Config> RefungibleHandle<T> {198 /// @notice A descriptive name for a collection of RFTs in this contract199 fn name(&self) -> Result<string> {200 Ok(decode_utf16(self.name.iter().copied())201 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))202 .collect::<string>())203 }204205 /// @notice An abbreviated name for RFTs in this contract206 fn symbol(&self) -> Result<string> {207 Ok(string::from_utf8_lossy(&self.token_prefix).into())208 }209210 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.211 ///212 /// @dev If the token has a `url` property and it is not empty, it is returned.213 /// 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`.214 /// If the collection property `baseURI` is empty or absent, return "" (empty string)215 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix216 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).217 ///218 /// @return token's const_metadata219 #[solidity(rename_selector = "tokenURI")]220 fn token_uri(&self, token_id: uint256) -> Result<string> {221 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;222223 match get_token_property(self, token_id_u32, &key::url()).as_deref() {224 Err(_) | Ok("") => (),225 Ok(url) => {226 return Ok(url.into());227 }228 };229230 let base_uri =231 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())232 .map(BoundedVec::into_inner)233 .map(string::from_utf8)234 .transpose()235 .map_err(|e| {236 Error::Revert(alloc::format!(237 "Can not convert value \"baseURI\" to string with error \"{}\"",238 e239 ))240 })?;241242 let base_uri = match base_uri.as_deref() {243 None | Some("") => {244 return Ok("".into());245 }246 Some(base_uri) => base_uri.into(),247 };248249 Ok(250 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {251 Err(_) | Ok("") => base_uri,252 Ok(suffix) => base_uri + suffix,253 },254 )255 }256}257258/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension259/// @dev See https://eips.ethereum.org/EIPS/eip-721260#[solidity_interface(name = ERC721Enumerable)]261impl<T: Config> RefungibleHandle<T> {262 /// @notice Enumerate valid RFTs263 /// @param index A counter less than `totalSupply()`264 /// @return The token identifier for the `index`th NFT,265 /// (sort order not specified)266 fn token_by_index(&self, index: uint256) -> Result<uint256> {267 Ok(index)268 }269270 /// Not implemented271 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {272 // TODO: Not implemetable273 Err("not implemented".into())274 }275276 /// @notice Count RFTs tracked by this contract277 /// @return A count of valid RFTs tracked by this contract, where each one of278 /// them has an assigned and queryable owner not equal to the zero address279 fn total_supply(&self) -> Result<uint256> {280 self.consume_store_reads(1)?;281 Ok(<Pallet<T>>::total_supply(self).into())282 }283}284285/// @title ERC-721 Non-Fungible Token Standard286/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md287#[solidity_interface(name = ERC721, events(ERC721Events))]288impl<T: Config> RefungibleHandle<T> {289 /// @notice Count all RFTs assigned to an owner290 /// @dev RFTs assigned to the zero address are considered invalid, and this291 /// function throws for queries about the zero address.292 /// @param owner An address for whom to query the balance293 /// @return The number of RFTs owned by `owner`, possibly zero294 fn balance_of(&self, owner: address) -> Result<uint256> {295 self.consume_store_reads(1)?;296 let owner = T::CrossAccountId::from_eth(owner);297 let balance = <AccountBalance<T>>::get((self.id, owner));298 Ok(balance.into())299 }300301 /// @notice Find the owner of an RFT302 /// @dev RFTs assigned to zero address are considered invalid, and queries303 /// about them do throw.304 /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for305 /// the tokens that are partially owned.306 /// @param tokenId The identifier for an RFT307 /// @return The address of the owner of the RFT308 fn owner_of(&self, token_id: uint256) -> Result<address> {309 self.consume_store_reads(2)?;310 let token = token_id.try_into()?;311 let owner = <Pallet<T>>::token_owner(self.id, token);312 Ok(owner313 .map(|address| *address.as_eth())314 .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))315 }316317 /// @dev Not implemented318 fn safe_transfer_from_with_data(319 &mut self,320 _from: address,321 _to: address,322 _token_id: uint256,323 _data: bytes,324 ) -> Result<void> {325 // TODO: Not implemetable326 Err("not implemented".into())327 }328329 /// @dev Not implemented330 fn safe_transfer_from(331 &mut self,332 _from: address,333 _to: address,334 _token_id: uint256,335 ) -> Result<void> {336 // TODO: Not implemetable337 Err("not implemented".into())338 }339340 /// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE341 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE342 /// THEY MAY BE PERMANENTLY LOST343 /// @dev Throws unless `msg.sender` is the current owner or an authorized344 /// operator for this RFT. Throws if `from` is not the current owner. Throws345 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.346 /// Throws if RFT pieces have multiple owners.347 /// @param from The current owner of the NFT348 /// @param to The new owner349 /// @param tokenId The NFT to transfer350 #[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]351 fn transfer_from(352 &mut self,353 caller: caller,354 from: address,355 to: address,356 token_id: uint256,357 ) -> Result<void> {358 let caller = T::CrossAccountId::from_eth(caller);359 let from = T::CrossAccountId::from_eth(from);360 let to = T::CrossAccountId::from_eth(to);361 let token = token_id.try_into()?;362 let budget = self363 .recorder364 .weight_calls_budget(<StructureWeight<T>>::find_parent());365366 let balance = balance(&self, token, &from)?;367 ensure_single_owner(&self, token, balance)?;368369 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)370 .map_err(dispatch_to_evm::<T>)?;371372 Ok(())373 }374375 /// @dev Not implemented376 fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {377 Err("not implemented".into())378 }379380 /// @dev Not implemented381 fn set_approval_for_all(382 &mut self,383 _caller: caller,384 _operator: address,385 _approved: bool,386 ) -> Result<void> {387 // TODO: Not implemetable388 Err("not implemented".into())389 }390391 /// @dev Not implemented392 fn get_approved(&self, _token_id: uint256) -> Result<address> {393 // TODO: Not implemetable394 Err("not implemented".into())395 }396397 /// @dev Not implemented398 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {399 // TODO: Not implemetable400 Err("not implemented".into())401 }402}403404/// Returns amount of pieces of `token` that `owner` have405pub fn balance<T: Config>(406 collection: &RefungibleHandle<T>,407 token: TokenId,408 owner: &T::CrossAccountId,409) -> Result<u128> {410 collection.consume_store_reads(1)?;411 let balance = <Balance<T>>::get((collection.id, token, &owner));412 Ok(balance)413}414415/// Throws if `owner_balance` is lower than total amount of `token` pieces416pub fn ensure_single_owner<T: Config>(417 collection: &RefungibleHandle<T>,418 token: TokenId,419 owner_balance: u128,420) -> Result<()> {421 collection.consume_store_reads(1)?;422 let total_supply = <TotalSupply<T>>::get((collection.id, token));423 if total_supply != owner_balance {424 return Err("token has multiple owners".into());425 }426 Ok(())427}428429/// @title ERC721 Token that can be irreversibly burned (destroyed).430#[solidity_interface(name = ERC721Burnable)]431impl<T: Config> RefungibleHandle<T> {432 /// @notice Burns a specific ERC721 token.433 /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized434 /// operator of the current owner.435 /// @param tokenId The RFT to approve436 #[weight(<SelfWeightOf<T>>::burn_item_fully())]437 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {438 let caller = T::CrossAccountId::from_eth(caller);439 let token = token_id.try_into()?;440441 let balance = balance(&self, token, &caller)?;442 ensure_single_owner(&self, token, balance)?;443444 <Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;445 Ok(())446 }447}448449/// @title ERC721 minting logic.450#[solidity_interface(name = ERC721Mintable, events(ERC721MintableEvents))]451impl<T: Config> RefungibleHandle<T> {452 fn minting_finished(&self) -> Result<bool> {453 Ok(false)454 }455456 /// @notice Function to mint token.457 /// @dev `tokenId` should be obtained with `nextTokenId` method,458 /// unlike standard, you can't specify it manually459 /// @param to The new owner460 /// @param tokenId ID of the minted RFT461 #[weight(<SelfWeightOf<T>>::create_item())]462 fn mint(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {463 let caller = T::CrossAccountId::from_eth(caller);464 let to = T::CrossAccountId::from_eth(to);465 let token_id: u32 = token_id.try_into()?;466 let budget = self467 .recorder468 .weight_calls_budget(<StructureWeight<T>>::find_parent());469470 if <TokensMinted<T>>::get(self.id)471 .checked_add(1)472 .ok_or("item id overflow")?473 != token_id474 {475 return Err("item id should be next".into());476 }477478 let users = [(to.clone(), 1)]479 .into_iter()480 .collect::<BTreeMap<_, _>>()481 .try_into()482 .unwrap();483 <Pallet<T>>::create_item(484 self,485 &caller,486 CreateItemData::<T::CrossAccountId> {487 users,488 properties: CollectionPropertiesVec::default(),489 },490 &budget,491 )492 .map_err(dispatch_to_evm::<T>)?;493494 Ok(true)495 }496497 /// @notice Function to mint token with the given tokenUri.498 /// @dev `tokenId` should be obtained with `nextTokenId` method,499 /// unlike standard, you can't specify it manually500 /// @param to The new owner501 /// @param tokenId ID of the minted RFT502 /// @param tokenUri Token URI that would be stored in the RFT properties503 #[solidity(rename_selector = "mintWithTokenURI")]504 #[weight(<SelfWeightOf<T>>::create_item())]505 fn mint_with_token_uri(506 &mut self,507 caller: caller,508 to: address,509 token_id: uint256,510 token_uri: string,511 ) -> Result<bool> {512 let key = key::url();513 let permission = get_token_permission::<T>(self.id, &key)?;514 if !permission.collection_admin {515 return Err("Operation is not allowed".into());516 }517518 let caller = T::CrossAccountId::from_eth(caller);519 let to = T::CrossAccountId::from_eth(to);520 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;521 let budget = self522 .recorder523 .weight_calls_budget(<StructureWeight<T>>::find_parent());524525 if <TokensMinted<T>>::get(self.id)526 .checked_add(1)527 .ok_or("item id overflow")?528 != token_id529 {530 return Err("item id should be next".into());531 }532533 let mut properties = CollectionPropertiesVec::default();534 properties535 .try_push(Property {536 key,537 value: token_uri538 .into_bytes()539 .try_into()540 .map_err(|_| "token uri is too long")?,541 })542 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;543544 let users = [(to.clone(), 1)]545 .into_iter()546 .collect::<BTreeMap<_, _>>()547 .try_into()548 .unwrap();549 <Pallet<T>>::create_item(550 self,551 &caller,552 CreateItemData::<T::CrossAccountId> { users, properties },553 &budget,554 )555 .map_err(dispatch_to_evm::<T>)?;556 Ok(true)557 }558559 /// @dev Not implemented560 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {561 Err("not implementable".into())562 }563}564565fn get_token_property<T: Config>(566 collection: &CollectionHandle<T>,567 token_id: u32,568 key: &up_data_structs::PropertyKey,569) -> Result<string> {570 collection.consume_store_reads(1)?;571 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))572 .map_err(|_| Error::Revert("Token properties not found".into()))?;573 if let Some(property) = properties.get(key) {574 return Ok(string::from_utf8_lossy(property).into());575 }576577 Err("Property tokenURI not found".into())578}579580fn get_token_permission<T: Config>(581 collection_id: CollectionId,582 key: &PropertyKey,583) -> Result<PropertyPermission> {584 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)585 .map_err(|_| Error::Revert("No permissions for collection".into()))?;586 let a = token_property_permissions587 .get(key)588 .map(Clone::clone)589 .ok_or_else(|| {590 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();591 Error::Revert(alloc::format!("No permission for key {}", key))592 })?;593 Ok(a)594}595596/// @title Unique extensions for ERC721.597#[solidity_interface(name = ERC721UniqueExtensions)]598impl<T: Config> RefungibleHandle<T> {599 /// @notice Transfer ownership of an RFT600 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`601 /// is the zero address. Throws if `tokenId` is not a valid RFT.602 /// Throws if RFT pieces have multiple owners.603 /// @param to The new owner604 /// @param tokenId The RFT to transfer605 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]606 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {607 let caller = T::CrossAccountId::from_eth(caller);608 let to = T::CrossAccountId::from_eth(to);609 let token = token_id.try_into()?;610 let budget = self611 .recorder612 .weight_calls_budget(<StructureWeight<T>>::find_parent());613614 let balance = balance(&self, token, &caller)?;615 ensure_single_owner(&self, token, balance)?;616617 <Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)618 .map_err(dispatch_to_evm::<T>)?;619 Ok(())620 }621622 /// @notice Burns a specific ERC721 token.623 /// @dev Throws unless `msg.sender` is the current owner or an authorized624 /// operator for this RFT. Throws if `from` is not the current owner. Throws625 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.626 /// Throws if RFT pieces have multiple owners.627 /// @param from The current owner of the RFT628 /// @param tokenId The RFT to transfer629 #[weight(<SelfWeightOf<T>>::burn_from())]630 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {631 let caller = T::CrossAccountId::from_eth(caller);632 let from = T::CrossAccountId::from_eth(from);633 let token = token_id.try_into()?;634 let budget = self635 .recorder636 .weight_calls_budget(<StructureWeight<T>>::find_parent());637638 let balance = balance(&self, token, &caller)?;639 ensure_single_owner(&self, token, balance)?;640641 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)642 .map_err(dispatch_to_evm::<T>)?;643 Ok(())644 }645646 /// @notice Returns next free RFT ID.647 fn next_token_id(&self) -> Result<uint256> {648 self.consume_store_reads(1)?;649 Ok(<TokensMinted<T>>::get(self.id)650 .checked_add(1)651 .ok_or("item id overflow")?652 .into())653 }654655 /// @notice Function to mint multiple tokens.656 /// @dev `tokenIds` should be an array of consecutive numbers and first number657 /// should be obtained with `nextTokenId` method658 /// @param to The new owner659 /// @param tokenIds IDs of the minted RFTs660 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]661 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {662 let caller = T::CrossAccountId::from_eth(caller);663 let to = T::CrossAccountId::from_eth(to);664 let mut expected_index = <TokensMinted<T>>::get(self.id)665 .checked_add(1)666 .ok_or("item id overflow")?;667 let budget = self668 .recorder669 .weight_calls_budget(<StructureWeight<T>>::find_parent());670671 let total_tokens = token_ids.len();672 for id in token_ids.into_iter() {673 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;674 if id != expected_index {675 return Err("item id should be next".into());676 }677 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;678 }679 let users = [(to.clone(), 1)]680 .into_iter()681 .collect::<BTreeMap<_, _>>()682 .try_into()683 .unwrap();684 let create_item_data = CreateItemData::<T::CrossAccountId> {685 users,686 properties: CollectionPropertiesVec::default(),687 };688 let data = (0..total_tokens)689 .map(|_| create_item_data.clone())690 .collect();691692 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)693 .map_err(dispatch_to_evm::<T>)?;694 Ok(true)695 }696697 /// @notice Function to mint multiple tokens with the given tokenUris.698 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive699 /// numbers and first number should be obtained with `nextTokenId` method700 /// @param to The new owner701 /// @param tokens array of pairs of token ID and token URI for minted tokens702 #[solidity(rename_selector = "mintBulkWithTokenURI")]703 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]704 fn mint_bulk_with_token_uri(705 &mut self,706 caller: caller,707 to: address,708 tokens: Vec<(uint256, string)>,709 ) -> Result<bool> {710 let key = key::url();711 let caller = T::CrossAccountId::from_eth(caller);712 let to = T::CrossAccountId::from_eth(to);713 let mut expected_index = <TokensMinted<T>>::get(self.id)714 .checked_add(1)715 .ok_or("item id overflow")?;716 let budget = self717 .recorder718 .weight_calls_budget(<StructureWeight<T>>::find_parent());719720 let mut data = Vec::with_capacity(tokens.len());721 let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]722 .into_iter()723 .collect::<BTreeMap<_, _>>()724 .try_into()725 .unwrap();726 for (id, token_uri) in tokens {727 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;728 if id != expected_index {729 return Err("item id should be next".into());730 }731 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;732733 let mut properties = CollectionPropertiesVec::default();734 properties735 .try_push(Property {736 key: key.clone(),737 value: token_uri738 .into_bytes()739 .try_into()740 .map_err(|_| "token uri is too long")?,741 })742 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;743744 let create_item_data = CreateItemData::<T::CrossAccountId> {745 users: users.clone(),746 properties,747 };748 data.push(create_item_data);749 }750751 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)752 .map_err(dispatch_to_evm::<T>)?;753 Ok(true)754 }755756 /// Returns EVM address for refungible token757 ///758 /// @param token ID of the token759 fn token_contract_address(&self, token: uint256) -> Result<address> {760 Ok(T::EvmTokenAddressMapping::token_to_address(761 self.id,762 token.try_into().map_err(|_| "token id overflow")?,763 ))764 }765}766767#[solidity_interface(768 name = UniqueRefungible,769 is(770 ERC721,771 ERC721Enumerable,772 ERC721UniqueExtensions,773 ERC721Mintable,774 ERC721Burnable,775 ERC721Metadata(if(this.flags.erc721metadata)),776 Collection(via(common_mut returns CollectionHandle<T>)),777 TokenProperties,778 )779)]780impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}781782// Not a tests, but code generators783generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);784generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);785786impl<T: Config> CommonEvmHandler for RefungibleHandle<T>787where788 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,789{790 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");791 fn call(792 self,793 handle: &mut impl PrecompileHandle,794 ) -> Option<pallet_common::erc::PrecompileResult> {795 call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)796 }797}pallets/unique/src/eth/mod.rsdiffbeforeafterboth--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -89,6 +89,26 @@
Ok((caller, name, description, token_prefix, base_uri_value))
}
+fn default_url_pkp() -> up_data_structs::PropertyKeyPermission {
+ up_data_structs::PropertyKeyPermission {
+ key: key::url(),
+ permission: up_data_structs::PropertyPermission {
+ mutable: true,
+ collection_admin: true,
+ token_owner: false,
+ },
+ }
+}
+fn default_suffix_pkp() -> up_data_structs::PropertyKeyPermission {
+ up_data_structs::PropertyKeyPermission {
+ key: key::suffix(),
+ permission: up_data_structs::PropertyPermission {
+ mutable: true,
+ collection_admin: true,
+ token_owner: false,
+ },
+ }
+}
fn make_data<T: Config>(
name: CollectionName,
mode: CollectionMode,
@@ -98,26 +118,9 @@
add_properties: bool,
) -> Result<CreateCollectionData<T::AccountId>> {
let token_property_permissions = if add_properties {
- vec![
- up_data_structs::PropertyKeyPermission {
- key: key::url(),
- permission: up_data_structs::PropertyPermission {
- mutable: true,
- collection_admin: true,
- token_owner: false,
- },
- },
- up_data_structs::PropertyKeyPermission {
- key: key::suffix(),
- permission: up_data_structs::PropertyPermission {
- mutable: true,
- collection_admin: true,
- token_owner: false,
- },
- },
- ]
- .try_into()
- .map_err(|e| Error::Revert(format!("{:?}", e)))?
+ vec![default_url_pkp(), default_suffix_pkp()]
+ .try_into()
+ .map_err(|e| Error::Revert(format!("{:?}", e)))?
} else {
up_data_structs::CollectionPropertiesPermissionsVec::default()
};
@@ -130,10 +133,6 @@
up_data_structs::Property {
key: key::schema_version(),
value: property_value::schema_version(),
- },
- up_data_structs::Property {
- key: key::erc721_metadata(),
- value: property_value::erc721_metadata_supported(),
},
];
if !base_uri_value.is_empty() {
primitives/data-structs/src/lib.rsdiffbeforeafterboth--- a/primitives/data-structs/src/lib.rs
+++ b/primitives/data-structs/src/lib.rs
@@ -365,11 +365,14 @@
/// Tokens in foreign collections can be transferred, but not burnt
#[bondrewd(bits = "0..1")]
pub foreign: bool,
+ /// Supports ERC721Metadata
+ #[bondrewd(bits = "1..2")]
+ pub erc721metadata: bool,
/// External collections can't be managed using `unique` api
#[bondrewd(bits = "7..8")]
pub external: bool,
- #[bondrewd(reserve, bits = "1..7")]
+ #[bondrewd(reserve, bits = "2..7")]
pub reserved: u8,
}
bondrewd_codec!(CollectionFlags);
@@ -434,6 +437,15 @@
pub meta_update_permission: MetaUpdatePermission,
}
+#[derive(Encode, Decode, Clone, PartialEq, TypeInfo)]
+#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
+pub struct RpcCollectionFlags {
+ /// Is collection is foreign.
+ pub foreign: bool,
+ /// Collection supports ERC721Metadata.
+ pub erc721metadata: bool,
+}
+
/// Collection parameters, used in RPC calls (see [`Collection`] for the storage version).
#[derive(Encode, Decode, Clone, PartialEq, TypeInfo)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
@@ -471,8 +483,8 @@
/// Is collection read only.
pub read_only: bool,
- /// Is collection is foreign.
- pub foreign: bool,
+ /// Extra collection flags
+ pub flags: RpcCollectionFlags,
}
/// Data used for create collection.