git.delta.rocks / unique-network / refs/commits / 5f9676850759

difftreelog

feat reorginize tests dir

Andy Smith2023-11-20parent: #beb0dfd.patch.diff
in: master

45 files changed

deletedjs-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');
-  });
-});
deletedjs-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');
-  });
-});
deletedjs-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');
-  });
-});
deletedjs-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');
-  });
-});
deletedjs-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');
-  });
-});
deletedjs-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');
-  });
-});
deletedjs-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');
-  });
-});
deletedjs-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++})));
-    });
-  });
-});
deletedjs-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]);
-    });
-  });
-});
deletedjs-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');
-  });
-
-});
deletedjs-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/);
-  });
-});
deletedjs-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));
-  });
-});
deletedjs-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);
-  });
-});
deletedjs-packages/tests/governance/technicalCommittee.test.tsdiffbeforeafterboth

no changes

deletedjs-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(),
-  };
-}
deletedjs-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);
-    });
-  }));
-});
deletedjs-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/);
-    });
-  }));
-});
deletedjs-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/);
-  });
-});
deletedjs-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));
-  });
-});
deletedjs-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);
-    });
-  }));
-});
deletedjs-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;
-  });
-});
deletedjs-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$/);
-  });
-});
modifiedjs-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",
addedjs-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');
+  });
+});
addedjs-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');
+  });
+});
addedjs-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');
+  });
+});
addedjs-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');
+  });
+});
addedjs-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');
+  });
+});
addedjs-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');
+  });
+});
addedjs-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');
+  });
+});
addedjs-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++})));
+    });
+  });
+});
addedjs-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]);
+    });
+  });
+});
addedjs-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');
+  });
+
+});
addedjs-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/);
+  });
+});
addedjs-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));
+  });
+});
addedjs-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);
+  });
+});
addedjs-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');
+  });
+});
addedjs-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(),
+  };
+}
addedjs-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);
+    });
+  }));
+});
addedjs-packages/tests/sub/nesting/collectionProperties.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/tests/sub/nesting/collectionProperties.test.ts
@@ -0,0 +1,326 @@
+// 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/);
+    });
+  }));
+});
addedjs-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/);
+  });
+});
addedjs-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));
+  });
+});
addedjs-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);
+    });
+  }));
+});
addedjs-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;
+  });
+});
addedjs-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$/);
+  });
+});