git.delta.rocks / unique-network / refs/commits / 690118c8eab3

difftreelog

add EVM event for `destoyCollection`, refactor `Unique` pallet code, add test for events

PraetorP2022-10-24parent: #73c74cb.patch.diff
in: master

14 files changed

modifiedCargo.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",
modifiedpallets/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.
modifiedpallets/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"
 
modifiedpallets/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
modifiedpallets/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(())
 	}
 
modifiedpallets/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
modifiedpallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/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
modifiedpallets/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(())
+	}
 }
modifiedtests/.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"
+	}
 }
modifiedtests/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
modifiedtests/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" }],
modifiedtests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth
before · tests/src/eth/createNFTCollection.test.ts
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.8//9// 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/>.1617import {evmToAddress} from '@polkadot/util-crypto';18import {IKeyringPair} from '@polkadot/types/types';19import {expect, itEth, usingEthPlaygrounds} from './util';202122describe('Create NFT collection from EVM', () => {23  let donor: IKeyringPair;2425  before(async function() {26    await usingEthPlaygrounds(async (_helper, privateKey) => {27      donor = await privateKey({filename: __filename});28    });29  });3031  itEth('Create collection', async ({helper}) => {32    const owner = await helper.eth.createAccountWithBalance(donor);3334    const name = 'CollectionEVM';35    const description = 'Some description';36    const prefix = 'token prefix';3738    const {collectionId} = await helper.eth.createNFTCollection(owner, name, description, prefix);39    const data = (await helper.rft.getData(collectionId))!;40    const collection = helper.nft.getCollectionObject(collectionId);41    42    expect(data.name).to.be.eq(name);43    expect(data.description).to.be.eq(description);44    expect(data.raw.tokenPrefix).to.be.eq(prefix);45    expect(data.raw.mode).to.be.eq('NFT');4647    const options = await collection.getOptions();4849    expect(options.tokenPropertyPermissions).to.be.empty;50  });5152  itEth('Create collection with properties', async ({helper}) => {53    const owner = await helper.eth.createAccountWithBalance(donor);5455    const name = 'CollectionEVM';56    const description = 'Some description';57    const prefix = 'token prefix';58    const baseUri = 'BaseURI';5960    const {collectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);6162    const collection = helper.nft.getCollectionObject(collectionId);63    const data = (await collection.getData())!;64    65    expect(data.name).to.be.eq(name);66    expect(data.description).to.be.eq(description);67    expect(data.raw.tokenPrefix).to.be.eq(prefix);68    expect(data.raw.mode).to.be.eq('NFT');6970    const options = await collection.getOptions();71    expect(options.tokenPropertyPermissions).to.be.deep.equal([72      {73        key: 'URI',74        permission: {mutable: true, collectionAdmin: true, tokenOwner: false},75      },76      {77        key: 'URISuffix',78        permission: {mutable: true, collectionAdmin: true, tokenOwner: false},79      },80    ]);81  });8283  // this test will occasionally fail when in async environment.84  itEth.skip('Check collection address exist', async ({helper}) => {85    const owner = await helper.eth.createAccountWithBalance(donor);8687    const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1;88    const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId);89    const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);9091    expect(await collectionHelpers.methods92      .isCollectionExist(expectedCollectionAddress)93      .call()).to.be.false;9495    await collectionHelpers.methods96      .createNFTCollection('A', 'A', 'A')97      .send({value: Number(2n * helper.balance.getOneTokenNominal())});98    99    expect(await collectionHelpers.methods100      .isCollectionExist(expectedCollectionAddress)101      .call()).to.be.true;102  });103  104  itEth('Set sponsorship', async ({helper}) => {105    const owner = await helper.eth.createAccountWithBalance(donor);106    const sponsor = await helper.eth.createAccountWithBalance(donor);107    const ss58Format = helper.chain.getChainProperties().ss58Format;108    const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Sponsor', 'absolutely anything', 'ROC');109110    const collection = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);111    await collection.methods.setCollectionSponsor(sponsor).send();112113    let data = (await helper.nft.getData(collectionId))!;114    expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));115116    await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');117118    const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);119    await sponsorCollection.methods.confirmCollectionSponsorship().send();120121    data = (await helper.nft.getData(collectionId))!;122    expect(data.raw.sponsorship.Confirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));123  });124125  itEth('Set limits', async ({helper}) => {126    const owner = await helper.eth.createAccountWithBalance(donor);127    const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'FLO');128    const limits = {129      accountTokenOwnershipLimit: 1000,130      sponsoredDataSize: 1024,131      sponsoredDataRateLimit: 30,132      tokenLimit: 1000000,133      sponsorTransferTimeout: 6,134      sponsorApproveTimeout: 6,135      ownerCanTransfer: false,136      ownerCanDestroy: false,137      transfersEnabled: false,138    };139140    const collection = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);141    await collection.methods['setCollectionLimit(string,uint32)']('accountTokenOwnershipLimit', limits.accountTokenOwnershipLimit).send();142    await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataSize', limits.sponsoredDataSize).send();143    await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataRateLimit', limits.sponsoredDataRateLimit).send();144    await collection.methods['setCollectionLimit(string,uint32)']('tokenLimit', limits.tokenLimit).send();145    await collection.methods['setCollectionLimit(string,uint32)']('sponsorTransferTimeout', limits.sponsorTransferTimeout).send();146    await collection.methods['setCollectionLimit(string,uint32)']('sponsorApproveTimeout', limits.sponsorApproveTimeout).send();147    await collection.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send();148    await collection.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send();149    await collection.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send();150    151    const data = (await helper.nft.getData(collectionId))!;152    expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit);153    expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize);154    expect(data.raw.limits.sponsoredDataRateLimit.blocks).to.be.eq(limits.sponsoredDataRateLimit);155    expect(data.raw.limits.tokenLimit).to.be.eq(limits.tokenLimit);156    expect(data.raw.limits.sponsorTransferTimeout).to.be.eq(limits.sponsorTransferTimeout);157    expect(data.raw.limits.sponsorApproveTimeout).to.be.eq(limits.sponsorApproveTimeout);158    expect(data.raw.limits.ownerCanTransfer).to.be.eq(limits.ownerCanTransfer);159    expect(data.raw.limits.ownerCanDestroy).to.be.eq(limits.ownerCanDestroy);160    expect(data.raw.limits.transfersEnabled).to.be.eq(limits.transfersEnabled);161  });162163  itEth('Collection address exist', async ({helper}) => {164    const owner = await helper.eth.createAccountWithBalance(donor);165    const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233';166    expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection)167      .methods.isCollectionExist(collectionAddressForNonexistentCollection).call())168      .to.be.false;169    170    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC');171    expect(await helper.ethNativeContract.collectionHelpers(collectionAddress)172      .methods.isCollectionExist(collectionAddress).call())173      .to.be.true;174  });175});176177describe('(!negative tests!) Create NFT collection from EVM', () => {178  let donor: IKeyringPair;179  let nominal: bigint;180181  before(async function() {182    await usingEthPlaygrounds(async (helper, privateKey) => {183      donor = await privateKey({filename: __filename});184      nominal = helper.balance.getOneTokenNominal();185    });186  });187188  itEth('(!negative test!) Create collection (bad lengths)', async ({helper}) => {189    const owner = await helper.eth.createAccountWithBalance(donor);190    const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);191    {192      const MAX_NAME_LENGTH = 64;193      const collectionName = 'A'.repeat(MAX_NAME_LENGTH + 1);194      const description = 'A';195      const tokenPrefix = 'A';196197      await expect(collectionHelper.methods198        .createNFTCollection(collectionName, description, tokenPrefix)199        .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);200      201    }202    {203      const MAX_DESCRIPTION_LENGTH = 256;204      const collectionName = 'A';205      const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1);206      const tokenPrefix = 'A';207      await expect(collectionHelper.methods208        .createNFTCollection(collectionName, description, tokenPrefix)209        .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH);210    }211    {212      const MAX_TOKEN_PREFIX_LENGTH = 16;213      const collectionName = 'A';214      const description = 'A';215      const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1);216      await expect(collectionHelper.methods217        .createNFTCollection(collectionName, description, tokenPrefix)218        .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);219    }220  });221  222  itEth('(!negative test!) Create collection (no funds)', async ({helper}) => {223    const owner = await helper.eth.createAccountWithBalance(donor);224    const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);225    await expect(collectionHelper.methods226      .createNFTCollection('Peasantry', 'absolutely anything', 'CVE')227      .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');228  });229230  itEth('(!negative test!) Check owner', async ({helper}) => {231    const owner = await helper.eth.createAccountWithBalance(donor);232    const malfeasant = helper.eth.createAccount();233    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Transgressed', 'absolutely anything', 'COR');234    const malfeasantCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', malfeasant);235    const EXPECTED_ERROR = 'NoPermission';236    {237      const sponsor = await helper.eth.createAccountWithBalance(donor);238      await expect(malfeasantCollection.methods239        .setCollectionSponsor(sponsor)240        .call()).to.be.rejectedWith(EXPECTED_ERROR);241      242      const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);243      await expect(sponsorCollection.methods244        .confirmCollectionSponsorship()245        .call()).to.be.rejectedWith('caller is not set as sponsor');246    }247    {248      await expect(malfeasantCollection.methods249        .setCollectionLimit('account_token_ownership_limit', '1000')250        .call()).to.be.rejectedWith(EXPECTED_ERROR);251    }252  });253254  itEth('(!negative test!) Set limits', async ({helper}) => {255    const owner = await helper.eth.createAccountWithBalance(donor);256    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF');257    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);258    await expect(collectionEvm.methods259      .setCollectionLimit('badLimit', 'true')260      .call()).to.be.rejectedWith('unknown boolean limit "badLimit"');261  });262});
after · tests/src/eth/createNFTCollection.test.ts
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.8//9// 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/>.1617import {evmToAddress} from '@polkadot/util-crypto';18import {IKeyringPair} from '@polkadot/types/types';19import {expect, itEth, usingEthPlaygrounds} from './util';202122describe('Create NFT collection from EVM', () => {23  let donor: IKeyringPair;2425  before(async function () {26    await usingEthPlaygrounds(async (_helper, privateKey) => {27      donor = await privateKey({filename: __filename});28    });29  });3031  itEth('Create collection', async ({helper}) => {32    const owner = await helper.eth.createAccountWithBalance(donor);3334    const name = 'CollectionEVM';35    const description = 'Some description';36    const prefix = 'token prefix';3738    // todo:playgrounds this might fail when in async environment.39    const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;40    const {collectionId, collectionAddress, events} = await helper.eth.createNFTCollection(owner, name, description, prefix);41    42    expect(events).to.be.deep.equal([43      {44        address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',45        event: 'CollectionCreated',46        args: {47          owner: owner,48          collectionId: collectionAddress,49        },50      },51    ]);52    53    const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;5455    const collection = helper.nft.getCollectionObject(collectionId);56    const data = (await collection.getData())!;5758    expect(collectionCountAfter - collectionCountBefore).to.be.eq(1);59    expect(collectionId).to.be.eq(collectionCountAfter);60    expect(data.name).to.be.eq(name);61    expect(data.description).to.be.eq(description);62    expect(data.raw.tokenPrefix).to.be.eq(prefix);63    expect(data.raw.mode).to.be.eq('NFT');6465    const options = await collection.getOptions();6667    expect(options.tokenPropertyPermissions).to.be.empty;68  });6970  itEth('Create collection with properties', async ({helper}) => {71    const owner = await helper.eth.createAccountWithBalance(donor);7273    const name = 'CollectionEVM';74    const description = 'Some description';75    const prefix = 'token prefix';76    const baseUri = 'BaseURI';7778    const {collectionId, collectionAddress, events} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);7980    expect(events).to.be.deep.equal([81      {82        address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',83        event: 'CollectionCreated',84        args: {85          owner: owner,86          collectionId: collectionAddress,87        },88      },89    ]);9091    const collection = helper.nft.getCollectionObject(collectionId);92    const data = (await collection.getData())!;93    94    expect(data.name).to.be.eq(name);95    expect(data.description).to.be.eq(description);96    expect(data.raw.tokenPrefix).to.be.eq(prefix);97    expect(data.raw.mode).to.be.eq('NFT');9899    const options = await collection.getOptions();100    expect(options.tokenPropertyPermissions).to.be.deep.equal([101      {102        key: 'URI',103        permission: {mutable: true, collectionAdmin: true, tokenOwner: false},104      },105      {106        key: 'URISuffix',107        permission: {mutable: true, collectionAdmin: true, tokenOwner: false},108      },109    ]);110  });111112  // this test will occasionally fail when in async environment.113  itEth.skip('Check collection address exist', async ({helper}) => {114    const owner = await helper.eth.createAccountWithBalance(donor);115116    const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1;117    const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId);118    const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner);119120    expect(await collectionHelpers.methods121      .isCollectionExist(expectedCollectionAddress)122      .call()).to.be.false;123124    await collectionHelpers.methods125      .createNFTCollection('A', 'A', 'A')126      .send({value: Number(2n * helper.balance.getOneTokenNominal())});127128    expect(await collectionHelpers.methods129      .isCollectionExist(expectedCollectionAddress)130      .call()).to.be.true;131  });132133  itEth('Set sponsorship', async ({helper}) => {134    const owner = await helper.eth.createAccountWithBalance(donor);135    const sponsor = await helper.eth.createAccountWithBalance(donor);136    const ss58Format = helper.chain.getChainProperties().ss58Format;137    const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Sponsor', 'absolutely anything', 'ROC');138139    const collection = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);140    await collection.methods.setCollectionSponsor(sponsor).send();141142    let data = (await helper.nft.getData(collectionId))!;143    expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));144145    await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor');146147    const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);148    await sponsorCollection.methods.confirmCollectionSponsorship().send();149150    data = (await helper.nft.getData(collectionId))!;151    expect(data.raw.sponsorship.Confirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format)));152  });153154  itEth('Set limits', async ({helper}) => {155    const owner = await helper.eth.createAccountWithBalance(donor);156    const {collectionId, collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'FLO');157    const limits = {158      accountTokenOwnershipLimit: 1000,159      sponsoredDataSize: 1024,160      sponsoredDataRateLimit: 30,161      tokenLimit: 1000000,162      sponsorTransferTimeout: 6,163      sponsorApproveTimeout: 6,164      ownerCanTransfer: false,165      ownerCanDestroy: false,166      transfersEnabled: false,167    };168169    const collection = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);170    await collection.methods['setCollectionLimit(string,uint32)']('accountTokenOwnershipLimit', limits.accountTokenOwnershipLimit).send();171    await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataSize', limits.sponsoredDataSize).send();172    await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataRateLimit', limits.sponsoredDataRateLimit).send();173    await collection.methods['setCollectionLimit(string,uint32)']('tokenLimit', limits.tokenLimit).send();174    await collection.methods['setCollectionLimit(string,uint32)']('sponsorTransferTimeout', limits.sponsorTransferTimeout).send();175    await collection.methods['setCollectionLimit(string,uint32)']('sponsorApproveTimeout', limits.sponsorApproveTimeout).send();176    await collection.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send();177    await collection.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send();178    await collection.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send();179180    const data = (await helper.nft.getData(collectionId))!;181    expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit);182    expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize);183    expect(data.raw.limits.sponsoredDataRateLimit.blocks).to.be.eq(limits.sponsoredDataRateLimit);184    expect(data.raw.limits.tokenLimit).to.be.eq(limits.tokenLimit);185    expect(data.raw.limits.sponsorTransferTimeout).to.be.eq(limits.sponsorTransferTimeout);186    expect(data.raw.limits.sponsorApproveTimeout).to.be.eq(limits.sponsorApproveTimeout);187    expect(data.raw.limits.ownerCanTransfer).to.be.eq(limits.ownerCanTransfer);188    expect(data.raw.limits.ownerCanDestroy).to.be.eq(limits.ownerCanDestroy);189    expect(data.raw.limits.transfersEnabled).to.be.eq(limits.transfersEnabled);190  });191192  itEth('Collection address exist', async ({helper}) => {193    const owner = await helper.eth.createAccountWithBalance(donor);194    const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233';195    expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection)196      .methods.isCollectionExist(collectionAddressForNonexistentCollection).call())197      .to.be.false;198199    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC');200    expect(await helper.ethNativeContract.collectionHelpers(collectionAddress)201      .methods.isCollectionExist(collectionAddress).call())202      .to.be.true;203  });204});205206describe('(!negative tests!) Create NFT collection from EVM', () => {207  let donor: IKeyringPair;208  let nominal: bigint;209210  before(async function () {211    await usingEthPlaygrounds(async (helper, privateKey) => {212      donor = await privateKey({filename: __filename});213      nominal = helper.balance.getOneTokenNominal();214    });215  });216217  itEth('(!negative test!) Create collection (bad lengths)', async ({helper}) => {218    const owner = await helper.eth.createAccountWithBalance(donor);219    const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);220    {221      const MAX_NAME_LENGTH = 64;222      const collectionName = 'A'.repeat(MAX_NAME_LENGTH + 1);223      const description = 'A';224      const tokenPrefix = 'A';225226      await expect(collectionHelper.methods227        .createNFTCollection(collectionName, description, tokenPrefix)228        .call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);229230    }231    {232      const MAX_DESCRIPTION_LENGTH = 256;233      const collectionName = 'A';234      const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1);235      const tokenPrefix = 'A';236      await expect(collectionHelper.methods237        .createNFTCollection(collectionName, description, tokenPrefix)238        .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH);239    }240    {241      const MAX_TOKEN_PREFIX_LENGTH = 16;242      const collectionName = 'A';243      const description = 'A';244      const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1);245      await expect(collectionHelper.methods246        .createNFTCollection(collectionName, description, tokenPrefix)247        .call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);248    }249  });250251  itEth('(!negative test!) Create collection (no funds)', async ({helper}) => {252    const owner = await helper.eth.createAccountWithBalance(donor);253    const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);254    await expect(collectionHelper.methods255      .createNFTCollection('Peasantry', 'absolutely anything', 'CVE')256      .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)');257  });258259  itEth('(!negative test!) Check owner', async ({helper}) => {260    const owner = await helper.eth.createAccountWithBalance(donor);261    const malfeasant = helper.eth.createAccount();262    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Transgressed', 'absolutely anything', 'COR');263    const malfeasantCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', malfeasant);264    const EXPECTED_ERROR = 'NoPermission';265    {266      const sponsor = await helper.eth.createAccountWithBalance(donor);267      await expect(malfeasantCollection.methods268        .setCollectionSponsor(sponsor)269        .call()).to.be.rejectedWith(EXPECTED_ERROR);270271      const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);272      await expect(sponsorCollection.methods273        .confirmCollectionSponsorship()274        .call()).to.be.rejectedWith('caller is not set as sponsor');275    }276    {277      await expect(malfeasantCollection.methods278        .setCollectionLimit('account_token_ownership_limit', '1000')279        .call()).to.be.rejectedWith(EXPECTED_ERROR);280    }281  });282283  itEth('(!negative test!) Set limits', async ({helper}) => {284    const owner = await helper.eth.createAccountWithBalance(donor);285    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF');286    const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner);287    await expect(collectionEvm.methods288      .setCollectionLimit('badLimit', 'true')289      .call()).to.be.rejectedWith('unknown boolean limit "badLimit"');290  });291292  itEth('destroyCollection', async ({helper}) => {293    const owner = await helper.eth.createAccountWithBalance(donor);294    const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF');295    const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);296297298    const result = await collectionHelper.methods299      .destroyCollection(collectionAddress)300      .send({from: owner});301302    const events = helper.eth.normalizeEvents(result.events);303    304    expect(events).to.be.deep.equal([305      {306        address: collectionHelper.options.address,307        event: 'CollectionDestroyed',308        args: {309          collectionId: collectionAddress,310        },311      },312    ]);313314    expect(await collectionHelper.methods315      .isCollectionExist(collectionAddress)316      .call()).to.be.false;317  });318});
modifiedtests/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> {