--- 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 . - -// 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'); - }); -}); --- 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 . - -// 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'); - }); -}); --- 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 . - -// 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'); - }); -}); --- 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 . - -// 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'); - }); -}); --- 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 . - -// 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'); - }); -}); --- 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 . - -// 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'); - }); -}); --- 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 . - -// 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'); - }); -}); --- 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 . - -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++}))); - }); - }); -}); --- 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 . - -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]); - }); - }); -}); --- 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'); - }); - -}); --- 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/); - }); -}); --- 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)); - }); -}); --- 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); - }); -}); --- a/js-packages/tests/governance/technicalCommittee.test.ts +++ /dev/null @@ -1,383 +0,0 @@ -import type {IKeyringPair} from '@polkadot/types/types'; -import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../util/index.js'; -import {Event} from '@unique/playgrounds/unique.dev.js'; -import {initCouncil, democracyLaunchPeriod, democracyFastTrackVotingPeriod, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js'; -import type {ITechComms} from './util.js'; - -describeGov('Governance: Technical Committee tests', () => { - let sudoer: IKeyringPair; - let techcomms: ITechComms; - let donor: IKeyringPair; - let preImageHash: string; - - - const allTechCommitteeThreshold = 3; - - before(async function() { - await usingPlaygrounds(async (helper, privateKey) => { - requirePalletsOrSkip(this, helper, [Pallets.TechnicalCommittee]); - sudoer = await privateKey('//Alice'); - donor = await privateKey({url: import.meta.url}); - - techcomms = await initTechComm(donor, sudoer); - - const proposalCall = await helper.constructApiCall('api.tx.balances.forceSetBalance', [donor.address, 20n * 10n ** 25n]); - preImageHash = await helper.preimage.notePreimageFromCall(sudoer, proposalCall, true); - }); - }); - - after(async () => { - await usingPlaygrounds(async (helper) => { - await clearTechComm(sudoer); - - await helper.preimage.unnotePreimage(sudoer, preImageHash); - await hardResetFellowshipReferenda(sudoer); - await hardResetDemocracy(sudoer); - await hardResetGovScheduler(sudoer); - }); - }); - - function proposalFromAllCommittee(proposal: any) { - return usingPlaygrounds(async (helper) => { - expect((await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length).to.be.equal(allTechCommitteeThreshold); - const proposeResult = await helper.technicalCommittee.collective.propose( - techcomms.andy, - proposal, - allTechCommitteeThreshold, - ); - - const commiteeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult); - const proposalIndex = commiteeProposedEvent.proposalIndex; - const proposalHash = commiteeProposedEvent.proposalHash; - - - await helper.technicalCommittee.collective.vote(techcomms.andy, proposalHash, proposalIndex, true); - await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true); - await helper.technicalCommittee.collective.vote(techcomms.greg, proposalHash, proposalIndex, true); - - const closeResult = await helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex); - Event.TechnicalCommittee.Closed.expect(closeResult); - Event.TechnicalCommittee.Approved.expect(closeResult); - const {result} = Event.TechnicalCommittee.Executed.expect(closeResult); - expect(result).to.eq('Ok'); - - return closeResult; - }); - } - - itSub('TechComm can fast-track Democracy proposals', async ({helper}) => { - const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); - await helper.wait.parachainBlockMultiplesOf(35n); - - await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); - - const fastTrackProposal = await proposalFromAllCommittee(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0)); - Event.Democracy.Started.expect(fastTrackProposal); - }); - - itSub('TechComm can cancel Democracy proposals', async ({helper}) => { - const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; - - const cancelProposal = await proposalFromAllCommittee(helper.democracy.cancelProposalCall(proposalIndex)); - Event.Democracy.ProposalCanceled.expect(cancelProposal); - }); - - itSub('TechComm can cancel ongoing Democracy referendums', async ({helper}) => { - await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper)); - const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started); - const referendumIndex = startedEvent.referendumIndex; - - const emergencyCancelProposal = await proposalFromAllCommittee(helper.democracy.emergencyCancelCall(referendumIndex)); - Event.Democracy.Cancelled.expect(emergencyCancelProposal); - }); - - itSub('TechComm member can veto Democracy proposals', async ({helper}) => { - const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); - await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); - - const vetoExternalCall = await helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.democracy.vetoExternalCall(preimageHash), - ); - Event.Democracy.Vetoed.expect(vetoExternalCall); - }); - - itSub('TechComm can cancel Fellowship referendums', async ({helper}) => { - const fellowship = await initFellowship(donor, sudoer); - const fellowshipProposer = fellowship[5][0]; - const proposal = dummyProposal(helper); - - const submitResult = await helper.fellowship.referenda.submit( - fellowshipProposer, - fellowshipPropositionOrigin, - proposal, - defaultEnactmentMoment, - ); - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; - const cancelProposal = await proposalFromAllCommittee(helper.fellowship.referenda.cancelCall(referendumIndex)); - Event.FellowshipReferenda.Cancelled.expect(cancelProposal); - }); - - itSub.skip('TechComm member can add a Fellowship member', async ({helper}) => { - const newFellowshipMember = helper.arrange.createEmptyAccount(); - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.fellowship.collective.addMemberCall(newFellowshipMember.address), - )).to.be.fulfilled; - const fellowshipMembers = (await helper.callRpc('api.query.fellowshipCollective.members')).toJSON(); - expect(fellowshipMembers).to.contains(newFellowshipMember.address); - await clearFellowship(sudoer); - }); - - itSub('[Negative] TechComm cannot submit regular democracy proposal', async ({helper}) => { - const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n); - - await expect(proposalFromAllCommittee(councilProposal)).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm cannot externally propose SuperMajorityAgainst', async ({helper}) => { - const commiteeProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper)); - - await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm cannot externally propose SimpleMajority', async ({helper}) => { - const commiteeProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper)); - - await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm cannot externally propose SuperMajorityApprove', async ({helper}) => { - const commiteeProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper)); - - await expect(proposalFromAllCommittee(commiteeProposal)).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot submit regular democracy proposal', async ({helper}) => { - const memberProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - memberProposal, - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot externally propose SuperMajorityAgainst', async ({helper}) => { - const memberProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper)); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - memberProposal, - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot externally propose SimpleMajority', async ({helper}) => { - const memberProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper)); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - memberProposal, - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot externally propose SuperMajorityApprove', async ({helper}) => { - const memberProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper)); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - memberProposal, - )).to.be.rejectedWith('BadOrigin'); - }); - - - itSub.skip('[Negative] TechComm cannot promote/demote Fellowship member', async () => { - - }); - - itSub.skip('[Negative] TechComm member cannot promote/demote Fellowship member', async () => { - - }); - - itSub('[Negative] TechComm cannot add/remove a Council member', async ({helper}) => { - const newCouncilMember = helper.arrange.createEmptyAccount(); - const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address); - const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address); - - await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin'); - await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot add/remove a Council member', async ({helper}) => { - const newCouncilMember = helper.arrange.createEmptyAccount(); - const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address); - const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - addMemberProposal, - )).to.be.rejectedWith('BadOrigin'); - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - removeMemberProposal, - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm cannot set/clear Council prime member', async ({helper}) => { - const counselors = await initCouncil(donor, sudoer); - const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address); - const proposalForClear = await helper.council.membership.clearPrimeCall(); - - await expect(proposalFromAllCommittee(proposalForSet)).to.be.rejectedWith('BadOrigin'); - await expect(proposalFromAllCommittee(proposalForClear)).to.be.rejectedWith('BadOrigin'); - await clearCouncil(sudoer); - }); - - itSub('[Negative] TechComm member cannot set/clear Council prime member', async ({helper}) => { - const counselors = await initCouncil(donor, sudoer); - const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address); - const proposalForClear = await helper.council.membership.clearPrimeCall(); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - proposalForSet, - )).to.be.rejectedWith('BadOrigin'); - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - proposalForClear, - )).to.be.rejectedWith('BadOrigin'); - await clearCouncil(sudoer); - }); - - itSub('[Negative] TechComm cannot add/remove a TechComm member', async ({helper}) => { - const newCommMember = helper.arrange.createEmptyAccount(); - const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address); - const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address); - - await expect(proposalFromAllCommittee(addMemberProposal)).to.be.rejectedWith('BadOrigin'); - await expect(proposalFromAllCommittee(removeMemberProposal)).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot add/remove a TechComm member', async ({helper}) => { - const newCommMember = helper.arrange.createEmptyAccount(); - const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address); - const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - addMemberProposal, - )).to.be.rejectedWith('BadOrigin'); - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - removeMemberProposal, - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm cannot remove a Fellowship member', async ({helper}) => { - const fellowship = await initFellowship(donor, sudoer); - - await expect(proposalFromAllCommittee(helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5))).to.be.rejectedWith('BadOrigin'); - await clearFellowship(sudoer); - }); - - itSub('[Negative] TechComm member cannot remove a Fellowship member', async ({helper}) => { - const fellowship = await initFellowship(donor, sudoer); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5), - )).to.be.rejectedWith('BadOrigin'); - await clearFellowship(sudoer); - }); - - itSub('[Negative] TechComm member cannot fast-track Democracy proposals', async ({helper}) => { - const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); - await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0), - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot cancel Democracy proposals', async ({helper}) => { - const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n); - const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex; - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.democracy.cancelProposalCall(proposalIndex), - )) - .to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot cancel ongoing Democracy referendums', async ({helper}) => { - await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper)); - const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started); - const referendumIndex = startedEvent.referendumIndex; - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.democracy.emergencyCancelCall(referendumIndex), - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm cannot blacklist Democracy proposals', async ({helper}) => { - const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); - await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); - - await expect(proposalFromAllCommittee(helper.democracy.blacklistCall(preimageHash))).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm member cannot blacklist Democracy proposals', async ({helper}) => { - const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true); - await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash); - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.democracy.blacklistCall(preimageHash), - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub.skip('[Negative] TechComm member cannot veto external Democracy proposals until the cool-off period pass', async () => { - - }); - - itSub('[Negative] TechComm member cannot cancel Fellowship referendums', async ({helper}) => { - const fellowship = await initFellowship(donor, sudoer); - const fellowshipProposer = fellowship[5][0]; - const proposal = dummyProposal(helper); - - const submitResult = await helper.fellowship.referenda.submit( - fellowshipProposer, - fellowshipPropositionOrigin, - proposal, - defaultEnactmentMoment, - ); - - const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex; - - await expect(helper.technicalCommittee.collective.execute( - techcomms.andy, - helper.fellowship.referenda.cancelCall(referendumIndex), - )).to.be.rejectedWith('BadOrigin'); - }); - - itSub('[Negative] TechComm referendum cannot be closed until the voting threshold is met', async ({helper}) => { - const committeeSize = (await helper.callRpc('api.query.technicalCommitteeMembership.members')).toJSON().length as any as number; - expect(committeeSize).is.greaterThan(1); - const proposeResult = await helper.technicalCommittee.collective.propose( - techcomms.andy, - dummyProposalCall(helper), - committeeSize, - ); - - const committeeProposedEvent = Event.TechnicalCommittee.Proposed.expect(proposeResult); - const proposalIndex = committeeProposedEvent.proposalIndex; - const proposalHash = committeeProposedEvent.proposalHash; - - await helper.technicalCommittee.collective.vote(techcomms.constantine, proposalHash, proposalIndex, true); - - await expect(helper.technicalCommittee.collective.close(techcomms.andy, proposalHash, proposalIndex)).to.be.rejectedWith('TooEarly'); - }); -}); --- 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(), - }; -} --- 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 . - -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); - }); - })); -}); --- 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 . - -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/); - }); - })); -}); --- 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 . - -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/); - }); -}); --- 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 . - -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)); - }); -}); --- 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 . - -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); - }); - })); -}); --- 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 . - -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 { - return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace; - } - - async function prepare(token: UniqueNFToken | UniqueRFToken, pieces: bigint): Promise { - 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 { - 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; - }); -}); --- 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 . - -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$/); - }); -}); --- 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", --- /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 . + +// 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'); + }); +}); --- /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 . + +// 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'); + }); +}); --- /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 . + +// 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'); + }); +}); --- /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 . + +// 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'); + }); +}); --- /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 . + +// 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'); + }); +}); --- /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 . + +// 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'); + }); +}); --- /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 . + +// 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'); + }); +}); --- /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 . + +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++}))); + }); + }); +}); --- /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 . + +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]); + }); + }); +}); --- /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'); + }); + +}); --- /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/); + }); +}); --- /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)); + }); +}); --- /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); + }); +}); --- /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'); + }); +}); --- /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(), + }; +} --- /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 . + +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); + }); + })); +}); --- /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 . + +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/); + }); + })); +}); --- /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 . + +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/); + }); +}); --- /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 . + +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)); + }); +}); --- /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 . + +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); + }); + })); +}); --- /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 . + +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 { + return (await (mode == 'NFT' ? api.query.nonfungible : api.query.refungible).tokenProperties(collectionId, tokenId)).toJSON().consumedSpace; + } + + async function prepare(token: UniqueNFToken | UniqueRFToken, pieces: bigint): Promise { + 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 { + 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; + }); +}); --- /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 . + +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$/); + }); +});