git.delta.rocks / unique-network / refs/commits / f32d3e56bf10

difftreelog

Merge pull request #996 from UniqueNetwork/tests/playgrounds-refactor

Yaroslav Bolyukin2023-09-15parents: #1054e65 #477a07d.patch.diff
in: master
refactor(playgorunds): rearranging the code structure

10 files changed

modified.envrcdiffbeforeafterboth
--- a/.envrc
+++ b/.envrc
@@ -30,7 +30,7 @@
     fi
 
     echo -e "${GREEN}Baedeker env updated${RESET}"
-    nginx_id=$(docker compose -f .baedeker/.bdk-env/docker-compose.yml ps --format=json | jq -r '.[] | select(.Service == "nginx") | .ID' -e)
+    nginx_id=$(docker compose -f .baedeker/.bdk-env/docker-compose.yml ps --format=json | jq -s 'flatten' | jq -r '.[] | select(.Service == "nginx") | .ID' -e)
     if ! [ $? -eq 0 ]; then
         echo -e "${RED}Nginx container not found${RESET}"
         exit 0
modifiedtests/src/governance/util.tsdiffbeforeafterboth
--- a/tests/src/governance/util.ts
+++ b/tests/src/governance/util.ts
@@ -2,6 +2,7 @@
 import {xxhashAsHex} from '@polkadot/util-crypto';
 import {usingPlaygrounds, expect} from '../util';
 import {UniqueHelper} from '../util/playgrounds/unique';
+import {DevUniqueHelper} from '../util/playgrounds/unique.dev';
 
 export const democracyLaunchPeriod = 35;
 export const democracyVotingPeriod = 35;
@@ -203,7 +204,7 @@
   });
 }
 
-export async function voteUnanimouslyInFellowship(helper: UniqueHelper, fellows: IKeyringPair[][], minRank: number, referendumIndex: number) {
+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);
modifiedtests/src/maintenance.seqtest.tsdiffbeforeafterboth
--- a/tests/src/maintenance.seqtest.ts
+++ b/tests/src/maintenance.seqtest.ts
@@ -192,19 +192,22 @@
       const blocksToWait = 6;
 
       // Scheduling works before the maintenance
-      await nftBeforeMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdBeforeMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdBeforeMM})
+        .nft.transferToken(bob, collection.collectionId, nftBeforeMM.tokenId, {Substrate: superuser.address});
+
 
       await helper.wait.newBlocks(blocksToWait + 1);
       expect(await nftBeforeMM.getOwner()).to.be.deep.equal({Substrate: superuser.address});
 
       // Schedule a transaction that should occur *during* the maintenance
-      await nftDuringMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdDuringMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdDuringMM})
+        .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address});
+
 
       // Schedule a transaction that should occur *after* the maintenance
-      await nftDuringMM.scheduleAfter(blocksToWait * 2, {scheduledId: scheduledIdBunkerThroughMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait * 2, {scheduledId: scheduledIdBunkerThroughMM})
+        .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address});
+
 
       await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);
       expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;
@@ -214,16 +217,16 @@
       expect(await nftDuringMM.getOwner()).to.be.deep.equal({Substrate: bob.address});
 
       // Any attempts to schedule a tx during the MM should be rejected
-      await expect(nftDuringMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAttemptDuringMM})
-        .transfer(bob, {Substrate: superuser.address}))
+      await expect(helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAttemptDuringMM})
+        .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address}))
         .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);
 
       await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);
       expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;
 
       // Scheduling works after the maintenance
-      await nftAfterMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAfterMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAfterMM})
+        .nft.transferToken(bob, collection.collectionId, nftAfterMM.tokenId, {Substrate: superuser.address});
 
       await helper.wait.newBlocks(blocksToWait + 1);
 
modifiedtests/src/scheduler.seqtest.tsdiffbeforeafterboth
before · tests/src/scheduler.seqtest.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util';18import {IKeyringPair} from '@polkadot/types/types';19import {DevUniqueHelper, Event} from './util/playgrounds/unique.dev';2021describe('Scheduling token and balance transfers', () => {22  let superuser: IKeyringPair;23  let alice: IKeyringPair;24  let bob: IKeyringPair;25  let charlie: IKeyringPair;2627  before(async function() {28    await usingPlaygrounds(async (helper, privateKey) => {29      requirePalletsOrSkip(this, helper, [Pallets.UniqueScheduler]);3031      superuser = await privateKey('//Alice');32      const donor = await privateKey({url: import.meta.url});33      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);3435      await helper.testUtils.enable();36    });37  });3839  beforeEach(async () => {40    await usingPlaygrounds(async (helper) => {41      await helper.wait.noScheduledTasks();42    });43  });4445  itSched('Can delay a transfer of an owned token', async (scheduleKind, {helper}) => {46    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});47    const token = await collection.mintToken(alice);48    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;49    const blocksBeforeExecution = 4;5051    await token.scheduleAfter(blocksBeforeExecution, {scheduledId})52      .transfer(alice, {Substrate: bob.address});53    const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;5455    expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});5657    await helper.wait.forParachainBlockNumber(executionBlock);5859    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});60  });6162  itSched('Can transfer funds periodically', async (scheduleKind, {helper}) => {63    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;64    const waitForBlocks = 1;6566    const amount = 1n * helper.balance.getOneTokenNominal();67    const periodic = {68      period: 2,69      repetitions: 2,70    };7172    const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);7374    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic})75      .balance.transferToSubstrate(alice, bob.address, amount);76    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;7778    await helper.wait.forParachainBlockNumber(executionBlock);7980    const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);81    expect(bobsBalanceAfterFirst)82      .to.be.equal(83        bobsBalanceBefore + 1n * amount,84        '#1 Balance of the recipient should be increased by 1 * amount',85      );8687    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);8889    const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);90    expect(bobsBalanceAfterSecond)91      .to.be.equal(92        bobsBalanceBefore + 2n * amount,93        '#2 Balance of the recipient should be increased by 2 * amount',94      );95  });9697  itSub('Can cancel a scheduled operation which has not yet taken effect', async ({helper}) => {98    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});99    const token = await collection.mintToken(alice);100101    const scheduledId = helper.arrange.makeScheduledId();102    const waitForBlocks = 4;103104    expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});105106    await token.scheduleAfter(waitForBlocks, {scheduledId})107      .transfer(alice, {Substrate: bob.address});108    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;109110    await helper.scheduler.cancelScheduled(alice, scheduledId);111112    await helper.wait.forParachainBlockNumber(executionBlock);113114    expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});115  });116117  itSub('Can cancel a periodic operation (transfer of funds)', async ({helper}) => {118    const waitForBlocks = 1;119    const periodic = {120      period: 3,121      repetitions: 2,122    };123124    const scheduledId = helper.arrange.makeScheduledId();125126    const amount = 1n * helper.balance.getOneTokenNominal();127128    const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);129130    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic})131      .balance.transferToSubstrate(alice, bob.address, amount);132    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;133134    await helper.wait.forParachainBlockNumber(executionBlock);135136    const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);137138    expect(bobsBalanceAfterFirst)139      .to.be.equal(140        bobsBalanceBefore + 1n * amount,141        '#1 Balance of the recipient should be increased by 1 * amount',142      );143144    await helper.scheduler.cancelScheduled(alice, scheduledId);145    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);146147    const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);148    expect(bobsBalanceAfterSecond)149      .to.be.equal(150        bobsBalanceAfterFirst,151        '#2 Balance of the recipient should not be changed',152      );153  });154155  itSched('scheduler will not insert more tasks than allowed', async (scheduleKind, {helper}) => {156    const maxScheduledPerBlock = 50;157    let fillScheduledIds = new Array(maxScheduledPerBlock);158    let extraScheduledId = undefined;159160    if(scheduleKind == 'named') {161      const scheduledIds = helper.arrange.makeScheduledIds(maxScheduledPerBlock + 1);162      fillScheduledIds = scheduledIds.slice(0, maxScheduledPerBlock);163      extraScheduledId = scheduledIds[maxScheduledPerBlock];164    }165166    // Since the dev node has Instant Seal,167    // we need a larger gap between the current block and the target one.168    //169    // We will schedule `maxScheduledPerBlock` transaction into the target block,170    // so we need at least `maxScheduledPerBlock`-wide gap.171    // We add some additional blocks to this gap to mitigate possible PolkadotJS delays.172    const waitForBlocks = await helper.arrange.isDevNode() ? maxScheduledPerBlock + 5 : 5;173174    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks;175176    const amount = 1n * helper.balance.getOneTokenNominal();177178    const balanceBefore = await helper.balance.getSubstrate(bob.address);179180    // Fill the target block181    for(let i = 0; i < maxScheduledPerBlock; i++) {182      await helper.scheduler.scheduleAt(executionBlock, {scheduledId: fillScheduledIds[i]})183        .balance.transferToSubstrate(superuser, bob.address, amount);184    }185186    // Try to schedule a task into a full block187    await expect(helper.scheduler.scheduleAt(executionBlock, {scheduledId: extraScheduledId})188      .balance.transferToSubstrate(superuser, bob.address, amount))189      .to.be.rejectedWith(/scheduler\.AgendaIsExhausted/);190191    await helper.wait.forParachainBlockNumber(executionBlock);192193    const balanceAfter = await helper.balance.getSubstrate(bob.address);194195    expect(balanceAfter > balanceBefore).to.be.true;196197    const diff = balanceAfter - balanceBefore;198    expect(diff).to.be.equal(amount * BigInt(maxScheduledPerBlock));199  });200201  itSched.ifWithPallets('Scheduled tasks are transactional', [Pallets.TestUtils], async (scheduleKind, {helper}) => {202    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;203    const waitForBlocks = 4;204205    const initTestVal = 42;206    const changedTestVal = 111;207208    await helper.testUtils.setTestValue(alice, initTestVal);209210    await helper.scheduler.scheduleAfter<DevUniqueHelper>(waitForBlocks, {scheduledId})211      .testUtils.setTestValueAndRollback(alice, changedTestVal);212    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;213214    await helper.wait.forParachainBlockNumber(executionBlock);215216    const testVal = await helper.testUtils.testValue();217    expect(testVal, 'The test value should NOT be commited')218      .to.be.equal(initTestVal);219  });220221  itSched.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.TestUtils], async function(scheduleKind, {helper}) {222    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;223    const waitForBlocks = 4;224    const periodic = {225      period: 2,226      repetitions: 2,227    };228229    const dummyTx = helper.constructApiCall('api.tx.testUtils.justTakeFee', []);230    const scheduledLen = dummyTx.callIndex.length;231232    const expectedScheduledFee = (await helper.getPaymentInfo(alice, dummyTx, scheduledLen))233      .partialFee.toBigInt();234235    await helper.scheduler.scheduleAfter<DevUniqueHelper>(waitForBlocks, {scheduledId, periodic})236      .testUtils.justTakeFee(alice);237    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;238239    const aliceInitBalance = await helper.balance.getSubstrate(alice.address);240    let diff;241242    await helper.wait.forParachainBlockNumber(executionBlock);243244    const aliceBalanceAfterFirst = await helper.balance.getSubstrate(alice.address);245    expect(246      aliceBalanceAfterFirst < aliceInitBalance,247      '[after execution #1] Scheduled task should take a fee',248    ).to.be.true;249250    diff = aliceInitBalance - aliceBalanceAfterFirst;251    expect(diff).to.be.equal(252      expectedScheduledFee,253      'Scheduled task should take the right amount of fees',254    );255256    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);257258    const aliceBalanceAfterSecond = await helper.balance.getSubstrate(alice.address);259    expect(260      aliceBalanceAfterSecond < aliceBalanceAfterFirst,261      '[after execution #2] Scheduled task should take a fee',262    ).to.be.true;263264    diff = aliceBalanceAfterFirst - aliceBalanceAfterSecond;265    expect(diff).to.be.equal(266      expectedScheduledFee,267      'Scheduled task should take the right amount of fees',268    );269  });270271  // Check if we can cancel a scheduled periodic operation272  // in the same block in which it is running273  itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.TestUtils], async ({helper}) => {274    const currentBlockNumber = await helper.chain.getLatestBlockNumber();275    const blocksBeforeExecution = 10;276    const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;277278    const [279      scheduledId,280      scheduledCancelId,281    ] = helper.arrange.makeScheduledIds(2);282283    const periodic = {284      period: 5,285      repetitions: 5,286    };287288    const initTestVal = 0;289    const incTestVal = initTestVal + 1;290    const finalTestVal = initTestVal + 2;291292    await helper.testUtils.setTestValue(alice, initTestVal);293294    await helper.scheduler.scheduleAt<DevUniqueHelper>(firstExecutionBlockNumber, {scheduledId, periodic})295      .testUtils.incTestValue(alice);296297    // Cancel the inc tx after 2 executions298    // *in the same block* in which the second execution is scheduled299    await helper.scheduler.scheduleAt(300      firstExecutionBlockNumber + periodic.period,301      {scheduledId: scheduledCancelId},302    ).scheduler.cancelScheduled(alice, scheduledId);303304    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);305306    // execution #0307    expect(await helper.testUtils.testValue())308      .to.be.equal(incTestVal);309310    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);311312    // execution #1313    expect(await helper.testUtils.testValue())314      .to.be.equal(finalTestVal);315316    for(let i = 1; i < periodic.repetitions; i++) {317      await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period * (i + 1));318      expect(await helper.testUtils.testValue())319        .to.be.equal(finalTestVal);320    }321  });322323  itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.TestUtils], async ({helper}) => {324    const scheduledId = helper.arrange.makeScheduledId();325    const waitForBlocks = 4;326    const periodic = {327      period: 2,328      repetitions: 5,329    };330331    const initTestVal = 0;332    const maxTestVal = 2;333334    await helper.testUtils.setTestValue(alice, initTestVal);335336    await helper.scheduler.scheduleAfter<DevUniqueHelper>(waitForBlocks, {scheduledId, periodic})337      .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal);338    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;339340    await helper.wait.forParachainBlockNumber(executionBlock);341342    // execution #0343    expect(await helper.testUtils.testValue())344      .to.be.equal(initTestVal + 1);345346    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);347348    // execution #1349    expect(await helper.testUtils.testValue())350      .to.be.equal(initTestVal + 2);351352    await helper.wait.forParachainBlockNumber(executionBlock + 2 * periodic.period);353354    // <canceled>355    expect(await helper.testUtils.testValue())356      .to.be.equal(initTestVal + 2);357  });358359  itSub('Root can cancel any scheduled operation', async ({helper}) => {360    const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});361    const token = await collection.mintToken(bob);362363    const scheduledId = helper.arrange.makeScheduledId();364    const waitForBlocks = 4;365366    await token.scheduleAfter(waitForBlocks, {scheduledId})367      .transfer(bob, {Substrate: alice.address});368    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;369370    await helper.getSudo().scheduler.cancelScheduled(superuser, scheduledId);371372    await helper.wait.forParachainBlockNumber(executionBlock);373374    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});375  });376377  itSched('Root can set prioritized scheduled operation', async (scheduleKind, {helper}) => {378    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;379    const waitForBlocks = 4;380381    const amount = 42n * helper.balance.getOneTokenNominal();382383    const balanceBefore = await helper.balance.getSubstrate(charlie.address);384385    await helper.getSudo()386      .scheduler.scheduleAfter(waitForBlocks, {scheduledId, priority: 42})387      .balance.forceTransferToSubstrate(superuser, bob.address, charlie.address, amount);388    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;389390    await helper.wait.forParachainBlockNumber(executionBlock);391392    const balanceAfter = await helper.balance.getSubstrate(charlie.address);393394    expect(balanceAfter > balanceBefore).to.be.true;395396    const diff = balanceAfter - balanceBefore;397    expect(diff).to.be.equal(amount);398  });399400  itSub("Root can change scheduled operation's priority", async ({helper}) => {401    const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});402    const token = await collection.mintToken(bob);403404    const scheduledId = helper.arrange.makeScheduledId();405    const waitForBlocks = 6;406407    await token.scheduleAfter(waitForBlocks, {scheduledId})408      .transfer(bob, {Substrate: alice.address});409    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;410411    const priority = 112;412    await helper.getSudo().scheduler.changePriority(superuser, scheduledId, priority);413414    const priorityChanged = await helper.wait.expectEvent(waitForBlocks, Event.UniqueScheduler.PriorityChanged);415416    const [blockNumber, index] = priorityChanged.task();417    expect(blockNumber).to.be.equal(executionBlock);418    expect(index).to.be.equal(0);419420    expect(priorityChanged.priority().toString()).to.be.equal(priority.toString());421  });422423  itSub('Prioritized operations execute in valid order', async ({helper}) => {424    const [425      scheduledFirstId,426      scheduledSecondId,427    ] = helper.arrange.makeScheduledIds(2);428429    const currentBlockNumber = await helper.chain.getLatestBlockNumber();430    const blocksBeforeExecution = 6;431    const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;432433    const prioHigh = 0;434    const prioLow = 255;435436    const periodic = {437      period: 6,438      repetitions: 2,439    };440441    const amount = 1n * helper.balance.getOneTokenNominal();442443    // Scheduler a task with a lower priority first, then with a higher priority444    await helper.getSudo().scheduler.scheduleAt(firstExecutionBlockNumber, {445      scheduledId: scheduledFirstId,446      priority: prioLow,447      periodic,448    }).balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);449450    await helper.getSudo().scheduler.scheduleAt(firstExecutionBlockNumber, {451      scheduledId: scheduledSecondId,452      priority: prioHigh,453      periodic,454    }).balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);455456    const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched');457458    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);459460    // Flip priorities461    await helper.getSudo().scheduler.changePriority(superuser, scheduledFirstId, prioHigh);462    await helper.getSudo().scheduler.changePriority(superuser, scheduledSecondId, prioLow);463464    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);465466    const dispatchEvents = capture.extractCapturedEvents();467    expect(dispatchEvents.length).to.be.equal(4);468469    const dispatchedIds = dispatchEvents.map(r => r.event.data[1].toString());470471    const firstExecuctionIds = [dispatchedIds[0], dispatchedIds[1]];472    const secondExecuctionIds = [dispatchedIds[2], dispatchedIds[3]];473474    expect(firstExecuctionIds[0]).to.be.equal(scheduledSecondId);475    expect(firstExecuctionIds[1]).to.be.equal(scheduledFirstId);476477    expect(secondExecuctionIds[0]).to.be.equal(scheduledFirstId);478    expect(secondExecuctionIds[1]).to.be.equal(scheduledSecondId);479  });480481  itSched('Periodic operations always can be rescheduled', async (scheduleKind, {helper}) => {482    const maxScheduledPerBlock = 50;483    const numFilledBlocks = 3;484    const ids = helper.arrange.makeScheduledIds(numFilledBlocks * maxScheduledPerBlock + 1);485    const periodicId = scheduleKind == 'named' ? ids[0] : undefined;486    const fillIds = ids.slice(1);487488    const fillScheduleFn = scheduleKind == 'named' ? 'scheduleNamed' : 'schedule';489490    const initTestVal = 0;491    const firstExecTestVal = 1;492    const secondExecTestVal = 2;493    await helper.testUtils.setTestValue(alice, initTestVal);494495    const currentBlockNumber = await helper.chain.getLatestBlockNumber();496    const blocksBeforeExecution = 8;497    const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;498499    const period = 5;500501    const periodic = {502      period,503      repetitions: 2,504    };505506    // Fill `numFilledBlocks` blocks beginning from the block in which the second execution should occur507    const txs = [];508    for(let offset = 0; offset < numFilledBlocks; offset ++) {509      for(let i = 0; i < maxScheduledPerBlock; i++) {510511        const scheduledTx = helper.constructApiCall('api.tx.balances.transfer', [bob.address, 1n]);512513        const when = firstExecutionBlockNumber + period + offset;514        const mandatoryArgs = [when, null, null, scheduledTx];515        const scheduleArgs = scheduleKind == 'named'516          ? [fillIds[i + offset * maxScheduledPerBlock], ...mandatoryArgs]517          : mandatoryArgs;518519        const tx = helper.constructApiCall(`api.tx.scheduler.${fillScheduleFn}`, scheduleArgs);520521        txs.push(tx);522      }523    }524    await helper.executeExtrinsic(alice, 'api.tx.testUtils.batchAll', [txs], true);525526    await helper.scheduler.scheduleAt<DevUniqueHelper>(firstExecutionBlockNumber, {527      scheduledId: periodicId,528      periodic,529    }).testUtils.incTestValue(alice);530531    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);532    expect(await helper.testUtils.testValue()).to.be.equal(firstExecTestVal);533534    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + period + numFilledBlocks);535536    // The periodic operation should be postponed by `numFilledBlocks`537    for(let i = 0; i < numFilledBlocks; i++) {538      expect(await helper.testUtils.testValue(firstExecutionBlockNumber + period + i)).to.be.equal(firstExecTestVal);539    }540541    // After the `numFilledBlocks` the periodic operation will eventually be executed542    expect(await helper.testUtils.testValue()).to.be.equal(secondExecTestVal);543  });544545  itSched('scheduled operations does not change nonce', async (scheduleKind, {helper}) => {546    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;547    const blocksBeforeExecution = 4;548549    await helper.scheduler550      .scheduleAfter<DevUniqueHelper>(blocksBeforeExecution, {scheduledId})551      .balance.transferToSubstrate(alice, bob.address, 1n);552    const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;553554    const initNonce = await helper.chain.getNonce(alice.address);555556    await helper.wait.forParachainBlockNumber(executionBlock);557558    const finalNonce = await helper.chain.getNonce(alice.address);559560    expect(initNonce).to.be.equal(finalNonce);561  });562});563564describe('Negative Test: Scheduling', () => {565  let alice: IKeyringPair;566  let bob: IKeyringPair;567568  before(async function() {569    await usingPlaygrounds(async (helper, privateKey) => {570      requirePalletsOrSkip(this, helper, [Pallets.UniqueScheduler]);571572      const donor = await privateKey({url: import.meta.url});573      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);574575      await helper.testUtils.enable();576    });577  });578579  itSub("Can't overwrite a scheduled ID", async ({helper}) => {580    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});581    const token = await collection.mintToken(alice);582583    const scheduledId = helper.arrange.makeScheduledId();584    const waitForBlocks = 4;585586    await token.scheduleAfter(waitForBlocks, {scheduledId})587      .transfer(alice, {Substrate: bob.address});588    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;589590    const scheduled = helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId});591    await expect(scheduled.balance.transferToSubstrate(alice, bob.address, 1n * helper.balance.getOneTokenNominal()))592      .to.be.rejectedWith(/scheduler\.FailedToSchedule/);593594    const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);595596    await helper.wait.forParachainBlockNumber(executionBlock);597598    const bobsBalanceAfter = await helper.balance.getSubstrate(bob.address);599600    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});601    expect(bobsBalanceBefore).to.be.equal(bobsBalanceAfter);602  });603604  itSub("Can't cancel an operation which is not scheduled", async ({helper}) => {605    const scheduledId = helper.arrange.makeScheduledId();606    await expect(helper.scheduler.cancelScheduled(alice, scheduledId))607      .to.be.rejectedWith(/scheduler\.NotFound/);608  });609610  itSub("Can't cancel a non-owned scheduled operation", async ({helper}) => {611    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});612    const token = await collection.mintToken(alice);613614    const scheduledId = helper.arrange.makeScheduledId();615    const waitForBlocks = 4;616617    await token.scheduleAfter(waitForBlocks, {scheduledId})618      .transfer(alice, {Substrate: bob.address});619    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;620621    await expect(helper.scheduler.cancelScheduled(bob, scheduledId))622      .to.be.rejectedWith(/BadOrigin/);623624    await helper.wait.forParachainBlockNumber(executionBlock);625626    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});627  });628629  itSched("Regular user can't set prioritized scheduled operation", async (scheduleKind, {helper}) => {630    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;631    const waitForBlocks = 4;632633    const amount = 42n * helper.balance.getOneTokenNominal();634635    const balanceBefore = await helper.balance.getSubstrate(bob.address);636637    const scheduled = helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, priority: 42});638639    await expect(scheduled.balance.transferToSubstrate(alice, bob.address, amount))640      .to.be.rejectedWith(/BadOrigin/);641642    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;643644    await helper.wait.forParachainBlockNumber(executionBlock);645646    const balanceAfter = await helper.balance.getSubstrate(bob.address);647648    expect(balanceAfter).to.be.equal(balanceBefore);649  });650651  itSub("Regular user can't change scheduled operation's priority", async ({helper}) => {652    const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});653    const token = await collection.mintToken(bob);654655    const scheduledId = helper.arrange.makeScheduledId();656    const waitForBlocks = 4;657658    await token.scheduleAfter(waitForBlocks, {scheduledId})659      .transfer(bob, {Substrate: alice.address});660661    const priority = 112;662    await expect(helper.scheduler.changePriority(alice, scheduledId, priority))663      .to.be.rejectedWith(/BadOrigin/);664665    await helper.wait.expectEvent(waitForBlocks, Event.UniqueScheduler.PriorityChanged);666  });667});668669// Implementation of the functionality tested here was postponed/shelved670describe.skip('Sponsoring scheduling', () => {671  // let alice: IKeyringPair;672  // let bob: IKeyringPair;673674  // before(async() => {675  //   await usingApi(async (_, privateKey) => {676  //     alice = privateKey('//Alice');677  //     bob = privateKey('//Bob');678  //   });679  // });680681  it('Can sponsor scheduling a transaction', async () => {682    // const collectionId = await createCollectionExpectSuccess();683    // await setCollectionSponsorExpectSuccess(collectionId, bob.address);684    // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');685686    // await usingApi(async api => {687    //   const scheduledId = await makeScheduledId();688    //   const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);689690    //   const bobBalanceBefore = await getFreeBalance(bob);691    //   const waitForBlocks = 4;692    //   // no need to wait to check, fees must be deducted on scheduling, immediately693    //   await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, scheduledId);694    //   const bobBalanceAfter = await getFreeBalance(bob);695    //   // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;696    //   expect(bobBalanceAfter < bobBalanceBefore).to.be.true;697    //   // wait for sequentiality matters698    //   await waitNewBlocks(waitForBlocks - 1);699    // });700  });701702  it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {703    // await usingApi(async (api, privateKey) => {704    //   // Find an empty, unused account705    //   const zeroBalance = await findUnusedAddress(api, privateKey);706707    //   const collectionId = await createCollectionExpectSuccess();708709    //   // Add zeroBalance address to allow list710    //   await enablePublicMintingExpectSuccess(alice, collectionId);711    //   await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);712713    //   // Grace zeroBalance with money, enough to cover future transactions714    //   const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);715    //   await submitTransactionAsync(alice, balanceTx);716717    //   // Mint a fresh NFT718    //   const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT');719    //   const scheduledId = await makeScheduledId();720721    //   // Schedule transfer of the NFT a few blocks ahead722    //   const waitForBlocks = 5;723    //   await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, scheduledId);724725    //   // Get rid of the account's funds before the scheduled transaction takes place726    //   const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);727    //   const events = await submitTransactionAsync(zeroBalance, balanceTx2);728    //   expect(getGenericResult(events).success).to.be.true;729    //   /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved?730    //   const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any);731    //   const events = await submitTransactionAsync(alice, sudoTx);732    //   expect(getGenericResult(events).success).to.be.true;*/733734    //   // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions735    //   await waitNewBlocks(waitForBlocks - 3);736737    //   expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));738    // });739  });740741  it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {742    // const collectionId = await createCollectionExpectSuccess();743744    // await usingApi(async (api, privateKey) => {745    //   const zeroBalance = await findUnusedAddress(api, privateKey);746    //   const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);747    //   await submitTransactionAsync(alice, balanceTx);748749    //   await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address);750    //   await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance);751752    //   const scheduledId = await makeScheduledId();753    //   const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);754755    //   const waitForBlocks = 5;756    //   await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, scheduledId);757758    //   const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);759    //   const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);760    //   const events = await submitTransactionAsync(alice, sudoTx);761    //   expect(getGenericResult(events).success).to.be.true;762763    //   // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions764    //   await waitNewBlocks(waitForBlocks - 3);765766    //   expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address));767    // });768  });769770  it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {771    // const collectionId = await createCollectionExpectSuccess();772    // await setCollectionSponsorExpectSuccess(collectionId, bob.address);773    // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');774775    // await usingApi(async (api, privateKey) => {776    //   const zeroBalance = await findUnusedAddress(api, privateKey);777778    //   await enablePublicMintingExpectSuccess(alice, collectionId);779    //   await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);780781    //   const bobBalanceBefore = await getFreeBalance(bob);782783    //   const createData = {nft: {const_data: [], variable_data: []}};784    //   const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any);785    //   const scheduledId = await makeScheduledId();786787    //   /*const badTransaction = async function () {788    //     await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);789    //   };790    //   await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/791792    //   await expect(scheduleAfter(api, creationTx, zeroBalance, 3, scheduledId, 1, 3)).to.be.rejectedWith(/Inability to pay some fees/);793794    //   expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);795    // });796  });797});
after · tests/src/scheduler.seqtest.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util';18import {IKeyringPair} from '@polkadot/types/types';19import {DevUniqueHelper, Event} from './util/playgrounds/unique.dev';2021describe('Scheduling token and balance transfers', () => {22  let superuser: IKeyringPair;23  let alice: IKeyringPair;24  let bob: IKeyringPair;25  let charlie: IKeyringPair;2627  before(async function() {28    await usingPlaygrounds(async (helper, privateKey) => {29      requirePalletsOrSkip(this, helper, [Pallets.UniqueScheduler]);3031      superuser = await privateKey('//Alice');32      const donor = await privateKey({url: import.meta.url});33      [alice, bob, charlie] = await helper.arrange.createAccounts([100n, 100n, 100n], donor);3435      await helper.testUtils.enable();36    });37  });3839  beforeEach(async () => {40    await usingPlaygrounds(async (helper) => {41      await helper.wait.noScheduledTasks();42    });43  });4445  itSched('Can delay a transfer of an owned token', async (scheduleKind, {helper}) => {46    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});47    const token = await collection.mintToken(alice);48    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;49    const blocksBeforeExecution = 4;50    await helper.scheduler.scheduleAfter(blocksBeforeExecution, {scheduledId})51      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});52    const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;5354    expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});5556    await helper.wait.forParachainBlockNumber(executionBlock);5758    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});59  });6061  itSched('Can transfer funds periodically', async (scheduleKind, {helper}) => {62    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;63    const waitForBlocks = 1;6465    const amount = 1n * helper.balance.getOneTokenNominal();66    const periodic = {67      period: 2,68      repetitions: 2,69    };7071    const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);7273    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic})74      .balance.transferToSubstrate(alice, bob.address, amount);75    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;7677    await helper.wait.forParachainBlockNumber(executionBlock);7879    const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);80    expect(bobsBalanceAfterFirst)81      .to.be.equal(82        bobsBalanceBefore + 1n * amount,83        '#1 Balance of the recipient should be increased by 1 * amount',84      );8586    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);8788    const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);89    expect(bobsBalanceAfterSecond)90      .to.be.equal(91        bobsBalanceBefore + 2n * amount,92        '#2 Balance of the recipient should be increased by 2 * amount',93      );94  });9596  itSub('Can cancel a scheduled operation which has not yet taken effect', async ({helper}) => {97    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});98    const token = await collection.mintToken(alice);99100    const scheduledId = helper.arrange.makeScheduledId();101    const waitForBlocks = 4;102103    expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});104105    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})106      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});107    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;108109    await helper.scheduler.cancelScheduled(alice, scheduledId);110111    await helper.wait.forParachainBlockNumber(executionBlock);112113    expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});114  });115116  itSub('Can cancel a periodic operation (transfer of funds)', async ({helper}) => {117    const waitForBlocks = 1;118    const periodic = {119      period: 3,120      repetitions: 2,121    };122123    const scheduledId = helper.arrange.makeScheduledId();124125    const amount = 1n * helper.balance.getOneTokenNominal();126127    const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);128129    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, periodic})130      .balance.transferToSubstrate(alice, bob.address, amount);131    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;132133    await helper.wait.forParachainBlockNumber(executionBlock);134135    const bobsBalanceAfterFirst = await helper.balance.getSubstrate(bob.address);136137    expect(bobsBalanceAfterFirst)138      .to.be.equal(139        bobsBalanceBefore + 1n * amount,140        '#1 Balance of the recipient should be increased by 1 * amount',141      );142143    await helper.scheduler.cancelScheduled(alice, scheduledId);144    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);145146    const bobsBalanceAfterSecond = await helper.balance.getSubstrate(bob.address);147    expect(bobsBalanceAfterSecond)148      .to.be.equal(149        bobsBalanceAfterFirst,150        '#2 Balance of the recipient should not be changed',151      );152  });153154  itSched('scheduler will not insert more tasks than allowed', async (scheduleKind, {helper}) => {155    const maxScheduledPerBlock = 50;156    let fillScheduledIds = new Array(maxScheduledPerBlock);157    let extraScheduledId = undefined;158159    if(scheduleKind == 'named') {160      const scheduledIds = helper.arrange.makeScheduledIds(maxScheduledPerBlock + 1);161      fillScheduledIds = scheduledIds.slice(0, maxScheduledPerBlock);162      extraScheduledId = scheduledIds[maxScheduledPerBlock];163    }164165    // Since the dev node has Instant Seal,166    // we need a larger gap between the current block and the target one.167    //168    // We will schedule `maxScheduledPerBlock` transaction into the target block,169    // so we need at least `maxScheduledPerBlock`-wide gap.170    // We add some additional blocks to this gap to mitigate possible PolkadotJS delays.171    const waitForBlocks = await helper.arrange.isDevNode() ? maxScheduledPerBlock + 5 : 5;172173    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks;174175    const amount = 1n * helper.balance.getOneTokenNominal();176177    const balanceBefore = await helper.balance.getSubstrate(bob.address);178179    // Fill the target block180    for(let i = 0; i < maxScheduledPerBlock; i++) {181      await helper.scheduler.scheduleAt(executionBlock, {scheduledId: fillScheduledIds[i]})182        .balance.transferToSubstrate(superuser, bob.address, amount);183    }184185    // Try to schedule a task into a full block186    await expect(helper.scheduler.scheduleAt(executionBlock, {scheduledId: extraScheduledId})187      .balance.transferToSubstrate(superuser, bob.address, amount))188      .to.be.rejectedWith(/scheduler\.AgendaIsExhausted/);189190    await helper.wait.forParachainBlockNumber(executionBlock);191192    const balanceAfter = await helper.balance.getSubstrate(bob.address);193194    expect(balanceAfter > balanceBefore).to.be.true;195196    const diff = balanceAfter - balanceBefore;197    expect(diff).to.be.equal(amount * BigInt(maxScheduledPerBlock));198  });199200  itSched.ifWithPallets('Scheduled tasks are transactional', [Pallets.TestUtils], async (scheduleKind, {helper}) => {201    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;202    const waitForBlocks = 4;203204    const initTestVal = 42;205    const changedTestVal = 111;206207    await helper.testUtils.setTestValue(alice, initTestVal);208209    await helper.scheduler.scheduleAfter<DevUniqueHelper>(waitForBlocks, {scheduledId})210      .testUtils.setTestValueAndRollback(alice, changedTestVal);211    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;212213    await helper.wait.forParachainBlockNumber(executionBlock);214215    const testVal = await helper.testUtils.testValue();216    expect(testVal, 'The test value should NOT be commited')217      .to.be.equal(initTestVal);218  });219220  itSched.ifWithPallets('Scheduled tasks should take correct fees', [Pallets.TestUtils], async function(scheduleKind, {helper}) {221    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;222    const waitForBlocks = 4;223    const periodic = {224      period: 2,225      repetitions: 2,226    };227228    const dummyTx = helper.constructApiCall('api.tx.testUtils.justTakeFee', []);229    const scheduledLen = dummyTx.callIndex.length;230231    const expectedScheduledFee = (await helper.getPaymentInfo(alice, dummyTx, scheduledLen))232      .partialFee.toBigInt();233234    await helper.scheduler.scheduleAfter<DevUniqueHelper>(waitForBlocks, {scheduledId, periodic})235      .testUtils.justTakeFee(alice);236    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;237238    const aliceInitBalance = await helper.balance.getSubstrate(alice.address);239    let diff;240241    await helper.wait.forParachainBlockNumber(executionBlock);242243    const aliceBalanceAfterFirst = await helper.balance.getSubstrate(alice.address);244    expect(245      aliceBalanceAfterFirst < aliceInitBalance,246      '[after execution #1] Scheduled task should take a fee',247    ).to.be.true;248249    diff = aliceInitBalance - aliceBalanceAfterFirst;250    expect(diff).to.be.equal(251      expectedScheduledFee,252      'Scheduled task should take the right amount of fees',253    );254255    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);256257    const aliceBalanceAfterSecond = await helper.balance.getSubstrate(alice.address);258    expect(259      aliceBalanceAfterSecond < aliceBalanceAfterFirst,260      '[after execution #2] Scheduled task should take a fee',261    ).to.be.true;262263    diff = aliceBalanceAfterFirst - aliceBalanceAfterSecond;264    expect(diff).to.be.equal(265      expectedScheduledFee,266      'Scheduled task should take the right amount of fees',267    );268  });269270  // Check if we can cancel a scheduled periodic operation271  // in the same block in which it is running272  itSub.ifWithPallets('Can cancel the periodic sheduled tx when the tx is running', [Pallets.TestUtils], async ({helper}) => {273    const currentBlockNumber = await helper.chain.getLatestBlockNumber();274    const blocksBeforeExecution = 10;275    const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;276277    const [278      scheduledId,279      scheduledCancelId,280    ] = helper.arrange.makeScheduledIds(2);281282    const periodic = {283      period: 5,284      repetitions: 5,285    };286287    const initTestVal = 0;288    const incTestVal = initTestVal + 1;289    const finalTestVal = initTestVal + 2;290291    await helper.testUtils.setTestValue(alice, initTestVal);292293    await helper.scheduler.scheduleAt<DevUniqueHelper>(firstExecutionBlockNumber, {scheduledId, periodic})294      .testUtils.incTestValue(alice);295296    // Cancel the inc tx after 2 executions297    // *in the same block* in which the second execution is scheduled298    await helper.scheduler.scheduleAt(299      firstExecutionBlockNumber + periodic.period,300      {scheduledId: scheduledCancelId},301    ).scheduler.cancelScheduled(alice, scheduledId);302303    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);304305    // execution #0306    expect(await helper.testUtils.testValue())307      .to.be.equal(incTestVal);308309    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);310311    // execution #1312    expect(await helper.testUtils.testValue())313      .to.be.equal(finalTestVal);314315    for(let i = 1; i < periodic.repetitions; i++) {316      await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period * (i + 1));317      expect(await helper.testUtils.testValue())318        .to.be.equal(finalTestVal);319    }320  });321322  itSub.ifWithPallets('A scheduled operation can cancel itself', [Pallets.TestUtils], async ({helper}) => {323    const scheduledId = helper.arrange.makeScheduledId();324    const waitForBlocks = 4;325    const periodic = {326      period: 2,327      repetitions: 5,328    };329330    const initTestVal = 0;331    const maxTestVal = 2;332333    await helper.testUtils.setTestValue(alice, initTestVal);334335    await helper.scheduler.scheduleAfter<DevUniqueHelper>(waitForBlocks, {scheduledId, periodic})336      .testUtils.selfCancelingInc(alice, scheduledId, maxTestVal);337    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;338339    await helper.wait.forParachainBlockNumber(executionBlock);340341    // execution #0342    expect(await helper.testUtils.testValue())343      .to.be.equal(initTestVal + 1);344345    await helper.wait.forParachainBlockNumber(executionBlock + periodic.period);346347    // execution #1348    expect(await helper.testUtils.testValue())349      .to.be.equal(initTestVal + 2);350351    await helper.wait.forParachainBlockNumber(executionBlock + 2 * periodic.period);352353    // <canceled>354    expect(await helper.testUtils.testValue())355      .to.be.equal(initTestVal + 2);356  });357358  itSub('Root can cancel any scheduled operation', async ({helper}) => {359    const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});360    const token = await collection.mintToken(bob);361362    const scheduledId = helper.arrange.makeScheduledId();363    const waitForBlocks = 4;364365    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})366      .nft.transferToken(bob, collection.collectionId, token.tokenId, {Substrate: alice.address});367    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;368369    await helper.getSudo().scheduler.cancelScheduled(superuser, scheduledId);370371    await helper.wait.forParachainBlockNumber(executionBlock);372373    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});374  });375376  itSched('Root can set prioritized scheduled operation', async (scheduleKind, {helper}) => {377    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;378    const waitForBlocks = 4;379380    const amount = 42n * helper.balance.getOneTokenNominal();381382    const balanceBefore = await helper.balance.getSubstrate(charlie.address);383384    await helper.getSudo()385      .scheduler.scheduleAfter(waitForBlocks, {scheduledId, priority: 42})386      .balance.forceTransferToSubstrate(superuser, bob.address, charlie.address, amount);387    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;388389    await helper.wait.forParachainBlockNumber(executionBlock);390391    const balanceAfter = await helper.balance.getSubstrate(charlie.address);392393    expect(balanceAfter > balanceBefore).to.be.true;394395    const diff = balanceAfter - balanceBefore;396    expect(diff).to.be.equal(amount);397  });398399  itSub("Root can change scheduled operation's priority", async ({helper}) => {400    const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});401    const token = await collection.mintToken(bob);402403    const scheduledId = helper.arrange.makeScheduledId();404    const waitForBlocks = 6;405406    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})407      .nft.transferToken(bob, collection.collectionId, token.tokenId, {Substrate: alice.address});408    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;409410    const priority = 112;411    await helper.getSudo().scheduler.changePriority(superuser, scheduledId, priority);412413    const priorityChanged = await helper.wait.expectEvent(waitForBlocks, Event.UniqueScheduler.PriorityChanged);414415    const [blockNumber, index] = priorityChanged.task();416    expect(blockNumber).to.be.equal(executionBlock);417    expect(index).to.be.equal(0);418419    expect(priorityChanged.priority().toString()).to.be.equal(priority.toString());420  });421422  itSub('Prioritized operations execute in valid order', async ({helper}) => {423    const [424      scheduledFirstId,425      scheduledSecondId,426    ] = helper.arrange.makeScheduledIds(2);427428    const currentBlockNumber = await helper.chain.getLatestBlockNumber();429    const blocksBeforeExecution = 6;430    const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;431432    const prioHigh = 0;433    const prioLow = 255;434435    const periodic = {436      period: 6,437      repetitions: 2,438    };439440    const amount = 1n * helper.balance.getOneTokenNominal();441442    // Scheduler a task with a lower priority first, then with a higher priority443    await helper.getSudo().scheduler.scheduleAt(firstExecutionBlockNumber, {444      scheduledId: scheduledFirstId,445      priority: prioLow,446      periodic,447    }).balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);448449    await helper.getSudo().scheduler.scheduleAt(firstExecutionBlockNumber, {450      scheduledId: scheduledSecondId,451      priority: prioHigh,452      periodic,453    }).balance.forceTransferToSubstrate(superuser, alice.address, bob.address, amount);454455    const capture = await helper.arrange.captureEvents('scheduler', 'Dispatched');456457    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);458459    // Flip priorities460    await helper.getSudo().scheduler.changePriority(superuser, scheduledFirstId, prioHigh);461    await helper.getSudo().scheduler.changePriority(superuser, scheduledSecondId, prioLow);462463    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + periodic.period);464465    const dispatchEvents = capture.extractCapturedEvents();466    expect(dispatchEvents.length).to.be.equal(4);467468    const dispatchedIds = dispatchEvents.map(r => r.event.data[1].toString());469470    const firstExecuctionIds = [dispatchedIds[0], dispatchedIds[1]];471    const secondExecuctionIds = [dispatchedIds[2], dispatchedIds[3]];472473    expect(firstExecuctionIds[0]).to.be.equal(scheduledSecondId);474    expect(firstExecuctionIds[1]).to.be.equal(scheduledFirstId);475476    expect(secondExecuctionIds[0]).to.be.equal(scheduledFirstId);477    expect(secondExecuctionIds[1]).to.be.equal(scheduledSecondId);478  });479480  itSched('Periodic operations always can be rescheduled', async (scheduleKind, {helper}) => {481    const maxScheduledPerBlock = 50;482    const numFilledBlocks = 3;483    const ids = helper.arrange.makeScheduledIds(numFilledBlocks * maxScheduledPerBlock + 1);484    const periodicId = scheduleKind == 'named' ? ids[0] : undefined;485    const fillIds = ids.slice(1);486487    const fillScheduleFn = scheduleKind == 'named' ? 'scheduleNamed' : 'schedule';488489    const initTestVal = 0;490    const firstExecTestVal = 1;491    const secondExecTestVal = 2;492    await helper.testUtils.setTestValue(alice, initTestVal);493494    const currentBlockNumber = await helper.chain.getLatestBlockNumber();495    const blocksBeforeExecution = 8;496    const firstExecutionBlockNumber = currentBlockNumber + blocksBeforeExecution;497498    const period = 5;499500    const periodic = {501      period,502      repetitions: 2,503    };504505    // Fill `numFilledBlocks` blocks beginning from the block in which the second execution should occur506    const txs = [];507    for(let offset = 0; offset < numFilledBlocks; offset ++) {508      for(let i = 0; i < maxScheduledPerBlock; i++) {509510        const scheduledTx = helper.constructApiCall('api.tx.balances.transfer', [bob.address, 1n]);511512        const when = firstExecutionBlockNumber + period + offset;513        const mandatoryArgs = [when, null, null, scheduledTx];514        const scheduleArgs = scheduleKind == 'named'515          ? [fillIds[i + offset * maxScheduledPerBlock], ...mandatoryArgs]516          : mandatoryArgs;517518        const tx = helper.constructApiCall(`api.tx.scheduler.${fillScheduleFn}`, scheduleArgs);519520        txs.push(tx);521      }522    }523    await helper.executeExtrinsic(alice, 'api.tx.testUtils.batchAll', [txs], true);524525    await helper.scheduler.scheduleAt<DevUniqueHelper>(firstExecutionBlockNumber, {526      scheduledId: periodicId,527      periodic,528    }).testUtils.incTestValue(alice);529530    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber);531    expect(await helper.testUtils.testValue()).to.be.equal(firstExecTestVal);532533    await helper.wait.forParachainBlockNumber(firstExecutionBlockNumber + period + numFilledBlocks);534535    // The periodic operation should be postponed by `numFilledBlocks`536    for(let i = 0; i < numFilledBlocks; i++) {537      expect(await helper.testUtils.testValue(firstExecutionBlockNumber + period + i)).to.be.equal(firstExecTestVal);538    }539540    // After the `numFilledBlocks` the periodic operation will eventually be executed541    expect(await helper.testUtils.testValue()).to.be.equal(secondExecTestVal);542  });543544  itSched('scheduled operations does not change nonce', async (scheduleKind, {helper}) => {545    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;546    const blocksBeforeExecution = 4;547548    await helper.scheduler549      .scheduleAfter<DevUniqueHelper>(blocksBeforeExecution, {scheduledId})550      .balance.transferToSubstrate(alice, bob.address, 1n);551    const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;552553    const initNonce = await helper.chain.getNonce(alice.address);554555    await helper.wait.forParachainBlockNumber(executionBlock);556557    const finalNonce = await helper.chain.getNonce(alice.address);558559    expect(initNonce).to.be.equal(finalNonce);560  });561});562563describe('Negative Test: Scheduling', () => {564  let alice: IKeyringPair;565  let bob: IKeyringPair;566567  before(async function() {568    await usingPlaygrounds(async (helper, privateKey) => {569      requirePalletsOrSkip(this, helper, [Pallets.UniqueScheduler]);570571      const donor = await privateKey({url: import.meta.url});572      [alice, bob] = await helper.arrange.createAccounts([100n, 100n], donor);573574      await helper.testUtils.enable();575    });576  });577578  itSub("Can't overwrite a scheduled ID", async ({helper}) => {579    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});580    const token = await collection.mintToken(alice);581582    const scheduledId = helper.arrange.makeScheduledId();583    const waitForBlocks = 4;584585    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})586      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});587    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;588589    const scheduled = helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId});590    await expect(scheduled.balance.transferToSubstrate(alice, bob.address, 1n * helper.balance.getOneTokenNominal()))591      .to.be.rejectedWith(/scheduler\.FailedToSchedule/);592593    const bobsBalanceBefore = await helper.balance.getSubstrate(bob.address);594595    await helper.wait.forParachainBlockNumber(executionBlock);596597    const bobsBalanceAfter = await helper.balance.getSubstrate(bob.address);598599    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});600    expect(bobsBalanceBefore).to.be.equal(bobsBalanceAfter);601  });602603  itSub("Can't cancel an operation which is not scheduled", async ({helper}) => {604    const scheduledId = helper.arrange.makeScheduledId();605    await expect(helper.scheduler.cancelScheduled(alice, scheduledId))606      .to.be.rejectedWith(/scheduler\.NotFound/);607  });608609  itSub("Can't cancel a non-owned scheduled operation", async ({helper}) => {610    const collection = await helper.nft.mintCollection(alice, {tokenPrefix: 'schd'});611    const token = await collection.mintToken(alice);612613    const scheduledId = helper.arrange.makeScheduledId();614    const waitForBlocks = 4;615616    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})617      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});618    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;619620    await expect(helper.scheduler.cancelScheduled(bob, scheduledId))621      .to.be.rejectedWith(/BadOrigin/);622623    await helper.wait.forParachainBlockNumber(executionBlock);624625    expect(await token.getOwner()).to.be.deep.equal({Substrate: bob.address});626  });627628  itSched("Regular user can't set prioritized scheduled operation", async (scheduleKind, {helper}) => {629    const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;630    const waitForBlocks = 4;631632    const amount = 42n * helper.balance.getOneTokenNominal();633634    const balanceBefore = await helper.balance.getSubstrate(bob.address);635636    const scheduled = helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId, priority: 42});637638    await expect(scheduled.balance.transferToSubstrate(alice, bob.address, amount))639      .to.be.rejectedWith(/BadOrigin/);640641    const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;642643    await helper.wait.forParachainBlockNumber(executionBlock);644645    const balanceAfter = await helper.balance.getSubstrate(bob.address);646647    expect(balanceAfter).to.be.equal(balanceBefore);648  });649650  itSub("Regular user can't change scheduled operation's priority", async ({helper}) => {651    const collection = await helper.nft.mintCollection(bob, {tokenPrefix: 'schd'});652    const token = await collection.mintToken(bob);653654    const scheduledId = helper.arrange.makeScheduledId();655    const waitForBlocks = 4;656657    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})658      .nft.transferToken(bob, collection.collectionId, token.tokenId, {Substrate: alice.address});659660    const priority = 112;661    await expect(helper.scheduler.changePriority(alice, scheduledId, priority))662      .to.be.rejectedWith(/BadOrigin/);663664    await helper.wait.expectEvent(waitForBlocks, Event.UniqueScheduler.PriorityChanged);665  });666});667668// Implementation of the functionality tested here was postponed/shelved669describe.skip('Sponsoring scheduling', () => {670  // let alice: IKeyringPair;671  // let bob: IKeyringPair;672673  // before(async() => {674  //   await usingApi(async (_, privateKey) => {675  //     alice = privateKey('//Alice');676  //     bob = privateKey('//Bob');677  //   });678  // });679680  it('Can sponsor scheduling a transaction', async () => {681    // const collectionId = await createCollectionExpectSuccess();682    // await setCollectionSponsorExpectSuccess(collectionId, bob.address);683    // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');684685    // await usingApi(async api => {686    //   const scheduledId = await makeScheduledId();687    //   const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);688689    //   const bobBalanceBefore = await getFreeBalance(bob);690    //   const waitForBlocks = 4;691    //   // no need to wait to check, fees must be deducted on scheduling, immediately692    //   await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, bob, 0, waitForBlocks, scheduledId);693    //   const bobBalanceAfter = await getFreeBalance(bob);694    //   // expect(aliceBalanceAfter == aliceBalanceBefore).to.be.true;695    //   expect(bobBalanceAfter < bobBalanceBefore).to.be.true;696    //   // wait for sequentiality matters697    //   await waitNewBlocks(waitForBlocks - 1);698    // });699  });700701  it('Schedules and dispatches a transaction even if the caller has no funds at the time of the dispatch', async () => {702    // await usingApi(async (api, privateKey) => {703    //   // Find an empty, unused account704    //   const zeroBalance = await findUnusedAddress(api, privateKey);705706    //   const collectionId = await createCollectionExpectSuccess();707708    //   // Add zeroBalance address to allow list709    //   await enablePublicMintingExpectSuccess(alice, collectionId);710    //   await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);711712    //   // Grace zeroBalance with money, enough to cover future transactions713    //   const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);714    //   await submitTransactionAsync(alice, balanceTx);715716    //   // Mint a fresh NFT717    //   const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT');718    //   const scheduledId = await makeScheduledId();719720    //   // Schedule transfer of the NFT a few blocks ahead721    //   const waitForBlocks = 5;722    //   await scheduleTransferExpectSuccess(api, collectionId, tokenId, zeroBalance, alice, 1, waitForBlocks, scheduledId);723724    //   // Get rid of the account's funds before the scheduled transaction takes place725    //   const balanceTx2 = api.tx.balances.transfer(alice.address, UNIQUE * 68n / 100n);726    //   const events = await submitTransactionAsync(zeroBalance, balanceTx2);727    //   expect(getGenericResult(events).success).to.be.true;728    //   /*const emptyBalanceTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0); // do not null reserved?729    //   const sudoTx = api.tx.sudo.sudo(emptyBalanceTx as any);730    //   const events = await submitTransactionAsync(alice, sudoTx);731    //   expect(getGenericResult(events).success).to.be.true;*/732733    //   // Wait for a certain number of blocks, discarding the ones that already happened while accepting the late transactions734    //   await waitNewBlocks(waitForBlocks - 3);735736    //   expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(alice.address));737    // });738  });739740  it('Sponsor going bankrupt does not impact a scheduled transaction', async () => {741    // const collectionId = await createCollectionExpectSuccess();742743    // await usingApi(async (api, privateKey) => {744    //   const zeroBalance = await findUnusedAddress(api, privateKey);745    //   const balanceTx = api.tx.balances.transfer(zeroBalance.address, 1n * UNIQUE);746    //   await submitTransactionAsync(alice, balanceTx);747748    //   await setCollectionSponsorExpectSuccess(collectionId, zeroBalance.address);749    //   await confirmSponsorshipByKeyExpectSuccess(collectionId, zeroBalance);750751    //   const scheduledId = await makeScheduledId();752    //   const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);753754    //   const waitForBlocks = 5;755    //   await scheduleTransferExpectSuccess(api, collectionId, tokenId, alice, zeroBalance, 1, waitForBlocks, scheduledId);756757    //   const emptyBalanceSponsorTx = api.tx.balances.setBalance(zeroBalance.address, 0, 0);758    //   const sudoTx = api.tx.sudo.sudo(emptyBalanceSponsorTx as any);759    //   const events = await submitTransactionAsync(alice, sudoTx);760    //   expect(getGenericResult(events).success).to.be.true;761762    //   // Wait for a certain number of blocks, save for the ones that already happened while accepting the late transactions763    //   await waitNewBlocks(waitForBlocks - 3);764765    //   expect(await getTokenOwner(api, collectionId, tokenId)).to.be.deep.equal(normalizeAccountId(zeroBalance.address));766    // });767  });768769  it('Exceeding sponsor rate limit without having enough funds prevents scheduling a periodic transaction', async () => {770    // const collectionId = await createCollectionExpectSuccess();771    // await setCollectionSponsorExpectSuccess(collectionId, bob.address);772    // await confirmSponsorshipExpectSuccess(collectionId, '//Bob');773774    // await usingApi(async (api, privateKey) => {775    //   const zeroBalance = await findUnusedAddress(api, privateKey);776777    //   await enablePublicMintingExpectSuccess(alice, collectionId);778    //   await addToAllowListExpectSuccess(alice, collectionId, zeroBalance.address);779780    //   const bobBalanceBefore = await getFreeBalance(bob);781782    //   const createData = {nft: {const_data: [], variable_data: []}};783    //   const creationTx = api.tx.unique.createItem(collectionId, normalizeAccountId(zeroBalance), createData as any);784    //   const scheduledId = await makeScheduledId();785786    //   /*const badTransaction = async function () {787    //     await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);788    //   };789    //   await expect(badTransaction()).to.be.rejectedWith('Inability to pay some fees');*/790791    //   await expect(scheduleAfter(api, creationTx, zeroBalance, 3, scheduledId, 1, 3)).to.be.rejectedWith(/Inability to pay some fees/);792793    //   expect(await getFreeBalance(bob)).to.be.equal(bobBalanceBefore);794    // });795  });796});
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -216,43 +216,6 @@
   },
 }
 
-export interface IForeignAssetMetadata {
-  name?: number | Uint8Array,
-  symbol?: string,
-  decimals?: number,
-  minimalBalance?: bigint,
-}
-
-export interface MoonbeamAssetInfo {
-  location: any,
-  metadata: {
-    name: string,
-    symbol: string,
-    decimals: number,
-    isFrozen: boolean,
-    minimalBalance: bigint,
-  },
-  existentialDeposit: bigint,
-  isSufficient: boolean,
-  unitsPerSecond: bigint,
-  numAssetsWeightHint: number,
-}
-
-export interface AcalaAssetMetadata {
-  name: string,
-  symbol: string,
-  decimals: number,
-  minimalBalance: bigint,
-}
-
-export interface DemocracyStandardAccountVote {
-  balance: bigint,
-  vote: {
-    aye: boolean,
-    conviction: number,
-  },
-}
-
 export interface DemocracySplitAccount {
   aye: bigint,
   nay: bigint,
addedtests/src/util/playgrounds/types.xcm.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/types.xcm.ts
@@ -0,0 +1,36 @@
+export interface AcalaAssetMetadata {
+  name: string,
+  symbol: string,
+  decimals: number,
+  minimalBalance: bigint,
+}
+
+export interface MoonbeamAssetInfo {
+  location: any,
+  metadata: {
+    name: string,
+    symbol: string,
+    decimals: number,
+    isFrozen: boolean,
+    minimalBalance: bigint,
+  },
+  existentialDeposit: bigint,
+  isSufficient: boolean,
+  unitsPerSecond: bigint,
+  numAssetsWeightHint: number,
+}
+
+export interface DemocracyStandardAccountVote {
+  balance: bigint,
+  vote: {
+    aye: boolean,
+    conviction: number,
+  },
+}
+
+export interface IForeignAssetMetadata {
+  name?: number | Uint8Array,
+  symbol?: string,
+  decimals?: number,
+  minimalBalance?: bigint,
+}
\ No newline at end of file
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -3,16 +3,18 @@
 
 import {stringToU8a} from '@polkadot/util';
 import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';
-import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper, PolkadexHelper} from './unique';
+import {UniqueHelper, ChainHelperBase, ChainHelperBaseConstructor, HelperGroup, UniqueHelperConstructor} from './unique';
 import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';
 import * as defs from '../../interfaces/definitions';
 import {IKeyringPair} from '@polkadot/types/types';
 import {EventRecord} from '@polkadot/types/interfaces';
-import {ICrossAccountId, IPovInfo, ITransactionResult, TSigner} from './types';
+import {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types';
 import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';
-import {VoidFn} from '@polkadot/api/types';
+import {SignerOptions, VoidFn} from '@polkadot/api/types';
 import {Pallets} from '..';
 import {spawnSync} from 'child_process';
+import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup} from './unique.xcm';
+import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, ICollectiveGroup, IFellowshipGroup, RankedCollectiveGroup, ReferendaGroup} from './unique.governance';
 
 export class SilentLogger {
   log(_msg: any, _level: any): void { }
@@ -260,6 +262,192 @@
   };
 }
 
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {
+  return class extends Base {
+    constructor(...args: any[]) {
+      super(...args);
+    }
+
+    async executeExtrinsic(
+      sender: IKeyringPair,
+      extrinsic: string,
+      params: any[],
+      expectSuccess?: boolean,
+      options: Partial<SignerOptions> | null = null,
+    ): Promise<ITransactionResult> {
+      const call = this.constructApiCall(extrinsic, params);
+      const result = await super.executeExtrinsic(
+        sender,
+        'api.tx.sudo.sudo',
+        [call],
+        expectSuccess,
+        options,
+      );
+
+      if(result.status === 'Fail') return result;
+
+      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
+      if(data.isErr) {
+        if(data.asErr.isModule) {
+          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
+          const metaError = super.getApi()?.registry.findMetaError(error);
+          throw new Error(`${metaError.section}.${metaError.name}`);
+        } else if(data.asErr.isToken) {
+          throw new Error(`Token: ${data.asErr.asToken}`);
+        }
+        // May be [object Object] in case of unhandled non-unit enum
+        throw new Error(`Misc: ${data.asErr.toHuman()}`);
+      }
+      return result;
+    }
+    async executeExtrinsicUncheckedWeight(
+      sender: IKeyringPair,
+      extrinsic: string,
+      params: any[],
+      expectSuccess?: boolean,
+      options: Partial<SignerOptions> | null = null,
+    ): Promise<ITransactionResult> {
+      const call = this.constructApiCall(extrinsic, params);
+      const result = await super.executeExtrinsic(
+        sender,
+        'api.tx.sudo.sudoUncheckedWeight',
+        [call, {refTime: 0, proofSize: 0}],
+        expectSuccess,
+        options,
+      );
+
+      if(result.status === 'Fail') return result;
+
+      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
+      if(data.isErr) {
+        if(data.asErr.isModule) {
+          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
+          const metaError = super.getApi()?.registry.findMetaError(error);
+          throw new Error(`${metaError.section}.${metaError.name}`);
+        } else if(data.asErr.isToken) {
+          throw new Error(`Token: ${data.asErr.asToken}`);
+        }
+        // May be [object Object] in case of unhandled non-unit enum
+        throw new Error(`Misc: ${data.asErr.toHuman()}`);
+      }
+      return result;
+    }
+  };
+}
+
+class SchedulerGroup extends HelperGroup<UniqueHelper> {
+  constructor(helper: UniqueHelper) {
+    super(helper);
+  }
+
+  cancelScheduled(signer: TSigner, scheduledId: string) {
+    return this.helper.executeExtrinsic(
+      signer,
+      'api.tx.scheduler.cancelNamed',
+      [scheduledId],
+      true,
+    );
+  }
+
+  changePriority(signer: TSigner, scheduledId: string, priority: number) {
+    return this.helper.executeExtrinsic(
+      signer,
+      'api.tx.scheduler.changeNamedPriority',
+      [scheduledId, priority],
+      true,
+    );
+  }
+
+  scheduleAt<T extends DevUniqueHelper>(
+    executionBlockNumber: number,
+    options: ISchedulerOptions = {},
+  ) {
+    return this.schedule<T>('schedule', executionBlockNumber, options);
+  }
+
+  scheduleAfter<T extends DevUniqueHelper>(
+    blocksBeforeExecution: number,
+    options: ISchedulerOptions = {},
+  ) {
+    return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);
+  }
+
+  schedule<T extends UniqueHelper>(
+    scheduleFn: 'schedule' | 'scheduleAfter',
+    blocksNum: number,
+    options: ISchedulerOptions = {},
+  ) {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);
+    return this.helper.clone(ScheduledHelperType, {
+      scheduleFn,
+      blocksNum,
+      options,
+    }) as T;
+  }
+}
+
+class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {
+  //todo:collator documentation
+  addInvulnerable(signer: TSigner, address: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);
+  }
+
+  removeInvulnerable(signer: TSigner, address: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);
+  }
+
+  async getInvulnerables(): Promise<string[]> {
+    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());
+  }
+
+  /** and also total max invulnerables */
+  maxCollators(): number {
+    return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);
+  }
+
+  async getDesiredCollators(): Promise<number> {
+    return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();
+  }
+
+  setLicenseBond(signer: TSigner, amount: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);
+  }
+
+  async getLicenseBond(): Promise<bigint> {
+    return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();
+  }
+
+  obtainLicense(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);
+  }
+
+  releaseLicense(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);
+  }
+
+  forceReleaseLicense(signer: TSigner, released: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);
+  }
+
+  async hasLicense(address: string): Promise<bigint> {
+    return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();
+  }
+
+  onboard(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);
+  }
+
+  offboard(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);
+  }
+
+  async getCandidates(): Promise<string[]> {
+    return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());
+  }
+}
+
 export class DevUniqueHelper extends UniqueHelper {
   /**
    * Arrange methods for tests
@@ -269,6 +457,16 @@
   admin: AdminGroup;
   session: SessionGroup;
   testUtils: TestUtilGroup;
+  foreignAssets: ForeignAssetsGroup;
+  xcm: XcmGroup<UniqueHelper>;
+  xTokens: XTokensGroup<UniqueHelper>;
+  tokens: TokensGroup<UniqueHelper>;
+  scheduler: SchedulerGroup;
+  collatorSelection: CollatorSelectionGroup;
+  council: ICollectiveGroup;
+  technicalCommittee: ICollectiveGroup;
+  fellowship: IFellowshipGroup;
+  democracy: DemocracyGroup;
 
   constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
     options.helperBase = options.helperBase ?? DevUniqueHelper;
@@ -279,6 +477,25 @@
     this.admin = new AdminGroup(this);
     this.testUtils = new TestUtilGroup(this);
     this.session = new SessionGroup(this);
+    this.foreignAssets = new ForeignAssetsGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+    this.xTokens = new XTokensGroup(this);
+    this.tokens = new TokensGroup(this);
+    this.scheduler = new SchedulerGroup(this);
+    this.collatorSelection = new CollatorSelectionGroup(this);
+    this.council = {
+      collective: new CollectiveGroup(this, 'council'),
+      membership: new CollectiveMembershipGroup(this, 'councilMembership'),
+    };
+    this.technicalCommittee = {
+      collective: new CollectiveGroup(this, 'technicalCommittee'),
+      membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),
+    };
+    this.fellowship = {
+      collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),
+      referenda: new ReferendaGroup(this, 'fellowshipReferenda'),
+    };
+    this.democracy = new DemocracyGroup(this);
   }
 
   async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
@@ -326,6 +543,11 @@
     this.network = await UniqueHelper.detectNetwork(this.api);
     this.wsEndpoint = wsEndpoint;
   }
+  getSudo<T extends DevUniqueHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
+  }
 }
 
 export class DevRelayHelper extends RelayHelper {
@@ -386,19 +608,16 @@
     super(logger, options);
     this.wait = new WaitGroup(this);
   }
-}
-
-export class DevShidenHelper extends AstarHelper {
-  wait: WaitGroup;
 
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevShidenHelper;
-
-    super(logger, options);
-    this.wait = new WaitGroup(this);
+  getSudo<T extends AstarHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
   }
 }
 
+export class DevShidenHelper extends DevAstarHelper { }
+
 export class DevAcalaHelper extends AcalaHelper {
   wait: WaitGroup;
 
@@ -408,6 +627,11 @@
     super(logger, options);
     this.wait = new WaitGroup(this);
   }
+  getSudo<T extends AcalaHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
+  }
 }
 
 export class DevPolkadexHelper extends PolkadexHelper {
@@ -418,6 +642,12 @@
     super(logger, options);
     this.wait = new WaitGroup(this);
   }
+
+  getSudo<T extends PolkadexHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
+  }
 }
 
 export class DevKaruraHelper extends DevAcalaHelper {}
@@ -1211,3 +1441,63 @@
     }));
   }
 }
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {
+  return class extends Base {
+    scheduleFn: 'schedule' | 'scheduleAfter';
+    blocksNum: number;
+    options: ISchedulerOptions;
+
+    constructor(...args: any[]) {
+      const logger = args[0] as ILogger;
+      const options = args[1] as {
+        scheduleFn: 'schedule' | 'scheduleAfter',
+        blocksNum: number,
+        options: ISchedulerOptions
+      };
+
+      super(logger);
+
+      this.scheduleFn = options.scheduleFn;
+      this.blocksNum = options.blocksNum;
+      this.options = options.options;
+    }
+
+    executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {
+      const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);
+
+      const mandatorySchedArgs = [
+        this.blocksNum,
+        this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,
+        this.options.priority ?? null,
+        scheduledTx,
+      ];
+
+      let schedArgs;
+      let scheduleFn;
+
+      if(this.options.scheduledId) {
+        schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];
+
+        if(this.scheduleFn == 'schedule') {
+          scheduleFn = 'scheduleNamed';
+        } else if(this.scheduleFn == 'scheduleAfter') {
+          scheduleFn = 'scheduleNamedAfter';
+        }
+      } else {
+        schedArgs = mandatorySchedArgs;
+        scheduleFn = this.scheduleFn;
+      }
+
+      const extrinsic = 'api.tx.scheduler.' + scheduleFn;
+
+      return super.executeExtrinsic(
+        sender,
+        extrinsic as any,
+        schedArgs,
+        expectSuccess,
+      );
+    }
+  };
+}
\ No newline at end of file
addedtests/src/util/playgrounds/unique.governance.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/unique.governance.ts
@@ -0,0 +1,531 @@
+import {blake2AsHex} from '@polkadot/util-crypto';
+import {PalletDemocracyConviction} from '@polkadot/types/lookup';
+import {IPhasicEvent, TSigner} from './types';
+import {HelperGroup, UniqueHelper} from './unique';
+
+export class CollectiveGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'council', 'technicalCommittee'
+   */
+  private collective: string;
+
+  constructor(helper: UniqueHelper, collective: string) {
+    super(helper);
+    this.collective = collective;
+  }
+
+  /**
+   * Check the result of a proposal execution for the success of the underlying proposed extrinsic.
+   * @param events events of the proposal execution
+   * @returns proposal hash
+   */
+  private checkExecutedEvent(events: IPhasicEvent[]): string {
+    const executionEvents = events.filter(x =>
+      x.event.section === this.collective && (x.event.method === 'Executed' || x.event.method === 'MemberExecuted'));
+
+    if(executionEvents.length != 1) {
+      if(events.filter(x => x.event.section === this.collective && x.event.method === 'Disapproved').length > 0)
+        throw new Error(`Disapproved by ${this.collective}`);
+      else
+        throw new Error(`Expected one 'Executed' or 'MemberExecuted' event for ${this.collective}`);
+    }
+
+    const result = (executionEvents[0].event.data as any).result;
+
+    if(result.isErr) {
+      if(result.asErr.isModule) {
+        const error = result.asErr.asModule;
+        const metaError = this.helper.getApi()?.registry.findMetaError(error);
+        throw new Error(`Proposal execution failed with ${metaError.section}.${metaError.name}`);
+      } else {
+        throw new Error('Proposal execution failed with ' + result.asErr.toHuman());
+      }
+    }
+
+    return (executionEvents[0].event.data as any).proposalHash;
+  }
+
+  /**
+   * Returns an array of members' addresses.
+   */
+  async getMembers() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.members`, [])).toHuman();
+  }
+
+  /**
+   * Returns the optional address of the prime member of the collective.
+   */
+  async getPrimeMember() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.prime`, [])).toHuman();
+  }
+
+  /**
+   * Returns an array of proposal hashes that are currently active for this collective.
+   */
+  async getProposals() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.proposals`, [])).toHuman();
+  }
+
+  /**
+   * Returns the call originally encoded under the specified hash.
+   * @param hash h256-encoded proposal
+   * @returns the optional call that the proposal hash stands for.
+   */
+  async getProposalCallOf(hash: string) {
+    return (await this.helper.callRpc(`api.query.${this.collective}.proposalOf`, [hash])).toHuman();
+  }
+
+  /**
+   * Returns the total number of proposals so far.
+   */
+  async getTotalProposalsCount() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, [])).toNumber();
+  }
+
+  /**
+   * Creates a new proposal up for voting. If the threshold is set to 1, the proposal will be executed immediately.
+   * @param signer keyring of the proposer
+   * @param proposal constructed call to be executed if the proposal is successful
+   * @param voteThreshold minimal number of votes for the proposal to be verified and executed
+   * @param lengthBound byte length of the encoded call
+   * @returns promise of extrinsic execution and its result
+   */
+  async propose(signer: TSigner, proposal: any, voteThreshold: number, lengthBound = 10000) {
+    return await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [voteThreshold, proposal, lengthBound]);
+  }
+
+  /**
+   * Casts a vote to either approve or reject a proposal.
+   * @param signer keyring of the voter
+   * @param proposalHash hash of the proposal to be voted for
+   * @param proposalIndex absolute index of the proposal used for absolutely nothing but throwing pointless errors
+   * @param approve aye or nay
+   * @returns promise of extrinsic execution and its result
+   */
+  vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve]);
+  }
+
+  /**
+   * Executes a call immediately as a member of the collective. Needed for the Member origin.
+   * @param signer keyring of the executor member
+   * @param proposal constructed call to be executed by the member
+   * @param lengthBound byte length of the encoded call
+   * @returns promise of extrinsic execution
+   */
+  async execute(signer: TSigner, proposal: any, lengthBound = 10000) {
+    const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.execute`, [proposal, lengthBound]);
+    this.checkExecutedEvent(result.result.events);
+    return result;
+  }
+
+  /**
+   * Attempt to close and execute a proposal. Note that there must already be enough votes to meet the threshold set when proposing.
+   * @param signer keyring of the executor. Can be absolutely anyone.
+   * @param proposalHash hash of the proposal to close
+   * @param proposalIndex index of the proposal generated on its creation
+   * @param weightBound weight of the proposed call. Can be obtained by calling `paymentInfo()` on the call.
+   * @param lengthBound byte length of the encoded call
+   * @returns promise of extrinsic execution and its result
+   */
+  async close(
+    signer: TSigner,
+    proposalHash: string,
+    proposalIndex: number,
+    weightBound: [number, number] | any = [20_000_000_000, 1000_000],
+    lengthBound = 10_000,
+  ) {
+    const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [
+      proposalHash,
+      proposalIndex,
+      weightBound,
+      lengthBound,
+    ]);
+    this.checkExecutedEvent(result.result.events);
+    return result;
+  }
+
+  /**
+   * Shut down a proposal, regardless of its current state.
+   * @param signer keyring of the disapprover. Must be root
+   * @param proposalHash hash of the proposal to close
+   * @returns promise of extrinsic execution and its result
+   */
+  disapproveProposal(signer: TSigner, proposalHash: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.disapproveProposal`, [proposalHash]);
+  }
+}
+
+export class CollectiveMembershipGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'councilMembership', 'technicalCommitteeMembership'
+   */
+  private membership: string;
+
+  constructor(helper: UniqueHelper, membership: string) {
+    super(helper);
+    this.membership = membership;
+  }
+
+  /**
+   * Returns an array of members' addresses according to the membership pallet's perception.
+   * Note that it does not recognize the original pallet's members set with `setMembers()`.
+   */
+  async getMembers() {
+    return (await this.helper.callRpc(`api.query.${this.membership}.members`, [])).toHuman();
+  }
+
+  /**
+   * Returns the optional address of the prime member of the collective.
+   */
+  async getPrimeMember() {
+    return (await this.helper.callRpc(`api.query.${this.membership}.prime`, [])).toHuman();
+  }
+
+  /**
+   * Add a member to the collective.
+   * @param signer keyring of the setter. Must be root
+   * @param member address of the member to add
+   * @returns promise of extrinsic execution and its result
+   */
+  addMember(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.addMember`, [member]);
+  }
+
+  addMemberCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.addMember`, [member]);
+  }
+
+  /**
+   * Remove a member from the collective.
+   * @param signer keyring of the setter. Must be root
+   * @param member address of the member to remove
+   * @returns promise of extrinsic execution and its result
+   */
+  removeMember(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.removeMember`, [member]);
+  }
+
+  removeMemberCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.removeMember`, [member]);
+  }
+
+  /**
+   * Set members of the collective to the given list of addresses.
+   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
+   * @param members addresses of the members to set
+   * @returns promise of extrinsic execution and its result
+   */
+  resetMembers(signer: TSigner, members: string[]) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.resetMembers`, [members]);
+  }
+
+  /**
+   * Set the collective's prime member to the given address.
+   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
+   * @param prime address of the prime member of the collective
+   * @returns promise of extrinsic execution and its result
+   */
+  setPrime(signer: TSigner, prime: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.setPrime`, [prime]);
+  }
+
+  setPrimeCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.setPrime`, [member]);
+  }
+
+  /**
+   * Remove the collective's prime member.
+   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
+   * @returns promise of extrinsic execution and its result
+   */
+  clearPrime(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.clearPrime`, []);
+  }
+
+  clearPrimeCall() {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.clearPrime`, []);
+  }
+}
+
+export class RankedCollectiveGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'FellowshipCollective'
+   */
+  private collective: string;
+
+  constructor(helper: UniqueHelper, collective: string) {
+    super(helper);
+    this.collective = collective;
+  }
+
+  addMember(signer: TSigner, newMember: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.addMember`, [newMember]);
+  }
+
+  addMemberCall(newMember: string) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.addMember`, [newMember]);
+  }
+
+  removeMember(signer: TSigner, member: string, minRank: number) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.removeMember`, [member, minRank]);
+  }
+
+  removeMemberCall(newMember: string, minRank: number) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.removeMember`, [newMember, minRank]);
+  }
+
+  promote(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.promoteMember`, [member]);
+  }
+
+  promoteCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.promoteMember`, [member]);
+  }
+
+  demote(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.demoteMember`, [member]);
+  }
+
+  demoteCall(newMember: string) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.demoteMember`, [newMember]);
+  }
+
+  vote(signer: TSigner, pollIndex: number, aye: boolean) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [pollIndex, aye]);
+  }
+
+  async getMembers() {
+    return (await this.helper.getApi().query.fellowshipCollective.members.keys())
+      .map((key) => key.args[0].toString());
+  }
+
+  async getMemberRank(member: string) {
+    return (await this.helper.callRpc('api.query.fellowshipCollective.members', [member])).toJSON().rank;
+  }
+}
+
+export class ReferendaGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'FellowshipReferenda'
+   */
+  private referenda: string;
+
+  constructor(helper: UniqueHelper, referenda: string) {
+    super(helper);
+    this.referenda = referenda;
+  }
+
+  submit(
+    signer: TSigner,
+    proposalOrigin: string,
+    proposal: any,
+    enactmentMoment: any,
+  ) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.submit`, [
+      {Origins: proposalOrigin},
+      proposal,
+      enactmentMoment,
+    ]);
+  }
+
+  placeDecisionDeposit(signer: TSigner, referendumIndex: number) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.placeDecisionDeposit`, [referendumIndex]);
+  }
+
+  cancel(signer: TSigner, referendumIndex: number) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.cancel`, [referendumIndex]);
+  }
+
+  cancelCall(referendumIndex: number) {
+    return this.helper.constructApiCall(`api.tx.${this.referenda}.cancel`, [referendumIndex]);
+  }
+
+  async referendumInfo(referendumIndex: number) {
+    return (await this.helper.callRpc(`api.query.${this.referenda}.referendumInfoFor`, [referendumIndex])).toJSON();
+  }
+
+  async enactmentEventId(referendumIndex: number) {
+    const api = await this.helper.getApi();
+
+    const bytes = api.createType('([u8;8], Text, u32)', ['assembly', 'enactment', referendumIndex]).toU8a();
+    return blake2AsHex(bytes, 256);
+  }
+}
+
+export interface IFellowshipGroup {
+  collective: RankedCollectiveGroup;
+  referenda: ReferendaGroup;
+}
+
+export interface ICollectiveGroup {
+  collective: CollectiveGroup;
+  membership: CollectiveMembershipGroup;
+}
+
+export class DemocracyGroup extends HelperGroup<UniqueHelper> {
+  // todo displace proposal into types?
+  propose(signer: TSigner, call: any, deposit: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
+  }
+
+  proposeWithPreimage(signer: TSigner, preimage: string, deposit: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Legacy: preimage}, deposit]);
+  }
+
+  proposeCall(call: any, deposit: bigint) {
+    return this.helper.constructApiCall('api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
+  }
+
+  second(signer: TSigner, proposalIndex: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.second', [proposalIndex]);
+  }
+
+  externalPropose(signer: TSigner, proposalCall: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeMajority(signer: TSigner, proposalCall: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefault(signer: TSigner, proposalCall: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefaultWithPreimage(signer: TSigner, preimage: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
+  }
+
+  externalProposeCall(proposalCall: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeMajorityCall(proposalCall: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefaultCall(proposalCall: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefaultWithPreimageCall(preimage: string) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
+  }
+
+  // ... and blacklist external proposal hash.
+  vetoExternal(signer: TSigner, proposalHash: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vetoExternal', [proposalHash]);
+  }
+
+  vetoExternalCall(proposalHash: string) {
+    return this.helper.constructApiCall('api.tx.democracy.vetoExternal', [proposalHash]);
+  }
+
+  blacklist(signer: TSigner, proposalHash: string, referendumIndex: number | null = null) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
+  }
+
+  blacklistCall(proposalHash: string, referendumIndex: number | null = null) {
+    return this.helper.constructApiCall('api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
+  }
+
+  // proposal. CancelProposalOrigin (root or all techcom)
+  cancelProposal(signer: TSigner, proposalIndex: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.cancelProposal', [proposalIndex]);
+  }
+
+  cancelProposalCall(proposalIndex: number) {
+    return this.helper.constructApiCall('api.tx.democracy.cancelProposal', [proposalIndex]);
+  }
+
+  clearPublicProposals(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.clearPublicProposals', []);
+  }
+
+  fastTrack(signer: TSigner, proposalHash: string, votingPeriod: number, delayPeriod: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
+  }
+
+  fastTrackCall(proposalHash: string, votingPeriod: number, delayPeriod: number) {
+    return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
+  }
+
+  // referendum. CancellationOrigin (TechCom member)
+  emergencyCancel(signer: TSigner, referendumIndex: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.emergencyCancel', [referendumIndex]);
+  }
+
+  emergencyCancelCall(referendumIndex: number) {
+    return this.helper.constructApiCall('api.tx.democracy.emergencyCancel', [referendumIndex]);
+  }
+
+  vote(signer: TSigner, referendumIndex: number, vote: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, vote]);
+  }
+
+  removeVote(signer: TSigner, referendumIndex: number, targetAccount?: string) {
+    if(targetAccount) {
+      return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeOtherVote', [targetAccount, referendumIndex]);
+    } else {
+      return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeVote', [referendumIndex]);
+    }
+  }
+
+  unlock(signer: TSigner, targetAccount: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.unlock', [targetAccount]);
+  }
+
+  delegate(signer: TSigner, toAccount: string, conviction: PalletDemocracyConviction, balance: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.delegate', [toAccount, conviction, balance]);
+  }
+
+  undelegate(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.undelegate', []);
+  }
+
+  async referendumInfo(referendumIndex: number) {
+    return (await this.helper.callRpc('api.query.democracy.referendumInfoOf', [referendumIndex])).toJSON();
+  }
+
+  async publicProposals() {
+    return (await this.helper.callRpc('api.query.democracy.publicProps', [])).toJSON();
+  }
+
+  async findPublicProposal(proposalIndex: number) {
+    const proposalInfo = (await this.publicProposals()).find((proposalInfo: any[]) => proposalInfo[0] == proposalIndex);
+
+    return proposalInfo ? proposalInfo[1] : null;
+  }
+
+  async expectPublicProposal(proposalIndex: number) {
+    const proposal = await this.findPublicProposal(proposalIndex);
+
+    if(proposal) {
+      return proposal;
+    } else {
+      throw Error(`Proposal #${proposalIndex} is expected to exist`);
+    }
+  }
+
+  async getExternalProposal() {
+    return (await this.helper.callRpc('api.query.democracy.nextExternal', []));
+  }
+
+  async expectExternalProposal() {
+    const proposal = await this.getExternalProposal();
+
+    if(proposal) {
+      return proposal;
+    } else {
+      throw Error('An external proposal is expected to exist');
+    }
+  }
+
+  /* setMetadata? */
+
+  /* todo?
+  referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
+  }*/
+}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -39,16 +39,11 @@
   TSigner,
   TSubstrateAccount,
   TNetworks,
-  IForeignAssetMetadata,
-  AcalaAssetMetadata,
-  MoonbeamAssetInfo,
-  DemocracyStandardAccountVote,
   IEthCrossAccountId,
-  IPhasicEvent,
 } from './types';
 import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';
 import type {Vec} from '@polkadot/types-codec';
-import {FrameSystemEventRecord, PalletDemocracyConviction} from '@polkadot/types/lookup';
+import {FrameSystemEventRecord} from '@polkadot/types/lookup';
 
 export class CrossAccountId {
   Substrate!: TSubstrateAccount;
@@ -818,7 +813,7 @@
 }
 
 
-class HelperGroup<T extends ChainHelperBase> {
+export class HelperGroup<T extends ChainHelperBase> {
   helper: T;
 
   constructor(uniqueHelper: T) {
@@ -2373,7 +2368,7 @@
   }
 }
 
-class SubstrateBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+export class SubstrateBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {
   /**
  * Get substrate address balance
  * @param address substrate address
@@ -2440,7 +2435,7 @@
   }
 }
 
-class EthereumBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+export class EthereumBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {
   /**
    * Get ethereum address balance
    * @param address ethereum address
@@ -2760,6 +2755,7 @@
   }
 }
 
+
 class StakingGroup extends HelperGroup<UniqueHelper> {
   /**
    * Stake tokens for App Promotion
@@ -2867,645 +2863,7 @@
   }
 }
 
-class SchedulerGroup extends HelperGroup<UniqueHelper> {
-  constructor(helper: UniqueHelper) {
-    super(helper);
-  }
-
-  cancelScheduled(signer: TSigner, scheduledId: string) {
-    return this.helper.executeExtrinsic(
-      signer,
-      'api.tx.scheduler.cancelNamed',
-      [scheduledId],
-      true,
-    );
-  }
-
-  changePriority(signer: TSigner, scheduledId: string, priority: number) {
-    return this.helper.executeExtrinsic(
-      signer,
-      'api.tx.scheduler.changeNamedPriority',
-      [scheduledId, priority],
-      true,
-    );
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    return this.schedule<T>('schedule', executionBlockNumber, options);
-  }
 
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);
-  }
-
-  schedule<T extends UniqueHelper>(
-    scheduleFn: 'schedule' | 'scheduleAfter',
-    blocksNum: number,
-    options: ISchedulerOptions = {},
-  ) {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);
-    return this.helper.clone(ScheduledHelperType, {
-      scheduleFn,
-      blocksNum,
-      options,
-    }) as T;
-  }
-}
-
-class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {
-  //todo:collator documentation
-  addInvulnerable(signer: TSigner, address: string) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);
-  }
-
-  removeInvulnerable(signer: TSigner, address: string) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);
-  }
-
-  async getInvulnerables(): Promise<string[]> {
-    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());
-  }
-
-  /** and also total max invulnerables */
-  maxCollators(): number {
-    return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);
-  }
-
-  async getDesiredCollators(): Promise<number> {
-    return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();
-  }
-
-  setLicenseBond(signer: TSigner, amount: bigint) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);
-  }
-
-  async getLicenseBond(): Promise<bigint> {
-    return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();
-  }
-
-  obtainLicense(signer: TSigner) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);
-  }
-
-  releaseLicense(signer: TSigner) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);
-  }
-
-  forceReleaseLicense(signer: TSigner, released: string) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);
-  }
-
-  async hasLicense(address: string): Promise<bigint> {
-    return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();
-  }
-
-  onboard(signer: TSigner) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);
-  }
-
-  offboard(signer: TSigner) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);
-  }
-
-  async getCandidates(): Promise<string[]> {
-    return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());
-  }
-}
-
-class CollectiveGroup extends HelperGroup<UniqueHelper> {
-  /**
-   * Pallet name to make an API call to. Examples: 'council', 'technicalCommittee'
-   */
-  private collective: string;
-
-  constructor(helper: UniqueHelper, collective: string) {
-    super(helper);
-    this.collective = collective;
-  }
-
-  /**
-   * Check the result of a proposal execution for the success of the underlying proposed extrinsic.
-   * @param events events of the proposal execution
-   * @returns proposal hash
-   */
-  private checkExecutedEvent(events: IPhasicEvent[]): string {
-    const executionEvents = events.filter(x =>
-      x.event.section === this.collective && (x.event.method === 'Executed' || x.event.method === 'MemberExecuted'));
-
-    if(executionEvents.length != 1) {
-      if(events.filter(x => x.event.section === this.collective && x.event.method === 'Disapproved').length > 0)
-        throw new Error(`Disapproved by ${this.collective}`);
-      else
-        throw new Error(`Expected one 'Executed' or 'MemberExecuted' event for ${this.collective}`);
-    }
-
-    const result = (executionEvents[0].event.data as any).result;
-
-    if(result.isErr) {
-      if(result.asErr.isModule) {
-        const error = result.asErr.asModule;
-        const metaError = this.helper.getApi()?.registry.findMetaError(error);
-        throw new Error(`Proposal execution failed with ${metaError.section}.${metaError.name}`);
-      } else {
-        throw new Error('Proposal execution failed with ' + result.asErr.toHuman());
-      }
-    }
-
-    return (executionEvents[0].event.data as any).proposalHash;
-  }
-
-  /**
-   * Returns an array of members' addresses.
-   */
-  async getMembers() {
-    return (await this.helper.callRpc(`api.query.${this.collective}.members`, [])).toHuman();
-  }
-
-  /**
-   * Returns the optional address of the prime member of the collective.
-   */
-  async getPrimeMember() {
-    return (await this.helper.callRpc(`api.query.${this.collective}.prime`, [])).toHuman();
-  }
-
-  /**
-   * Returns an array of proposal hashes that are currently active for this collective.
-   */
-  async getProposals() {
-    return (await this.helper.callRpc(`api.query.${this.collective}.proposals`, [])).toHuman();
-  }
-
-  /**
-   * Returns the call originally encoded under the specified hash.
-   * @param hash h256-encoded proposal
-   * @returns the optional call that the proposal hash stands for.
-   */
-  async getProposalCallOf(hash: string) {
-    return (await this.helper.callRpc(`api.query.${this.collective}.proposalOf`, [hash])).toHuman();
-  }
-
-  /**
-   * Returns the total number of proposals so far.
-   */
-  async getTotalProposalsCount() {
-    return (await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, [])).toNumber();
-  }
-
-  /**
-   * Creates a new proposal up for voting. If the threshold is set to 1, the proposal will be executed immediately.
-   * @param signer keyring of the proposer
-   * @param proposal constructed call to be executed if the proposal is successful
-   * @param voteThreshold minimal number of votes for the proposal to be verified and executed
-   * @param lengthBound byte length of the encoded call
-   * @returns promise of extrinsic execution and its result
-   */
-  async propose(signer: TSigner, proposal: any, voteThreshold: number, lengthBound = 10000) {
-    return await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [voteThreshold, proposal, lengthBound]);
-  }
-
-  /**
-   * Casts a vote to either approve or reject a proposal.
-   * @param signer keyring of the voter
-   * @param proposalHash hash of the proposal to be voted for
-   * @param proposalIndex absolute index of the proposal used for absolutely nothing but throwing pointless errors
-   * @param approve aye or nay
-   * @returns promise of extrinsic execution and its result
-   */
-  vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve]);
-  }
-
-  /**
-   * Executes a call immediately as a member of the collective. Needed for the Member origin.
-   * @param signer keyring of the executor member
-   * @param proposal constructed call to be executed by the member
-   * @param lengthBound byte length of the encoded call
-   * @returns promise of extrinsic execution
-   */
-  async execute(signer: TSigner, proposal: any, lengthBound = 10000) {
-    const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.execute`, [proposal, lengthBound]);
-    this.checkExecutedEvent(result.result.events);
-    return result;
-  }
-
-  /**
-   * Attempt to close and execute a proposal. Note that there must already be enough votes to meet the threshold set when proposing.
-   * @param signer keyring of the executor. Can be absolutely anyone.
-   * @param proposalHash hash of the proposal to close
-   * @param proposalIndex index of the proposal generated on its creation
-   * @param weightBound weight of the proposed call. Can be obtained by calling `paymentInfo()` on the call.
-   * @param lengthBound byte length of the encoded call
-   * @returns promise of extrinsic execution and its result
-   */
-  async close(
-    signer: TSigner,
-    proposalHash: string,
-    proposalIndex: number,
-    weightBound: [number, number] | any = [20_000_000_000, 1000_000],
-    lengthBound = 10_000,
-  ) {
-    const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [
-      proposalHash,
-      proposalIndex,
-      weightBound,
-      lengthBound,
-    ]);
-    this.checkExecutedEvent(result.result.events);
-    return result;
-  }
-
-  /**
-   * Shut down a proposal, regardless of its current state.
-   * @param signer keyring of the disapprover. Must be root
-   * @param proposalHash hash of the proposal to close
-   * @returns promise of extrinsic execution and its result
-   */
-  disapproveProposal(signer: TSigner, proposalHash: string) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.disapproveProposal`, [proposalHash]);
-  }
-}
-
-class CollectiveMembershipGroup extends HelperGroup<UniqueHelper> {
-  /**
-   * Pallet name to make an API call to. Examples: 'councilMembership', 'technicalCommitteeMembership'
-   */
-  private membership: string;
-
-  constructor(helper: UniqueHelper, membership: string) {
-    super(helper);
-    this.membership = membership;
-  }
-
-  /**
-   * Returns an array of members' addresses according to the membership pallet's perception.
-   * Note that it does not recognize the original pallet's members set with `setMembers()`.
-   */
-  async getMembers() {
-    return (await this.helper.callRpc(`api.query.${this.membership}.members`, [])).toHuman();
-  }
-
-  /**
-   * Returns the optional address of the prime member of the collective.
-   */
-  async getPrimeMember() {
-    return (await this.helper.callRpc(`api.query.${this.membership}.prime`, [])).toHuman();
-  }
-
-  /**
-   * Add a member to the collective.
-   * @param signer keyring of the setter. Must be root
-   * @param member address of the member to add
-   * @returns promise of extrinsic execution and its result
-   */
-  addMember(signer: TSigner, member: string) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.addMember`, [member]);
-  }
-
-  addMemberCall(member: string) {
-    return this.helper.constructApiCall(`api.tx.${this.membership}.addMember`, [member]);
-  }
-
-  /**
-   * Remove a member from the collective.
-   * @param signer keyring of the setter. Must be root
-   * @param member address of the member to remove
-   * @returns promise of extrinsic execution and its result
-   */
-  removeMember(signer: TSigner, member: string) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.removeMember`, [member]);
-  }
-
-  removeMemberCall(member: string) {
-    return this.helper.constructApiCall(`api.tx.${this.membership}.removeMember`, [member]);
-  }
-
-  /**
-   * Set members of the collective to the given list of addresses.
-   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
-   * @param members addresses of the members to set
-   * @returns promise of extrinsic execution and its result
-   */
-  resetMembers(signer: TSigner, members: string[]) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.resetMembers`, [members]);
-  }
-
-  /**
-   * Set the collective's prime member to the given address.
-   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
-   * @param prime address of the prime member of the collective
-   * @returns promise of extrinsic execution and its result
-   */
-  setPrime(signer: TSigner, prime: string) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.setPrime`, [prime]);
-  }
-
-  setPrimeCall(member: string) {
-    return this.helper.constructApiCall(`api.tx.${this.membership}.setPrime`, [member]);
-  }
-
-  /**
-   * Remove the collective's prime member.
-   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
-   * @returns promise of extrinsic execution and its result
-   */
-  clearPrime(signer: TSigner) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.clearPrime`, []);
-  }
-
-  clearPrimeCall() {
-    return this.helper.constructApiCall(`api.tx.${this.membership}.clearPrime`, []);
-  }
-}
-
-class RankedCollectiveGroup extends HelperGroup<UniqueHelper> {
-  /**
-   * Pallet name to make an API call to. Examples: 'FellowshipCollective'
-   */
-  private collective: string;
-
-  constructor(helper: UniqueHelper, collective: string) {
-    super(helper);
-    this.collective = collective;
-  }
-
-  addMember(signer: TSigner, newMember: string) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.addMember`, [newMember]);
-  }
-
-  addMemberCall(newMember: string) {
-    return this.helper.constructApiCall(`api.tx.${this.collective}.addMember`, [newMember]);
-  }
-
-  removeMember(signer: TSigner, member: string, minRank: number) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.removeMember`, [member, minRank]);
-  }
-
-  removeMemberCall(newMember: string, minRank: number) {
-    return this.helper.constructApiCall(`api.tx.${this.collective}.removeMember`, [newMember, minRank]);
-  }
-
-  promote(signer: TSigner, member: string) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.promoteMember`, [member]);
-  }
-
-  promoteCall(member: string) {
-    return this.helper.constructApiCall(`api.tx.${this.collective}.promoteMember`, [member]);
-  }
-
-  demote(signer: TSigner, member: string) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.demoteMember`, [member]);
-  }
-
-  demoteCall(newMember: string) {
-    return this.helper.constructApiCall(`api.tx.${this.collective}.demoteMember`, [newMember]);
-  }
-
-  vote(signer: TSigner, pollIndex: number, aye: boolean) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [pollIndex, aye]);
-  }
-
-  async getMembers() {
-    return (await this.helper.getApi().query.fellowshipCollective.members.keys())
-      .map((key) => key.args[0].toString());
-  }
-
-  async getMemberRank(member: string) {
-    return (await this.helper.callRpc('api.query.fellowshipCollective.members', [member])).toJSON().rank;
-  }
-}
-
-class ReferendaGroup extends HelperGroup<UniqueHelper> {
-  /**
-   * Pallet name to make an API call to. Examples: 'FellowshipReferenda'
-   */
-  private referenda: string;
-
-  constructor(helper: UniqueHelper, referenda: string) {
-    super(helper);
-    this.referenda = referenda;
-  }
-
-  submit(
-    signer: TSigner,
-    proposalOrigin: string,
-    proposal: any,
-    enactmentMoment: any,
-  ) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.submit`, [
-      {Origins: proposalOrigin},
-      proposal,
-      enactmentMoment,
-    ]);
-  }
-
-  placeDecisionDeposit(signer: TSigner, referendumIndex: number) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.placeDecisionDeposit`, [referendumIndex]);
-  }
-
-  cancel(signer: TSigner, referendumIndex: number) {
-    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.cancel`, [referendumIndex]);
-  }
-
-  cancelCall(referendumIndex: number) {
-    return this.helper.constructApiCall(`api.tx.${this.referenda}.cancel`, [referendumIndex]);
-  }
-
-  async referendumInfo(referendumIndex: number) {
-    return (await this.helper.callRpc(`api.query.${this.referenda}.referendumInfoFor`, [referendumIndex])).toJSON();
-  }
-
-  async enactmentEventId(referendumIndex: number) {
-    const api = await this.helper.getApi();
-
-    const bytes = api.createType('([u8;8], Text, u32)', ['assembly', 'enactment', referendumIndex]).toU8a();
-    return blake2AsHex(bytes, 256);
-  }
-}
-
-export interface IFellowshipGroup {
-  collective: RankedCollectiveGroup;
-  referenda: ReferendaGroup;
-}
-
-export interface ICollectiveGroup {
-  collective: CollectiveGroup;
-  membership: CollectiveMembershipGroup;
-}
-
-class DemocracyGroup extends HelperGroup<UniqueHelper> {
-  // todo displace proposal into types?
-  propose(signer: TSigner, call: any, deposit: bigint) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
-  }
-
-  proposeWithPreimage(signer: TSigner, preimage: string, deposit: bigint) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Legacy: preimage}, deposit]);
-  }
-
-  proposeCall(call: any, deposit: bigint) {
-    return this.helper.constructApiCall('api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
-  }
-
-  second(signer: TSigner, proposalIndex: number) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.second', [proposalIndex]);
-  }
-
-  externalPropose(signer: TSigner, proposalCall: any) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
-  }
-
-  externalProposeMajority(signer: TSigner, proposalCall: any) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
-  }
-
-  externalProposeDefault(signer: TSigner, proposalCall: any) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
-  }
-
-  externalProposeDefaultWithPreimage(signer: TSigner, preimage: string) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
-  }
-
-  externalProposeCall(proposalCall: any) {
-    return this.helper.constructApiCall('api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
-  }
-
-  externalProposeMajorityCall(proposalCall: any) {
-    return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
-  }
-
-  externalProposeDefaultCall(proposalCall: any) {
-    return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
-  }
-
-  externalProposeDefaultWithPreimageCall(preimage: string) {
-    return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
-  }
-
-  // ... and blacklist external proposal hash.
-  vetoExternal(signer: TSigner, proposalHash: string) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vetoExternal', [proposalHash]);
-  }
-
-  vetoExternalCall(proposalHash: string) {
-    return this.helper.constructApiCall('api.tx.democracy.vetoExternal', [proposalHash]);
-  }
-
-  blacklist(signer: TSigner, proposalHash: string, referendumIndex: number | null = null) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
-  }
-
-  blacklistCall(proposalHash: string, referendumIndex: number | null = null) {
-    return this.helper.constructApiCall('api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
-  }
-
-  // proposal. CancelProposalOrigin (root or all techcom)
-  cancelProposal(signer: TSigner, proposalIndex: number) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.cancelProposal', [proposalIndex]);
-  }
-
-  cancelProposalCall(proposalIndex: number) {
-    return this.helper.constructApiCall('api.tx.democracy.cancelProposal', [proposalIndex]);
-  }
-
-  clearPublicProposals(signer: TSigner) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.clearPublicProposals', []);
-  }
-
-  fastTrack(signer: TSigner, proposalHash: string, votingPeriod: number, delayPeriod: number) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
-  }
-
-  fastTrackCall(proposalHash: string, votingPeriod: number, delayPeriod: number) {
-    return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
-  }
-
-  // referendum. CancellationOrigin (TechCom member)
-  emergencyCancel(signer: TSigner, referendumIndex: number) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.emergencyCancel', [referendumIndex]);
-  }
-
-  emergencyCancelCall(referendumIndex: number) {
-    return this.helper.constructApiCall('api.tx.democracy.emergencyCancel', [referendumIndex]);
-  }
-
-  vote(signer: TSigner, referendumIndex: number, vote: any) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, vote]);
-  }
-
-  removeVote(signer: TSigner, referendumIndex: number, targetAccount?: string) {
-    if(targetAccount) {
-      return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeOtherVote', [targetAccount, referendumIndex]);
-    } else {
-      return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeVote', [referendumIndex]);
-    }
-  }
-
-  unlock(signer: TSigner, targetAccount: string) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.unlock', [targetAccount]);
-  }
-
-  delegate(signer: TSigner, toAccount: string, conviction: PalletDemocracyConviction, balance: bigint) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.delegate', [toAccount, conviction, balance]);
-  }
-
-  undelegate(signer: TSigner) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.undelegate', []);
-  }
-
-  async referendumInfo(referendumIndex: number) {
-    return (await this.helper.callRpc('api.query.democracy.referendumInfoOf', [referendumIndex])).toJSON();
-  }
-
-  async publicProposals() {
-    return (await this.helper.callRpc('api.query.democracy.publicProps', [])).toJSON();
-  }
-
-  async findPublicProposal(proposalIndex: number) {
-    const proposalInfo = (await this.publicProposals()).find((proposalInfo: any[]) => proposalInfo[0] == proposalIndex);
-
-    return proposalInfo ? proposalInfo[1] : null;
-  }
-
-  async expectPublicProposal(proposalIndex: number) {
-    const proposal = await this.findPublicProposal(proposalIndex);
-
-    if(proposal) {
-      return proposal;
-    } else {
-      throw Error(`Proposal #${proposalIndex} is expected to exist`);
-    }
-  }
-
-  async getExternalProposal() {
-    return (await this.helper.callRpc('api.query.democracy.nextExternal', []));
-  }
-
-  async expectExternalProposal() {
-    const proposal = await this.getExternalProposal();
-
-    if(proposal) {
-      return proposal;
-    } else {
-      throw Error('An external proposal is expected to exist');
-    }
-  }
-
-  /* setMetadata? */
-
-  /* todo?
-  referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
-    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
-  }*/
-}
-
 class PreimageGroup extends HelperGroup<UniqueHelper> {
   async getPreimageInfo(h256: string) {
     return (await this.helper.callRpc('api.query.preimage.statusFor', [h256])).toJSON();
@@ -3572,173 +2930,7 @@
    */
   unrequestPreimage(signer: TSigner, h256: string) {
     return this.helper.executeExtrinsic(signer, 'api.tx.preimage.unrequestPreimage', [h256]);
-  }
-}
-
-class ForeignAssetsGroup extends HelperGroup<UniqueHelper> {
-  async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) {
-    await this.helper.executeExtrinsic(
-      signer,
-      'api.tx.foreignAssets.registerForeignAsset',
-      [ownerAddress, location, metadata],
-      true,
-    );
-  }
-
-  async update(signer: TSigner, foreignAssetId: number, location: any, metadata: IForeignAssetMetadata) {
-    await this.helper.executeExtrinsic(
-      signer,
-      'api.tx.foreignAssets.updateForeignAsset',
-      [foreignAssetId, location, metadata],
-      true,
-    );
-  }
-}
-
-class XcmGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  palletName: string;
-
-  constructor(helper: T, palletName: string) {
-    super(helper);
-
-    this.palletName = palletName;
-  }
-
-  async limitedReserveTransferAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number, weightLimit: any) {
-    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.limitedReserveTransferAssets`, [destination, beneficiary, assets, feeAssetItem, weightLimit], true);
-  }
-
-  async setSafeXcmVersion(signer: TSigner, version: number) {
-    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.forceDefaultXcmVersion`, [version], true);
   }
-
-  async teleportAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number) {
-    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.teleportAssets`, [destination, beneficiary, assets, feeAssetItem], true);
-  }
-
-  async teleportNativeAsset(signer: TSigner, destinationParaId: number, targetAccount: Uint8Array, amount: bigint, xcmVersion = 3) {
-    const destinationContent = {
-      parents: 0,
-      interior: {
-        X1: {
-          Parachain: destinationParaId,
-        },
-      },
-    };
-
-    const beneficiaryContent = {
-      parents: 0,
-      interior: {
-        X1: {
-          AccountId32: {
-            network: 'Any',
-            id: targetAccount,
-          },
-        },
-      },
-    };
-
-    const assetsContent = [
-      {
-        id: {
-          Concrete: {
-            parents: 0,
-            interior: 'Here',
-          },
-        },
-        fun: {
-          Fungible: amount,
-        },
-      },
-    ];
-
-    let destination;
-    let beneficiary;
-    let assets;
-
-    if(xcmVersion == 2) {
-      destination = {V1: destinationContent};
-      beneficiary = {V1: beneficiaryContent};
-      assets = {V1: assetsContent};
-
-    } else if(xcmVersion == 3) {
-      destination = {V2: destinationContent};
-      beneficiary = {V2: beneficiaryContent};
-      assets = {V2: assetsContent};
-
-    } else {
-      throw Error('Unknown XCM version: ' + xcmVersion);
-    }
-
-    const feeAssetItem = 0;
-
-    await this.teleportAssets(signer, destination, beneficiary, assets, feeAssetItem);
-  }
-
-  async send(signer: IKeyringPair, destination: any, message: any) {
-    await this.helper.executeExtrinsic(
-      signer,
-      `api.tx.${this.palletName}.send`,
-      [
-        destination,
-        message,
-      ],
-      true,
-    );
-  }
-}
-
-class XTokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  async transfer(signer: TSigner, currencyId: any, amount: bigint, destination: any, destWeight: any) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transfer', [currencyId, amount, destination, destWeight], true);
-  }
-
-  async transferMultiasset(signer: TSigner, asset: any, destination: any, destWeight: any) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMultiasset', [asset, destination, destWeight], true);
-  }
-
-  async transferMulticurrencies(signer: TSigner, currencies: any[], feeItem: number, destLocation: any, destWeight: any) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMulticurrencies', [currencies, feeItem, destLocation, destWeight], true);
-  }
-}
-
-class PolkadexXcmHelperGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  async whitelistToken(signer: TSigner, assetId: any) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.xcmHelper.whitelistToken', [assetId], true);
-  }
-}
-
-class TokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  async accounts(address: string, currencyId: any) {
-    const {free} = (await this.helper.callRpc('api.query.tokens.accounts', [address, currencyId])).toJSON() as any;
-    return BigInt(free);
-  }
-}
-
-class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
-  }
-
-  async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true);
-  }
-
-  async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
-  }
-
-  async account(assetId: string | number, address: string) {
-    const accountAsset = (
-      await this.helper.callRpc('api.query.assets.account', [assetId, address])
-    ).toJSON()! as any;
-
-    if(accountAsset !== null) {
-      return BigInt(accountAsset['balance']);
-    } else {
-      return null;
-    }
-  }
 }
 
 class UtilityGroup<T extends ChainHelperBase> extends HelperGroup<T> {
@@ -3752,90 +2944,9 @@
 
   batchAllCall(txs: any[]) {
     return this.helper.constructApiCall('api.tx.utility.batchAll', [txs]);
-  }
-}
-
-class AcalaAssetRegistryGroup extends HelperGroup<AcalaHelper> {
-  async registerForeignAsset(signer: TSigner, destination: any, metadata: AcalaAssetMetadata) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.assetRegistry.registerForeignAsset', [destination, metadata], true);
   }
 }
 
-class MoonbeamAssetManagerGroup extends HelperGroup<MoonbeamHelper> {
-  makeRegisterForeignAssetProposal(assetInfo: MoonbeamAssetInfo) {
-    const apiPrefix = 'api.tx.assetManager.';
-
-    const registerTx = this.helper.constructApiCall(
-      apiPrefix + 'registerForeignAsset',
-      [assetInfo.location, assetInfo.metadata, assetInfo.existentialDeposit, assetInfo.isSufficient],
-    );
-
-    const setUnitsTx = this.helper.constructApiCall(
-      apiPrefix + 'setAssetUnitsPerSecond',
-      [assetInfo.location, assetInfo.unitsPerSecond, assetInfo.numAssetsWeightHint],
-    );
-
-    const batchCall = this.helper.getApi().tx.utility.batchAll([registerTx, setUnitsTx]);
-    const encodedProposal = batchCall?.method.toHex() || '';
-    return encodedProposal;
-  }
-
-  async assetTypeId(location: any) {
-    return await this.helper.callRpc('api.query.assetManager.assetTypeId', [location]);
-  }
-}
-
-class MoonbeamDemocracyGroup extends HelperGroup<MoonbeamHelper> {
-  notePreimagePallet: string;
-
-  constructor(helper: MoonbeamHelper, options: { [key: string]: any } = {}) {
-    super(helper);
-    this.notePreimagePallet = options.notePreimagePallet;
-  }
-
-  async notePreimage(signer: TSigner, encodedProposal: string) {
-    await this.helper.executeExtrinsic(signer, `api.tx.${this.notePreimagePallet}.notePreimage`, [encodedProposal], true);
-  }
-
-  externalProposeMajority(proposal: any) {
-    return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [proposal]);
-  }
-
-  fastTrack(proposalHash: string, votingPeriod: number, delayPeriod: number) {
-    return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
-  }
-
-  async referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
-  }
-}
-
-class MoonbeamCollectiveGroup extends HelperGroup<MoonbeamHelper> {
-  collective: string;
-
-  constructor(helper: MoonbeamHelper, collective: string) {
-    super(helper);
-
-    this.collective = collective;
-  }
-
-  async propose(signer: TSigner, threshold: number, proposalHash: string, lengthBound: number) {
-    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [threshold, proposalHash, lengthBound], true);
-  }
-
-  async vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
-    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve], true);
-  }
-
-  async close(signer: TSigner, proposalHash: string, proposalIndex: number, weightBound: any, lengthBound: number) {
-    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [proposalHash, proposalIndex, weightBound, lengthBound], true);
-  }
-
-  async proposalCount() {
-    return Number(await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, []));
-  }
-}
-
 export type ChainHelperBaseConstructor = new (...args: any[]) => ChainHelperBase;
 export type UniqueHelperConstructor = new (...args: any[]) => UniqueHelper;
 
@@ -3846,17 +2957,7 @@
   rft: RFTGroup;
   ft: FTGroup;
   staking: StakingGroup;
-  scheduler: SchedulerGroup;
-  collatorSelection: CollatorSelectionGroup;
-  council: ICollectiveGroup;
-  technicalCommittee: ICollectiveGroup;
-  fellowship: IFellowshipGroup;
-  democracy: DemocracyGroup;
   preimage: PreimageGroup;
-  foreignAssets: ForeignAssetsGroup;
-  xcm: XcmGroup<UniqueHelper>;
-  xTokens: XTokensGroup<UniqueHelper>;
-  tokens: TokensGroup<UniqueHelper>;
   utility: UtilityGroup<UniqueHelper>;
 
   constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
@@ -3868,303 +2969,11 @@
     this.rft = new RFTGroup(this);
     this.ft = new FTGroup(this);
     this.staking = new StakingGroup(this);
-    this.scheduler = new SchedulerGroup(this);
-    this.collatorSelection = new CollatorSelectionGroup(this);
-    this.council = {
-      collective: new CollectiveGroup(this, 'council'),
-      membership: new CollectiveMembershipGroup(this, 'councilMembership'),
-    };
-    this.technicalCommittee = {
-      collective: new CollectiveGroup(this, 'technicalCommittee'),
-      membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),
-    };
-    this.fellowship = {
-      collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),
-      referenda: new ReferendaGroup(this, 'fellowshipReferenda'),
-    };
-    this.democracy = new DemocracyGroup(this);
     this.preimage = new PreimageGroup(this);
-    this.foreignAssets = new ForeignAssetsGroup(this);
-    this.xcm = new XcmGroup(this, 'polkadotXcm');
-    this.xTokens = new XTokensGroup(this);
-    this.tokens = new TokensGroup(this);
     this.utility = new UtilityGroup(this);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const SudoHelperType = SudoHelper(this.helperBase);
-    return this.clone(SudoHelperType) as T;
-  }
-}
-
-export class XcmChainHelper extends ChainHelperBase {
-  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
-    const wsProvider = new WsProvider(wsEndpoint);
-    this.api = new ApiPromise({
-      provider: wsProvider,
-    });
-    await this.api.isReadyOrError;
-    this.network = await UniqueHelper.detectNetwork(this.api);
-  }
-}
-
-export class RelayHelper extends XcmChainHelper {
-  balance: SubstrateBalanceGroup<RelayHelper>;
-  xcm: XcmGroup<RelayHelper>;
-
-  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
-    super(logger, options.helperBase ?? RelayHelper);
-
-    this.balance = new SubstrateBalanceGroup(this);
-    this.xcm = new XcmGroup(this, 'xcmPallet');
-  }
-}
-
-export class WestmintHelper extends XcmChainHelper {
-  balance: SubstrateBalanceGroup<WestmintHelper>;
-  xcm: XcmGroup<WestmintHelper>;
-  assets: AssetsGroup<WestmintHelper>;
-  xTokens: XTokensGroup<WestmintHelper>;
-
-  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
-    super(logger, options.helperBase ?? WestmintHelper);
-
-    this.balance = new SubstrateBalanceGroup(this);
-    this.xcm = new XcmGroup(this, 'polkadotXcm');
-    this.assets = new AssetsGroup(this);
-    this.xTokens = new XTokensGroup(this);
-  }
-}
-
-export class MoonbeamHelper extends XcmChainHelper {
-  balance: EthereumBalanceGroup<MoonbeamHelper>;
-  assetManager: MoonbeamAssetManagerGroup;
-  assets: AssetsGroup<MoonbeamHelper>;
-  xTokens: XTokensGroup<MoonbeamHelper>;
-  democracy: MoonbeamDemocracyGroup;
-  collective: {
-    council: MoonbeamCollectiveGroup,
-    techCommittee: MoonbeamCollectiveGroup,
-  };
-
-  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
-    super(logger, options.helperBase ?? MoonbeamHelper);
-
-    this.balance = new EthereumBalanceGroup(this);
-    this.assetManager = new MoonbeamAssetManagerGroup(this);
-    this.assets = new AssetsGroup(this);
-    this.xTokens = new XTokensGroup(this);
-    this.democracy = new MoonbeamDemocracyGroup(this, options);
-    this.collective = {
-      council: new MoonbeamCollectiveGroup(this, 'councilCollective'),
-      techCommittee: new MoonbeamCollectiveGroup(this, 'techCommitteeCollective'),
-    };
   }
 }
 
-export class AstarHelper extends XcmChainHelper {
-  balance: SubstrateBalanceGroup<AstarHelper>;
-  assets: AssetsGroup<AstarHelper>;
-  xcm: XcmGroup<AstarHelper>;
-
-  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
-    super(logger, options.helperBase ?? AstarHelper);
-
-    this.balance = new SubstrateBalanceGroup(this);
-    this.assets = new AssetsGroup(this);
-    this.xcm = new XcmGroup(this, 'polkadotXcm');
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const SudoHelperType = SudoHelper(this.helperBase);
-    return this.clone(SudoHelperType) as T;
-  }
-}
-
-export class AcalaHelper extends XcmChainHelper {
-  balance: SubstrateBalanceGroup<AcalaHelper>;
-  assetRegistry: AcalaAssetRegistryGroup;
-  xTokens: XTokensGroup<AcalaHelper>;
-  tokens: TokensGroup<AcalaHelper>;
-  xcm: XcmGroup<AcalaHelper>;
-
-  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
-    super(logger, options.helperBase ?? AcalaHelper);
-
-    this.balance = new SubstrateBalanceGroup(this);
-    this.assetRegistry = new AcalaAssetRegistryGroup(this);
-    this.xTokens = new XTokensGroup(this);
-    this.tokens = new TokensGroup(this);
-    this.xcm = new XcmGroup(this, 'polkadotXcm');
-  }
-
-  getSudo<T extends AcalaHelper>() {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const SudoHelperType = SudoHelper(this.helperBase);
-    return this.clone(SudoHelperType) as T;
-  }
-}
-
-export class PolkadexHelper extends XcmChainHelper {
-  assets: AssetsGroup<PolkadexHelper>;
-  balance: SubstrateBalanceGroup<PolkadexHelper>;
-  xTokens: XTokensGroup<PolkadexHelper>;
-  xcm: XcmGroup<PolkadexHelper>;
-  xcmHelper: PolkadexXcmHelperGroup<PolkadexHelper>;
-
-  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
-    super(logger, options.helperBase ?? PolkadexHelper);
-
-    this.assets = new AssetsGroup(this);
-    this.balance = new SubstrateBalanceGroup(this);
-    this.xTokens = new XTokensGroup(this);
-    this.xcm = new XcmGroup(this, 'polkadotXcm');
-    this.xcmHelper = new PolkadexXcmHelperGroup(this);
-  }
-
-  getSudo<T extends PolkadexHelper>() {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const SudoHelperType = SudoHelper(this.helperBase);
-    return this.clone(SudoHelperType) as T;
-  }
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {
-  return class extends Base {
-    scheduleFn: 'schedule' | 'scheduleAfter';
-    blocksNum: number;
-    options: ISchedulerOptions;
-
-    constructor(...args: any[]) {
-      const logger = args[0] as ILogger;
-      const options = args[1] as {
-        scheduleFn: 'schedule' | 'scheduleAfter',
-        blocksNum: number,
-        options: ISchedulerOptions
-      };
-
-      super(logger);
-
-      this.scheduleFn = options.scheduleFn;
-      this.blocksNum = options.blocksNum;
-      this.options = options.options;
-    }
-
-    executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {
-      const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);
-
-      const mandatorySchedArgs = [
-        this.blocksNum,
-        this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,
-        this.options.priority ?? null,
-        scheduledTx,
-      ];
-
-      let schedArgs;
-      let scheduleFn;
-
-      if(this.options.scheduledId) {
-        schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];
-
-        if(this.scheduleFn == 'schedule') {
-          scheduleFn = 'scheduleNamed';
-        } else if(this.scheduleFn == 'scheduleAfter') {
-          scheduleFn = 'scheduleNamedAfter';
-        }
-      } else {
-        schedArgs = mandatorySchedArgs;
-        scheduleFn = this.scheduleFn;
-      }
-
-      const extrinsic = 'api.tx.scheduler.' + scheduleFn;
-
-      return super.executeExtrinsic(
-        sender,
-        extrinsic as any,
-        schedArgs,
-        expectSuccess,
-      );
-    }
-  };
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {
-  return class extends Base {
-    constructor(...args: any[]) {
-      super(...args);
-    }
-
-    async executeExtrinsic(
-      sender: IKeyringPair,
-      extrinsic: string,
-      params: any[],
-      expectSuccess?: boolean,
-      options: Partial<SignerOptions> | null = null,
-    ): Promise<ITransactionResult> {
-      const call = this.constructApiCall(extrinsic, params);
-      const result = await super.executeExtrinsic(
-        sender,
-        'api.tx.sudo.sudo',
-        [call],
-        expectSuccess,
-        options,
-      );
-
-      if(result.status === 'Fail') return result;
-
-      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
-      if(data.isErr) {
-        if(data.asErr.isModule) {
-          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
-          const metaError = super.getApi()?.registry.findMetaError(error);
-          throw new Error(`${metaError.section}.${metaError.name}`);
-        } else if(data.asErr.isToken) {
-          throw new Error(`Token: ${data.asErr.asToken}`);
-        }
-        // May be [object Object] in case of unhandled non-unit enum
-        throw new Error(`Misc: ${data.asErr.toHuman()}`);
-      }
-      return result;
-    }
-    async executeExtrinsicUncheckedWeight(
-      sender: IKeyringPair,
-      extrinsic: string,
-      params: any[],
-      expectSuccess?: boolean,
-      options: Partial<SignerOptions> | null = null,
-    ): Promise<ITransactionResult> {
-      const call = this.constructApiCall(extrinsic, params);
-      const result = await super.executeExtrinsic(
-        sender,
-        'api.tx.sudo.sudoUncheckedWeight',
-        [call, {refTime: 0, proofSize: 0}],
-        expectSuccess,
-        options,
-      );
-
-      if(result.status === 'Fail') return result;
-
-      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
-      if(data.isErr) {
-        if(data.asErr.isModule) {
-          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
-          const metaError = super.getApi()?.registry.findMetaError(error);
-          throw new Error(`${metaError.section}.${metaError.name}`);
-        } else if(data.asErr.isToken) {
-          throw new Error(`Token: ${data.asErr.asToken}`);
-        }
-        // May be [object Object] in case of unhandled non-unit enum
-        throw new Error(`Misc: ${data.asErr.toHuman()}`);
-      }
-      return result;
-    }
-  };
-}
-
 export class UniqueBaseCollection {
   helper: UniqueHelper;
   collectionId: number;
@@ -4272,29 +3081,8 @@
 
   async burn(signer: TSigner) {
     return await this.helper.collection.burn(signer, this.collectionId);
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
-    return new UniqueBaseCollection(this.collectionId, scheduledHelper);
-  }
-
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
-    return new UniqueBaseCollection(this.collectionId, scheduledHelper);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    return new UniqueBaseCollection(this.collectionId, this.helper.getSudo<T>());
   }
 }
-
 
 export class UniqueNFTCollection extends UniqueBaseCollection {
   getTokenObject(tokenId: number) {
@@ -4386,29 +3174,8 @@
 
   async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {
     return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
-    return new UniqueNFTCollection(this.collectionId, scheduledHelper);
-  }
-
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
-    return new UniqueNFTCollection(this.collectionId, scheduledHelper);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    return new UniqueNFTCollection(this.collectionId, this.helper.getSudo<T>());
   }
 }
-
 
 export class UniqueRFTCollection extends UniqueBaseCollection {
   getTokenObject(tokenId: number) {
@@ -4512,29 +3279,8 @@
 
   async unnestToken(signer: TSigner, tokenId: number, fromTokenObj: IToken, toAddressObj: ICrossAccountId) {
     return await this.helper.rft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
-    return new UniqueRFTCollection(this.collectionId, scheduledHelper);
-  }
-
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
-    return new UniqueRFTCollection(this.collectionId, scheduledHelper);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    return new UniqueRFTCollection(this.collectionId, this.helper.getSudo<T>());
   }
 }
-
 
 export class UniqueFTCollection extends UniqueBaseCollection {
   async getBalance(addressObj: ICrossAccountId) {
@@ -4579,29 +3325,8 @@
 
   async approveTokens(signer: TSigner, toAddressObj: ICrossAccountId, amount = 1n) {
     return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount);
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
-    return new UniqueFTCollection(this.collectionId, scheduledHelper);
-  }
-
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
-    return new UniqueFTCollection(this.collectionId, scheduledHelper);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    return new UniqueFTCollection(this.collectionId, this.helper.getSudo<T>());
   }
 }
-
 
 export class UniqueBaseToken {
   collection: UniqueNFTCollection | UniqueRFTCollection;
@@ -4640,29 +3365,8 @@
 
   nestingAccount() {
     return this.collection.helper.util.getTokenAccount(this);
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);
-    return new UniqueBaseToken(this.tokenId, scheduledCollection);
-  }
-
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);
-    return new UniqueBaseToken(this.tokenId, scheduledCollection);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    return new UniqueBaseToken(this.tokenId, this.collection.getSudo<T>());
   }
 }
-
 
 export class UniqueNFToken extends UniqueBaseToken {
   collection: UniqueNFTCollection;
@@ -4718,26 +3422,6 @@
 
   async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId) {
     return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj);
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);
-    return new UniqueNFToken(this.tokenId, scheduledCollection);
-  }
-
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);
-    return new UniqueNFToken(this.tokenId, scheduledCollection);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    return new UniqueNFToken(this.tokenId, this.collection.getSudo<T>());
   }
 }
 
@@ -4807,25 +3491,5 @@
 
   async burnFrom(signer: TSigner, fromAddressObj: ICrossAccountId, amount = 1n) {
     return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount);
-  }
-
-  scheduleAt<T extends UniqueHelper>(
-    executionBlockNumber: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);
-    return new UniqueRFToken(this.tokenId, scheduledCollection);
-  }
-
-  scheduleAfter<T extends UniqueHelper>(
-    blocksBeforeExecution: number,
-    options: ISchedulerOptions = {},
-  ) {
-    const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);
-    return new UniqueRFToken(this.tokenId, scheduledCollection);
-  }
-
-  getSudo<T extends UniqueHelper>() {
-    return new UniqueRFToken(this.tokenId, this.collection.getSudo<T>());
   }
 }
addedtests/src/util/playgrounds/unique.xcm.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/unique.xcm.ts
@@ -0,0 +1,371 @@
+import {ApiPromise, WsProvider} from '@polkadot/api';
+import {IKeyringPair} from '@polkadot/types/types';
+import {ChainHelperBase, EthereumBalanceGroup, HelperGroup, SubstrateBalanceGroup, UniqueHelper} from './unique';
+import {ILogger, TSigner, TSubstrateAccount} from './types';
+import {AcalaAssetMetadata, DemocracyStandardAccountVote, IForeignAssetMetadata, MoonbeamAssetInfo} from './types.xcm';
+
+
+export class XcmChainHelper extends ChainHelperBase {
+  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
+    const wsProvider = new WsProvider(wsEndpoint);
+    this.api = new ApiPromise({
+      provider: wsProvider,
+    });
+    await this.api.isReadyOrError;
+    this.network = await UniqueHelper.detectNetwork(this.api);
+  }
+}
+
+class AcalaAssetRegistryGroup extends HelperGroup<AcalaHelper> {
+  async registerForeignAsset(signer: TSigner, destination: any, metadata: AcalaAssetMetadata) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assetRegistry.registerForeignAsset', [destination, metadata], true);
+  }
+}
+
+class MoonbeamAssetManagerGroup extends HelperGroup<MoonbeamHelper> {
+  makeRegisterForeignAssetProposal(assetInfo: MoonbeamAssetInfo) {
+    const apiPrefix = 'api.tx.assetManager.';
+
+    const registerTx = this.helper.constructApiCall(
+      apiPrefix + 'registerForeignAsset',
+      [assetInfo.location, assetInfo.metadata, assetInfo.existentialDeposit, assetInfo.isSufficient],
+    );
+
+    const setUnitsTx = this.helper.constructApiCall(
+      apiPrefix + 'setAssetUnitsPerSecond',
+      [assetInfo.location, assetInfo.unitsPerSecond, assetInfo.numAssetsWeightHint],
+    );
+
+    const batchCall = this.helper.getApi().tx.utility.batchAll([registerTx, setUnitsTx]);
+    const encodedProposal = batchCall?.method.toHex() || '';
+    return encodedProposal;
+  }
+
+  async assetTypeId(location: any) {
+    return await this.helper.callRpc('api.query.assetManager.assetTypeId', [location]);
+  }
+}
+
+class MoonbeamDemocracyGroup extends HelperGroup<MoonbeamHelper> {
+  notePreimagePallet: string;
+
+  constructor(helper: MoonbeamHelper, options: { [key: string]: any } = {}) {
+    super(helper);
+    this.notePreimagePallet = options.notePreimagePallet;
+  }
+
+  async notePreimage(signer: TSigner, encodedProposal: string) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.notePreimagePallet}.notePreimage`, [encodedProposal], true);
+  }
+
+  externalProposeMajority(proposal: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [proposal]);
+  }
+
+  fastTrack(proposalHash: string, votingPeriod: number, delayPeriod: number) {
+    return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
+  }
+
+  async referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
+  }
+}
+
+class MoonbeamCollectiveGroup extends HelperGroup<MoonbeamHelper> {
+  collective: string;
+
+  constructor(helper: MoonbeamHelper, collective: string) {
+    super(helper);
+
+    this.collective = collective;
+  }
+
+  async propose(signer: TSigner, threshold: number, proposalHash: string, lengthBound: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [threshold, proposalHash, lengthBound], true);
+  }
+
+  async vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve], true);
+  }
+
+  async close(signer: TSigner, proposalHash: string, proposalIndex: number, weightBound: any, lengthBound: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [proposalHash, proposalIndex, weightBound, lengthBound], true);
+  }
+
+  async proposalCount() {
+    return Number(await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, []));
+  }
+}
+
+class PolkadexXcmHelperGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async whitelistToken(signer: TSigner, assetId: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xcmHelper.whitelistToken', [assetId], true);
+  }
+}
+
+export class ForeignAssetsGroup extends HelperGroup<UniqueHelper> {
+  async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) {
+    await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.foreignAssets.registerForeignAsset',
+      [ownerAddress, location, metadata],
+      true,
+    );
+  }
+
+  async update(signer: TSigner, foreignAssetId: number, location: any, metadata: IForeignAssetMetadata) {
+    await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.foreignAssets.updateForeignAsset',
+      [foreignAssetId, location, metadata],
+      true,
+    );
+  }
+}
+
+export class XcmGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  palletName: string;
+
+  constructor(helper: T, palletName: string) {
+    super(helper);
+
+    this.palletName = palletName;
+  }
+
+  async limitedReserveTransferAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number, weightLimit: any) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.limitedReserveTransferAssets`, [destination, beneficiary, assets, feeAssetItem, weightLimit], true);
+  }
+
+  async setSafeXcmVersion(signer: TSigner, version: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.forceDefaultXcmVersion`, [version], true);
+  }
+
+  async teleportAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.teleportAssets`, [destination, beneficiary, assets, feeAssetItem], true);
+  }
+
+  async teleportNativeAsset(signer: TSigner, destinationParaId: number, targetAccount: Uint8Array, amount: bigint, xcmVersion = 3) {
+    const destinationContent = {
+      parents: 0,
+      interior: {
+        X1: {
+          Parachain: destinationParaId,
+        },
+      },
+    };
+
+    const beneficiaryContent = {
+      parents: 0,
+      interior: {
+        X1: {
+          AccountId32: {
+            network: 'Any',
+            id: targetAccount,
+          },
+        },
+      },
+    };
+
+    const assetsContent = [
+      {
+        id: {
+          Concrete: {
+            parents: 0,
+            interior: 'Here',
+          },
+        },
+        fun: {
+          Fungible: amount,
+        },
+      },
+    ];
+
+    let destination;
+    let beneficiary;
+    let assets;
+
+    if(xcmVersion == 2) {
+      destination = {V1: destinationContent};
+      beneficiary = {V1: beneficiaryContent};
+      assets = {V1: assetsContent};
+
+    } else if(xcmVersion == 3) {
+      destination = {V2: destinationContent};
+      beneficiary = {V2: beneficiaryContent};
+      assets = {V2: assetsContent};
+
+    } else {
+      throw Error('Unknown XCM version: ' + xcmVersion);
+    }
+
+    const feeAssetItem = 0;
+
+    await this.teleportAssets(signer, destination, beneficiary, assets, feeAssetItem);
+  }
+
+  async send(signer: IKeyringPair, destination: any, message: any) {
+    await this.helper.executeExtrinsic(
+      signer,
+      `api.tx.${this.palletName}.send`,
+      [
+        destination,
+        message,
+      ],
+      true,
+    );
+  }
+}
+
+export class XTokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async transfer(signer: TSigner, currencyId: any, amount: bigint, destination: any, destWeight: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transfer', [currencyId, amount, destination, destWeight], true);
+  }
+
+  async transferMultiasset(signer: TSigner, asset: any, destination: any, destWeight: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMultiasset', [asset, destination, destWeight], true);
+  }
+
+  async transferMulticurrencies(signer: TSigner, currencies: any[], feeItem: number, destLocation: any, destWeight: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMulticurrencies', [currencies, feeItem, destLocation, destWeight], true);
+  }
+}
+
+
+
+export class TokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async accounts(address: string, currencyId: any) {
+    const {free} = (await this.helper.callRpc('api.query.tokens.accounts', [address, currencyId])).toJSON() as any;
+    return BigInt(free);
+  }
+}
+
+export class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
+  }
+
+  async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true);
+  }
+
+  async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
+  }
+
+  async account(assetId: string | number, address: string) {
+    const accountAsset = (
+      await this.helper.callRpc('api.query.assets.account', [assetId, address])
+    ).toJSON()! as any;
+
+    if(accountAsset !== null) {
+      return BigInt(accountAsset['balance']);
+    } else {
+      return null;
+    }
+  }
+}
+
+export class RelayHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<RelayHelper>;
+  xcm: XcmGroup<RelayHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? RelayHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.xcm = new XcmGroup(this, 'xcmPallet');
+  }
+}
+
+export class WestmintHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<WestmintHelper>;
+  xcm: XcmGroup<WestmintHelper>;
+  assets: AssetsGroup<WestmintHelper>;
+  xTokens: XTokensGroup<WestmintHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? WestmintHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+    this.assets = new AssetsGroup(this);
+    this.xTokens = new XTokensGroup(this);
+  }
+}
+
+export class MoonbeamHelper extends XcmChainHelper {
+  balance: EthereumBalanceGroup<MoonbeamHelper>;
+  assetManager: MoonbeamAssetManagerGroup;
+  assets: AssetsGroup<MoonbeamHelper>;
+  xTokens: XTokensGroup<MoonbeamHelper>;
+  democracy: MoonbeamDemocracyGroup;
+  collective: {
+    council: MoonbeamCollectiveGroup,
+    techCommittee: MoonbeamCollectiveGroup,
+  };
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? MoonbeamHelper);
+
+    this.balance = new EthereumBalanceGroup(this);
+    this.assetManager = new MoonbeamAssetManagerGroup(this);
+    this.assets = new AssetsGroup(this);
+    this.xTokens = new XTokensGroup(this);
+    this.democracy = new MoonbeamDemocracyGroup(this, options);
+    this.collective = {
+      council: new MoonbeamCollectiveGroup(this, 'councilCollective'),
+      techCommittee: new MoonbeamCollectiveGroup(this, 'techCommitteeCollective'),
+    };
+  }
+}
+
+export class AstarHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<AstarHelper>;
+  assets: AssetsGroup<AstarHelper>;
+  xcm: XcmGroup<AstarHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? AstarHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.assets = new AssetsGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+  }
+}
+
+export class AcalaHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<AcalaHelper>;
+  assetRegistry: AcalaAssetRegistryGroup;
+  xTokens: XTokensGroup<AcalaHelper>;
+  tokens: TokensGroup<AcalaHelper>;
+  xcm: XcmGroup<AcalaHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? AcalaHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.assetRegistry = new AcalaAssetRegistryGroup(this);
+    this.xTokens = new XTokensGroup(this);
+    this.tokens = new TokensGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+  }
+}
+
+export class PolkadexHelper extends XcmChainHelper {
+  assets: AssetsGroup<PolkadexHelper>;
+  balance: SubstrateBalanceGroup<PolkadexHelper>;
+  xTokens: XTokensGroup<PolkadexHelper>;
+  xcm: XcmGroup<PolkadexHelper>;
+  xcmHelper: PolkadexXcmHelperGroup<PolkadexHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? PolkadexHelper);
+
+    this.assets = new AssetsGroup(this);
+    this.balance = new SubstrateBalanceGroup(this);
+    this.xTokens = new XTokensGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+    this.xcmHelper = new PolkadexXcmHelperGroup(this);
+  }
+}
+