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.rsdiffbeforeafterboth--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -19,9 +19,10 @@
use core::marker::PhantomData;
use ethereum as _;
use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*};
-use frame_support::{traits::Get, storage::StorageNMap};
+use frame_support::traits::Get;
+
+use crate::Pallet;
-use crate::sp_api_hidden_includes_decl_storage::hidden_include::StorageDoubleMap;
use pallet_common::{
CollectionById,
dispatch::CollectionDispatch,
@@ -39,10 +40,7 @@
CollectionMode, PropertyValue, CollectionFlags,
};
-use crate::{
- Config, SelfWeightOf, weights::WeightInfo, NftTransferBasket, FungibleTransferBasket,
- ReFungibleTransferBasket, NftApproveBasket, FungibleApproveBasket, RefungibleApproveBasket,
-};
+use crate::{Config, SelfWeightOf, weights::WeightInfo};
use sp_std::vec::Vec;
use alloc::format;
@@ -302,30 +300,13 @@
}
#[weight(<SelfWeightOf<T>>::destroy_collection())]
- #[solidity(rename_selector = "destroyCollection")]
fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result<void> {
let caller = T::CrossAccountId::from_eth(caller);
- let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
- .ok_or("Invalid collection address format".into())
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- let collection = <pallet_common::CollectionHandle<T>>::try_get(collection_id)
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- collection
- .check_is_internal()
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- T::CollectionDispatch::destroy(caller, collection)
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
-
- let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- Ok(())
+ let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
+ .ok_or("Invalid collection address format")?;
+ <Pallet<T>>::destroy_collection_internal(caller, collection_id)
+ .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)
}
/// Check if a collection exists
pallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -20,6 +20,7 @@
/// @dev inlined interface
contract CollectionHelpersEvents {
event CollectionCreated(address indexed owner, address indexed collectionId);
+ event CollectionDestroyed(address indexed collectionId);
}
/// @title Contract, which allows users to operate with collections
pallets/unique/src/lib.rsdiffbeforeafterboth--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -362,25 +362,8 @@
#[weight = <SelfWeightOf<T>>::destroy_collection()]
pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {
let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
- let collection = <CollectionHandle<T>>::try_get(collection_id)?;
- collection.check_is_internal()?;
-
- // =========
-
- T::CollectionDispatch::destroy(sender, collection)?;
- // TODO: basket cleanup should be moved elsewhere
- // Maybe runtime dispatch.rs should perform it?
-
- let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- Ok(())
+ Self::destroy_collection_internal(sender, collection_id)
}
/// Add an address to allow list.
@@ -1151,4 +1134,28 @@
target_collection.save()
}
+
+ #[inline(always)]
+ pub(crate) fn destroy_collection_internal(
+ sender: T::CrossAccountId,
+ collection_id: CollectionId,
+ ) -> DispatchResult {
+ let collection = <CollectionHandle<T>>::try_get(collection_id)?;
+ collection.check_is_internal()?;
+
+ T::CollectionDispatch::destroy(sender, collection)?;
+
+ // TODO: basket cleanup should be moved elsewhere
+ // Maybe runtime dispatch.rs should perform it?
+
+ let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ Ok(())
+ }
}
tests/.vscode/settings.jsondiffbeforeafterboth--- a/tests/.vscode/settings.json
+++ b/tests/.vscode/settings.json
@@ -1,5 +1,12 @@
{
- "mocha.enabled": true,
- "mochaExplorer.files": "**/*.test.ts",
- "mochaExplorer.require": "ts-node/register"
+ "mocha.enabled": true,
+ "mochaExplorer.files": "**/*.test.ts",
+ "mochaExplorer.require": "ts-node/register",
+ "eslint.format.enable": true,
+ "[javascript]": {
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ }
}
tests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -15,6 +15,7 @@
/// @dev inlined interface
interface CollectionHelpersEvents {
event CollectionCreated(address indexed owner, address indexed collectionId);
+ event CollectionDestroyed(address indexed collectionId);
}
/// @title Contract, which allows users to operate with collections
tests/src/eth/collectionHelpersAbi.jsondiffbeforeafterboth--- a/tests/src/eth/collectionHelpersAbi.json
+++ b/tests/src/eth/collectionHelpersAbi.json
@@ -19,6 +19,19 @@
"type": "event"
},
{
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "collectionId",
+ "type": "address"
+ }
+ ],
+ "name": "CollectionDestroyed",
+ "type": "event"
+ },
+ {
"inputs": [],
"name": "collectionCreationFee",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
tests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth1// 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});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});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> {