From 690118c8eab346828ff5060713d1f11240a54174 Mon Sep 17 00:00:00 2001 From: PraetorP Date: Mon, 24 Oct 2022 21:43:48 +0000 Subject: [PATCH] add EVM event for `destoyCollection`, refactor `Unique` pallet code, add test for events --- --- 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", --- 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). + + ## [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. --- 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" --- 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 --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -999,6 +999,13 @@ >::remove(collection.id); >::deposit_event(Event::CollectionDestroyed(collection.id)); + + >::deposit_log( + erc::CollectionHelpersEvents::CollectionDestroyed { + collection_id: eth::collection_id_to_address(collection.id), + } + .to_log(T::ContractAddress::get()), + ); Ok(()) } --- 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(>::destroy_collection())] - #[solidity(rename_selector = "destroyCollection")] fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result { 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::)?; - let collection = >::try_get(collection_id) - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; - collection - .check_is_internal() - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; - T::CollectionDispatch::destroy(caller, collection) - .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; - - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix((collection_id,), u32::MAX, None); - - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::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")?; + >::destroy_collection_internal(caller, collection_id) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::) } /// Check if a collection exists --- 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 --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -362,25 +362,8 @@ #[weight = >::destroy_collection()] pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult { let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); - let collection = >::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 _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix((collection_id,), u32::MAX, None); - - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::clear_prefix(collection_id, u32::MAX, None); - let _ = >::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 = >::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 _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix((collection_id,), u32::MAX, None); + + let _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix(collection_id, u32::MAX, None); + let _ = >::clear_prefix((collection_id,), u32::MAX, None); + + Ok(()) + } } --- 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" + } } --- 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 --- 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" }], --- 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 --- 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 { -- gitstuff