difftreelog
feat reorginize tests dir
in: master
45 files changed
js-packages/tests/check-event/burnItemEvent.test.tsdiffbeforeafterboth--- a/js-packages/tests/check-event/burnItemEvent.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from '../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
-
-
-describe('Burn Item event ', () => {
- let alice: IKeyringPair;
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([10n], donor);
- });
- });
- itSub('Check event from burnItem(): ', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
- const token = await collection.mintToken(alice, {Substrate: alice.address});
- await token.burn(alice);
- await helper.wait.newBlocks(1);
-
- const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
- const eventStrings = event.map(e => `${e.section}.${e.method}`);
-
- expect(eventStrings).to.contains('common.ItemDestroyed');
- expect(eventStrings).to.contains('treasury.Deposit');
- expect(eventStrings).to.contains('system.ExtrinsicSuccess');
- });
-});
js-packages/tests/check-event/createCollectionEvent.test.tsdiffbeforeafterboth--- a/js-packages/tests/check-event/createCollectionEvent.test.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect} from '../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
-
-describe('Create collection event ', () => {
- let alice: IKeyringPair;
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([10n], donor);
- });
- });
- itSub('Check event from createCollection(): ', async ({helper}) => {
- await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
- await helper.wait.newBlocks(1);
- const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
- const eventStrings = event.map(e => `${e.section}.${e.method}`);
-
- expect(eventStrings).to.contains('common.CollectionCreated');
- expect(eventStrings).to.contains('treasury.Deposit');
- expect(eventStrings).to.contains('system.ExtrinsicSuccess');
- });
-});
js-packages/tests/check-event/createItemEvent.test.tsdiffbeforeafterboth--- a/js-packages/tests/check-event/createItemEvent.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
-import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from '../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
-
-describe('Create Item event ', () => {
- let alice: IKeyringPair;
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([10n], donor);
- });
- });
- itSub('Check event from createItem(): ', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
- await collection.mintToken(alice, {Substrate: alice.address});
- await helper.wait.newBlocks(1);
- const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
- const eventStrings = event.map(e => `${e.section}.${e.method}`);
-
- expect(eventStrings).to.contains('common.ItemCreated');
- expect(eventStrings).to.contains('treasury.Deposit');
- expect(eventStrings).to.contains('system.ExtrinsicSuccess');
- });
-});
js-packages/tests/check-event/createMultipleItemsEvent.test.tsdiffbeforeafterboth--- a/js-packages/tests/check-event/createMultipleItemsEvent.test.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect} from '../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
-
-describe('Create Multiple Items Event event ', () => {
- let alice: IKeyringPair;
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([10n], donor);
- });
- });
- itSub('Check event from createMultipleItems(): ', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
-
- await collection.mintMultipleTokens(alice, [
- {owner: {Substrate: alice.address}},
- {owner: {Substrate: alice.address}},
- ]);
-
- await helper.wait.newBlocks(1);
- const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
- const eventStrings = event.map(e => `${e.section}.${e.method}`);
-
- expect(eventStrings).to.contains('common.ItemCreated');
- expect(eventStrings).to.contains('treasury.Deposit');
- expect(eventStrings).to.contains('system.ExtrinsicSuccess');
- });
-});
js-packages/tests/check-event/destroyCollectionEvent.test.tsdiffbeforeafterboth--- a/js-packages/tests/check-event/destroyCollectionEvent.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
-import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from '../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
-
-describe('Destroy collection event ', () => {
- let alice: IKeyringPair;
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([10n], donor);
- });
- });
-
- itSub('Check event from destroyCollection(): ', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
- await collection.burn(alice);
- await helper.wait.newBlocks(1);
- const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
- const eventStrings = event.map(e => `${e.section}.${e.method}`);
-
- expect(eventStrings).to.contains('common.CollectionDestroyed');
- expect(eventStrings).to.contains('treasury.Deposit');
- expect(eventStrings).to.contains('system.ExtrinsicSuccess');
- });
-});
js-packages/tests/check-event/transferEvent.test.tsdiffbeforeafterboth--- a/js-packages/tests/check-event/transferEvent.test.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from '../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
-
-describe('Transfer event ', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob] = await helper.arrange.createAccounts([10n, 10n], donor);
- });
- });
-
- itSub('Check event from transfer(): ', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
- const token = await collection.mintToken(alice, {Substrate: alice.address});
- await token.transfer(alice, {Substrate: bob.address});
- await helper.wait.newBlocks(1);
- const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
- const eventStrings = event.map(e => `${e.section}.${e.method}`);
-
- expect(eventStrings).to.contains('common.Transfer');
- expect(eventStrings).to.contains('treasury.Deposit');
- expect(eventStrings).to.contains('system.ExtrinsicSuccess');
- });
-});
js-packages/tests/check-event/transferFromEvent.test.tsdiffbeforeafterboth--- a/js-packages/tests/check-event/transferFromEvent.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from '../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
-
-describe('Transfer event ', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob] = await helper.arrange.createAccounts([10n, 10n], donor);
- });
- });
-
- itSub('Check event from transfer(): ', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
- const token = await collection.mintToken(alice, {Substrate: alice.address});
- await token.transferFrom(alice, {Substrate: alice.address}, {Substrate: bob.address});
- await helper.wait.newBlocks(1);
- const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
- const eventStrings = event.map(e => `${e.section}.${e.method}`);
-
- expect(eventStrings).to.contains('common.Transfer');
- expect(eventStrings).to.contains('treasury.Deposit');
- expect(eventStrings).to.contains('system.ExtrinsicSuccess');
- });
-});
js-packages/tests/collator-selection/collatorSelection.seqtest.tsdiffbeforeafterboth--- a/js-packages/tests/collator-selection/collatorSelection.seqtest.ts
+++ /dev/null
@@ -1,423 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../util/index.js';
-
-async function nodeAddress(name: string) {
- // eslint-disable-next-line require-await
- return await usingPlaygrounds(async (helper) => {
- const envNodeStash = `RELAY_UNIQUE_NODE_${name.toUpperCase()}_STASH`;
-
- const nodeStash = process.env[envNodeStash];
- if(nodeStash) {
- return helper.address.normalizeSubstrateToChainFormat(nodeStash);
- } else {
- throw Error(`"${envNodeStash}" env var is not set`);
- }
- });
-}
-
-async function getInitialInvulnerables() {
- return await Promise.all([
- nodeAddress('alpha'),
- nodeAddress('beta'),
- nodeAddress('gamma'),
- nodeAddress('delta'),
- ]);
-}
-
-async function resetInvulnerables() {
- await usingPlaygrounds(async (helper, privateKey) => {
- const superuser = await privateKey('//Alice');
- const initialInvulnerables = await getInitialInvulnerables();
-
- const invulnerables = await helper.collatorSelection.getInvulnerables();
-
- // Remove all invulnerables but the first one
- const firstInvulnerable = invulnerables[0];
-
- let nonce = await helper.chain.getNonce(superuser.address);
- await Promise.all(invulnerables.slice(1).map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++})));
-
- // Add the initial invulnerables
- await Promise.all(initialInvulnerables.map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [invulnerable], true, {nonce: nonce++})));
-
- // Remove the first invulnerable if it's not an initial one
- if(!initialInvulnerables.includes(firstInvulnerable)) {
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [firstInvulnerable]);
- }
- });
-}
-
-// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).
-describe('Integration Test: Collator Selection', () => {
- let superuser: IKeyringPair;
- let previousLicenseBond = 0n;
- let licenseBond = 0n;
-
- before(async function() {
- if(!process.env.RUN_COLLATOR_TESTS) this.skip();
-
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]);
- superuser = await privateKey('//Alice');
-
- previousLicenseBond = await helper.collatorSelection.getLicenseBond();
- licenseBond = 10n * helper.balance.getOneTokenNominal();
- await helper.getSudo().collatorSelection.setLicenseBond(superuser, licenseBond);
- });
- });
-
- describe('Dynamic shuffling of collators', () => {
- // These two are the default invulnerables, and should return to be invulnerables after this suite.
- let alphaNode: string;
- let betaNode: string;
-
- let gammaNode: string;
- let deltaNode: string;
-
- before(async function() {
- await usingPlaygrounds(async (helper) => {
- // todo:collator see again if blocks start to be finalized in dev mode
- // Skip the collator block production in dev mode, since the blocks are sealed automatically.
- if(await helper.arrange.isDevNode()) this.skip();
-
- [alphaNode, betaNode, gammaNode, deltaNode] = await getInitialInvulnerables();
-
- const invulnerables = await helper.collatorSelection.getInvulnerables();
- expect(invulnerables.length, 'Invalid initial invulnerables number').to.be.equal(4);
- expect(invulnerables, 'Invalid initial invulnerables').containSubset([alphaNode, betaNode, gammaNode, deltaNode]);
- });
- });
-
- itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {
- let nonce = await helper.chain.getNonce(superuser.address);
-
- nonce = await helper.chain.getNonce(superuser.address);
- await expect(Promise.all([
- helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alphaNode], true, {nonce: nonce++}),
- helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [betaNode], true, {nonce: nonce++}),
- ])).to.be.fulfilled;
-
- const newInvulnerables = await helper.collatorSelection.getInvulnerables();
- expect(newInvulnerables).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2);
-
- await helper.wait.newSessions(2);
-
- const newValidators = await helper.callRpc('api.query.session.validators');
- expect(newValidators).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2);
-
- const lastBlockNumber = await helper.chain.getLatestBlockNumber();
- await helper.wait.newBlocks(1);
- const lastGammaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [gammaNode])).toNumber();
- const lastDeltaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [deltaNode])).toNumber();
- expect(lastGammaBlock >= lastBlockNumber || lastDeltaBlock >= lastBlockNumber).to.be.true;
- });
-
- after(async () => {
- await resetInvulnerables();
- });
- });
-
- describe('Getting and releasing licenses to collate', () => {
- let crowd: IKeyringPair[];
-
- before(async function() {
- await usingPlaygrounds(async (helper) => {
- crowd = await helper.arrange.createCrowd(20, 100n, superuser);
-
- // set session keys for everyone
- await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));
- });
- });
-
- describe('Positive', () => {
- itSub('Can lease and release a license', async ({helper}) => {
- const account = crowd.pop()!;
-
- // make sure it does not have any reserved funds
- expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n);
-
- // getting a license reserves a license bond cost
- await helper.collatorSelection.obtainLicense(account);
- expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);
- expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(licenseBond);
-
- // releasing a license un-reserves the license bond cost
- await helper.collatorSelection.releaseLicense(account);
- expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);
-
- const balance = await helper.balance.getSubstrateFull(account.address);
- expect(balance.reserved).to.be.equal(0n);
- expect(balance.free > 100n - licenseBond);
- });
-
- itSub('Can force revoke a license', async ({helper}) => {
- const account = crowd.pop()!;
-
- // getting a license reserves a license bond cost
- const previousBalance = await helper.balance.getSubstrateFull(account.address);
- await helper.collatorSelection.obtainLicense(account);
- expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);
-
- // force-releasing a license un-reserves the license bond cost as well
- await helper.getSudo().collatorSelection.forceReleaseLicense(superuser, account.address);
- expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(previousBalance.reserved);
-
- const balance = await helper.balance.getSubstrateFull(account.address);
- expect(balance.reserved).to.be.equal(previousBalance.reserved);
- expect(balance.free > previousBalance.free - licenseBond);
- });
- });
-
- describe('Negative', () => {
- itSub('Cannot get a license without session keys set', async ({helper}) => {
- const [account] = await helper.arrange.createAccounts([100n], superuser);
- await expect(helper.collatorSelection.obtainLicense(account))
- .to.be.rejectedWith(/collatorSelection.ValidatorNotRegistered/);
- });
-
- itSub('Cannot register a license twice', async ({helper}) => {
- const account = crowd.pop()!;
- await helper.collatorSelection.obtainLicense(account);
- await expect(helper.collatorSelection.obtainLicense(account))
- .to.be.rejectedWith(/collatorSelection.AlreadyHoldingLicense/);
- });
-
- itSub('Cannot release a license twice', async ({helper}) => {
- const account = crowd.pop()!;
- await helper.collatorSelection.obtainLicense(account);
- await helper.collatorSelection.releaseLicense(account);
- await expect(helper.collatorSelection.releaseLicense(account))
- .to.be.rejectedWith(/collatorSelection.NoLicense/);
- });
-
- itSub('Cannot force revoke a license as non-sudo', async ({helper}) => {
- const account = crowd.pop()!;
- await helper.collatorSelection.obtainLicense(account);
- await expect(helper.collatorSelection.forceReleaseLicense(superuser, account.address))
- .to.be.rejectedWith(/BadOrigin/);
- });
- });
- });
-
- describe('Onboarding, collating, and offboarding as collator candidates', () => {
- let crowd: IKeyringPair[];
-
- before(async function() {
- await usingPlaygrounds(async (helper) => {
- crowd = await helper.arrange.createCrowd(20, 100n, superuser);
-
- // set session keys for everyone
- await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));
- });
- });
-
- describe('Positive', () => {
- itSub('Can onboard and offboard repeatedly', async ({helper}) => {
- const account = crowd.pop()!;
- await helper.collatorSelection.obtainLicense(account);
- await helper.collatorSelection.onboard(account);
- expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);
-
- await helper.collatorSelection.offboard(account);
- expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);
-
- await helper.collatorSelection.onboard(account);
- expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);
-
- await helper.collatorSelection.offboard(account);
- expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);
- });
-
- itSub('Penalizes and forfeits license from faulty collators', async ({helper}) => {
- // This one shouldn't even be able to produce blocks.
- const account = crowd.pop()!;
- await helper.collatorSelection.obtainLicense(account);
- await helper.collatorSelection.onboard(account);
- expect(await helper.collatorSelection.getCandidates()).to.contain(account.address);
-
- // Wait for 3 new sessions before checking that the collator will be kicked:
- // one to get collator onboarded, and another two for the collator to fail
- await helper.wait.newSessions(3);
-
- expect(await helper.collatorSelection.getCandidates()).to.not.contain(account.address);
- expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);
-
- // The account's reserved funds get slashed as a penalty
- const balance = await helper.balance.getSubstrateFull(account.address);
- expect(balance.reserved).to.be.equal(0n);
- expect(balance.free < 100n - licenseBond);
- });
- });
-
- describe('Negative', () => {
- itSub('Cannot onboard without a license', async ({helper}) => {
- const account = crowd.pop()!;
- await expect(helper.collatorSelection.onboard(account))
- .to.be.rejectedWith(/collatorSelection.NoLicense/);
- });
-
- itSub('Cannot offboard without a license', async ({helper}) => {
- const account = crowd.pop()!;
- await expect(helper.collatorSelection.offboard(account))
- .to.be.rejectedWith(/collatorSelection.NotCandidate/);
- });
-
- itSub('Cannot offboard while not onboarded', async ({helper}) => {
- const account = crowd.pop()!;
- await helper.collatorSelection.obtainLicense(account);
- await expect(helper.collatorSelection.offboard(account))
- .to.be.rejectedWith(/collatorSelection.NotCandidate/);
- });
-
- itSub('Cannot onboard while already onboarded', async ({helper}) => {
- const account = crowd.pop()!;
- await helper.collatorSelection.obtainLicense(account);
- await helper.collatorSelection.onboard(account);
- await expect(helper.collatorSelection.onboard(account))
- .to.be.rejectedWith(/collatorSelection.AlreadyCandidate/);
- });
- });
- });
-
- describe('Addition and removal of invulnerables', () => {
- describe('Positive', () => {
- itSub('Adds an invulnerable', async ({helper}) => {
- const [account] = await helper.arrange.createAccounts([10n], superuser);
- const invulnerables = await helper.collatorSelection.getInvulnerables();
-
- await helper.session.setOwnKeysFromAddress(account);
- await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address);
-
- const newInvulnerables = await helper.collatorSelection.getInvulnerables();
- expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables);
- });
-
- itSub('Removes an invulnerable', async ({helper}) => {
- const invulnerables = await helper.collatorSelection.getInvulnerables();
- const lastInvulnerable = invulnerables.pop()!;
-
- await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable);
- const newInvulnerables = await helper.collatorSelection.getInvulnerables();
- // invulnerables had its last element removed, so they should be equal
- expect(newInvulnerables).to.have.all.members(invulnerables);
- });
- });
-
- describe('Negative', () => {
- itSub('Does not duplicate an invulnerable', async ({helper}) => {
- const invulnerables = await helper.collatorSelection.getInvulnerables();
- // adding an already invulnerable should not fail, but should not duplicate it either
- await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0]))
- .to.be.fulfilled;
- const newInvulnerables = await helper.collatorSelection.getInvulnerables();
- expect(newInvulnerables).to.have.all.members(invulnerables);
- });
-
- itSub('Cannot remove a non-existent invulnerable', async ({helper}) => {
- const [account] = await helper.arrange.createAccounts([0n], superuser);
- await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, account.address))
- .to.be.rejectedWith(/collatorSelection.NotInvulnerable/);
- });
-
- itSub('Cannot allow invulnerables to be empty', async ({helper}) => {
- const invulnerables = await helper.collatorSelection.getInvulnerables();
- const lastInvulnerable = invulnerables.pop()!;
-
- let nonce = await helper.chain.getNonce(superuser.address);
- await Promise.all(invulnerables.map((i: any) =>
- helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++})));
-
- await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable))
- .to.be.rejectedWith(/collatorSelection.TooFewInvulnerables/);
-
- const newInvulnerables = await helper.collatorSelection.getInvulnerables();
- expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]);
-
- // restore the invulnerables to the previous state
- nonce = await helper.chain.getNonce(superuser.address);
- await Promise.all(invulnerables.map((i: any) =>
- helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++})));
- });
-
- itSub('Cannot have too many invulnerables', async ({helper}) => {
- // todo:collator make sure that there is enough session time for a set of tests
- // 28 non-functioning collators, teehee.
-
- const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;
- const invulnerablesUntilLimit = (await helper.collatorSelection.getDesiredCollators()) - invulnerablesLength;
- const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);
- const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);
-
- await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
- helper.session.setOwnKeysFromAddress(i)));
- await helper.session.setOwnKeysFromAddress(lastInvulnerable);
-
- let nonce = await helper.chain.getNonce(superuser.address);
- await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
- helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++})));
-
- await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address))
- .to.be.rejectedWith(/collatorSelection.TooManyInvulnerables/);
-
- // restore the invulnerables to the previous state
- nonce = await helper.chain.getNonce(superuser.address);
- await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
- helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++})));
- });
-
- itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => {
- const [account] = await helper.arrange.createAccounts([10n], superuser);
- const invulnerables = await helper.collatorSelection.getInvulnerables();
-
- await helper.session.setOwnKeysFromAddress(account);
- await expect(helper.collatorSelection.addInvulnerable(superuser, account.address))
- .to.be.rejectedWith(/BadOrigin/);
-
- const newInvulnerables = await helper.collatorSelection.getInvulnerables();
- expect(newInvulnerables).to.be.members(invulnerables);
- });
-
- itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => {
- const invulnerables = await helper.collatorSelection.getInvulnerables();
- await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0]))
- .to.be.rejectedWith(/BadOrigin/);
- expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables);
- });
-
- after(async function() {
- await resetInvulnerables();
- });
- });
- });
-
- after(async function() {
- if(!process.env.RUN_COLLATOR_TESTS) return;
-
- await usingPlaygrounds(async (helper) => {
- if(helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return;
-
- await helper.getSudo().collatorSelection.setLicenseBond(superuser, previousLicenseBond);
-
- const candidates = await helper.collatorSelection.getCandidates();
- let nonce = await helper.chain.getNonce(superuser.address);
- await Promise.all(candidates.map(candidate =>
- helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.forceReleaseLicense', [candidate], true, {nonce: nonce++})));
- });
- });
-});
js-packages/tests/collator-selection/identity.seqtest.tsdiffbeforeafterboth--- a/js-packages/tests/collator-selection/identity.seqtest.ts
+++ /dev/null
@@ -1,284 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../util/index.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
-
-async function getIdentities(helper: UniqueHelper) {
- const identities: [string, any][] = [];
- for(const [key, value] of await helper.getApi().query.identity.identityOf.entries())
- identities.push([(key as any).toHuman(), (value as any).unwrap()]);
- return identities;
-}
-
-async function getIdentityAccounts(helper: UniqueHelper) {
- return (await getIdentities(helper)).flatMap(([key, _value]) => key);
-}
-
-async function getSubIdentityAccounts(helper: UniqueHelper, address: string) {
- return ((await helper.getApi().query.identity.subsOf(address)).toHuman() as any)[1];
-}
-
-async function getSubIdentityName(helper: UniqueHelper, address: string) {
- return ((await helper.getApi().query.identity.superOf(address)).toHuman() as any);
-}
-
-describe('Integration Test: Identities Manipulation', () => {
- let superuser: IKeyringPair;
-
- before(async function() {
- if(!process.env.RUN_COLLATOR_TESTS) this.skip();
-
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.Identity]);
- superuser = await privateKey('//Alice');
- });
- });
-
- itSub('Normal calls do not work', async ({helper}) => {
- // console.error = () => {};
- await expect(helper.executeExtrinsic(superuser, 'api.tx.identity.setIdentity', [{info: {display: {Raw: 'Meowser'}}}] as any))
- .to.be.rejectedWith(/Transaction call is not expected/);
- });
-
- describe('Identities', () => {
- itSub('Sets identities', async ({helper}) => {
- const oldIdentitiesCount = (await getIdentityAccounts(helper)).length;
-
- const crowdSize = 10;
- const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
- const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]);
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
-
- expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + crowdSize);
- });
-
- itSub('Setting identities does not delete existing but does overwrite', async ({helper}) => {
- const crowd = await helper.arrange.createCrowd(10, 0n, superuser);
- const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]);
-
- // insert a single identity
- let singleIdentity = identities.pop()!;
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [[singleIdentity]] as any);
-
- const oldIdentitiesCount = (await getIdentityAccounts(helper)).length;
-
- // change an identity and push it with a few new others
- singleIdentity = [singleIdentity[0], {info: {display: {Raw: 'something special'}}}];
- identities.push(singleIdentity);
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
-
- // oldIdentitiesCount + 9 because one identity is overwritten, not inserted on top
- expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + 9);
- expect((await helper.callRpc('api.query.identity.identityOf', [singleIdentity[0]])).toHuman().info.display)
- .to.be.deep.equal({Raw: 'something special'});
- });
-
- itSub('Removes identities', async ({helper}) => {
- const crowd = await helper.arrange.createCrowd(10, 0n, superuser);
- const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]);
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
- const oldIdentities = await getIdentityAccounts(helper);
-
- // delete a couple, check that they are no longer there
- const scapegoats = [crowd.pop()!.address, crowd.pop()!.address];
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [scapegoats]);
- const newIdentities = await getIdentityAccounts(helper);
- expect(newIdentities.concat(scapegoats)).to.be.have.members(oldIdentities);
- });
- });
-
- describe('Sub-identities', () => {
- itSub('Sets subs', async ({helper}) => {
- const crowdSize = 18;
- const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
- const supers = [crowd.pop()!, crowd.pop()!, crowd.pop()!];
-
- const subsPerSup = crowd.length / supers.length;
- let subCount = 0;
- const subs = [
- crowd.slice(subCount, subCount += subsPerSup + 1),
- crowd.slice(subCount, subCount += subsPerSup),
- crowd.slice(subCount, subCount += subsPerSup - 1),
- ];
-
- const subsInfo = supers.map((acc, i) => [
- acc.address, [
- 1000000n + BigInt(i + 1),
- subs[i].map((sub, j) => [
- sub.address, {Raw: `accounter #${j}`},
- ]),
- ],
- ]);
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo] as any);
-
- for(let i = 0; i < supers.length; i++) {
- // check deposit
- expect(((await helper.getApi().query.identity.subsOf(supers[i].address)).toJSON() as any)[0]).to.be.equal(1000001 + i);
-
- const subsAccounts = await getSubIdentityAccounts(helper, supers[i].address);
- // check sub-identities as account ids
- expect(subsAccounts).to.include.members(subs[i].map(x => x.address));
-
- for(let j = 0; j < subsAccounts.length; j++) {
- // check sub-identities' names
- expect((await getSubIdentityName(helper, subsAccounts[j]))[1]).to.be.deep.equal({Raw: `accounter #${j}`});
- }
- }
- });
-
- itSub('Setting sub-identities does not delete other existing but does overwrite own', async ({helper}) => {
- const crowdSize = 18;
- const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
- const supers = [crowd.pop()!, crowd.pop()!, crowd.pop()!];
-
- const subsPerSup = crowd.length / supers.length;
- let subCount = 0;
- const subs = [
- crowd.slice(subCount, subCount += subsPerSup + 1),
- crowd.slice(subCount, subCount += subsPerSup),
- crowd.slice(subCount, subCount += subsPerSup - 1),
- ];
-
- const subsInfo1 = supers.map((acc, i) => [
- acc.address, [
- 1000000n + BigInt(i + 1),
- subs[i].map((sub, j) => [
- sub.address, {Raw: `accounter #${j}`},
- ]),
- ],
- ]);
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo1] as any);
-
- // change some sub-identities...
- subs[2].pop(); subs[2].pop(); subs[2].push(...await helper.arrange.createAccounts([0n], superuser));
-
- // ...and set them
- const subsInfo2 = [[
- supers[2].address, [
- 999999n,
- subs[2].map((sub, j) => [
- sub.address, {Raw: `discounter #${j}`},
- ]),
- ],
- ]];
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo2] as any);
-
- // make sure everything else is the same
- for(let i = 0; i < supers.length - 1; i++) {
- // check deposit
- expect(((await helper.getApi().query.identity.subsOf(supers[i].address)).toJSON() as any)[0]).to.be.equal(1000001 + i);
-
- const subsAccounts = await getSubIdentityAccounts(helper, supers[i].address);
- // check sub-identities as account ids
- expect(subsAccounts).to.include.members(subs[i].map(x => x.address));
-
- for(let j = 0; j < subsAccounts; j++) {
- // check sub-identities' names
- expect((await getSubIdentityName(helper, subsAccounts[j]))[1]).to.be.deep.equal({Raw: `accounter #${j}`});
- }
- }
-
- // check deposit
- expect(((await helper.getApi().query.identity.subsOf(supers[2].address)).toJSON() as any)[0]).to.be.equal(999999);
-
- const subsAccounts = await getSubIdentityAccounts(helper, supers[2].address);
- // check sub-identities as account ids
- expect(subsAccounts).to.include.members(subs[2].map(x => x.address));
-
- for(let j = 0; j < subsAccounts.length; j++) {
- // check sub-identities' names
- expect((await getSubIdentityName(helper, subsAccounts[j]))[1]).to.be.deep.equal({Raw: `discounter #${j}`});
- }
- });
-
- itSub('Removes sub-identities', async ({helper}) => {
- const crowdSize = 3;
- const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
- const sup = crowd.pop()!;
-
- const subsInfo1 = [[
- sup.address, [
- 1000000n,
- crowd.map((sub, j) => [
- sub.address, {Raw: `accounter #${j}`},
- ]),
- ],
- ]];
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo1] as any);
-
- // empty sub-identities should delete the records
- const subsInfo2 = [[
- sup.address, [
- 1000000n,
- [],
- ],
- ]];
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo2] as any);
-
- // check deposit
- expect((await helper.getApi().query.identity.subsOf(sup.address)).toHuman()).to.be.deep.equal(['0', []]);
-
- for(let j = 0; j < crowd.length; j++) {
- // check sub-identities' names
- expect((await getSubIdentityName(helper, crowd[j].address))).to.be.null;
- }
- });
-
- itSub('Removing identities deletes associated sub-identities', async ({helper}) => {
- const crowd = await helper.arrange.createCrowd(3, 0n, superuser);
- const sup = crowd.pop()!;
-
- // insert identity
- const identities = [[sup.address, {info: {display: {Raw: 'mental'}}}]];
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
-
- // and its sub-identities
- const subsInfo = [[
- sup.address, [
- 1000000n,
- crowd.map((sub, j) => [
- sub.address, {Raw: `accounter #${j}`},
- ]),
- ],
- ]];
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo] as any);
-
- // delete top identity
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [[sup.address]]);
-
- // check that sub-identities are deleted
- expect((await helper.getApi().query.identity.subsOf(sup.address)).toHuman()).to.be.deep.equal(['0', []]);
-
- for(let j = 0; j < crowd.length; j++) {
- // check sub-identities' names
- expect((await getSubIdentityName(helper, crowd[j].address))).to.be.null;
- }
- });
- });
-
- after(async function() {
- if(!process.env.RUN_COLLATOR_TESTS) return;
-
- await usingPlaygrounds(async helper => {
- if(helper.fetchMissingPalletNames([Pallets.Identity]).length != 0) return;
-
- const identitiesToRemove: string[] = await getIdentityAccounts(helper);
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [identitiesToRemove]);
- });
- });
-});
js-packages/tests/governance/council.test.tsdiffbeforeafterboth--- a/js-packages/tests/governance/council.test.ts
+++ /dev/null
@@ -1,457 +0,0 @@
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
-import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin} from './util.js';
-import type {ICounselors} from './util.js';
-
-describeGov('Governance: Council tests', () => {
- let donor: IKeyringPair;
- let counselors: ICounselors;
- let sudoer: IKeyringPair;
-
- const moreThanHalfCouncilThreshold = 3;
-
- before(async function() {
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.Council]);
-
- donor = await privateKey({url: import.meta.url});
- sudoer = await privateKey('//Alice');
- });
- });
-
- beforeEach(async () => {
- counselors = await initCouncil(donor, sudoer);
- });
-
- afterEach(async () => {
- await clearCouncil(sudoer);
- await clearTechComm(sudoer);
- });
-
- async function proposalFromMoreThanHalfCouncil(proposal: any) {
- return await usingPlaygrounds(async (helper) => {
- expect((await helper.callRpc('api.query.councilMembership.members')).toJSON().length).to.be.equal(5);
- const proposeResult = await helper.council.collective.propose(
- counselors.filip,
- proposal,
- moreThanHalfCouncilThreshold,
- );
-
- const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
- const proposalIndex = councilProposedEvent.proposalIndex;
- const proposalHash = councilProposedEvent.proposalHash;
-
-
- await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);
-
- return await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
- });
- }
-
- async function proposalFromAllCouncil(proposal: any) {
- return await usingPlaygrounds(async (helper) => {
- expect((await helper.callRpc('api.query.councilMembership.members')).toJSON().length).to.be.equal(5);
- const proposeResult = await helper.council.collective.propose(
- counselors.filip,
- proposal,
- moreThanHalfCouncilThreshold,
- );
-
- const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
- const proposalIndex = councilProposedEvent.proposalIndex;
- const proposalHash = councilProposedEvent.proposalHash;
-
-
- await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.ildar, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.irina, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);
-
- return await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
- });
- }
-
- itSub('>50% of Council can externally propose SuperMajorityAgainst', async ({helper}) => {
- const forceSetBalanceReceiver = helper.arrange.createEmptyAccount();
- const forceSetBalanceTestValue = 20n * 10n ** 25n;
-
- const democracyProposal = await helper.constructApiCall('api.tx.balances.forceSetBalance', [
- forceSetBalanceReceiver.address, forceSetBalanceTestValue,
- ]);
-
- const councilProposal = await helper.democracy.externalProposeDefaultCall(democracyProposal);
-
- const proposeResult = await helper.council.collective.propose(
- counselors.filip,
- councilProposal,
- moreThanHalfCouncilThreshold,
- );
-
- const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
- const proposalIndex = councilProposedEvent.proposalIndex;
- const proposalHash = councilProposedEvent.proposalHash;
-
- await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);
- await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);
-
- await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
-
- const democracyStartedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
- const democracyReferendumIndex = democracyStartedEvent.referendumIndex;
- const democracyThreshold = democracyStartedEvent.threshold;
-
- expect(democracyThreshold).to.be.equal('SuperMajorityAgainst');
-
- await helper.democracy.vote(counselors.filip, democracyReferendumIndex, {
- Standard: {
- vote: {
- aye: true,
- conviction: 1,
- },
- balance: 10_000n,
- },
- });
-
- await helper.democracy.vote(counselors.charu, democracyReferendumIndex, {
- Standard: {
- vote: {
- aye: false,
- conviction: 1,
- },
- balance: 50_000n,
- },
- });
-
- const passedReferendumEvent = await helper.wait.expectEvent(democracyVotingPeriod, Event.Democracy.Passed);
- expect(passedReferendumEvent.referendumIndex).to.be.equal(democracyReferendumIndex);
-
- await helper.wait.expectEvent(democracyEnactmentPeriod, Event.Scheduler.Dispatched);
- const receiverBalance = await helper.balance.getSubstrate(forceSetBalanceReceiver.address);
- expect(receiverBalance).to.be.equal(forceSetBalanceTestValue);
- });
-
- itSub('Council prime member vote is the default', async ({helper}) => {
- const newTechCommMember = helper.arrange.createEmptyAccount();
- const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address);
- const proposeResult = await helper.council.collective.propose(
- counselors.filip,
- addMemberProposal,
- moreThanHalfCouncilThreshold,
- );
-
- const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
- const proposalIndex = councilProposedEvent.proposalIndex;
- const proposalHash = councilProposedEvent.proposalHash;
-
- await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
-
- await helper.wait.newBlocks(councilMotionDuration);
- const closeResult = await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
- const closeEvent = Event.Council.Closed.expect(closeResult);
- const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON() as string[];
- expect(closeEvent.yes).to.be.equal(members.length);
- });
-
- itSub('Superuser can add a member', async ({helper}) => {
- const newMember = helper.arrange.createEmptyAccount();
- await expect(helper.getSudo().council.membership.addMember(sudoer, newMember.address)).to.be.fulfilled;
-
- const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
- expect(members).to.contains(newMember.address);
- });
-
- itSub('Superuser can remove a member', async ({helper}) => {
- await expect(helper.getSudo().council.membership.removeMember(sudoer, counselors.alex.address)).to.be.fulfilled;
-
- const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
- expect(members).to.not.contains(counselors.alex.address);
- });
-
- itSub('>50% Council can add TechComm member', async ({helper}) => {
- const newTechCommMember = helper.arrange.createEmptyAccount();
- const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address);
-
- await proposalFromMoreThanHalfCouncil(addMemberProposal);
-
- const techCommMembers = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
- expect(techCommMembers).to.contains(newTechCommMember.address);
- });
-
- itSub('Council can remove TechComm member', async ({helper}) => {
- const techComm = await initTechComm(donor, sudoer);
- const removeMemberPrpoposal = helper.technicalCommittee.membership.removeMemberCall(techComm.andy.address);
- await proposalFromMoreThanHalfCouncil(removeMemberPrpoposal);
-
- const techCommMembers = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
- expect(techCommMembers).to.not.contains(techComm.andy.address);
- });
-
- itSub.skip('Council member can add Fellowship member', async ({helper}) => {
- const newFellowshipMember = helper.arrange.createEmptyAccount();
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.fellowship.collective.addMemberCall(newFellowshipMember.address),
- )).to.be.fulfilled;
- const fellowshipMembers = (await helper.callRpc('api.query.fellowshipCollective.members')).toJSON();
- expect(fellowshipMembers).to.contains(newFellowshipMember.address);
- });
-
- itSub('>50% Council can promote Fellowship member', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
- const memberWithZeroRank = fellowship[0][0];
-
- const proposal = helper.fellowship.collective.promoteCall(memberWithZeroRank.address);
- await proposalFromMoreThanHalfCouncil(proposal);
- const record = (await helper.callRpc('api.query.fellowshipCollective.members', [memberWithZeroRank.address])).toJSON();
- expect(record).to.be.deep.equal({rank: 1});
-
- await clearFellowship(sudoer);
- });
-
- itSub('>50% Council can demote Fellowship member', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
- const memberWithRankOne = fellowship[1][0];
-
- const proposal = helper.fellowship.collective.demoteCall(memberWithRankOne.address);
- await proposalFromMoreThanHalfCouncil(proposal);
-
- const record = (await helper.callRpc('api.query.fellowshipCollective.members', [memberWithRankOne.address])).toJSON();
- expect(record).to.be.deep.equal({rank: 0});
-
- await clearFellowship(sudoer);
- });
-
- itSub('>50% Council can add\remove Fellowship member', async ({helper}) => {
- try {
- const newMember = helper.arrange.createEmptyAccount();
-
- const proposalAdd = helper.fellowship.collective.addMemberCall(newMember.address);
- const proposalRemove = helper.fellowship.collective.removeMemberCall(newMember.address, fellowshipRankLimit);
- await expect(proposalFromMoreThanHalfCouncil(proposalAdd)).to.be.fulfilled;
- expect(await helper.fellowship.collective.getMembers()).to.be.deep.contain(newMember.address);
- await expect(proposalFromMoreThanHalfCouncil(proposalRemove)).to.be.fulfilled;
- expect(await helper.fellowship.collective.getMembers()).to.be.not.deep.contain(newMember.address);
- }
- finally {
- await clearFellowship(sudoer);
- }
- });
-
- itSub('Council can blacklist Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await expect(proposalFromAllCouncil(helper.democracy.blacklistCall(preimageHash, null))).to.be.fulfilled;
- });
-
- itSub('Sudo can blacklist Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await expect(helper.getSudo().democracy.blacklist(sudoer, preimageHash)).to.be.fulfilled;
- });
-
- itSub('[Negative] Council cannot add Council member', async ({helper}) => {
- const newCouncilMember = helper.arrange.createEmptyAccount();
- const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
-
- await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejected;
- });
-
- itSub('[Negative] Council cannot remove Council member', async ({helper}) => {
- const removeMemberProposal = helper.council.membership.removeMemberCall(counselors.alex.address);
-
- await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejected;
- });
-
- itSub('[Negative] Council cannot submit regular democracy proposal', async ({helper}) => {
- const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
-
- await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Council cannot externally propose SimpleMajority', async ({helper}) => {
- const councilProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
-
- await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Council cannot externally propose SuperMajorityApprove', async ({helper}) => {
- const councilProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
-
- await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Council member cannot add/remove a Council member', async ({helper}) => {
- const newCouncilMember = helper.arrange.createEmptyAccount();
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.council.membership.addMemberCall(newCouncilMember.address),
- )).to.be.rejectedWith('BadOrigin');
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.council.membership.removeMemberCall(counselors.charu.address),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] Council cannot set/clear Council prime member', async ({helper}) => {
- const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
- const proposalForClear = await helper.council.membership.clearPrimeCall();
-
- await expect(proposalFromAllCouncil(proposalForSet)).to.be.rejectedWith(/BadOrigin/);
- await expect(proposalFromAllCouncil(proposalForClear)).to.be.rejectedWith(/BadOrigin/);
-
- });
-
- itSub('[Negative] Council member cannot set/clear Council prime member', async ({helper}) => {
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.council.membership.setPrimeCall(counselors.charu.address),
- )).to.be.rejectedWith('BadOrigin');
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.council.membership.clearPrimeCall(),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] Council member cannot add/remove a TechComm member', async ({helper}) => {
- const newTechCommMember = helper.arrange.createEmptyAccount();
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address),
- )).to.be.rejectedWith('BadOrigin');
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.technicalCommittee.membership.removeMemberCall(newTechCommMember.address),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] Council member cannot promote/demote a Fellowship member', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
- const memberWithRankOne = fellowship[1][0];
-
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.fellowship.collective.promoteCall(memberWithRankOne.address),
- )).to.be.rejectedWith('BadOrigin');
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.fellowship.collective.demoteCall(memberWithRankOne.address),
- )).to.be.rejectedWith('BadOrigin');
- await clearFellowship(sudoer);
- });
-
- itSub('[Negative] Council cannot fast-track Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await expect(proposalFromAllCouncil(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0)))
- .to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Council member cannot fast-track Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] Council cannot cancel Democracy proposals', async ({helper}) => {
- const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
- const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
-
- await expect(proposalFromAllCouncil(helper.democracy.cancelProposalCall(proposalIndex)))
- .to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Council member cannot cancel Democracy proposals', async ({helper}) => {
-
- const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
- const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
-
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.democracy.cancelProposalCall(proposalIndex),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] Council cannot cancel ongoing Democracy referendums', async ({helper}) => {
- await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
- const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
- const referendumIndex = startedEvent.referendumIndex;
-
- await expect(proposalFromAllCouncil(helper.democracy.emergencyCancelCall(referendumIndex)))
- .to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Council member cannot cancel ongoing Democracy referendums', async ({helper}) => {
- await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
- const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
- const referendumIndex = startedEvent.referendumIndex;
-
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.democracy.emergencyCancelCall(referendumIndex),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] Council cannot cancel Fellowship referendums', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
- const fellowshipProposer = fellowship[5][0];
- const proposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- fellowshipProposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
-
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
-
- await expect(proposalFromAllCouncil(helper.fellowship.referenda.cancelCall(referendumIndex)))
- .to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Council member cannot cancel Fellowship referendums', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
- const fellowshipProposer = fellowship[5][0];
- const proposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- fellowshipProposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
- await expect(helper.council.collective.execute(
- counselors.alex,
- helper.fellowship.referenda.cancelCall(referendumIndex),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] Council referendum cannot be closed until the voting threshold is met', async ({helper}) => {
- const councilSize = (await helper.callRpc('api.query.councilMembership.members')).toJSON().length as any as number;
- expect(councilSize).is.greaterThan(1);
- const proposeResult = await helper.council.collective.propose(
- counselors.filip,
- dummyProposalCall(helper),
- councilSize,
- );
-
- const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
- const proposalIndex = councilProposedEvent.proposalIndex;
- const proposalHash = councilProposedEvent.proposalHash;
-
-
- await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
- await expect(helper.council.collective.close(counselors.filip, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly');
- });
-
-});
js-packages/tests/governance/democracy.test.tsdiffbeforeafterboth--- a/js-packages/tests/governance/democracy.test.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util/index.js';
-import {clearFellowship, democracyLaunchPeriod, democracyTrackMinRank, dummyProposalCall, fellowshipConfirmPeriod, fellowshipMinEnactPeriod, fellowshipPreparePeriod, fellowshipPropositionOrigin, initFellowship, voteUnanimouslyInFellowship} from './util.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
-
-describeGov('Governance: Democracy tests', () => {
- let regularUser: IKeyringPair;
- let donor: IKeyringPair;
- let sudoer: IKeyringPair;
-
- before(async function() {
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.Democracy]);
-
- donor = await privateKey({url: import.meta.url});
- sudoer = await privateKey('//Alice');
-
- [regularUser] = await helper.arrange.createAccounts([1000n], donor);
- });
- });
-
- itSub('Regular user can vote', async ({helper}) => {
- const fellows = await initFellowship(donor, sudoer);
- const rank1Proposer = fellows[1][0];
-
- const democracyProposalCall = dummyProposalCall(helper);
- const fellowshipProposal = {
- Inline: helper.democracy.proposeCall(democracyProposalCall, 0n).method.toHex(),
- };
-
- const submitResult = await helper.fellowship.referenda.submit(
- rank1Proposer,
- fellowshipPropositionOrigin,
- fellowshipProposal,
- {After: 0},
- );
-
- const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
- await voteUnanimouslyInFellowship(helper, fellows, democracyTrackMinRank, fellowshipReferendumIndex);
- await helper.fellowship.referenda.placeDecisionDeposit(donor, fellowshipReferendumIndex);
-
- await helper.wait.expectEvent(
- fellowshipPreparePeriod + fellowshipConfirmPeriod + fellowshipMinEnactPeriod,
- Event.Democracy.Proposed,
- );
-
- const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
- const referendumIndex = startedEvent.referendumIndex;
-
- const ayeBalance = 10_000n;
-
- await helper.democracy.vote(regularUser, referendumIndex, {
- Standard: {
- vote: {
- aye: true,
- conviction: 1,
- },
- balance: ayeBalance,
- },
- });
-
- const referendumInfo = await helper.democracy.referendumInfo(referendumIndex);
- const tally = referendumInfo.ongoing.tally;
-
- expect(BigInt(tally.ayes)).to.be.equal(ayeBalance);
-
- await clearFellowship(sudoer);
- });
-
- itSub('[Negative] Regular user cannot submit a regular proposal', async ({helper}) => {
- const submitResult = helper.democracy.propose(regularUser, dummyProposalCall(helper), 0n);
- await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Regular user cannot externally propose SuperMajorityAgainst', async ({helper}) => {
- const submitResult = helper.democracy.externalProposeDefault(regularUser, dummyProposalCall(helper));
- await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Regular user cannot externally propose SimpleMajority', async ({helper}) => {
- const submitResult = helper.democracy.externalProposeMajority(regularUser, dummyProposalCall(helper));
- await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Regular user cannot externally propose SuperMajorityApprove', async ({helper}) => {
- const submitResult = helper.democracy.externalPropose(regularUser, dummyProposalCall(helper));
- await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
- });
-});
js-packages/tests/governance/fellowship.test.tsdiffbeforeafterboth--- a/js-packages/tests/governance/fellowship.test.ts
+++ /dev/null
@@ -1,335 +0,0 @@
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util/index.js';
-import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';
-import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, voteUnanimouslyInFellowship, democracyTrackMinRank, fellowshipPreparePeriod, fellowshipConfirmPeriod, fellowshipMinEnactPeriod, democracyTrackId, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js';
-import type {ICounselors, ITechComms} from './util.js';
-
-describeGov('Governance: Fellowship tests', () => {
- let members: IKeyringPair[][];
-
- let rank1Proposer: IKeyringPair;
-
- let sudoer: any;
- let donor: any;
- let counselors: ICounselors;
- let techcomms: ITechComms;
-
- const submissionDeposit = 1000n;
-
- async function testBadFellowshipProposal(
- helper: DevUniqueHelper,
- proposalCall: any,
- ) {
- const badProposal = {
- Inline: proposalCall.method.toHex(),
- };
- const submitResult = await helper.fellowship.referenda.submit(
- rank1Proposer,
- fellowshipPropositionOrigin,
- badProposal,
- defaultEnactmentMoment,
- );
-
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
- await voteUnanimouslyInFellowship(helper, members, democracyTrackMinRank, referendumIndex);
- await helper.fellowship.referenda.placeDecisionDeposit(donor, referendumIndex);
-
- const enactmentId = await helper.fellowship.referenda.enactmentEventId(referendumIndex);
- const dispatchedEvent = await helper.wait.expectEvent(
- fellowshipPreparePeriod + fellowshipConfirmPeriod + defaultEnactmentMoment.After,
- Event.Scheduler.Dispatched,
- (event: any) => event.id == enactmentId,
- );
-
- expect(dispatchedEvent.result.isErr, 'Bad Fellowship must fail')
- .to.be.true;
-
- expect(dispatchedEvent.result.asErr.isBadOrigin, 'Bad Fellowship must fail with BadOrigin')
- .to.be.true;
- }
-
- before(async function() {
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.Democracy, Pallets.Fellowship, Pallets.TechnicalCommittee, Pallets.Council]);
-
- sudoer = await privateKey('//Alice');
- donor = await privateKey({url: import.meta.url});
- });
-
- counselors = await initCouncil(donor, sudoer);
- techcomms = await initTechComm(donor, sudoer);
- members = await initFellowship(donor, sudoer);
-
- rank1Proposer = members[1][0];
- });
-
- after(async () => {
- await clearFellowship(sudoer);
- await clearTechComm(sudoer);
- await clearCouncil(sudoer);
- await hardResetFellowshipReferenda(sudoer);
- await hardResetDemocracy(sudoer);
- await hardResetGovScheduler(sudoer);
- });
-
- itSub('FellowshipProposition can submit regular Democracy proposals', async ({helper}) => {
- const democracyProposalCall = dummyProposalCall(helper);
- const fellowshipProposal = {
- Inline: helper.democracy.proposeCall(democracyProposalCall, 0n).method.toHex(),
- };
-
- const submitResult = await helper.fellowship.referenda.submit(
- rank1Proposer,
- fellowshipPropositionOrigin,
- fellowshipProposal,
- defaultEnactmentMoment,
- );
-
- const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
- await voteUnanimouslyInFellowship(helper, members, democracyTrackMinRank, fellowshipReferendumIndex);
- await helper.fellowship.referenda.placeDecisionDeposit(donor, fellowshipReferendumIndex);
-
- const democracyProposed = await helper.wait.expectEvent(
- fellowshipPreparePeriod + fellowshipConfirmPeriod + fellowshipMinEnactPeriod,
- Event.Democracy.Proposed,
- );
-
- const democracyEnqueuedProposal = await helper.democracy.expectPublicProposal(democracyProposed.proposalIndex);
- expect(democracyEnqueuedProposal.inline, 'Fellowship proposal expected to be in the Democracy')
- .to.be.equal(democracyProposalCall.method.toHex());
-
- await helper.wait.newBlocks(democracyVotingPeriod);
- });
-
- itSub('Fellowship (rank-1 or greater) member can submit Fellowship proposals on the Democracy track', async ({helper}) => {
- for(let rank = 1; rank < fellowshipRankLimit; rank++) {
- const rankMembers = members[rank];
-
- for(let memberIdx = 0; memberIdx < rankMembers.length; memberIdx++) {
- const member = rankMembers[memberIdx];
- const newDummyProposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- member,
- fellowshipPropositionOrigin,
- newDummyProposal,
- defaultEnactmentMoment,
- );
-
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
- const referendumInfo = await helper.fellowship.referenda.referendumInfo(referendumIndex);
- expect(referendumInfo.ongoing.track, `${memberIdx}-th member of rank #${rank}: proposal #${referendumIndex} is on invalid track`)
- .to.be.equal(democracyTrackId);
- }
- }
- });
-
- itSub(`Fellowship (rank-${democracyTrackMinRank} or greater) members can vote on the Democracy track`, async ({helper}) => {
- const proposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- rank1Proposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
-
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
-
- let expectedAyes = 0;
- for(let rank = democracyTrackMinRank; rank < fellowshipRankLimit; rank++) {
- const rankMembers = members[rank];
-
- for(let memberIdx = 0; memberIdx < rankMembers.length; memberIdx++) {
- const member = rankMembers[memberIdx];
- await helper.fellowship.collective.vote(member, referendumIndex, true);
- expectedAyes += 1;
-
- const referendumInfo = await helper.fellowship.referenda.referendumInfo(referendumIndex);
- expect(referendumInfo.ongoing.tally.bareAyes, `Vote from ${memberIdx}-th member of rank #${rank} isn't accounted`)
- .to.be.equal(expectedAyes);
- }
- }
- });
-
- itSub('Fellowship rank vote strength is correct', async ({helper}) => {
- const excessRankWeightTable = [
- 1,
- 3,
- 6,
- 10,
- 15,
- 21,
- ];
-
- const proposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- rank1Proposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
-
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
-
- for(let rank = democracyTrackMinRank; rank < fellowshipRankLimit; rank++) {
- const rankMembers = members[rank];
-
- for(let memberIdx = 0; memberIdx < rankMembers.length; memberIdx++) {
- const member = rankMembers[memberIdx];
-
- const referendumInfoBefore = await helper.fellowship.referenda.referendumInfo(referendumIndex);
- const ayesBefore = referendumInfoBefore.ongoing.tally.ayes;
-
- await helper.fellowship.collective.vote(member, referendumIndex, true);
-
- const referendumInfoAfter = await helper.fellowship.referenda.referendumInfo(referendumIndex);
- const ayesAfter = referendumInfoAfter.ongoing.tally.ayes;
-
- const expectedVoteWeight = excessRankWeightTable[rank - democracyTrackMinRank];
- const voteWeight = ayesAfter - ayesBefore;
-
- expect(voteWeight, `Vote weight of ${memberIdx}-th member of rank #${rank} is invalid`)
- .to.be.equal(expectedVoteWeight);
- }
- }
- });
-
- itSub('[Negative] FellowshipProposition cannot externally propose SuperMajorityAgainst', async ({helper}) => {
- await testBadFellowshipProposal(helper, helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper)));
- });
-
- itSub('[Negative] FellowshipProposition cannot externally propose SimpleMajority', async ({helper}) => {
- await testBadFellowshipProposal(helper, helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper)));
- });
-
- itSub('[Negative] FellowshipProposition cannot externally propose SuperMajorityApprove', async ({helper}) => {
- await testBadFellowshipProposal(helper, helper.democracy.externalProposeCall(dummyProposalCall(helper)));
- });
-
- itSub('[Negative] Fellowship (rank-0) member cannot submit Fellowship proposals on the Democracy track', async ({helper}) => {
- const rank0Proposer = members[0][0];
-
- const proposal = dummyProposal(helper);
-
- const submitResult = helper.fellowship.referenda.submit(
- rank0Proposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
-
- await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
- });
-
- itSub('[Negative] Fellowship (rank-1 or greater) member cannot submit if no deposit can be provided', async ({helper}) => {
- const poorMember = rank1Proposer;
-
- const balanceBefore = await helper.balance.getSubstrate(poorMember.address);
- await helper.getSudo().balance.setBalanceSubstrate(sudoer, poorMember.address, submissionDeposit - 1n);
-
- const proposal = dummyProposal(helper);
-
- const submitResult = helper.fellowship.referenda.submit(
- poorMember,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
-
- await expect(submitResult).to.be.rejectedWith(/account balance too low/);
-
- await helper.getSudo().balance.setBalanceSubstrate(sudoer, poorMember.address, balanceBefore);
- });
-
- itSub(`[Negative] Fellowship (rank-${democracyTrackMinRank - 1} or less) members cannot vote on the Democracy track`, async ({helper}) => {
- const proposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- rank1Proposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
-
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
-
- for(let rank = 0; rank < democracyTrackMinRank; rank++) {
- for(const member of members[rank]) {
- const voteResult = helper.fellowship.collective.vote(member, referendumIndex, true);
- await expect(voteResult).to.be.rejectedWith(/RankTooLow/);
- }
- }
- });
-
- itSub('[Negative] FellowshipProposition cannot add/remove a Council member', async ({helper}) => {
- const [councilNonMember] = await helper.arrange.createAccounts([10n], donor);
-
- await testBadFellowshipProposal(helper, helper.council.membership.addMemberCall(councilNonMember.address));
- await testBadFellowshipProposal(helper, helper.council.membership.removeMemberCall(counselors.ildar.address));
- });
-
- itSub('[Negative] FellowshipProposition cannot set/clear Council prime member', async ({helper}) => {
- await testBadFellowshipProposal(helper, helper.council.membership.setPrimeCall(counselors.ildar.address));
- await testBadFellowshipProposal(helper, helper.council.membership.clearPrimeCall());
- });
-
- itSub('[Negative] FellowshipProposition cannot add/remove a TechComm member', async ({helper}) => {
- const [techCommNonMember] = await helper.arrange.createAccounts([10n], donor);
-
- await testBadFellowshipProposal(helper, helper.technicalCommittee.membership.addMemberCall(techCommNonMember.address));
- await testBadFellowshipProposal(helper, helper.technicalCommittee.membership.removeMemberCall(techcomms.constantine.address));
- });
-
- itSub('[Negative] FellowshipProposition cannot add/remove a Fellowship member', async ({helper}) => {
- const [fellowshipNonMember] = await helper.arrange.createAccounts([10n], donor);
-
- await testBadFellowshipProposal(helper, helper.fellowship.collective.addMemberCall(fellowshipNonMember.address));
- await testBadFellowshipProposal(helper, helper.fellowship.collective.removeMemberCall(rank1Proposer.address, 1));
- });
-
- itSub('[Negative] FellowshipProposition cannot promote/demote a Fellowship member', async ({helper}) => {
- await testBadFellowshipProposal(helper, helper.fellowship.collective.promoteCall(rank1Proposer.address));
- await testBadFellowshipProposal(helper, helper.fellowship.collective.demoteCall(rank1Proposer.address));
- });
-
- itSub('[Negative] FellowshipProposition cannot fast-track Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
-
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await testBadFellowshipProposal(helper, helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0));
- });
-
- itSub('[Negative] FellowshipProposition cannot cancel Democracy proposals', async ({helper}) => {
- const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
- const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
-
- await testBadFellowshipProposal(helper, helper.democracy.cancelProposalCall(proposalIndex));
- });
-
- itSub('[Negative] FellowshipProposition cannot cancel ongoing Democracy referendums', async ({helper}) => {
- await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
- const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
- const referendumIndex = startedEvent.referendumIndex;
-
- await testBadFellowshipProposal(helper, helper.democracy.emergencyCancelCall(referendumIndex));
- });
-
- itSub('[Negative] FellowshipProposition cannot blacklist Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
-
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await testBadFellowshipProposal(helper, helper.democracy.blacklistCall(preimageHash, null));
- });
-
- itSub('[Negative] FellowshipProposition cannot veto external proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
-
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await testBadFellowshipProposal(helper, helper.democracy.vetoExternalCall(preimageHash));
- });
-});
js-packages/tests/governance/init.test.tsdiffbeforeafterboth--- a/js-packages/tests/governance/init.test.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
-import {democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, clearCouncil, clearTechComm, clearFellowship} from './util.js';
-import type {ICounselors, ITechComms} from './util.js';
-
-describeGov('Governance: Initialization', () => {
- let donor: IKeyringPair;
- let sudoer: IKeyringPair;
- let counselors: ICounselors;
- let techcomms: ITechComms;
- let coreDevs: any;
-
- const expectedAlexFellowRank = 7;
- const expectedFellowRank = 6;
-
- before(async function() {
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.Democracy, Pallets.Council, Pallets.TechnicalCommittee]);
-
- const councilMembers = await helper.council.membership.getMembers();
- const techcommMembers = await helper.technicalCommittee.membership.getMembers();
- expect(councilMembers.length == 0, 'The Council must be empty before the Gov Init');
- expect(techcommMembers.length == 0, 'The Technical Commettee must be empty before the Gov Init');
-
- donor = await privateKey({url: import.meta.url});
- sudoer = await privateKey('//Alice');
-
- const counselorsNum = 5;
- const techCommsNum = 3;
- const coreDevsNum = 2;
- const [
- alex,
- ildar,
- charu,
- filip,
- irina,
-
- greg,
- andy,
- constantine,
-
- yaroslav,
- daniel,
- ] = await helper.arrange.createAccounts(new Array(counselorsNum + techCommsNum + coreDevsNum).fill(10_000n), donor);
-
- counselors = {
- alex,
- ildar,
- charu,
- filip,
- irina,
- };
-
- techcomms = {
- greg,
- andy,
- constantine,
- };
-
- coreDevs = {
- yaroslav: yaroslav,
- daniel: daniel,
- };
- });
- });
-
- itSub('Initialize Governance', async ({helper}) => {
- const promoteFellow = (fellow: string, promotionsNum: number) => new Array(promotionsNum).fill(helper.fellowship.collective.promoteCall(fellow));
-
- const expectFellowRank = async (fellow: string, expectedRank: number) => {
- expect(await helper.fellowship.collective.getMemberRank(fellow)).to.be.equal(expectedRank);
- };
-
- console.log('\t- Setup the Prime of the Council via sudo');
- await helper.getSudo().utility.batchAll(sudoer, [
- helper.council.membership.addMemberCall(counselors.alex.address),
- helper.council.membership.setPrimeCall(counselors.alex.address),
-
- helper.fellowship.collective.addMemberCall(counselors.alex.address),
- ...promoteFellow(counselors.alex.address, expectedAlexFellowRank),
- ]);
-
- let councilMembers = await helper.council.membership.getMembers();
- const councilPrime = await helper.council.collective.getPrimeMember();
- const alexFellowRank = await helper.fellowship.collective.getMemberRank(counselors.alex.address);
- expect(councilMembers).to.be.deep.equal([counselors.alex.address]);
- expect(councilPrime).to.be.equal(counselors.alex.address);
- expect(alexFellowRank).to.be.equal(expectedAlexFellowRank);
-
- console.log('\t- The Council Prime initializes the Technical Commettee');
- const councilProposalThreshold = 1;
-
- await helper.council.collective.propose(
- counselors.alex,
- helper.utility.batchAllCall([
- helper.technicalCommittee.membership.addMemberCall(techcomms.greg.address),
- helper.technicalCommittee.membership.addMemberCall(techcomms.andy.address),
- helper.technicalCommittee.membership.addMemberCall(techcomms.constantine.address),
-
- helper.technicalCommittee.membership.setPrimeCall(techcomms.greg.address),
- ]),
- councilProposalThreshold,
- );
-
- const techCommMembers = await helper.technicalCommittee.membership.getMembers();
- const techCommPrime = await helper.technicalCommittee.membership.getPrimeMember();
- const expectedTechComms = [techcomms.greg.address, techcomms.andy.address, techcomms.constantine.address];
- expect(techCommMembers.length).to.be.equal(expectedTechComms.length);
- expect(techCommMembers).to.containSubset(expectedTechComms);
- expect(techCommPrime).to.be.equal(techcomms.greg.address);
-
- console.log('\t- The Council Prime initiates a referendum to add counselors');
- const returnPreimageHash = true;
- const preimageHash = await helper.preimage.notePreimageFromCall(counselors.alex, helper.utility.batchAllCall([
- helper.council.membership.addMemberCall(counselors.ildar.address),
- helper.council.membership.addMemberCall(counselors.charu.address),
- helper.council.membership.addMemberCall(counselors.filip.address),
- helper.council.membership.addMemberCall(counselors.irina.address),
-
- helper.fellowship.collective.addMemberCall(counselors.charu.address),
- helper.fellowship.collective.addMemberCall(counselors.ildar.address),
- helper.fellowship.collective.addMemberCall(counselors.irina.address),
- helper.fellowship.collective.addMemberCall(counselors.filip.address),
- helper.fellowship.collective.addMemberCall(techcomms.greg.address),
- helper.fellowship.collective.addMemberCall(techcomms.andy.address),
- helper.fellowship.collective.addMemberCall(techcomms.constantine.address),
- helper.fellowship.collective.addMemberCall(coreDevs.yaroslav.address),
- helper.fellowship.collective.addMemberCall(coreDevs.daniel.address),
-
- ...promoteFellow(counselors.charu.address, expectedFellowRank),
- ...promoteFellow(counselors.ildar.address, expectedFellowRank),
- ...promoteFellow(counselors.irina.address, expectedFellowRank),
- ...promoteFellow(counselors.filip.address, expectedFellowRank),
- ...promoteFellow(techcomms.greg.address, expectedFellowRank),
- ...promoteFellow(techcomms.andy.address, expectedFellowRank),
- ...promoteFellow(techcomms.constantine.address, expectedFellowRank),
- ...promoteFellow(coreDevs.yaroslav.address, expectedFellowRank),
- ...promoteFellow(coreDevs.daniel.address, expectedFellowRank),
- ]), returnPreimageHash);
-
- await helper.council.collective.propose(
- counselors.alex,
- helper.democracy.externalProposeDefaultWithPreimageCall(preimageHash),
- councilProposalThreshold,
- );
-
- console.log('\t- The referendum is being decided');
- const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
-
- await helper.democracy.vote(counselors.filip, startedEvent.referendumIndex, {
- Standard: {
- vote: {
- aye: true,
- conviction: 1,
- },
- balance: 10_000n,
- },
- });
-
- const passedReferendumEvent = await helper.wait.expectEvent(democracyVotingPeriod, Event.Democracy.Passed);
- expect(passedReferendumEvent.referendumIndex).to.be.equal(startedEvent.referendumIndex);
-
- await helper.wait.expectEvent(democracyEnactmentPeriod, Event.Scheduler.Dispatched);
-
- councilMembers = await helper.council.membership.getMembers();
- const expectedCounselors = [
- counselors.alex.address,
- counselors.ildar.address,
- counselors.charu.address,
- counselors.filip.address,
- counselors.irina.address,
- ];
- expect(councilMembers.length).to.be.equal(expectedCounselors.length);
- expect(councilMembers).to.containSubset(expectedCounselors);
-
- await expectFellowRank(counselors.ildar.address, expectedFellowRank);
- await expectFellowRank(counselors.charu.address, expectedFellowRank);
- await expectFellowRank(counselors.filip.address, expectedFellowRank);
- await expectFellowRank(counselors.irina.address, expectedFellowRank);
- await expectFellowRank(techcomms.greg.address, expectedFellowRank);
- await expectFellowRank(techcomms.andy.address, expectedFellowRank);
- await expectFellowRank(techcomms.constantine.address, expectedFellowRank);
- await expectFellowRank(coreDevs.yaroslav.address, expectedFellowRank);
- await expectFellowRank(coreDevs.daniel.address, expectedFellowRank);
- });
-
- after(async function() {
- await clearFellowship(sudoer);
- await clearTechComm(sudoer);
- await clearCouncil(sudoer);
- });
-});
js-packages/tests/governance/technicalCommittee.test.tsdiffbeforeafterboth--- a/js-packages/tests/governance/technicalCommittee.test.ts
+++ /dev/null
@@ -1,383 +0,0 @@
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
-import {initCouncil, democracyLaunchPeriod, democracyFastTrackVotingPeriod, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js';
-import type {ITechComms} from './util.js';
-
-describeGov('Governance: Technical Committee tests', () => {
- let sudoer: IKeyringPair;
- let techcomms: ITechComms;
- let donor: IKeyringPair;
- let preImageHash: string;
-
-
- const allTechCommitteeThreshold = 3;
-
- before(async function() {
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.TechnicalCommittee]);
- sudoer = await privateKey('//Alice');
- donor = await privateKey({url: import.meta.url});
-
- techcomms = await initTechComm(donor, sudoer);
-
- const proposalCall = await helper.constructApiCall('api.tx.balances.forceSetBalance', [donor.address, 20n * 10n ** 25n]);
- preImageHash = await helper.preimage.notePreimageFromCall(sudoer, proposalCall, true);
- });
- });
-
- after(async () => {
- await usingPlaygrounds(async (helper) => {
- await clearTechComm(sudoer);
-
- await helper.preimage.unnotePreimage(sudoer, preImageHash);
- await hardResetFellowshipReferenda(sudoer);
- await hardResetDemocracy(sudoer);
- await hardResetGovScheduler(sudoer);
- });
- });
-
- function proposalFromAllCommittee(proposal: any) {
- return usingPlaygrounds(async (helper) => {
- expect((await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length).to.be.equal(allTechCommitteeThreshold);
- const proposeResult = await helper.technicalCommittee.collective.propose(
- techcomms.andy,
- proposal,
- allTechCommitteeThreshold,
- );
-
- const commiteeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult);
- const proposalIndex = commiteeProposedEvent.proposalIndex;
- const proposalHash = commiteeProposedEvent.proposalHash;
-
-
- await helper.technicalCommittee.collective.vote(techcomms.andy, proposalHash, proposalIndex, true);
- await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true);
- await helper.technicalCommittee.collective.vote(techcomms.greg, proposalHash, proposalIndex, true);
-
- const closeResult = await helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex);
- Event.TechnicalCommittee.Closed.expect(closeResult);
- Event.TechnicalCommittee.Approved.expect(closeResult);
- const {result} = Event.TechnicalCommittee.Executed.expect(closeResult);
- expect(result).to.eq('Ok');
-
- return closeResult;
- });
- }
-
- itSub('TechComm can fast-track Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await helper.wait.parachainBlockMultiplesOf(35n);
-
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- const fastTrackProposal = await proposalFromAllCommittee(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0));
- Event.Democracy.Started.expect(fastTrackProposal);
- });
-
- itSub('TechComm can cancel Democracy proposals', async ({helper}) => {
- const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
- const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
-
- const cancelProposal = await proposalFromAllCommittee(helper.democracy.cancelProposalCall(proposalIndex));
- Event.Democracy.ProposalCanceled.expect(cancelProposal);
- });
-
- itSub('TechComm can cancel ongoing Democracy referendums', async ({helper}) => {
- await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
- const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
- const referendumIndex = startedEvent.referendumIndex;
-
- const emergencyCancelProposal = await proposalFromAllCommittee(helper.democracy.emergencyCancelCall(referendumIndex));
- Event.Democracy.Cancelled.expect(emergencyCancelProposal);
- });
-
- itSub('TechComm member can veto Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- const vetoExternalCall = await helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.democracy.vetoExternalCall(preimageHash),
- );
- Event.Democracy.Vetoed.expect(vetoExternalCall);
- });
-
- itSub('TechComm can cancel Fellowship referendums', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
- const fellowshipProposer = fellowship[5][0];
- const proposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- fellowshipProposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
- const cancelProposal = await proposalFromAllCommittee(helper.fellowship.referenda.cancelCall(referendumIndex));
- Event.FellowshipReferenda.Cancelled.expect(cancelProposal);
- });
-
- itSub.skip('TechComm member can add a Fellowship member', async ({helper}) => {
- const newFellowshipMember = helper.arrange.createEmptyAccount();
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.fellowship.collective.addMemberCall(newFellowshipMember.address),
- )).to.be.fulfilled;
- const fellowshipMembers = (await helper.callRpc('api.query.fellowshipCollective.members')).toJSON();
- expect(fellowshipMembers).to.contains(newFellowshipMember.address);
- await clearFellowship(sudoer);
- });
-
- itSub('[Negative] TechComm cannot submit regular democracy proposal', async ({helper}) => {
- const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
-
- await expect(proposalFromAllCommittee(councilProposal)).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm cannot externally propose SuperMajorityAgainst', async ({helper}) => {
- const commiteeProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
-
- await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm cannot externally propose SimpleMajority', async ({helper}) => {
- const commiteeProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
-
- await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm cannot externally propose SuperMajorityApprove', async ({helper}) => {
- const commiteeProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
-
- await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot submit regular democracy proposal', async ({helper}) => {
- const memberProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- memberProposal,
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot externally propose SuperMajorityAgainst', async ({helper}) => {
- const memberProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- memberProposal,
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot externally propose SimpleMajority', async ({helper}) => {
- const memberProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- memberProposal,
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot externally propose SuperMajorityApprove', async ({helper}) => {
- const memberProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- memberProposal,
- )).to.be.rejectedWith('BadOrigin');
- });
-
-
- itSub.skip('[Negative] TechComm cannot promote/demote Fellowship member', async () => {
-
- });
-
- itSub.skip('[Negative] TechComm member cannot promote/demote Fellowship member', async () => {
-
- });
-
- itSub('[Negative] TechComm cannot add/remove a Council member', async ({helper}) => {
- const newCouncilMember = helper.arrange.createEmptyAccount();
- const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
- const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
-
- await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin');
- await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot add/remove a Council member', async ({helper}) => {
- const newCouncilMember = helper.arrange.createEmptyAccount();
- const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
- const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- addMemberProposal,
- )).to.be.rejectedWith('BadOrigin');
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- removeMemberProposal,
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm cannot set/clear Council prime member', async ({helper}) => {
- const counselors = await initCouncil(donor, sudoer);
- const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
- const proposalForClear = await helper.council.membership.clearPrimeCall();
-
- await expect(proposalFromAllCommittee(proposalForSet)).to.be.rejectedWith('BadOrigin');
- await expect(proposalFromAllCommittee(proposalForClear)).to.be.rejectedWith('BadOrigin');
- await clearCouncil(sudoer);
- });
-
- itSub('[Negative] TechComm member cannot set/clear Council prime member', async ({helper}) => {
- const counselors = await initCouncil(donor, sudoer);
- const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
- const proposalForClear = await helper.council.membership.clearPrimeCall();
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- proposalForSet,
- )).to.be.rejectedWith('BadOrigin');
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- proposalForClear,
- )).to.be.rejectedWith('BadOrigin');
- await clearCouncil(sudoer);
- });
-
- itSub('[Negative] TechComm cannot add/remove a TechComm member', async ({helper}) => {
- const newCommMember = helper.arrange.createEmptyAccount();
- const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
- const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
-
- await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin');
- await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot add/remove a TechComm member', async ({helper}) => {
- const newCommMember = helper.arrange.createEmptyAccount();
- const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
- const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- addMemberProposal,
- )).to.be.rejectedWith('BadOrigin');
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- removeMemberProposal,
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm cannot remove a Fellowship member', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
-
- await expect(proposalFromAllCommittee(helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5))).to.be.rejectedWith('BadOrigin');
- await clearFellowship(sudoer);
- });
-
- itSub('[Negative] TechComm member cannot remove a Fellowship member', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5),
- )).to.be.rejectedWith('BadOrigin');
- await clearFellowship(sudoer);
- });
-
- itSub('[Negative] TechComm member cannot fast-track Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot cancel Democracy proposals', async ({helper}) => {
- const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
- const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.democracy.cancelProposalCall(proposalIndex),
- ))
- .to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot cancel ongoing Democracy referendums', async ({helper}) => {
- await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
- const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
- const referendumIndex = startedEvent.referendumIndex;
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.democracy.emergencyCancelCall(referendumIndex),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm cannot blacklist Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await expect(proposalFromAllCommittee(helper.democracy.blacklistCall(preimageHash))).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm member cannot blacklist Democracy proposals', async ({helper}) => {
- const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
- await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.democracy.blacklistCall(preimageHash),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub.skip('[Negative] TechComm member cannot veto external Democracy proposals until the cool-off period pass', async () => {
-
- });
-
- itSub('[Negative] TechComm member cannot cancel Fellowship referendums', async ({helper}) => {
- const fellowship = await initFellowship(donor, sudoer);
- const fellowshipProposer = fellowship[5][0];
- const proposal = dummyProposal(helper);
-
- const submitResult = await helper.fellowship.referenda.submit(
- fellowshipProposer,
- fellowshipPropositionOrigin,
- proposal,
- defaultEnactmentMoment,
- );
-
- const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
-
- await expect(helper.technicalCommittee.collective.execute(
- techcomms.andy,
- helper.fellowship.referenda.cancelCall(referendumIndex),
- )).to.be.rejectedWith('BadOrigin');
- });
-
- itSub('[Negative] TechComm referendum cannot be closed until the voting threshold is met', async ({helper}) => {
- const committeeSize = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length as any as number;
- expect(committeeSize).is.greaterThan(1);
- const proposeResult = await helper.technicalCommittee.collective.propose(
- techcomms.andy,
- dummyProposalCall(helper),
- committeeSize,
- );
-
- const committeeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult);
- const proposalIndex = committeeProposedEvent.proposalIndex;
- const proposalHash = committeeProposedEvent.proposalHash;
-
- await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true);
-
- await expect(helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly');
- });
-});
js-packages/tests/governance/util.tsdiffbeforeafterboth--- a/js-packages/tests/governance/util.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-import type {IKeyringPair} from '@polkadot/types/types';
-import {xxhashAsHex} from '@polkadot/util-crypto';
-import type {u32} from '@polkadot/types-codec';
-import {usingPlaygrounds, expect} from '../util/index.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
-import {DevUniqueHelper} from '@unique/playgrounds/unique.dev.js';
-
-export const democracyLaunchPeriod = 35;
-export const democracyVotingPeriod = 35;
-export const councilMotionDuration = 35;
-export const democracyEnactmentPeriod = 40;
-export const democracyFastTrackVotingPeriod = 5;
-
-export const fellowshipRankLimit = 7;
-export const fellowshipPropositionOrigin = 'FellowshipProposition';
-export const fellowshipPreparePeriod = 3;
-export const fellowshipConfirmPeriod = 3;
-export const fellowshipMinEnactPeriod = 1;
-
-export const defaultEnactmentMoment = {After: 0};
-
-export const democracyTrackId = 10;
-export const democracyTrackMinRank = 3;
-const twox128 = (data: any) => xxhashAsHex(data, 128);
-export interface ICounselors {
- alex: IKeyringPair;
- ildar: IKeyringPair;
- charu: IKeyringPair;
- filip: IKeyringPair;
- irina: IKeyringPair;
-}
-export interface ITechComms {
- greg: IKeyringPair;
- andy: IKeyringPair;
- constantine: IKeyringPair;
-}
-
-export async function initCouncil(donor: IKeyringPair, superuser: IKeyringPair) {
- let counselors: IKeyringPair[] = [];
-
- await usingPlaygrounds(async (helper) => {
- const [alex, ildar, charu, filip, irina] = await helper.arrange.createAccounts([10_000n, 10_000n, 10_000n, 10_000n, 10_000n], donor);
- const sudo = helper.getSudo();
- {
- const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON() as [];
- if(members.length != 0) {
- await clearCouncil(superuser);
- }
- }
- const expectedMembers = [alex, ildar, charu, filip, irina];
- for(const member of expectedMembers) {
- await sudo.executeExtrinsic(superuser, 'api.tx.councilMembership.addMember', [member.address]);
- }
- await sudo.executeExtrinsic(superuser, 'api.tx.councilMembership.setPrime', [alex.address]);
- {
- const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
- expect(members).to.containSubset(expectedMembers.map((x: IKeyringPair) => x.address));
- expect(members.length).to.be.equal(expectedMembers.length);
- }
-
- counselors = [alex, ildar, charu, filip, irina];
- });
- return {
- alex: counselors[0],
- ildar: counselors[1],
- charu: counselors[2],
- filip: counselors[3],
- irina: counselors[4],
- };
-}
-
-export async function clearCouncil(superuser: IKeyringPair) {
- await usingPlaygrounds(async (helper) => {
- let members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
- if(members.length) {
- const sudo = helper.getSudo();
- for(const address of members) {
- await sudo.executeExtrinsic(superuser, 'api.tx.councilMembership.removeMember', [address]);
- }
- members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
- }
- expect(members).to.be.deep.equal([]);
- });
-}
-
-
-export async function initTechComm(donor: IKeyringPair, superuser: IKeyringPair) {
- let techcomms: IKeyringPair[] = [];
-
- await usingPlaygrounds(async (helper) => {
- const [greg, andy, constantine] = await helper.arrange.createAccounts([10_000n, 10_000n, 10_000n], donor);
- const sudo = helper.getSudo();
- {
- const members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON() as [];
- if(members.length != 0) {
- await clearTechComm(superuser);
- }
- }
- await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.addMember', [greg.address]);
- await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.addMember', [andy.address]);
- await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.addMember', [constantine.address]);
- await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.setPrime', [greg.address]);
- {
- const members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
- expect(members).to.containSubset([greg.address, andy.address, constantine.address]);
- expect(members.length).to.be.equal(3);
- }
-
- techcomms = [greg, andy, constantine];
- });
-
- return {
- greg: techcomms[0],
- andy: techcomms[1],
- constantine: techcomms[2],
- };
-}
-
-export async function clearTechComm(superuser: IKeyringPair) {
- await usingPlaygrounds(async (helper) => {
- let members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
- if(members.length) {
- const sudo = helper.getSudo();
- for(const address of members) {
- await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.removeMember', [address]);
- }
- members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
- }
- expect(members).to.be.deep.equal([]);
- });
-}
-
-export async function initFellowship(donor: IKeyringPair, sudoer: IKeyringPair) {
- const numMembersInRank = 3;
- const memberBalance = 5000n;
- const members: IKeyringPair[][] = [];
-
- await usingPlaygrounds(async (helper) => {
- const currentFellows = await helper.getApi().query.fellowshipCollective.members.keys();
-
- if(currentFellows.length != 0) {
- await clearFellowship(sudoer);
- }
- for(let i = 0; i < fellowshipRankLimit; i++) {
- const rankMembers = await helper.arrange.createAccounts(
- Array(numMembersInRank).fill(memberBalance),
- donor,
- );
-
- for(const member of rankMembers) {
- await helper.getSudo().fellowship.collective.addMember(sudoer, member.address);
-
- for(let rank = 0; rank < i; rank++) {
- await helper.getSudo().fellowship.collective.promote(sudoer, member.address);
- }
- }
-
- members.push(rankMembers);
- }
- });
-
- return members;
-}
-
-export async function clearFellowship(sudoer: IKeyringPair) {
- await usingPlaygrounds(async (helper) => {
- const fellowship = (await helper.getApi().query.fellowshipCollective.members.keys())
- .map((key) => key.args[0].toString());
- for(const member of fellowship) {
- await helper.getSudo().fellowship.collective.removeMember(sudoer, member, fellowshipRankLimit);
- }
- });
-}
-
-export async function clearFellowshipReferenda(sudoer: IKeyringPair) {
- await usingPlaygrounds(async (helper) => {
- const proposalsCount = (await helper.getApi().query.fellowshipReferenda.referendumCount()) as u32;
- for(let i = 0; i < proposalsCount.toNumber(); i++) {
- await helper.getSudo().fellowship.referenda.cancel(sudoer, i);
- }
- });
-}
-
-export async function hardResetFellowshipReferenda(sudoer: IKeyringPair) {
- await usingPlaygrounds(async (helper) => {
- const api = helper.getApi();
- const prefix = twox128('FellowshipReferenda');
- await helper.signTransaction(sudoer, api.tx.sudo.sudo(api.tx.system.killPrefix(prefix, 100)));
- });
-}
-
-export async function hardResetDemocracy(sudoer: IKeyringPair) {
- await usingPlaygrounds(async (helper) => {
- const api = helper.getApi();
- const prefix = twox128('Democracy');
- await helper.signTransaction(sudoer, api.tx.sudo.sudo(api.tx.system.killPrefix(prefix, 100)));
- });
-}
-
-export async function hardResetGovScheduler(sudoer: IKeyringPair) {
- await usingPlaygrounds(async (helper) => {
- const api = helper.getApi();
- const prefix = twox128('GovScheduler');
- await helper.signTransaction(sudoer, api.tx.sudo.sudo(api.tx.system.killPrefix(prefix, 500)));
- });
-}
-
-export async function voteUnanimouslyInFellowship(helper: DevUniqueHelper, fellows: IKeyringPair[][], minRank: number, referendumIndex: number) {
- for(let rank = minRank; rank < fellowshipRankLimit; rank++) {
- for(const member of fellows[rank]) {
- await helper.fellowship.collective.vote(member, referendumIndex, true);
- }
- }
-}
-
-export function dummyProposalCall(helper: UniqueHelper) {
- return helper.constructApiCall('api.tx.system.remark', ['dummy proposal' + (new Date()).getTime()]);
-}
-
-export function dummyProposal(helper: UniqueHelper) {
- return {
- Inline: dummyProposalCall(helper).method.toHex(),
- };
-}
js-packages/tests/nesting/collectionProperties.seqtest.tsdiffbeforeafterboth--- a/js-packages/tests/nesting/collectionProperties.seqtest.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../util/index.js';
-
-describe('Integration Test: Collection Properties with sudo', () => {
- let superuser: IKeyringPair;
- let alice: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- superuser = await privateKey('//Alice');
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([100n], donor);
- });
- });
-
- [
- {mode: 'nft' as const, requiredPallets: []},
- {mode: 'ft' as const, requiredPallets: []},
- {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
- ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {
- before(async function() {
- // eslint-disable-next-line require-await
- await usingPlaygrounds(async helper => {
- requirePalletsOrSkip(this, helper, testSuite.requiredPallets);
- });
- });
-
- itSub('Repairing an unbroken collection\'s properties preserves the consumed space', async({helper}) => {
- const properties = [
- {key: 'sea-creatures', value: 'mermaids'},
- {key: 'goldenratio', value: '1.6180339887498948482045868343656381177203091798057628621354486227052604628189'},
- ];
- const collection = await helper[testSuite.mode].mintCollection(alice, {properties});
-
- const newProperty = {key: 'space', value: ' '.repeat(4096)};
- await collection.setProperties(alice, [newProperty]);
- const originalSpace = await collection.getPropertiesConsumedSpace();
- expect(originalSpace).to.be.equal(sizeOfProperty(properties[0]) + sizeOfProperty(properties[1]) + sizeOfProperty(newProperty));
-
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.unique.forceRepairCollection', [collection.collectionId], true);
- const recomputedSpace = await collection.getPropertiesConsumedSpace();
- expect(recomputedSpace).to.be.equal(originalSpace);
- });
- }));
-});
js-packages/tests/nesting/collectionProperties.test.tsdiffbeforeafterboth--- a/js-packages/tests/nesting/collectionProperties.test.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../util/index.js';
-
-describe('Integration Test: Collection Properties', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob] = await helper.arrange.createAccounts([200n, 10n], donor);
- });
- });
-
- itSub('Properties are initially empty', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice);
- expect(await collection.getProperties()).to.be.empty;
- });
-
- [
- {mode: 'nft' as const, requiredPallets: []},
- {mode: 'ft' as const, requiredPallets: []},
- {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
- ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {
- before(async function() {
- // eslint-disable-next-line require-await
- await usingPlaygrounds(async helper => {
- requirePalletsOrSkip(this, helper, testSuite.requiredPallets);
- });
- });
-
- itSub('Sets properties for a collection', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- // As owner
- await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}])).to.be.fulfilled;
-
- await collection.addAdmin(alice, {Substrate: bob.address});
-
- // As administrator
- await expect(collection.setProperties(bob, [{key: 'black_hole'}])).to.be.fulfilled;
-
- const properties = await collection.getProperties();
- expect(properties).to.include.deep.members([
- {key: 'electron', value: 'come bond'},
- {key: 'black_hole', value: ''},
- ]);
- });
-
- itSub('Check valid names for collection properties keys', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- // alpha symbols
- await expect(collection.setProperties(alice, [{key: 'answer'}])).to.be.fulfilled;
-
- // numeric symbols
- await expect(collection.setProperties(alice, [{key: '451'}])).to.be.fulfilled;
-
- // underscore symbol
- await expect(collection.setProperties(alice, [{key: 'black_hole'}])).to.be.fulfilled;
-
- // dash symbol
- await expect(collection.setProperties(alice, [{key: '-'}])).to.be.fulfilled;
-
- // dot symbol
- await expect(collection.setProperties(alice, [{key: 'once.in.a.long.long.while...', value: 'you get a little lost'}])).to.be.fulfilled;
-
- const properties = await collection.getProperties();
- expect(properties).to.include.deep.members([
- {key: 'answer', value: ''},
- {key: '451', value: ''},
- {key: 'black_hole', value: ''},
- {key: '-', value: ''},
- {key: 'once.in.a.long.long.while...', value: 'you get a little lost'},
- ]);
- });
-
- itSub('Changes properties of a collection', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: ''}])).to.be.fulfilled;
-
- // Mutate the properties
- await expect(collection.setProperties(alice, [{key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;
-
- const properties = await collection.getProperties();
- expect(properties).to.include.deep.members([
- {key: 'electron', value: 'come bond'},
- {key: 'black_hole', value: 'LIGO'},
- ]);
- });
-
- itSub('Deletes properties of a collection', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;
-
- await expect(collection.deleteProperties(alice, ['electron'])).to.be.fulfilled;
-
- const properties = await collection.getProperties(['black_hole', 'electron']);
- expect(properties).to.be.deep.equal([
- {key: 'black_hole', value: 'LIGO'},
- ]);
- });
-
- itSub('Allows modifying a collection property multiple times with the same size', async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- const maxCollectionPropertiesSize = 40960;
-
- const propDataSize = 4096;
-
- let propDataChar = 'a';
- const makeNewPropData = () => {
- propDataChar = String.fromCharCode(propDataChar.charCodeAt(0) + 1);
- return `${propDataChar}`.repeat(propDataSize);
- };
-
- const property = {key: propKey, value: makeNewPropData()};
- await collection.setProperties(alice, [property]);
- const originalSpace = await collection.getPropertiesConsumedSpace();
- expect(originalSpace).to.be.equal(sizeOfProperty(property));
-
- const sameSizePropertiesPossibleNum = maxCollectionPropertiesSize / propDataSize;
-
- // It is possible to modify a property as many times as needed.
- // It will not consume any additional space.
- for(let i = 0; i < sameSizePropertiesPossibleNum + 1; i++) {
- await collection.setProperties(alice, [{key: propKey, value: makeNewPropData()}]);
- const consumedSpace = await collection.getPropertiesConsumedSpace();
- expect(consumedSpace).to.be.equal(originalSpace);
- }
- });
-
- itSub('Adding then removing a collection property doesn\'t change the consumed space', async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testSuite.mode].mintCollection(alice);
- const originalSpace = await collection.getPropertiesConsumedSpace();
-
- const propDataSize = 4096;
- const propData = 'a'.repeat(propDataSize);
-
- const property = {key: propKey, value: propData};
- await collection.setProperties(alice, [property]);
- let consumedSpace = await collection.getPropertiesConsumedSpace();
- expect(consumedSpace).to.be.equal(sizeOfProperty(property));
-
- await collection.deleteProperties(alice, [propKey]);
- consumedSpace = await collection.getPropertiesConsumedSpace();
- expect(consumedSpace).to.be.equal(originalSpace);
- });
-
- itSub('Modifying a collection property with different sizes correctly changes the consumed space', async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testSuite.mode].mintCollection(alice);
- const originalSpace = await collection.getPropertiesConsumedSpace();
-
- const initProp = {key: propKey, value: 'a'.repeat(4096)};
- const biggerProp = {key: propKey, value: 'b'.repeat(5000)};
- const smallerProp = {key: propKey, value: 'c'.repeat(4000)};
-
- let consumedSpace;
- let expectedConsumedSpaceDiff;
-
- await collection.setProperties(alice, [initProp]);
- consumedSpace = await collection.getPropertiesConsumedSpace();
- expectedConsumedSpaceDiff = sizeOfProperty(initProp) - originalSpace;
- expect(consumedSpace).to.be.equal(originalSpace + expectedConsumedSpaceDiff);
-
- await collection.setProperties(alice, [biggerProp]);
- consumedSpace = await collection.getPropertiesConsumedSpace();
- expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(initProp);
- expect(consumedSpace).to.be.equal(sizeOfProperty(initProp) + expectedConsumedSpaceDiff);
-
- await collection.setProperties(alice, [smallerProp]);
- consumedSpace = await collection.getPropertiesConsumedSpace();
- expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(smallerProp);
- expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);
- });
- }));
-});
-
-describe('Negative Integration Test: Collection Properties', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob] = await helper.arrange.createAccounts([1000n, 100n], donor);
- });
- });
-
- [
- {mode: 'nft' as const, requiredPallets: []},
- {mode: 'ft' as const, requiredPallets: []},
- {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
- ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {
- before(async function() {
- // eslint-disable-next-line require-await
- await usingPlaygrounds(async helper => {
- requirePalletsOrSkip(this, helper, testSuite.requiredPallets);
- });
- });
-
- itSub('Fails to set properties in a collection if not its onwer/administrator', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- await expect(collection.setProperties(bob, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]))
- .to.be.rejectedWith(/common\.NoPermission/);
-
- expect(await collection.getProperties()).to.be.empty;
- });
-
- itSub('Fails to set properties that exceed the limits', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- const spaceLimit = (helper.getApi().consts.unique.maxCollectionPropertiesSize as any).toNumber();
-
- // Mute the general tx parsing error, too many bytes to process
- {
- console.error = () => {};
- await expect(collection.setProperties(alice, [
- {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))},
- ])).to.be.rejected;
- }
-
- expect(await collection.getProperties(['electron'])).to.be.empty;
-
- await expect(collection.setProperties(alice, [
- {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))},
- {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))},
- ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);
-
- expect(await collection.getProperties(['electron', 'black_hole'])).to.be.empty;
- });
-
- itSub('Fails to set more properties than it is allowed', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- const propertiesToBeSet = [];
- for(let i = 0; i < 65; i++) {
- propertiesToBeSet.push({
- key: 'electron_' + i,
- value: Math.random() > 0.5 ? 'high' : 'low',
- });
- }
-
- await expect(collection.setProperties(alice, propertiesToBeSet)).
- to.be.rejectedWith(/common\.PropertyLimitReached/);
-
- expect(await collection.getProperties()).to.be.empty;
- });
-
- itSub('Fails to set properties with invalid names', async ({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice);
-
- const invalidProperties = [
- [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],
- [{key: 'Mr/Sandman', value: 'Bring me a gene'}],
- [{key: 'déjà vu', value: 'hmm...'}],
- ];
-
- for(let i = 0; i < invalidProperties.length; i++) {
- await expect(
- collection.setProperties(alice, invalidProperties[i]),
- `on rejecting the new badly-named property #${i}`,
- ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
- }
-
- await expect(
- collection.setProperties(alice, [{key: '', value: 'nothing must not exist'}]),
- 'on rejecting an unnamed property',
- ).to.be.rejectedWith(/common\.EmptyPropertyKey/);
-
- await expect(
- collection.setProperties(alice, [{key: 'CRISPR-Cas9', value: 'rewriting nature!'}]),
- 'on setting the correctly-but-still-badly-named property',
- ).to.be.fulfilled;
-
- const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');
-
- const properties = await collection.getProperties(keys);
- expect(properties).to.be.deep.equal([
- {key: 'CRISPR-Cas9', value: 'rewriting nature!'},
- ]);
-
- for(let i = 0; i < invalidProperties.length; i++) {
- await expect(
- collection.deleteProperties(alice, invalidProperties[i].map(propertySet => propertySet.key)),
- `on trying to delete the non-existent badly-named property #${i}`,
- ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
- }
- });
-
- itSub('Forbids to repair a collection if called with non-sudo', async({helper}) => {
- const collection = await helper[testSuite.mode].mintCollection(alice, {properties: [
- {key: 'sea-creatures', value: 'mermaids'},
- {key: 'goldenratio', value: '1.6180339887498948482045868343656381177203091798057628621354486227052604628189'},
- ]});
-
- await expect(helper.executeExtrinsic(alice, 'api.tx.unique.forceRepairCollection', [collection.collectionId], true))
- .to.be.rejectedWith(/BadOrigin/);
- });
- }));
-});
js-packages/tests/nesting/graphs.test.tsdiffbeforeafterboth--- a/js-packages/tests/nesting/graphs.test.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from '../util/index.js';
-import {UniqueHelper, UniqueNFTCollection, UniqueNFToken} from '@unique/playgrounds/unique.js';
-
-/**
- * ```dot
- * 4 -> 3 -> 2 -> 1
- * 7 -> 6 -> 5 -> 2
- * 8 -> 5
- * ```
- */
-async function buildComplexObjectGraph(helper: UniqueHelper, sender: IKeyringPair): Promise<[UniqueNFTCollection,UniqueNFToken[]]> {
- const collection = await helper.nft.mintCollection(sender, {permissions: {nesting: {tokenOwner: true}}});
- const tokens = await collection.mintMultipleTokens(sender, Array(8).fill({owner: {Substrate: sender.address}}));
-
- await tokens[7].nest(sender, tokens[4]);
- await tokens[6].nest(sender, tokens[5]);
- await tokens[5].nest(sender, tokens[4]);
- await tokens[4].nest(sender, tokens[1]);
- await tokens[3].nest(sender, tokens[2]);
- await tokens[2].nest(sender, tokens[1]);
- await tokens[1].nest(sender, tokens[0]);
-
- return [collection, tokens];
-}
-
-describe('Graphs', () => {
- let alice: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([10n], donor);
- });
- });
-
- itSub('Ouroboros can\'t be created in a complex graph', async ({helper}) => {
- const [collection, tokens] = await buildComplexObjectGraph(helper, alice);
-
- await collection.setPermissions(alice, {nesting: {collectionAdmin: false, tokenOwner: true}});
-
- // [token owner] to self
- await expect(
- tokens[0].nest(alice, tokens[0]),
- '[token owner] self-nesting is forbidden',
- ).to.be.rejectedWith(/structure\.OuroborosDetected/);
- // [token owner] to nested part of graph
- await expect(
- tokens[0].nest(alice, tokens[4]),
- '[token owner] cannot nest the root node into an internal node',
- ).to.be.rejectedWith(/structure\.OuroborosDetected/);
- await expect(
- tokens[1].transferFrom(alice, tokens[0].nestingAccount(), tokens[7].nestingAccount()),
- '[token owner] cannot nest higher internal node into lower internal node',
- ).to.be.rejectedWith(/structure\.OuroborosDetected/);
-
- await collection.setPermissions(alice, {nesting: {collectionAdmin: true, tokenOwner: false}});
-
- // [collection owner] to self
- await expect(
- tokens[0].nest(alice, tokens[0]),
- '[collection owner] self-nesting is forbidden',
- ).to.be.rejectedWith(/structure\.OuroborosDetected/);
- // [collection owner] to nested part of graph
- await expect(
- tokens[0].nest(alice, tokens[4]),
- '[collection owner] cannot nest the root node into an internal node',
- ).to.be.rejectedWith(/structure\.OuroborosDetected/);
- });
-});
js-packages/tests/nesting/propertyPermissions.test.tsdiffbeforeafterboth--- a/js-packages/tests/nesting/propertyPermissions.test.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect} from '../util/index.js';
-import {UniqueNFTCollection, UniqueRFTCollection} from '@unique/playgrounds/unique.js';
-
-describe('Integration Test: Access Rights to Token Properties', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor);
- });
- });
-
- itSub('Reads access rights to properties of a collection', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice);
- const propertyRights = (await helper.callRpc('api.query.common.collectionPropertyPermissions', [collection.collectionId])).toJSON();
- expect(propertyRights).to.be.empty;
- });
-
- async function testSetsAccessRightsToProperties(collection: UniqueNFTCollection | UniqueRFTCollection) {
- await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true}}]))
- .to.be.fulfilled;
-
- await collection.addAdmin(alice, {Substrate: bob.address});
-
- await expect(collection.setTokenPropertyPermissions(bob, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]))
- .to.be.fulfilled;
-
- const propertyRights = await collection.getPropertyPermissions(['skullduggery', 'mindgame']);
- expect(propertyRights).to.include.deep.members([
- {key: 'skullduggery', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}},
- {key: 'mindgame', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},
- ]);
- }
-
- itSub('Sets access rights to properties of a collection (NFT)', async ({helper}) => {
- await testSetsAccessRightsToProperties(await helper.nft.mintCollection(alice));
- });
-
- itSub.ifWithPallets('Sets access rights to properties of a collection (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- await testSetsAccessRightsToProperties(await helper.rft.mintCollection(alice));
- });
-
- async function testChangesAccessRightsToProperty(collection: UniqueNFTCollection | UniqueRFTCollection) {
- await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]))
- .to.be.fulfilled;
-
- await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))
- .to.be.fulfilled;
-
- const propertyRights = await collection.getPropertyPermissions();
- expect(propertyRights).to.be.deep.equal([
- {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
- ]);
- }
-
- itSub('Changes access rights to properties of a NFT collection', async ({helper}) => {
- await testChangesAccessRightsToProperty(await helper.nft.mintCollection(alice));
- });
-
- itSub.ifWithPallets('Changes access rights to properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {
- await testChangesAccessRightsToProperty(await helper.rft.mintCollection(alice));
- });
-});
-
-describe('Negative Integration Test: Access Rights to Token Properties', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);
- });
- });
-
- async function testPreventsFromSettingAccessRightsNotAdminOrOwner(collection: UniqueNFTCollection | UniqueRFTCollection) {
- await expect(collection.setTokenPropertyPermissions(bob, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]))
- .to.be.rejectedWith(/common\.NoPermission/);
-
- const propertyRights = await collection.getPropertyPermissions(['skullduggery']);
- expect(propertyRights).to.be.empty;
- }
-
- itSub('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async ({helper}) => {
- await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.nft.mintCollection(alice));
- });
-
- itSub.ifWithPallets('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', [Pallets.ReFungible], async ({helper}) => {
- await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.rft.mintCollection(alice));
- });
-
- async function testPreventFromAddingTooManyPossibleProperties(collection: UniqueNFTCollection | UniqueRFTCollection) {
- const constitution = [];
- for(let i = 0; i < 65; i++) {
- constitution.push({
- key: 'property_' + i,
- permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},
- });
- }
-
- await expect(collection.setTokenPropertyPermissions(alice, constitution))
- .to.be.rejectedWith(/common\.PropertyLimitReached/);
-
- const propertyRights = await collection.getPropertyPermissions();
- expect(propertyRights).to.be.empty;
- }
-
- itSub('Prevents from adding too many possible properties (NFT)', async ({helper}) => {
- await testPreventFromAddingTooManyPossibleProperties(await helper.nft.mintCollection(alice));
- });
-
- itSub.ifWithPallets('Prevents from adding too many possible properties (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- await testPreventFromAddingTooManyPossibleProperties(await helper.rft.mintCollection(alice));
- });
-
- async function testPreventAccessRightsModifiedIfConstant(collection: UniqueNFTCollection | UniqueRFTCollection) {
- await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))
- .to.be.fulfilled;
-
- await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {collectionAdmin: true}}]))
- .to.be.rejectedWith(/common\.NoPermission/);
-
- const propertyRights = await collection.getPropertyPermissions(['skullduggery']);
- expect(propertyRights).to.deep.equal([
- {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
- ]);
- }
-
- itSub('Prevents access rights to be modified if constant (NFT)', async ({helper}) => {
- await testPreventAccessRightsModifiedIfConstant(await helper.nft.mintCollection(alice));
- });
-
- itSub.ifWithPallets('Prevents access rights to be modified if constant (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- await testPreventAccessRightsModifiedIfConstant(await helper.rft.mintCollection(alice));
- });
-
- async function testPreventsAddingPropertiesWithInvalidNames(collection: UniqueNFTCollection | UniqueRFTCollection) {
- const invalidProperties = [
- [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],
- [{key: 'G#4', permission: {tokenOwner: true}}],
- [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],
- ];
-
- for(let i = 0; i < invalidProperties.length; i++) {
- await expect(
- collection.setTokenPropertyPermissions(alice, invalidProperties[i]),
- `on setting the new badly-named property #${i}`,
- ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
- }
-
- await expect(
- collection.setTokenPropertyPermissions(alice, [{key: '', permission: {}}]),
- 'on rejecting an unnamed property',
- ).to.be.rejectedWith(/common\.EmptyPropertyKey/);
-
- const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string
- await expect(
- collection.setTokenPropertyPermissions(alice, [
- {key: correctKey, permission: {collectionAdmin: true}},
- ]),
- 'on setting the correctly-but-still-badly-named property',
- ).to.be.fulfilled;
-
- const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');
-
- const propertyRights = await collection.getPropertyPermissions(keys);
- expect(propertyRights).to.be.deep.equal([
- {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},
- ]);
- }
-
- itSub('Prevents adding properties with invalid names (NFT)', async ({helper}) => {
- await testPreventsAddingPropertiesWithInvalidNames(await helper.nft.mintCollection(alice));
- });
-
- itSub.ifWithPallets('Prevents adding properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- await testPreventsAddingPropertiesWithInvalidNames(await helper.rft.mintCollection(alice));
- });
-});
js-packages/tests/nesting/tokenProperties.seqtest.tsdiffbeforeafterboth--- a/js-packages/tests/nesting/tokenProperties.seqtest.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../util/index.js';
-
-describe('Integration Test: Token Properties with sudo', () => {
- let superuser: IKeyringPair;
- let alice: IKeyringPair; // collection owner
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- superuser = await privateKey('//Alice');
- const donor = await privateKey({url: import.meta.url});
- [alice] = await helper.arrange.createAccounts([100n], donor);
- });
- });
-
- [
- {mode: 'nft' as const, pieces: undefined, requiredPallets: []} as const,
- {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]} as const,
- ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {
- before(async function() {
- // eslint-disable-next-line require-await
- await usingPlaygrounds(async helper => {
- requirePalletsOrSkip(this, helper, testSuite.requiredPallets);
- });
- });
-
- itSub('force_repair_item preserves valid consumed space', async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testSuite.mode].mintCollection(alice, {
- tokenPropertyPermissions: [
- {
- key: propKey,
- permission: {mutable: true, tokenOwner: true},
- },
- ],
- });
- const token = await (
- testSuite.pieces
- ? collection.mintToken(alice, testSuite.pieces as any)
- : collection.mintToken(alice)
- );
-
- const prop = {key: propKey, value: 'a'.repeat(4096)};
-
- await token.setProperties(alice, [prop]);
- const originalSpace = await token.getTokenPropertiesConsumedSpace();
- expect(originalSpace).to.be.equal(sizeOfProperty(prop));
-
- await helper.getSudo().executeExtrinsic(superuser, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true);
- const recomputedSpace = await token.getTokenPropertiesConsumedSpace();
- expect(recomputedSpace).to.be.equal(originalSpace);
- });
- }));
-});
js-packages/tests/nesting/tokenProperties.test.tsdiffbeforeafterboth--- a/js-packages/tests/nesting/tokenProperties.test.ts
+++ /dev/null
@@ -1,816 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect, sizeOfProperty} from '../util/index.js';
-import {UniqueHelper, UniqueNFToken, UniqueRFToken} from '@unique/playgrounds/unique.js';
-
-describe('Integration Test: Token Properties', () => {
- let alice: IKeyringPair; // collection owner
- let bob: IKeyringPair; // collection admin
- let charlie: IKeyringPair; // token owner
-
- let permissions: {permission: any, signers: IKeyringPair[]}[];
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 100n, 100n], donor);
- });
-
- permissions = [
- {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},
- {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},
- {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},
- {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},
- {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},
- {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},
- ];
- });
-
- async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {
- const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {
- tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
- signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
- });
- return mode == 'NFT' ? [await collection.mintToken(alice), 1n] : [await collection.mintToken(alice, 100n as any), 100n];
- }
-
- async function testReadsYetEmptyProperties(token: UniqueNFToken | UniqueRFToken) {
- const properties = await token.getProperties();
- expect(properties).to.be.empty;
-
- const tokenData = await token.getData();
- expect(tokenData!.properties).to.be.empty;
- }
-
- itSub('Reads yet empty properties of a token (NFT)', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice);
- const token = await collection.mintToken(alice);
- await testReadsYetEmptyProperties(token);
- });
-
- itSub.ifWithPallets('Reads yet empty properties of a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const collection = await helper.rft.mintCollection(alice);
- const token = await collection.mintToken(alice);
- await testReadsYetEmptyProperties(token);
- });
-
- async function testAssignPropertiesAccordingToPermissions(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
- await token.collection.addAdmin(alice, {Substrate: bob.address});
- await token.transfer(alice, {Substrate: charlie.address}, pieces);
-
- const propertyKeys: string[] = [];
- let i = 0;
- for(const permission of permissions) {
- i++;
- let j = 0;
- for(const signer of permission.signers) {
- j++;
- const key = i + '_' + signer.address;
- propertyKeys.push(key);
-
- await expect(
- token.setProperties(signer, [{key: key, value: 'Serotonin increase'}]),
- `on adding property #${i} by signer #${j}`,
- ).to.be.fulfilled;
- }
- }
-
- const properties = await token.getProperties(propertyKeys);
- const tokenData = await token.getData();
- for(let i = 0; i < properties.length; i++) {
- expect(properties[i].value).to.be.equal('Serotonin increase');
- expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');
- }
- }
-
- itSub('Assigns properties to a token according to permissions (NFT)', async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
- await testAssignPropertiesAccordingToPermissions(token, amount);
- });
-
- itSub.ifWithPallets('Assigns properties to a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
- await testAssignPropertiesAccordingToPermissions(token, amount);
- });
-
- async function testChangesPropertiesAccordingPermission(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
- await token.collection.addAdmin(alice, {Substrate: bob.address});
- await token.transfer(alice, {Substrate: charlie.address}, pieces);
-
- const propertyKeys: string[] = [];
- let i = 0;
- for(const permission of permissions) {
- i++;
- if(!permission.permission.mutable) continue;
-
- let j = 0;
- for(const signer of permission.signers) {
- j++;
- const key = i + '_' + signer.address;
- propertyKeys.push(key);
-
- await expect(
- token.setProperties(signer, [{key, value: 'Serotonin increase'}]),
- `on adding property #${i} by signer #${j}`,
- ).to.be.fulfilled;
-
- await expect(
- token.setProperties(signer, [{key, value: 'Serotonin stable'}]),
- `on changing property #${i} by signer #${j}`,
- ).to.be.fulfilled;
- }
- }
-
- const properties = await token.getProperties(propertyKeys);
- const tokenData = await token.getData();
- for(let i = 0; i < properties.length; i++) {
- expect(properties[i].value).to.be.equal('Serotonin stable');
- expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');
- }
- }
-
- itSub('Changes properties of a token according to permissions (NFT)', async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
- await testChangesPropertiesAccordingPermission(token, amount);
- });
-
- itSub.ifWithPallets('Changes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
- await testChangesPropertiesAccordingPermission(token, amount);
- });
-
- async function testDeletePropertiesAccordingPermission(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
- await token.collection.addAdmin(alice, {Substrate: bob.address});
- await token.transfer(alice, {Substrate: charlie.address}, pieces);
-
- const propertyKeys: string[] = [];
- let i = 0;
-
- for(const permission of permissions) {
- i++;
- if(!permission.permission.mutable) continue;
-
- let j = 0;
- for(const signer of permission.signers) {
- j++;
- const key = i + '_' + signer.address;
- propertyKeys.push(key);
-
- await expect(
- token.setProperties(signer, [{key, value: 'Serotonin increase'}]),
- `on adding property #${i} by signer #${j}`,
- ).to.be.fulfilled;
-
- await expect(
- token.deleteProperties(signer, [key]),
- `on deleting property #${i} by signer #${j}`,
- ).to.be.fulfilled;
- }
- }
-
- expect(await token.getProperties(propertyKeys)).to.be.empty;
- expect((await token.getData())!.properties).to.be.empty;
- }
-
- itSub('Deletes properties of a token according to permissions (NFT)', async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
- await testDeletePropertiesAccordingPermission(token, amount);
- });
-
- itSub.ifWithPallets('Deletes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
- await testDeletePropertiesAccordingPermission(token, amount);
- });
-
- itSub('Assigns properties to a nested token according to permissions', async ({helper}) => {
- const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const collectionB = await helper.nft.mintCollection(alice, {
- tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
- signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
- });
- const targetToken = await collectionA.mintToken(alice);
- const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());
-
- await collectionB.addAdmin(alice, {Substrate: bob.address});
- await targetToken.transfer(alice, {Substrate: charlie.address});
-
- const propertyKeys: string[] = [];
- let i = 0;
- for(const permission of permissions) {
- i++;
- let j = 0;
- for(const signer of permission.signers) {
- j++;
- const key = i + '_' + signer.address;
- propertyKeys.push(key);
-
- await expect(
- nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]),
- `on adding property #${i} by signer #${j}`,
- ).to.be.fulfilled;
- }
- }
-
- const properties = await nestedToken.getProperties(propertyKeys);
- const tokenData = await nestedToken.getData();
- for(let i = 0; i < properties.length; i++) {
- expect(properties[i].value).to.be.equal('Serotonin increase');
- expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');
- }
- expect(await targetToken.getProperties()).to.be.empty;
- });
-
- itSub('Changes properties of a nested token according to permissions', async ({helper}) => {
- const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const collectionB = await helper.nft.mintCollection(alice, {
- tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
- signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
- });
- const targetToken = await collectionA.mintToken(alice);
- const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());
-
- await collectionB.addAdmin(alice, {Substrate: bob.address});
- await targetToken.transfer(alice, {Substrate: charlie.address});
-
- const propertyKeys: string[] = [];
- let i = 0;
- for(const permission of permissions) {
- i++;
- if(!permission.permission.mutable) continue;
-
- let j = 0;
- for(const signer of permission.signers) {
- j++;
- const key = i + '_' + signer.address;
- propertyKeys.push(key);
-
- await expect(
- nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]),
- `on adding property #${i} by signer #${j}`,
- ).to.be.fulfilled;
-
- await expect(
- nestedToken.setProperties(signer, [{key, value: 'Serotonin stable'}]),
- `on changing property #${i} by signer #${j}`,
- ).to.be.fulfilled;
- }
- }
-
- const properties = await nestedToken.getProperties(propertyKeys);
- const tokenData = await nestedToken.getData();
- for(let i = 0; i < properties.length; i++) {
- expect(properties[i].value).to.be.equal('Serotonin stable');
- expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');
- }
- expect(await targetToken.getProperties()).to.be.empty;
- });
-
- itSub('Deletes properties of a nested token according to permissions', async ({helper}) => {
- const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const collectionB = await helper.nft.mintCollection(alice, {
- tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
- signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
- });
- const targetToken = await collectionA.mintToken(alice);
- const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());
-
- await collectionB.addAdmin(alice, {Substrate: bob.address});
- await targetToken.transfer(alice, {Substrate: charlie.address});
-
- const propertyKeys: string[] = [];
- let i = 0;
- for(const permission of permissions) {
- i++;
- if(!permission.permission.mutable) continue;
-
- let j = 0;
- for(const signer of permission.signers) {
- j++;
- const key = i + '_' + signer.address;
- propertyKeys.push(key);
-
- await expect(
- nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]),
- `on adding property #${i} by signer #${j}`,
- ).to.be.fulfilled;
-
- await expect(
- nestedToken.deleteProperties(signer, [key]),
- `on deleting property #${i} by signer #${j}`,
- ).to.be.fulfilled;
- }
- }
-
- expect(await nestedToken.getProperties(propertyKeys)).to.be.empty;
- expect((await nestedToken.getData())!.properties).to.be.empty;
- expect(await targetToken.getProperties()).to.be.empty;
- });
-
- [
- {mode: 'nft' as const, storage: 'nonfungible' as const, pieces: undefined, requiredPallets: []} as const,
- {mode: 'rft' as const, storage: 'refungible' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]} as const,
- ].map(testCase =>
- itSub.ifWithPallets(`Allows modifying a token property multiple times with the same size (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testCase.mode].mintCollection(alice, {
- tokenPropertyPermissions: [
- {
- key: propKey,
- permission: {mutable: true, tokenOwner: true},
- },
- ],
- });
-
- const maxTokenPropertiesSize = 32768;
-
- const propDataSize = 4096;
-
- let propDataChar = 'a';
- const makeNewPropData = () => {
- propDataChar = String.fromCharCode(propDataChar.charCodeAt(0) + 1);
- return `${propDataChar}`.repeat(propDataSize);
- };
-
- const token = await (
- testCase.pieces
- ? collection.mintToken(alice, testCase.pieces as any)
- : collection.mintToken(alice)
- );
-
- const property = {key: propKey, value: makeNewPropData()};
- await token.setProperties(alice, [property]);
- const originalSpace = await token.getTokenPropertiesConsumedSpace();
- expect(originalSpace).to.be.equal(sizeOfProperty(property));
-
- const sameSizePropertiesPossibleNum = maxTokenPropertiesSize / propDataSize;
-
- // It is possible to modify a property as many times as needed.
- // It will not consume any additional space.
- for(let i = 0; i < sameSizePropertiesPossibleNum + 1; i++) {
- await token.setProperties(alice, [{key: propKey, value: makeNewPropData()}]);
- const consumedSpace = await token.getTokenPropertiesConsumedSpace();
- expect(consumedSpace).to.be.equal(originalSpace);
- }
- }));
-
- [
- {mode: 'nft' as const, pieces: undefined, requiredPallets: []},
- {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]},
- ].map(testCase =>
- itSub.ifWithPallets(`Adding then removing a token property doesn't change the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testCase.mode].mintCollection(alice, {
- tokenPropertyPermissions: [
- {
- key: propKey,
- permission: {mutable: true, tokenOwner: true},
- },
- ],
- });
- const token = await (
- testCase.pieces
- ? collection.mintToken(alice, testCase.pieces as any)
- : collection.mintToken(alice)
- );
- const originalSpace = await token.getTokenPropertiesConsumedSpace();
-
- const propDataSize = 4096;
- const propData = 'a'.repeat(propDataSize);
-
- const property = {key: propKey, value: propData};
- await token.setProperties(alice, [property]);
- let consumedSpace = await token.getTokenPropertiesConsumedSpace();
- expect(consumedSpace).to.be.equal(sizeOfProperty(property));
-
- await token.deleteProperties(alice, [propKey]);
- consumedSpace = await token.getTokenPropertiesConsumedSpace();
- expect(consumedSpace).to.be.equal(originalSpace);
- }));
-
- [
- {mode: 'nft' as const, pieces: undefined, requiredPallets: []},
- {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]},
- ].map(testCase =>
- itSub.ifWithPallets(`Modifying a token property with different sizes correctly changes the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testCase.mode].mintCollection(alice, {
- tokenPropertyPermissions: [
- {
- key: propKey,
- permission: {mutable: true, tokenOwner: true},
- },
- ],
- });
- const token = await (
- testCase.pieces
- ? collection.mintToken(alice, testCase.pieces as any)
- : collection.mintToken(alice)
- );
- const originalSpace = await token.getTokenPropertiesConsumedSpace();
-
- const initProp = {key: propKey, value: 'a'.repeat(4096)};
- const biggerProp = {key: propKey, value: 'b'.repeat(5000)};
- const smallerProp = {key: propKey, value: 'c'.repeat(4000)};
-
- let consumedSpace;
- let expectedConsumedSpaceDiff;
-
- await token.setProperties(alice, [initProp]);
- consumedSpace = await token.getTokenPropertiesConsumedSpace();
- expectedConsumedSpaceDiff = sizeOfProperty(initProp) - originalSpace;
- expect(consumedSpace).to.be.equal(originalSpace + expectedConsumedSpaceDiff);
-
- await token.setProperties(alice, [biggerProp]);
- consumedSpace = await token.getTokenPropertiesConsumedSpace();
- expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(initProp);
- expect(consumedSpace).to.be.equal(sizeOfProperty(initProp) + expectedConsumedSpaceDiff);
-
- await token.setProperties(alice, [smallerProp]);
- consumedSpace = await token.getTokenPropertiesConsumedSpace();
- expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(smallerProp);
- expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);
- }));
-
- itSub('Set sponsored properties', async({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: true}}]});
-
- await collection.setSponsor(alice, alice.address);
- await collection.confirmSponsorship(alice);
- await collection.setPermissions(alice, {access: 'AllowList', mintMode: true});
- await collection.addToAllowList(alice, {Substrate: bob.address});
- await collection.setLimits(alice, {sponsoredDataRateLimit: {blocks: 30}});
-
- const token = await collection.mintToken(alice, {Substrate: bob.address});
-
- const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
- const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
-
- await token.setProperties(bob, [{key: 'k', value: 'val'}]);
-
- const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
- const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
-
- expect(bobBalanceAfter).to.be.equal(bobBalanceBefore);
- expect(aliceBalanceBefore > aliceBalanceAfter).to.be.true;
- });
-});
-
-describe('Negative Integration Test: Token Properties', () => {
- let alice: IKeyringPair; // collection owner
- let bob: IKeyringPair; // collection admin
- let charlie: IKeyringPair; // token owner
-
- let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- let dave: IKeyringPair;
- [alice, bob, charlie, dave] = await helper.arrange.createAccounts([100n, 100n, 100n, 100n], donor);
-
- // todo:playgrounds probably separate these tests later
- constitution = [
- {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},
- {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},
- {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},
- {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},
- {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},
- {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},
- ];
- });
- });
-
- [
- {mode: 'nft' as const, requiredPallets: [Pallets.NFT]},
- {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
- ].map(testCase =>
- itSub.ifWithPallets(`Forbids adding/deleting properties of a token if token doesn't exist (${testCase.mode.toLocaleUpperCase})`, testCase.requiredPallets, async({helper}) => {
- const collection = await helper[testCase.mode].mintCollection(alice, {
- tokenPropertyPermissions: constitution.slice(0, 1).map(({permission}) => ({key: '1', permission})),
- });
- const nonExistentToken = collection.getTokenObject(1);
-
- await expect(
- nonExistentToken.setProperties(alice, [{key: '1', value: 'Serotonin increase'}]),
- 'on expecting failure whilst adding a property by alice',
- ).to.be.rejectedWith(/common\.TokenNotFound/);
-
- await expect(
- nonExistentToken.deleteProperties(alice, ['1']),
- 'on expecting failure whilst deleting a property by alice',
- ).to.be.rejectedWith(/common\.TokenNotFound/);
- }));
-
- async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {
- const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {
- tokenPropertyPermissions: constitution.map(({permission}, i) => ({key: `${i+1}`, permission})),
- });
- return mode == 'NFT' ? [await collection.mintToken(alice), 1n] : [await collection.mintToken(alice, 100n as any), 100n];
- }
-
- async function getConsumedSpace(api: any, collectionId: number, tokenId: number, mode: 'NFT' | 'RFT'): Promise<number> {
- return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace;
- }
-
- async function prepare(token: UniqueNFToken | UniqueRFToken, pieces: bigint): Promise<number> {
- await token.collection.addAdmin(alice, {Substrate: bob.address});
- await token.transfer(alice, {Substrate: charlie.address}, pieces);
-
- let i = 0;
- for(const passage of constitution) {
- i++;
- const signer = passage.signers[0];
- await expect(
- token.setProperties(signer, [{key: `${i}`, value: 'Serotonin increase'}]),
- `on adding property ${i} by ${signer.address}`,
- ).to.be.fulfilled;
- }
-
- const originalSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
- return originalSpace;
- }
-
- async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
- const originalSpace = await prepare(token, pieces);
-
- let i = 0;
- for(const forbiddance of constitution) {
- i++;
- if(!forbiddance.permission.mutable) continue;
-
- await expect(
- token.setProperties(forbiddance.sinner, [{key: `${i}`, value: 'Serotonin down'}]),
- `on failing to change property ${i} by the malefactor`,
- ).to.be.rejectedWith(/common\.NoPermission/);
-
- await expect(
- token.deleteProperties(forbiddance.sinner, [`${i}`]),
- `on failing to delete property ${i} by the malefactor`,
- ).to.be.rejectedWith(/common\.NoPermission/);
- }
-
- const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
- expect(consumedSpace).to.be.equal(originalSpace);
- }
-
- itSub('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
- await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, amount);
- });
-
- itSub.ifWithPallets('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
- await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, amount);
- });
-
- async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
- const originalSpace = await prepare(token, pieces);
-
- let i = 0;
- for(const permission of constitution) {
- i++;
- if(permission.permission.mutable) continue;
-
- await expect(
- token.setProperties(permission.signers[0], [{key: `${i}`, value: 'Serotonin down'}]),
- `on failing to change property ${i} by signer #0`,
- ).to.be.rejectedWith(/common\.NoPermission/);
-
- await expect(
- token.deleteProperties(permission.signers[0], [i.toString()]),
- `on failing to delete property ${i} by signer #0`,
- ).to.be.rejectedWith(/common\.NoPermission/);
- }
-
- const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
- expect(consumedSpace).to.be.equal(originalSpace);
- }
-
- itSub('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
- await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, amount);
- });
-
- itSub.ifWithPallets('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
- await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, amount);
- });
-
- async function testForbidsAddingPropertiesIfPropertyNotDeclared(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
- const originalSpace = await prepare(token, pieces);
-
- await expect(
- token.setProperties(alice, [{key: 'non-existent', value: 'I exist!'}]),
- 'on failing to add a previously non-existent property',
- ).to.be.rejectedWith(/common\.NoPermission/);
-
- await expect(
- token.collection.setTokenPropertyPermissions(alice, [{key: 'now-existent', permission: {}}]),
- 'on setting a new non-permitted property',
- ).to.be.fulfilled;
-
- await expect(
- token.setProperties(alice, [{key: 'now-existent', value: 'I exist!'}]),
- 'on failing to add a property forbidden by the \'None\' permission',
- ).to.be.rejectedWith(/common\.NoPermission/);
-
- expect(await token.getProperties(['non-existent', 'now-existent'])).to.be.empty;
-
- const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
- expect(consumedSpace).to.be.equal(originalSpace);
- }
-
- itSub('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
- await testForbidsAddingPropertiesIfPropertyNotDeclared(token, amount);
- });
-
- itSub.ifWithPallets('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
- await testForbidsAddingPropertiesIfPropertyNotDeclared(token, amount);
- });
-
- async function testForbidsAddingTooLargeProperties(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
- const originalSpace = await prepare(token, pieces);
-
- await expect(
- token.collection.setTokenPropertyPermissions(alice, [
- {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}},
- {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},
- ]),
- 'on setting new permissions for properties',
- ).to.be.fulfilled;
-
- // Mute the general tx parsing error
- {
- console.error = () => {};
- await expect(token.setProperties(alice, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]))
- .to.be.rejected;
- }
-
- await expect(token.setProperties(alice, [
- {key: 'a_holy_book', value: 'word '.repeat(3277)},
- {key: 'young_years', value: 'neverending'.repeat(1490)},
- ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);
-
- expect(await token.getProperties(['a_holy_book', 'young_years'])).to.be.empty;
- const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
- expect(consumedSpace).to.be.equal(originalSpace);
- }
-
- itSub('Forbids adding too large properties to a token (NFT)', async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
- await testForbidsAddingTooLargeProperties(token, amount);
- });
-
- itSub.ifWithPallets('Forbids adding too large properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
- const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
- await testForbidsAddingTooLargeProperties(token, amount);
- });
-
- [
- {mode: 'nft' as const, requiredPallets: []},
- {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
- ].map(testCase =>
- itSub.ifWithPallets(`Forbids adding too many propeties to a token (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
- const collection = await helper[testCase.mode].mintCollection(alice);
- const maxPropertiesPerItem = 64;
-
- for(let i = 0; i < maxPropertiesPerItem; i++) {
- await collection.setTokenPropertyPermissions(alice, [{
- key: `${i+1}`,
- permission: {mutable: true, tokenOwner: true, collectionAdmin: true},
- }]);
- }
-
- await expect(collection.setTokenPropertyPermissions(alice, [{
- key: `${maxPropertiesPerItem}-th`,
- permission: {mutable: true, tokenOwner: true, collectionAdmin: true},
- }])).to.be.rejectedWith(/common\.PropertyLimitReached/);
- }));
-
- [
- {mode: 'nft' as const, pieces: undefined, requiredPallets: []},
- {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]},
- ].map(testCase =>
- itSub.ifWithPallets(`Forbids force_repair_item from non-sudo (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
- const propKey = 'tok-prop';
-
- const collection = await helper[testCase.mode].mintCollection(alice, {
- tokenPropertyPermissions: [
- {
- key: propKey,
- permission: {mutable: true, tokenOwner: true},
- },
- ],
- });
- const token = await (
- testCase.pieces
- ? collection.mintToken(alice, testCase.pieces as any)
- : collection.mintToken(alice)
- );
-
- const propDataSize = 4096;
- const propData = 'a'.repeat(propDataSize);
- await token.setProperties(alice, [{key: propKey, value: propData}]);
-
- await expect(helper.executeExtrinsic(alice, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true))
- .to.be.rejectedWith(/BadOrigin/);
- }));
-});
-
-describe('ReFungible token properties permissions tests', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
- let charlie: IKeyringPair;
-
- before(async function() {
- await usingPlaygrounds(async (helper, privateKey) => {
- requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);
-
- const donor = await privateKey({url: import.meta.url});
- [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
- });
- });
-
- async function prepare(helper: UniqueHelper): Promise<UniqueRFToken> {
- const collection = await helper.rft.mintCollection(alice);
- const token = await collection.mintToken(alice, 100n);
-
- await collection.addAdmin(alice, {Substrate: bob.address});
- await collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable: true, tokenOwner: true}}]);
-
- return token;
- }
-
- itSub('Forbids adding token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {
- const token = await prepare(helper);
-
- await token.transfer(alice, {Substrate: charlie.address}, 33n);
-
- await expect(token.setProperties(alice, [
- {key: 'fractals', value: 'multiverse'},
- ])).to.be.rejectedWith(/common\.NoPermission/);
- });
-
- itSub('Forbids mutating token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {
- const token = await prepare(helper);
-
- await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, tokenOwner: true}}]))
- .to.be.fulfilled;
-
- await expect(token.setProperties(alice, [
- {key: 'fractals', value: 'multiverse'},
- ])).to.be.fulfilled;
-
- await token.transfer(alice, {Substrate: charlie.address}, 33n);
-
- await expect(token.setProperties(alice, [
- {key: 'fractals', value: 'want to rule the world'},
- ])).to.be.rejectedWith(/common\.NoPermission/);
- });
-
- itSub('Forbids deleting token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {
- const token = await prepare(helper);
-
- await expect(token.setProperties(alice, [
- {key: 'fractals', value: 'one headline - why believe it'},
- ])).to.be.fulfilled;
-
- await token.transfer(alice, {Substrate: charlie.address}, 33n);
-
- await expect(token.deleteProperties(alice, ['fractals'])).
- to.be.rejectedWith(/common\.NoPermission/);
- });
-
- itSub('Allows token property mutation with collectionOwner==true when admin doesn\'t have all pieces', async ({helper}) => {
- const token = await prepare(helper);
-
- await token.transfer(alice, {Substrate: charlie.address}, 33n);
-
- await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, collectionAdmin: true}}]))
- .to.be.fulfilled;
-
- await expect(token.setProperties(alice, [
- {key: 'fractals', value: 'multiverse'},
- ])).to.be.fulfilled;
- });
-});
js-packages/tests/nesting/unnest.test.tsdiffbeforeafterboth--- a/js-packages/tests/nesting/unnest.test.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-// 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 <http://www.gnu.org/licenses/>.
-
-import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, usingPlaygrounds} from '../util/index.js';
-import {CrossAccountId, UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '@unique/playgrounds/unique.js';
-
-describe('Integration Test: Unnesting', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
- let charlie: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 50n, 50n], donor);
- });
- });
-
- itSub('NFT: allows the owner to successfully unnest a token', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const targetToken = await collection.mintToken(alice);
-
- // Create a nested token
- const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());
-
- // Unnest
- await expect(nestedToken.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}), 'while unnesting').to.be.fulfilled;
- expect(await nestedToken.getOwner()).to.be.deep.equal({Substrate: alice.address});
-
- // Nest and burn
- await nestedToken.nest(alice, targetToken);
- await expect(nestedToken.burnFrom(alice, targetToken.nestingAccount()), 'while burning').to.be.fulfilled;
- await expect(nestedToken.getOwner()).to.be.rejected;
- });
-
- itSub('NativeFungible: allows the owner to successfully unnest a token', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const targetToken = await collection.mintToken(alice);
-
- const collectionFT = helper.ft.getCollectionObject(0);
-
- // Nest
- await collectionFT.transfer(alice, targetToken.nestingAccount(), 10n);
- // Unnest
- await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
-
- expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(1n);
- });
-
- itSub('Fungible: allows the owner to successfully unnest a token', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const targetToken = await collection.mintToken(alice);
-
- const collectionFT = await helper.ft.mintCollection(alice);
-
- // Nest and unnest
- await collectionFT.mint(alice, 10n, targetToken.nestingAccount());
- await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
- expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(9n);
- expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(1n);
-
- // Nest and burn
- await collectionFT.transfer(alice, targetToken.nestingAccount(), 5n);
- await expect(collectionFT.burnTokensFrom(alice, targetToken.nestingAccount(), 6n), 'while burning').to.be.fulfilled;
- expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(4n);
- expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(0n);
- expect(await targetToken.getChildren()).to.be.length(0);
- });
-
- itSub.ifWithPallets('ReFungible: allows the owner to successfully unnest a token', [Pallets.ReFungible], async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const targetToken = await collection.mintToken(alice);
-
- const collectionRFT = await helper.rft.mintCollection(alice);
-
- // Nest and unnest
- const token = await collectionRFT.mintToken(alice, 10n, targetToken.nestingAccount());
- await expect(token.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
- expect(await token.getBalance({Substrate: alice.address})).to.be.equal(9n);
- expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(1n);
-
- // Nest and burn
- await token.transfer(alice, targetToken.nestingAccount(), 5n);
- await expect(token.burnFrom(alice, targetToken.nestingAccount(), 6n), 'while burning').to.be.fulfilled;
- expect(await token.getBalance({Substrate: alice.address})).to.be.equal(4n);
- expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(0n);
- expect(await targetToken.getChildren()).to.be.length(0);
- });
-
- async function checkNestedAmountState({
- expectedBalance,
- childrenShouldPresent,
- nested,
- targetNft,
- }: {
- expectedBalance: bigint,
- childrenShouldPresent: boolean,
- nested: UniqueFTCollection | UniqueRFToken,
- targetNft: UniqueNFToken,
- }) {
- const balance = await nested.getBalance(targetNft.nestingAccount());
- expect(balance).to.be.equal(expectedBalance);
-
- const children = await targetNft.getChildren();
-
- if(childrenShouldPresent) {
- expect(children[0]).to.be.deep.equal({
- collectionId: nested.collectionId,
- tokenId: (nested instanceof UniqueFTCollection) ? 0 : nested.tokenId,
- });
- } else {
- expect(children.length).to.be.equal(0);
- }
- }
-
- function ownerOrAdminUnnestCases(modes: ('ft' | 'nft' | 'rft')[]): {
- mode: 'ft' | 'nft' | 'rft',
- sender: string,
- op: 'transfer' | 'burn',
- requiredPallets: Pallets[],
- }[] {
- const senders = ['owner', 'admin'];
- const ops = ['transfer', 'burn'];
-
- const cases = [];
- for(const mode of modes) {
- const requiredPallets = (mode === 'rft')
- ? [Pallets.ReFungible]
- : [];
-
- for(const sender of senders) {
- for(const op of ops) {
- cases.push({
- mode: mode as 'ft' | 'nft' | 'rft',
- sender,
- op: op as 'transfer' | 'burn',
- requiredPallets,
- });
- }
- }
- }
-
- return cases;
- }
-
- ownerOrAdminUnnestCases(['ft', 'rft']).map(testCase =>
- itSub.ifWithPallets(`[${testCase.mode}]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, testCase.requiredPallets, async({helper}) => {
- const owner = alice;
- const admin = bob;
-
- const unnester = (testCase.sender === 'owner')
- ? owner
- : admin;
-
- const collectionNFT = await helper.nft.mintCollection(owner);
- await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});
-
- const collectionNested = await helper[testCase.mode as 'ft' | 'rft'].mintCollection(owner, {
- limits: {
- ownerCanTransfer: true,
- },
- });
- await collectionNested.addAdmin(owner, {Substrate: admin.address});
-
- const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});
-
- let nested: UniqueFTCollection | UniqueRFToken;
- const totalAmount = 5n;
- const firstUnnestAmount = 2n;
- const restUnnestAmount = totalAmount - firstUnnestAmount;
-
- if(collectionNested instanceof UniqueFTCollection) {
- await collectionNested.mint(owner, totalAmount, {Substrate: charlie.address});
- nested = collectionNested;
- } else {
- nested = await collectionNested.mintToken(owner, totalAmount, {Substrate: charlie.address});
- }
-
- // transfer/burn `amount` of nested assets by `unnester`.
- const doOperationAndCheck = async ({
- amount,
- shouldBeNestedAfterOp,
- }: {
- amount: bigint,
- shouldBeNestedAfterOp: boolean,
- }) => {
- const nestedBalanceBeforeOp = await nested.getBalance(targetNft.nestingAccount());
-
- if(testCase.op === 'transfer') {
- const bobBalanceBeforeOp = await nested.getBalance({Substrate: bob.address});
-
- await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}, amount);
- expect(await nested.getBalance({Substrate: bob.address})).to.be.equal(bobBalanceBeforeOp + amount);
- } else {
- if(nested instanceof UniqueFTCollection) {
- await nested.burnTokensFrom(unnester, targetNft.nestingAccount(), amount);
- } else {
- await nested.burnFrom(unnester, targetNft.nestingAccount(), amount);
- }
- }
-
- await checkNestedAmountState({
- expectedBalance: nestedBalanceBeforeOp - amount,
- childrenShouldPresent: shouldBeNestedAfterOp,
- nested,
- targetNft,
- });
- };
-
- // Initial setup: nest (fungibles/rft parts).
- // Check NFT's balance of nested assets and NFT's children.
- await nested.transfer(charlie, targetNft.nestingAccount(), totalAmount);
- await checkNestedAmountState({
- expectedBalance: totalAmount,
- childrenShouldPresent: true,
- nested,
- targetNft,
- });
-
- // Transfer/burn only a part of nested assets.
- // Check that NFT's balance of the nested assets correctly decreased and NFT's children are not changed.
- await doOperationAndCheck({
- amount: firstUnnestAmount,
- shouldBeNestedAfterOp: true,
- });
-
- // Transfer/burn all remaining nested assets.
- // Check that NFT's balance of the nested assets is 0 and NFT has no more children.
- await doOperationAndCheck({
- amount: restUnnestAmount,
- shouldBeNestedAfterOp: false,
- });
- }));
-
- ownerOrAdminUnnestCases(['nft']).map(testCase =>
- itSub(`[nft]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, async ({helper}) => {
- const owner = alice;
- const admin = bob;
-
- const unnester = (testCase.sender === 'owner')
- ? owner
- : admin;
-
- const collectionNFT = await helper.nft.mintCollection(owner);
- await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});
-
- const collectionNested = await helper.nft.mintCollection(owner, {
- limits: {
- ownerCanTransfer: true,
- },
- });
- await collectionNested.addAdmin(owner, {Substrate: admin.address});
-
- const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});
- const nested = await collectionNested.mintToken(owner, {Substrate: charlie.address});
-
- await nested.transfer(charlie, targetNft.nestingAccount());
- expect(await targetNft.getChildren()).to.be.deep.equal([{
- collectionId: nested.collectionId,
- tokenId: nested.tokenId,
- }]);
-
- if(testCase.op === 'transfer') {
- await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address});
- } else {
- await nested.burnFrom(unnester, targetNft.nestingAccount());
- }
-
- expect((await targetNft.getChildren()).length).to.be.equal(0);
- }));
-});
-
-describe('Negative Test: Unnesting', () => {
- let alice: IKeyringPair;
- let bob: IKeyringPair;
-
- before(async () => {
- await usingPlaygrounds(async (helper, privateKey) => {
- const donor = await privateKey({url: import.meta.url});
- [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);
- });
- });
-
- itSub('Disallows a non-owner to unnest/burn a token', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const targetToken = await collection.mintToken(alice);
-
- // Create a nested token
- const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());
-
- // Try to unnest
- await expect(nestedToken.unnest(bob, targetToken, {Substrate: alice.address})).to.be.rejectedWith(/common\.ApprovedValueTooLow/);
- expect(await nestedToken.getOwner()).to.be.deep.equal(CrossAccountId.toLowerCase(targetToken.nestingAccount()));
-
- // Try to burn
- await expect(nestedToken.burnFrom(bob, targetToken.nestingAccount())).to.be.rejectedWith(/common\.ApprovedValueTooLow/);
- expect(await nestedToken.getOwner()).to.be.deep.equal(CrossAccountId.toLowerCase(targetToken.nestingAccount()));
- });
-
- // todo another test for creating excessive depth matryoshka with Ethereum?
-
- // Recursive nesting
- itSub('Prevents Ouroboros creation', async ({helper}) => {
- const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
- const targetToken = await collection.mintToken(alice);
-
- // Fail to create a nested token ouroboros
- const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());
- await expect(targetToken.nest(alice, nestedToken)).to.be.rejectedWith(/^structure\.OuroborosDetected$/);
- });
-});
js-packages/tests/package.jsondiffbeforeafterboth--- a/js-packages/tests/package.json
+++ b/js-packages/tests/package.json
@@ -22,8 +22,8 @@
"testParallel": "yarn _testParallel './**/*.test.ts'",
"testSequential": "yarn _test './**/*.seqtest.ts'",
"testEth": "yarn _test ./**/eth/*.*test.ts",
- "testGovernance": "RUN_GOV_TESTS=1 yarn _test ./**/governance/*.*test.ts",
- "testCollators": "RUN_COLLATOR_TESTS=1 yarn _test ./**/collator-selection/**.*test.ts --timeout 49999999",
+ "testGovernance": "RUN_GOV_TESTS=1 yarn _test ./**/sub/governance/*.*test.ts",
+ "testCollators": "RUN_COLLATOR_TESTS=1 yarn _test ./**/sub/collator-selection/**.*test.ts --timeout 49999999",
"testFullXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Unique.test.ts",
"testFullXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Quartz.test.ts",
"testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts",
js-packages/tests/sub/check-event/burnItemEvent.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/check-event/burnItemEvent.test.ts
@@ -0,0 +1,44 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub} from '../../util/index.js';
+import type {IEvent} from '@unique/playgrounds/types.js';
+
+
+describe('Burn Item event ', () => {
+ let alice: IKeyringPair;
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([10n], donor);
+ });
+ });
+ itSub('Check event from burnItem(): ', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+ const token = await collection.mintToken(alice, {Substrate: alice.address});
+ await token.burn(alice);
+ await helper.wait.newBlocks(1);
+
+ const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
+ const eventStrings = event.map(e => `${e.section}.${e.method}`);
+
+ expect(eventStrings).to.contains('common.ItemDestroyed');
+ expect(eventStrings).to.contains('treasury.Deposit');
+ expect(eventStrings).to.contains('system.ExtrinsicSuccess');
+ });
+});
js-packages/tests/sub/check-event/createCollectionEvent.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/check-event/createCollectionEvent.test.ts
@@ -0,0 +1,40 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect} from '../../util/index.js';
+import type {IEvent} from '@unique/playgrounds/types.js';
+
+describe('Create collection event ', () => {
+ let alice: IKeyringPair;
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([10n], donor);
+ });
+ });
+ itSub('Check event from createCollection(): ', async ({helper}) => {
+ await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+ await helper.wait.newBlocks(1);
+ const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
+ const eventStrings = event.map(e => `${e.section}.${e.method}`);
+
+ expect(eventStrings).to.contains('common.CollectionCreated');
+ expect(eventStrings).to.contains('treasury.Deposit');
+ expect(eventStrings).to.contains('system.ExtrinsicSuccess');
+ });
+});
js-packages/tests/sub/check-event/createItemEvent.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/check-event/createItemEvent.test.ts
@@ -0,0 +1,41 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
+import type {IKeyringPair} from '@polkadot/types/types';
+import {itSub, usingPlaygrounds, expect} from '../../util/index.js';
+import type {IEvent} from '@unique/playgrounds/types.js';
+
+describe('Create Item event ', () => {
+ let alice: IKeyringPair;
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([10n], donor);
+ });
+ });
+ itSub('Check event from createItem(): ', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+ await collection.mintToken(alice, {Substrate: alice.address});
+ await helper.wait.newBlocks(1);
+ const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
+ const eventStrings = event.map(e => `${e.section}.${e.method}`);
+
+ expect(eventStrings).to.contains('common.ItemCreated');
+ expect(eventStrings).to.contains('treasury.Deposit');
+ expect(eventStrings).to.contains('system.ExtrinsicSuccess');
+ });
+});
js-packages/tests/sub/check-event/createMultipleItemsEvent.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/check-event/createMultipleItemsEvent.test.ts
@@ -0,0 +1,46 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect} from '../../util/index.js';
+import type {IEvent} from '@unique/playgrounds/types.js';
+
+describe('Create Multiple Items Event event ', () => {
+ let alice: IKeyringPair;
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([10n], donor);
+ });
+ });
+ itSub('Check event from createMultipleItems(): ', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+
+ await collection.mintMultipleTokens(alice, [
+ {owner: {Substrate: alice.address}},
+ {owner: {Substrate: alice.address}},
+ ]);
+
+ await helper.wait.newBlocks(1);
+ const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
+ const eventStrings = event.map(e => `${e.section}.${e.method}`);
+
+ expect(eventStrings).to.contains('common.ItemCreated');
+ expect(eventStrings).to.contains('treasury.Deposit');
+ expect(eventStrings).to.contains('system.ExtrinsicSuccess');
+ });
+});
js-packages/tests/sub/check-event/destroyCollectionEvent.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/check-event/destroyCollectionEvent.test.ts
@@ -0,0 +1,42 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
+import type {IKeyringPair} from '@polkadot/types/types';
+import {itSub, usingPlaygrounds, expect} from '../../util/index.js';
+import type {IEvent} from '@unique/playgrounds/types.js';
+
+describe('Destroy collection event ', () => {
+ let alice: IKeyringPair;
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([10n], donor);
+ });
+ });
+
+ itSub('Check event from destroyCollection(): ', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+ await collection.burn(alice);
+ await helper.wait.newBlocks(1);
+ const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
+ const eventStrings = event.map(e => `${e.section}.${e.method}`);
+
+ expect(eventStrings).to.contains('common.CollectionDestroyed');
+ expect(eventStrings).to.contains('treasury.Deposit');
+ expect(eventStrings).to.contains('system.ExtrinsicSuccess');
+ });
+});
js-packages/tests/sub/check-event/transferEvent.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/check-event/transferEvent.test.ts
@@ -0,0 +1,45 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub} from '../../util/index.js';
+import type {IEvent} from '@unique/playgrounds/types.js';
+
+describe('Transfer event ', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob] = await helper.arrange.createAccounts([10n, 10n], donor);
+ });
+ });
+
+ itSub('Check event from transfer(): ', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+ const token = await collection.mintToken(alice, {Substrate: alice.address});
+ await token.transfer(alice, {Substrate: bob.address});
+ await helper.wait.newBlocks(1);
+ const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
+ const eventStrings = event.map(e => `${e.section}.${e.method}`);
+
+ expect(eventStrings).to.contains('common.Transfer');
+ expect(eventStrings).to.contains('treasury.Deposit');
+ expect(eventStrings).to.contains('system.ExtrinsicSuccess');
+ });
+});
js-packages/tests/sub/check-event/transferFromEvent.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/check-event/transferFromEvent.test.ts
@@ -0,0 +1,44 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+// https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub} from '../../util/index.js';
+import type {IEvent} from '@unique/playgrounds/types.js';
+
+describe('Transfer event ', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob] = await helper.arrange.createAccounts([10n, 10n], donor);
+ });
+ });
+
+ itSub('Check event from transfer(): ', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {name: 'test', description: 'test', tokenPrefix: 'test'});
+ const token = await collection.mintToken(alice, {Substrate: alice.address});
+ await token.transferFrom(alice, {Substrate: alice.address}, {Substrate: bob.address});
+ await helper.wait.newBlocks(1);
+ const event = helper.chainLog[helper.chainLog.length - 1].events as IEvent[];
+ const eventStrings = event.map(e => `${e.section}.${e.method}`);
+
+ expect(eventStrings).to.contains('common.Transfer');
+ expect(eventStrings).to.contains('treasury.Deposit');
+ expect(eventStrings).to.contains('system.ExtrinsicSuccess');
+ });
+});
js-packages/tests/sub/collator-selection/collatorSelection.seqtest.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/collator-selection/collatorSelection.seqtest.ts
@@ -0,0 +1,423 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../../util/index.js';
+
+async function nodeAddress(name: string) {
+ // eslint-disable-next-line require-await
+ return await usingPlaygrounds(async (helper) => {
+ const envNodeStash = `RELAY_UNIQUE_NODE_${name.toUpperCase()}_STASH`;
+
+ const nodeStash = process.env[envNodeStash];
+ if(nodeStash) {
+ return helper.address.normalizeSubstrateToChainFormat(nodeStash);
+ } else {
+ throw Error(`"${envNodeStash}" env var is not set`);
+ }
+ });
+}
+
+async function getInitialInvulnerables() {
+ return await Promise.all([
+ nodeAddress('alpha'),
+ nodeAddress('beta'),
+ nodeAddress('gamma'),
+ nodeAddress('delta'),
+ ]);
+}
+
+async function resetInvulnerables() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const superuser = await privateKey('//Alice');
+ const initialInvulnerables = await getInitialInvulnerables();
+
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+
+ // Remove all invulnerables but the first one
+ const firstInvulnerable = invulnerables[0];
+
+ let nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(invulnerables.slice(1).map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [invulnerable], true, {nonce: nonce++})));
+
+ // Add the initial invulnerables
+ await Promise.all(initialInvulnerables.map(invulnerable => helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [invulnerable], true, {nonce: nonce++})));
+
+ // Remove the first invulnerable if it's not an initial one
+ if(!initialInvulnerables.includes(firstInvulnerable)) {
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [firstInvulnerable]);
+ }
+ });
+}
+
+// todo:collator Most preferable to launch this test in parallel somehow -- or change the session period (1 hr).
+describe('Integration Test: Collator Selection', () => {
+ let superuser: IKeyringPair;
+ let previousLicenseBond = 0n;
+ let licenseBond = 0n;
+
+ before(async function() {
+ if(!process.env.RUN_COLLATOR_TESTS) this.skip();
+
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.CollatorSelection]);
+ superuser = await privateKey('//Alice');
+
+ previousLicenseBond = await helper.collatorSelection.getLicenseBond();
+ licenseBond = 10n * helper.balance.getOneTokenNominal();
+ await helper.getSudo().collatorSelection.setLicenseBond(superuser, licenseBond);
+ });
+ });
+
+ describe('Dynamic shuffling of collators', () => {
+ // These two are the default invulnerables, and should return to be invulnerables after this suite.
+ let alphaNode: string;
+ let betaNode: string;
+
+ let gammaNode: string;
+ let deltaNode: string;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper) => {
+ // todo:collator see again if blocks start to be finalized in dev mode
+ // Skip the collator block production in dev mode, since the blocks are sealed automatically.
+ if(await helper.arrange.isDevNode()) this.skip();
+
+ [alphaNode, betaNode, gammaNode, deltaNode] = await getInitialInvulnerables();
+
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(invulnerables.length, 'Invalid initial invulnerables number').to.be.equal(4);
+ expect(invulnerables, 'Invalid initial invulnerables').containSubset([alphaNode, betaNode, gammaNode, deltaNode]);
+ });
+ });
+
+ itSub('Change invulnerables and make sure they start producing blocks', async ({helper}) => {
+ let nonce = await helper.chain.getNonce(superuser.address);
+
+ nonce = await helper.chain.getNonce(superuser.address);
+ await expect(Promise.all([
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [alphaNode], true, {nonce: nonce++}),
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [betaNode], true, {nonce: nonce++}),
+ ])).to.be.fulfilled;
+
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(newInvulnerables).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2);
+
+ await helper.wait.newSessions(2);
+
+ const newValidators = await helper.callRpc('api.query.session.validators');
+ expect(newValidators).to.contain(gammaNode).and.contain(deltaNode).and.be.length(2);
+
+ const lastBlockNumber = await helper.chain.getLatestBlockNumber();
+ await helper.wait.newBlocks(1);
+ const lastGammaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [gammaNode])).toNumber();
+ const lastDeltaBlock = (await helper.callRpc('api.query.collatorSelection.lastAuthoredBlock', [deltaNode])).toNumber();
+ expect(lastGammaBlock >= lastBlockNumber || lastDeltaBlock >= lastBlockNumber).to.be.true;
+ });
+
+ after(async () => {
+ await resetInvulnerables();
+ });
+ });
+
+ describe('Getting and releasing licenses to collate', () => {
+ let crowd: IKeyringPair[];
+
+ before(async function() {
+ await usingPlaygrounds(async (helper) => {
+ crowd = await helper.arrange.createCrowd(20, 100n, superuser);
+
+ // set session keys for everyone
+ await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));
+ });
+ });
+
+ describe('Positive', () => {
+ itSub('Can lease and release a license', async ({helper}) => {
+ const account = crowd.pop()!;
+
+ // make sure it does not have any reserved funds
+ expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(0n);
+
+ // getting a license reserves a license bond cost
+ await helper.collatorSelection.obtainLicense(account);
+ expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);
+ expect((await helper.balance.getSubstrateFull(account.address)).reserved).to.be.equal(licenseBond);
+
+ // releasing a license un-reserves the license bond cost
+ await helper.collatorSelection.releaseLicense(account);
+ expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);
+
+ const balance = await helper.balance.getSubstrateFull(account.address);
+ expect(balance.reserved).to.be.equal(0n);
+ expect(balance.free > 100n - licenseBond);
+ });
+
+ itSub('Can force revoke a license', async ({helper}) => {
+ const account = crowd.pop()!;
+
+ // getting a license reserves a license bond cost
+ const previousBalance = await helper.balance.getSubstrateFull(account.address);
+ await helper.collatorSelection.obtainLicense(account);
+ expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(licenseBond);
+
+ // force-releasing a license un-reserves the license bond cost as well
+ await helper.getSudo().collatorSelection.forceReleaseLicense(superuser, account.address);
+ expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(previousBalance.reserved);
+
+ const balance = await helper.balance.getSubstrateFull(account.address);
+ expect(balance.reserved).to.be.equal(previousBalance.reserved);
+ expect(balance.free > previousBalance.free - licenseBond);
+ });
+ });
+
+ describe('Negative', () => {
+ itSub('Cannot get a license without session keys set', async ({helper}) => {
+ const [account] = await helper.arrange.createAccounts([100n], superuser);
+ await expect(helper.collatorSelection.obtainLicense(account))
+ .to.be.rejectedWith(/collatorSelection.ValidatorNotRegistered/);
+ });
+
+ itSub('Cannot register a license twice', async ({helper}) => {
+ const account = crowd.pop()!;
+ await helper.collatorSelection.obtainLicense(account);
+ await expect(helper.collatorSelection.obtainLicense(account))
+ .to.be.rejectedWith(/collatorSelection.AlreadyHoldingLicense/);
+ });
+
+ itSub('Cannot release a license twice', async ({helper}) => {
+ const account = crowd.pop()!;
+ await helper.collatorSelection.obtainLicense(account);
+ await helper.collatorSelection.releaseLicense(account);
+ await expect(helper.collatorSelection.releaseLicense(account))
+ .to.be.rejectedWith(/collatorSelection.NoLicense/);
+ });
+
+ itSub('Cannot force revoke a license as non-sudo', async ({helper}) => {
+ const account = crowd.pop()!;
+ await helper.collatorSelection.obtainLicense(account);
+ await expect(helper.collatorSelection.forceReleaseLicense(superuser, account.address))
+ .to.be.rejectedWith(/BadOrigin/);
+ });
+ });
+ });
+
+ describe('Onboarding, collating, and offboarding as collator candidates', () => {
+ let crowd: IKeyringPair[];
+
+ before(async function() {
+ await usingPlaygrounds(async (helper) => {
+ crowd = await helper.arrange.createCrowd(20, 100n, superuser);
+
+ // set session keys for everyone
+ await Promise.all(crowd.map(acc => helper.session.setOwnKeysFromAddress(acc)));
+ });
+ });
+
+ describe('Positive', () => {
+ itSub('Can onboard and offboard repeatedly', async ({helper}) => {
+ const account = crowd.pop()!;
+ await helper.collatorSelection.obtainLicense(account);
+ await helper.collatorSelection.onboard(account);
+ expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);
+
+ await helper.collatorSelection.offboard(account);
+ expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);
+
+ await helper.collatorSelection.onboard(account);
+ expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([account.address]);
+
+ await helper.collatorSelection.offboard(account);
+ expect(await helper.collatorSelection.getCandidates()).to.be.deep.equal([]);
+ });
+
+ itSub('Penalizes and forfeits license from faulty collators', async ({helper}) => {
+ // This one shouldn't even be able to produce blocks.
+ const account = crowd.pop()!;
+ await helper.collatorSelection.obtainLicense(account);
+ await helper.collatorSelection.onboard(account);
+ expect(await helper.collatorSelection.getCandidates()).to.contain(account.address);
+
+ // Wait for 3 new sessions before checking that the collator will be kicked:
+ // one to get collator onboarded, and another two for the collator to fail
+ await helper.wait.newSessions(3);
+
+ expect(await helper.collatorSelection.getCandidates()).to.not.contain(account.address);
+ expect(await helper.collatorSelection.hasLicense(account.address)).to.be.equal(0n);
+
+ // The account's reserved funds get slashed as a penalty
+ const balance = await helper.balance.getSubstrateFull(account.address);
+ expect(balance.reserved).to.be.equal(0n);
+ expect(balance.free < 100n - licenseBond);
+ });
+ });
+
+ describe('Negative', () => {
+ itSub('Cannot onboard without a license', async ({helper}) => {
+ const account = crowd.pop()!;
+ await expect(helper.collatorSelection.onboard(account))
+ .to.be.rejectedWith(/collatorSelection.NoLicense/);
+ });
+
+ itSub('Cannot offboard without a license', async ({helper}) => {
+ const account = crowd.pop()!;
+ await expect(helper.collatorSelection.offboard(account))
+ .to.be.rejectedWith(/collatorSelection.NotCandidate/);
+ });
+
+ itSub('Cannot offboard while not onboarded', async ({helper}) => {
+ const account = crowd.pop()!;
+ await helper.collatorSelection.obtainLicense(account);
+ await expect(helper.collatorSelection.offboard(account))
+ .to.be.rejectedWith(/collatorSelection.NotCandidate/);
+ });
+
+ itSub('Cannot onboard while already onboarded', async ({helper}) => {
+ const account = crowd.pop()!;
+ await helper.collatorSelection.obtainLicense(account);
+ await helper.collatorSelection.onboard(account);
+ await expect(helper.collatorSelection.onboard(account))
+ .to.be.rejectedWith(/collatorSelection.AlreadyCandidate/);
+ });
+ });
+ });
+
+ describe('Addition and removal of invulnerables', () => {
+ describe('Positive', () => {
+ itSub('Adds an invulnerable', async ({helper}) => {
+ const [account] = await helper.arrange.createAccounts([10n], superuser);
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+
+ await helper.session.setOwnKeysFromAddress(account);
+ await helper.getSudo().collatorSelection.addInvulnerable(superuser, account.address);
+
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(invulnerables.concat(account.address)).to.have.all.members(newInvulnerables);
+ });
+
+ itSub('Removes an invulnerable', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ const lastInvulnerable = invulnerables.pop()!;
+
+ await helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable);
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ // invulnerables had its last element removed, so they should be equal
+ expect(newInvulnerables).to.have.all.members(invulnerables);
+ });
+ });
+
+ describe('Negative', () => {
+ itSub('Does not duplicate an invulnerable', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ // adding an already invulnerable should not fail, but should not duplicate it either
+ await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, invulnerables[0]))
+ .to.be.fulfilled;
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(newInvulnerables).to.have.all.members(invulnerables);
+ });
+
+ itSub('Cannot remove a non-existent invulnerable', async ({helper}) => {
+ const [account] = await helper.arrange.createAccounts([0n], superuser);
+ await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, account.address))
+ .to.be.rejectedWith(/collatorSelection.NotInvulnerable/);
+ });
+
+ itSub('Cannot allow invulnerables to be empty', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ const lastInvulnerable = invulnerables.pop()!;
+
+ let nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(invulnerables.map((i: any) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i], true, {nonce: nonce++})));
+
+ await expect(helper.getSudo().collatorSelection.removeInvulnerable(superuser, lastInvulnerable))
+ .to.be.rejectedWith(/collatorSelection.TooFewInvulnerables/);
+
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(newInvulnerables).to.be.deep.equal([lastInvulnerable]);
+
+ // restore the invulnerables to the previous state
+ nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(invulnerables.map((i: any) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i], true, {nonce: nonce++})));
+ });
+
+ itSub('Cannot have too many invulnerables', async ({helper}) => {
+ // todo:collator make sure that there is enough session time for a set of tests
+ // 28 non-functioning collators, teehee.
+
+ const invulnerablesLength = (await helper.collatorSelection.getInvulnerables()).length;
+ const invulnerablesUntilLimit = (await helper.collatorSelection.getDesiredCollators()) - invulnerablesLength;
+ const newInvulnerables = await helper.arrange.createAccounts(Array(invulnerablesUntilLimit).fill(10n), superuser);
+ const [lastInvulnerable] = await helper.arrange.createAccounts([10n], superuser);
+
+ await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
+ helper.session.setOwnKeysFromAddress(i)));
+ await helper.session.setOwnKeysFromAddress(lastInvulnerable);
+
+ let nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.addInvulnerable', [i.address], true, {nonce: nonce++})));
+
+ await expect(helper.getSudo().collatorSelection.addInvulnerable(superuser, lastInvulnerable.address))
+ .to.be.rejectedWith(/collatorSelection.TooManyInvulnerables/);
+
+ // restore the invulnerables to the previous state
+ nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(newInvulnerables.map((i: IKeyringPair) =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.removeInvulnerable', [i.address], true, {nonce: nonce++})));
+ });
+
+ itSub('Forbids a non-sudo to add an invulnerable', async ({helper}) => {
+ const [account] = await helper.arrange.createAccounts([10n], superuser);
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+
+ await helper.session.setOwnKeysFromAddress(account);
+ await expect(helper.collatorSelection.addInvulnerable(superuser, account.address))
+ .to.be.rejectedWith(/BadOrigin/);
+
+ const newInvulnerables = await helper.collatorSelection.getInvulnerables();
+ expect(newInvulnerables).to.be.members(invulnerables);
+ });
+
+ itSub('Forbids a non-sudo to remove an invulnerable', async ({helper}) => {
+ const invulnerables = await helper.collatorSelection.getInvulnerables();
+ await expect(helper.collatorSelection.removeInvulnerable(superuser, invulnerables[0]))
+ .to.be.rejectedWith(/BadOrigin/);
+ expect(await helper.collatorSelection.getInvulnerables()).to.have.all.members(invulnerables);
+ });
+
+ after(async function() {
+ await resetInvulnerables();
+ });
+ });
+ });
+
+ after(async function() {
+ if(!process.env.RUN_COLLATOR_TESTS) return;
+
+ await usingPlaygrounds(async (helper) => {
+ if(helper.fetchMissingPalletNames([Pallets.CollatorSelection]).length != 0) return;
+
+ await helper.getSudo().collatorSelection.setLicenseBond(superuser, previousLicenseBond);
+
+ const candidates = await helper.collatorSelection.getCandidates();
+ let nonce = await helper.chain.getNonce(superuser.address);
+ await Promise.all(candidates.map(candidate =>
+ helper.getSudo().executeExtrinsic(superuser, 'api.tx.collatorSelection.forceReleaseLicense', [candidate], true, {nonce: nonce++})));
+ });
+ });
+});
js-packages/tests/sub/collator-selection/identity.seqtest.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/collator-selection/identity.seqtest.ts
@@ -0,0 +1,284 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../../util/index.js';
+import {UniqueHelper} from '@unique/playgrounds/unique.js';
+
+async function getIdentities(helper: UniqueHelper) {
+ const identities: [string, any][] = [];
+ for(const [key, value] of await helper.getApi().query.identity.identityOf.entries())
+ identities.push([(key as any).toHuman(), (value as any).unwrap()]);
+ return identities;
+}
+
+async function getIdentityAccounts(helper: UniqueHelper) {
+ return (await getIdentities(helper)).flatMap(([key, _value]) => key);
+}
+
+async function getSubIdentityAccounts(helper: UniqueHelper, address: string) {
+ return ((await helper.getApi().query.identity.subsOf(address)).toHuman() as any)[1];
+}
+
+async function getSubIdentityName(helper: UniqueHelper, address: string) {
+ return ((await helper.getApi().query.identity.superOf(address)).toHuman() as any);
+}
+
+describe('Integration Test: Identities Manipulation', () => {
+ let superuser: IKeyringPair;
+
+ before(async function() {
+ if(!process.env.RUN_COLLATOR_TESTS) this.skip();
+
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.Identity]);
+ superuser = await privateKey('//Alice');
+ });
+ });
+
+ itSub('Normal calls do not work', async ({helper}) => {
+ // console.error = () => {};
+ await expect(helper.executeExtrinsic(superuser, 'api.tx.identity.setIdentity', [{info: {display: {Raw: 'Meowser'}}}] as any))
+ .to.be.rejectedWith(/Transaction call is not expected/);
+ });
+
+ describe('Identities', () => {
+ itSub('Sets identities', async ({helper}) => {
+ const oldIdentitiesCount = (await getIdentityAccounts(helper)).length;
+
+ const crowdSize = 10;
+ const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
+ const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]);
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
+
+ expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + crowdSize);
+ });
+
+ itSub('Setting identities does not delete existing but does overwrite', async ({helper}) => {
+ const crowd = await helper.arrange.createCrowd(10, 0n, superuser);
+ const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]);
+
+ // insert a single identity
+ let singleIdentity = identities.pop()!;
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [[singleIdentity]] as any);
+
+ const oldIdentitiesCount = (await getIdentityAccounts(helper)).length;
+
+ // change an identity and push it with a few new others
+ singleIdentity = [singleIdentity[0], {info: {display: {Raw: 'something special'}}}];
+ identities.push(singleIdentity);
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
+
+ // oldIdentitiesCount + 9 because one identity is overwritten, not inserted on top
+ expect((await getIdentityAccounts(helper)).length).to.be.equal(oldIdentitiesCount + 9);
+ expect((await helper.callRpc('api.query.identity.identityOf', [singleIdentity[0]])).toHuman().info.display)
+ .to.be.deep.equal({Raw: 'something special'});
+ });
+
+ itSub('Removes identities', async ({helper}) => {
+ const crowd = await helper.arrange.createCrowd(10, 0n, superuser);
+ const identities = crowd.map((acc, i) => [acc.address, {info: {display: {Raw: `accounter #${i}`}}}]);
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
+ const oldIdentities = await getIdentityAccounts(helper);
+
+ // delete a couple, check that they are no longer there
+ const scapegoats = [crowd.pop()!.address, crowd.pop()!.address];
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [scapegoats]);
+ const newIdentities = await getIdentityAccounts(helper);
+ expect(newIdentities.concat(scapegoats)).to.be.have.members(oldIdentities);
+ });
+ });
+
+ describe('Sub-identities', () => {
+ itSub('Sets subs', async ({helper}) => {
+ const crowdSize = 18;
+ const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
+ const supers = [crowd.pop()!, crowd.pop()!, crowd.pop()!];
+
+ const subsPerSup = crowd.length / supers.length;
+ let subCount = 0;
+ const subs = [
+ crowd.slice(subCount, subCount += subsPerSup + 1),
+ crowd.slice(subCount, subCount += subsPerSup),
+ crowd.slice(subCount, subCount += subsPerSup - 1),
+ ];
+
+ const subsInfo = supers.map((acc, i) => [
+ acc.address, [
+ 1000000n + BigInt(i + 1),
+ subs[i].map((sub, j) => [
+ sub.address, {Raw: `accounter #${j}`},
+ ]),
+ ],
+ ]);
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo] as any);
+
+ for(let i = 0; i < supers.length; i++) {
+ // check deposit
+ expect(((await helper.getApi().query.identity.subsOf(supers[i].address)).toJSON() as any)[0]).to.be.equal(1000001 + i);
+
+ const subsAccounts = await getSubIdentityAccounts(helper, supers[i].address);
+ // check sub-identities as account ids
+ expect(subsAccounts).to.include.members(subs[i].map(x => x.address));
+
+ for(let j = 0; j < subsAccounts.length; j++) {
+ // check sub-identities' names
+ expect((await getSubIdentityName(helper, subsAccounts[j]))[1]).to.be.deep.equal({Raw: `accounter #${j}`});
+ }
+ }
+ });
+
+ itSub('Setting sub-identities does not delete other existing but does overwrite own', async ({helper}) => {
+ const crowdSize = 18;
+ const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
+ const supers = [crowd.pop()!, crowd.pop()!, crowd.pop()!];
+
+ const subsPerSup = crowd.length / supers.length;
+ let subCount = 0;
+ const subs = [
+ crowd.slice(subCount, subCount += subsPerSup + 1),
+ crowd.slice(subCount, subCount += subsPerSup),
+ crowd.slice(subCount, subCount += subsPerSup - 1),
+ ];
+
+ const subsInfo1 = supers.map((acc, i) => [
+ acc.address, [
+ 1000000n + BigInt(i + 1),
+ subs[i].map((sub, j) => [
+ sub.address, {Raw: `accounter #${j}`},
+ ]),
+ ],
+ ]);
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo1] as any);
+
+ // change some sub-identities...
+ subs[2].pop(); subs[2].pop(); subs[2].push(...await helper.arrange.createAccounts([0n], superuser));
+
+ // ...and set them
+ const subsInfo2 = [[
+ supers[2].address, [
+ 999999n,
+ subs[2].map((sub, j) => [
+ sub.address, {Raw: `discounter #${j}`},
+ ]),
+ ],
+ ]];
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo2] as any);
+
+ // make sure everything else is the same
+ for(let i = 0; i < supers.length - 1; i++) {
+ // check deposit
+ expect(((await helper.getApi().query.identity.subsOf(supers[i].address)).toJSON() as any)[0]).to.be.equal(1000001 + i);
+
+ const subsAccounts = await getSubIdentityAccounts(helper, supers[i].address);
+ // check sub-identities as account ids
+ expect(subsAccounts).to.include.members(subs[i].map(x => x.address));
+
+ for(let j = 0; j < subsAccounts; j++) {
+ // check sub-identities' names
+ expect((await getSubIdentityName(helper, subsAccounts[j]))[1]).to.be.deep.equal({Raw: `accounter #${j}`});
+ }
+ }
+
+ // check deposit
+ expect(((await helper.getApi().query.identity.subsOf(supers[2].address)).toJSON() as any)[0]).to.be.equal(999999);
+
+ const subsAccounts = await getSubIdentityAccounts(helper, supers[2].address);
+ // check sub-identities as account ids
+ expect(subsAccounts).to.include.members(subs[2].map(x => x.address));
+
+ for(let j = 0; j < subsAccounts.length; j++) {
+ // check sub-identities' names
+ expect((await getSubIdentityName(helper, subsAccounts[j]))[1]).to.be.deep.equal({Raw: `discounter #${j}`});
+ }
+ });
+
+ itSub('Removes sub-identities', async ({helper}) => {
+ const crowdSize = 3;
+ const crowd = await helper.arrange.createCrowd(crowdSize, 0n, superuser);
+ const sup = crowd.pop()!;
+
+ const subsInfo1 = [[
+ sup.address, [
+ 1000000n,
+ crowd.map((sub, j) => [
+ sub.address, {Raw: `accounter #${j}`},
+ ]),
+ ],
+ ]];
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo1] as any);
+
+ // empty sub-identities should delete the records
+ const subsInfo2 = [[
+ sup.address, [
+ 1000000n,
+ [],
+ ],
+ ]];
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo2] as any);
+
+ // check deposit
+ expect((await helper.getApi().query.identity.subsOf(sup.address)).toHuman()).to.be.deep.equal(['0', []]);
+
+ for(let j = 0; j < crowd.length; j++) {
+ // check sub-identities' names
+ expect((await getSubIdentityName(helper, crowd[j].address))).to.be.null;
+ }
+ });
+
+ itSub('Removing identities deletes associated sub-identities', async ({helper}) => {
+ const crowd = await helper.arrange.createCrowd(3, 0n, superuser);
+ const sup = crowd.pop()!;
+
+ // insert identity
+ const identities = [[sup.address, {info: {display: {Raw: 'mental'}}}]];
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceInsertIdentities', [identities] as any);
+
+ // and its sub-identities
+ const subsInfo = [[
+ sup.address, [
+ 1000000n,
+ crowd.map((sub, j) => [
+ sub.address, {Raw: `accounter #${j}`},
+ ]),
+ ],
+ ]];
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceSetSubs', [subsInfo] as any);
+
+ // delete top identity
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [[sup.address]]);
+
+ // check that sub-identities are deleted
+ expect((await helper.getApi().query.identity.subsOf(sup.address)).toHuman()).to.be.deep.equal(['0', []]);
+
+ for(let j = 0; j < crowd.length; j++) {
+ // check sub-identities' names
+ expect((await getSubIdentityName(helper, crowd[j].address))).to.be.null;
+ }
+ });
+ });
+
+ after(async function() {
+ if(!process.env.RUN_COLLATOR_TESTS) return;
+
+ await usingPlaygrounds(async helper => {
+ if(helper.fetchMissingPalletNames([Pallets.Identity]).length != 0) return;
+
+ const identitiesToRemove: string[] = await getIdentityAccounts(helper);
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [identitiesToRemove]);
+ });
+ });
+});
js-packages/tests/sub/governance/council.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/governance/council.test.ts
@@ -0,0 +1,457 @@
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
+import {Event} from '@unique/playgrounds/unique.dev.js';
+import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin} from './util.js';
+import type {ICounselors} from './util.js';
+
+describeGov('Governance: Council tests', () => {
+ let donor: IKeyringPair;
+ let counselors: ICounselors;
+ let sudoer: IKeyringPair;
+
+ const moreThanHalfCouncilThreshold = 3;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.Council]);
+
+ donor = await privateKey({url: import.meta.url});
+ sudoer = await privateKey('//Alice');
+ });
+ });
+
+ beforeEach(async () => {
+ counselors = await initCouncil(donor, sudoer);
+ });
+
+ afterEach(async () => {
+ await clearCouncil(sudoer);
+ await clearTechComm(sudoer);
+ });
+
+ async function proposalFromMoreThanHalfCouncil(proposal: any) {
+ return await usingPlaygrounds(async (helper) => {
+ expect((await helper.callRpc('api.query.councilMembership.members')).toJSON().length).to.be.equal(5);
+ const proposeResult = await helper.council.collective.propose(
+ counselors.filip,
+ proposal,
+ moreThanHalfCouncilThreshold,
+ );
+
+ const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
+ const proposalIndex = councilProposedEvent.proposalIndex;
+ const proposalHash = councilProposedEvent.proposalHash;
+
+
+ await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);
+
+ return await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
+ });
+ }
+
+ async function proposalFromAllCouncil(proposal: any) {
+ return await usingPlaygrounds(async (helper) => {
+ expect((await helper.callRpc('api.query.councilMembership.members')).toJSON().length).to.be.equal(5);
+ const proposeResult = await helper.council.collective.propose(
+ counselors.filip,
+ proposal,
+ moreThanHalfCouncilThreshold,
+ );
+
+ const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
+ const proposalIndex = councilProposedEvent.proposalIndex;
+ const proposalHash = councilProposedEvent.proposalHash;
+
+
+ await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.ildar, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.irina, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);
+
+ return await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
+ });
+ }
+
+ itSub('>50% of Council can externally propose SuperMajorityAgainst', async ({helper}) => {
+ const forceSetBalanceReceiver = helper.arrange.createEmptyAccount();
+ const forceSetBalanceTestValue = 20n * 10n ** 25n;
+
+ const democracyProposal = await helper.constructApiCall('api.tx.balances.forceSetBalance', [
+ forceSetBalanceReceiver.address, forceSetBalanceTestValue,
+ ]);
+
+ const councilProposal = await helper.democracy.externalProposeDefaultCall(democracyProposal);
+
+ const proposeResult = await helper.council.collective.propose(
+ counselors.filip,
+ councilProposal,
+ moreThanHalfCouncilThreshold,
+ );
+
+ const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
+ const proposalIndex = councilProposedEvent.proposalIndex;
+ const proposalHash = councilProposedEvent.proposalHash;
+
+ await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.charu, proposalHash, proposalIndex, true);
+ await helper.council.collective.vote(counselors.filip, proposalHash, proposalIndex, true);
+
+ await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
+
+ const democracyStartedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const democracyReferendumIndex = democracyStartedEvent.referendumIndex;
+ const democracyThreshold = democracyStartedEvent.threshold;
+
+ expect(democracyThreshold).to.be.equal('SuperMajorityAgainst');
+
+ await helper.democracy.vote(counselors.filip, democracyReferendumIndex, {
+ Standard: {
+ vote: {
+ aye: true,
+ conviction: 1,
+ },
+ balance: 10_000n,
+ },
+ });
+
+ await helper.democracy.vote(counselors.charu, democracyReferendumIndex, {
+ Standard: {
+ vote: {
+ aye: false,
+ conviction: 1,
+ },
+ balance: 50_000n,
+ },
+ });
+
+ const passedReferendumEvent = await helper.wait.expectEvent(democracyVotingPeriod, Event.Democracy.Passed);
+ expect(passedReferendumEvent.referendumIndex).to.be.equal(democracyReferendumIndex);
+
+ await helper.wait.expectEvent(democracyEnactmentPeriod, Event.Scheduler.Dispatched);
+ const receiverBalance = await helper.balance.getSubstrate(forceSetBalanceReceiver.address);
+ expect(receiverBalance).to.be.equal(forceSetBalanceTestValue);
+ });
+
+ itSub('Council prime member vote is the default', async ({helper}) => {
+ const newTechCommMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address);
+ const proposeResult = await helper.council.collective.propose(
+ counselors.filip,
+ addMemberProposal,
+ moreThanHalfCouncilThreshold,
+ );
+
+ const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
+ const proposalIndex = councilProposedEvent.proposalIndex;
+ const proposalHash = councilProposedEvent.proposalHash;
+
+ await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
+
+ await helper.wait.newBlocks(councilMotionDuration);
+ const closeResult = await helper.council.collective.close(counselors.filip, proposalHash, proposalIndex);
+ const closeEvent = Event.Council.Closed.expect(closeResult);
+ const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON() as string[];
+ expect(closeEvent.yes).to.be.equal(members.length);
+ });
+
+ itSub('Superuser can add a member', async ({helper}) => {
+ const newMember = helper.arrange.createEmptyAccount();
+ await expect(helper.getSudo().council.membership.addMember(sudoer, newMember.address)).to.be.fulfilled;
+
+ const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
+ expect(members).to.contains(newMember.address);
+ });
+
+ itSub('Superuser can remove a member', async ({helper}) => {
+ await expect(helper.getSudo().council.membership.removeMember(sudoer, counselors.alex.address)).to.be.fulfilled;
+
+ const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
+ expect(members).to.not.contains(counselors.alex.address);
+ });
+
+ itSub('>50% Council can add TechComm member', async ({helper}) => {
+ const newTechCommMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address);
+
+ await proposalFromMoreThanHalfCouncil(addMemberProposal);
+
+ const techCommMembers = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
+ expect(techCommMembers).to.contains(newTechCommMember.address);
+ });
+
+ itSub('Council can remove TechComm member', async ({helper}) => {
+ const techComm = await initTechComm(donor, sudoer);
+ const removeMemberPrpoposal = helper.technicalCommittee.membership.removeMemberCall(techComm.andy.address);
+ await proposalFromMoreThanHalfCouncil(removeMemberPrpoposal);
+
+ const techCommMembers = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
+ expect(techCommMembers).to.not.contains(techComm.andy.address);
+ });
+
+ itSub.skip('Council member can add Fellowship member', async ({helper}) => {
+ const newFellowshipMember = helper.arrange.createEmptyAccount();
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.fellowship.collective.addMemberCall(newFellowshipMember.address),
+ )).to.be.fulfilled;
+ const fellowshipMembers = (await helper.callRpc('api.query.fellowshipCollective.members')).toJSON();
+ expect(fellowshipMembers).to.contains(newFellowshipMember.address);
+ });
+
+ itSub('>50% Council can promote Fellowship member', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const memberWithZeroRank = fellowship[0][0];
+
+ const proposal = helper.fellowship.collective.promoteCall(memberWithZeroRank.address);
+ await proposalFromMoreThanHalfCouncil(proposal);
+ const record = (await helper.callRpc('api.query.fellowshipCollective.members', [memberWithZeroRank.address])).toJSON();
+ expect(record).to.be.deep.equal({rank: 1});
+
+ await clearFellowship(sudoer);
+ });
+
+ itSub('>50% Council can demote Fellowship member', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const memberWithRankOne = fellowship[1][0];
+
+ const proposal = helper.fellowship.collective.demoteCall(memberWithRankOne.address);
+ await proposalFromMoreThanHalfCouncil(proposal);
+
+ const record = (await helper.callRpc('api.query.fellowshipCollective.members', [memberWithRankOne.address])).toJSON();
+ expect(record).to.be.deep.equal({rank: 0});
+
+ await clearFellowship(sudoer);
+ });
+
+ itSub('>50% Council can add\remove Fellowship member', async ({helper}) => {
+ try {
+ const newMember = helper.arrange.createEmptyAccount();
+
+ const proposalAdd = helper.fellowship.collective.addMemberCall(newMember.address);
+ const proposalRemove = helper.fellowship.collective.removeMemberCall(newMember.address, fellowshipRankLimit);
+ await expect(proposalFromMoreThanHalfCouncil(proposalAdd)).to.be.fulfilled;
+ expect(await helper.fellowship.collective.getMembers()).to.be.deep.contain(newMember.address);
+ await expect(proposalFromMoreThanHalfCouncil(proposalRemove)).to.be.fulfilled;
+ expect(await helper.fellowship.collective.getMembers()).to.be.not.deep.contain(newMember.address);
+ }
+ finally {
+ await clearFellowship(sudoer);
+ }
+ });
+
+ itSub('Council can blacklist Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await expect(proposalFromAllCouncil(helper.democracy.blacklistCall(preimageHash, null))).to.be.fulfilled;
+ });
+
+ itSub('Sudo can blacklist Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await expect(helper.getSudo().democracy.blacklist(sudoer, preimageHash)).to.be.fulfilled;
+ });
+
+ itSub('[Negative] Council cannot add Council member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
+
+ await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejected;
+ });
+
+ itSub('[Negative] Council cannot remove Council member', async ({helper}) => {
+ const removeMemberProposal = helper.council.membership.removeMemberCall(counselors.alex.address);
+
+ await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejected;
+ });
+
+ itSub('[Negative] Council cannot submit regular democracy proposal', async ({helper}) => {
+ const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
+
+ await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Council cannot externally propose SimpleMajority', async ({helper}) => {
+ const councilProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Council cannot externally propose SuperMajorityApprove', async ({helper}) => {
+ const councilProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Council member cannot add/remove a Council member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.council.membership.addMemberCall(newCouncilMember.address),
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.council.membership.removeMemberCall(counselors.charu.address),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council cannot set/clear Council prime member', async ({helper}) => {
+ const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
+ const proposalForClear = await helper.council.membership.clearPrimeCall();
+
+ await expect(proposalFromAllCouncil(proposalForSet)).to.be.rejectedWith(/BadOrigin/);
+ await expect(proposalFromAllCouncil(proposalForClear)).to.be.rejectedWith(/BadOrigin/);
+
+ });
+
+ itSub('[Negative] Council member cannot set/clear Council prime member', async ({helper}) => {
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.council.membership.setPrimeCall(counselors.charu.address),
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.council.membership.clearPrimeCall(),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council member cannot add/remove a TechComm member', async ({helper}) => {
+ const newTechCommMember = helper.arrange.createEmptyAccount();
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.technicalCommittee.membership.addMemberCall(newTechCommMember.address),
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.technicalCommittee.membership.removeMemberCall(newTechCommMember.address),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council member cannot promote/demote a Fellowship member', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const memberWithRankOne = fellowship[1][0];
+
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.fellowship.collective.promoteCall(memberWithRankOne.address),
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.fellowship.collective.demoteCall(memberWithRankOne.address),
+ )).to.be.rejectedWith('BadOrigin');
+ await clearFellowship(sudoer);
+ });
+
+ itSub('[Negative] Council cannot fast-track Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(proposalFromAllCouncil(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0)))
+ .to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Council member cannot fast-track Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council cannot cancel Democracy proposals', async ({helper}) => {
+ const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+ const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+ await expect(proposalFromAllCouncil(helper.democracy.cancelProposalCall(proposalIndex)))
+ .to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Council member cannot cancel Democracy proposals', async ({helper}) => {
+
+ const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+ const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.democracy.cancelProposalCall(proposalIndex),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council cannot cancel ongoing Democracy referendums', async ({helper}) => {
+ await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ await expect(proposalFromAllCouncil(helper.democracy.emergencyCancelCall(referendumIndex)))
+ .to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Council member cannot cancel ongoing Democracy referendums', async ({helper}) => {
+ await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.democracy.emergencyCancelCall(referendumIndex),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council cannot cancel Fellowship referendums', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const fellowshipProposer = fellowship[5][0];
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ fellowshipProposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+
+ await expect(proposalFromAllCouncil(helper.fellowship.referenda.cancelCall(referendumIndex)))
+ .to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Council member cannot cancel Fellowship referendums', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const fellowshipProposer = fellowship[5][0];
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ fellowshipProposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.fellowship.referenda.cancelCall(referendumIndex),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council referendum cannot be closed until the voting threshold is met', async ({helper}) => {
+ const councilSize = (await helper.callRpc('api.query.councilMembership.members')).toJSON().length as any as number;
+ expect(councilSize).is.greaterThan(1);
+ const proposeResult = await helper.council.collective.propose(
+ counselors.filip,
+ dummyProposalCall(helper),
+ councilSize,
+ );
+
+ const councilProposedEvent = Event.Council.Proposed.expect(proposeResult);
+ const proposalIndex = councilProposedEvent.proposalIndex;
+ const proposalHash = councilProposedEvent.proposalHash;
+
+
+ await helper.council.collective.vote(counselors.alex, proposalHash, proposalIndex, true);
+ await expect(helper.council.collective.close(counselors.filip, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly');
+ });
+
+});
js-packages/tests/sub/governance/democracy.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/governance/democracy.test.ts
@@ -0,0 +1,89 @@
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
+import {clearFellowship, democracyLaunchPeriod, democracyTrackMinRank, dummyProposalCall, fellowshipConfirmPeriod, fellowshipMinEnactPeriod, fellowshipPreparePeriod, fellowshipPropositionOrigin, initFellowship, voteUnanimouslyInFellowship} from './util.js';
+import {Event} from '@unique/playgrounds/unique.dev.js';
+
+describeGov('Governance: Democracy tests', () => {
+ let regularUser: IKeyringPair;
+ let donor: IKeyringPair;
+ let sudoer: IKeyringPair;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.Democracy]);
+
+ donor = await privateKey({url: import.meta.url});
+ sudoer = await privateKey('//Alice');
+
+ [regularUser] = await helper.arrange.createAccounts([1000n], donor);
+ });
+ });
+
+ itSub('Regular user can vote', async ({helper}) => {
+ const fellows = await initFellowship(donor, sudoer);
+ const rank1Proposer = fellows[1][0];
+
+ const democracyProposalCall = dummyProposalCall(helper);
+ const fellowshipProposal = {
+ Inline: helper.democracy.proposeCall(democracyProposalCall, 0n).method.toHex(),
+ };
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ rank1Proposer,
+ fellowshipPropositionOrigin,
+ fellowshipProposal,
+ {After: 0},
+ );
+
+ const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+ await voteUnanimouslyInFellowship(helper, fellows, democracyTrackMinRank, fellowshipReferendumIndex);
+ await helper.fellowship.referenda.placeDecisionDeposit(donor, fellowshipReferendumIndex);
+
+ await helper.wait.expectEvent(
+ fellowshipPreparePeriod + fellowshipConfirmPeriod + fellowshipMinEnactPeriod,
+ Event.Democracy.Proposed,
+ );
+
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ const ayeBalance = 10_000n;
+
+ await helper.democracy.vote(regularUser, referendumIndex, {
+ Standard: {
+ vote: {
+ aye: true,
+ conviction: 1,
+ },
+ balance: ayeBalance,
+ },
+ });
+
+ const referendumInfo = await helper.democracy.referendumInfo(referendumIndex);
+ const tally = referendumInfo.ongoing.tally;
+
+ expect(BigInt(tally.ayes)).to.be.equal(ayeBalance);
+
+ await clearFellowship(sudoer);
+ });
+
+ itSub('[Negative] Regular user cannot submit a regular proposal', async ({helper}) => {
+ const submitResult = helper.democracy.propose(regularUser, dummyProposalCall(helper), 0n);
+ await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Regular user cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+ const submitResult = helper.democracy.externalProposeDefault(regularUser, dummyProposalCall(helper));
+ await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Regular user cannot externally propose SimpleMajority', async ({helper}) => {
+ const submitResult = helper.democracy.externalProposeMajority(regularUser, dummyProposalCall(helper));
+ await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Regular user cannot externally propose SuperMajorityApprove', async ({helper}) => {
+ const submitResult = helper.democracy.externalPropose(regularUser, dummyProposalCall(helper));
+ await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
+ });
+});
js-packages/tests/sub/governance/fellowship.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/governance/fellowship.test.ts
@@ -0,0 +1,335 @@
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
+import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';
+import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, voteUnanimouslyInFellowship, democracyTrackMinRank, fellowshipPreparePeriod, fellowshipConfirmPeriod, fellowshipMinEnactPeriod, democracyTrackId, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js';
+import type {ICounselors, ITechComms} from './util.js';
+
+describeGov('Governance: Fellowship tests', () => {
+ let members: IKeyringPair[][];
+
+ let rank1Proposer: IKeyringPair;
+
+ let sudoer: any;
+ let donor: any;
+ let counselors: ICounselors;
+ let techcomms: ITechComms;
+
+ const submissionDeposit = 1000n;
+
+ async function testBadFellowshipProposal(
+ helper: DevUniqueHelper,
+ proposalCall: any,
+ ) {
+ const badProposal = {
+ Inline: proposalCall.method.toHex(),
+ };
+ const submitResult = await helper.fellowship.referenda.submit(
+ rank1Proposer,
+ fellowshipPropositionOrigin,
+ badProposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+ await voteUnanimouslyInFellowship(helper, members, democracyTrackMinRank, referendumIndex);
+ await helper.fellowship.referenda.placeDecisionDeposit(donor, referendumIndex);
+
+ const enactmentId = await helper.fellowship.referenda.enactmentEventId(referendumIndex);
+ const dispatchedEvent = await helper.wait.expectEvent(
+ fellowshipPreparePeriod + fellowshipConfirmPeriod + defaultEnactmentMoment.After,
+ Event.Scheduler.Dispatched,
+ (event: any) => event.id == enactmentId,
+ );
+
+ expect(dispatchedEvent.result.isErr, 'Bad Fellowship must fail')
+ .to.be.true;
+
+ expect(dispatchedEvent.result.asErr.isBadOrigin, 'Bad Fellowship must fail with BadOrigin')
+ .to.be.true;
+ }
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.Democracy, Pallets.Fellowship, Pallets.TechnicalCommittee, Pallets.Council]);
+
+ sudoer = await privateKey('//Alice');
+ donor = await privateKey({url: import.meta.url});
+ });
+
+ counselors = await initCouncil(donor, sudoer);
+ techcomms = await initTechComm(donor, sudoer);
+ members = await initFellowship(donor, sudoer);
+
+ rank1Proposer = members[1][0];
+ });
+
+ after(async () => {
+ await clearFellowship(sudoer);
+ await clearTechComm(sudoer);
+ await clearCouncil(sudoer);
+ await hardResetFellowshipReferenda(sudoer);
+ await hardResetDemocracy(sudoer);
+ await hardResetGovScheduler(sudoer);
+ });
+
+ itSub('FellowshipProposition can submit regular Democracy proposals', async ({helper}) => {
+ const democracyProposalCall = dummyProposalCall(helper);
+ const fellowshipProposal = {
+ Inline: helper.democracy.proposeCall(democracyProposalCall, 0n).method.toHex(),
+ };
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ rank1Proposer,
+ fellowshipPropositionOrigin,
+ fellowshipProposal,
+ defaultEnactmentMoment,
+ );
+
+ const fellowshipReferendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+ await voteUnanimouslyInFellowship(helper, members, democracyTrackMinRank, fellowshipReferendumIndex);
+ await helper.fellowship.referenda.placeDecisionDeposit(donor, fellowshipReferendumIndex);
+
+ const democracyProposed = await helper.wait.expectEvent(
+ fellowshipPreparePeriod + fellowshipConfirmPeriod + fellowshipMinEnactPeriod,
+ Event.Democracy.Proposed,
+ );
+
+ const democracyEnqueuedProposal = await helper.democracy.expectPublicProposal(democracyProposed.proposalIndex);
+ expect(democracyEnqueuedProposal.inline, 'Fellowship proposal expected to be in the Democracy')
+ .to.be.equal(democracyProposalCall.method.toHex());
+
+ await helper.wait.newBlocks(democracyVotingPeriod);
+ });
+
+ itSub('Fellowship (rank-1 or greater) member can submit Fellowship proposals on the Democracy track', async ({helper}) => {
+ for(let rank = 1; rank < fellowshipRankLimit; rank++) {
+ const rankMembers = members[rank];
+
+ for(let memberIdx = 0; memberIdx < rankMembers.length; memberIdx++) {
+ const member = rankMembers[memberIdx];
+ const newDummyProposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ member,
+ fellowshipPropositionOrigin,
+ newDummyProposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+ const referendumInfo = await helper.fellowship.referenda.referendumInfo(referendumIndex);
+ expect(referendumInfo.ongoing.track, `${memberIdx}-th member of rank #${rank}: proposal #${referendumIndex} is on invalid track`)
+ .to.be.equal(democracyTrackId);
+ }
+ }
+ });
+
+ itSub(`Fellowship (rank-${democracyTrackMinRank} or greater) members can vote on the Democracy track`, async ({helper}) => {
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ rank1Proposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+
+ let expectedAyes = 0;
+ for(let rank = democracyTrackMinRank; rank < fellowshipRankLimit; rank++) {
+ const rankMembers = members[rank];
+
+ for(let memberIdx = 0; memberIdx < rankMembers.length; memberIdx++) {
+ const member = rankMembers[memberIdx];
+ await helper.fellowship.collective.vote(member, referendumIndex, true);
+ expectedAyes += 1;
+
+ const referendumInfo = await helper.fellowship.referenda.referendumInfo(referendumIndex);
+ expect(referendumInfo.ongoing.tally.bareAyes, `Vote from ${memberIdx}-th member of rank #${rank} isn't accounted`)
+ .to.be.equal(expectedAyes);
+ }
+ }
+ });
+
+ itSub('Fellowship rank vote strength is correct', async ({helper}) => {
+ const excessRankWeightTable = [
+ 1,
+ 3,
+ 6,
+ 10,
+ 15,
+ 21,
+ ];
+
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ rank1Proposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+
+ for(let rank = democracyTrackMinRank; rank < fellowshipRankLimit; rank++) {
+ const rankMembers = members[rank];
+
+ for(let memberIdx = 0; memberIdx < rankMembers.length; memberIdx++) {
+ const member = rankMembers[memberIdx];
+
+ const referendumInfoBefore = await helper.fellowship.referenda.referendumInfo(referendumIndex);
+ const ayesBefore = referendumInfoBefore.ongoing.tally.ayes;
+
+ await helper.fellowship.collective.vote(member, referendumIndex, true);
+
+ const referendumInfoAfter = await helper.fellowship.referenda.referendumInfo(referendumIndex);
+ const ayesAfter = referendumInfoAfter.ongoing.tally.ayes;
+
+ const expectedVoteWeight = excessRankWeightTable[rank - democracyTrackMinRank];
+ const voteWeight = ayesAfter - ayesBefore;
+
+ expect(voteWeight, `Vote weight of ${memberIdx}-th member of rank #${rank} is invalid`)
+ .to.be.equal(expectedVoteWeight);
+ }
+ }
+ });
+
+ itSub('[Negative] FellowshipProposition cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+ await testBadFellowshipProposal(helper, helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper)));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot externally propose SimpleMajority', async ({helper}) => {
+ await testBadFellowshipProposal(helper, helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper)));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot externally propose SuperMajorityApprove', async ({helper}) => {
+ await testBadFellowshipProposal(helper, helper.democracy.externalProposeCall(dummyProposalCall(helper)));
+ });
+
+ itSub('[Negative] Fellowship (rank-0) member cannot submit Fellowship proposals on the Democracy track', async ({helper}) => {
+ const rank0Proposer = members[0][0];
+
+ const proposal = dummyProposal(helper);
+
+ const submitResult = helper.fellowship.referenda.submit(
+ rank0Proposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ await expect(submitResult).to.be.rejectedWith(/BadOrigin/);
+ });
+
+ itSub('[Negative] Fellowship (rank-1 or greater) member cannot submit if no deposit can be provided', async ({helper}) => {
+ const poorMember = rank1Proposer;
+
+ const balanceBefore = await helper.balance.getSubstrate(poorMember.address);
+ await helper.getSudo().balance.setBalanceSubstrate(sudoer, poorMember.address, submissionDeposit - 1n);
+
+ const proposal = dummyProposal(helper);
+
+ const submitResult = helper.fellowship.referenda.submit(
+ poorMember,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ await expect(submitResult).to.be.rejectedWith(/account balance too low/);
+
+ await helper.getSudo().balance.setBalanceSubstrate(sudoer, poorMember.address, balanceBefore);
+ });
+
+ itSub(`[Negative] Fellowship (rank-${democracyTrackMinRank - 1} or less) members cannot vote on the Democracy track`, async ({helper}) => {
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ rank1Proposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+
+ for(let rank = 0; rank < democracyTrackMinRank; rank++) {
+ for(const member of members[rank]) {
+ const voteResult = helper.fellowship.collective.vote(member, referendumIndex, true);
+ await expect(voteResult).to.be.rejectedWith(/RankTooLow/);
+ }
+ }
+ });
+
+ itSub('[Negative] FellowshipProposition cannot add/remove a Council member', async ({helper}) => {
+ const [councilNonMember] = await helper.arrange.createAccounts([10n], donor);
+
+ await testBadFellowshipProposal(helper, helper.council.membership.addMemberCall(councilNonMember.address));
+ await testBadFellowshipProposal(helper, helper.council.membership.removeMemberCall(counselors.ildar.address));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot set/clear Council prime member', async ({helper}) => {
+ await testBadFellowshipProposal(helper, helper.council.membership.setPrimeCall(counselors.ildar.address));
+ await testBadFellowshipProposal(helper, helper.council.membership.clearPrimeCall());
+ });
+
+ itSub('[Negative] FellowshipProposition cannot add/remove a TechComm member', async ({helper}) => {
+ const [techCommNonMember] = await helper.arrange.createAccounts([10n], donor);
+
+ await testBadFellowshipProposal(helper, helper.technicalCommittee.membership.addMemberCall(techCommNonMember.address));
+ await testBadFellowshipProposal(helper, helper.technicalCommittee.membership.removeMemberCall(techcomms.constantine.address));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot add/remove a Fellowship member', async ({helper}) => {
+ const [fellowshipNonMember] = await helper.arrange.createAccounts([10n], donor);
+
+ await testBadFellowshipProposal(helper, helper.fellowship.collective.addMemberCall(fellowshipNonMember.address));
+ await testBadFellowshipProposal(helper, helper.fellowship.collective.removeMemberCall(rank1Proposer.address, 1));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot promote/demote a Fellowship member', async ({helper}) => {
+ await testBadFellowshipProposal(helper, helper.fellowship.collective.promoteCall(rank1Proposer.address));
+ await testBadFellowshipProposal(helper, helper.fellowship.collective.demoteCall(rank1Proposer.address));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot fast-track Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await testBadFellowshipProposal(helper, helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot cancel Democracy proposals', async ({helper}) => {
+ const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+ const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+ await testBadFellowshipProposal(helper, helper.democracy.cancelProposalCall(proposalIndex));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot cancel ongoing Democracy referendums', async ({helper}) => {
+ await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ await testBadFellowshipProposal(helper, helper.democracy.emergencyCancelCall(referendumIndex));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot blacklist Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await testBadFellowshipProposal(helper, helper.democracy.blacklistCall(preimageHash, null));
+ });
+
+ itSub('[Negative] FellowshipProposition cannot veto external proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await testBadFellowshipProposal(helper, helper.democracy.vetoExternalCall(preimageHash));
+ });
+});
js-packages/tests/sub/governance/init.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/governance/init.test.ts
@@ -0,0 +1,193 @@
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
+import {Event} from '@unique/playgrounds/unique.dev.js';
+import {democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, clearCouncil, clearTechComm, clearFellowship} from './util.js';
+import type {ICounselors, ITechComms} from './util.js';
+
+describeGov('Governance: Initialization', () => {
+ let donor: IKeyringPair;
+ let sudoer: IKeyringPair;
+ let counselors: ICounselors;
+ let techcomms: ITechComms;
+ let coreDevs: any;
+
+ const expectedAlexFellowRank = 7;
+ const expectedFellowRank = 6;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.Democracy, Pallets.Council, Pallets.TechnicalCommittee]);
+
+ const councilMembers = await helper.council.membership.getMembers();
+ const techcommMembers = await helper.technicalCommittee.membership.getMembers();
+ expect(councilMembers.length == 0, 'The Council must be empty before the Gov Init');
+ expect(techcommMembers.length == 0, 'The Technical Commettee must be empty before the Gov Init');
+
+ donor = await privateKey({url: import.meta.url});
+ sudoer = await privateKey('//Alice');
+
+ const counselorsNum = 5;
+ const techCommsNum = 3;
+ const coreDevsNum = 2;
+ const [
+ alex,
+ ildar,
+ charu,
+ filip,
+ irina,
+
+ greg,
+ andy,
+ constantine,
+
+ yaroslav,
+ daniel,
+ ] = await helper.arrange.createAccounts(new Array(counselorsNum + techCommsNum + coreDevsNum).fill(10_000n), donor);
+
+ counselors = {
+ alex,
+ ildar,
+ charu,
+ filip,
+ irina,
+ };
+
+ techcomms = {
+ greg,
+ andy,
+ constantine,
+ };
+
+ coreDevs = {
+ yaroslav: yaroslav,
+ daniel: daniel,
+ };
+ });
+ });
+
+ itSub('Initialize Governance', async ({helper}) => {
+ const promoteFellow = (fellow: string, promotionsNum: number) => new Array(promotionsNum).fill(helper.fellowship.collective.promoteCall(fellow));
+
+ const expectFellowRank = async (fellow: string, expectedRank: number) => {
+ expect(await helper.fellowship.collective.getMemberRank(fellow)).to.be.equal(expectedRank);
+ };
+
+ console.log('\t- Setup the Prime of the Council via sudo');
+ await helper.getSudo().utility.batchAll(sudoer, [
+ helper.council.membership.addMemberCall(counselors.alex.address),
+ helper.council.membership.setPrimeCall(counselors.alex.address),
+
+ helper.fellowship.collective.addMemberCall(counselors.alex.address),
+ ...promoteFellow(counselors.alex.address, expectedAlexFellowRank),
+ ]);
+
+ let councilMembers = await helper.council.membership.getMembers();
+ const councilPrime = await helper.council.collective.getPrimeMember();
+ const alexFellowRank = await helper.fellowship.collective.getMemberRank(counselors.alex.address);
+ expect(councilMembers).to.be.deep.equal([counselors.alex.address]);
+ expect(councilPrime).to.be.equal(counselors.alex.address);
+ expect(alexFellowRank).to.be.equal(expectedAlexFellowRank);
+
+ console.log('\t- The Council Prime initializes the Technical Commettee');
+ const councilProposalThreshold = 1;
+
+ await helper.council.collective.propose(
+ counselors.alex,
+ helper.utility.batchAllCall([
+ helper.technicalCommittee.membership.addMemberCall(techcomms.greg.address),
+ helper.technicalCommittee.membership.addMemberCall(techcomms.andy.address),
+ helper.technicalCommittee.membership.addMemberCall(techcomms.constantine.address),
+
+ helper.technicalCommittee.membership.setPrimeCall(techcomms.greg.address),
+ ]),
+ councilProposalThreshold,
+ );
+
+ const techCommMembers = await helper.technicalCommittee.membership.getMembers();
+ const techCommPrime = await helper.technicalCommittee.membership.getPrimeMember();
+ const expectedTechComms = [techcomms.greg.address, techcomms.andy.address, techcomms.constantine.address];
+ expect(techCommMembers.length).to.be.equal(expectedTechComms.length);
+ expect(techCommMembers).to.containSubset(expectedTechComms);
+ expect(techCommPrime).to.be.equal(techcomms.greg.address);
+
+ console.log('\t- The Council Prime initiates a referendum to add counselors');
+ const returnPreimageHash = true;
+ const preimageHash = await helper.preimage.notePreimageFromCall(counselors.alex, helper.utility.batchAllCall([
+ helper.council.membership.addMemberCall(counselors.ildar.address),
+ helper.council.membership.addMemberCall(counselors.charu.address),
+ helper.council.membership.addMemberCall(counselors.filip.address),
+ helper.council.membership.addMemberCall(counselors.irina.address),
+
+ helper.fellowship.collective.addMemberCall(counselors.charu.address),
+ helper.fellowship.collective.addMemberCall(counselors.ildar.address),
+ helper.fellowship.collective.addMemberCall(counselors.irina.address),
+ helper.fellowship.collective.addMemberCall(counselors.filip.address),
+ helper.fellowship.collective.addMemberCall(techcomms.greg.address),
+ helper.fellowship.collective.addMemberCall(techcomms.andy.address),
+ helper.fellowship.collective.addMemberCall(techcomms.constantine.address),
+ helper.fellowship.collective.addMemberCall(coreDevs.yaroslav.address),
+ helper.fellowship.collective.addMemberCall(coreDevs.daniel.address),
+
+ ...promoteFellow(counselors.charu.address, expectedFellowRank),
+ ...promoteFellow(counselors.ildar.address, expectedFellowRank),
+ ...promoteFellow(counselors.irina.address, expectedFellowRank),
+ ...promoteFellow(counselors.filip.address, expectedFellowRank),
+ ...promoteFellow(techcomms.greg.address, expectedFellowRank),
+ ...promoteFellow(techcomms.andy.address, expectedFellowRank),
+ ...promoteFellow(techcomms.constantine.address, expectedFellowRank),
+ ...promoteFellow(coreDevs.yaroslav.address, expectedFellowRank),
+ ...promoteFellow(coreDevs.daniel.address, expectedFellowRank),
+ ]), returnPreimageHash);
+
+ await helper.council.collective.propose(
+ counselors.alex,
+ helper.democracy.externalProposeDefaultWithPreimageCall(preimageHash),
+ councilProposalThreshold,
+ );
+
+ console.log('\t- The referendum is being decided');
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+
+ await helper.democracy.vote(counselors.filip, startedEvent.referendumIndex, {
+ Standard: {
+ vote: {
+ aye: true,
+ conviction: 1,
+ },
+ balance: 10_000n,
+ },
+ });
+
+ const passedReferendumEvent = await helper.wait.expectEvent(democracyVotingPeriod, Event.Democracy.Passed);
+ expect(passedReferendumEvent.referendumIndex).to.be.equal(startedEvent.referendumIndex);
+
+ await helper.wait.expectEvent(democracyEnactmentPeriod, Event.Scheduler.Dispatched);
+
+ councilMembers = await helper.council.membership.getMembers();
+ const expectedCounselors = [
+ counselors.alex.address,
+ counselors.ildar.address,
+ counselors.charu.address,
+ counselors.filip.address,
+ counselors.irina.address,
+ ];
+ expect(councilMembers.length).to.be.equal(expectedCounselors.length);
+ expect(councilMembers).to.containSubset(expectedCounselors);
+
+ await expectFellowRank(counselors.ildar.address, expectedFellowRank);
+ await expectFellowRank(counselors.charu.address, expectedFellowRank);
+ await expectFellowRank(counselors.filip.address, expectedFellowRank);
+ await expectFellowRank(counselors.irina.address, expectedFellowRank);
+ await expectFellowRank(techcomms.greg.address, expectedFellowRank);
+ await expectFellowRank(techcomms.andy.address, expectedFellowRank);
+ await expectFellowRank(techcomms.constantine.address, expectedFellowRank);
+ await expectFellowRank(coreDevs.yaroslav.address, expectedFellowRank);
+ await expectFellowRank(coreDevs.daniel.address, expectedFellowRank);
+ });
+
+ after(async function() {
+ await clearFellowship(sudoer);
+ await clearTechComm(sudoer);
+ await clearCouncil(sudoer);
+ });
+});
js-packages/tests/sub/governance/technicalCommittee.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/governance/technicalCommittee.test.ts
@@ -0,0 +1,383 @@
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
+import {Event} from '@unique/playgrounds/unique.dev.js';
+import {initCouncil, democracyLaunchPeriod, democracyFastTrackVotingPeriod, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js';
+import type {ITechComms} from './util.js';
+
+describeGov('Governance: Technical Committee tests', () => {
+ let sudoer: IKeyringPair;
+ let techcomms: ITechComms;
+ let donor: IKeyringPair;
+ let preImageHash: string;
+
+
+ const allTechCommitteeThreshold = 3;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.TechnicalCommittee]);
+ sudoer = await privateKey('//Alice');
+ donor = await privateKey({url: import.meta.url});
+
+ techcomms = await initTechComm(donor, sudoer);
+
+ const proposalCall = await helper.constructApiCall('api.tx.balances.forceSetBalance', [donor.address, 20n * 10n ** 25n]);
+ preImageHash = await helper.preimage.notePreimageFromCall(sudoer, proposalCall, true);
+ });
+ });
+
+ after(async () => {
+ await usingPlaygrounds(async (helper) => {
+ await clearTechComm(sudoer);
+
+ await helper.preimage.unnotePreimage(sudoer, preImageHash);
+ await hardResetFellowshipReferenda(sudoer);
+ await hardResetDemocracy(sudoer);
+ await hardResetGovScheduler(sudoer);
+ });
+ });
+
+ function proposalFromAllCommittee(proposal: any) {
+ return usingPlaygrounds(async (helper) => {
+ expect((await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length).to.be.equal(allTechCommitteeThreshold);
+ const proposeResult = await helper.technicalCommittee.collective.propose(
+ techcomms.andy,
+ proposal,
+ allTechCommitteeThreshold,
+ );
+
+ const commiteeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult);
+ const proposalIndex = commiteeProposedEvent.proposalIndex;
+ const proposalHash = commiteeProposedEvent.proposalHash;
+
+
+ await helper.technicalCommittee.collective.vote(techcomms.andy, proposalHash, proposalIndex, true);
+ await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true);
+ await helper.technicalCommittee.collective.vote(techcomms.greg, proposalHash, proposalIndex, true);
+
+ const closeResult = await helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex);
+ Event.TechnicalCommittee.Closed.expect(closeResult);
+ Event.TechnicalCommittee.Approved.expect(closeResult);
+ const {result} = Event.TechnicalCommittee.Executed.expect(closeResult);
+ expect(result).to.eq('Ok');
+
+ return closeResult;
+ });
+ }
+
+ itSub('TechComm can fast-track Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.wait.parachainBlockMultiplesOf(35n);
+
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ const fastTrackProposal = await proposalFromAllCommittee(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0));
+ Event.Democracy.Started.expect(fastTrackProposal);
+ });
+
+ itSub('TechComm can cancel Democracy proposals', async ({helper}) => {
+ const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+ const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+ const cancelProposal = await proposalFromAllCommittee(helper.democracy.cancelProposalCall(proposalIndex));
+ Event.Democracy.ProposalCanceled.expect(cancelProposal);
+ });
+
+ itSub('TechComm can cancel ongoing Democracy referendums', async ({helper}) => {
+ await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ const emergencyCancelProposal = await proposalFromAllCommittee(helper.democracy.emergencyCancelCall(referendumIndex));
+ Event.Democracy.Cancelled.expect(emergencyCancelProposal);
+ });
+
+ itSub('TechComm member can veto Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ const vetoExternalCall = await helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.democracy.vetoExternalCall(preimageHash),
+ );
+ Event.Democracy.Vetoed.expect(vetoExternalCall);
+ });
+
+ itSub('TechComm can cancel Fellowship referendums', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const fellowshipProposer = fellowship[5][0];
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ fellowshipProposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+ const cancelProposal = await proposalFromAllCommittee(helper.fellowship.referenda.cancelCall(referendumIndex));
+ Event.FellowshipReferenda.Cancelled.expect(cancelProposal);
+ });
+
+ itSub.skip('TechComm member can add a Fellowship member', async ({helper}) => {
+ const newFellowshipMember = helper.arrange.createEmptyAccount();
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.fellowship.collective.addMemberCall(newFellowshipMember.address),
+ )).to.be.fulfilled;
+ const fellowshipMembers = (await helper.callRpc('api.query.fellowshipCollective.members')).toJSON();
+ expect(fellowshipMembers).to.contains(newFellowshipMember.address);
+ await clearFellowship(sudoer);
+ });
+
+ itSub('[Negative] TechComm cannot submit regular democracy proposal', async ({helper}) => {
+ const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
+
+ await expect(proposalFromAllCommittee(councilProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+ const commiteeProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm cannot externally propose SimpleMajority', async ({helper}) => {
+ const commiteeProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm cannot externally propose SuperMajorityApprove', async ({helper}) => {
+ const commiteeProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot submit regular democracy proposal', async ({helper}) => {
+ const memberProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+ const memberProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot externally propose SimpleMajority', async ({helper}) => {
+ const memberProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot externally propose SuperMajorityApprove', async ({helper}) => {
+ const memberProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+
+ itSub.skip('[Negative] TechComm cannot promote/demote Fellowship member', async () => {
+
+ });
+
+ itSub.skip('[Negative] TechComm member cannot promote/demote Fellowship member', async () => {
+
+ });
+
+ itSub('[Negative] TechComm cannot add/remove a Council member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
+
+ await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+ await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot add/remove a Council member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ addMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ removeMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm cannot set/clear Council prime member', async ({helper}) => {
+ const counselors = await initCouncil(donor, sudoer);
+ const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
+ const proposalForClear = await helper.council.membership.clearPrimeCall();
+
+ await expect(proposalFromAllCommittee(proposalForSet)).to.be.rejectedWith('BadOrigin');
+ await expect(proposalFromAllCommittee(proposalForClear)).to.be.rejectedWith('BadOrigin');
+ await clearCouncil(sudoer);
+ });
+
+ itSub('[Negative] TechComm member cannot set/clear Council prime member', async ({helper}) => {
+ const counselors = await initCouncil(donor, sudoer);
+ const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
+ const proposalForClear = await helper.council.membership.clearPrimeCall();
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ proposalForSet,
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ proposalForClear,
+ )).to.be.rejectedWith('BadOrigin');
+ await clearCouncil(sudoer);
+ });
+
+ itSub('[Negative] TechComm cannot add/remove a TechComm member', async ({helper}) => {
+ const newCommMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
+
+ await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+ await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot add/remove a TechComm member', async ({helper}) => {
+ const newCommMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ addMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ removeMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm cannot remove a Fellowship member', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+
+ await expect(proposalFromAllCommittee(helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5))).to.be.rejectedWith('BadOrigin');
+ await clearFellowship(sudoer);
+ });
+
+ itSub('[Negative] TechComm member cannot remove a Fellowship member', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5),
+ )).to.be.rejectedWith('BadOrigin');
+ await clearFellowship(sudoer);
+ });
+
+ itSub('[Negative] TechComm member cannot fast-track Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot cancel Democracy proposals', async ({helper}) => {
+ const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+ const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.democracy.cancelProposalCall(proposalIndex),
+ ))
+ .to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot cancel ongoing Democracy referendums', async ({helper}) => {
+ await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.democracy.emergencyCancelCall(referendumIndex),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm cannot blacklist Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(proposalFromAllCommittee(helper.democracy.blacklistCall(preimageHash))).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot blacklist Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.democracy.blacklistCall(preimageHash),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub.skip('[Negative] TechComm member cannot veto external Democracy proposals until the cool-off period pass', async () => {
+
+ });
+
+ itSub('[Negative] TechComm member cannot cancel Fellowship referendums', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const fellowshipProposer = fellowship[5][0];
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ fellowshipProposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.andy,
+ helper.fellowship.referenda.cancelCall(referendumIndex),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm referendum cannot be closed until the voting threshold is met', async ({helper}) => {
+ const committeeSize = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length as any as number;
+ expect(committeeSize).is.greaterThan(1);
+ const proposeResult = await helper.technicalCommittee.collective.propose(
+ techcomms.andy,
+ dummyProposalCall(helper),
+ committeeSize,
+ );
+
+ const committeeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult);
+ const proposalIndex = committeeProposedEvent.proposalIndex;
+ const proposalHash = committeeProposedEvent.proposalHash;
+
+ await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true);
+
+ await expect(helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly');
+ });
+});
js-packages/tests/sub/governance/util.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/governance/util.ts
@@ -0,0 +1,224 @@
+import type {IKeyringPair} from '@polkadot/types/types';
+import {xxhashAsHex} from '@polkadot/util-crypto';
+import type {u32} from '@polkadot/types-codec';
+import {usingPlaygrounds, expect} from '../../util/index.js';
+import {UniqueHelper} from '@unique/playgrounds/unique.js';
+import {DevUniqueHelper} from '@unique/playgrounds/unique.dev.js';
+
+export const democracyLaunchPeriod = 35;
+export const democracyVotingPeriod = 35;
+export const councilMotionDuration = 35;
+export const democracyEnactmentPeriod = 40;
+export const democracyFastTrackVotingPeriod = 5;
+
+export const fellowshipRankLimit = 7;
+export const fellowshipPropositionOrigin = 'FellowshipProposition';
+export const fellowshipPreparePeriod = 3;
+export const fellowshipConfirmPeriod = 3;
+export const fellowshipMinEnactPeriod = 1;
+
+export const defaultEnactmentMoment = {After: 0};
+
+export const democracyTrackId = 10;
+export const democracyTrackMinRank = 3;
+const twox128 = (data: any) => xxhashAsHex(data, 128);
+export interface ICounselors {
+ alex: IKeyringPair;
+ ildar: IKeyringPair;
+ charu: IKeyringPair;
+ filip: IKeyringPair;
+ irina: IKeyringPair;
+}
+export interface ITechComms {
+ greg: IKeyringPair;
+ andy: IKeyringPair;
+ constantine: IKeyringPair;
+}
+
+export async function initCouncil(donor: IKeyringPair, superuser: IKeyringPair) {
+ let counselors: IKeyringPair[] = [];
+
+ await usingPlaygrounds(async (helper) => {
+ const [alex, ildar, charu, filip, irina] = await helper.arrange.createAccounts([10_000n, 10_000n, 10_000n, 10_000n, 10_000n], donor);
+ const sudo = helper.getSudo();
+ {
+ const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON() as [];
+ if(members.length != 0) {
+ await clearCouncil(superuser);
+ }
+ }
+ const expectedMembers = [alex, ildar, charu, filip, irina];
+ for(const member of expectedMembers) {
+ await sudo.executeExtrinsic(superuser, 'api.tx.councilMembership.addMember', [member.address]);
+ }
+ await sudo.executeExtrinsic(superuser, 'api.tx.councilMembership.setPrime', [alex.address]);
+ {
+ const members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
+ expect(members).to.containSubset(expectedMembers.map((x: IKeyringPair) => x.address));
+ expect(members.length).to.be.equal(expectedMembers.length);
+ }
+
+ counselors = [alex, ildar, charu, filip, irina];
+ });
+ return {
+ alex: counselors[0],
+ ildar: counselors[1],
+ charu: counselors[2],
+ filip: counselors[3],
+ irina: counselors[4],
+ };
+}
+
+export async function clearCouncil(superuser: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ let members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
+ if(members.length) {
+ const sudo = helper.getSudo();
+ for(const address of members) {
+ await sudo.executeExtrinsic(superuser, 'api.tx.councilMembership.removeMember', [address]);
+ }
+ members = (await helper.callRpc('api.query.councilMembership.members')).toJSON();
+ }
+ expect(members).to.be.deep.equal([]);
+ });
+}
+
+
+export async function initTechComm(donor: IKeyringPair, superuser: IKeyringPair) {
+ let techcomms: IKeyringPair[] = [];
+
+ await usingPlaygrounds(async (helper) => {
+ const [greg, andy, constantine] = await helper.arrange.createAccounts([10_000n, 10_000n, 10_000n], donor);
+ const sudo = helper.getSudo();
+ {
+ const members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON() as [];
+ if(members.length != 0) {
+ await clearTechComm(superuser);
+ }
+ }
+ await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.addMember', [greg.address]);
+ await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.addMember', [andy.address]);
+ await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.addMember', [constantine.address]);
+ await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.setPrime', [greg.address]);
+ {
+ const members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
+ expect(members).to.containSubset([greg.address, andy.address, constantine.address]);
+ expect(members.length).to.be.equal(3);
+ }
+
+ techcomms = [greg, andy, constantine];
+ });
+
+ return {
+ greg: techcomms[0],
+ andy: techcomms[1],
+ constantine: techcomms[2],
+ };
+}
+
+export async function clearTechComm(superuser: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ let members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
+ if(members.length) {
+ const sudo = helper.getSudo();
+ for(const address of members) {
+ await sudo.executeExtrinsic(superuser, 'api.tx.technicalCommitteeMembership.removeMember', [address]);
+ }
+ members = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON();
+ }
+ expect(members).to.be.deep.equal([]);
+ });
+}
+
+export async function initFellowship(donor: IKeyringPair, sudoer: IKeyringPair) {
+ const numMembersInRank = 3;
+ const memberBalance = 5000n;
+ const members: IKeyringPair[][] = [];
+
+ await usingPlaygrounds(async (helper) => {
+ const currentFellows = await helper.getApi().query.fellowshipCollective.members.keys();
+
+ if(currentFellows.length != 0) {
+ await clearFellowship(sudoer);
+ }
+ for(let i = 0; i < fellowshipRankLimit; i++) {
+ const rankMembers = await helper.arrange.createAccounts(
+ Array(numMembersInRank).fill(memberBalance),
+ donor,
+ );
+
+ for(const member of rankMembers) {
+ await helper.getSudo().fellowship.collective.addMember(sudoer, member.address);
+
+ for(let rank = 0; rank < i; rank++) {
+ await helper.getSudo().fellowship.collective.promote(sudoer, member.address);
+ }
+ }
+
+ members.push(rankMembers);
+ }
+ });
+
+ return members;
+}
+
+export async function clearFellowship(sudoer: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ const fellowship = (await helper.getApi().query.fellowshipCollective.members.keys())
+ .map((key) => key.args[0].toString());
+ for(const member of fellowship) {
+ await helper.getSudo().fellowship.collective.removeMember(sudoer, member, fellowshipRankLimit);
+ }
+ });
+}
+
+export async function clearFellowshipReferenda(sudoer: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ const proposalsCount = (await helper.getApi().query.fellowshipReferenda.referendumCount()) as u32;
+ for(let i = 0; i < proposalsCount.toNumber(); i++) {
+ await helper.getSudo().fellowship.referenda.cancel(sudoer, i);
+ }
+ });
+}
+
+export async function hardResetFellowshipReferenda(sudoer: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ const api = helper.getApi();
+ const prefix = twox128('FellowshipReferenda');
+ await helper.signTransaction(sudoer, api.tx.sudo.sudo(api.tx.system.killPrefix(prefix, 100)));
+ });
+}
+
+export async function hardResetDemocracy(sudoer: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ const api = helper.getApi();
+ const prefix = twox128('Democracy');
+ await helper.signTransaction(sudoer, api.tx.sudo.sudo(api.tx.system.killPrefix(prefix, 100)));
+ });
+}
+
+export async function hardResetGovScheduler(sudoer: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ const api = helper.getApi();
+ const prefix = twox128('GovScheduler');
+ await helper.signTransaction(sudoer, api.tx.sudo.sudo(api.tx.system.killPrefix(prefix, 500)));
+ });
+}
+
+export async function voteUnanimouslyInFellowship(helper: DevUniqueHelper, fellows: IKeyringPair[][], minRank: number, referendumIndex: number) {
+ for(let rank = minRank; rank < fellowshipRankLimit; rank++) {
+ for(const member of fellows[rank]) {
+ await helper.fellowship.collective.vote(member, referendumIndex, true);
+ }
+ }
+}
+
+export function dummyProposalCall(helper: UniqueHelper) {
+ return helper.constructApiCall('api.tx.system.remark', ['dummy proposal' + (new Date()).getTime()]);
+}
+
+export function dummyProposal(helper: UniqueHelper) {
+ return {
+ Inline: dummyProposalCall(helper).method.toHex(),
+ };
+}
js-packages/tests/sub/nesting/collectionProperties.seqtest.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/nesting/collectionProperties.seqtest.ts
@@ -0,0 +1,61 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../../util/index.js';
+
+describe('Integration Test: Collection Properties with sudo', () => {
+ let superuser: IKeyringPair;
+ let alice: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ superuser = await privateKey('//Alice');
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([100n], donor);
+ });
+ });
+
+ [
+ {mode: 'nft' as const, requiredPallets: []},
+ {mode: 'ft' as const, requiredPallets: []},
+ {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+ ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {
+ before(async function() {
+ // eslint-disable-next-line require-await
+ await usingPlaygrounds(async helper => {
+ requirePalletsOrSkip(this, helper, testSuite.requiredPallets);
+ });
+ });
+
+ itSub('Repairing an unbroken collection\'s properties preserves the consumed space', async({helper}) => {
+ const properties = [
+ {key: 'sea-creatures', value: 'mermaids'},
+ {key: 'goldenratio', value: '1.6180339887498948482045868343656381177203091798057628621354486227052604628189'},
+ ];
+ const collection = await helper[testSuite.mode].mintCollection(alice, {properties});
+
+ const newProperty = {key: 'space', value: ' '.repeat(4096)};
+ await collection.setProperties(alice, [newProperty]);
+ const originalSpace = await collection.getPropertiesConsumedSpace();
+ expect(originalSpace).to.be.equal(sizeOfProperty(properties[0]) + sizeOfProperty(properties[1]) + sizeOfProperty(newProperty));
+
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.unique.forceRepairCollection', [collection.collectionId], true);
+ const recomputedSpace = await collection.getPropertiesConsumedSpace();
+ expect(recomputedSpace).to.be.equal(originalSpace);
+ });
+ }));
+});
js-packages/tests/sub/nesting/collectionProperties.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.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import type {IKeyringPair} from '@polkadot/types/types';18import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../../util/index.js';1920describe('Integration Test: Collection Properties', () => {21 let alice: IKeyringPair;22 let bob: IKeyringPair;2324 before(async () => {25 await usingPlaygrounds(async (helper, privateKey) => {26 const donor = await privateKey({url: import.meta.url});27 [alice, bob] = await helper.arrange.createAccounts([200n, 10n], donor);28 });29 });3031 itSub('Properties are initially empty', async ({helper}) => {32 const collection = await helper.nft.mintCollection(alice);33 expect(await collection.getProperties()).to.be.empty;34 });3536 [37 {mode: 'nft' as const, requiredPallets: []},38 {mode: 'ft' as const, requiredPallets: []},39 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},40 ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {41 before(async function() {42 // eslint-disable-next-line require-await43 await usingPlaygrounds(async helper => {44 requirePalletsOrSkip(this, helper, testSuite.requiredPallets);45 });46 });4748 itSub('Sets properties for a collection', async ({helper}) => {49 const collection = await helper[testSuite.mode].mintCollection(alice);5051 // As owner52 await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}])).to.be.fulfilled;5354 await collection.addAdmin(alice, {Substrate: bob.address});5556 // As administrator57 await expect(collection.setProperties(bob, [{key: 'black_hole'}])).to.be.fulfilled;5859 const properties = await collection.getProperties();60 expect(properties).to.include.deep.members([61 {key: 'electron', value: 'come bond'},62 {key: 'black_hole', value: ''},63 ]);64 });6566 itSub('Check valid names for collection properties keys', async ({helper}) => {67 const collection = await helper[testSuite.mode].mintCollection(alice);6869 // alpha symbols70 await expect(collection.setProperties(alice, [{key: 'answer'}])).to.be.fulfilled;7172 // numeric symbols73 await expect(collection.setProperties(alice, [{key: '451'}])).to.be.fulfilled;7475 // underscore symbol76 await expect(collection.setProperties(alice, [{key: 'black_hole'}])).to.be.fulfilled;7778 // dash symbol79 await expect(collection.setProperties(alice, [{key: '-'}])).to.be.fulfilled;8081 // dot symbol82 await expect(collection.setProperties(alice, [{key: 'once.in.a.long.long.while...', value: 'you get a little lost'}])).to.be.fulfilled;8384 const properties = await collection.getProperties();85 expect(properties).to.include.deep.members([86 {key: 'answer', value: ''},87 {key: '451', value: ''},88 {key: 'black_hole', value: ''},89 {key: '-', value: ''},90 {key: 'once.in.a.long.long.while...', value: 'you get a little lost'},91 ]);92 });9394 itSub('Changes properties of a collection', async ({helper}) => {95 const collection = await helper[testSuite.mode].mintCollection(alice);9697 await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: ''}])).to.be.fulfilled;9899 // Mutate the properties100 await expect(collection.setProperties(alice, [{key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;101102 const properties = await collection.getProperties();103 expect(properties).to.include.deep.members([104 {key: 'electron', value: 'come bond'},105 {key: 'black_hole', value: 'LIGO'},106 ]);107 });108109 itSub('Deletes properties of a collection', async ({helper}) => {110 const collection = await helper[testSuite.mode].mintCollection(alice);111112 await expect(collection.setProperties(alice, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}])).to.be.fulfilled;113114 await expect(collection.deleteProperties(alice, ['electron'])).to.be.fulfilled;115116 const properties = await collection.getProperties(['black_hole', 'electron']);117 expect(properties).to.be.deep.equal([118 {key: 'black_hole', value: 'LIGO'},119 ]);120 });121122 itSub('Allows modifying a collection property multiple times with the same size', async({helper}) => {123 const propKey = 'tok-prop';124125 const collection = await helper[testSuite.mode].mintCollection(alice);126127 const maxCollectionPropertiesSize = 40960;128129 const propDataSize = 4096;130131 let propDataChar = 'a';132 const makeNewPropData = () => {133 propDataChar = String.fromCharCode(propDataChar.charCodeAt(0) + 1);134 return `${propDataChar}`.repeat(propDataSize);135 };136137 const property = {key: propKey, value: makeNewPropData()};138 await collection.setProperties(alice, [property]);139 const originalSpace = await collection.getPropertiesConsumedSpace();140 expect(originalSpace).to.be.equal(sizeOfProperty(property));141142 const sameSizePropertiesPossibleNum = maxCollectionPropertiesSize / propDataSize;143144 // It is possible to modify a property as many times as needed.145 // It will not consume any additional space.146 for(let i = 0; i < sameSizePropertiesPossibleNum + 1; i++) {147 await collection.setProperties(alice, [{key: propKey, value: makeNewPropData()}]);148 const consumedSpace = await collection.getPropertiesConsumedSpace();149 expect(consumedSpace).to.be.equal(originalSpace);150 }151 });152153 itSub('Adding then removing a collection property doesn\'t change the consumed space', async({helper}) => {154 const propKey = 'tok-prop';155156 const collection = await helper[testSuite.mode].mintCollection(alice);157 const originalSpace = await collection.getPropertiesConsumedSpace();158159 const propDataSize = 4096;160 const propData = 'a'.repeat(propDataSize);161162 const property = {key: propKey, value: propData};163 await collection.setProperties(alice, [property]);164 let consumedSpace = await collection.getPropertiesConsumedSpace();165 expect(consumedSpace).to.be.equal(sizeOfProperty(property));166167 await collection.deleteProperties(alice, [propKey]);168 consumedSpace = await collection.getPropertiesConsumedSpace();169 expect(consumedSpace).to.be.equal(originalSpace);170 });171172 itSub('Modifying a collection property with different sizes correctly changes the consumed space', async({helper}) => {173 const propKey = 'tok-prop';174175 const collection = await helper[testSuite.mode].mintCollection(alice);176 const originalSpace = await collection.getPropertiesConsumedSpace();177178 const initProp = {key: propKey, value: 'a'.repeat(4096)};179 const biggerProp = {key: propKey, value: 'b'.repeat(5000)};180 const smallerProp = {key: propKey, value: 'c'.repeat(4000)};181182 let consumedSpace;183 let expectedConsumedSpaceDiff;184185 await collection.setProperties(alice, [initProp]);186 consumedSpace = await collection.getPropertiesConsumedSpace();187 expectedConsumedSpaceDiff = sizeOfProperty(initProp) - originalSpace;188 expect(consumedSpace).to.be.equal(originalSpace + expectedConsumedSpaceDiff);189190 await collection.setProperties(alice, [biggerProp]);191 consumedSpace = await collection.getPropertiesConsumedSpace();192 expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(initProp);193 expect(consumedSpace).to.be.equal(sizeOfProperty(initProp) + expectedConsumedSpaceDiff);194195 await collection.setProperties(alice, [smallerProp]);196 consumedSpace = await collection.getPropertiesConsumedSpace();197 expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(smallerProp);198 expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);199 });200 }));201});202203describe('Negative Integration Test: Collection Properties', () => {204 let alice: IKeyringPair;205 let bob: IKeyringPair;206207 before(async () => {208 await usingPlaygrounds(async (helper, privateKey) => {209 const donor = await privateKey({url: import.meta.url});210 [alice, bob] = await helper.arrange.createAccounts([1000n, 100n], donor);211 });212 });213214 [215 {mode: 'nft' as const, requiredPallets: []},216 {mode: 'ft' as const, requiredPallets: []},217 {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},218 ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {219 before(async function() {220 // eslint-disable-next-line require-await221 await usingPlaygrounds(async helper => {222 requirePalletsOrSkip(this, helper, testSuite.requiredPallets);223 });224 });225226 itSub('Fails to set properties in a collection if not its onwer/administrator', async ({helper}) => {227 const collection = await helper[testSuite.mode].mintCollection(alice);228229 await expect(collection.setProperties(bob, [{key: 'electron', value: 'come bond'}, {key: 'black_hole', value: 'LIGO'}]))230 .to.be.rejectedWith(/common\.NoPermission/);231232 expect(await collection.getProperties()).to.be.empty;233 });234235 itSub('Fails to set properties that exceed the limits', async ({helper}) => {236 const collection = await helper[testSuite.mode].mintCollection(alice);237238 const spaceLimit = (helper.getApi().consts.unique.maxCollectionPropertiesSize as any).toNumber();239240 // Mute the general tx parsing error, too many bytes to process241 {242 console.error = () => {};243 await expect(collection.setProperties(alice, [244 {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 9))},245 ])).to.be.rejected;246 }247248 expect(await collection.getProperties(['electron'])).to.be.empty;249250 await expect(collection.setProperties(alice, [251 {key: 'electron', value: 'low high '.repeat(Math.ceil(spaceLimit! / 18))},252 {key: 'black_hole', value: '0'.repeat(Math.ceil(spaceLimit! / 2))},253 ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);254255 expect(await collection.getProperties(['electron', 'black_hole'])).to.be.empty;256 });257258 itSub('Fails to set more properties than it is allowed', async ({helper}) => {259 const collection = await helper[testSuite.mode].mintCollection(alice);260261 const propertiesToBeSet = [];262 for(let i = 0; i < 65; i++) {263 propertiesToBeSet.push({264 key: 'electron_' + i,265 value: Math.random() > 0.5 ? 'high' : 'low',266 });267 }268269 await expect(collection.setProperties(alice, propertiesToBeSet)).270 to.be.rejectedWith(/common\.PropertyLimitReached/);271272 expect(await collection.getProperties()).to.be.empty;273 });274275 itSub('Fails to set properties with invalid names', async ({helper}) => {276 const collection = await helper[testSuite.mode].mintCollection(alice);277278 const invalidProperties = [279 [{key: 'electron', value: 'negative'}, {key: 'string theory', value: 'understandable'}],280 [{key: 'Mr/Sandman', value: 'Bring me a gene'}],281 [{key: 'déjà vu', value: 'hmm...'}],282 ];283284 for(let i = 0; i < invalidProperties.length; i++) {285 await expect(286 collection.setProperties(alice, invalidProperties[i]),287 `on rejecting the new badly-named property #${i}`,288 ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);289 }290291 await expect(292 collection.setProperties(alice, [{key: '', value: 'nothing must not exist'}]),293 'on rejecting an unnamed property',294 ).to.be.rejectedWith(/common\.EmptyPropertyKey/);295296 await expect(297 collection.setProperties(alice, [{key: 'CRISPR-Cas9', value: 'rewriting nature!'}]),298 'on setting the correctly-but-still-badly-named property',299 ).to.be.fulfilled;300301 const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat('CRISPR-Cas9').concat('');302303 const properties = await collection.getProperties(keys);304 expect(properties).to.be.deep.equal([305 {key: 'CRISPR-Cas9', value: 'rewriting nature!'},306 ]);307308 for(let i = 0; i < invalidProperties.length; i++) {309 await expect(310 collection.deleteProperties(alice, invalidProperties[i].map(propertySet => propertySet.key)),311 `on trying to delete the non-existent badly-named property #${i}`,312 ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);313 }314 });315316 itSub('Forbids to repair a collection if called with non-sudo', async({helper}) => {317 const collection = await helper[testSuite.mode].mintCollection(alice, {properties: [318 {key: 'sea-creatures', value: 'mermaids'},319 {key: 'goldenratio', value: '1.6180339887498948482045868343656381177203091798057628621354486227052604628189'},320 ]});321322 await expect(helper.executeExtrinsic(alice, 'api.tx.unique.forceRepairCollection', [collection.collectionId], true))323 .to.be.rejectedWith(/BadOrigin/);324 });325 }));326});js-packages/tests/sub/nesting/graphs.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/nesting/graphs.test.ts
@@ -0,0 +1,86 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {expect, itSub, usingPlaygrounds} from '../../util/index.js';
+import {UniqueHelper, UniqueNFTCollection, UniqueNFToken} from '@unique/playgrounds/unique.js';
+
+/**
+ * ```dot
+ * 4 -> 3 -> 2 -> 1
+ * 7 -> 6 -> 5 -> 2
+ * 8 -> 5
+ * ```
+ */
+async function buildComplexObjectGraph(helper: UniqueHelper, sender: IKeyringPair): Promise<[UniqueNFTCollection,UniqueNFToken[]]> {
+ const collection = await helper.nft.mintCollection(sender, {permissions: {nesting: {tokenOwner: true}}});
+ const tokens = await collection.mintMultipleTokens(sender, Array(8).fill({owner: {Substrate: sender.address}}));
+
+ await tokens[7].nest(sender, tokens[4]);
+ await tokens[6].nest(sender, tokens[5]);
+ await tokens[5].nest(sender, tokens[4]);
+ await tokens[4].nest(sender, tokens[1]);
+ await tokens[3].nest(sender, tokens[2]);
+ await tokens[2].nest(sender, tokens[1]);
+ await tokens[1].nest(sender, tokens[0]);
+
+ return [collection, tokens];
+}
+
+describe('Graphs', () => {
+ let alice: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([10n], donor);
+ });
+ });
+
+ itSub('Ouroboros can\'t be created in a complex graph', async ({helper}) => {
+ const [collection, tokens] = await buildComplexObjectGraph(helper, alice);
+
+ await collection.setPermissions(alice, {nesting: {collectionAdmin: false, tokenOwner: true}});
+
+ // [token owner] to self
+ await expect(
+ tokens[0].nest(alice, tokens[0]),
+ '[token owner] self-nesting is forbidden',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+ // [token owner] to nested part of graph
+ await expect(
+ tokens[0].nest(alice, tokens[4]),
+ '[token owner] cannot nest the root node into an internal node',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+ await expect(
+ tokens[1].transferFrom(alice, tokens[0].nestingAccount(), tokens[7].nestingAccount()),
+ '[token owner] cannot nest higher internal node into lower internal node',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+
+ await collection.setPermissions(alice, {nesting: {collectionAdmin: true, tokenOwner: false}});
+
+ // [collection owner] to self
+ await expect(
+ tokens[0].nest(alice, tokens[0]),
+ '[collection owner] self-nesting is forbidden',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+ // [collection owner] to nested part of graph
+ await expect(
+ tokens[0].nest(alice, tokens[4]),
+ '[collection owner] cannot nest the root node into an internal node',
+ ).to.be.rejectedWith(/structure\.OuroborosDetected/);
+ });
+});
js-packages/tests/sub/nesting/propertyPermissions.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/nesting/propertyPermissions.test.ts
@@ -0,0 +1,198 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {itSub, Pallets, usingPlaygrounds, expect} from '../../util/index.js';
+import {UniqueNFTCollection, UniqueRFTCollection} from '@unique/playgrounds/unique.js';
+
+describe('Integration Test: Access Rights to Token Properties', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob] = await helper.arrange.createAccounts([100n, 10n], donor);
+ });
+ });
+
+ itSub('Reads access rights to properties of a collection', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice);
+ const propertyRights = (await helper.callRpc('api.query.common.collectionPropertyPermissions', [collection.collectionId])).toJSON();
+ expect(propertyRights).to.be.empty;
+ });
+
+ async function testSetsAccessRightsToProperties(collection: UniqueNFTCollection | UniqueRFTCollection) {
+ await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true}}]))
+ .to.be.fulfilled;
+
+ await collection.addAdmin(alice, {Substrate: bob.address});
+
+ await expect(collection.setTokenPropertyPermissions(bob, [{key: 'mindgame', permission: {collectionAdmin: true, tokenOwner: false}}]))
+ .to.be.fulfilled;
+
+ const propertyRights = await collection.getPropertyPermissions(['skullduggery', 'mindgame']);
+ expect(propertyRights).to.include.deep.members([
+ {key: 'skullduggery', permission: {mutable: true, collectionAdmin: false, tokenOwner: false}},
+ {key: 'mindgame', permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},
+ ]);
+ }
+
+ itSub('Sets access rights to properties of a collection (NFT)', async ({helper}) => {
+ await testSetsAccessRightsToProperties(await helper.nft.mintCollection(alice));
+ });
+
+ itSub.ifWithPallets('Sets access rights to properties of a collection (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ await testSetsAccessRightsToProperties(await helper.rft.mintCollection(alice));
+ });
+
+ async function testChangesAccessRightsToProperty(collection: UniqueNFTCollection | UniqueRFTCollection) {
+ await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: true, collectionAdmin: true}}]))
+ .to.be.fulfilled;
+
+ await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))
+ .to.be.fulfilled;
+
+ const propertyRights = await collection.getPropertyPermissions();
+ expect(propertyRights).to.be.deep.equal([
+ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
+ ]);
+ }
+
+ itSub('Changes access rights to properties of a NFT collection', async ({helper}) => {
+ await testChangesAccessRightsToProperty(await helper.nft.mintCollection(alice));
+ });
+
+ itSub.ifWithPallets('Changes access rights to properties of a ReFungible collection', [Pallets.ReFungible], async ({helper}) => {
+ await testChangesAccessRightsToProperty(await helper.rft.mintCollection(alice));
+ });
+});
+
+describe('Negative Integration Test: Access Rights to Token Properties', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);
+ });
+ });
+
+ async function testPreventsFromSettingAccessRightsNotAdminOrOwner(collection: UniqueNFTCollection | UniqueRFTCollection) {
+ await expect(collection.setTokenPropertyPermissions(bob, [{key: 'skullduggery', permission: {mutable: true, tokenOwner: true}}]))
+ .to.be.rejectedWith(/common\.NoPermission/);
+
+ const propertyRights = await collection.getPropertyPermissions(['skullduggery']);
+ expect(propertyRights).to.be.empty;
+ }
+
+ itSub('Prevents from setting access rights to properties of a NFT collection if not an onwer/admin', async ({helper}) => {
+ await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.nft.mintCollection(alice));
+ });
+
+ itSub.ifWithPallets('Prevents from setting access rights to properties of a ReFungible collection if not an onwer/admin', [Pallets.ReFungible], async ({helper}) => {
+ await testPreventsFromSettingAccessRightsNotAdminOrOwner(await helper.rft.mintCollection(alice));
+ });
+
+ async function testPreventFromAddingTooManyPossibleProperties(collection: UniqueNFTCollection | UniqueRFTCollection) {
+ const constitution = [];
+ for(let i = 0; i < 65; i++) {
+ constitution.push({
+ key: 'property_' + i,
+ permission: Math.random() > 0.5 ? {mutable: true, collectionAdmin: true, tokenOwner: true} : {},
+ });
+ }
+
+ await expect(collection.setTokenPropertyPermissions(alice, constitution))
+ .to.be.rejectedWith(/common\.PropertyLimitReached/);
+
+ const propertyRights = await collection.getPropertyPermissions();
+ expect(propertyRights).to.be.empty;
+ }
+
+ itSub('Prevents from adding too many possible properties (NFT)', async ({helper}) => {
+ await testPreventFromAddingTooManyPossibleProperties(await helper.nft.mintCollection(alice));
+ });
+
+ itSub.ifWithPallets('Prevents from adding too many possible properties (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ await testPreventFromAddingTooManyPossibleProperties(await helper.rft.mintCollection(alice));
+ });
+
+ async function testPreventAccessRightsModifiedIfConstant(collection: UniqueNFTCollection | UniqueRFTCollection) {
+ await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {mutable: false, tokenOwner: true}}]))
+ .to.be.fulfilled;
+
+ await expect(collection.setTokenPropertyPermissions(alice, [{key: 'skullduggery', permission: {collectionAdmin: true}}]))
+ .to.be.rejectedWith(/common\.NoPermission/);
+
+ const propertyRights = await collection.getPropertyPermissions(['skullduggery']);
+ expect(propertyRights).to.deep.equal([
+ {key: 'skullduggery', permission: {'mutable': false, 'collectionAdmin': false, 'tokenOwner': true}},
+ ]);
+ }
+
+ itSub('Prevents access rights to be modified if constant (NFT)', async ({helper}) => {
+ await testPreventAccessRightsModifiedIfConstant(await helper.nft.mintCollection(alice));
+ });
+
+ itSub.ifWithPallets('Prevents access rights to be modified if constant (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ await testPreventAccessRightsModifiedIfConstant(await helper.rft.mintCollection(alice));
+ });
+
+ async function testPreventsAddingPropertiesWithInvalidNames(collection: UniqueNFTCollection | UniqueRFTCollection) {
+ const invalidProperties = [
+ [{key: 'skullduggery', permission: {tokenOwner: true}}, {key: 'im possible', permission: {collectionAdmin: true}}],
+ [{key: 'G#4', permission: {tokenOwner: true}}],
+ [{key: 'HÆMILTON', permission: {mutable: false, collectionAdmin: true, tokenOwner: true}}],
+ ];
+
+ for(let i = 0; i < invalidProperties.length; i++) {
+ await expect(
+ collection.setTokenPropertyPermissions(alice, invalidProperties[i]),
+ `on setting the new badly-named property #${i}`,
+ ).to.be.rejectedWith(/common\.InvalidCharacterInPropertyKey/);
+ }
+
+ await expect(
+ collection.setTokenPropertyPermissions(alice, [{key: '', permission: {}}]),
+ 'on rejecting an unnamed property',
+ ).to.be.rejectedWith(/common\.EmptyPropertyKey/);
+
+ const correctKey = '--0x03116e387820CA05'; // PolkadotJS would parse this as an already encoded hex-string
+ await expect(
+ collection.setTokenPropertyPermissions(alice, [
+ {key: correctKey, permission: {collectionAdmin: true}},
+ ]),
+ 'on setting the correctly-but-still-badly-named property',
+ ).to.be.fulfilled;
+
+ const keys = invalidProperties.flatMap(propertySet => propertySet.map(property => property.key)).concat(correctKey).concat('');
+
+ const propertyRights = await collection.getPropertyPermissions(keys);
+ expect(propertyRights).to.be.deep.equal([
+ {key: correctKey, permission: {mutable: false, collectionAdmin: true, tokenOwner: false}},
+ ]);
+ }
+
+ itSub('Prevents adding properties with invalid names (NFT)', async ({helper}) => {
+ await testPreventsAddingPropertiesWithInvalidNames(await helper.nft.mintCollection(alice));
+ });
+
+ itSub.ifWithPallets('Prevents adding properties with invalid names (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ await testPreventsAddingPropertiesWithInvalidNames(await helper.rft.mintCollection(alice));
+ });
+});
js-packages/tests/sub/nesting/tokenProperties.seqtest.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/nesting/tokenProperties.seqtest.ts
@@ -0,0 +1,71 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../../util/index.js';
+
+describe('Integration Test: Token Properties with sudo', () => {
+ let superuser: IKeyringPair;
+ let alice: IKeyringPair; // collection owner
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ superuser = await privateKey('//Alice');
+ const donor = await privateKey({url: import.meta.url});
+ [alice] = await helper.arrange.createAccounts([100n], donor);
+ });
+ });
+
+ [
+ {mode: 'nft' as const, pieces: undefined, requiredPallets: []} as const,
+ {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]} as const,
+ ].map(testSuite => describe(`${testSuite.mode.toUpperCase()}`, () => {
+ before(async function() {
+ // eslint-disable-next-line require-await
+ await usingPlaygrounds(async helper => {
+ requirePalletsOrSkip(this, helper, testSuite.requiredPallets);
+ });
+ });
+
+ itSub('force_repair_item preserves valid consumed space', async({helper}) => {
+ const propKey = 'tok-prop';
+
+ const collection = await helper[testSuite.mode].mintCollection(alice, {
+ tokenPropertyPermissions: [
+ {
+ key: propKey,
+ permission: {mutable: true, tokenOwner: true},
+ },
+ ],
+ });
+ const token = await (
+ testSuite.pieces
+ ? collection.mintToken(alice, testSuite.pieces as any)
+ : collection.mintToken(alice)
+ );
+
+ const prop = {key: propKey, value: 'a'.repeat(4096)};
+
+ await token.setProperties(alice, [prop]);
+ const originalSpace = await token.getTokenPropertiesConsumedSpace();
+ expect(originalSpace).to.be.equal(sizeOfProperty(prop));
+
+ await helper.getSudo().executeExtrinsic(superuser, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true);
+ const recomputedSpace = await token.getTokenPropertiesConsumedSpace();
+ expect(recomputedSpace).to.be.equal(originalSpace);
+ });
+ }));
+});
js-packages/tests/sub/nesting/tokenProperties.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/nesting/tokenProperties.test.ts
@@ -0,0 +1,816 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect, sizeOfProperty} from '../../util/index.js';
+import {UniqueHelper, UniqueNFToken, UniqueRFToken} from '@unique/playgrounds/unique.js';
+
+describe('Integration Test: Token Properties', () => {
+ let alice: IKeyringPair; // collection owner
+ let bob: IKeyringPair; // collection admin
+ let charlie: IKeyringPair; // token owner
+
+ let permissions: {permission: any, signers: IKeyringPair[]}[];
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 100n, 100n], donor);
+ });
+
+ permissions = [
+ {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob]},
+ {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob]},
+ {permission: {mutable: true, tokenOwner: true}, signers: [charlie]},
+ {permission: {mutable: false, tokenOwner: true}, signers: [charlie]},
+ {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},
+ {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie]},
+ ];
+ });
+
+ async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {
+ const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {
+ tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
+ signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
+ });
+ return mode == 'NFT' ? [await collection.mintToken(alice), 1n] : [await collection.mintToken(alice, 100n as any), 100n];
+ }
+
+ async function testReadsYetEmptyProperties(token: UniqueNFToken | UniqueRFToken) {
+ const properties = await token.getProperties();
+ expect(properties).to.be.empty;
+
+ const tokenData = await token.getData();
+ expect(tokenData!.properties).to.be.empty;
+ }
+
+ itSub('Reads yet empty properties of a token (NFT)', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice);
+ const token = await collection.mintToken(alice);
+ await testReadsYetEmptyProperties(token);
+ });
+
+ itSub.ifWithPallets('Reads yet empty properties of a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const collection = await helper.rft.mintCollection(alice);
+ const token = await collection.mintToken(alice);
+ await testReadsYetEmptyProperties(token);
+ });
+
+ async function testAssignPropertiesAccordingToPermissions(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
+ await token.collection.addAdmin(alice, {Substrate: bob.address});
+ await token.transfer(alice, {Substrate: charlie.address}, pieces);
+
+ const propertyKeys: string[] = [];
+ let i = 0;
+ for(const permission of permissions) {
+ i++;
+ let j = 0;
+ for(const signer of permission.signers) {
+ j++;
+ const key = i + '_' + signer.address;
+ propertyKeys.push(key);
+
+ await expect(
+ token.setProperties(signer, [{key: key, value: 'Serotonin increase'}]),
+ `on adding property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+ }
+ }
+
+ const properties = await token.getProperties(propertyKeys);
+ const tokenData = await token.getData();
+ for(let i = 0; i < properties.length; i++) {
+ expect(properties[i].value).to.be.equal('Serotonin increase');
+ expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');
+ }
+ }
+
+ itSub('Assigns properties to a token according to permissions (NFT)', async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
+ await testAssignPropertiesAccordingToPermissions(token, amount);
+ });
+
+ itSub.ifWithPallets('Assigns properties to a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
+ await testAssignPropertiesAccordingToPermissions(token, amount);
+ });
+
+ async function testChangesPropertiesAccordingPermission(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
+ await token.collection.addAdmin(alice, {Substrate: bob.address});
+ await token.transfer(alice, {Substrate: charlie.address}, pieces);
+
+ const propertyKeys: string[] = [];
+ let i = 0;
+ for(const permission of permissions) {
+ i++;
+ if(!permission.permission.mutable) continue;
+
+ let j = 0;
+ for(const signer of permission.signers) {
+ j++;
+ const key = i + '_' + signer.address;
+ propertyKeys.push(key);
+
+ await expect(
+ token.setProperties(signer, [{key, value: 'Serotonin increase'}]),
+ `on adding property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+
+ await expect(
+ token.setProperties(signer, [{key, value: 'Serotonin stable'}]),
+ `on changing property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+ }
+ }
+
+ const properties = await token.getProperties(propertyKeys);
+ const tokenData = await token.getData();
+ for(let i = 0; i < properties.length; i++) {
+ expect(properties[i].value).to.be.equal('Serotonin stable');
+ expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');
+ }
+ }
+
+ itSub('Changes properties of a token according to permissions (NFT)', async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
+ await testChangesPropertiesAccordingPermission(token, amount);
+ });
+
+ itSub.ifWithPallets('Changes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
+ await testChangesPropertiesAccordingPermission(token, amount);
+ });
+
+ async function testDeletePropertiesAccordingPermission(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
+ await token.collection.addAdmin(alice, {Substrate: bob.address});
+ await token.transfer(alice, {Substrate: charlie.address}, pieces);
+
+ const propertyKeys: string[] = [];
+ let i = 0;
+
+ for(const permission of permissions) {
+ i++;
+ if(!permission.permission.mutable) continue;
+
+ let j = 0;
+ for(const signer of permission.signers) {
+ j++;
+ const key = i + '_' + signer.address;
+ propertyKeys.push(key);
+
+ await expect(
+ token.setProperties(signer, [{key, value: 'Serotonin increase'}]),
+ `on adding property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+
+ await expect(
+ token.deleteProperties(signer, [key]),
+ `on deleting property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+ }
+ }
+
+ expect(await token.getProperties(propertyKeys)).to.be.empty;
+ expect((await token.getData())!.properties).to.be.empty;
+ }
+
+ itSub('Deletes properties of a token according to permissions (NFT)', async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
+ await testDeletePropertiesAccordingPermission(token, amount);
+ });
+
+ itSub.ifWithPallets('Deletes properties of a token according to permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
+ await testDeletePropertiesAccordingPermission(token, amount);
+ });
+
+ itSub('Assigns properties to a nested token according to permissions', async ({helper}) => {
+ const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const collectionB = await helper.nft.mintCollection(alice, {
+ tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
+ signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
+ });
+ const targetToken = await collectionA.mintToken(alice);
+ const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());
+
+ await collectionB.addAdmin(alice, {Substrate: bob.address});
+ await targetToken.transfer(alice, {Substrate: charlie.address});
+
+ const propertyKeys: string[] = [];
+ let i = 0;
+ for(const permission of permissions) {
+ i++;
+ let j = 0;
+ for(const signer of permission.signers) {
+ j++;
+ const key = i + '_' + signer.address;
+ propertyKeys.push(key);
+
+ await expect(
+ nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]),
+ `on adding property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+ }
+ }
+
+ const properties = await nestedToken.getProperties(propertyKeys);
+ const tokenData = await nestedToken.getData();
+ for(let i = 0; i < properties.length; i++) {
+ expect(properties[i].value).to.be.equal('Serotonin increase');
+ expect(tokenData!.properties[i].value).to.be.equal('Serotonin increase');
+ }
+ expect(await targetToken.getProperties()).to.be.empty;
+ });
+
+ itSub('Changes properties of a nested token according to permissions', async ({helper}) => {
+ const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const collectionB = await helper.nft.mintCollection(alice, {
+ tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
+ signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
+ });
+ const targetToken = await collectionA.mintToken(alice);
+ const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());
+
+ await collectionB.addAdmin(alice, {Substrate: bob.address});
+ await targetToken.transfer(alice, {Substrate: charlie.address});
+
+ const propertyKeys: string[] = [];
+ let i = 0;
+ for(const permission of permissions) {
+ i++;
+ if(!permission.permission.mutable) continue;
+
+ let j = 0;
+ for(const signer of permission.signers) {
+ j++;
+ const key = i + '_' + signer.address;
+ propertyKeys.push(key);
+
+ await expect(
+ nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]),
+ `on adding property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+
+ await expect(
+ nestedToken.setProperties(signer, [{key, value: 'Serotonin stable'}]),
+ `on changing property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+ }
+ }
+
+ const properties = await nestedToken.getProperties(propertyKeys);
+ const tokenData = await nestedToken.getData();
+ for(let i = 0; i < properties.length; i++) {
+ expect(properties[i].value).to.be.equal('Serotonin stable');
+ expect(tokenData!.properties[i].value).to.be.equal('Serotonin stable');
+ }
+ expect(await targetToken.getProperties()).to.be.empty;
+ });
+
+ itSub('Deletes properties of a nested token according to permissions', async ({helper}) => {
+ const collectionA = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const collectionB = await helper.nft.mintCollection(alice, {
+ tokenPropertyPermissions: permissions.flatMap(({permission, signers}, i) =>
+ signers.map(signer => ({key: `${i+1}_${signer.address}`, permission}))),
+ });
+ const targetToken = await collectionA.mintToken(alice);
+ const nestedToken = await collectionB.mintToken(alice, targetToken.nestingAccount());
+
+ await collectionB.addAdmin(alice, {Substrate: bob.address});
+ await targetToken.transfer(alice, {Substrate: charlie.address});
+
+ const propertyKeys: string[] = [];
+ let i = 0;
+ for(const permission of permissions) {
+ i++;
+ if(!permission.permission.mutable) continue;
+
+ let j = 0;
+ for(const signer of permission.signers) {
+ j++;
+ const key = i + '_' + signer.address;
+ propertyKeys.push(key);
+
+ await expect(
+ nestedToken.setProperties(signer, [{key, value: 'Serotonin increase'}]),
+ `on adding property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+
+ await expect(
+ nestedToken.deleteProperties(signer, [key]),
+ `on deleting property #${i} by signer #${j}`,
+ ).to.be.fulfilled;
+ }
+ }
+
+ expect(await nestedToken.getProperties(propertyKeys)).to.be.empty;
+ expect((await nestedToken.getData())!.properties).to.be.empty;
+ expect(await targetToken.getProperties()).to.be.empty;
+ });
+
+ [
+ {mode: 'nft' as const, storage: 'nonfungible' as const, pieces: undefined, requiredPallets: []} as const,
+ {mode: 'rft' as const, storage: 'refungible' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]} as const,
+ ].map(testCase =>
+ itSub.ifWithPallets(`Allows modifying a token property multiple times with the same size (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
+ const propKey = 'tok-prop';
+
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPropertyPermissions: [
+ {
+ key: propKey,
+ permission: {mutable: true, tokenOwner: true},
+ },
+ ],
+ });
+
+ const maxTokenPropertiesSize = 32768;
+
+ const propDataSize = 4096;
+
+ let propDataChar = 'a';
+ const makeNewPropData = () => {
+ propDataChar = String.fromCharCode(propDataChar.charCodeAt(0) + 1);
+ return `${propDataChar}`.repeat(propDataSize);
+ };
+
+ const token = await (
+ testCase.pieces
+ ? collection.mintToken(alice, testCase.pieces as any)
+ : collection.mintToken(alice)
+ );
+
+ const property = {key: propKey, value: makeNewPropData()};
+ await token.setProperties(alice, [property]);
+ const originalSpace = await token.getTokenPropertiesConsumedSpace();
+ expect(originalSpace).to.be.equal(sizeOfProperty(property));
+
+ const sameSizePropertiesPossibleNum = maxTokenPropertiesSize / propDataSize;
+
+ // It is possible to modify a property as many times as needed.
+ // It will not consume any additional space.
+ for(let i = 0; i < sameSizePropertiesPossibleNum + 1; i++) {
+ await token.setProperties(alice, [{key: propKey, value: makeNewPropData()}]);
+ const consumedSpace = await token.getTokenPropertiesConsumedSpace();
+ expect(consumedSpace).to.be.equal(originalSpace);
+ }
+ }));
+
+ [
+ {mode: 'nft' as const, pieces: undefined, requiredPallets: []},
+ {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itSub.ifWithPallets(`Adding then removing a token property doesn't change the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
+ const propKey = 'tok-prop';
+
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPropertyPermissions: [
+ {
+ key: propKey,
+ permission: {mutable: true, tokenOwner: true},
+ },
+ ],
+ });
+ const token = await (
+ testCase.pieces
+ ? collection.mintToken(alice, testCase.pieces as any)
+ : collection.mintToken(alice)
+ );
+ const originalSpace = await token.getTokenPropertiesConsumedSpace();
+
+ const propDataSize = 4096;
+ const propData = 'a'.repeat(propDataSize);
+
+ const property = {key: propKey, value: propData};
+ await token.setProperties(alice, [property]);
+ let consumedSpace = await token.getTokenPropertiesConsumedSpace();
+ expect(consumedSpace).to.be.equal(sizeOfProperty(property));
+
+ await token.deleteProperties(alice, [propKey]);
+ consumedSpace = await token.getTokenPropertiesConsumedSpace();
+ expect(consumedSpace).to.be.equal(originalSpace);
+ }));
+
+ [
+ {mode: 'nft' as const, pieces: undefined, requiredPallets: []},
+ {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itSub.ifWithPallets(`Modifying a token property with different sizes correctly changes the consumed space (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
+ const propKey = 'tok-prop';
+
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPropertyPermissions: [
+ {
+ key: propKey,
+ permission: {mutable: true, tokenOwner: true},
+ },
+ ],
+ });
+ const token = await (
+ testCase.pieces
+ ? collection.mintToken(alice, testCase.pieces as any)
+ : collection.mintToken(alice)
+ );
+ const originalSpace = await token.getTokenPropertiesConsumedSpace();
+
+ const initProp = {key: propKey, value: 'a'.repeat(4096)};
+ const biggerProp = {key: propKey, value: 'b'.repeat(5000)};
+ const smallerProp = {key: propKey, value: 'c'.repeat(4000)};
+
+ let consumedSpace;
+ let expectedConsumedSpaceDiff;
+
+ await token.setProperties(alice, [initProp]);
+ consumedSpace = await token.getTokenPropertiesConsumedSpace();
+ expectedConsumedSpaceDiff = sizeOfProperty(initProp) - originalSpace;
+ expect(consumedSpace).to.be.equal(originalSpace + expectedConsumedSpaceDiff);
+
+ await token.setProperties(alice, [biggerProp]);
+ consumedSpace = await token.getTokenPropertiesConsumedSpace();
+ expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(initProp);
+ expect(consumedSpace).to.be.equal(sizeOfProperty(initProp) + expectedConsumedSpaceDiff);
+
+ await token.setProperties(alice, [smallerProp]);
+ consumedSpace = await token.getTokenPropertiesConsumedSpace();
+ expectedConsumedSpaceDiff = sizeOfProperty(biggerProp) - sizeOfProperty(smallerProp);
+ expect(consumedSpace).to.be.equal(sizeOfProperty(biggerProp) - expectedConsumedSpaceDiff);
+ }));
+
+ itSub('Set sponsored properties', async({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {tokenPropertyPermissions: [{key: 'k', permission: {tokenOwner: true}}]});
+
+ await collection.setSponsor(alice, alice.address);
+ await collection.confirmSponsorship(alice);
+ await collection.setPermissions(alice, {access: 'AllowList', mintMode: true});
+ await collection.addToAllowList(alice, {Substrate: bob.address});
+ await collection.setLimits(alice, {sponsoredDataRateLimit: {blocks: 30}});
+
+ const token = await collection.mintToken(alice, {Substrate: bob.address});
+
+ const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);
+ const bobBalanceBefore = await helper.balance.getSubstrate(bob.address);
+
+ await token.setProperties(bob, [{key: 'k', value: 'val'}]);
+
+ const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);
+ const bobBalanceAfter = await helper.balance.getSubstrate(bob.address);
+
+ expect(bobBalanceAfter).to.be.equal(bobBalanceBefore);
+ expect(aliceBalanceBefore > aliceBalanceAfter).to.be.true;
+ });
+});
+
+describe('Negative Integration Test: Token Properties', () => {
+ let alice: IKeyringPair; // collection owner
+ let bob: IKeyringPair; // collection admin
+ let charlie: IKeyringPair; // token owner
+
+ let constitution: {permission: any, signers: IKeyringPair[], sinner: IKeyringPair}[];
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ let dave: IKeyringPair;
+ [alice, bob, charlie, dave] = await helper.arrange.createAccounts([100n, 100n, 100n, 100n], donor);
+
+ // todo:playgrounds probably separate these tests later
+ constitution = [
+ {permission: {mutable: true, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},
+ {permission: {mutable: false, collectionAdmin: true}, signers: [alice, bob], sinner: charlie},
+ {permission: {mutable: true, tokenOwner: true}, signers: [charlie], sinner: alice},
+ {permission: {mutable: false, tokenOwner: true}, signers: [charlie], sinner: alice},
+ {permission: {mutable: true, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},
+ {permission: {mutable: false, collectionAdmin: true, tokenOwner: true}, signers: [alice, bob, charlie], sinner: dave},
+ ];
+ });
+ });
+
+ [
+ {mode: 'nft' as const, requiredPallets: [Pallets.NFT]},
+ {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itSub.ifWithPallets(`Forbids adding/deleting properties of a token if token doesn't exist (${testCase.mode.toLocaleUpperCase})`, testCase.requiredPallets, async({helper}) => {
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPropertyPermissions: constitution.slice(0, 1).map(({permission}) => ({key: '1', permission})),
+ });
+ const nonExistentToken = collection.getTokenObject(1);
+
+ await expect(
+ nonExistentToken.setProperties(alice, [{key: '1', value: 'Serotonin increase'}]),
+ 'on expecting failure whilst adding a property by alice',
+ ).to.be.rejectedWith(/common\.TokenNotFound/);
+
+ await expect(
+ nonExistentToken.deleteProperties(alice, ['1']),
+ 'on expecting failure whilst deleting a property by alice',
+ ).to.be.rejectedWith(/common\.TokenNotFound/);
+ }));
+
+ async function mintCollectionWithAllPermissionsAndToken(helper: UniqueHelper, mode: 'NFT' | 'RFT'): Promise<[UniqueNFToken | UniqueRFToken, bigint]> {
+ const collection = await (mode == 'NFT' ? helper.nft : helper.rft).mintCollection(alice, {
+ tokenPropertyPermissions: constitution.map(({permission}, i) => ({key: `${i+1}`, permission})),
+ });
+ return mode == 'NFT' ? [await collection.mintToken(alice), 1n] : [await collection.mintToken(alice, 100n as any), 100n];
+ }
+
+ async function getConsumedSpace(api: any, collectionId: number, tokenId: number, mode: 'NFT' | 'RFT'): Promise<number> {
+ return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace;
+ }
+
+ async function prepare(token: UniqueNFToken | UniqueRFToken, pieces: bigint): Promise<number> {
+ await token.collection.addAdmin(alice, {Substrate: bob.address});
+ await token.transfer(alice, {Substrate: charlie.address}, pieces);
+
+ let i = 0;
+ for(const passage of constitution) {
+ i++;
+ const signer = passage.signers[0];
+ await expect(
+ token.setProperties(signer, [{key: `${i}`, value: 'Serotonin increase'}]),
+ `on adding property ${i} by ${signer.address}`,
+ ).to.be.fulfilled;
+ }
+
+ const originalSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
+ return originalSpace;
+ }
+
+ async function testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
+ const originalSpace = await prepare(token, pieces);
+
+ let i = 0;
+ for(const forbiddance of constitution) {
+ i++;
+ if(!forbiddance.permission.mutable) continue;
+
+ await expect(
+ token.setProperties(forbiddance.sinner, [{key: `${i}`, value: 'Serotonin down'}]),
+ `on failing to change property ${i} by the malefactor`,
+ ).to.be.rejectedWith(/common\.NoPermission/);
+
+ await expect(
+ token.deleteProperties(forbiddance.sinner, [`${i}`]),
+ `on failing to delete property ${i} by the malefactor`,
+ ).to.be.rejectedWith(/common\.NoPermission/);
+ }
+
+ const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
+ expect(consumedSpace).to.be.equal(originalSpace);
+ }
+
+ itSub('Forbids changing/deleting properties of a token if the user is outside of permissions (NFT)', async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
+ await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, amount);
+ });
+
+ itSub.ifWithPallets('Forbids changing/deleting properties of a token if the user is outside of permissions (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
+ await testForbidsChangingDeletingPropertiesUserOutsideOfPermissions(token, amount);
+ });
+
+ async function testForbidsChangingDeletingPropertiesIfPropertyImmutable(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
+ const originalSpace = await prepare(token, pieces);
+
+ let i = 0;
+ for(const permission of constitution) {
+ i++;
+ if(permission.permission.mutable) continue;
+
+ await expect(
+ token.setProperties(permission.signers[0], [{key: `${i}`, value: 'Serotonin down'}]),
+ `on failing to change property ${i} by signer #0`,
+ ).to.be.rejectedWith(/common\.NoPermission/);
+
+ await expect(
+ token.deleteProperties(permission.signers[0], [i.toString()]),
+ `on failing to delete property ${i} by signer #0`,
+ ).to.be.rejectedWith(/common\.NoPermission/);
+ }
+
+ const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
+ expect(consumedSpace).to.be.equal(originalSpace);
+ }
+
+ itSub('Forbids changing/deleting properties of a token if the property is permanent (immutable) (NFT)', async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
+ await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, amount);
+ });
+
+ itSub.ifWithPallets('Forbids changing/deleting properties of a token if the property is permanent (immutable) (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
+ await testForbidsChangingDeletingPropertiesIfPropertyImmutable(token, amount);
+ });
+
+ async function testForbidsAddingPropertiesIfPropertyNotDeclared(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
+ const originalSpace = await prepare(token, pieces);
+
+ await expect(
+ token.setProperties(alice, [{key: 'non-existent', value: 'I exist!'}]),
+ 'on failing to add a previously non-existent property',
+ ).to.be.rejectedWith(/common\.NoPermission/);
+
+ await expect(
+ token.collection.setTokenPropertyPermissions(alice, [{key: 'now-existent', permission: {}}]),
+ 'on setting a new non-permitted property',
+ ).to.be.fulfilled;
+
+ await expect(
+ token.setProperties(alice, [{key: 'now-existent', value: 'I exist!'}]),
+ 'on failing to add a property forbidden by the \'None\' permission',
+ ).to.be.rejectedWith(/common\.NoPermission/);
+
+ expect(await token.getProperties(['non-existent', 'now-existent'])).to.be.empty;
+
+ const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
+ expect(consumedSpace).to.be.equal(originalSpace);
+ }
+
+ itSub('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (NFT)', async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
+ await testForbidsAddingPropertiesIfPropertyNotDeclared(token, amount);
+ });
+
+ itSub.ifWithPallets('Forbids adding properties to a token if the property is not declared / forbidden with the \'None\' permission (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
+ await testForbidsAddingPropertiesIfPropertyNotDeclared(token, amount);
+ });
+
+ async function testForbidsAddingTooLargeProperties(token: UniqueNFToken | UniqueRFToken, pieces: bigint) {
+ const originalSpace = await prepare(token, pieces);
+
+ await expect(
+ token.collection.setTokenPropertyPermissions(alice, [
+ {key: 'a_holy_book', permission: {collectionAdmin: true, tokenOwner: true}},
+ {key: 'young_years', permission: {collectionAdmin: true, tokenOwner: true}},
+ ]),
+ 'on setting new permissions for properties',
+ ).to.be.fulfilled;
+
+ // Mute the general tx parsing error
+ {
+ console.error = () => {};
+ await expect(token.setProperties(alice, [{key: 'a_holy_book', value: 'word '.repeat(6554)}]))
+ .to.be.rejected;
+ }
+
+ await expect(token.setProperties(alice, [
+ {key: 'a_holy_book', value: 'word '.repeat(3277)},
+ {key: 'young_years', value: 'neverending'.repeat(1490)},
+ ])).to.be.rejectedWith(/common\.NoSpaceForProperty/);
+
+ expect(await token.getProperties(['a_holy_book', 'young_years'])).to.be.empty;
+ const consumedSpace = await getConsumedSpace(token.collection.helper.getApi(), token.collectionId, token.tokenId, pieces == 1n ? 'NFT' : 'RFT');
+ expect(consumedSpace).to.be.equal(originalSpace);
+ }
+
+ itSub('Forbids adding too large properties to a token (NFT)', async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'NFT');
+ await testForbidsAddingTooLargeProperties(token, amount);
+ });
+
+ itSub.ifWithPallets('Forbids adding too large properties to a token (ReFungible)', [Pallets.ReFungible], async ({helper}) => {
+ const [token, amount] = await mintCollectionWithAllPermissionsAndToken(helper, 'RFT');
+ await testForbidsAddingTooLargeProperties(token, amount);
+ });
+
+ [
+ {mode: 'nft' as const, requiredPallets: []},
+ {mode: 'rft' as const, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itSub.ifWithPallets(`Forbids adding too many propeties to a token (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
+ const collection = await helper[testCase.mode].mintCollection(alice);
+ const maxPropertiesPerItem = 64;
+
+ for(let i = 0; i < maxPropertiesPerItem; i++) {
+ await collection.setTokenPropertyPermissions(alice, [{
+ key: `${i+1}`,
+ permission: {mutable: true, tokenOwner: true, collectionAdmin: true},
+ }]);
+ }
+
+ await expect(collection.setTokenPropertyPermissions(alice, [{
+ key: `${maxPropertiesPerItem}-th`,
+ permission: {mutable: true, tokenOwner: true, collectionAdmin: true},
+ }])).to.be.rejectedWith(/common\.PropertyLimitReached/);
+ }));
+
+ [
+ {mode: 'nft' as const, pieces: undefined, requiredPallets: []},
+ {mode: 'rft' as const, pieces: 100n, requiredPallets: [Pallets.ReFungible]},
+ ].map(testCase =>
+ itSub.ifWithPallets(`Forbids force_repair_item from non-sudo (${testCase.mode})`, testCase.requiredPallets, async({helper}) => {
+ const propKey = 'tok-prop';
+
+ const collection = await helper[testCase.mode].mintCollection(alice, {
+ tokenPropertyPermissions: [
+ {
+ key: propKey,
+ permission: {mutable: true, tokenOwner: true},
+ },
+ ],
+ });
+ const token = await (
+ testCase.pieces
+ ? collection.mintToken(alice, testCase.pieces as any)
+ : collection.mintToken(alice)
+ );
+
+ const propDataSize = 4096;
+ const propData = 'a'.repeat(propDataSize);
+ await token.setProperties(alice, [{key: propKey, value: propData}]);
+
+ await expect(helper.executeExtrinsic(alice, 'api.tx.unique.forceRepairItem', [token.collectionId, token.tokenId], true))
+ .to.be.rejectedWith(/BadOrigin/);
+ }));
+});
+
+describe('ReFungible token properties permissions tests', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+ let charlie: IKeyringPair;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.ReFungible]);
+
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);
+ });
+ });
+
+ async function prepare(helper: UniqueHelper): Promise<UniqueRFToken> {
+ const collection = await helper.rft.mintCollection(alice);
+ const token = await collection.mintToken(alice, 100n);
+
+ await collection.addAdmin(alice, {Substrate: bob.address});
+ await collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable: true, tokenOwner: true}}]);
+
+ return token;
+ }
+
+ itSub('Forbids adding token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {
+ const token = await prepare(helper);
+
+ await token.transfer(alice, {Substrate: charlie.address}, 33n);
+
+ await expect(token.setProperties(alice, [
+ {key: 'fractals', value: 'multiverse'},
+ ])).to.be.rejectedWith(/common\.NoPermission/);
+ });
+
+ itSub('Forbids mutating token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {
+ const token = await prepare(helper);
+
+ await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, tokenOwner: true}}]))
+ .to.be.fulfilled;
+
+ await expect(token.setProperties(alice, [
+ {key: 'fractals', value: 'multiverse'},
+ ])).to.be.fulfilled;
+
+ await token.transfer(alice, {Substrate: charlie.address}, 33n);
+
+ await expect(token.setProperties(alice, [
+ {key: 'fractals', value: 'want to rule the world'},
+ ])).to.be.rejectedWith(/common\.NoPermission/);
+ });
+
+ itSub('Forbids deleting token property with tokenOwner==true when signer doesn\'t have all pieces', async ({helper}) => {
+ const token = await prepare(helper);
+
+ await expect(token.setProperties(alice, [
+ {key: 'fractals', value: 'one headline - why believe it'},
+ ])).to.be.fulfilled;
+
+ await token.transfer(alice, {Substrate: charlie.address}, 33n);
+
+ await expect(token.deleteProperties(alice, ['fractals'])).
+ to.be.rejectedWith(/common\.NoPermission/);
+ });
+
+ itSub('Allows token property mutation with collectionOwner==true when admin doesn\'t have all pieces', async ({helper}) => {
+ const token = await prepare(helper);
+
+ await token.transfer(alice, {Substrate: charlie.address}, 33n);
+
+ await expect(token.collection.setTokenPropertyPermissions(alice, [{key: 'fractals', permission: {mutable:true, collectionAdmin: true}}]))
+ .to.be.fulfilled;
+
+ await expect(token.setProperties(alice, [
+ {key: 'fractals', value: 'multiverse'},
+ ])).to.be.fulfilled;
+ });
+});
js-packages/tests/sub/nesting/unnest.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/nesting/unnest.test.ts
@@ -0,0 +1,325 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+import type {IKeyringPair} from '@polkadot/types/types';
+import {expect, itSub, Pallets, usingPlaygrounds} from '../../util/index.js';
+import {CrossAccountId, UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '@unique/playgrounds/unique.js';
+
+describe('Integration Test: Unnesting', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+ let charlie: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob, charlie] = await helper.arrange.createAccounts([200n, 50n, 50n], donor);
+ });
+ });
+
+ itSub('NFT: allows the owner to successfully unnest a token', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const targetToken = await collection.mintToken(alice);
+
+ // Create a nested token
+ const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());
+
+ // Unnest
+ await expect(nestedToken.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}), 'while unnesting').to.be.fulfilled;
+ expect(await nestedToken.getOwner()).to.be.deep.equal({Substrate: alice.address});
+
+ // Nest and burn
+ await nestedToken.nest(alice, targetToken);
+ await expect(nestedToken.burnFrom(alice, targetToken.nestingAccount()), 'while burning').to.be.fulfilled;
+ await expect(nestedToken.getOwner()).to.be.rejected;
+ });
+
+ itSub('NativeFungible: allows the owner to successfully unnest a token', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const targetToken = await collection.mintToken(alice);
+
+ const collectionFT = helper.ft.getCollectionObject(0);
+
+ // Nest
+ await collectionFT.transfer(alice, targetToken.nestingAccount(), 10n);
+ // Unnest
+ await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
+
+ expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(1n);
+ });
+
+ itSub('Fungible: allows the owner to successfully unnest a token', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const targetToken = await collection.mintToken(alice);
+
+ const collectionFT = await helper.ft.mintCollection(alice);
+
+ // Nest and unnest
+ await collectionFT.mint(alice, 10n, targetToken.nestingAccount());
+ await expect(collectionFT.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
+ expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(9n);
+ expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(1n);
+
+ // Nest and burn
+ await collectionFT.transfer(alice, targetToken.nestingAccount(), 5n);
+ await expect(collectionFT.burnTokensFrom(alice, targetToken.nestingAccount(), 6n), 'while burning').to.be.fulfilled;
+ expect(await collectionFT.getBalance({Substrate: alice.address})).to.be.equal(4n);
+ expect(await collectionFT.getBalance(targetToken.nestingAccount())).to.be.equal(0n);
+ expect(await targetToken.getChildren()).to.be.length(0);
+ });
+
+ itSub.ifWithPallets('ReFungible: allows the owner to successfully unnest a token', [Pallets.ReFungible], async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const targetToken = await collection.mintToken(alice);
+
+ const collectionRFT = await helper.rft.mintCollection(alice);
+
+ // Nest and unnest
+ const token = await collectionRFT.mintToken(alice, 10n, targetToken.nestingAccount());
+ await expect(token.transferFrom(alice, targetToken.nestingAccount(), {Substrate: alice.address}, 9n), 'while unnesting').to.be.fulfilled;
+ expect(await token.getBalance({Substrate: alice.address})).to.be.equal(9n);
+ expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(1n);
+
+ // Nest and burn
+ await token.transfer(alice, targetToken.nestingAccount(), 5n);
+ await expect(token.burnFrom(alice, targetToken.nestingAccount(), 6n), 'while burning').to.be.fulfilled;
+ expect(await token.getBalance({Substrate: alice.address})).to.be.equal(4n);
+ expect(await token.getBalance(targetToken.nestingAccount())).to.be.equal(0n);
+ expect(await targetToken.getChildren()).to.be.length(0);
+ });
+
+ async function checkNestedAmountState({
+ expectedBalance,
+ childrenShouldPresent,
+ nested,
+ targetNft,
+ }: {
+ expectedBalance: bigint,
+ childrenShouldPresent: boolean,
+ nested: UniqueFTCollection | UniqueRFToken,
+ targetNft: UniqueNFToken,
+ }) {
+ const balance = await nested.getBalance(targetNft.nestingAccount());
+ expect(balance).to.be.equal(expectedBalance);
+
+ const children = await targetNft.getChildren();
+
+ if(childrenShouldPresent) {
+ expect(children[0]).to.be.deep.equal({
+ collectionId: nested.collectionId,
+ tokenId: (nested instanceof UniqueFTCollection) ? 0 : nested.tokenId,
+ });
+ } else {
+ expect(children.length).to.be.equal(0);
+ }
+ }
+
+ function ownerOrAdminUnnestCases(modes: ('ft' | 'nft' | 'rft')[]): {
+ mode: 'ft' | 'nft' | 'rft',
+ sender: string,
+ op: 'transfer' | 'burn',
+ requiredPallets: Pallets[],
+ }[] {
+ const senders = ['owner', 'admin'];
+ const ops = ['transfer', 'burn'];
+
+ const cases = [];
+ for(const mode of modes) {
+ const requiredPallets = (mode === 'rft')
+ ? [Pallets.ReFungible]
+ : [];
+
+ for(const sender of senders) {
+ for(const op of ops) {
+ cases.push({
+ mode: mode as 'ft' | 'nft' | 'rft',
+ sender,
+ op: op as 'transfer' | 'burn',
+ requiredPallets,
+ });
+ }
+ }
+ }
+
+ return cases;
+ }
+
+ ownerOrAdminUnnestCases(['ft', 'rft']).map(testCase =>
+ itSub.ifWithPallets(`[${testCase.mode}]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, testCase.requiredPallets, async({helper}) => {
+ const owner = alice;
+ const admin = bob;
+
+ const unnester = (testCase.sender === 'owner')
+ ? owner
+ : admin;
+
+ const collectionNFT = await helper.nft.mintCollection(owner);
+ await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});
+
+ const collectionNested = await helper[testCase.mode as 'ft' | 'rft'].mintCollection(owner, {
+ limits: {
+ ownerCanTransfer: true,
+ },
+ });
+ await collectionNested.addAdmin(owner, {Substrate: admin.address});
+
+ const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});
+
+ let nested: UniqueFTCollection | UniqueRFToken;
+ const totalAmount = 5n;
+ const firstUnnestAmount = 2n;
+ const restUnnestAmount = totalAmount - firstUnnestAmount;
+
+ if(collectionNested instanceof UniqueFTCollection) {
+ await collectionNested.mint(owner, totalAmount, {Substrate: charlie.address});
+ nested = collectionNested;
+ } else {
+ nested = await collectionNested.mintToken(owner, totalAmount, {Substrate: charlie.address});
+ }
+
+ // transfer/burn `amount` of nested assets by `unnester`.
+ const doOperationAndCheck = async ({
+ amount,
+ shouldBeNestedAfterOp,
+ }: {
+ amount: bigint,
+ shouldBeNestedAfterOp: boolean,
+ }) => {
+ const nestedBalanceBeforeOp = await nested.getBalance(targetNft.nestingAccount());
+
+ if(testCase.op === 'transfer') {
+ const bobBalanceBeforeOp = await nested.getBalance({Substrate: bob.address});
+
+ await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address}, amount);
+ expect(await nested.getBalance({Substrate: bob.address})).to.be.equal(bobBalanceBeforeOp + amount);
+ } else {
+ if(nested instanceof UniqueFTCollection) {
+ await nested.burnTokensFrom(unnester, targetNft.nestingAccount(), amount);
+ } else {
+ await nested.burnFrom(unnester, targetNft.nestingAccount(), amount);
+ }
+ }
+
+ await checkNestedAmountState({
+ expectedBalance: nestedBalanceBeforeOp - amount,
+ childrenShouldPresent: shouldBeNestedAfterOp,
+ nested,
+ targetNft,
+ });
+ };
+
+ // Initial setup: nest (fungibles/rft parts).
+ // Check NFT's balance of nested assets and NFT's children.
+ await nested.transfer(charlie, targetNft.nestingAccount(), totalAmount);
+ await checkNestedAmountState({
+ expectedBalance: totalAmount,
+ childrenShouldPresent: true,
+ nested,
+ targetNft,
+ });
+
+ // Transfer/burn only a part of nested assets.
+ // Check that NFT's balance of the nested assets correctly decreased and NFT's children are not changed.
+ await doOperationAndCheck({
+ amount: firstUnnestAmount,
+ shouldBeNestedAfterOp: true,
+ });
+
+ // Transfer/burn all remaining nested assets.
+ // Check that NFT's balance of the nested assets is 0 and NFT has no more children.
+ await doOperationAndCheck({
+ amount: restUnnestAmount,
+ shouldBeNestedAfterOp: false,
+ });
+ }));
+
+ ownerOrAdminUnnestCases(['nft']).map(testCase =>
+ itSub(`[nft]: allows a collection ${testCase.sender} to ${testCase.op} nested token`, async ({helper}) => {
+ const owner = alice;
+ const admin = bob;
+
+ const unnester = (testCase.sender === 'owner')
+ ? owner
+ : admin;
+
+ const collectionNFT = await helper.nft.mintCollection(owner);
+ await collectionNFT.setPermissions(owner, {nesting: {tokenOwner: true}});
+
+ const collectionNested = await helper.nft.mintCollection(owner, {
+ limits: {
+ ownerCanTransfer: true,
+ },
+ });
+ await collectionNested.addAdmin(owner, {Substrate: admin.address});
+
+ const targetNft = await collectionNFT.mintToken(owner, {Substrate: charlie.address});
+ const nested = await collectionNested.mintToken(owner, {Substrate: charlie.address});
+
+ await nested.transfer(charlie, targetNft.nestingAccount());
+ expect(await targetNft.getChildren()).to.be.deep.equal([{
+ collectionId: nested.collectionId,
+ tokenId: nested.tokenId,
+ }]);
+
+ if(testCase.op === 'transfer') {
+ await nested.transferFrom(unnester, targetNft.nestingAccount(), {Substrate: bob.address});
+ } else {
+ await nested.burnFrom(unnester, targetNft.nestingAccount());
+ }
+
+ expect((await targetNft.getChildren()).length).to.be.equal(0);
+ }));
+});
+
+describe('Negative Test: Unnesting', () => {
+ let alice: IKeyringPair;
+ let bob: IKeyringPair;
+
+ before(async () => {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ const donor = await privateKey({url: import.meta.url});
+ [alice, bob] = await helper.arrange.createAccounts([50n, 10n], donor);
+ });
+ });
+
+ itSub('Disallows a non-owner to unnest/burn a token', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const targetToken = await collection.mintToken(alice);
+
+ // Create a nested token
+ const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());
+
+ // Try to unnest
+ await expect(nestedToken.unnest(bob, targetToken, {Substrate: alice.address})).to.be.rejectedWith(/common\.ApprovedValueTooLow/);
+ expect(await nestedToken.getOwner()).to.be.deep.equal(CrossAccountId.toLowerCase(targetToken.nestingAccount()));
+
+ // Try to burn
+ await expect(nestedToken.burnFrom(bob, targetToken.nestingAccount())).to.be.rejectedWith(/common\.ApprovedValueTooLow/);
+ expect(await nestedToken.getOwner()).to.be.deep.equal(CrossAccountId.toLowerCase(targetToken.nestingAccount()));
+ });
+
+ // todo another test for creating excessive depth matryoshka with Ethereum?
+
+ // Recursive nesting
+ itSub('Prevents Ouroboros creation', async ({helper}) => {
+ const collection = await helper.nft.mintCollection(alice, {permissions: {nesting: {tokenOwner: true}}});
+ const targetToken = await collection.mintToken(alice);
+
+ // Fail to create a nested token ouroboros
+ const nestedToken = await collection.mintToken(alice, targetToken.nestingAccount());
+ await expect(targetToken.nest(alice, nestedToken)).to.be.rejectedWith(/^structure\.OuroborosDetected$/);
+ });
+});