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.rsdiffbeforeafterboth--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -53,6 +53,12 @@
#[indexed]
collection_id: address,
},
+ /// The collection has been destroyed.
+ CollectionDestroyed {
+ /// Collection ID.
+ #[indexed]
+ collection_id: address,
+ },
}
/// Does not always represent a full collection, for RFT it is either
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.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//! Implementation of CollectionHelpers contract.1819use core::marker::PhantomData;20use ethereum as _;21use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*};22use frame_support::{traits::Get, storage::StorageNMap};2324use crate::sp_api_hidden_includes_decl_storage::hidden_include::StorageDoubleMap;25use pallet_common::{26 CollectionById,27 dispatch::CollectionDispatch,28 erc::{29 CollectionHelpersEvents,30 static_property::{key},31 },32 Pallet as PalletCommon,33};34use pallet_evm_coder_substrate::{dispatch_to_evm, SubstrateRecorder, WithRecorder};35use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult};36use sp_std::vec;37use up_data_structs::{38 CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData,39 CollectionMode, PropertyValue, CollectionFlags,40};4142use crate::{43 Config, SelfWeightOf, weights::WeightInfo, NftTransferBasket, FungibleTransferBasket,44 ReFungibleTransferBasket, NftApproveBasket, FungibleApproveBasket, RefungibleApproveBasket,45};4647use sp_std::vec::Vec;48use alloc::format;4950/// See [`CollectionHelpersCall`]51pub struct EvmCollectionHelpers<T: Config>(SubstrateRecorder<T>);52impl<T: Config> WithRecorder<T> for EvmCollectionHelpers<T> {53 fn recorder(&self) -> &SubstrateRecorder<T> {54 &self.055 }5657 fn into_recorder(self) -> SubstrateRecorder<T> {58 self.059 }60}6162fn convert_data<T: Config>(63 caller: caller,64 name: string,65 description: string,66 token_prefix: string,67) -> Result<(68 T::CrossAccountId,69 CollectionName,70 CollectionDescription,71 CollectionTokenPrefix,72)> {73 let caller = T::CrossAccountId::from_eth(caller);74 let name = name75 .encode_utf16()76 .collect::<Vec<u16>>()77 .try_into()78 .map_err(|_| error_field_too_long(stringify!(name), CollectionName::bound()))?;79 let description = description80 .encode_utf16()81 .collect::<Vec<u16>>()82 .try_into()83 .map_err(|_| {84 error_field_too_long(stringify!(description), CollectionDescription::bound())85 })?;86 let token_prefix = token_prefix.into_bytes().try_into().map_err(|_| {87 error_field_too_long(stringify!(token_prefix), CollectionTokenPrefix::bound())88 })?;89 Ok((caller, name, description, token_prefix))90}9192fn create_refungible_collection_internal<93 T: Config + pallet_nonfungible::Config + pallet_refungible::Config,94>(95 caller: caller,96 value: value,97 name: string,98 description: string,99 token_prefix: string,100) -> Result<address> {101 let (caller, name, description, token_prefix) =102 convert_data::<T>(caller, name, description, token_prefix)?;103 let data = CreateCollectionData {104 name,105 mode: CollectionMode::ReFungible,106 description,107 token_prefix,108 ..Default::default()109 };110 check_sent_amount_equals_collection_creation_price::<T>(value)?;111 let collection_helpers_address =112 T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());113114 let collection_id = T::CollectionDispatch::create(115 caller.clone(),116 collection_helpers_address,117 data,118 Default::default(),119 )120 .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;121 let address = pallet_common::eth::collection_id_to_address(collection_id);122 Ok(address)123}124125fn check_sent_amount_equals_collection_creation_price<T: Config>(value: value) -> Result<()> {126 let value = value.as_u128();127 let creation_price: u128 = T::CollectionCreationPrice::get()128 .try_into()129 .map_err(|_| ()) // workaround for `expect` requiring `Debug` trait130 .expect("Collection creation price should be convertible to u128");131 if value != creation_price {132 return Err(format!(133 "Sent amount not equals to collection creation price ({0})",134 creation_price135 )136 .into());137 }138 Ok(())139}140141/// @title Contract, which allows users to operate with collections142#[solidity_interface(name = CollectionHelpers, events(CollectionHelpersEvents))]143impl<T> EvmCollectionHelpers<T>144where145 T: Config + pallet_common::Config + pallet_nonfungible::Config + pallet_refungible::Config,146{147 /// Create an NFT collection148 /// @param name Name of the collection149 /// @param description Informative description of the collection150 /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications151 /// @return address Address of the newly created collection152 #[weight(<SelfWeightOf<T>>::create_collection())]153 #[solidity(rename_selector = "createNFTCollection")]154 fn create_nft_collection(155 &mut self,156 caller: caller,157 value: value,158 name: string,159 description: string,160 token_prefix: string,161 ) -> Result<address> {162 let (caller, name, description, token_prefix) =163 convert_data::<T>(caller, name, description, token_prefix)?;164 let data = CreateCollectionData {165 name,166 mode: CollectionMode::NFT,167 description,168 token_prefix,169 ..Default::default()170 };171 check_sent_amount_equals_collection_creation_price::<T>(value)?;172 let collection_helpers_address =173 T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());174 let collection_id = T::CollectionDispatch::create(175 caller,176 collection_helpers_address,177 data,178 Default::default(),179 )180 .map_err(dispatch_to_evm::<T>)?;181182 let address = pallet_common::eth::collection_id_to_address(collection_id);183 Ok(address)184 }185 /// Create an NFT collection186 /// @param name Name of the collection187 /// @param description Informative description of the collection188 /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications189 /// @return address Address of the newly created collection190 #[weight(<SelfWeightOf<T>>::create_collection())]191 #[deprecated(note = "mathod was renamed to `create_nft_collection`, prefer it instead")]192 #[solidity(hide)]193 fn create_nonfungible_collection(194 &mut self,195 caller: caller,196 value: value,197 name: string,198 description: string,199 token_prefix: string,200 ) -> Result<address> {201 self.create_nft_collection(caller, value, name, description, token_prefix)202 }203204 #[weight(<SelfWeightOf<T>>::create_collection())]205 #[solidity(rename_selector = "createRFTCollection")]206 fn create_rft_collection(207 &mut self,208 caller: caller,209 value: value,210 name: string,211 description: string,212 token_prefix: string,213 ) -> Result<address> {214 create_refungible_collection_internal::<T>(caller, value, name, description, token_prefix)215 }216217 #[solidity(rename_selector = "makeCollectionERC721MetadataCompatible")]218 fn make_collection_metadata_compatible(219 &mut self,220 caller: caller,221 collection: address,222 base_uri: string,223 ) -> Result<()> {224 let caller = T::CrossAccountId::from_eth(caller);225 let collection =226 pallet_common::eth::map_eth_to_id(&collection).ok_or("not a collection address")?;227 let mut collection =228 <crate::CollectionHandle<T>>::new(collection).ok_or("collection not found")?;229230 if !matches!(231 collection.mode,232 CollectionMode::NFT | CollectionMode::ReFungible233 ) {234 return Err("target collection should be either NFT or Refungible".into());235 }236237 self.recorder().consume_sstore()?;238 collection239 .check_is_owner_or_admin(&caller)240 .map_err(dispatch_to_evm::<T>)?;241242 if collection.flags.erc721metadata {243 return Err("target collection is already Erc721Metadata compatible".into());244 }245 collection.flags.erc721metadata = true;246247 let all_permissions = <pallet_common::CollectionPropertyPermissions<T>>::get(collection.id);248 if all_permissions.get(&key::url()).is_none() {249 self.recorder().consume_sstore()?;250 <PalletCommon<T>>::set_property_permission(251 &collection,252 &caller,253 up_data_structs::PropertyKeyPermission {254 key: key::url(),255 permission: up_data_structs::PropertyPermission {256 mutable: true,257 collection_admin: true,258 token_owner: false,259 },260 },261 )262 .map_err(dispatch_to_evm::<T>)?;263 }264 if all_permissions.get(&key::suffix()).is_none() {265 self.recorder().consume_sstore()?;266 <PalletCommon<T>>::set_property_permission(267 &collection,268 &caller,269 up_data_structs::PropertyKeyPermission {270 key: key::suffix(),271 permission: up_data_structs::PropertyPermission {272 mutable: true,273 collection_admin: true,274 token_owner: false,275 },276 },277 )278 .map_err(dispatch_to_evm::<T>)?;279 }280281 let all_properties = <pallet_common::CollectionProperties<T>>::get(collection.id);282 if all_properties.get(&key::base_uri()).is_none() && !base_uri.is_empty() {283 self.recorder().consume_sstore()?;284 <PalletCommon<T>>::set_collection_properties(285 &collection,286 &caller,287 vec![up_data_structs::Property {288 key: key::base_uri(),289 value: base_uri290 .into_bytes()291 .try_into()292 .map_err(|_| "base uri is too large")?,293 }],294 )295 .map_err(dispatch_to_evm::<T>)?;296 }297298 self.recorder().consume_sstore()?;299 collection.save().map_err(dispatch_to_evm::<T>)?;300301 Ok(())302 }303304 #[weight(<SelfWeightOf<T>>::destroy_collection())]305 #[solidity(rename_selector = "destroyCollection")]306 fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result<void> {307 let caller = T::CrossAccountId::from_eth(caller);308 let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)309 .ok_or("Invalid collection address format".into())310 .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;311 let collection = <pallet_common::CollectionHandle<T>>::try_get(collection_id)312 .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;313 collection314 .check_is_internal()315 .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;316317 T::CollectionDispatch::destroy(caller, collection)318 .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;319320 let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);321 let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);322 let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);323324 let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);325 let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);326 let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);327328 Ok(())329 }330331 /// Check if a collection exists332 /// @param collectionAddress Address of the collection in question333 /// @return bool Does the collection exist?334 fn is_collection_exist(&self, _caller: caller, collection_address: address) -> Result<bool> {335 if let Some(id) = pallet_common::eth::map_eth_to_id(&collection_address) {336 let collection_id = id;337 return Ok(<CollectionById<T>>::contains_key(collection_id));338 }339340 Ok(false)341 }342343 fn collection_creation_fee(&self) -> Result<value> {344 let price: u128 = T::CollectionCreationPrice::get()345 .try_into()346 .map_err(|_| ()) // workaround for `expect` requiring `Debug` trait347 .expect("Collection creation price should be convertible to u128");348 Ok(price.into())349 }350}351352/// Implements [`OnMethodCall`], which delegates call to [`EvmCollectionHelpers`]353pub struct CollectionHelpersOnMethodCall<T: Config>(PhantomData<*const T>);354impl<T: Config + pallet_nonfungible::Config + pallet_refungible::Config> OnMethodCall<T>355 for CollectionHelpersOnMethodCall<T>356{357 fn is_reserved(contract: &sp_core::H160) -> bool {358 contract == &T::ContractAddress::get()359 }360361 fn is_used(contract: &sp_core::H160) -> bool {362 contract == &T::ContractAddress::get()363 }364365 fn call(handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {366 if handle.code_address() != T::ContractAddress::get() {367 return None;368 }369370 let helpers =371 EvmCollectionHelpers::<T>(SubstrateRecorder::<T>::new(handle.remaining_gas()));372 pallet_evm_coder_substrate::call(handle, helpers)373 }374375 fn get_code(contract: &sp_core::H160) -> Option<Vec<u8>> {376 (contract == &T::ContractAddress::get())377 .then(|| include_bytes!("./stubs/CollectionHelpers.raw").to_vec())378 }379}380381generate_stubgen!(collection_helper_impl, CollectionHelpersCall<()>, true);382generate_stubgen!(collection_helper_iface, CollectionHelpersCall<()>, false);383384fn error_field_too_long(feild: &str, bound: usize) -> Error {385 Error::Revert(format!("{} is too long. Max length is {}.", feild, bound))386}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//! Implementation of CollectionHelpers contract.1819use core::marker::PhantomData;20use ethereum as _;21use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*};22use frame_support::traits::Get;2324use crate::Pallet;2526use pallet_common::{27 CollectionById,28 dispatch::CollectionDispatch,29 erc::{30 CollectionHelpersEvents,31 static_property::{key},32 },33 Pallet as PalletCommon,34};35use pallet_evm_coder_substrate::{dispatch_to_evm, SubstrateRecorder, WithRecorder};36use pallet_evm::{account::CrossAccountId, OnMethodCall, PrecompileHandle, PrecompileResult};37use sp_std::vec;38use up_data_structs::{39 CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData,40 CollectionMode, PropertyValue, CollectionFlags,41};4243use crate::{Config, SelfWeightOf, weights::WeightInfo};4445use sp_std::vec::Vec;46use alloc::format;4748/// See [`CollectionHelpersCall`]49pub struct EvmCollectionHelpers<T: Config>(SubstrateRecorder<T>);50impl<T: Config> WithRecorder<T> for EvmCollectionHelpers<T> {51 fn recorder(&self) -> &SubstrateRecorder<T> {52 &self.053 }5455 fn into_recorder(self) -> SubstrateRecorder<T> {56 self.057 }58}5960fn convert_data<T: Config>(61 caller: caller,62 name: string,63 description: string,64 token_prefix: string,65) -> Result<(66 T::CrossAccountId,67 CollectionName,68 CollectionDescription,69 CollectionTokenPrefix,70)> {71 let caller = T::CrossAccountId::from_eth(caller);72 let name = name73 .encode_utf16()74 .collect::<Vec<u16>>()75 .try_into()76 .map_err(|_| error_field_too_long(stringify!(name), CollectionName::bound()))?;77 let description = description78 .encode_utf16()79 .collect::<Vec<u16>>()80 .try_into()81 .map_err(|_| {82 error_field_too_long(stringify!(description), CollectionDescription::bound())83 })?;84 let token_prefix = token_prefix.into_bytes().try_into().map_err(|_| {85 error_field_too_long(stringify!(token_prefix), CollectionTokenPrefix::bound())86 })?;87 Ok((caller, name, description, token_prefix))88}8990fn create_refungible_collection_internal<91 T: Config + pallet_nonfungible::Config + pallet_refungible::Config,92>(93 caller: caller,94 value: value,95 name: string,96 description: string,97 token_prefix: string,98) -> Result<address> {99 let (caller, name, description, token_prefix) =100 convert_data::<T>(caller, name, description, token_prefix)?;101 let data = CreateCollectionData {102 name,103 mode: CollectionMode::ReFungible,104 description,105 token_prefix,106 ..Default::default()107 };108 check_sent_amount_equals_collection_creation_price::<T>(value)?;109 let collection_helpers_address =110 T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());111112 let collection_id = T::CollectionDispatch::create(113 caller.clone(),114 collection_helpers_address,115 data,116 Default::default(),117 )118 .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;119 let address = pallet_common::eth::collection_id_to_address(collection_id);120 Ok(address)121}122123fn check_sent_amount_equals_collection_creation_price<T: Config>(value: value) -> Result<()> {124 let value = value.as_u128();125 let creation_price: u128 = T::CollectionCreationPrice::get()126 .try_into()127 .map_err(|_| ()) // workaround for `expect` requiring `Debug` trait128 .expect("Collection creation price should be convertible to u128");129 if value != creation_price {130 return Err(format!(131 "Sent amount not equals to collection creation price ({0})",132 creation_price133 )134 .into());135 }136 Ok(())137}138139/// @title Contract, which allows users to operate with collections140#[solidity_interface(name = CollectionHelpers, events(CollectionHelpersEvents))]141impl<T> EvmCollectionHelpers<T>142where143 T: Config + pallet_common::Config + pallet_nonfungible::Config + pallet_refungible::Config,144{145 /// Create an NFT collection146 /// @param name Name of the collection147 /// @param description Informative description of the collection148 /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications149 /// @return address Address of the newly created collection150 #[weight(<SelfWeightOf<T>>::create_collection())]151 #[solidity(rename_selector = "createNFTCollection")]152 fn create_nft_collection(153 &mut self,154 caller: caller,155 value: value,156 name: string,157 description: string,158 token_prefix: string,159 ) -> Result<address> {160 let (caller, name, description, token_prefix) =161 convert_data::<T>(caller, name, description, token_prefix)?;162 let data = CreateCollectionData {163 name,164 mode: CollectionMode::NFT,165 description,166 token_prefix,167 ..Default::default()168 };169 check_sent_amount_equals_collection_creation_price::<T>(value)?;170 let collection_helpers_address =171 T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());172 let collection_id = T::CollectionDispatch::create(173 caller,174 collection_helpers_address,175 data,176 Default::default(),177 )178 .map_err(dispatch_to_evm::<T>)?;179180 let address = pallet_common::eth::collection_id_to_address(collection_id);181 Ok(address)182 }183 /// Create an NFT collection184 /// @param name Name of the collection185 /// @param description Informative description of the collection186 /// @param tokenPrefix Token prefix to represent the collection tokens in UI and user applications187 /// @return address Address of the newly created collection188 #[weight(<SelfWeightOf<T>>::create_collection())]189 #[deprecated(note = "mathod was renamed to `create_nft_collection`, prefer it instead")]190 #[solidity(hide)]191 fn create_nonfungible_collection(192 &mut self,193 caller: caller,194 value: value,195 name: string,196 description: string,197 token_prefix: string,198 ) -> Result<address> {199 self.create_nft_collection(caller, value, name, description, token_prefix)200 }201202 #[weight(<SelfWeightOf<T>>::create_collection())]203 #[solidity(rename_selector = "createRFTCollection")]204 fn create_rft_collection(205 &mut self,206 caller: caller,207 value: value,208 name: string,209 description: string,210 token_prefix: string,211 ) -> Result<address> {212 create_refungible_collection_internal::<T>(caller, value, name, description, token_prefix)213 }214215 #[solidity(rename_selector = "makeCollectionERC721MetadataCompatible")]216 fn make_collection_metadata_compatible(217 &mut self,218 caller: caller,219 collection: address,220 base_uri: string,221 ) -> Result<()> {222 let caller = T::CrossAccountId::from_eth(caller);223 let collection =224 pallet_common::eth::map_eth_to_id(&collection).ok_or("not a collection address")?;225 let mut collection =226 <crate::CollectionHandle<T>>::new(collection).ok_or("collection not found")?;227228 if !matches!(229 collection.mode,230 CollectionMode::NFT | CollectionMode::ReFungible231 ) {232 return Err("target collection should be either NFT or Refungible".into());233 }234235 self.recorder().consume_sstore()?;236 collection237 .check_is_owner_or_admin(&caller)238 .map_err(dispatch_to_evm::<T>)?;239240 if collection.flags.erc721metadata {241 return Err("target collection is already Erc721Metadata compatible".into());242 }243 collection.flags.erc721metadata = true;244245 let all_permissions = <pallet_common::CollectionPropertyPermissions<T>>::get(collection.id);246 if all_permissions.get(&key::url()).is_none() {247 self.recorder().consume_sstore()?;248 <PalletCommon<T>>::set_property_permission(249 &collection,250 &caller,251 up_data_structs::PropertyKeyPermission {252 key: key::url(),253 permission: up_data_structs::PropertyPermission {254 mutable: true,255 collection_admin: true,256 token_owner: false,257 },258 },259 )260 .map_err(dispatch_to_evm::<T>)?;261 }262 if all_permissions.get(&key::suffix()).is_none() {263 self.recorder().consume_sstore()?;264 <PalletCommon<T>>::set_property_permission(265 &collection,266 &caller,267 up_data_structs::PropertyKeyPermission {268 key: key::suffix(),269 permission: up_data_structs::PropertyPermission {270 mutable: true,271 collection_admin: true,272 token_owner: false,273 },274 },275 )276 .map_err(dispatch_to_evm::<T>)?;277 }278279 let all_properties = <pallet_common::CollectionProperties<T>>::get(collection.id);280 if all_properties.get(&key::base_uri()).is_none() && !base_uri.is_empty() {281 self.recorder().consume_sstore()?;282 <PalletCommon<T>>::set_collection_properties(283 &collection,284 &caller,285 vec![up_data_structs::Property {286 key: key::base_uri(),287 value: base_uri288 .into_bytes()289 .try_into()290 .map_err(|_| "base uri is too large")?,291 }],292 )293 .map_err(dispatch_to_evm::<T>)?;294 }295296 self.recorder().consume_sstore()?;297 collection.save().map_err(dispatch_to_evm::<T>)?;298299 Ok(())300 }301302 #[weight(<SelfWeightOf<T>>::destroy_collection())]303 fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result<void> {304 let caller = T::CrossAccountId::from_eth(caller);305306 let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)307 .ok_or("Invalid collection address format")?;308 <Pallet<T>>::destroy_collection_internal(caller, collection_id)309 .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)310 }311312 /// Check if a collection exists313 /// @param collectionAddress Address of the collection in question314 /// @return bool Does the collection exist?315 fn is_collection_exist(&self, _caller: caller, collection_address: address) -> Result<bool> {316 if let Some(id) = pallet_common::eth::map_eth_to_id(&collection_address) {317 let collection_id = id;318 return Ok(<CollectionById<T>>::contains_key(collection_id));319 }320321 Ok(false)322 }323324 fn collection_creation_fee(&self) -> Result<value> {325 let price: u128 = T::CollectionCreationPrice::get()326 .try_into()327 .map_err(|_| ()) // workaround for `expect` requiring `Debug` trait328 .expect("Collection creation price should be convertible to u128");329 Ok(price.into())330 }331}332333/// Implements [`OnMethodCall`], which delegates call to [`EvmCollectionHelpers`]334pub struct CollectionHelpersOnMethodCall<T: Config>(PhantomData<*const T>);335impl<T: Config + pallet_nonfungible::Config + pallet_refungible::Config> OnMethodCall<T>336 for CollectionHelpersOnMethodCall<T>337{338 fn is_reserved(contract: &sp_core::H160) -> bool {339 contract == &T::ContractAddress::get()340 }341342 fn is_used(contract: &sp_core::H160) -> bool {343 contract == &T::ContractAddress::get()344 }345346 fn call(handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {347 if handle.code_address() != T::ContractAddress::get() {348 return None;349 }350351 let helpers =352 EvmCollectionHelpers::<T>(SubstrateRecorder::<T>::new(handle.remaining_gas()));353 pallet_evm_coder_substrate::call(handle, helpers)354 }355356 fn get_code(contract: &sp_core::H160) -> Option<Vec<u8>> {357 (contract == &T::ContractAddress::get())358 .then(|| include_bytes!("./stubs/CollectionHelpers.raw").to_vec())359 }360}361362generate_stubgen!(collection_helper_impl, CollectionHelpersCall<()>, true);363generate_stubgen!(collection_helper_iface, CollectionHelpersCall<()>, false);364365fn error_field_too_long(feild: &str, bound: usize) -> Error {366 Error::Revert(format!("{} is too long. Max length is {}.", feild, bound))367}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> {