difftreelog
add EVM event for `destoyCollection`, refactor `Unique` pallet code, add test for events
in: master
14 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5825,7 +5825,7 @@
[[package]]
name = "pallet-common"
-version = "0.1.8"
+version = "0.1.9"
dependencies = [
"ethereum",
"evm-coder",
pallets/common/CHANGELOG.mddiffbeforeafterboth--- a/pallets/common/CHANGELOG.md
+++ b/pallets/common/CHANGELOG.md
@@ -2,29 +2,37 @@
All notable changes to this project will be documented in this file.
+## [0.1.9] - 2022-10-13
+
+## Added
+
+- EVM event for `destroy_collection`.
+
## [0.1.8] - 2022-08-24
## Added
- - Eth methods for collection
- + set_collection_sponsor_substrate
- + has_collection_pending_sponsor
- + remove_collection_sponsor
- + get_collection_sponsor
+
+- Eth methods for collection
+ - set_collection_sponsor_substrate
+ - has_collection_pending_sponsor
+ - remove_collection_sponsor
+ - get_collection_sponsor
- Add convert function from `uint256` to `CrossAccountId`.
## [0.1.7] - 2022-08-19
### Added
- - Add convert funtion from `CrossAccountId` to eth `uint256`.
+- Add convert funtion from `CrossAccountId` to eth `uint256`.
-
## [0.1.6] - 2022-08-16
### Added
-- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate).
+- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate).
+
<!-- bureaucrate goes here -->
+
## [v0.1.5] 2022-08-16
### Other changes
@@ -45,19 +53,21 @@
- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b
## [0.1.3] - 2022-07-25
+
### Add
-- Some static property keys and values.
+- Some static property keys and values.
+
## [0.1.2] - 2022-07-20
### Fixed
-- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid
- mutability modifiers, causing invalid stub/abi generation.
+- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid
+ mutability modifiers, causing invalid stub/abi generation.
## [0.1.1] - 2022-07-14
### Added
- - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
- This was an internal request to improve the web interface and support fractionalization event.
+- Implementation of RPC method `token_owners` returning 10 owners in no particular order.
+ This was an internal request to improve the web interface and support fractionalization event.
pallets/common/Cargo.tomldiffbeforeafterboth--- a/pallets/common/Cargo.toml
+++ b/pallets/common/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pallet-common"
-version = "0.1.8"
+version = "0.1.9"
license = "GPLv3"
edition = "2021"
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}5758/// Does not always represent a full collection, for RFT it is either59/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).60pub trait CommonEvmHandler {61 const CODE: &'static [u8];6263 /// Call precompiled handle.64 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;65}6667/// @title A contract that allows you to work with collections.68#[solidity_interface(name = Collection)]69impl<T: Config> CollectionHandle<T>70where71 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,72{73 /// Set collection property.74 ///75 /// @param key Property key.76 /// @param value Propery value.77 #[weight(<SelfWeightOf<T>>::set_collection_properties(1))]78 fn set_collection_property(79 &mut self,80 caller: caller,81 key: string,82 value: bytes,83 ) -> Result<void> {84 let caller = T::CrossAccountId::from_eth(caller);85 let key = <Vec<u8>>::from(key)86 .try_into()87 .map_err(|_| "key too large")?;88 let value = value.0.try_into().map_err(|_| "value too large")?;8990 <Pallet<T>>::set_collection_property(self, &caller, Property { key, value })91 .map_err(dispatch_to_evm::<T>)92 }9394 /// Delete collection property.95 ///96 /// @param key Property key.97 #[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]98 fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {99 self.consume_store_reads_and_writes(1, 1)?;100101 let caller = T::CrossAccountId::from_eth(caller);102 let key = <Vec<u8>>::from(key)103 .try_into()104 .map_err(|_| "key too large")?;105106 <Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)107 }108109 /// Get collection property.110 ///111 /// @dev Throws error if key not found.112 ///113 /// @param key Property key.114 /// @return bytes The property corresponding to the key.115 fn collection_property(&self, key: string) -> Result<bytes> {116 let key = <Vec<u8>>::from(key)117 .try_into()118 .map_err(|_| "key too large")?;119120 let props = <CollectionProperties<T>>::get(self.id);121 let prop = props.get(&key).ok_or("key not found")?;122123 Ok(bytes(prop.to_vec()))124 }125126 /// Set the sponsor of the collection.127 ///128 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.129 ///130 /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.131 fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {132 self.consume_store_reads_and_writes(1, 1)?;133134 check_is_owner_or_admin(caller, self)?;135136 let sponsor = T::CrossAccountId::from_eth(sponsor);137 self.set_sponsor(sponsor.as_sub().clone())138 .map_err(dispatch_to_evm::<T>)?;139 save(self)140 }141142 /// Set the sponsor of the collection.143 ///144 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.145 ///146 /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.147 fn set_collection_sponsor_cross(148 &mut self,149 caller: caller,150 sponsor: (address, uint256),151 ) -> Result<void> {152 self.consume_store_reads_and_writes(1, 1)?;153154 check_is_owner_or_admin(caller, self)?;155156 let sponsor = convert_tuple_to_cross_account::<T>(sponsor)?;157 self.set_sponsor(sponsor.as_sub().clone())158 .map_err(dispatch_to_evm::<T>)?;159 save(self)160 }161162 /// Whether there is a pending sponsor.163 fn has_collection_pending_sponsor(&self) -> Result<bool> {164 Ok(matches!(165 self.collection.sponsorship,166 SponsorshipState::Unconfirmed(_)167 ))168 }169170 /// Collection sponsorship confirmation.171 ///172 /// @dev After setting the sponsor for the collection, it must be confirmed with this function.173 fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {174 self.consume_store_writes(1)?;175176 let caller = T::CrossAccountId::from_eth(caller);177 if !self178 .confirm_sponsorship(caller.as_sub())179 .map_err(dispatch_to_evm::<T>)?180 {181 return Err("caller is not set as sponsor".into());182 }183 save(self)184 }185186 /// Remove collection sponsor.187 fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {188 self.consume_store_reads_and_writes(1, 1)?;189 check_is_owner_or_admin(caller, self)?;190 self.remove_sponsor().map_err(dispatch_to_evm::<T>)?;191 save(self)192 }193194 /// Get current sponsor.195 ///196 /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.197 fn collection_sponsor(&self) -> Result<(address, uint256)> {198 let sponsor = match self.collection.sponsorship.sponsor() {199 Some(sponsor) => sponsor,200 None => return Ok(Default::default()),201 };202 let sponsor = T::CrossAccountId::from_sub(sponsor.clone());203 let result: (address, uint256) = if sponsor.is_canonical_substrate() {204 let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);205 (Default::default(), sponsor)206 } else {207 let sponsor = *sponsor.as_eth();208 (sponsor, Default::default())209 };210 Ok(result)211 }212213 /// Set limits for the collection.214 /// @dev Throws error if limit not found.215 /// @param limit Name of the limit. Valid names:216 /// "accountTokenOwnershipLimit",217 /// "sponsoredDataSize",218 /// "sponsoredDataRateLimit",219 /// "tokenLimit",220 /// "sponsorTransferTimeout",221 /// "sponsorApproveTimeout"222 /// @param value Value of the limit.223 #[solidity(rename_selector = "setCollectionLimit")]224 fn set_int_limit(&mut self, caller: caller, limit: string, value: uint32) -> Result<void> {225 self.consume_store_reads_and_writes(1, 1)?;226227 check_is_owner_or_admin(caller, self)?;228 let mut limits = self.limits.clone();229230 match limit.as_str() {231 "accountTokenOwnershipLimit" => {232 limits.account_token_ownership_limit = Some(value);233 }234 "sponsoredDataSize" => {235 limits.sponsored_data_size = Some(value);236 }237 "sponsoredDataRateLimit" => {238 limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));239 }240 "tokenLimit" => {241 limits.token_limit = Some(value);242 }243 "sponsorTransferTimeout" => {244 limits.sponsor_transfer_timeout = Some(value);245 }246 "sponsorApproveTimeout" => {247 limits.sponsor_approve_timeout = Some(value);248 }249 _ => {250 return Err(Error::Revert(format!(251 "unknown integer limit \"{}\"",252 limit253 )))254 }255 }256 self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)257 .map_err(dispatch_to_evm::<T>)?;258 save(self)259 }260261 /// Set limits for the collection.262 /// @dev Throws error if limit not found.263 /// @param limit Name of the limit. Valid names:264 /// "ownerCanTransfer",265 /// "ownerCanDestroy",266 /// "transfersEnabled"267 /// @param value Value of the limit.268 #[solidity(rename_selector = "setCollectionLimit")]269 fn set_bool_limit(&mut self, caller: caller, limit: string, value: bool) -> Result<void> {270 self.consume_store_reads_and_writes(1, 1)?;271272 check_is_owner_or_admin(caller, self)?;273 let mut limits = self.limits.clone();274275 match limit.as_str() {276 "ownerCanTransfer" => {277 limits.owner_can_transfer = Some(value);278 }279 "ownerCanDestroy" => {280 limits.owner_can_destroy = Some(value);281 }282 "transfersEnabled" => {283 limits.transfers_enabled = Some(value);284 }285 _ => {286 return Err(Error::Revert(format!(287 "unknown boolean limit \"{}\"",288 limit289 )))290 }291 }292 self.limits = <Pallet<T>>::clamp_limits(self.mode.clone(), &self.limits, limits)293 .map_err(dispatch_to_evm::<T>)?;294 save(self)295 }296297 /// Get contract address.298 fn contract_address(&self) -> Result<address> {299 Ok(crate::eth::collection_id_to_address(self.id))300 }301302 /// Add collection admin.303 /// @param newAdmin Cross account administrator address.304 fn add_collection_admin_cross(305 &mut self,306 caller: caller,307 new_admin: (address, uint256),308 ) -> Result<void> {309 self.consume_store_writes(2)?;310311 let caller = T::CrossAccountId::from_eth(caller);312 let new_admin = convert_tuple_to_cross_account::<T>(new_admin)?;313 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;314 Ok(())315 }316317 /// Remove collection admin.318 /// @param admin Cross account administrator address.319 fn remove_collection_admin_cross(320 &mut self,321 caller: caller,322 admin: (address, uint256),323 ) -> Result<void> {324 self.consume_store_writes(2)?;325326 let caller = T::CrossAccountId::from_eth(caller);327 let admin = convert_tuple_to_cross_account::<T>(admin)?;328 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;329 Ok(())330 }331332 /// Add collection admin.333 /// @param newAdmin Address of the added administrator.334 fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {335 self.consume_store_writes(2)?;336337 let caller = T::CrossAccountId::from_eth(caller);338 let new_admin = T::CrossAccountId::from_eth(new_admin);339 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;340 Ok(())341 }342343 /// Remove collection admin.344 ///345 /// @param admin Address of the removed administrator.346 fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {347 self.consume_store_writes(2)?;348349 let caller = T::CrossAccountId::from_eth(caller);350 let admin = T::CrossAccountId::from_eth(admin);351 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;352 Ok(())353 }354355 /// Toggle accessibility of collection nesting.356 ///357 /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'358 #[solidity(rename_selector = "setCollectionNesting")]359 fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {360 self.consume_store_reads_and_writes(1, 1)?;361362 check_is_owner_or_admin(caller, self)?;363364 let mut permissions = self.collection.permissions.clone();365 let mut nesting = permissions.nesting().clone();366 nesting.token_owner = enable;367 nesting.restricted = None;368 permissions.nesting = Some(nesting);369370 self.collection.permissions = <Pallet<T>>::clamp_permissions(371 self.collection.mode.clone(),372 &self.collection.permissions,373 permissions,374 )375 .map_err(dispatch_to_evm::<T>)?;376377 save(self)378 }379380 /// Toggle accessibility of collection nesting.381 ///382 /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'383 /// @param collections Addresses of collections that will be available for nesting.384 #[solidity(rename_selector = "setCollectionNesting")]385 fn set_nesting(386 &mut self,387 caller: caller,388 enable: bool,389 collections: Vec<address>,390 ) -> Result<void> {391 self.consume_store_reads_and_writes(1, 1)?;392393 if collections.is_empty() {394 return Err("no addresses provided".into());395 }396 check_is_owner_or_admin(caller, self)?;397398 let mut permissions = self.collection.permissions.clone();399 match enable {400 false => {401 let mut nesting = permissions.nesting().clone();402 nesting.token_owner = false;403 nesting.restricted = None;404 permissions.nesting = Some(nesting);405 }406 true => {407 let mut bv = OwnerRestrictedSet::new();408 for i in collections {409 bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {410 Error::Revert("Can't convert address into collection id".into())411 })?)412 .map_err(|_| "too many collections")?;413 }414 let mut nesting = permissions.nesting().clone();415 nesting.token_owner = true;416 nesting.restricted = Some(bv);417 permissions.nesting = Some(nesting);418 }419 };420421 self.collection.permissions = <Pallet<T>>::clamp_permissions(422 self.collection.mode.clone(),423 &self.collection.permissions,424 permissions,425 )426 .map_err(dispatch_to_evm::<T>)?;427428 save(self)429 }430431 /// Set the collection access method.432 /// @param mode Access mode433 /// 0 for Normal434 /// 1 for AllowList435 fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {436 self.consume_store_reads_and_writes(1, 1)?;437438 check_is_owner_or_admin(caller, self)?;439 let permissions = CollectionPermissions {440 access: Some(match mode {441 0 => AccessMode::Normal,442 1 => AccessMode::AllowList,443 _ => return Err("not supported access mode".into()),444 }),445 ..Default::default()446 };447 self.collection.permissions = <Pallet<T>>::clamp_permissions(448 self.collection.mode.clone(),449 &self.collection.permissions,450 permissions,451 )452 .map_err(dispatch_to_evm::<T>)?;453454 save(self)455 }456457 /// Checks that user allowed to operate with collection.458 ///459 /// @param user User address to check.460 fn allowed(&self, user: address) -> Result<bool> {461 Ok(Pallet::<T>::allowed(462 self.id,463 T::CrossAccountId::from_eth(user),464 ))465 }466467 /// Add the user to the allowed list.468 ///469 /// @param user Address of a trusted user.470 fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {471 self.consume_store_writes(1)?;472473 let caller = T::CrossAccountId::from_eth(caller);474 let user = T::CrossAccountId::from_eth(user);475 <Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;476 Ok(())477 }478479 /// Add user to allowed list.480 ///481 /// @param user User cross account address.482 fn add_to_collection_allow_list_cross(483 &mut self,484 caller: caller,485 user: (address, uint256),486 ) -> Result<void> {487 self.consume_store_writes(1)?;488489 let caller = T::CrossAccountId::from_eth(caller);490 let user = convert_tuple_to_cross_account::<T>(user)?;491 Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;492 Ok(())493 }494495 /// Remove the user from the allowed list.496 ///497 /// @param user Address of a removed user.498 fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {499 self.consume_store_writes(1)?;500501 let caller = T::CrossAccountId::from_eth(caller);502 let user = T::CrossAccountId::from_eth(user);503 <Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;504 Ok(())505 }506507 /// Remove user from allowed list.508 ///509 /// @param user User cross account address.510 fn remove_from_collection_allow_list_cross(511 &mut self,512 caller: caller,513 user: (address, uint256),514 ) -> Result<void> {515 self.consume_store_writes(1)?;516517 let caller = T::CrossAccountId::from_eth(caller);518 let user = convert_tuple_to_cross_account::<T>(user)?;519 Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;520 Ok(())521 }522523 /// Switch permission for minting.524 ///525 /// @param mode Enable if "true".526 fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {527 self.consume_store_reads_and_writes(1, 1)?;528529 check_is_owner_or_admin(caller, self)?;530 let permissions = CollectionPermissions {531 mint_mode: Some(mode),532 ..Default::default()533 };534 self.collection.permissions = <Pallet<T>>::clamp_permissions(535 self.collection.mode.clone(),536 &self.collection.permissions,537 permissions,538 )539 .map_err(dispatch_to_evm::<T>)?;540541 save(self)542 }543544 /// Check that account is the owner or admin of the collection545 ///546 /// @param user account to verify547 /// @return "true" if account is the owner or admin548 #[solidity(rename_selector = "isOwnerOrAdmin")]549 fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {550 let user = T::CrossAccountId::from_eth(user);551 Ok(self.is_owner_or_admin(&user))552 }553554 /// Check that account is the owner or admin of the collection555 ///556 /// @param user User cross account to verify557 /// @return "true" if account is the owner or admin558 fn is_owner_or_admin_cross(&self, user: (address, uint256)) -> Result<bool> {559 let user = convert_tuple_to_cross_account::<T>(user)?;560 Ok(self.is_owner_or_admin(&user))561 }562563 /// Returns collection type564 ///565 /// @return `Fungible` or `NFT` or `ReFungible`566 fn unique_collection_type(&self) -> Result<string> {567 let mode = match self.collection.mode {568 CollectionMode::Fungible(_) => "Fungible",569 CollectionMode::NFT => "NFT",570 CollectionMode::ReFungible => "ReFungible",571 };572 Ok(mode.into())573 }574575 /// Get collection owner.576 ///577 /// @return Tuble with sponsor address and his substrate mirror.578 /// If address is canonical then substrate mirror is zero and vice versa.579 fn collection_owner(&self) -> Result<(address, uint256)> {580 Ok(convert_cross_account_to_tuple::<T>(581 &T::CrossAccountId::from_sub(self.owner.clone()),582 ))583 }584585 /// Changes collection owner to another account586 ///587 /// @dev Owner can be changed only by current owner588 /// @param newOwner new owner account589 #[solidity(rename_selector = "changeCollectionOwner")]590 fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {591 self.consume_store_writes(1)?;592593 let caller = T::CrossAccountId::from_eth(caller);594 let new_owner = T::CrossAccountId::from_eth(new_owner);595 self.set_owner_internal(caller, new_owner)596 .map_err(dispatch_to_evm::<T>)597 }598599 /// Get collection administrators600 ///601 /// @return Vector of tuples with admins address and his substrate mirror.602 /// If address is canonical then substrate mirror is zero and vice versa.603 fn collection_admins(&self) -> Result<Vec<(address, uint256)>> {604 let result = crate::IsAdmin::<T>::iter_prefix((self.id,))605 .map(|(admin, _)| crate::eth::convert_cross_account_to_tuple::<T>(&admin))606 .collect();607 Ok(result)608 }609610 /// Changes collection owner to another account611 ///612 /// @dev Owner can be changed only by current owner613 /// @param newOwner new owner cross account614 fn set_owner_cross(&mut self, caller: caller, new_owner: (address, uint256)) -> Result<void> {615 self.consume_store_writes(1)?;616617 let caller = T::CrossAccountId::from_eth(caller);618 let new_owner = convert_tuple_to_cross_account::<T>(new_owner)?;619 self.set_owner_internal(caller, new_owner)620 .map_err(dispatch_to_evm::<T>)621 }622}623624/// ### Note625/// Do not forget to add: `self.consume_store_reads(1)?;`626fn check_is_owner_or_admin<T: Config>(627 caller: caller,628 collection: &CollectionHandle<T>,629) -> Result<T::CrossAccountId> {630 let caller = T::CrossAccountId::from_eth(caller);631 collection632 .check_is_owner_or_admin(&caller)633 .map_err(dispatch_to_evm::<T>)?;634 Ok(caller)635}636637/// ### Note638/// Do not forget to add: `self.consume_store_writes(1)?;`639fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {640 collection641 .check_is_internal()642 .map_err(dispatch_to_evm::<T>)?;643 collection.save().map_err(dispatch_to_evm::<T>)?;644 Ok(())645}646647/// Contains static property keys and values.648pub mod static_property {649 use evm_coder::{650 execution::{Result, Error},651 };652 use alloc::format;653654 const EXPECT_CONVERT_ERROR: &str = "length < limit";655656 /// Keys.657 pub mod key {658 use super::*;659660 /// Key "baseURI".661 pub fn base_uri() -> up_data_structs::PropertyKey {662 property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)663 }664665 /// Key "url".666 pub fn url() -> up_data_structs::PropertyKey {667 property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)668 }669670 /// Key "suffix".671 pub fn suffix() -> up_data_structs::PropertyKey {672 property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)673 }674675 /// Key "parentNft".676 pub fn parent_nft() -> up_data_structs::PropertyKey {677 property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)678 }679 }680681 /// Convert `byte` to [`PropertyKey`].682 pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {683 bytes.to_vec().try_into().map_err(|_| {684 Error::Revert(format!(685 "Property key is too long. Max length is {}.",686 up_data_structs::PropertyKey::bound()687 ))688 })689 }690691 /// Convert `bytes` to [`PropertyValue`].692 pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {693 bytes.to_vec().try_into().map_err(|_| {694 Error::Revert(format!(695 "Property key is too long. Max length is {}.",696 up_data_structs::PropertyKey::bound()697 ))698 })699 }700}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,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}pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -999,6 +999,13 @@
<CollectionProperties<T>>::remove(collection.id);
<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));
+
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionDestroyed {
+ collection_id: eth::collection_id_to_address(collection.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
Ok(())
}
pallets/unique/src/eth/mod.rsdiffbeforeafterboth--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -19,9 +19,10 @@
use core::marker::PhantomData;
use ethereum as _;
use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*};
-use frame_support::{traits::Get, storage::StorageNMap};
+use frame_support::traits::Get;
+
+use crate::Pallet;
-use crate::sp_api_hidden_includes_decl_storage::hidden_include::StorageDoubleMap;
use pallet_common::{
CollectionById,
dispatch::CollectionDispatch,
@@ -39,10 +40,7 @@
CollectionMode, PropertyValue, CollectionFlags,
};
-use crate::{
- Config, SelfWeightOf, weights::WeightInfo, NftTransferBasket, FungibleTransferBasket,
- ReFungibleTransferBasket, NftApproveBasket, FungibleApproveBasket, RefungibleApproveBasket,
-};
+use crate::{Config, SelfWeightOf, weights::WeightInfo};
use sp_std::vec::Vec;
use alloc::format;
@@ -302,30 +300,13 @@
}
#[weight(<SelfWeightOf<T>>::destroy_collection())]
- #[solidity(rename_selector = "destroyCollection")]
fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result<void> {
let caller = T::CrossAccountId::from_eth(caller);
- let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
- .ok_or("Invalid collection address format".into())
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- let collection = <pallet_common::CollectionHandle<T>>::try_get(collection_id)
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- collection
- .check_is_internal()
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- T::CollectionDispatch::destroy(caller, collection)
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
-
- let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- Ok(())
+ let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
+ .ok_or("Invalid collection address format")?;
+ <Pallet<T>>::destroy_collection_internal(caller, collection_id)
+ .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)
}
/// Check if a collection exists
pallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -20,6 +20,7 @@
/// @dev inlined interface
contract CollectionHelpersEvents {
event CollectionCreated(address indexed owner, address indexed collectionId);
+ event CollectionDestroyed(address indexed collectionId);
}
/// @title Contract, which allows users to operate with collections
pallets/unique/src/lib.rsdiffbeforeafterboth--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -362,25 +362,8 @@
#[weight = <SelfWeightOf<T>>::destroy_collection()]
pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {
let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
- let collection = <CollectionHandle<T>>::try_get(collection_id)?;
- collection.check_is_internal()?;
-
- // =========
-
- T::CollectionDispatch::destroy(sender, collection)?;
- // TODO: basket cleanup should be moved elsewhere
- // Maybe runtime dispatch.rs should perform it?
-
- let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- Ok(())
+ Self::destroy_collection_internal(sender, collection_id)
}
/// Add an address to allow list.
@@ -1151,4 +1134,28 @@
target_collection.save()
}
+
+ #[inline(always)]
+ pub(crate) fn destroy_collection_internal(
+ sender: T::CrossAccountId,
+ collection_id: CollectionId,
+ ) -> DispatchResult {
+ let collection = <CollectionHandle<T>>::try_get(collection_id)?;
+ collection.check_is_internal()?;
+
+ T::CollectionDispatch::destroy(sender, collection)?;
+
+ // TODO: basket cleanup should be moved elsewhere
+ // Maybe runtime dispatch.rs should perform it?
+
+ let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ Ok(())
+ }
}
tests/.vscode/settings.jsondiffbeforeafterboth--- a/tests/.vscode/settings.json
+++ b/tests/.vscode/settings.json
@@ -1,5 +1,12 @@
{
- "mocha.enabled": true,
- "mochaExplorer.files": "**/*.test.ts",
- "mochaExplorer.require": "ts-node/register"
+ "mocha.enabled": true,
+ "mochaExplorer.files": "**/*.test.ts",
+ "mochaExplorer.require": "ts-node/register",
+ "eslint.format.enable": true,
+ "[javascript]": {
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ }
}
tests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -15,6 +15,7 @@
/// @dev inlined interface
interface CollectionHelpersEvents {
event CollectionCreated(address indexed owner, address indexed collectionId);
+ event CollectionDestroyed(address indexed collectionId);
}
/// @title Contract, which allows users to operate with collections
tests/src/eth/collectionHelpersAbi.jsondiffbeforeafterboth--- a/tests/src/eth/collectionHelpersAbi.json
+++ b/tests/src/eth/collectionHelpersAbi.json
@@ -19,6 +19,19 @@
"type": "event"
},
{
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "collectionId",
+ "type": "address"
+ }
+ ],
+ "name": "CollectionDestroyed",
+ "type": "event"
+ },
+ {
"inputs": [],
"name": "collectionCreationFee",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
tests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth--- a/tests/src/eth/createNFTCollection.test.ts
+++ b/tests/src/eth/createNFTCollection.test.ts
@@ -22,7 +22,7 @@
describe('Create NFT collection from EVM', () => {
let donor: IKeyringPair;
- before(async function() {
+ before(async function () {
await usingEthPlaygrounds(async (_helper, privateKey) => {
donor = await privateKey({filename: __filename});
});
@@ -35,10 +35,28 @@
const description = 'Some description';
const prefix = 'token prefix';
- const {collectionId} = await helper.eth.createNFTCollection(owner, name, description, prefix);
- const data = (await helper.rft.getData(collectionId))!;
+ // todo:playgrounds this might fail when in async environment.
+ const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
+ const {collectionId, collectionAddress, events} = await helper.eth.createNFTCollection(owner, name, description, prefix);
+
+ expect(events).to.be.deep.equal([
+ {
+ address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',
+ event: 'CollectionCreated',
+ args: {
+ owner: owner,
+ collectionId: collectionAddress,
+ },
+ },
+ ]);
+
+ const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
+
const collection = helper.nft.getCollectionObject(collectionId);
-
+ const data = (await collection.getData())!;
+
+ expect(collectionCountAfter - collectionCountBefore).to.be.eq(1);
+ expect(collectionId).to.be.eq(collectionCountAfter);
expect(data.name).to.be.eq(name);
expect(data.description).to.be.eq(description);
expect(data.raw.tokenPrefix).to.be.eq(prefix);
@@ -57,8 +75,19 @@
const prefix = 'token prefix';
const baseUri = 'BaseURI';
- const {collectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);
+ const {collectionId, collectionAddress, events} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);
+ expect(events).to.be.deep.equal([
+ {
+ address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',
+ event: 'CollectionCreated',
+ args: {
+ owner: owner,
+ collectionId: collectionAddress,
+ },
+ },
+ ]);
+
const collection = helper.nft.getCollectionObject(collectionId);
const data = (await collection.getData())!;
@@ -95,12 +124,12 @@
await collectionHelpers.methods
.createNFTCollection('A', 'A', 'A')
.send({value: Number(2n * helper.balance.getOneTokenNominal())});
-
+
expect(await collectionHelpers.methods
.isCollectionExist(expectedCollectionAddress)
.call()).to.be.true;
});
-
+
itEth('Set sponsorship', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const sponsor = await helper.eth.createAccountWithBalance(donor);
@@ -147,7 +176,7 @@
await collection.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send();
await collection.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send();
await collection.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send();
-
+
const data = (await helper.nft.getData(collectionId))!;
expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit);
expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize);
@@ -166,7 +195,7 @@
expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection)
.methods.isCollectionExist(collectionAddressForNonexistentCollection).call())
.to.be.false;
-
+
const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC');
expect(await helper.ethNativeContract.collectionHelpers(collectionAddress)
.methods.isCollectionExist(collectionAddress).call())
@@ -178,7 +207,7 @@
let donor: IKeyringPair;
let nominal: bigint;
- before(async function() {
+ before(async function () {
await usingEthPlaygrounds(async (helper, privateKey) => {
donor = await privateKey({filename: __filename});
nominal = helper.balance.getOneTokenNominal();
@@ -197,7 +226,7 @@
await expect(collectionHelper.methods
.createNFTCollection(collectionName, description, tokenPrefix)
.call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);
-
+
}
{
const MAX_DESCRIPTION_LENGTH = 256;
@@ -218,7 +247,7 @@
.call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);
}
});
-
+
itEth('(!negative test!) Create collection (no funds)', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
@@ -238,7 +267,7 @@
await expect(malfeasantCollection.methods
.setCollectionSponsor(sponsor)
.call()).to.be.rejectedWith(EXPECTED_ERROR);
-
+
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
@@ -259,4 +288,31 @@
.setCollectionLimit('badLimit', 'true')
.call()).to.be.rejectedWith('unknown boolean limit "badLimit"');
});
-});
+
+ itEth('destroyCollection', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF');
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+
+
+ const result = await collectionHelper.methods
+ .destroyCollection(collectionAddress)
+ .send({from: owner});
+
+ const events = helper.eth.normalizeEvents(result.events);
+
+ expect(events).to.be.deep.equal([
+ {
+ address: collectionHelper.options.address,
+ event: 'CollectionDestroyed',
+ args: {
+ collectionId: collectionAddress,
+ },
+ },
+ ]);
+
+ expect(await collectionHelper.methods
+ .isCollectionExist(collectionAddress)
+ .call()).to.be.false;
+ });
+});
\ No newline at end of file
tests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth--- a/tests/src/eth/util/playgrounds/unique.dev.ts
+++ b/tests/src/eth/util/playgrounds/unique.dev.ts
@@ -173,49 +173,46 @@
async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {
return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);
}
-
- async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
+
+ async createCollecion(functionName: string, signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
-
- const result = await collectionHelper.methods.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
+
+ const result = await collectionHelper.methods[functionName](name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
-
- return {collectionId, collectionAddress};
+ const events = this.helper.eth.normalizeEvents(result.events);
+
+ return {collectionId, collectionAddress, events};
+ }
+
+ async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
+ return this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);
}
- async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+ async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
- const {collectionId, collectionAddress} = await this.createNFTCollection(signer, name, description, tokenPrefix);
+ const {collectionId, collectionAddress, events} = await this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);
await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();
- return {collectionId, collectionAddress};
+ return {collectionId, collectionAddress, events};
}
async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {
- const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();
- const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
-
- const result = await collectionHelper.methods.createRFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});
-
- const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
- const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
-
- return {collectionId, collectionAddress};
+ return this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);
}
- async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {
+ async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {
const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);
- const {collectionId, collectionAddress} = await this.createRFTCollection(signer, name, description, tokenPrefix);
+ const {collectionId, collectionAddress, events} = await this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);
await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();
- return {collectionId, collectionAddress};
+ return {collectionId, collectionAddress, events};
}
async deployCollectorContract(signer: string): Promise<Contract> {