difftreelog
feat Add AbiWrite support for vec with dynamic type
in: master
6 files changed
crates/evm-coder/Cargo.tomldiffbeforeafterboth--- a/crates/evm-coder/Cargo.toml
+++ b/crates/evm-coder/Cargo.toml
@@ -22,6 +22,7 @@
# We want to assert some large binary blobs equality in tests
hex = "0.4.3"
hex-literal = "0.3.4"
+similar-asserts = "1.4.2"
[features]
default = ["std"]
crates/evm-coder/src/abi.rsdiffbeforeafterboth--- a/crates/evm-coder/src/abi.rs
+++ b/crates/evm-coder/src/abi.rs
@@ -25,7 +25,7 @@
use crate::{
execution::{Error, ResultWithPostInfo, WithPostDispatchInfo},
- types::{string, self},
+ types::*,
};
use crate::execution::Result;
@@ -56,7 +56,7 @@
}
}
/// Start reading RLP buffer, parsing first 4 bytes as selector
- pub fn new_call(buf: &'i [u8]) -> Result<(types::bytes4, Self)> {
+ pub fn new_call(buf: &'i [u8]) -> Result<(bytes4, Self)> {
if buf.len() < 4 {
return Err(Error::Error(ExitError::OutOfOffset));
}
@@ -252,11 +252,11 @@
self.static_part.extend(block);
}
- fn write_padright(&mut self, bytes: &[u8]) {
- assert!(bytes.len() <= ABI_ALIGNMENT);
- self.static_part.extend(bytes);
+ fn write_padright(&mut self, block: &[u8]) {
+ assert!(block.len() <= ABI_ALIGNMENT);
+ self.static_part.extend(block);
self.static_part
- .extend(&[0; ABI_ALIGNMENT][0..ABI_ALIGNMENT - bytes.len()]);
+ .extend(&[0; ABI_ALIGNMENT][0..ABI_ALIGNMENT - block.len()]);
}
/// Write [`H160`] to end of buffer
@@ -369,16 +369,30 @@
};
}
-impl_abi_readable!(u8, uint8, false);
-impl_abi_readable!(u32, uint32, false);
-impl_abi_readable!(u64, uint64, false);
-impl_abi_readable!(u128, uint128, false);
-impl_abi_readable!(U256, uint256, false);
-impl_abi_readable!([u8; 4], bytes4, false);
-impl_abi_readable!(H160, address, false);
-impl_abi_readable!(Vec<u8>, bytes, true);
-impl_abi_readable!(bool, bool, true);
+impl_abi_readable!(bool, bool, false);
+impl_abi_readable!(uint8, uint8, false);
+impl_abi_readable!(uint32, uint32, false);
+impl_abi_readable!(uint64, uint64, false);
+impl_abi_readable!(uint128, uint128, false);
+impl_abi_readable!(uint256, uint256, false);
+impl_abi_readable!(bytes4, bytes4, false);
+impl_abi_readable!(address, address, false);
impl_abi_readable!(string, string, true);
+// impl_abi_readable!(bytes, bytes, true);
+
+impl TypeHelper for bytes {
+ fn is_dynamic() -> bool {
+ true
+ }
+ fn size() -> usize {
+ ABI_ALIGNMENT
+ }
+}
+impl AbiRead<bytes> for AbiReader<'_> {
+ fn abi_read(&mut self) -> Result<bytes> {
+ Ok(bytes(self.bytes()?))
+ }
+}
mod sealed {
/// Not all types can be placed in vec, i.e `Vec<u8>` is restricted, `bytes` should be used instead
@@ -524,17 +538,19 @@
impl_abi_writeable!(H160, address);
impl_abi_writeable!(bool, bool);
impl_abi_writeable!(&str, string);
+
impl AbiWrite for string {
fn abi_write(&self, writer: &mut AbiWriter) {
writer.string(self)
}
}
-// impl AbiWrite for Vec<u8> {
-// fn abi_write(&self, writer: &mut AbiWriter) {
-// writer.bytes(self)
-// }
-// }
+impl AbiWrite for bytes {
+ fn abi_write(&self, writer: &mut AbiWriter) {
+ writer.bytes(self.0.as_slice())
+ }
+}
+
impl<T: AbiWrite + TypeHelper> AbiWrite for Vec<T> {
fn abi_write(&self, writer: &mut AbiWriter) {
let is_dynamic = T::is_dynamic();
crates/evm-coder/src/lib.rsdiffbeforeafterboth--- a/crates/evm-coder/src/lib.rs
+++ b/crates/evm-coder/src/lib.rs
@@ -133,8 +133,10 @@
pub type string = ::alloc::string::String;
#[cfg(feature = "std")]
pub type string = ::std::string::String;
- pub type bytes = Vec<u8>;
+ #[derive(Default, Debug)]
+ pub struct bytes(pub Vec<u8>);
+
/// Solidity doesn't have `void` type, however we have special implementation
/// for empty tuple return type
pub type void = ();
@@ -157,6 +159,30 @@
/// and there is no `receiver()` function defined.
pub value: U256,
}
+
+ impl From<Vec<u8>> for bytes {
+ fn from(src: Vec<u8>) -> Self {
+ Self(src)
+ }
+ }
+
+ impl Into<Vec<u8>> for bytes {
+ fn into(self) -> Vec<u8> {
+ self.0
+ }
+ }
+
+ impl bytes {
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.0.len()
+ }
+
+ #[must_use]
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+ }
}
/// Parseable EVM call, this trait should be implemented with [`solidity_interface`] macro
pallets/common/src/erc.rsdiffbeforeafterboth--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -85,7 +85,7 @@
let key = <Vec<u8>>::from(key)
.try_into()
.map_err(|_| "key too large")?;
- let value = value.try_into().map_err(|_| "value too large")?;
+ let value = value.0.try_into().map_err(|_| "value too large")?;
<Pallet<T>>::set_collection_property(self, &caller, Property { key, value })
.map_err(dispatch_to_evm::<T>)
@@ -120,7 +120,7 @@
let props = <CollectionProperties<T>>::get(self.id);
let prop = props.get(&key).ok_or("key not found")?;
- Ok(prop.to_vec())
+ Ok(bytes(prop.to_vec()))
}
/// Set the sponsor of the collection.
pallets/nonfungible/src/erc.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/erc.rs
+++ b/pallets/nonfungible/src/erc.rs
@@ -97,7 +97,7 @@
let key = <Vec<u8>>::from(key)
.try_into()
.map_err(|_| "key too long")?;
- let value = value.try_into().map_err(|_| "value too long")?;
+ let value = value.0.try_into().map_err(|_| "value too long")?;
let nesting_budget = self
.recorder
@@ -146,7 +146,7 @@
let props = <TokenProperties<T>>::get((self.id, token_id));
let prop = props.get(&key).ok_or("key not found")?;
- Ok(prop.to_vec())
+ Ok(prop.to_vec().into())
}
}
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};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 ERC721UniqueMintableEvents {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 NFTs in this contract199 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`200 #[solidity(hide, rename_selector = "name")]201 fn name_proxy(&self) -> Result<string> {202 self.name()203 }204205 /// @notice An abbreviated name for NFTs in this contract206 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`207 #[solidity(hide, rename_selector = "symbol")]208 fn symbol_proxy(&self) -> Result<string> {209 self.symbol()210 }211212 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.213 ///214 /// @dev If the token has a `url` property and it is not empty, it is returned.215 /// 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`.216 /// If the collection property `baseURI` is empty or absent, return "" (empty string)217 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix218 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).219 ///220 /// @return token's const_metadata221 #[solidity(rename_selector = "tokenURI")]222 fn token_uri(&self, token_id: uint256) -> Result<string> {223 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;224225 match get_token_property(self, token_id_u32, &key::url()).as_deref() {226 Err(_) | Ok("") => (),227 Ok(url) => {228 return Ok(url.into());229 }230 };231232 let base_uri =233 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())234 .map(BoundedVec::into_inner)235 .map(string::from_utf8)236 .transpose()237 .map_err(|e| {238 Error::Revert(alloc::format!(239 "Can not convert value \"baseURI\" to string with error \"{}\"",240 e241 ))242 })?;243244 let base_uri = match base_uri.as_deref() {245 None | Some("") => {246 return Ok("".into());247 }248 Some(base_uri) => base_uri.into(),249 };250251 Ok(252 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {253 Err(_) | Ok("") => base_uri,254 Ok(suffix) => base_uri + suffix,255 },256 )257 }258}259260/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension261/// @dev See https://eips.ethereum.org/EIPS/eip-721262#[solidity_interface(name = ERC721Enumerable)]263impl<T: Config> RefungibleHandle<T> {264 /// @notice Enumerate valid RFTs265 /// @param index A counter less than `totalSupply()`266 /// @return The token identifier for the `index`th NFT,267 /// (sort order not specified)268 fn token_by_index(&self, index: uint256) -> Result<uint256> {269 Ok(index)270 }271272 /// Not implemented273 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {274 // TODO: Not implemetable275 Err("not implemented".into())276 }277278 /// @notice Count RFTs tracked by this contract279 /// @return A count of valid RFTs tracked by this contract, where each one of280 /// them has an assigned and queryable owner not equal to the zero address281 fn total_supply(&self) -> Result<uint256> {282 self.consume_store_reads(1)?;283 Ok(<Pallet<T>>::total_supply(self).into())284 }285}286287/// @title ERC-721 Non-Fungible Token Standard288/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md289#[solidity_interface(name = ERC721, events(ERC721Events))]290impl<T: Config> RefungibleHandle<T> {291 /// @notice Count all RFTs assigned to an owner292 /// @dev RFTs assigned to the zero address are considered invalid, and this293 /// function throws for queries about the zero address.294 /// @param owner An address for whom to query the balance295 /// @return The number of RFTs owned by `owner`, possibly zero296 fn balance_of(&self, owner: address) -> Result<uint256> {297 self.consume_store_reads(1)?;298 let owner = T::CrossAccountId::from_eth(owner);299 let balance = <AccountBalance<T>>::get((self.id, owner));300 Ok(balance.into())301 }302303 /// @notice Find the owner of an RFT304 /// @dev RFTs assigned to zero address are considered invalid, and queries305 /// about them do throw.306 /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for307 /// the tokens that are partially owned.308 /// @param tokenId The identifier for an RFT309 /// @return The address of the owner of the RFT310 fn owner_of(&self, token_id: uint256) -> Result<address> {311 self.consume_store_reads(2)?;312 let token = token_id.try_into()?;313 let owner = <Pallet<T>>::token_owner(self.id, token);314 Ok(owner315 .map(|address| *address.as_eth())316 .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))317 }318319 /// @dev Not implemented320 fn safe_transfer_from_with_data(321 &mut self,322 _from: address,323 _to: address,324 _token_id: uint256,325 _data: bytes,326 ) -> Result<void> {327 // TODO: Not implemetable328 Err("not implemented".into())329 }330331 /// @dev Not implemented332 fn safe_transfer_from(333 &mut self,334 _from: address,335 _to: address,336 _token_id: uint256,337 ) -> Result<void> {338 // TODO: Not implemetable339 Err("not implemented".into())340 }341342 /// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE343 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE344 /// THEY MAY BE PERMANENTLY LOST345 /// @dev Throws unless `msg.sender` is the current owner or an authorized346 /// operator for this RFT. Throws if `from` is not the current owner. Throws347 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.348 /// Throws if RFT pieces have multiple owners.349 /// @param from The current owner of the NFT350 /// @param to The new owner351 /// @param tokenId The NFT to transfer352 #[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]353 fn transfer_from(354 &mut self,355 caller: caller,356 from: address,357 to: address,358 token_id: uint256,359 ) -> Result<void> {360 let caller = T::CrossAccountId::from_eth(caller);361 let from = T::CrossAccountId::from_eth(from);362 let to = T::CrossAccountId::from_eth(to);363 let token = token_id.try_into()?;364 let budget = self365 .recorder366 .weight_calls_budget(<StructureWeight<T>>::find_parent());367368 let balance = balance(&self, token, &from)?;369 ensure_single_owner(&self, token, balance)?;370371 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)372 .map_err(dispatch_to_evm::<T>)?;373374 Ok(())375 }376377 /// @dev Not implemented378 fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {379 Err("not implemented".into())380 }381382 /// @dev Not implemented383 fn set_approval_for_all(384 &mut self,385 _caller: caller,386 _operator: address,387 _approved: bool,388 ) -> Result<void> {389 // TODO: Not implemetable390 Err("not implemented".into())391 }392393 /// @dev Not implemented394 fn get_approved(&self, _token_id: uint256) -> Result<address> {395 // TODO: Not implemetable396 Err("not implemented".into())397 }398399 /// @dev Not implemented400 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {401 // TODO: Not implemetable402 Err("not implemented".into())403 }404}405406/// Returns amount of pieces of `token` that `owner` have407pub fn balance<T: Config>(408 collection: &RefungibleHandle<T>,409 token: TokenId,410 owner: &T::CrossAccountId,411) -> Result<u128> {412 collection.consume_store_reads(1)?;413 let balance = <Balance<T>>::get((collection.id, token, &owner));414 Ok(balance)415}416417/// Throws if `owner_balance` is lower than total amount of `token` pieces418pub fn ensure_single_owner<T: Config>(419 collection: &RefungibleHandle<T>,420 token: TokenId,421 owner_balance: u128,422) -> Result<()> {423 collection.consume_store_reads(1)?;424 let total_supply = <TotalSupply<T>>::get((collection.id, token));425 if total_supply != owner_balance {426 return Err("token has multiple owners".into());427 }428 Ok(())429}430431/// @title ERC721 Token that can be irreversibly burned (destroyed).432#[solidity_interface(name = ERC721Burnable)]433impl<T: Config> RefungibleHandle<T> {434 /// @notice Burns a specific ERC721 token.435 /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized436 /// operator of the current owner.437 /// @param tokenId The RFT to approve438 #[weight(<SelfWeightOf<T>>::burn_item_fully())]439 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {440 let caller = T::CrossAccountId::from_eth(caller);441 let token = token_id.try_into()?;442443 let balance = balance(&self, token, &caller)?;444 ensure_single_owner(&self, token, balance)?;445446 <Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;447 Ok(())448 }449}450451/// @title ERC721 minting logic.452#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]453impl<T: Config> RefungibleHandle<T> {454 fn minting_finished(&self) -> Result<bool> {455 Ok(false)456 }457458 /// @notice Function to mint token.459 /// @param to The new owner460 /// @return uint256 The id of the newly minted token461 #[weight(<SelfWeightOf<T>>::create_item())]462 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {463 let token_id: uint256 = <TokensMinted<T>>::get(self.id)464 .checked_add(1)465 .ok_or("item id overflow")?466 .into();467 self.mint_check_id(caller, to, token_id)?;468 Ok(token_id)469 }470471 /// @notice Function to mint token.472 /// @dev `tokenId` should be obtained with `nextTokenId` method,473 /// unlike standard, you can't specify it manually474 /// @param to The new owner475 /// @param tokenId ID of the minted RFT476 #[solidity(hide, rename_selector = "mint")]477 #[weight(<SelfWeightOf<T>>::create_item())]478 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {479 let caller = T::CrossAccountId::from_eth(caller);480 let to = T::CrossAccountId::from_eth(to);481 let token_id: u32 = token_id.try_into()?;482 let budget = self483 .recorder484 .weight_calls_budget(<StructureWeight<T>>::find_parent());485486 if <TokensMinted<T>>::get(self.id)487 .checked_add(1)488 .ok_or("item id overflow")?489 != token_id490 {491 return Err("item id should be next".into());492 }493494 let users = [(to.clone(), 1)]495 .into_iter()496 .collect::<BTreeMap<_, _>>()497 .try_into()498 .unwrap();499 <Pallet<T>>::create_item(500 self,501 &caller,502 CreateItemData::<T::CrossAccountId> {503 users,504 properties: CollectionPropertiesVec::default(),505 },506 &budget,507 )508 .map_err(dispatch_to_evm::<T>)?;509510 Ok(true)511 }512513 /// @notice Function to mint token with the given tokenUri.514 /// @param to The new owner515 /// @param tokenUri Token URI that would be stored in the NFT properties516 /// @return uint256 The id of the newly minted token517 #[solidity(rename_selector = "mintWithTokenURI")]518 #[weight(<SelfWeightOf<T>>::create_item())]519 fn mint_with_token_uri(520 &mut self,521 caller: caller,522 to: address,523 token_uri: string,524 ) -> Result<uint256> {525 let token_id: uint256 = <TokensMinted<T>>::get(self.id)526 .checked_add(1)527 .ok_or("item id overflow")?528 .into();529 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;530 Ok(token_id)531 }532533 /// @notice Function to mint token with the given tokenUri.534 /// @dev `tokenId` should be obtained with `nextTokenId` method,535 /// unlike standard, you can't specify it manually536 /// @param to The new owner537 /// @param tokenId ID of the minted RFT538 /// @param tokenUri Token URI that would be stored in the RFT properties539 #[solidity(hide, rename_selector = "mintWithTokenURI")]540 #[weight(<SelfWeightOf<T>>::create_item())]541 fn mint_with_token_uri_check_id(542 &mut self,543 caller: caller,544 to: address,545 token_id: uint256,546 token_uri: string,547 ) -> Result<bool> {548 let key = key::url();549 let permission = get_token_permission::<T>(self.id, &key)?;550 if !permission.collection_admin {551 return Err("Operation is not allowed".into());552 }553554 let caller = T::CrossAccountId::from_eth(caller);555 let to = T::CrossAccountId::from_eth(to);556 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;557 let budget = self558 .recorder559 .weight_calls_budget(<StructureWeight<T>>::find_parent());560561 if <TokensMinted<T>>::get(self.id)562 .checked_add(1)563 .ok_or("item id overflow")?564 != token_id565 {566 return Err("item id should be next".into());567 }568569 let mut properties = CollectionPropertiesVec::default();570 properties571 .try_push(Property {572 key,573 value: token_uri574 .into_bytes()575 .try_into()576 .map_err(|_| "token uri is too long")?,577 })578 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;579580 let users = [(to.clone(), 1)]581 .into_iter()582 .collect::<BTreeMap<_, _>>()583 .try_into()584 .unwrap();585 <Pallet<T>>::create_item(586 self,587 &caller,588 CreateItemData::<T::CrossAccountId> { users, properties },589 &budget,590 )591 .map_err(dispatch_to_evm::<T>)?;592 Ok(true)593 }594595 /// @dev Not implemented596 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {597 Err("not implementable".into())598 }599}600601fn get_token_property<T: Config>(602 collection: &CollectionHandle<T>,603 token_id: u32,604 key: &up_data_structs::PropertyKey,605) -> Result<string> {606 collection.consume_store_reads(1)?;607 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))608 .map_err(|_| Error::Revert("Token properties not found".into()))?;609 if let Some(property) = properties.get(key) {610 return Ok(string::from_utf8_lossy(property).into());611 }612613 Err("Property tokenURI not found".into())614}615616fn get_token_permission<T: Config>(617 collection_id: CollectionId,618 key: &PropertyKey,619) -> Result<PropertyPermission> {620 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)621 .map_err(|_| Error::Revert("No permissions for collection".into()))?;622 let a = token_property_permissions623 .get(key)624 .map(Clone::clone)625 .ok_or_else(|| {626 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();627 Error::Revert(alloc::format!("No permission for key {}", key))628 })?;629 Ok(a)630}631632/// @title Unique extensions for ERC721.633#[solidity_interface(name = ERC721UniqueExtensions)]634impl<T: Config> RefungibleHandle<T> {635 /// @notice A descriptive name for a collection of NFTs in this contract636 fn name(&self) -> Result<string> {637 Ok(decode_utf16(self.name.iter().copied())638 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))639 .collect::<string>())640 }641642 /// @notice An abbreviated name for NFTs in this contract643 fn symbol(&self) -> Result<string> {644 Ok(string::from_utf8_lossy(&self.token_prefix).into())645 }646647 /// @notice Transfer ownership of an RFT648 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`649 /// is the zero address. Throws if `tokenId` is not a valid RFT.650 /// Throws if RFT pieces have multiple owners.651 /// @param to The new owner652 /// @param tokenId The RFT to transfer653 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]654 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {655 let caller = T::CrossAccountId::from_eth(caller);656 let to = T::CrossAccountId::from_eth(to);657 let token = token_id.try_into()?;658 let budget = self659 .recorder660 .weight_calls_budget(<StructureWeight<T>>::find_parent());661662 let balance = balance(&self, token, &caller)?;663 ensure_single_owner(&self, token, balance)?;664665 <Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)666 .map_err(dispatch_to_evm::<T>)?;667 Ok(())668 }669670 /// @notice Burns a specific ERC721 token.671 /// @dev Throws unless `msg.sender` is the current owner or an authorized672 /// operator for this RFT. Throws if `from` is not the current owner. Throws673 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.674 /// Throws if RFT pieces have multiple owners.675 /// @param from The current owner of the RFT676 /// @param tokenId The RFT to transfer677 #[weight(<SelfWeightOf<T>>::burn_from())]678 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {679 let caller = T::CrossAccountId::from_eth(caller);680 let from = T::CrossAccountId::from_eth(from);681 let token = token_id.try_into()?;682 let budget = self683 .recorder684 .weight_calls_budget(<StructureWeight<T>>::find_parent());685686 let balance = balance(&self, token, &caller)?;687 ensure_single_owner(&self, token, balance)?;688689 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)690 .map_err(dispatch_to_evm::<T>)?;691 Ok(())692 }693694 /// @notice Returns next free RFT ID.695 fn next_token_id(&self) -> Result<uint256> {696 self.consume_store_reads(1)?;697 Ok(<TokensMinted<T>>::get(self.id)698 .checked_add(1)699 .ok_or("item id overflow")?700 .into())701 }702703 /// @notice Function to mint multiple tokens.704 /// @dev `tokenIds` should be an array of consecutive numbers and first number705 /// should be obtained with `nextTokenId` method706 /// @param to The new owner707 /// @param tokenIds IDs of the minted RFTs708 #[solidity(hide)]709 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]710 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {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 total_tokens = token_ids.len();721 for id in token_ids.into_iter() {722 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;723 if id != expected_index {724 return Err("item id should be next".into());725 }726 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;727 }728 let users = [(to.clone(), 1)]729 .into_iter()730 .collect::<BTreeMap<_, _>>()731 .try_into()732 .unwrap();733 let create_item_data = CreateItemData::<T::CrossAccountId> {734 users,735 properties: CollectionPropertiesVec::default(),736 };737 let data = (0..total_tokens)738 .map(|_| create_item_data.clone())739 .collect();740741 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)742 .map_err(dispatch_to_evm::<T>)?;743 Ok(true)744 }745746 /// @notice Function to mint multiple tokens with the given tokenUris.747 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive748 /// numbers and first number should be obtained with `nextTokenId` method749 /// @param to The new owner750 /// @param tokens array of pairs of token ID and token URI for minted tokens751 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]752 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]753 fn mint_bulk_with_token_uri(754 &mut self,755 caller: caller,756 to: address,757 tokens: Vec<(uint256, string)>,758 ) -> Result<bool> {759 let key = key::url();760 let caller = T::CrossAccountId::from_eth(caller);761 let to = T::CrossAccountId::from_eth(to);762 let mut expected_index = <TokensMinted<T>>::get(self.id)763 .checked_add(1)764 .ok_or("item id overflow")?;765 let budget = self766 .recorder767 .weight_calls_budget(<StructureWeight<T>>::find_parent());768769 let mut data = Vec::with_capacity(tokens.len());770 let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]771 .into_iter()772 .collect::<BTreeMap<_, _>>()773 .try_into()774 .unwrap();775 for (id, token_uri) in tokens {776 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;777 if id != expected_index {778 return Err("item id should be next".into());779 }780 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;781782 let mut properties = CollectionPropertiesVec::default();783 properties784 .try_push(Property {785 key: key.clone(),786 value: token_uri787 .into_bytes()788 .try_into()789 .map_err(|_| "token uri is too long")?,790 })791 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;792793 let create_item_data = CreateItemData::<T::CrossAccountId> {794 users: users.clone(),795 properties,796 };797 data.push(create_item_data);798 }799800 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)801 .map_err(dispatch_to_evm::<T>)?;802 Ok(true)803 }804805 /// Returns EVM address for refungible token806 ///807 /// @param token ID of the token808 fn token_contract_address(&self, token: uint256) -> Result<address> {809 Ok(T::EvmTokenAddressMapping::token_to_address(810 self.id,811 token.try_into().map_err(|_| "token id overflow")?,812 ))813 }814}815816#[solidity_interface(817 name = UniqueRefungible,818 is(819 ERC721,820 ERC721Enumerable,821 ERC721UniqueExtensions,822 ERC721UniqueMintable,823 ERC721Burnable,824 ERC721Metadata(if(this.flags.erc721metadata)),825 Collection(via(common_mut returns CollectionHandle<T>)),826 TokenProperties,827 )828)]829impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}830831// Not a tests, but code generators832generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);833generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);834835impl<T: Config> CommonEvmHandler for RefungibleHandle<T>836where837 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,838{839 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");840 fn call(841 self,842 handle: &mut impl PrecompileHandle,843 ) -> Option<pallet_common::erc::PrecompileResult> {844 call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)845 }846}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};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.0.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().into())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 ERC721UniqueMintableEvents {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 NFTs in this contract199 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`200 #[solidity(hide, rename_selector = "name")]201 fn name_proxy(&self) -> Result<string> {202 self.name()203 }204205 /// @notice An abbreviated name for NFTs in this contract206 /// @dev real implementation of this function lies in `ERC721UniqueExtensions`207 #[solidity(hide, rename_selector = "symbol")]208 fn symbol_proxy(&self) -> Result<string> {209 self.symbol()210 }211212 /// @notice A distinct Uniform Resource Identifier (URI) for a given asset.213 ///214 /// @dev If the token has a `url` property and it is not empty, it is returned.215 /// 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`.216 /// If the collection property `baseURI` is empty or absent, return "" (empty string)217 /// otherwise, if token property `suffix` present and is non-empty, return concatenation of baseURI and suffix218 /// otherwise, return concatenation of `baseURI` and stringified token id (decimal stringifying, without paddings).219 ///220 /// @return token's const_metadata221 #[solidity(rename_selector = "tokenURI")]222 fn token_uri(&self, token_id: uint256) -> Result<string> {223 let token_id_u32: u32 = token_id.try_into().map_err(|_| "token id overflow")?;224225 match get_token_property(self, token_id_u32, &key::url()).as_deref() {226 Err(_) | Ok("") => (),227 Ok(url) => {228 return Ok(url.into());229 }230 };231232 let base_uri =233 pallet_common::Pallet::<T>::get_collection_property(self.id, &key::base_uri())234 .map(BoundedVec::into_inner)235 .map(string::from_utf8)236 .transpose()237 .map_err(|e| {238 Error::Revert(alloc::format!(239 "Can not convert value \"baseURI\" to string with error \"{}\"",240 e241 ))242 })?;243244 let base_uri = match base_uri.as_deref() {245 None | Some("") => {246 return Ok("".into());247 }248 Some(base_uri) => base_uri.into(),249 };250251 Ok(252 match get_token_property(self, token_id_u32, &key::suffix()).as_deref() {253 Err(_) | Ok("") => base_uri,254 Ok(suffix) => base_uri + suffix,255 },256 )257 }258}259260/// @title ERC-721 Non-Fungible Token Standard, optional enumeration extension261/// @dev See https://eips.ethereum.org/EIPS/eip-721262#[solidity_interface(name = ERC721Enumerable)]263impl<T: Config> RefungibleHandle<T> {264 /// @notice Enumerate valid RFTs265 /// @param index A counter less than `totalSupply()`266 /// @return The token identifier for the `index`th NFT,267 /// (sort order not specified)268 fn token_by_index(&self, index: uint256) -> Result<uint256> {269 Ok(index)270 }271272 /// Not implemented273 fn token_of_owner_by_index(&self, _owner: address, _index: uint256) -> Result<uint256> {274 // TODO: Not implemetable275 Err("not implemented".into())276 }277278 /// @notice Count RFTs tracked by this contract279 /// @return A count of valid RFTs tracked by this contract, where each one of280 /// them has an assigned and queryable owner not equal to the zero address281 fn total_supply(&self) -> Result<uint256> {282 self.consume_store_reads(1)?;283 Ok(<Pallet<T>>::total_supply(self).into())284 }285}286287/// @title ERC-721 Non-Fungible Token Standard288/// @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md289#[solidity_interface(name = ERC721, events(ERC721Events))]290impl<T: Config> RefungibleHandle<T> {291 /// @notice Count all RFTs assigned to an owner292 /// @dev RFTs assigned to the zero address are considered invalid, and this293 /// function throws for queries about the zero address.294 /// @param owner An address for whom to query the balance295 /// @return The number of RFTs owned by `owner`, possibly zero296 fn balance_of(&self, owner: address) -> Result<uint256> {297 self.consume_store_reads(1)?;298 let owner = T::CrossAccountId::from_eth(owner);299 let balance = <AccountBalance<T>>::get((self.id, owner));300 Ok(balance.into())301 }302303 /// @notice Find the owner of an RFT304 /// @dev RFTs assigned to zero address are considered invalid, and queries305 /// about them do throw.306 /// Returns special 0xffffffffffffffffffffffffffffffffffffffff address for307 /// the tokens that are partially owned.308 /// @param tokenId The identifier for an RFT309 /// @return The address of the owner of the RFT310 fn owner_of(&self, token_id: uint256) -> Result<address> {311 self.consume_store_reads(2)?;312 let token = token_id.try_into()?;313 let owner = <Pallet<T>>::token_owner(self.id, token);314 Ok(owner315 .map(|address| *address.as_eth())316 .unwrap_or_else(|| ADDRESS_FOR_PARTIALLY_OWNED_TOKENS))317 }318319 /// @dev Not implemented320 fn safe_transfer_from_with_data(321 &mut self,322 _from: address,323 _to: address,324 _token_id: uint256,325 _data: bytes,326 ) -> Result<void> {327 // TODO: Not implemetable328 Err("not implemented".into())329 }330331 /// @dev Not implemented332 fn safe_transfer_from(333 &mut self,334 _from: address,335 _to: address,336 _token_id: uint256,337 ) -> Result<void> {338 // TODO: Not implemetable339 Err("not implemented".into())340 }341342 /// @notice Transfer ownership of an RFT -- THE CALLER IS RESPONSIBLE343 /// TO CONFIRM THAT `to` IS CAPABLE OF RECEIVING NFTS OR ELSE344 /// THEY MAY BE PERMANENTLY LOST345 /// @dev Throws unless `msg.sender` is the current owner or an authorized346 /// operator for this RFT. Throws if `from` is not the current owner. Throws347 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.348 /// Throws if RFT pieces have multiple owners.349 /// @param from The current owner of the NFT350 /// @param to The new owner351 /// @param tokenId The NFT to transfer352 #[weight(<SelfWeightOf<T>>::transfer_from_creating_removing())]353 fn transfer_from(354 &mut self,355 caller: caller,356 from: address,357 to: address,358 token_id: uint256,359 ) -> Result<void> {360 let caller = T::CrossAccountId::from_eth(caller);361 let from = T::CrossAccountId::from_eth(from);362 let to = T::CrossAccountId::from_eth(to);363 let token = token_id.try_into()?;364 let budget = self365 .recorder366 .weight_calls_budget(<StructureWeight<T>>::find_parent());367368 let balance = balance(&self, token, &from)?;369 ensure_single_owner(&self, token, balance)?;370371 <Pallet<T>>::transfer_from(self, &caller, &from, &to, token, balance, &budget)372 .map_err(dispatch_to_evm::<T>)?;373374 Ok(())375 }376377 /// @dev Not implemented378 fn approve(&mut self, _caller: caller, _approved: address, _token_id: uint256) -> Result<void> {379 Err("not implemented".into())380 }381382 /// @dev Not implemented383 fn set_approval_for_all(384 &mut self,385 _caller: caller,386 _operator: address,387 _approved: bool,388 ) -> Result<void> {389 // TODO: Not implemetable390 Err("not implemented".into())391 }392393 /// @dev Not implemented394 fn get_approved(&self, _token_id: uint256) -> Result<address> {395 // TODO: Not implemetable396 Err("not implemented".into())397 }398399 /// @dev Not implemented400 fn is_approved_for_all(&self, _owner: address, _operator: address) -> Result<address> {401 // TODO: Not implemetable402 Err("not implemented".into())403 }404}405406/// Returns amount of pieces of `token` that `owner` have407pub fn balance<T: Config>(408 collection: &RefungibleHandle<T>,409 token: TokenId,410 owner: &T::CrossAccountId,411) -> Result<u128> {412 collection.consume_store_reads(1)?;413 let balance = <Balance<T>>::get((collection.id, token, &owner));414 Ok(balance)415}416417/// Throws if `owner_balance` is lower than total amount of `token` pieces418pub fn ensure_single_owner<T: Config>(419 collection: &RefungibleHandle<T>,420 token: TokenId,421 owner_balance: u128,422) -> Result<()> {423 collection.consume_store_reads(1)?;424 let total_supply = <TotalSupply<T>>::get((collection.id, token));425 if total_supply != owner_balance {426 return Err("token has multiple owners".into());427 }428 Ok(())429}430431/// @title ERC721 Token that can be irreversibly burned (destroyed).432#[solidity_interface(name = ERC721Burnable)]433impl<T: Config> RefungibleHandle<T> {434 /// @notice Burns a specific ERC721 token.435 /// @dev Throws unless `msg.sender` is the current RFT owner, or an authorized436 /// operator of the current owner.437 /// @param tokenId The RFT to approve438 #[weight(<SelfWeightOf<T>>::burn_item_fully())]439 fn burn(&mut self, caller: caller, token_id: uint256) -> Result<void> {440 let caller = T::CrossAccountId::from_eth(caller);441 let token = token_id.try_into()?;442443 let balance = balance(&self, token, &caller)?;444 ensure_single_owner(&self, token, balance)?;445446 <Pallet<T>>::burn(self, &caller, token, balance).map_err(dispatch_to_evm::<T>)?;447 Ok(())448 }449}450451/// @title ERC721 minting logic.452#[solidity_interface(name = ERC721UniqueMintable, events(ERC721UniqueMintableEvents))]453impl<T: Config> RefungibleHandle<T> {454 fn minting_finished(&self) -> Result<bool> {455 Ok(false)456 }457458 /// @notice Function to mint token.459 /// @param to The new owner460 /// @return uint256 The id of the newly minted token461 #[weight(<SelfWeightOf<T>>::create_item())]462 fn mint(&mut self, caller: caller, to: address) -> Result<uint256> {463 let token_id: uint256 = <TokensMinted<T>>::get(self.id)464 .checked_add(1)465 .ok_or("item id overflow")?466 .into();467 self.mint_check_id(caller, to, token_id)?;468 Ok(token_id)469 }470471 /// @notice Function to mint token.472 /// @dev `tokenId` should be obtained with `nextTokenId` method,473 /// unlike standard, you can't specify it manually474 /// @param to The new owner475 /// @param tokenId ID of the minted RFT476 #[solidity(hide, rename_selector = "mint")]477 #[weight(<SelfWeightOf<T>>::create_item())]478 fn mint_check_id(&mut self, caller: caller, to: address, token_id: uint256) -> Result<bool> {479 let caller = T::CrossAccountId::from_eth(caller);480 let to = T::CrossAccountId::from_eth(to);481 let token_id: u32 = token_id.try_into()?;482 let budget = self483 .recorder484 .weight_calls_budget(<StructureWeight<T>>::find_parent());485486 if <TokensMinted<T>>::get(self.id)487 .checked_add(1)488 .ok_or("item id overflow")?489 != token_id490 {491 return Err("item id should be next".into());492 }493494 let users = [(to.clone(), 1)]495 .into_iter()496 .collect::<BTreeMap<_, _>>()497 .try_into()498 .unwrap();499 <Pallet<T>>::create_item(500 self,501 &caller,502 CreateItemData::<T::CrossAccountId> {503 users,504 properties: CollectionPropertiesVec::default(),505 },506 &budget,507 )508 .map_err(dispatch_to_evm::<T>)?;509510 Ok(true)511 }512513 /// @notice Function to mint token with the given tokenUri.514 /// @param to The new owner515 /// @param tokenUri Token URI that would be stored in the NFT properties516 /// @return uint256 The id of the newly minted token517 #[solidity(rename_selector = "mintWithTokenURI")]518 #[weight(<SelfWeightOf<T>>::create_item())]519 fn mint_with_token_uri(520 &mut self,521 caller: caller,522 to: address,523 token_uri: string,524 ) -> Result<uint256> {525 let token_id: uint256 = <TokensMinted<T>>::get(self.id)526 .checked_add(1)527 .ok_or("item id overflow")?528 .into();529 self.mint_with_token_uri_check_id(caller, to, token_id, token_uri)?;530 Ok(token_id)531 }532533 /// @notice Function to mint token with the given tokenUri.534 /// @dev `tokenId` should be obtained with `nextTokenId` method,535 /// unlike standard, you can't specify it manually536 /// @param to The new owner537 /// @param tokenId ID of the minted RFT538 /// @param tokenUri Token URI that would be stored in the RFT properties539 #[solidity(hide, rename_selector = "mintWithTokenURI")]540 #[weight(<SelfWeightOf<T>>::create_item())]541 fn mint_with_token_uri_check_id(542 &mut self,543 caller: caller,544 to: address,545 token_id: uint256,546 token_uri: string,547 ) -> Result<bool> {548 let key = key::url();549 let permission = get_token_permission::<T>(self.id, &key)?;550 if !permission.collection_admin {551 return Err("Operation is not allowed".into());552 }553554 let caller = T::CrossAccountId::from_eth(caller);555 let to = T::CrossAccountId::from_eth(to);556 let token_id: u32 = token_id.try_into().map_err(|_| "amount overflow")?;557 let budget = self558 .recorder559 .weight_calls_budget(<StructureWeight<T>>::find_parent());560561 if <TokensMinted<T>>::get(self.id)562 .checked_add(1)563 .ok_or("item id overflow")?564 != token_id565 {566 return Err("item id should be next".into());567 }568569 let mut properties = CollectionPropertiesVec::default();570 properties571 .try_push(Property {572 key,573 value: token_uri574 .into_bytes()575 .try_into()576 .map_err(|_| "token uri is too long")?,577 })578 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;579580 let users = [(to.clone(), 1)]581 .into_iter()582 .collect::<BTreeMap<_, _>>()583 .try_into()584 .unwrap();585 <Pallet<T>>::create_item(586 self,587 &caller,588 CreateItemData::<T::CrossAccountId> { users, properties },589 &budget,590 )591 .map_err(dispatch_to_evm::<T>)?;592 Ok(true)593 }594595 /// @dev Not implemented596 fn finish_minting(&mut self, _caller: caller) -> Result<bool> {597 Err("not implementable".into())598 }599}600601fn get_token_property<T: Config>(602 collection: &CollectionHandle<T>,603 token_id: u32,604 key: &up_data_structs::PropertyKey,605) -> Result<string> {606 collection.consume_store_reads(1)?;607 let properties = <TokenProperties<T>>::try_get((collection.id, token_id))608 .map_err(|_| Error::Revert("Token properties not found".into()))?;609 if let Some(property) = properties.get(key) {610 return Ok(string::from_utf8_lossy(property).into());611 }612613 Err("Property tokenURI not found".into())614}615616fn get_token_permission<T: Config>(617 collection_id: CollectionId,618 key: &PropertyKey,619) -> Result<PropertyPermission> {620 let token_property_permissions = CollectionPropertyPermissions::<T>::try_get(collection_id)621 .map_err(|_| Error::Revert("No permissions for collection".into()))?;622 let a = token_property_permissions623 .get(key)624 .map(Clone::clone)625 .ok_or_else(|| {626 let key = string::from_utf8(key.clone().into_inner()).unwrap_or_default();627 Error::Revert(alloc::format!("No permission for key {}", key))628 })?;629 Ok(a)630}631632/// @title Unique extensions for ERC721.633#[solidity_interface(name = ERC721UniqueExtensions)]634impl<T: Config> RefungibleHandle<T> {635 /// @notice A descriptive name for a collection of NFTs in this contract636 fn name(&self) -> Result<string> {637 Ok(decode_utf16(self.name.iter().copied())638 .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))639 .collect::<string>())640 }641642 /// @notice An abbreviated name for NFTs in this contract643 fn symbol(&self) -> Result<string> {644 Ok(string::from_utf8_lossy(&self.token_prefix).into())645 }646647 /// @notice Transfer ownership of an RFT648 /// @dev Throws unless `msg.sender` is the current owner. Throws if `to`649 /// is the zero address. Throws if `tokenId` is not a valid RFT.650 /// Throws if RFT pieces have multiple owners.651 /// @param to The new owner652 /// @param tokenId The RFT to transfer653 #[weight(<SelfWeightOf<T>>::transfer_creating_removing())]654 fn transfer(&mut self, caller: caller, to: address, token_id: uint256) -> Result<void> {655 let caller = T::CrossAccountId::from_eth(caller);656 let to = T::CrossAccountId::from_eth(to);657 let token = token_id.try_into()?;658 let budget = self659 .recorder660 .weight_calls_budget(<StructureWeight<T>>::find_parent());661662 let balance = balance(&self, token, &caller)?;663 ensure_single_owner(&self, token, balance)?;664665 <Pallet<T>>::transfer(self, &caller, &to, token, balance, &budget)666 .map_err(dispatch_to_evm::<T>)?;667 Ok(())668 }669670 /// @notice Burns a specific ERC721 token.671 /// @dev Throws unless `msg.sender` is the current owner or an authorized672 /// operator for this RFT. Throws if `from` is not the current owner. Throws673 /// if `to` is the zero address. Throws if `tokenId` is not a valid RFT.674 /// Throws if RFT pieces have multiple owners.675 /// @param from The current owner of the RFT676 /// @param tokenId The RFT to transfer677 #[weight(<SelfWeightOf<T>>::burn_from())]678 fn burn_from(&mut self, caller: caller, from: address, token_id: uint256) -> Result<void> {679 let caller = T::CrossAccountId::from_eth(caller);680 let from = T::CrossAccountId::from_eth(from);681 let token = token_id.try_into()?;682 let budget = self683 .recorder684 .weight_calls_budget(<StructureWeight<T>>::find_parent());685686 let balance = balance(&self, token, &caller)?;687 ensure_single_owner(&self, token, balance)?;688689 <Pallet<T>>::burn_from(self, &caller, &from, token, balance, &budget)690 .map_err(dispatch_to_evm::<T>)?;691 Ok(())692 }693694 /// @notice Returns next free RFT ID.695 fn next_token_id(&self) -> Result<uint256> {696 self.consume_store_reads(1)?;697 Ok(<TokensMinted<T>>::get(self.id)698 .checked_add(1)699 .ok_or("item id overflow")?700 .into())701 }702703 /// @notice Function to mint multiple tokens.704 /// @dev `tokenIds` should be an array of consecutive numbers and first number705 /// should be obtained with `nextTokenId` method706 /// @param to The new owner707 /// @param tokenIds IDs of the minted RFTs708 #[solidity(hide)]709 #[weight(<SelfWeightOf<T>>::create_multiple_items(token_ids.len() as u32))]710 fn mint_bulk(&mut self, caller: caller, to: address, token_ids: Vec<uint256>) -> Result<bool> {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 total_tokens = token_ids.len();721 for id in token_ids.into_iter() {722 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;723 if id != expected_index {724 return Err("item id should be next".into());725 }726 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;727 }728 let users = [(to.clone(), 1)]729 .into_iter()730 .collect::<BTreeMap<_, _>>()731 .try_into()732 .unwrap();733 let create_item_data = CreateItemData::<T::CrossAccountId> {734 users,735 properties: CollectionPropertiesVec::default(),736 };737 let data = (0..total_tokens)738 .map(|_| create_item_data.clone())739 .collect();740741 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)742 .map_err(dispatch_to_evm::<T>)?;743 Ok(true)744 }745746 /// @notice Function to mint multiple tokens with the given tokenUris.747 /// @dev `tokenIds` is array of pairs of token ID and token URI. Token IDs should be consecutive748 /// numbers and first number should be obtained with `nextTokenId` method749 /// @param to The new owner750 /// @param tokens array of pairs of token ID and token URI for minted tokens751 #[solidity(hide, rename_selector = "mintBulkWithTokenURI")]752 #[weight(<SelfWeightOf<T>>::create_multiple_items(tokens.len() as u32))]753 fn mint_bulk_with_token_uri(754 &mut self,755 caller: caller,756 to: address,757 tokens: Vec<(uint256, string)>,758 ) -> Result<bool> {759 let key = key::url();760 let caller = T::CrossAccountId::from_eth(caller);761 let to = T::CrossAccountId::from_eth(to);762 let mut expected_index = <TokensMinted<T>>::get(self.id)763 .checked_add(1)764 .ok_or("item id overflow")?;765 let budget = self766 .recorder767 .weight_calls_budget(<StructureWeight<T>>::find_parent());768769 let mut data = Vec::with_capacity(tokens.len());770 let users: BoundedBTreeMap<_, _, _> = [(to.clone(), 1)]771 .into_iter()772 .collect::<BTreeMap<_, _>>()773 .try_into()774 .unwrap();775 for (id, token_uri) in tokens {776 let id: u32 = id.try_into().map_err(|_| "token id overflow")?;777 if id != expected_index {778 return Err("item id should be next".into());779 }780 expected_index = expected_index.checked_add(1).ok_or("item id overflow")?;781782 let mut properties = CollectionPropertiesVec::default();783 properties784 .try_push(Property {785 key: key.clone(),786 value: token_uri787 .into_bytes()788 .try_into()789 .map_err(|_| "token uri is too long")?,790 })791 .map_err(|e| Error::Revert(alloc::format!("Can't add property: {:?}", e)))?;792793 let create_item_data = CreateItemData::<T::CrossAccountId> {794 users: users.clone(),795 properties,796 };797 data.push(create_item_data);798 }799800 <Pallet<T>>::create_multiple_items(self, &caller, data, &budget)801 .map_err(dispatch_to_evm::<T>)?;802 Ok(true)803 }804805 /// Returns EVM address for refungible token806 ///807 /// @param token ID of the token808 fn token_contract_address(&self, token: uint256) -> Result<address> {809 Ok(T::EvmTokenAddressMapping::token_to_address(810 self.id,811 token.try_into().map_err(|_| "token id overflow")?,812 ))813 }814}815816#[solidity_interface(817 name = UniqueRefungible,818 is(819 ERC721,820 ERC721Enumerable,821 ERC721UniqueExtensions,822 ERC721UniqueMintable,823 ERC721Burnable,824 ERC721Metadata(if(this.flags.erc721metadata)),825 Collection(via(common_mut returns CollectionHandle<T>)),826 TokenProperties,827 )828)]829impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]> {}830831// Not a tests, but code generators832generate_stubgen!(gen_impl, UniqueRefungibleCall<()>, true);833generate_stubgen!(gen_iface, UniqueRefungibleCall<()>, false);834835impl<T: Config> CommonEvmHandler for RefungibleHandle<T>836where837 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,838{839 const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");840 fn call(841 self,842 handle: &mut impl PrecompileHandle,843 ) -> Option<pallet_common::erc::PrecompileResult> {844 call::<T, UniqueRefungibleCall<T>, _, _>(handle, self)845 }846}