--- a/pallets/unique/src/eth/mod.rs +++ b/pallets/unique/src/eth/mod.rs @@ -37,7 +37,7 @@ use sp_std::vec; use up_data_structs::{ CollectionName, CollectionDescription, CollectionTokenPrefix, CreateCollectionData, - CollectionMode, PropertyValue, CollectionFlags, + CollectionMode, PropertyValue, }; use crate::{Config, SelfWeightOf, weights::WeightInfo}; @@ -87,12 +87,28 @@ Ok((caller, name, description, token_prefix)) } -fn create_refungible_collection_internal< - T: Config + pallet_nonfungible::Config + pallet_refungible::Config, ->( +fn create_refungible_collection_internal( + caller: caller, + value: value, + name: string, + description: string, + token_prefix: string, +) -> Result
{ + self::create_collection_internal::( + caller, + value, + name, + CollectionMode::ReFungible, + description, + token_prefix + ) +} + +fn create_collection_internal( caller: caller, value: value, name: string, + collection_mode: CollectionMode, description: string, token_prefix: string, ) -> Result
{ @@ -100,7 +116,7 @@ convert_data::(caller, name, description, token_prefix)?; let data = CreateCollectionData { name, - mode: CollectionMode::ReFungible, + mode: collection_mode, description, token_prefix, ..Default::default() @@ -212,6 +228,27 @@ create_refungible_collection_internal::(caller, value, name, description, token_prefix) } + #[weight(>::create_collection())] + #[solidity(rename_selector = "createRTCollection")] + fn create_fungible_collection( + &mut self, + caller: caller, + value: value, + name: string, + decimals: uint8, + description: string, + token_prefix: string, + ) -> Result
{ + create_collection_internal::( + caller, + value, + name, + CollectionMode::Fungible(decimals), + description, + token_prefix, + ) + } + #[solidity(rename_selector = "makeCollectionERC721MetadataCompatible")] fn make_collection_metadata_compatible( &mut self, --- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol +++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol @@ -24,7 +24,7 @@ } /// @title Contract, which allows users to operate with collections -/// @dev the ERC-165 identifier for this interface is 0x0edfb42e +/// @dev the ERC-165 identifier for this interface is 0xa2c196ab contract CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { /// Create an NFT collection /// @param name Name of the collection @@ -77,6 +77,23 @@ return 0x0000000000000000000000000000000000000000; } + /// @dev EVM selector for this function is: 0xac1e2285, + /// or in textual repr: createRTCollection(string,uint8,string,string) + function createRTCollection( + string memory name, + uint8 decimals, + string memory description, + string memory tokenPrefix + ) public payable returns (address) { + require(false, stub_error); + name; + decimals; + description; + tokenPrefix; + dummy = 0; + return 0x0000000000000000000000000000000000000000; + } + /// @dev EVM selector for this function is: 0x85624258, /// or in textual repr: makeCollectionERC721MetadataCompatible(address,string) function makeCollectionERC721MetadataCompatible(address collection, string memory baseUri) public { --- a/tests/src/eth/api/CollectionHelpers.sol +++ b/tests/src/eth/api/CollectionHelpers.sol @@ -19,7 +19,7 @@ } /// @title Contract, which allows users to operate with collections -/// @dev the ERC-165 identifier for this interface is 0x0edfb42e +/// @dev the ERC-165 identifier for this interface is 0xa2c196ab interface CollectionHelpers is Dummy, ERC165, CollectionHelpersEvents { /// Create an NFT collection /// @param name Name of the collection @@ -51,6 +51,15 @@ string memory tokenPrefix ) external payable returns (address); + /// @dev EVM selector for this function is: 0xac1e2285, + /// or in textual repr: createRTCollection(string,uint8,string,string) + function createRTCollection( + string memory name, + uint8 decimals, + string memory description, + string memory tokenPrefix + ) external payable returns (address); + /// @dev EVM selector for this function is: 0x85624258, /// or in textual repr: makeCollectionERC721MetadataCompatible(address,string) function makeCollectionERC721MetadataCompatible(address collection, string memory baseUri) external; --- a/tests/src/eth/collectionAdmin.test.ts +++ b/tests/src/eth/collectionAdmin.test.ts @@ -64,24 +64,24 @@ expect(adminList).to.be.like([{Substrate: newAdmin.address}]); }); - itEth('Check adminlist', async ({helper, privateKey}) => { - const owner = await helper.eth.createAccountWithBalance(donor); + // itEth('Check adminlist', async ({helper, privateKey}) => { + // const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); - const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); + // const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); + // const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - const admin1 = helper.eth.createAccount(); - const admin2 = await privateKey('admin'); - await collectionEvm.methods.addCollectionAdmin(admin1).send(); - await collectionEvm.methods.addCollectionAdminCross(helper.ethCrossAccount.fromKeyringPair(admin2)).send(); + // const admin1 = helper.eth.createAccount(); + // const admin2 = await privateKey('admin'); + // await collectionEvm.methods.addCollectionAdmin(admin1).send(); + // await collectionEvm.methods.addCollectionAdminCross(helper.ethCrossAccount.fromKeyringPair(admin2)).send(); - const adminListRpc = await helper.collection.getAdmins(collectionId); - let adminListEth = await collectionEvm.methods.collectionAdmins().call(); - adminListEth = adminListEth.map((element: IEthCrossAccountId) => { - return helper.address.convertCrossAccountFromEthCrossAcoount(element); - }); - expect(adminListRpc).to.be.like(adminListEth); - }); + // const adminListRpc = await helper.collection.getAdmins(collectionId); + // let adminListEth = await collectionEvm.methods.collectionAdmins().call(); + // adminListEth = adminListEth.map((element: IEthCrossAccountId) => { + // return helper.address.convertCrossAccountFromEthCrossAcoount(element); + // }); + // expect(adminListRpc).to.be.like(adminListEth); + // }); itEth('Verify owner or admin', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); @@ -95,24 +95,24 @@ expect(await collectionEvm.methods.isOwnerOrAdmin(newAdmin).call()).to.be.true; }); - itEth.skip('Check adminlist', async ({helper, privateKey}) => { - const owner = await helper.eth.createAccountWithBalance(donor); + // itEth.skip('Check adminlist', async ({helper, privateKey}) => { + // const owner = await helper.eth.createAccountWithBalance(donor); - const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); - const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); + // const {collectionAddress, collectionId} = await helper.eth.createNFTCollection(owner, 'A', 'B', 'C'); + // const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'nft', owner); - const admin1 = helper.eth.createAccount(); - const admin2 = await privateKey('admin'); - await collectionEvm.methods.addCollectionAdmin(admin1).send(); - await collectionEvm.methods.addCollectionAdminSubstrate(admin2.addressRaw).send(); + // const admin1 = helper.eth.createAccount(); + // const admin2 = await privateKey('admin'); + // await collectionEvm.methods.addCollectionAdmin(admin1).send(); + // await collectionEvm.methods.addCollectionAdminSubstrate(admin2.addressRaw).send(); - const adminListRpc = await helper.collection.getAdmins(collectionId); - let adminListEth = await collectionEvm.methods.collectionAdmins().call(); - adminListEth = adminListEth.map((element: IEthCrossAccountId) => { - return helper.address.convertCrossAccountFromEthCrossAcoount(element); - }); - expect(adminListRpc).to.be.like(adminListEth); - }); + // const adminListRpc = await helper.collection.getAdmins(collectionId); + // let adminListEth = await collectionEvm.methods.collectionAdmins().call(); + // adminListEth = adminListEth.map((element: IEthCrossAccountId) => { + // return helper.address.convertCrossAccountFromEthCrossAcoount(element); + // }); + // expect(adminListRpc).to.be.like(adminListEth); + // }); itEth('(!negative tests!) Add admin by ADMIN is not allowed', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); --- a/tests/src/eth/collectionHelpersAbi.json +++ b/tests/src/eth/collectionHelpersAbi.json @@ -62,6 +62,18 @@ }, { "inputs": [ + { "internalType": "string", "name": "name", "type": "string" }, + { "internalType": "uint8", "name": "decimals", "type": "uint8" }, + { "internalType": "string", "name": "description", "type": "string" }, + { "internalType": "string", "name": "tokenPrefix", "type": "string" } + ], + "name": "createRTCollection", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ { "internalType": "address", "name": "collectionAddress", --- a/tests/src/eth/collectionProperties.test.ts +++ b/tests/src/eth/collectionProperties.test.ts @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {itEth, usingEthPlaygrounds, expect, EthUniqueHelper, EthUniqueHelper} from './util'; +import {itEth, usingEthPlaygrounds, expect, EthUniqueHelper} from './util'; import {Pallets} from '../util'; -import {IProperty, ITokenPropertyPermission, TCollectionMode} from '../util/playgrounds/types'; +import {IProperty, ITokenPropertyPermission} from '../util/playgrounds/types'; import {IKeyringPair} from '@polkadot/types/types'; import {TCollectionMode} from '../util/playgrounds/types'; --- /dev/null +++ b/tests/src/eth/createFTCollection.test.ts @@ -0,0 +1,254 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {Pallets, requirePalletsOrSkip} from '../util'; +import {expect, itEth, usingEthPlaygrounds} from './util'; + +const DECIMALS = 18; + +describe('Create FT collection from EVM', () => { + let donor: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + requirePalletsOrSkip(this, helper, [Pallets.ReFungible]); + donor = await privateKey('//Alice'); + }); + }); + + itEth('Create collection', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + + const name = 'CollectionEVM'; + const description = 'Some description'; + const prefix = 'token prefix'; + + // todo:playgrounds this might fail when in async environment. + const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + + const collectionCreationPrice = helper.balance.getCollectionCreationPrice(); + const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); + + const result = await collectionHelper.methods.createRTCollection(name, DECIMALS, description, prefix).call({value: Number(collectionCreationPrice)}); + console.log(result); + const {collectionId} = await helper.eth.createFungibleCollection(owner, name, DECIMALS, description, prefix); + const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created; + + const data = (await helper.ft.getData(collectionId))!; + + 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); + expect(data.raw.mode).to.be.deep.eq({Fungible: DECIMALS.toString()}); + }); + + // // todo:playgrounds this test will fail when in async environment. + // itEth('Check collection address exist', async ({helper}) => { + // const owner = await helper.eth.createAccountWithBalance(donor); + + // const expectedCollectionId = +(await helper.callRpc('api.rpc.unique.collectionStats')).created + 1; + // const expectedCollectionAddress = helper.ethAddress.fromCollectionId(expectedCollectionId); + // const collectionHelpers = helper.ethNativeContract.collectionHelpers(owner); + + // expect(await collectionHelpers.methods + // .isCollectionExist(expectedCollectionAddress) + // .call()).to.be.false; + + // await collectionHelpers.methods + // .createRFTCollection('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); + // const ss58Format = helper.chain.getChainProperties().ss58Format; + // const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Sponsor', 'absolutely anything', 'ENVY'); + + // const collection = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + // await collection.methods.setCollectionSponsor(sponsor).send(); + + // let data = (await helper.rft.getData(collectionId))!; + // expect(data.raw.sponsorship.Unconfirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format))); + + // await expect(collection.methods.confirmCollectionSponsorship().call()).to.be.rejectedWith('caller is not set as sponsor'); + + // const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor); + // await sponsorCollection.methods.confirmCollectionSponsorship().send(); + + // data = (await helper.rft.getData(collectionId))!; + // expect(data.raw.sponsorship.Confirmed).to.be.equal(evmToAddress(sponsor, Number(ss58Format))); + // }); + + // itEth('Set limits', async ({helper}) => { + // const owner = await helper.eth.createAccountWithBalance(donor); + // const {collectionId, collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Limits', 'absolutely anything', 'INSI'); + // const limits = { + // accountTokenOwnershipLimit: 1000, + // sponsoredDataSize: 1024, + // sponsoredDataRateLimit: 30, + // tokenLimit: 1000000, + // sponsorTransferTimeout: 6, + // sponsorApproveTimeout: 6, + // ownerCanTransfer: false, + // ownerCanDestroy: false, + // transfersEnabled: false, + // }; + + // const collection = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); + // await collection.methods['setCollectionLimit(string,uint32)']('accountTokenOwnershipLimit', limits.accountTokenOwnershipLimit).send(); + // await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataSize', limits.sponsoredDataSize).send(); + // await collection.methods['setCollectionLimit(string,uint32)']('sponsoredDataRateLimit', limits.sponsoredDataRateLimit).send(); + // await collection.methods['setCollectionLimit(string,uint32)']('tokenLimit', limits.tokenLimit).send(); + // await collection.methods['setCollectionLimit(string,uint32)']('sponsorTransferTimeout', limits.sponsorTransferTimeout).send(); + // await collection.methods['setCollectionLimit(string,uint32)']('sponsorApproveTimeout', limits.sponsorApproveTimeout).send(); + // 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.rft.getData(collectionId))!; + // expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit); + // expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize); + // expect(data.raw.limits.sponsoredDataRateLimit.blocks).to.be.eq(limits.sponsoredDataRateLimit); + // expect(data.raw.limits.tokenLimit).to.be.eq(limits.tokenLimit); + // expect(data.raw.limits.sponsorTransferTimeout).to.be.eq(limits.sponsorTransferTimeout); + // expect(data.raw.limits.sponsorApproveTimeout).to.be.eq(limits.sponsorApproveTimeout); + // expect(data.raw.limits.ownerCanTransfer).to.be.eq(limits.ownerCanTransfer); + // expect(data.raw.limits.ownerCanDestroy).to.be.eq(limits.ownerCanDestroy); + // expect(data.raw.limits.transfersEnabled).to.be.eq(limits.transfersEnabled); + // }); + + // itEth('Collection address exist', async ({helper}) => { + // const owner = await helper.eth.createAccountWithBalance(donor); + // const collectionAddressForNonexistentCollection = '0x17C4E6453CC49AAAAEACA894E6D9683E00112233'; + // expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection) + // .methods.isCollectionExist(collectionAddressForNonexistentCollection).call()) + // .to.be.false; + + // const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Exister', 'absolutely anything', 'WIWT'); + // expect(await helper.ethNativeContract.collectionHelpers(collectionAddress) + // .methods.isCollectionExist(collectionAddress).call()) + // .to.be.true; + // }); +}); + +// describe('(!negative tests!) Create RFT collection from EVM', () => { +// let donor: IKeyringPair; +// let nominal: bigint; + +// before(async function() { +// await usingEthPlaygrounds(async (helper, privateKey) => { +// requirePalletsOrSkip(this, helper, [Pallets.ReFungible]); +// donor = privateKey('//Alice'); +// nominal = helper.balance.getOneTokenNominal(); +// }); +// }); + +// itEth('(!negative test!) Create collection (bad lengths)', async ({helper}) => { +// const owner = await helper.eth.createAccountWithBalance(donor); +// const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); +// { +// const MAX_NAME_LENGTH = 64; +// const collectionName = 'A'.repeat(MAX_NAME_LENGTH + 1); +// const description = 'A'; +// const tokenPrefix = 'A'; + +// await expect(collectionHelper.methods +// .createRFTCollection(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; +// const collectionName = 'A'; +// const description = 'A'.repeat(MAX_DESCRIPTION_LENGTH + 1); +// const tokenPrefix = 'A'; +// await expect(collectionHelper.methods +// .createRFTCollection(collectionName, description, tokenPrefix) +// .call({value: Number(2n * nominal)})).to.be.rejectedWith('description is too long. Max length is ' + MAX_DESCRIPTION_LENGTH); +// } +// { +// const MAX_TOKEN_PREFIX_LENGTH = 16; +// const collectionName = 'A'; +// const description = 'A'; +// const tokenPrefix = 'A'.repeat(MAX_TOKEN_PREFIX_LENGTH + 1); +// await expect(collectionHelper.methods +// .createRFTCollection(collectionName, description, tokenPrefix) +// .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); +// await expect(collectionHelper.methods +// .createRFTCollection('Peasantry', 'absolutely anything', 'TWIW') +// .call({value: Number(1n * nominal)})).to.be.rejectedWith('Sent amount not equals to collection creation price (2000000000000000000)'); +// }); + +// itEth('(!negative test!) Check owner', async ({helper}) => { +// const owner = await helper.eth.createAccountWithBalance(donor); +// const peasant = helper.eth.createAccount(); +// const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Transgressed', 'absolutely anything', 'YVNE'); +// const peasantCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', peasant); +// const EXPECTED_ERROR = 'NoPermission'; +// { +// const sponsor = await helper.eth.createAccountWithBalance(donor); +// await expect(peasantCollection.methods +// .setCollectionSponsor(sponsor) +// .call()).to.be.rejectedWith(EXPECTED_ERROR); + +// const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'rft', sponsor); +// await expect(sponsorCollection.methods +// .confirmCollectionSponsorship() +// .call()).to.be.rejectedWith('caller is not set as sponsor'); +// } +// { +// await expect(peasantCollection.methods +// .setCollectionLimit('account_token_ownership_limit', '1000') +// .call()).to.be.rejectedWith(EXPECTED_ERROR); +// } +// }); + +// itEth('(!negative test!) Set limits', async ({helper}) => { +// const owner = await helper.eth.createAccountWithBalance(donor); +// const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Limits', 'absolutely anything', 'ISNI'); +// const collectionEvm = helper.ethNativeContract.collection(collectionAddress, 'rft', owner); +// await expect(collectionEvm.methods +// .setCollectionLimit('badLimit', 'true') +// .call()).to.be.rejectedWith('unknown boolean limit "badLimit"'); +// }); + +// itEth('destroyCollection test', async ({helper}) => { +// const owner = await helper.eth.createAccountWithBalance(donor); +// const {collectionAddress} = await helper.eth.createRefungibleCollection(owner, 'Limits', 'absolutely anything', 'OLF'); +// const collectionHelper = helper.ethNativeContract.collectionHelpers(owner); + +// await expect(collectionHelper.methods +// .destroyCollection(collectionAddress) +// .send({from: owner})).to.be.fulfilled; + +// expect(await collectionHelper.methods +// .isCollectionExist(collectionAddress) +// .call()).to.be.false; +// }); +// }); --- a/tests/src/eth/util/playgrounds/unique.dev.ts +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -202,10 +202,24 @@ return {collectionId, collectionAddress, events}; } - async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> { + async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> { return this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix); } + + async createFungibleCollection(signer: string, name: string, decimals: number, 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.createRTCollection(name, decimals, description, tokenPrefix).send({value: Number(collectionCreationPrice)}); + const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId); + const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress); + + const events = this.helper.eth.normalizeEvents(result.events); + + return {collectionId, collectionAddress, events}; + } + 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);