difftreelog
feat add eth methots for bulk properties
in: master
14 files changed
pallets/common/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//! This module contains the implementation of pallet methods for evm.1819use evm_coder::{20 solidity_interface, solidity, ToLog,21 types::*,22 execution::{Result, Error},23 weight,24};25pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};26use pallet_evm_coder_substrate::dispatch_to_evm;27use sp_std::vec::Vec;28use up_data_structs::{29 AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,30 SponsoringRateLimit, SponsorshipState,31};32use alloc::format;3334use crate::{35 Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,36 eth::{37 convert_cross_account_to_uint256, convert_cross_account_to_tuple,38 convert_tuple_to_cross_account,39 },40 weights::WeightInfo,41};4243/// Events for ethereum collection helper.44#[derive(ToLog)]45pub enum CollectionHelpersEvents {46 /// The collection has been created.47 CollectionCreated {48 /// Collection owner.49 #[indexed]50 owner: address,5152 /// Collection ID.53 #[indexed]54 collection_id: address,55 },56 /// The collection has been destroyed.57 CollectionDestroyed {58 /// Collection ID.59 #[indexed]60 collection_id: address,61 },62}6364/// Does not always represent a full collection, for RFT it is either65/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).66pub trait CommonEvmHandler {67 const CODE: &'static [u8];6869 /// Call precompiled handle.70 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;71}7273/// @title A contract that allows you to work with collections.74#[solidity_interface(name = Collection)]75impl<T: Config> CollectionHandle<T>76where77 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,78{79 /// Set collection property.80 ///81 /// @param key Property key.82 /// @param value Propery value.83 #[weight(<SelfWeightOf<T>>::set_collection_properties(1))]84 fn set_collection_property(85 &mut self,86 caller: caller,87 key: string,88 value: bytes,89 ) -> Result<void> {90 let caller = T::CrossAccountId::from_eth(caller);91 let key = <Vec<u8>>::from(key)92 .try_into()93 .map_err(|_| "key too large")?;94 let value = value.0.try_into().map_err(|_| "value too large")?;9596 <Pallet<T>>::set_collection_property(self, &caller, Property { key, value })97 .map_err(dispatch_to_evm::<T>)98 }99100 /// Delete collection property.101 ///102 /// @param key Property key.103 #[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]104 fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {105 self.consume_store_reads_and_writes(1, 1)?;106107 let caller = T::CrossAccountId::from_eth(caller);108 let key = <Vec<u8>>::from(key)109 .try_into()110 .map_err(|_| "key too large")?;111112 <Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)113 }114115 /// Get collection property.116 ///117 /// @dev Throws error if key not found.118 ///119 /// @param key Property key.120 /// @return bytes The property corresponding to the key.121 fn collection_property(&self, key: string) -> Result<bytes> {122 let key = <Vec<u8>>::from(key)123 .try_into()124 .map_err(|_| "key too large")?;125126 let props = <CollectionProperties<T>>::get(self.id);127 let prop = props.get(&key).ok_or("key not found")?;128129 Ok(bytes(prop.to_vec()))130 }131132 /// Set the sponsor of the collection.133 ///134 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.135 ///136 /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.137 fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {138 self.consume_store_reads_and_writes(1, 1)?;139140 check_is_owner_or_admin(caller, self)?;141142 let sponsor = T::CrossAccountId::from_eth(sponsor);143 self.set_sponsor(sponsor.as_sub().clone())144 .map_err(dispatch_to_evm::<T>)?;145 save(self)146 }147148 /// Set the sponsor of the collection.149 ///150 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.151 ///152 /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.153 fn set_collection_sponsor_cross(154 &mut self,155 caller: caller,156 sponsor: (address, uint256),157 ) -> Result<void> {158 self.consume_store_reads_and_writes(1, 1)?;159160 check_is_owner_or_admin(caller, self)?;161162 let sponsor = convert_tuple_to_cross_account::<T>(sponsor)?;163 self.set_sponsor(sponsor.as_sub().clone())164 .map_err(dispatch_to_evm::<T>)?;165 save(self)166 }167168 /// Whether there is a pending sponsor.169 fn has_collection_pending_sponsor(&self) -> Result<bool> {170 Ok(matches!(171 self.collection.sponsorship,172 SponsorshipState::Unconfirmed(_)173 ))174 }175176 /// Collection sponsorship confirmation.177 ///178 /// @dev After setting the sponsor for the collection, it must be confirmed with this function.179 fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {180 self.consume_store_writes(1)?;181182 let caller = T::CrossAccountId::from_eth(caller);183 if !self184 .confirm_sponsorship(caller.as_sub())185 .map_err(dispatch_to_evm::<T>)?186 {187 return Err("caller is not set as sponsor".into());188 }189 save(self)190 }191192 /// Remove collection sponsor.193 fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {194 self.consume_store_reads_and_writes(1, 1)?;195 check_is_owner_or_admin(caller, self)?;196 self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;197 save(self)198 }199200 /// Get current sponsor.201 ///202 /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.203 fn collection_sponsor(&self) -> Result<(address, uint256)> {204 let sponsor = match self.collection.sponsorship.sponsor() {205 Some(sponsor) => sponsor,206 None => return Ok(Default::default()),207 };208 let sponsor = T::CrossAccountId::from_sub(sponsor.clone());209 let result: (address, uint256) = if sponsor.is_canonical_substrate() {210 let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);211 (Default::default(), sponsor)212 } else {213 let sponsor = *sponsor.as_eth();214 (sponsor, Default::default())215 };216 Ok(result)217 }218219 /// Set limits for the collection.220 /// @dev Throws error if limit not found.221 /// @param limit Name of the limit. Valid names:222 /// "accountTokenOwnershipLimit",223 /// "sponsoredDataSize",224 /// "sponsoredDataRateLimit",225 /// "tokenLimit",226 /// "sponsorTransferTimeout",227 /// "sponsorApproveTimeout"228 /// @param value Value of the limit.229 #[solidity(rename_selector = "setCollectionLimit")]230 fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {231 self.consume_store_reads_and_writes(1, 1)?;232233 check_is_owner_or_admin(caller, self)?;234 let mut limits = self.limits.clone();235236 match limit.as_str() {237 "accountTokenOwnershipLimit" => {238 limits.account_token_ownership_limit = Some(value);239 }240 "sponsoredDataSize" => {241 limits.sponsored_data_size = Some(value);242 }243 "sponsoredDataRateLimit" => {244 limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));245 }246 "tokenLimit" => {247 limits.token_limit = Some(value);248 }249 "sponsorTransferTimeout" => {250 limits.sponsor_transfer_timeout = Some(value);251 }252 "sponsorApproveTimeout" => {253 limits.sponsor_approve_timeout = Some(value);254 }255 _ => {256 return Err(Error::Revert(format!(257 "unknown integer limit \"{}\"",258 limit259 )))260 }261 }262 self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)263 .map_err(dispatch_to_evm::<T>)?;264 save(self)265 }266267 /// Set limits for the collection.268 /// @dev Throws error if limit not found.269 /// @param limit Name of the limit. Valid names:270 /// "ownerCanTransfer",271 /// "ownerCanDestroy",272 /// "transfersEnabled"273 /// @param value Value of the limit.274 #[solidity(rename_selector = "setCollectionLimit")]275 fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {276 self.consume_store_reads_and_writes(1, 1)?;277278 check_is_owner_or_admin(caller, self)?;279 let mut limits = self.limits.clone();280281 match limit.as_str() {282 "ownerCanTransfer" => {283 limits.owner_can_transfer = Some(value);284 }285 "ownerCanDestroy" => {286 limits.owner_can_destroy = Some(value);287 }288 "transfersEnabled" => {289 limits.transfers_enabled = Some(value);290 }291 _ => {292 return Err(Error::Revert(format!(293 "unknown boolean limit \"{}\"",294 limit295 )))296 }297 }298 self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)299 .map_err(dispatch_to_evm::<T>)?;300 save(self)301 }302303 /// Get contract address.304 fn contract_address(&self) -> Result<address> {305 Ok(crate::eth::collection_id_to_address(self.id))306 }307308 /// Add collection admin.309 /// @param newAdmin Cross account administrator address.310 fn add_collection_admin_cross(311 &mut self,312 caller: caller,313 new_admin: (address, uint256),314 ) -> Result<void> {315 self.consume_store_writes(2)?;316317 let caller = T::CrossAccountId::from_eth(caller);318 let new_admin = convert_tuple_to_cross_account::<T>(new_admin)?;319 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;320 Ok(())321 }322323 /// Remove collection admin.324 /// @param admin Cross account administrator address.325 fn remove_collection_admin_cross(326 &mut self,327 caller: caller,328 admin: (address, uint256),329 ) -> Result<void> {330 self.consume_store_writes(2)?;331332 let caller = T::CrossAccountId::from_eth(caller);333 let admin = convert_tuple_to_cross_account::<T>(admin)?;334 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;335 Ok(())336 }337338 /// Add collection admin.339 /// @param newAdmin Address of the added administrator.340 fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {341 self.consume_store_writes(2)?;342343 let caller = T::CrossAccountId::from_eth(caller);344 let new_admin = T::CrossAccountId::from_eth(new_admin);345 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;346 Ok(())347 }348349 /// Remove collection admin.350 ///351 /// @param admin Address of the removed administrator.352 fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {353 self.consume_store_writes(2)?;354355 let caller = T::CrossAccountId::from_eth(caller);356 let admin = T::CrossAccountId::from_eth(admin);357 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;358 Ok(())359 }360361 /// Toggle accessibility of collection nesting.362 ///363 /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'364 #[solidity(rename_selector = "setCollectionNesting")]365 fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {366 self.consume_store_reads_and_writes(1, 1)?;367368 check_is_owner_or_admin(caller, self)?;369370 let mut permissions = self.collection.permissions.clone();371 let mut nesting = permissions.nesting().clone();372 nesting.token_owner = enable;373 nesting.restricted = None;374 permissions.nesting = Some(nesting);375376 self.collection.permissions = <Pallet<T>>::clamp_permissions(377 self.collection.mode.clone(),378 &self.collection.permissions,379 permissions,380 )381 .map_err(dispatch_to_evm::<T>)?;382383 save(self)384 }385386 /// Toggle accessibility of collection nesting.387 ///388 /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'389 /// @param collections Addresses of collections that will be available for nesting.390 #[solidity(rename_selector = "setCollectionNesting")]391 fn set_nesting(392 &mut self,393 caller: caller,394 enable: bool,395 collections: Vec<address>,396 ) -> Result<void> {397 self.consume_store_reads_and_writes(1, 1)?;398399 if collections.is_empty() {400 return Err("no addresses provided".into());401 }402 check_is_owner_or_admin(caller, self)?;403404 let mut permissions = self.collection.permissions.clone();405 match enable {406 false => {407 let mut nesting = permissions.nesting().clone();408 nesting.token_owner = false;409 nesting.restricted = None;410 permissions.nesting = Some(nesting);411 }412 true => {413 let mut bv = OwnerRestrictedSet::new();414 for i in collections {415 bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {416 Error::Revert("Can't convert address into collection id".into())417 })?)418 .map_err(|_| "too many collections")?;419 }420 let mut nesting = permissions.nesting().clone();421 nesting.token_owner = true;422 nesting.restricted = Some(bv);423 permissions.nesting = Some(nesting);424 }425 };426427 self.collection.permissions = <Pallet<T>>::clamp_permissions(428 self.collection.mode.clone(),429 &self.collection.permissions,430 permissions,431 )432 .map_err(dispatch_to_evm::<T>)?;433434 save(self)435 }436437 /// Set the collection access method.438 /// @param mode Access mode439 /// 0 for Normal440 /// 1 for AllowList441 fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {442 self.consume_store_reads_and_writes(1, 1)?;443444 check_is_owner_or_admin(caller, self)?;445 let permissions = CollectionPermissions {446 access: Some(match mode {447 0 => AccessMode::Normal,448 1 => AccessMode::AllowList,449 _ => return Err("not supported access mode".into()),450 }),451 ..Default::default()452 };453 self.collection.permissions = <Pallet<T>>::clamp_permissions(454 self.collection.mode.clone(),455 &self.collection.permissions,456 permissions,457 )458 .map_err(dispatch_to_evm::<T>)?;459460 save(self)461 }462463 /// Checks that user allowed to operate with collection.464 ///465 /// @param user User address to check.466 fn allowed(&self, user: address) -> Result<bool> {467 Ok(Pallet::<T>::allowed(468 self.id,469 T::CrossAccountId::from_eth(user),470 ))471 }472473 /// Add the user to the allowed list.474 ///475 /// @param user Address of a trusted user.476 fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {477 self.consume_store_writes(1)?;478479 let caller = T::CrossAccountId::from_eth(caller);480 let user = T::CrossAccountId::from_eth(user);481 <Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;482 Ok(())483 }484485 /// Add user to allowed list.486 ///487 /// @param user User cross account address.488 fn add_to_collection_allow_list_cross(489 &mut self,490 caller: caller,491 user: (address, uint256),492 ) -> Result<void> {493 self.consume_store_writes(1)?;494495 let caller = T::CrossAccountId::from_eth(caller);496 let user = convert_tuple_to_cross_account::<T>(user)?;497 Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;498 Ok(())499 }500501 /// Remove the user from the allowed list.502 ///503 /// @param user Address of a removed user.504 fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {505 self.consume_store_writes(1)?;506507 let caller = T::CrossAccountId::from_eth(caller);508 let user = T::CrossAccountId::from_eth(user);509 <Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;510 Ok(())511 }512513 /// Remove user from allowed list.514 ///515 /// @param user User cross account address.516 fn remove_from_collection_allow_list_cross(517 &mut self,518 caller: caller,519 user: (address, uint256),520 ) -> Result<void> {521 self.consume_store_writes(1)?;522523 let caller = T::CrossAccountId::from_eth(caller);524 let user = convert_tuple_to_cross_account::<T>(user)?;525 Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;526 Ok(())527 }528529 /// Switch permission for minting.530 ///531 /// @param mode Enable if "true".532 fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {533 self.consume_store_reads_and_writes(1, 1)?;534535 check_is_owner_or_admin(caller, self)?;536 let permissions = CollectionPermissions {537 mint_mode: Some(mode),538 ..Default::default()539 };540 self.collection.permissions = <Pallet<T>>::clamp_permissions(541 self.collection.mode.clone(),542 &self.collection.permissions,543 permissions,544 )545 .map_err(dispatch_to_evm::<T>)?;546547 save(self)548 }549550 /// Check that account is the owner or admin of the collection551 ///552 /// @param user account to verify553 /// @return "true" if account is the owner or admin554 #[solidity(rename_selector = "isOwnerOrAdmin")]555 fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {556 let user = T::CrossAccountId::from_eth(user);557 Ok(self.is_owner_or_admin(&user))558 }559560 /// Check that account is the owner or admin of the collection561 ///562 /// @param user User cross account to verify563 /// @return "true" if account is the owner or admin564 fn is_owner_or_admin_cross(&self, user: (address, uint256)) -> Result<bool> {565 let user = convert_tuple_to_cross_account::<T>(user)?;566 Ok(self.is_owner_or_admin(&user))567 }568569 /// Returns collection type570 ///571 /// @return `Fungible` or `NFT` or `ReFungible`572 fn unique_collection_type(&self) -> Result<string> {573 let mode = match self.collection.mode {574 CollectionMode::Fungible(_) => "Fungible",575 CollectionMode::NFT => "NFT",576 CollectionMode::ReFungible => "ReFungible",577 };578 Ok(mode.into())579 }580581 /// Get collection owner.582 ///583 /// @return Tuble with sponsor address and his substrate mirror.584 /// If address is canonical then substrate mirror is zero and vice versa.585 fn collection_owner(&self) -> Result<(address, uint256)> {586 Ok(convert_cross_account_to_tuple::<T>(587 &T::CrossAccountId::from_sub(self.owner.clone()),588 ))589 }590591 /// Changes collection owner to another account592 ///593 /// @dev Owner can be changed only by current owner594 /// @param newOwner new owner account595 #[solidity(rename_selector = "changeCollectionOwner")]596 fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {597 self.consume_store_writes(1)?;598599 let caller = T::CrossAccountId::from_eth(caller);600 let new_owner = T::CrossAccountId::from_eth(new_owner);601 self.set_owner_internal(caller, new_owner)602 .map_err(dispatch_to_evm::<T>)603 }604605 /// Get collection administrators606 ///607 /// @return Vector of tuples with admins address and his substrate mirror.608 /// If address is canonical then substrate mirror is zero and vice versa.609 fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {610 let result = crate::IsAdmin::<T>::iter_prefix((self.id,))611 .map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::<T>(&admin))612 .collect();613 Ok(result)614 }615616 /// Changes collection owner to another account617 ///618 /// @dev Owner can be changed only by current owner619 /// @param newOwner new owner cross account620 fn set_owner_cross(&mut self, caller: caller, new_owner: (address, uint256)) -> Result<void> {621 self.consume_store_writes(1)?;622623 let caller = T::CrossAccountId::from_eth(caller);624 let new_owner = convert_tuple_to_cross_account::<T>(new_owner)?;625 self.set_owner_internal(caller, new_owner)626 .map_err(dispatch_to_evm::<T>)627 }628}629630/// ### Note631/// Do not forget to add: `self.consume_store_reads(1)?;`632fn check_is_owner_or_admin<T: Config>(633 caller: caller,634 collection: &CollectionHandle<T>,635) -> Result<T::CrossAccountId> {636 let caller = T::CrossAccountId::from_eth(caller);637 collection638 .check_is_owner_or_admin(&caller)639 .map_err(dispatch_to_evm::<T>)?;640 Ok(caller)641}642643/// ### Note644/// Do not forget to add: `self.consume_store_writes(1)?;`645fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {646 collection647 .check_is_internal()648 .map_err(dispatch_to_evm::<T>)?;649 collection.save().map_err(dispatch_to_evm::<T>)?;650 Ok(())651}652653/// Contains static property keys and values.654pub mod static_property {655 use evm_coder::{656 execution::{Result, Error},657 };658 use alloc::format;659660 const EXPECT_CONVERT_ERROR: &str = "length < limit";661662 /// Keys.663 pub mod key {664 use super::*;665666 /// Key "baseURI".667 pub fn base_uri() -> up_data_structs::PropertyKey {668 property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)669 }670671 /// Key "url".672 pub fn url() -> up_data_structs::PropertyKey {673 property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)674 }675676 /// Key "suffix".677 pub fn suffix() -> up_data_structs::PropertyKey {678 property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)679 }680681 /// Key "parentNft".682 pub fn parent_nft() -> up_data_structs::PropertyKey {683 property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)684 }685 }686687 /// Convert `byte` to [`PropertyKey`].688 pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {689 bytes.to_vec().try_into().map_err(|_| {690 Error::Revert(format!(691 "Property key is too long. Max length is {}.",692 up_data_structs::PropertyKey::bound()693 ))694 })695 }696697 /// Convert `bytes` to [`PropertyValue`].698 pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {699 bytes.to_vec().try_into().map_err(|_| {700 Error::Revert(format!(701 "Property key is too long. Max length is {}.",702 up_data_structs::PropertyKey::bound()703 ))704 })705 }706}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//! This module contains the implementation of pallet methods for evm.1819use evm_coder::{20 solidity_interface, solidity, ToLog,21 types::*,22 execution::{Result, Error},23 weight,24};25pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};26use pallet_evm_coder_substrate::dispatch_to_evm;27use sp_std::vec::Vec;28use up_data_structs::{29 AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,30 SponsoringRateLimit, SponsorshipState, PropertyKey,31};32use alloc::format;3334use crate::{35 Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,36 eth::{37 convert_cross_account_to_uint256, convert_cross_account_to_tuple,38 convert_tuple_to_cross_account,39 },40 weights::WeightInfo,41};4243/// Events for ethereum collection helper.44#[derive(ToLog)]45pub enum CollectionHelpersEvents {46 /// The collection has been created.47 CollectionCreated {48 /// Collection owner.49 #[indexed]50 owner: address,5152 /// Collection ID.53 #[indexed]54 collection_id: address,55 },56 /// The collection has been destroyed.57 CollectionDestroyed {58 /// Collection ID.59 #[indexed]60 collection_id: address,61 },62}6364/// Does not always represent a full collection, for RFT it is either65/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).66pub trait CommonEvmHandler {67 const CODE: &'static [u8];6869 /// Call precompiled handle.70 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;71}7273/// @title A contract that allows you to work with collections.74#[solidity_interface(name = Collection)]75impl<T: Config> CollectionHandle<T>76where77 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,78{79 /// Set collection property.80 ///81 /// @param key Property key.82 /// @param value Propery value.83 #[weight(<SelfWeightOf<T>>::set_collection_properties(1))]84 fn set_collection_property(85 &mut self,86 caller: caller,87 key: string,88 value: bytes,89 ) -> Result<void> {90 let caller = T::CrossAccountId::from_eth(caller);91 let key = <Vec<u8>>::from(key)92 .try_into()93 .map_err(|_| "key too large")?;94 let value = value.0.try_into().map_err(|_| "value too large")?;9596 <Pallet<T>>::set_collection_property(self, &caller, Property { key, value })97 .map_err(dispatch_to_evm::<T>)98 }99100 /// Set collection properties.101 ///102 /// @param properties Vector of properties key/value pair.103 #[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]104 fn set_collection_properties(105 &mut self,106 caller: caller,107 properties: Vec<(string, bytes)>,108 ) -> Result<void> {109 for (key, value) in properties.into_iter() {110 self.set_collection_property(caller, key, value)?;111 }112 Ok(())113 }114115 /// Delete collection property.116 ///117 /// @param key Property key.118 #[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]119 fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {120 self.consume_store_reads_and_writes(1, 1)?;121122 let caller = T::CrossAccountId::from_eth(caller);123 let key = <Vec<u8>>::from(key)124 .try_into()125 .map_err(|_| "key too large")?;126127 <Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)128 }129130 /// Get collection property.131 ///132 /// @dev Throws error if key not found.133 ///134 /// @param key Property key.135 /// @return bytes The property corresponding to the key.136 fn collection_property(&self, key: string) -> Result<bytes> {137 let key = <Vec<u8>>::from(key)138 .try_into()139 .map_err(|_| "key too large")?;140141 let props = CollectionProperties::<T>::get(self.id);142 let prop = props.get(&key).ok_or("key not found")?;143144 Ok(bytes(prop.to_vec()))145 }146147 /// Get collection properties.148 ///149 /// @param keys Properties keys.150 /// @return Vector of properties key/value pairs.151 fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<(string, bytes)>> {152 let mut keys_ = Vec::<PropertyKey>::with_capacity(keys.len());153 for key in keys {154 keys_.push(155 <Vec<u8>>::from(key)156 .try_into()157 .map_err(|_| Error::Revert("key too large".into()))?,158 )159 }160 let properties = Pallet::<T>::filter_collection_properties(self.id, Some(keys_))161 .map_err(dispatch_to_evm::<T>)?;162163 let mut properties_ = Vec::<(string, bytes)>::with_capacity(properties.len());164 for p in properties {165 let key =166 string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;167 let value = bytes(p.value.to_vec());168 properties_.push((key, value));169 }170 Ok(properties_)171 }172173 /// Set the sponsor of the collection.174 ///175 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.176 ///177 /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.178 fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {179 self.consume_store_reads_and_writes(1, 1)?;180181 check_is_owner_or_admin(caller, self)?;182183 let sponsor = T::CrossAccountId::from_eth(sponsor);184 self.set_sponsor(sponsor.as_sub().clone())185 .map_err(dispatch_to_evm::<T>)?;186 save(self)187 }188189 /// Set the sponsor of the collection.190 ///191 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.192 ///193 /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.194 fn set_collection_sponsor_cross(195 &mut self,196 caller: caller,197 sponsor: (address, uint256),198 ) -> Result<void> {199 self.consume_store_reads_and_writes(1, 1)?;200201 check_is_owner_or_admin(caller, self)?;202203 let sponsor = convert_tuple_to_cross_account::<T>(sponsor)?;204 self.set_sponsor(sponsor.as_sub().clone())205 .map_err(dispatch_to_evm::<T>)?;206 save(self)207 }208209 /// Whether there is a pending sponsor.210 fn has_collection_pending_sponsor(&self) -> Result<bool> {211 Ok(matches!(212 self.collection.sponsorship,213 SponsorshipState::Unconfirmed(_)214 ))215 }216217 /// Collection sponsorship confirmation.218 ///219 /// @dev After setting the sponsor for the collection, it must be confirmed with this function.220 fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {221 self.consume_store_writes(1)?;222223 let caller = T::CrossAccountId::from_eth(caller);224 if !self225 .confirm_sponsorship(caller.as_sub())226 .map_err(dispatch_to_evm::<T>)?227 {228 return Err("caller is not set as sponsor".into());229 }230 save(self)231 }232233 /// Remove collection sponsor.234 fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {235 self.consume_store_reads_and_writes(1, 1)?;236 check_is_owner_or_admin(caller, self)?;237 self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;238 save(self)239 }240241 /// Get current sponsor.242 ///243 /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.244 fn collection_sponsor(&self) -> Result<(address, uint256)> {245 let sponsor = match self.collection.sponsorship.sponsor() {246 Some(sponsor) => sponsor,247 None => return Ok(Default::default()),248 };249 let sponsor = T::CrossAccountId::from_sub(sponsor.clone());250 let result: (address, uint256) = if sponsor.is_canonical_substrate() {251 let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);252 (Default::default(), sponsor)253 } else {254 let sponsor = *sponsor.as_eth();255 (sponsor, Default::default())256 };257 Ok(result)258 }259260 /// Set limits for the collection.261 /// @dev Throws error if limit not found.262 /// @param limit Name of the limit. Valid names:263 /// "accountTokenOwnershipLimit",264 /// "sponsoredDataSize",265 /// "sponsoredDataRateLimit",266 /// "tokenLimit",267 /// "sponsorTransferTimeout",268 /// "sponsorApproveTimeout"269 /// @param value Value of the limit.270 #[solidity(rename_selector = "setCollectionLimit")]271 fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {272 self.consume_store_reads_and_writes(1, 1)?;273274 check_is_owner_or_admin(caller, self)?;275 let mut limits = self.limits.clone();276277 match limit.as_str() {278 "accountTokenOwnershipLimit" => {279 limits.account_token_ownership_limit = Some(value);280 }281 "sponsoredDataSize" => {282 limits.sponsored_data_size = Some(value);283 }284 "sponsoredDataRateLimit" => {285 limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));286 }287 "tokenLimit" => {288 limits.token_limit = Some(value);289 }290 "sponsorTransferTimeout" => {291 limits.sponsor_transfer_timeout = Some(value);292 }293 "sponsorApproveTimeout" => {294 limits.sponsor_approve_timeout = Some(value);295 }296 _ => {297 return Err(Error::Revert(format!(298 "unknown integer limit \"{}\"",299 limit300 )))301 }302 }303 self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)304 .map_err(dispatch_to_evm::<T>)?;305 save(self)306 }307308 /// Set limits for the collection.309 /// @dev Throws error if limit not found.310 /// @param limit Name of the limit. Valid names:311 /// "ownerCanTransfer",312 /// "ownerCanDestroy",313 /// "transfersEnabled"314 /// @param value Value of the limit.315 #[solidity(rename_selector = "setCollectionLimit")]316 fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {317 self.consume_store_reads_and_writes(1, 1)?;318319 check_is_owner_or_admin(caller, self)?;320 let mut limits = self.limits.clone();321322 match limit.as_str() {323 "ownerCanTransfer" => {324 limits.owner_can_transfer = Some(value);325 }326 "ownerCanDestroy" => {327 limits.owner_can_destroy = Some(value);328 }329 "transfersEnabled" => {330 limits.transfers_enabled = Some(value);331 }332 _ => {333 return Err(Error::Revert(format!(334 "unknown boolean limit \"{}\"",335 limit336 )))337 }338 }339 self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)340 .map_err(dispatch_to_evm::<T>)?;341 save(self)342 }343344 /// Get contract address.345 fn contract_address(&self) -> Result<address> {346 Ok(crate::eth::collection_id_to_address(self.id))347 }348349 /// Add collection admin.350 /// @param newAdmin Cross account administrator address.351 fn add_collection_admin_cross(352 &mut self,353 caller: caller,354 new_admin: (address, uint256),355 ) -> Result<void> {356 self.consume_store_writes(2)?;357358 let caller = T::CrossAccountId::from_eth(caller);359 let new_admin = convert_tuple_to_cross_account::<T>(new_admin)?;360 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;361 Ok(())362 }363364 /// Remove collection admin.365 /// @param admin Cross account administrator address.366 fn remove_collection_admin_cross(367 &mut self,368 caller: caller,369 admin: (address, uint256),370 ) -> Result<void> {371 self.consume_store_writes(2)?;372373 let caller = T::CrossAccountId::from_eth(caller);374 let admin = convert_tuple_to_cross_account::<T>(admin)?;375 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;376 Ok(())377 }378379 /// Add collection admin.380 /// @param newAdmin Address of the added administrator.381 fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {382 self.consume_store_writes(2)?;383384 let caller = T::CrossAccountId::from_eth(caller);385 let new_admin = T::CrossAccountId::from_eth(new_admin);386 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;387 Ok(())388 }389390 /// Remove collection admin.391 ///392 /// @param admin Address of the removed administrator.393 fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {394 self.consume_store_writes(2)?;395396 let caller = T::CrossAccountId::from_eth(caller);397 let admin = T::CrossAccountId::from_eth(admin);398 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;399 Ok(())400 }401402 /// Toggle accessibility of collection nesting.403 ///404 /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'405 #[solidity(rename_selector = "setCollectionNesting")]406 fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {407 self.consume_store_reads_and_writes(1, 1)?;408409 check_is_owner_or_admin(caller, self)?;410411 let mut permissions = self.collection.permissions.clone();412 let mut nesting = permissions.nesting().clone();413 nesting.token_owner = enable;414 nesting.restricted = None;415 permissions.nesting = Some(nesting);416417 self.collection.permissions = <Pallet<T>>::clamp_permissions(418 self.collection.mode.clone(),419 &self.collection.permissions,420 permissions,421 )422 .map_err(dispatch_to_evm::<T>)?;423424 save(self)425 }426427 /// Toggle accessibility of collection nesting.428 ///429 /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'430 /// @param collections Addresses of collections that will be available for nesting.431 #[solidity(rename_selector = "setCollectionNesting")]432 fn set_nesting(433 &mut self,434 caller: caller,435 enable: bool,436 collections: Vec<address>,437 ) -> Result<void> {438 self.consume_store_reads_and_writes(1, 1)?;439440 if collections.is_empty() {441 return Err("no addresses provided".into());442 }443 check_is_owner_or_admin(caller, self)?;444445 let mut permissions = self.collection.permissions.clone();446 match enable {447 false => {448 let mut nesting = permissions.nesting().clone();449 nesting.token_owner = false;450 nesting.restricted = None;451 permissions.nesting = Some(nesting);452 }453 true => {454 let mut bv = OwnerRestrictedSet::new();455 for i in collections {456 bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {457 Error::Revert("Can't convert address into collection id".into())458 })?)459 .map_err(|_| "too many collections")?;460 }461 let mut nesting = permissions.nesting().clone();462 nesting.token_owner = true;463 nesting.restricted = Some(bv);464 permissions.nesting = Some(nesting);465 }466 };467468 self.collection.permissions = <Pallet<T>>::clamp_permissions(469 self.collection.mode.clone(),470 &self.collection.permissions,471 permissions,472 )473 .map_err(dispatch_to_evm::<T>)?;474475 save(self)476 }477478 /// Set the collection access method.479 /// @param mode Access mode480 /// 0 for Normal481 /// 1 for AllowList482 fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {483 self.consume_store_reads_and_writes(1, 1)?;484485 check_is_owner_or_admin(caller, self)?;486 let permissions = CollectionPermissions {487 access: Some(match mode {488 0 => AccessMode::Normal,489 1 => AccessMode::AllowList,490 _ => return Err("not supported access mode".into()),491 }),492 ..Default::default()493 };494 self.collection.permissions = <Pallet<T>>::clamp_permissions(495 self.collection.mode.clone(),496 &self.collection.permissions,497 permissions,498 )499 .map_err(dispatch_to_evm::<T>)?;500501 save(self)502 }503504 /// Checks that user allowed to operate with collection.505 ///506 /// @param user User address to check.507 fn allowed(&self, user: address) -> Result<bool> {508 Ok(Pallet::<T>::allowed(509 self.id,510 T::CrossAccountId::from_eth(user),511 ))512 }513514 /// Add the user to the allowed list.515 ///516 /// @param user Address of a trusted user.517 fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {518 self.consume_store_writes(1)?;519520 let caller = T::CrossAccountId::from_eth(caller);521 let user = T::CrossAccountId::from_eth(user);522 <Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;523 Ok(())524 }525526 /// Add user to allowed list.527 ///528 /// @param user User cross account address.529 fn add_to_collection_allow_list_cross(530 &mut self,531 caller: caller,532 user: (address, uint256),533 ) -> Result<void> {534 self.consume_store_writes(1)?;535536 let caller = T::CrossAccountId::from_eth(caller);537 let user = convert_tuple_to_cross_account::<T>(user)?;538 Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;539 Ok(())540 }541542 /// Remove the user from the allowed list.543 ///544 /// @param user Address of a removed user.545 fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {546 self.consume_store_writes(1)?;547548 let caller = T::CrossAccountId::from_eth(caller);549 let user = T::CrossAccountId::from_eth(user);550 <Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;551 Ok(())552 }553554 /// Remove user from allowed list.555 ///556 /// @param user User cross account address.557 fn remove_from_collection_allow_list_cross(558 &mut self,559 caller: caller,560 user: (address, uint256),561 ) -> Result<void> {562 self.consume_store_writes(1)?;563564 let caller = T::CrossAccountId::from_eth(caller);565 let user = convert_tuple_to_cross_account::<T>(user)?;566 Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;567 Ok(())568 }569570 /// Switch permission for minting.571 ///572 /// @param mode Enable if "true".573 fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {574 self.consume_store_reads_and_writes(1, 1)?;575576 check_is_owner_or_admin(caller, self)?;577 let permissions = CollectionPermissions {578 mint_mode: Some(mode),579 ..Default::default()580 };581 self.collection.permissions = <Pallet<T>>::clamp_permissions(582 self.collection.mode.clone(),583 &self.collection.permissions,584 permissions,585 )586 .map_err(dispatch_to_evm::<T>)?;587588 save(self)589 }590591 /// Check that account is the owner or admin of the collection592 ///593 /// @param user account to verify594 /// @return "true" if account is the owner or admin595 #[solidity(rename_selector = "isOwnerOrAdmin")]596 fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {597 let user = T::CrossAccountId::from_eth(user);598 Ok(self.is_owner_or_admin(&user))599 }600601 /// Check that account is the owner or admin of the collection602 ///603 /// @param user User cross account to verify604 /// @return "true" if account is the owner or admin605 fn is_owner_or_admin_cross(&self, user: (address, uint256)) -> Result<bool> {606 let user = convert_tuple_to_cross_account::<T>(user)?;607 Ok(self.is_owner_or_admin(&user))608 }609610 /// Returns collection type611 ///612 /// @return `Fungible` or `NFT` or `ReFungible`613 fn unique_collection_type(&self) -> Result<string> {614 let mode = match self.collection.mode {615 CollectionMode::Fungible(_) => "Fungible",616 CollectionMode::NFT => "NFT",617 CollectionMode::ReFungible => "ReFungible",618 };619 Ok(mode.into())620 }621622 /// Get collection owner.623 ///624 /// @return Tuble with sponsor address and his substrate mirror.625 /// If address is canonical then substrate mirror is zero and vice versa.626 fn collection_owner(&self) -> Result<(address, uint256)> {627 Ok(convert_cross_account_to_tuple::<T>(628 &T::CrossAccountId::from_sub(self.owner.clone()),629 ))630 }631632 /// Changes collection owner to another account633 ///634 /// @dev Owner can be changed only by current owner635 /// @param newOwner new owner account636 #[solidity(rename_selector = "changeCollectionOwner")]637 fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {638 self.consume_store_writes(1)?;639640 let caller = T::CrossAccountId::from_eth(caller);641 let new_owner = T::CrossAccountId::from_eth(new_owner);642 self.set_owner_internal(caller, new_owner)643 .map_err(dispatch_to_evm::<T>)644 }645646 /// Get collection administrators647 ///648 /// @return Vector of tuples with admins address and his substrate mirror.649 /// If address is canonical then substrate mirror is zero and vice versa.650 fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {651 let result = crate::IsAdmin::<T>::iter_prefix((self.id,))652 .map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::<T>(&admin))653 .collect();654 Ok(result)655 }656657 /// Changes collection owner to another account658 ///659 /// @dev Owner can be changed only by current owner660 /// @param newOwner new owner cross account661 fn set_owner_cross(&mut self, caller: caller, new_owner: (address, uint256)) -> Result<void> {662 self.consume_store_writes(1)?;663664 let caller = T::CrossAccountId::from_eth(caller);665 let new_owner = convert_tuple_to_cross_account::<T>(new_owner)?;666 self.set_owner_internal(caller, new_owner)667 .map_err(dispatch_to_evm::<T>)668 }669}670671/// ### Note672/// Do not forget to add: `self.consume_store_reads(1)?;`673fn check_is_owner_or_admin<T: Config>(674 caller: caller,675 collection: &CollectionHandle<T>,676) -> Result<T::CrossAccountId> {677 let caller = T::CrossAccountId::from_eth(caller);678 collection679 .check_is_owner_or_admin(&caller)680 .map_err(dispatch_to_evm::<T>)?;681 Ok(caller)682}683684/// ### Note685/// Do not forget to add: `self.consume_store_writes(1)?;`686fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {687 collection688 .check_is_internal()689 .map_err(dispatch_to_evm::<T>)?;690 collection.save().map_err(dispatch_to_evm::<T>)?;691 Ok(())692}693694/// Contains static property keys and values.695pub mod static_property {696 use evm_coder::{697 execution::{Result, Error},698 };699 use alloc::format;700701 const EXPECT_CONVERT_ERROR: &str = "length < limit";702703 /// Keys.704 pub mod key {705 use super::*;706707 /// Key "baseURI".708 pub fn base_uri() -> up_data_structs::PropertyKey {709 property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)710 }711712 /// Key "url".713 pub fn url() -> up_data_structs::PropertyKey {714 property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)715 }716717 /// Key "suffix".718 pub fn suffix() -> up_data_structs::PropertyKey {719 property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)720 }721722 /// Key "parentNft".723 pub fn parent_nft() -> up_data_structs::PropertyKey {724 property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)725 }726 }727728 /// Convert `byte` to [`PropertyKey`].729 pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {730 bytes.to_vec().try_into().map_err(|_| {731 Error::Revert(format!(732 "Property key is too long. Max length is {}.",733 up_data_structs::PropertyKey::bound()734 ))735 })736 }737738 /// Convert `bytes` to [`PropertyValue`].739 pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {740 bytes.to_vec().try_into().map_err(|_| {741 Error::Revert(format!(742 "Property key is too long. Max length is {}.",743 up_data_structs::PropertyKey::bound()744 ))745 })746 }747}pallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterbothbinary blob — no preview
pallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth--- a/pallets/fungible/src/stubs/UniqueFungible.sol
+++ b/pallets/fungible/src/stubs/UniqueFungible.sol
@@ -18,7 +18,7 @@
}
/// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x25d897dc
+/// @dev the ERC-165 identifier for this interface is 0x5d354410
contract Collection is Dummy, ERC165 {
/// Set collection property.
///
@@ -33,6 +33,17 @@
dummy = 0;
}
+ /// Set collection properties.
+ ///
+ /// @param properties Vector of properties key/value pair.
+ /// @dev EVM selector for this function is: 0x50b26b2a,
+ /// or in textual repr: setCollectionProperties((string,bytes)[])
+ function setCollectionProperties(Tuple10[] memory properties) public {
+ require(false, stub_error);
+ properties;
+ dummy = 0;
+ }
+
/// Delete collection property.
///
/// @param key Property key.
@@ -59,6 +70,19 @@
return hex"";
}
+ /// Get collection properties.
+ ///
+ /// @param keys Properties keys.
+ /// @return Vector of properties key/value pairs.
+ /// @dev EVM selector for this function is: 0x285fb8e6,
+ /// or in textual repr: collectionProperties(string[])
+ function collectionProperties(string[] memory keys) public view returns (Tuple10[] memory) {
+ require(false, stub_error);
+ keys;
+ dummy;
+ return new Tuple10[](0);
+ }
+
/// Set the sponsor of the collection.
///
/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
@@ -397,6 +421,12 @@
}
}
+/// @dev anonymous struct
+struct Tuple10 {
+ string field_0;
+ bytes field_1;
+}
+
/// @dev the ERC-165 identifier for this interface is 0x032e5926
contract ERC20UniqueExtensions is Dummy, ERC165 {
/// @dev EVM selector for this function is: 0x0ecd0ab0,
pallets/nonfungible/src/stubs/UniqueNFT.rawdiffbeforeafterbothbinary blob — no preview
pallets/nonfungible/src/stubs/UniqueNFT.soldiffbeforeafterboth--- a/pallets/nonfungible/src/stubs/UniqueNFT.sol
+++ b/pallets/nonfungible/src/stubs/UniqueNFT.sol
@@ -91,7 +91,7 @@
}
/// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x25d897dc
+/// @dev the ERC-165 identifier for this interface is 0x5d354410
contract Collection is Dummy, ERC165 {
/// Set collection property.
///
@@ -106,6 +106,17 @@
dummy = 0;
}
+ /// Set collection properties.
+ ///
+ /// @param properties Vector of properties key/value pair.
+ /// @dev EVM selector for this function is: 0x50b26b2a,
+ /// or in textual repr: setCollectionProperties((string,bytes)[])
+ function setCollectionProperties(Tuple19[] memory properties) public {
+ require(false, stub_error);
+ properties;
+ dummy = 0;
+ }
+
/// Delete collection property.
///
/// @param key Property key.
@@ -132,6 +143,19 @@
return hex"";
}
+ /// Get collection properties.
+ ///
+ /// @param keys Properties keys.
+ /// @return Vector of properties key/value pairs.
+ /// @dev EVM selector for this function is: 0x285fb8e6,
+ /// or in textual repr: collectionProperties(string[])
+ function collectionProperties(string[] memory keys) public view returns (Tuple19[] memory) {
+ require(false, stub_error);
+ keys;
+ dummy;
+ return new Tuple19[](0);
+ }
+
/// Set the sponsor of the collection.
///
/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
@@ -470,6 +494,12 @@
}
}
+/// @dev anonymous struct
+struct Tuple19 {
+ string field_0;
+ bytes field_1;
+}
+
/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// @dev the ERC-165 identifier for this interface is 0x5b5e139f
pallets/refungible/src/stubs/UniqueRefungible.rawdiffbeforeafterbothbinary blob — no preview
pallets/refungible/src/stubs/UniqueRefungible.soldiffbeforeafterboth--- a/pallets/refungible/src/stubs/UniqueRefungible.sol
+++ b/pallets/refungible/src/stubs/UniqueRefungible.sol
@@ -91,7 +91,7 @@
}
/// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x25d897dc
+/// @dev the ERC-165 identifier for this interface is 0x5d354410
contract Collection is Dummy, ERC165 {
/// Set collection property.
///
@@ -106,6 +106,17 @@
dummy = 0;
}
+ /// Set collection properties.
+ ///
+ /// @param properties Vector of properties key/value pair.
+ /// @dev EVM selector for this function is: 0x50b26b2a,
+ /// or in textual repr: setCollectionProperties((string,bytes)[])
+ function setCollectionProperties(Tuple19[] memory properties) public {
+ require(false, stub_error);
+ properties;
+ dummy = 0;
+ }
+
/// Delete collection property.
///
/// @param key Property key.
@@ -132,6 +143,19 @@
return hex"";
}
+ /// Get collection properties.
+ ///
+ /// @param keys Properties keys.
+ /// @return Vector of properties key/value pairs.
+ /// @dev EVM selector for this function is: 0x285fb8e6,
+ /// or in textual repr: collectionProperties(string[])
+ function collectionProperties(string[] memory keys) public view returns (Tuple19[] memory) {
+ require(false, stub_error);
+ keys;
+ dummy;
+ return new Tuple19[](0);
+ }
+
/// Set the sponsor of the collection.
///
/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
@@ -470,6 +494,12 @@
}
}
+/// @dev anonymous struct
+struct Tuple19 {
+ string field_0;
+ bytes field_1;
+}
+
/// @dev the ERC-165 identifier for this interface is 0x5b5e139f
contract ERC721Metadata is Dummy, ERC165 {
// /// @notice A descriptive name for a collection of NFTs in this contract
tests/src/eth/api/UniqueFungible.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueFungible.sol
+++ b/tests/src/eth/api/UniqueFungible.sol
@@ -13,7 +13,7 @@
}
/// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x25d897dc
+/// @dev the ERC-165 identifier for this interface is 0x5d354410
interface Collection is Dummy, ERC165 {
/// Set collection property.
///
@@ -23,6 +23,13 @@
/// or in textual repr: setCollectionProperty(string,bytes)
function setCollectionProperty(string memory key, bytes memory value) external;
+ /// Set collection properties.
+ ///
+ /// @param properties Vector of properties key/value pair.
+ /// @dev EVM selector for this function is: 0x50b26b2a,
+ /// or in textual repr: setCollectionProperties((string,bytes)[])
+ function setCollectionProperties(Tuple10[] memory properties) external;
+
/// Delete collection property.
///
/// @param key Property key.
@@ -40,6 +47,14 @@
/// or in textual repr: collectionProperty(string)
function collectionProperty(string memory key) external view returns (bytes memory);
+ /// Get collection properties.
+ ///
+ /// @param keys Properties keys.
+ /// @return Vector of properties key/value pairs.
+ /// @dev EVM selector for this function is: 0x285fb8e6,
+ /// or in textual repr: collectionProperties(string[])
+ function collectionProperties(string[] memory keys) external view returns (Tuple10[] memory);
+
/// Set the sponsor of the collection.
///
/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
@@ -258,6 +273,12 @@
function setOwnerCross(Tuple6 memory newOwner) external;
}
+/// @dev anonymous struct
+struct Tuple10 {
+ string field_0;
+ bytes field_1;
+}
+
/// @dev the ERC-165 identifier for this interface is 0x032e5926
interface ERC20UniqueExtensions is Dummy, ERC165 {
/// @dev EVM selector for this function is: 0x0ecd0ab0,
tests/src/eth/api/UniqueNFT.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueNFT.sol
+++ b/tests/src/eth/api/UniqueNFT.sol
@@ -62,7 +62,7 @@
}
/// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x25d897dc
+/// @dev the ERC-165 identifier for this interface is 0x5d354410
interface Collection is Dummy, ERC165 {
/// Set collection property.
///
@@ -72,6 +72,13 @@
/// or in textual repr: setCollectionProperty(string,bytes)
function setCollectionProperty(string memory key, bytes memory value) external;
+ /// Set collection properties.
+ ///
+ /// @param properties Vector of properties key/value pair.
+ /// @dev EVM selector for this function is: 0x50b26b2a,
+ /// or in textual repr: setCollectionProperties((string,bytes)[])
+ function setCollectionProperties(Tuple19[] memory properties) external;
+
/// Delete collection property.
///
/// @param key Property key.
@@ -89,6 +96,14 @@
/// or in textual repr: collectionProperty(string)
function collectionProperty(string memory key) external view returns (bytes memory);
+ /// Get collection properties.
+ ///
+ /// @param keys Properties keys.
+ /// @return Vector of properties key/value pairs.
+ /// @dev EVM selector for this function is: 0x285fb8e6,
+ /// or in textual repr: collectionProperties(string[])
+ function collectionProperties(string[] memory keys) external view returns (Tuple19[] memory);
+
/// Set the sponsor of the collection.
///
/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
@@ -307,6 +322,12 @@
function setOwnerCross(Tuple6 memory newOwner) external;
}
+/// @dev anonymous struct
+struct Tuple19 {
+ string field_0;
+ bytes field_1;
+}
+
/// @title ERC-721 Non-Fungible Token Standard, optional metadata extension
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// @dev the ERC-165 identifier for this interface is 0x5b5e139f
tests/src/eth/api/UniqueRefungible.soldiffbeforeafterboth--- a/tests/src/eth/api/UniqueRefungible.sol
+++ b/tests/src/eth/api/UniqueRefungible.sol
@@ -62,7 +62,7 @@
}
/// @title A contract that allows you to work with collections.
-/// @dev the ERC-165 identifier for this interface is 0x25d897dc
+/// @dev the ERC-165 identifier for this interface is 0x5d354410
interface Collection is Dummy, ERC165 {
/// Set collection property.
///
@@ -72,6 +72,13 @@
/// or in textual repr: setCollectionProperty(string,bytes)
function setCollectionProperty(string memory key, bytes memory value) external;
+ /// Set collection properties.
+ ///
+ /// @param properties Vector of properties key/value pair.
+ /// @dev EVM selector for this function is: 0x50b26b2a,
+ /// or in textual repr: setCollectionProperties((string,bytes)[])
+ function setCollectionProperties(Tuple19[] memory properties) external;
+
/// Delete collection property.
///
/// @param key Property key.
@@ -89,6 +96,14 @@
/// or in textual repr: collectionProperty(string)
function collectionProperty(string memory key) external view returns (bytes memory);
+ /// Get collection properties.
+ ///
+ /// @param keys Properties keys.
+ /// @return Vector of properties key/value pairs.
+ /// @dev EVM selector for this function is: 0x285fb8e6,
+ /// or in textual repr: collectionProperties(string[])
+ function collectionProperties(string[] memory keys) external view returns (Tuple19[] memory);
+
/// Set the sponsor of the collection.
///
/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.
@@ -307,6 +322,12 @@
function setOwnerCross(Tuple6 memory newOwner) external;
}
+/// @dev anonymous struct
+struct Tuple19 {
+ string field_0;
+ bytes field_1;
+}
+
/// @dev the ERC-165 identifier for this interface is 0x5b5e139f
interface ERC721Metadata is Dummy, ERC165 {
// /// @notice A descriptive name for a collection of NFTs in this contract
tests/src/eth/collectionProperties.test.tsdiffbeforeafterboth--- a/tests/src/eth/collectionProperties.test.ts
+++ b/tests/src/eth/collectionProperties.test.ts
@@ -161,3 +161,31 @@
await checkERC721Metadata(helper, 'rft');
});
});
+
+describe('EVM collection property', () => {
+ itEth('Set/read properties', async ({helper, privateKey}) => {
+ const alice = await privateKey('//Alice');
+ const collection = await helper.nft.mintCollection(alice, {name: 'A', description: 'B', tokenPrefix: 'C'});
+
+ const sender = await helper.eth.createAccountWithBalance(alice, 100n);
+ await collection.addAdmin(alice, {Ethereum: sender});
+
+ const collectionAddress = helper.ethAddress.fromCollectionId(collection.collectionId);
+ const contract = helper.ethNativeContract.collection(collectionAddress, 'nft', sender);
+
+ const key1 = 'key1';
+ const value1 = Buffer.from('value1');
+
+ const key2 = 'key2';
+ const value2 = Buffer.from('value2');
+
+ const writeProperties = [
+ [key1, '0x'+value1.toString('hex')],
+ [key2, '0x'+value2.toString('hex')],
+ ];
+
+ await contract.methods.setCollectionProperties(writeProperties).send();
+ const readProperties = await contract.methods.collectionProperties([key1, key2]).call();
+ expect(readProperties).to.be.like(writeProperties);
+ });
+});
tests/src/eth/fungibleAbi.jsondiffbeforeafterboth--- a/tests/src/eth/fungibleAbi.json
+++ b/tests/src/eth/fungibleAbi.json
@@ -229,6 +229,25 @@
"type": "function"
},
{
+ "inputs": [
+ { "internalType": "string[]", "name": "keys", "type": "string[]" }
+ ],
+ "name": "collectionProperties",
+ "outputs": [
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple10[]",
+ "name": "",
+ "type": "tuple[]"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
"inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
"name": "collectionProperty",
"outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
@@ -463,6 +482,23 @@
},
{
"inputs": [
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple10[]",
+ "name": "properties",
+ "type": "tuple[]"
+ }
+ ],
+ "name": "setCollectionProperties",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
{ "internalType": "string", "name": "key", "type": "string" },
{ "internalType": "bytes", "name": "value", "type": "bytes" }
],
tests/src/eth/nonFungibleAbi.jsondiffbeforeafterboth--- a/tests/src/eth/nonFungibleAbi.json
+++ b/tests/src/eth/nonFungibleAbi.json
@@ -259,6 +259,25 @@
"type": "function"
},
{
+ "inputs": [
+ { "internalType": "string[]", "name": "keys", "type": "string[]" }
+ ],
+ "name": "collectionProperties",
+ "outputs": [
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple19[]",
+ "name": "",
+ "type": "tuple[]"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
"inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
"name": "collectionProperty",
"outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
@@ -578,6 +597,23 @@
},
{
"inputs": [
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple19[]",
+ "name": "properties",
+ "type": "tuple[]"
+ }
+ ],
+ "name": "setCollectionProperties",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
{ "internalType": "string", "name": "key", "type": "string" },
{ "internalType": "bytes", "name": "value", "type": "bytes" }
],
tests/src/eth/reFungibleAbi.jsondiffbeforeafterboth--- a/tests/src/eth/reFungibleAbi.json
+++ b/tests/src/eth/reFungibleAbi.json
@@ -241,6 +241,25 @@
"type": "function"
},
{
+ "inputs": [
+ { "internalType": "string[]", "name": "keys", "type": "string[]" }
+ ],
+ "name": "collectionProperties",
+ "outputs": [
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple19[]",
+ "name": "",
+ "type": "tuple[]"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
"inputs": [{ "internalType": "string", "name": "key", "type": "string" }],
"name": "collectionProperty",
"outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }],
@@ -560,6 +579,23 @@
},
{
"inputs": [
+ {
+ "components": [
+ { "internalType": "string", "name": "field_0", "type": "string" },
+ { "internalType": "bytes", "name": "field_1", "type": "bytes" }
+ ],
+ "internalType": "struct Tuple19[]",
+ "name": "properties",
+ "type": "tuple[]"
+ }
+ ],
+ "name": "setCollectionProperties",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
{ "internalType": "string", "name": "key", "type": "string" },
{ "internalType": "bytes", "name": "value", "type": "bytes" }
],