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

difftreelog

refactor js-packages structure

Andy Smith2024-01-06parent: #a478235.patch.diff
in: master

154 files changed

modified.github/workflows/xcm.ymldiffbeforeafterboth
--- a/.github/workflows/xcm.yml
+++ b/.github/workflows/xcm.yml
@@ -368,9 +368,9 @@
           yarn add mochawesome
 
       - name: Call HRMP initialization
-        working-directory: js-packages/tests
+        working-directory: js-packages/scripts
         run: |
-          yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm util/createHrmp.ts ${{matrix.network}}
+          yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm createHrmp.ts ${{matrix.network}}
 
       - name: Run XCM tests
         working-directory: js-packages/tests
modifiedjs-packages/package.jsondiffbeforeafterboth
--- a/js-packages/package.json
+++ b/js-packages/package.json
@@ -26,8 +26,9 @@
     "@types/node": "^20.8.10",
     "@typescript-eslint/eslint-plugin": "^6.10.0",
     "@typescript-eslint/parser": "^6.10.0",
-    "@unique/opal-types": "workspace:*",
-    "@unique/playgrounds": "workspace:*",
+    "@unique-nft/opal-testnet-types": "workspace:*",
+    "@unique-nft/playgrounds": "workspace:*",
+    "@unique/test-utils": "workspace:*",
     "chai": "^4.3.10",
     "chai-subset": "^1.6.0",
     "eslint": "^8.53.0",
@@ -59,6 +60,7 @@
     "types",
     "playgrounds",
     "scripts",
+    "test-utils",
     "tests"
   ]
 }
modifiedjs-packages/playgrounds/package.jsondiffbeforeafterboth
--- a/js-packages/playgrounds/package.json
+++ b/js-packages/playgrounds/package.json
@@ -1,18 +1,17 @@
 {
-  "author": "",
-  "license": "SEE LICENSE IN ../../../LICENSE",
-  "description": "Playground scripts",
+  "author": "Unique Network",
+  "license": "Apache 2.0",
+  "description": "Helpers for Unique Network chain",
   "engines": {
-    "node": ">=16"
+    "node": ">=18"
   },
-  "name": "@unique/playgrounds",
+  "name": "@unique-nft/playgrounds",
   "type": "module",
   "version": "1.0.0",
   "main": "unique.js",
   "dependencies": {
     "@polkadot/api": "10.10.1",
     "@polkadot/util": "^12.5.1",
-    "@polkadot/util-crypto": "^12.5.1",
-    "@unique/opal-types": "workspace:*"
+    "@polkadot/util-crypto": "^12.5.1"
   }
 }
deletedjs-packages/playgrounds/types.xcm.tsdiffbeforeafterboth
--- a/js-packages/playgrounds/types.xcm.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-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,
-  },
-}
deletedjs-packages/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/js-packages/playgrounds/unique.dev.ts
+++ /dev/null
@@ -1,1646 +0,0 @@
-// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
-// SPDX-License-Identifier: Apache-2.0
-
-import '@unique/opal-types/augment-api.js';
-import '@unique/opal-types/augment-types.js';
-import '@unique/opal-types/types-lookup.js';
-
-import {stringToU8a} from '@polkadot/util';
-import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';
-import type {ChainHelperBaseConstructor, UniqueHelperConstructor} from './unique.js';
-import {UniqueHelper, ChainHelperBase, HelperGroup} from './unique.js';
-import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';
-import * as defs from '@unique/opal-types/definitions.js';
-import type {IKeyringPair} from '@polkadot/types/types';
-import type {EventRecord} from '@polkadot/types/interfaces';
-import type {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types.js';
-import type {FrameSystemEventRecord, StagingXcmV2TraitsError, StagingXcmV3TraitsOutcome} from '@polkadot/types/lookup';
-import type {SignerOptions, VoidFn} from '@polkadot/api/types';
-import {spawnSync} from 'child_process';
-import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup, HydraDxHelper} from './unique.xcm.js';
-import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, RankedCollectiveGroup, ReferendaGroup} from './unique.governance.js';
-import type {ICollectiveGroup, IFellowshipGroup} from './unique.governance.js';
-
-export class SilentLogger {
-  log(_msg: any, _level: any): void { }
-  level = {
-    ERROR: 'ERROR' as const,
-    WARNING: 'WARNING' as const,
-    INFO: 'INFO' as const,
-  };
-}
-
-export class SilentConsole {
-  // TODO: Remove, this is temporary: Filter unneeded API output
-  // (Jaco promised it will be removed in the next version)
-  consoleErr: any;
-  consoleLog: any;
-  consoleWarn: any;
-
-  constructor() {
-    this.consoleErr = console.error;
-    this.consoleLog = console.log;
-    this.consoleWarn = console.warn;
-  }
-
-  enable() {
-    const outFn = (printer: any) => (...args: any[]) => {
-      for(const arg of args) {
-        if(typeof arg !== 'string')
-          continue;
-        const skippedWarnings = ['1000:: Normal connection closure', 'Not decorating unknown runtime apis:', 'RPC methods not decorated:', 'Not decorating runtime apis', 'Bad input data provided to validate_transaction', 'account balance too low', '1006:: Abnormal Closure'];
-        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);
-        if(needToSkip || arg === 'Normal connection closure')
-          return;
-      }
-      printer(...args);
-    };
-
-    console.error = outFn(this.consoleErr.bind(console));
-    console.log = outFn(this.consoleLog.bind(console));
-    console.warn = outFn(this.consoleWarn.bind(console));
-  }
-
-  disable() {
-    console.error = this.consoleErr;
-    console.log = this.consoleLog;
-    console.warn = this.consoleWarn;
-  }
-}
-
-export interface IEventHelper {
-  section(): string;
-
-  method(): string;
-
-  wrapEvent(data: any[]): any;
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {
-  const helperClass = class implements IEventHelper {
-    wrapEvent: (data: any[]) => any;
-    _section: string;
-    _method: string;
-
-    constructor() {
-      this.wrapEvent = wrapEvent;
-      this._section = section;
-      this._method = method;
-    }
-
-    section(): string {
-      return this._section;
-    }
-
-    method(): string {
-      return this._method;
-    }
-
-    filter(txres: ITransactionResult) {
-      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)
-        .map(e => this.wrapEvent(e.event.data));
-    }
-
-    find(txres: ITransactionResult) {
-      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);
-      return e ? this.wrapEvent(e.event.data) : null;
-    }
-
-    expect(txres: ITransactionResult) {
-      const e = this.find(txres);
-      if(e) {
-        return e;
-      } else {
-        throw Error(`Expected event ${section}.${method}`);
-      }
-    }
-  };
-
-  return helperClass;
-}
-
-function eventJsonData<T = any>(data: any[], index: number) {
-  return data[index].toJSON() as T;
-}
-
-function eventHumanData(data: any[], index: number) {
-  return data[index].toHuman();
-}
-
-function eventData<T = any>(data: any[], index: number) {
-  return data[index] as T;
-}
-
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function EventSection(section: string) {
-  return class Section {
-    static section = section;
-
-    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {
-      const helperClass = EventHelper(Section.section, name, wrapEvent);
-      return new helperClass();
-    }
-  };
-}
-
-function schedulerSection(schedulerInstance: string) {
-  return class extends EventSection(schedulerInstance) {
-    static Dispatched = this.Method('Dispatched', data => ({
-      task: eventJsonData(data, 0),
-      id: eventHumanData(data, 1),
-      result: data[2],
-    }));
-
-    static PriorityChanged = this.Method('PriorityChanged', data => ({
-      task: eventJsonData(data, 0),
-      priority: eventJsonData(data, 1),
-    }));
-  };
-}
-
-export class Event {
-  static Democracy = class extends EventSection('democracy') {
-    static Proposed = this.Method('Proposed', data => ({
-      proposalIndex: eventJsonData<number>(data, 0),
-    }));
-
-    static ExternalTabled = this.Method('ExternalTabled');
-
-    static Started = this.Method('Started', data => ({
-      referendumIndex: eventJsonData<number>(data, 0),
-      threshold: eventHumanData(data, 1),
-    }));
-
-    static Voted = this.Method('Voted', data => ({
-      voter: eventJsonData(data, 0),
-      referendumIndex: eventJsonData<number>(data, 1),
-      vote: eventJsonData(data, 2),
-    }));
-
-    static Passed = this.Method('Passed', data => ({
-      referendumIndex: eventJsonData<number>(data, 0),
-    }));
-
-    static ProposalCanceled = this.Method('ProposalCanceled', data => ({
-      propIndex: eventJsonData<number>(data, 0),
-    }));
-
-    static Cancelled = this.Method('Cancelled', data => ({
-      propIndex: eventJsonData<number>(data, 0),
-    }));
-
-    static Vetoed = this.Method('Vetoed', data => ({
-      who: eventHumanData(data, 0),
-      proposalHash: eventHumanData(data, 1),
-      until: eventJsonData<number>(data, 1),
-    }));
-  };
-
-  static Council = class extends EventSection('council') {
-    static Proposed = this.Method('Proposed', data => ({
-      account: eventHumanData(data, 0),
-      proposalIndex: eventJsonData<number>(data, 1),
-      proposalHash: eventHumanData(data, 2),
-      threshold: eventJsonData<number>(data, 3),
-    }));
-    static Closed = this.Method('Closed', data => ({
-      proposalHash: eventHumanData(data, 0),
-      yes: eventJsonData<number>(data, 1),
-      no: eventJsonData<number>(data, 2),
-    }));
-    static Executed = this.Method('Executed', data => ({
-      proposalHash: eventHumanData(data, 0),
-    }));
-  };
-
-  static TechnicalCommittee = class extends EventSection('technicalCommittee') {
-    static Proposed = this.Method('Proposed', data => ({
-      account: eventHumanData(data, 0),
-      proposalIndex: eventJsonData<number>(data, 1),
-      proposalHash: eventHumanData(data, 2),
-      threshold: eventJsonData<number>(data, 3),
-    }));
-    static Closed = this.Method('Closed', data => ({
-      proposalHash: eventHumanData(data, 0),
-      yes: eventJsonData<number>(data, 1),
-      no: eventJsonData<number>(data, 2),
-    }));
-    static Approved = this.Method('Approved', data => ({
-      proposalHash: eventHumanData(data, 0),
-    }));
-    static Executed = this.Method('Executed', data => ({
-      proposalHash: eventHumanData(data, 0),
-      result: eventHumanData(data, 1),
-    }));
-  };
-
-  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {
-    static Submitted = this.Method('Submitted', data => ({
-      referendumIndex: eventJsonData<number>(data, 0),
-      trackId: eventJsonData<number>(data, 1),
-      proposal: eventJsonData(data, 2),
-    }));
-
-    static Cancelled = this.Method('Cancelled', data => ({
-      index: eventJsonData<number>(data, 0),
-      tally: eventJsonData(data, 1),
-    }));
-  };
-
-  static UniqueScheduler = schedulerSection('uniqueScheduler');
-  static Scheduler = schedulerSection('scheduler');
-
-  static XcmpQueue = class extends EventSection('xcmpQueue') {
-    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({
-      messageHash: eventJsonData(data, 0),
-    }));
-
-    static Success = this.Method('Success', data => ({
-      messageHash: eventJsonData(data, 0),
-    }));
-
-    static Fail = this.Method('Fail', data => ({
-      messageHash: eventJsonData(data, 0),
-      outcome: eventData<StagingXcmV2TraitsError>(data, 2),
-    }));
-  };
-
-  static DmpQueue = class extends EventSection('dmpQueue') {
-    static ExecutedDownward = this.Method('ExecutedDownward', data => ({
-      outcome: eventData<StagingXcmV3TraitsOutcome>(data, 2),
-    }));
-  };
-}
-
-// 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);
-    }
-
-    override 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;
-    }
-    override 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
-   */
-  arrange: ArrangeGroup;
-  wait: WaitGroup;
-  admin: AdminGroup;
-  session: SessionGroup;
-  testUtils: TestUtilGroup;
-  foreignAssets: ForeignAssetsGroup;
-  xcm: XcmGroup<DevUniqueHelper>;
-  xTokens: XTokensGroup<DevUniqueHelper>;
-  tokens: TokensGroup<DevUniqueHelper>;
-  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;
-
-    super(logger, options);
-    this.arrange = new ArrangeGroup(this);
-    this.wait = new WaitGroup(this);
-    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);
-  }
-
-  override async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
-    if(!wsEndpoint) throw new Error('wsEndpoint was not set');
-    const wsProvider = new WsProvider(wsEndpoint);
-    this.api = new ApiPromise({
-      provider: wsProvider,
-      signedExtensions: {
-        ContractHelpers: {
-          extrinsic: {},
-          payload: {},
-        },
-        CheckMaintenance: {
-          extrinsic: {},
-          payload: {},
-        },
-        DisableIdentityCalls: {
-          extrinsic: {},
-          payload: {},
-        },
-        FakeTransactionFinalizer: {
-          extrinsic: {},
-          payload: {},
-        },
-      },
-      rpc: {
-        unique: defs.unique.rpc,
-        appPromotion: defs.appPromotion.rpc,
-        povinfo: defs.povinfo.rpc,
-        eth: {
-          feeHistory: {
-            description: 'Dummy',
-            params: [],
-            type: 'u8',
-          },
-          maxPriorityFeePerGas: {
-            description: 'Dummy',
-            params: [],
-            type: 'u8',
-          },
-        },
-      },
-    });
-    await this.api.isReadyOrError;
-    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 {
-  wait: WaitGroup;
-
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevRelayHelper;
-
-    super(logger, options);
-    this.wait = new WaitGroup(this);
-  }
-
-  getSudo() {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const SudoHelperType = SudoHelper(this.helperBase);
-    return this.clone(SudoHelperType) as DevRelayHelper;
-  }
-}
-
-export class DevWestmintHelper extends WestmintHelper {
-  wait: WaitGroup;
-
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevWestmintHelper;
-
-    super(logger, options);
-    this.wait = new WaitGroup(this);
-  }
-}
-
-export class DevStatemineHelper extends DevWestmintHelper {}
-
-export class DevStatemintHelper extends DevWestmintHelper {}
-
-export class DevMoonbeamHelper extends MoonbeamHelper {
-  account: MoonbeamAccountGroup;
-  wait: WaitGroup;
-  fastDemocracy: MoonbeamFastDemocracyGroup;
-
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevMoonbeamHelper;
-    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';
-
-    super(logger, options);
-    this.account = new MoonbeamAccountGroup(this);
-    this.wait = new WaitGroup(this);
-    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);
-  }
-}
-
-export class DevMoonriverHelper extends DevMoonbeamHelper {
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';
-    super(logger, options);
-  }
-}
-
-export class DevAstarHelper extends AstarHelper {
-  wait: WaitGroup;
-
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevAstarHelper;
-
-    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;
-
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevAcalaHelper;
-
-    super(logger, options);
-    this.wait = new WaitGroup(this);
-  }
-  getSudo() {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const SudoHelperType = SudoHelper(this.helperBase);
-    return this.clone(SudoHelperType) as DevAcalaHelper;
-  }
-}
-
-export class DevPolkadexHelper extends PolkadexHelper {
-  wait: WaitGroup;
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? PolkadexHelper;
-
-    super(logger, options);
-    this.wait = new WaitGroup(this);
-  }
-
-  getSudo() {
-    // eslint-disable-next-line @typescript-eslint/naming-convention
-    const SudoHelperType = SudoHelper(this.helperBase);
-    return this.clone(SudoHelperType) as DevPolkadexHelper;
-  }
-}
-
-export class DevHydraDxHelper extends HydraDxHelper {
-  wait: WaitGroup;
-  fastDemocracy: HydraFastDemocracyGroup;
-
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevHydraDxHelper;
-
-    super(logger, options);
-
-    this.wait = new WaitGroup(this);
-    this.fastDemocracy = new HydraFastDemocracyGroup(this);
-  }
-}
-
-export class DevKaruraHelper extends DevAcalaHelper {}
-
-export class ArrangeGroup {
-  helper: DevUniqueHelper;
-
-  scheduledIdSlider = 0;
-
-  constructor(helper: DevUniqueHelper) {
-    this.helper = helper;
-  }
-
-  /**
-   * Generates accounts with the specified UNQ token balance
-   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.
-   * @param donor donor account for balances
-   * @returns array of newly created accounts
-   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);
-   */
-  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {
-    let nonce = await this.helper.chain.getNonce(donor.address);
-    const wait = new WaitGroup(this.helper);
-    const ss58Format = this.helper.chain.getChainProperties().ss58Format;
-    const tokenNominal = this.helper.balance.getOneTokenNominal();
-    const transactions = [];
-    const accounts: IKeyringPair[] = [];
-    for(const balance of balances) {
-      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);
-      accounts.push(recipient);
-      if(balance !== 0n) {
-        const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, balance * tokenNominal]);
-        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));
-        nonce++;
-      }
-    }
-
-    await Promise.all(transactions).catch(_e => {});
-
-    //#region TODO remove this region, when nonce problem will be solved
-    const checkBalances = async () => {
-      let isSuccess = true;
-      for(let i = 0; i < balances.length; i++) {
-        const balance = await this.helper.balance.getSubstrate(accounts[i].address);
-        if(balance !== balances[i] * tokenNominal) {
-          isSuccess = false;
-          break;
-        }
-      }
-      return isSuccess;
-    };
-
-    let accountsCreated = false;
-    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;
-    // checkBalances retry up to 5-50 blocks
-    for(let index = 0; index < maxBlocksChecked; index++) {
-      accountsCreated = await checkBalances();
-      if(accountsCreated) break;
-      await wait.newBlocks(1);
-    }
-
-    if(!accountsCreated) throw Error('Accounts generation failed');
-    //#endregion
-
-    return accounts;
-  };
-
-  // TODO combine this method and createAccounts into one
-  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {
-    const createAsManyAsCan = async () => {
-      let transactions: any = [];
-      const accounts: IKeyringPair[] = [];
-      let nonce = await this.helper.chain.getNonce(donor.address);
-      const tokenNominal = this.helper.balance.getOneTokenNominal();
-      const ss58Format = this.helper.chain.getChainProperties().ss58Format;
-      for(let i = 0; i < accountsToCreate; i++) {
-        if(i === 500) { // if there are too many accounts to create
-          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled
-          transactions = []; //
-          nonce = await this.helper.chain.getNonce(donor.address); // update nonce
-        }
-        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);
-        accounts.push(recipient);
-        if(withBalance !== 0n) {
-          const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, withBalance * tokenNominal]);
-          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));
-          nonce++;
-        }
-      }
-
-      const fullfilledAccounts = [];
-      await Promise.allSettled(transactions);
-      for(const account of accounts) {
-        const accountBalance = await this.helper.balance.getSubstrate(account.address);
-        if(accountBalance === withBalance * tokenNominal) {
-          fullfilledAccounts.push(account);
-        }
-      }
-      return fullfilledAccounts;
-    };
-
-
-    const crowd: IKeyringPair[] = [];
-    // do up to 5 retries
-    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {
-      const asManyAsCan = await createAsManyAsCan();
-      crowd.push(...asManyAsCan);
-      accountsToCreate -= asManyAsCan.length;
-    }
-
-    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);
-
-    return crowd;
-  };
-
-  /**
-   * Generates one account with zero balance
-   * @returns the newly generated account
-   * @example const account = await helper.arrange.createEmptyAccount();
-   */
-  createEmptyAccount = (): IKeyringPair => {
-    const ss58Format = this.helper.chain.getChainProperties().ss58Format;
-    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);
-  };
-
-  isDevNode = async () => {
-    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();
-    if(blockNumber == 0) {
-      await this.helper.wait.newBlocks(1);
-      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();
-    }
-    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);
-    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);
-    const findCreationDate = (block: any) => {
-      const humanBlock = block.toHuman();
-      let date;
-      humanBlock.block.extrinsics.forEach((ext: any) => {
-        if(ext.method.section === 'timestamp') {
-          date = Number(ext.method.args.now.replaceAll(',', ''));
-        }
-      });
-      return date;
-    };
-    const block1date = await findCreationDate(block1);
-    const block2date = await findCreationDate(block2);
-    if(block2date! - block1date! < 9000) return true;
-    return false;
-  };
-
-  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {
-    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);
-    let balance = await this.helper.balance.getSubstrate(address);
-
-    await promise();
-
-    balance -= await this.helper.balance.getSubstrate(address);
-
-    return balance;
-  }
-
-  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {
-    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);
-
-    const kvJson: {[key: string]: string} = {};
-
-    for(const kv of rawPovInfo.keyValues) {
-      kvJson[kv.key.toHex()] = kv.value.toHex();
-    }
-
-    const kvStr = JSON.stringify(kvJson);
-
-    const chainql = spawnSync(
-      'chainql',
-      [
-        `--tla-code=data=${kvStr}`,
-        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,
-      ],
-    );
-
-    if(!chainql.stdout) {
-      throw Error('unable to get an output from the `chainql`');
-    }
-
-    return {
-      proofSize: rawPovInfo.proofSize.toNumber(),
-      compactProofSize: rawPovInfo.compactProofSize.toNumber(),
-      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),
-      results: rawPovInfo.results,
-      kv: JSON.parse(chainql.stdout.toString()),
-    };
-  }
-
-  calculatePalletAddress(palletId: any) {
-    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));
-    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);
-  }
-
-  makeScheduledIds(num: number): string[] {
-    function makeId(slider: number) {
-      const scheduledIdSize = 64;
-      const hexId = slider.toString(16);
-      const prefixSize = scheduledIdSize - hexId.length;
-
-      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;
-
-      return scheduledId;
-    }
-
-    const ids = [];
-    for(let i = 0; i < num; i++) {
-      ids.push(makeId(this.scheduledIdSlider));
-      this.scheduledIdSlider += 1;
-    }
-
-    return ids;
-  }
-
-  makeScheduledId(): string {
-    return (this.makeScheduledIds(1))[0];
-  }
-
-  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {
-    const capture = new EventCapture(this.helper, eventSection, eventMethod);
-    await capture.startCapture();
-
-    return capture;
-  }
-
-  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {
-    return {
-      V2: [
-        {
-          WithdrawAsset: [
-            {
-              id,
-              fun: {
-                Fungible: amount,
-              },
-            },
-          ],
-        },
-        {
-          BuyExecution: {
-            fees: {
-              id,
-              fun: {
-                Fungible: amount,
-              },
-            },
-            weightLimit: 'Unlimited',
-          },
-        },
-        {
-          DepositAsset: {
-            assets: {
-              Wild: 'All',
-            },
-            maxAssets: 1,
-            beneficiary: {
-              parents: 0,
-              interior: {
-                X1: {
-                  AccountId32: {
-                    network: 'Any',
-                    id: beneficiary,
-                  },
-                },
-              },
-            },
-          },
-        },
-      ],
-    };
-  }
-
-  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {
-    return {
-      V2: [
-        {
-          ReserveAssetDeposited: [
-            {
-              id,
-              fun: {
-                Fungible: amount,
-              },
-            },
-          ],
-        },
-        {
-          BuyExecution: {
-            fees: {
-              id,
-              fun: {
-                Fungible: amount,
-              },
-            },
-            weightLimit: 'Unlimited',
-          },
-        },
-        {
-          DepositAsset: {
-            assets: {
-              Wild: 'All',
-            },
-            maxAssets: 1,
-            beneficiary: {
-              parents: 0,
-              interior: {
-                X1: {
-                  AccountId32: {
-                    network: 'Any',
-                    id: beneficiary,
-                  },
-                },
-              },
-            },
-          },
-        },
-      ],
-    };
-  }
-
-  makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {
-    return {
-      V3: [
-        {
-          UnpaidExecution: {
-            weightLimit: 'Unlimited',
-            checkOrigin: null,
-          },
-        },
-        {
-          Transact: {
-            originKind: 'Superuser',
-            requireWeightAtMost: {
-              refTime: info.weightMultiplier * 200000000,
-              proofSize: info.weightMultiplier * 3000,
-            },
-            call: {
-              encoded: info.call,
-            },
-          },
-        },
-      ],
-    };
-  }
-}
-
-class MoonbeamAccountGroup {
-  helper: MoonbeamHelper;
-
-  keyring: Keyring;
-  _alithAccount: IKeyringPair;
-  _baltatharAccount: IKeyringPair;
-  _dorothyAccount: IKeyringPair;
-
-  constructor(helper: MoonbeamHelper) {
-    this.helper = helper;
-
-    this.keyring = new Keyring({type: 'ethereum'});
-    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';
-    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';
-    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';
-
-    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');
-    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');
-    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');
-  }
-
-  alithAccount() {
-    return this._alithAccount;
-  }
-
-  baltatharAccount() {
-    return this._baltatharAccount;
-  }
-
-  dorothyAccount() {
-    return this._dorothyAccount;
-  }
-
-  create() {
-    return this.keyring.addFromUri(mnemonicGenerate());
-  }
-}
-
-class MoonbeamFastDemocracyGroup {
-  helper: DevMoonbeamHelper;
-
-  constructor(helper: DevMoonbeamHelper) {
-    this.helper = helper;
-  }
-
-  async executeProposal(proposalDesciption: string, encodedProposal: string) {
-    const proposalHash = blake2AsHex(encodedProposal);
-
-    const alithAccount = this.helper.account.alithAccount();
-    const baltatharAccount = this.helper.account.baltatharAccount();
-    const dorothyAccount = this.helper.account.dorothyAccount();
-
-    const councilVotingThreshold = 2;
-    const technicalCommitteeThreshold = 2;
-    const fastTrackVotingPeriod = 3;
-    const fastTrackDelayPeriod = 0;
-
-    console.log(`[democracy] executing '${proposalDesciption}' proposal`);
-
-    // >>> Propose external motion through council >>>
-    console.log('\t* Propose external motion through council.......');
-    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});
-    const encodedMotion = externalMotion?.method.toHex() || '';
-    const motionHash = blake2AsHex(encodedMotion);
-    console.log('\t* Motion hash is %s', motionHash);
-
-    await this.helper.collective.council.propose(
-      baltatharAccount,
-      councilVotingThreshold,
-      externalMotion,
-      externalMotion.encodedLength,
-    );
-
-    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;
-    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);
-    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);
-
-    await this.helper.collective.council.close(
-      dorothyAccount,
-      motionHash,
-      councilProposalIdx,
-      {
-        refTime: 1_000_000_000,
-        proofSize: 1_000_000,
-      },
-      externalMotion.encodedLength,
-    );
-    console.log('\t* Propose external motion through council.......DONE');
-    // <<< Propose external motion through council <<<
-
-    // >>> Fast track proposal through technical committee >>>
-    console.log('\t* Fast track proposal through technical committee.......');
-    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);
-    const encodedFastTrack = fastTrack?.method.toHex() || '';
-    const fastTrackHash = blake2AsHex(encodedFastTrack);
-    console.log('\t* FastTrack hash is %s', fastTrackHash);
-
-    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);
-
-    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;
-    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);
-    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);
-
-    await this.helper.collective.techCommittee.close(
-      baltatharAccount,
-      fastTrackHash,
-      techProposalIdx,
-      {
-        refTime: 1_000_000_000,
-        proofSize: 1_000_000,
-      },
-      fastTrack.encodedLength,
-    );
-    console.log('\t* Fast track proposal through technical committee.......DONE');
-    // <<< Fast track proposal through technical committee <<<
-
-    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);
-    const referendumIndex = democracyStarted.referendumIndex;
-
-    // >>> Referendum voting >>>
-    console.log(`\t* Referendum #${referendumIndex} voting.......`);
-    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {
-      balance: 10_000_000_000_000_000_000n,
-      vote: {aye: true, conviction: 1},
-    });
-    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);
-    // <<< Referendum voting <<<
-
-    // Wait the proposal to pass
-    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);
-
-    await this.helper.wait.newBlocks(1);
-
-    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);
-  }
-}
-
-class HydraFastDemocracyGroup {
-  helper: DevHydraDxHelper;
-
-  constructor(helper: DevHydraDxHelper) {
-    this.helper = helper;
-  }
-
-  async executeProposal(proposalDesciption: string, encodedProposal: string) {
-    const proposalHash = blake2AsHex(encodedProposal);
-    const aliceAccount = this.helper.util.fromSeed('//Alice');
-    const bobAccount = this.helper.util.fromSeed('//Bob');
-    const eveAccount = this.helper.util.fromSeed('//Eve');
-
-    const councilVotingThreshold = 1;
-    const technicalCommitteeThreshold = 3;
-    const fastTrackVotingPeriod = 3;
-    const fastTrackDelayPeriod = 0;
-
-    console.log(`[democracy] executing '${proposalDesciption}' proposal`);
-
-    // >>> Propose external motion through council >>>
-    console.log('\t* Propose external motion through council.......');
-    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});
-    const encodedMotion = externalMotion?.method.toHex() || '';
-    const motionHash = blake2AsHex(encodedMotion);
-    console.log('\t* Motion hash is %s', motionHash);
-
-    await this.helper.collective.council.propose(
-      aliceAccount,
-      councilVotingThreshold,
-      externalMotion,
-      externalMotion.encodedLength,
-    );
-
-    console.log('\t* Propose external motion through council.......DONE');
-    // <<< Propose external motion through council <<<
-
-    // >>> Fast track proposal through technical committee >>>
-    console.log('\t* Fast track proposal through technical committee.......');
-    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);
-    const encodedFastTrack = fastTrack?.method.toHex() || '';
-    const fastTrackHash = blake2AsHex(encodedFastTrack);
-    console.log('\t* FastTrack hash is %s', fastTrackHash);
-
-    await this.helper.collective.techCommittee.propose(aliceAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);
-
-    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;
-    await this.helper.collective.techCommittee.vote(aliceAccount, fastTrackHash, techProposalIdx, true);
-    await this.helper.collective.techCommittee.vote(bobAccount, fastTrackHash, techProposalIdx, true);
-    await this.helper.collective.techCommittee.vote(eveAccount, fastTrackHash, techProposalIdx, true);
-
-    await this.helper.collective.techCommittee.close(
-      bobAccount,
-      fastTrackHash,
-      techProposalIdx,
-      {
-        refTime: 1_000_000_000,
-        proofSize: 1_000_000,
-      },
-      fastTrack.encodedLength,
-    );
-    console.log('\t* Fast track proposal through technical committee.......DONE');
-    // <<< Fast track proposal through technical committee <<<
-
-    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);
-    const referendumIndex = democracyStarted.referendumIndex;
-
-    // >>> Referendum voting >>>
-    console.log(`\t* Referendum #${referendumIndex} voting.......`);
-    await this.helper.democracy.referendumVote(eveAccount, referendumIndex, {
-      balance: 10_000_000_000_000_000_000n,
-      vote: {aye: true, conviction: 1},
-    });
-    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);
-    // <<< Referendum voting <<<
-
-    // Wait the proposal to pass
-    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);
-
-    await this.helper.wait.newBlocks(1);
-
-    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);
-  }
-}
-
-class WaitGroup {
-  helper: ChainHelperBase;
-
-  constructor(helper: ChainHelperBase) {
-    this.helper = helper;
-  }
-
-  sleep(milliseconds: number) {
-    return new Promise((resolve) => setTimeout(resolve, milliseconds));
-  }
-
-  private async waitWithTimeout(promise: Promise<any>, timeout: number) {
-    let isBlock = false;
-    promise.then(() => isBlock = true).catch(() => isBlock = true);
-    let totalTime = 0;
-    const step = 100;
-    while(!isBlock) {
-      await this.sleep(step);
-      totalTime += step;
-      if(totalTime >= timeout) throw Error('Blocks production failed');
-    }
-    return promise;
-  }
-
-  /**
-   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.
-   * @param promise async operation to race against the timeout
-   * @param timeoutMS time after which to time out
-   * @param timeoutError error message to throw
-   * @returns promise of the same type the operation had
-   */
-  withTimeout<T>(
-    promise: Promise<T>,
-    timeoutMS = 30000,
-    timeoutError = 'The operation has timed out!',
-  ): Promise<T> {
-    const timeout = new Promise<never>((_, reject) => {
-      setTimeout(() => {
-        reject(new Error(timeoutError));
-      }, timeoutMS);
-    });
-
-    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});
-  }
-
-  /**
-   * Wait for specified number of blocks
-   * @param blocksCount number of blocks to wait
-   * @returns
-   */
-  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {
-    timeout = timeout ?? blocksCount * 60_000;
-    // eslint-disable-next-line no-async-promise-executor
-    const promise = new Promise<void>(async (resolve) => {
-      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {
-        if(blocksCount > 0) {
-          blocksCount--;
-        } else {
-          unsubscribe();
-          resolve();
-        }
-      });
-    });
-    await this.waitWithTimeout(promise, timeout);
-    return promise;
-  }
-
-  /**
-   * Wait for the specified number of sessions to pass.
-   * Only applicable if the Session pallet is turned on.
-   * @param sessionCount number of sessions to wait
-   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks
-   * @returns
-   */
-  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {
-    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`
-      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');
-
-    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;
-    let currentSessionIndex = -1;
-
-    while(currentSessionIndex < expectedSessionIndex) {
-      // eslint-disable-next-line no-async-promise-executor
-      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {
-        await this.newBlocks(1);
-        const res = await (this.helper as DevUniqueHelper).session.getIndex();
-        resolve(res);
-      }), blockTimeout, 'The chain has stopped producing blocks!');
-    }
-  }
-
-  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {
-    timeout = timeout ?? 30 * 60 * 1000;
-    // eslint-disable-next-line no-async-promise-executor
-    const promise = new Promise<void>(async (resolve) => {
-      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {
-        if(data.number.toNumber() >= blockNumber) {
-          unsubscribe();
-          resolve();
-        }
-      });
-    });
-    await this.waitWithTimeout(promise, timeout);
-    return promise;
-  }
-
-  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {
-    timeout = timeout ?? 30 * 60 * 1000;
-    // eslint-disable-next-line no-async-promise-executor
-    const promise = new Promise<void>(async (resolve) => {
-      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {
-        if(data.value.relayParentNumber.toNumber() >= blockNumber) {
-          // @ts-ignore
-          unsubscribe();
-          resolve();
-        }
-      });
-    });
-    await this.waitWithTimeout(promise, timeout);
-    return promise;
-  }
-
-  noScheduledTasks() {
-    const api = this.helper.getApi();
-
-    // eslint-disable-next-line no-async-promise-executor
-    const promise = new Promise<void>(async resolve => {
-      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {
-        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();
-
-        if(areThereScheduledTasks.length == 0) {
-          unsubscribe();
-          resolve();
-        }
-      });
-    });
-
-    return promise;
-  }
-
-  parachainBlockMultiplesOf(val: bigint) {
-    // eslint-disable-next-line no-async-promise-executor
-    const promise = new Promise<void>(async resolve => {
-      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {
-        if(data.number.toBigInt() % val == 0n) {
-          console.log(`from waiter: ${data.number.toBigInt()}`);
-          unsubscribe();
-          resolve();
-        }
-      });
-    });
-    return promise;
-  }
-
-  event<T extends IEventHelper>(
-    maxBlocksToWait: number,
-    eventHelper: T,
-    filter: (_: any) => boolean = () => true,
-  ): any {
-    // eslint-disable-next-line no-async-promise-executor
-    const promise = new Promise<T | null>(async (resolve) => {
-      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {
-        const blockNumber = header.number.toJSON();
-        const blockHash = header.hash;
-        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;
-        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;
-
-        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);
-
-        const apiAt = await this.helper.getApi().at(blockHash);
-        const eventRecords = (await apiAt.query.system.events()) as any;
-
-        const neededEvent = eventRecords.toArray()
-          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())
-          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))
-          .find(filter);
-
-        if(neededEvent) {
-          unsubscribe();
-          resolve(neededEvent);
-        } else if(maxBlocksToWait > 0) {
-          maxBlocksToWait--;
-        } else {
-          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.
-          The wait lasted until block ${blockNumber} inclusive`);
-          unsubscribe();
-          resolve(null);
-        }
-      });
-    });
-    return promise;
-  }
-
-  async expectEvent<T extends IEventHelper>(
-    maxBlocksToWait: number,
-    eventHelper: T,
-    filter: (e: any) => boolean = () => true,
-  ) {
-    const e = await this.event(maxBlocksToWait, eventHelper, filter);
-    if(e == null) {
-      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);
-    } else {
-      return e;
-    }
-  }
-}
-
-class SessionGroup {
-  helper: ChainHelperBase;
-
-  constructor(helper: ChainHelperBase) {
-    this.helper = helper;
-  }
-
-  //todo:collator documentation
-  async getIndex(): Promise<number> {
-    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();
-  }
-
-  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {
-    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);
-  }
-
-  setOwnKeys(signer: TSigner, key: string) {
-    return this.helper.executeExtrinsic(
-      signer,
-      'api.tx.session.setKeys',
-      [key, '0x0'],
-      true,
-    );
-  }
-
-  setOwnKeysFromAddress(signer: TSigner) {
-    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));
-  }
-}
-
-class TestUtilGroup {
-  helper: DevUniqueHelper;
-
-  constructor(helper: DevUniqueHelper) {
-    this.helper = helper;
-  }
-
-  async enable(testUtilsPalletName: string) {
-    if(this.helper.fetchMissingPalletNames([testUtilsPalletName]).length != 0) {
-      return;
-    }
-
-    const signer = this.helper.util.fromSeed('//Alice');
-    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);
-  }
-
-  async setTestValue(signer: TSigner, testVal: number) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);
-  }
-
-  async incTestValue(signer: TSigner) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);
-  }
-
-  async setTestValueAndRollback(signer: TSigner, testVal: number) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);
-  }
-
-  async testValue(blockIdx?: number) {
-    const api = blockIdx
-      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))
-      : this.helper.getApi();
-
-    return (await api.query.testUtils.testValue()).toJSON();
-  }
-
-  async justTakeFee(signer: TSigner) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);
-  }
-
-  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);
-  }
-}
-
-class EventCapture {
-  helper: DevUniqueHelper;
-  eventSection: string;
-  eventMethod: string;
-  events: EventRecord[] = [];
-  unsubscribe: VoidFn | null = null;
-
-  constructor(
-    helper: DevUniqueHelper,
-    eventSection: string,
-    eventMethod: string,
-  ) {
-    this.helper = helper;
-    this.eventSection = eventSection;
-    this.eventMethod = eventMethod;
-  }
-
-  async startCapture() {
-    this.stopCapture();
-    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {
-      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);
-
-      this.events.push(...newEvents);
-    })) as any;
-  }
-
-  stopCapture() {
-    if(this.unsubscribe !== null) {
-      this.unsubscribe();
-    }
-  }
-
-  extractCapturedEvents() {
-    return this.events;
-  }
-}
-
-class AdminGroup {
-  helper: UniqueHelper;
-
-  constructor(helper: UniqueHelper) {
-    this.helper = helper;
-  }
-
-  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {
-    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);
-    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({
-      staker: e.event.data[0].toString(),
-      stake: e.event.data[1].toBigInt(),
-      payout: e.event.data[2].toBigInt(),
-    }));
-  }
-}
-
-// 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;
-    }
-
-    override 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,
-      );
-    }
-  };
-}
deletedjs-packages/playgrounds/unique.governance.tsdiffbeforeafterboth
--- a/js-packages/playgrounds/unique.governance.ts
+++ /dev/null
@@ -1,531 +0,0 @@
-import {blake2AsHex} from '@polkadot/util-crypto';
-import type {PalletDemocracyConviction} from '@polkadot/types/lookup';
-import type {IPhasicEvent, TSigner} from './types.js';
-import {HelperGroup, UniqueHelper} from './unique.js';
-
-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);
-  }*/
-}
deletedjs-packages/playgrounds/unique.xcm.tsdiffbeforeafterboth
--- a/js-packages/playgrounds/unique.xcm.ts
+++ /dev/null
@@ -1,449 +0,0 @@
-import {ApiPromise, WsProvider} from '@polkadot/api';
-import type {IKeyringPair} from '@polkadot/types/types';
-import {ChainHelperBase, EthereumBalanceGroup, HelperGroup, SubstrateBalanceGroup, UniqueHelper} from './unique.js';
-import type {ILogger, TSigner, TSubstrateAccount} from './types.js';
-import type {AcalaAssetMetadata, DemocracyStandardAccountVote, MoonbeamAssetInfo} from './types.xcm.js';
-
-
-export class XcmChainHelper extends ChainHelperBase {
-  override 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 DemocracyGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  notePreimagePallet: string;
-
-  constructor(helper: T, 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 CollectiveGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  collective: string;
-
-  constructor(helper: T, palletName: string) {
-    super(helper);
-
-    this.collective = palletName;
-  }
-
-  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, assetId: any, name: string, tokenPrefix: string, mode: 'NFT' | { Fungible: number }) {
-    await this.helper.executeExtrinsic(
-      signer,
-      'api.tx.foreignAssets.forceRegisterForeignAsset',
-      [{V3: assetId}, this.helper.util.str2vec(name), tokenPrefix, mode],
-      true,
-    );
-  }
-
-  async foreignCollectionId(assetId: any) {
-    return (await this.helper.callRpc('api.query.foreignAssets.foreignAssetToCollection', [assetId])).toJSON();
-  }
-}
-
-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 | bigint, admin: string, minimalBalance: bigint) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
-  }
-
-  async forceCreate(signer: TSigner, assetId: number | bigint, admin: string, minimalBalance: bigint, isSufficient = true) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.assets.forceCreate', [assetId, admin, isSufficient, minimalBalance], true);
-  }
-
-  async setMetadata(signer: TSigner, assetId: number | bigint, 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 | bigint, beneficiary: string, amount: bigint) {
-    await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
-  }
-
-  async assetInfo(assetId: number | bigint) {
-    return (await this.helper.callRpc('api.query.assets.asset', [assetId])).toJSON();
-  }
-
-  async account(assetId: string | number | bigint, 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);
-  }
-}
-
-export class HydraDxHelper extends XcmChainHelper {
-  balance: SubstrateBalanceGroup<HydraDxHelper>;
-  xcm: XcmGroup<HydraDxHelper>;
-  xTokens: XTokensGroup<HydraDxHelper>;
-  democracy: DemocracyGroup<HydraDxHelper>;
-  collective: {
-    council: CollectiveGroup<HydraDxHelper>,
-    techCommittee: CollectiveGroup<HydraDxHelper>,
-  };
-
-  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
-    super(logger, options.helperBase ?? HydraDxHelper);
-    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';
-
-    this.balance = new SubstrateBalanceGroup(this);
-    this.xcm = new XcmGroup(this, 'polkadotXcm');
-    this.xTokens = new XTokensGroup(this);
-    this.democracy = new DemocracyGroup(this, options);
-    this.collective = {
-      council: new CollectiveGroup(this, 'council'),
-      techCommittee: new CollectiveGroup(this, 'technicalCommittee'),
-    };
-  }
-}
-
addedjs-packages/scripts/authorizeEnactUpgrade.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/scripts/authorizeEnactUpgrade.ts
@@ -0,0 +1,19 @@
+import {readFile} from 'fs/promises';
+import {u8aToHex} from '@polkadot/util';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
+import {blake2AsHex} from '@polkadot/util-crypto';
+
+
+const codePath = process.argv[2];
+if(!codePath) throw new Error('missing code path argument');
+
+const code = await readFile(codePath);
+
+await usingPlaygrounds(async (helper, privateKey) => {
+  const alice = await privateKey('//Alice');
+  const hex = blake2AsHex(code);
+  await helper.getSudo().executeExtrinsic(alice, 'api.tx.parachainSystem.authorizeUpgrade', [hex, true]);
+  await helper.getSudo().executeExtrinsicUncheckedWeight(alice, 'api.tx.parachainSystem.enactAuthorizedUpgrade', [u8aToHex(code)]);
+});
+// We miss disconnect/unref somewhere.
+process.exit(0);
modifiedjs-packages/scripts/benchmarks/mintFee/index.tsdiffbeforeafterboth
--- a/js-packages/scripts/benchmarks/mintFee/index.ts
+++ b/js-packages/scripts/benchmarks/mintFee/index.ts
@@ -1,13 +1,13 @@
 import {usingEthPlaygrounds} from '@unique/tests/eth/util/index.js';
 import {EthUniqueHelper} from '@unique/tests/eth/util/playgrounds/unique.dev.js';
 import {readFile} from 'fs/promises';
-import type {ICrossAccountId} from '@unique/playgrounds/types.js';
+import type {ICrossAccountId} from '@unique-nft/playgrounds/types.js';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {UniqueNFTCollection} from '@unique/playgrounds/unique.js';
+import {UniqueNFTCollection} from '@unique-nft/playgrounds/unique.js';
 import {Contract} from 'web3-eth-contract';
 import {createObjectCsvWriter} from 'csv-writer';
 import {convertToTokens, createCollectionForBenchmarks, PERMISSIONS, PROPERTIES} from '../utils/common.js';
-import {makeNames} from '@unique/tests/util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 import type {ContractImports} from '@unique/tests/eth/util/playgrounds/types.js';
 
 const {dirname} = makeNames(import.meta.url);
modifiedjs-packages/scripts/benchmarks/nesting/index.tsdiffbeforeafterboth
--- a/js-packages/scripts/benchmarks/nesting/index.ts
+++ b/js-packages/scripts/benchmarks/nesting/index.ts
@@ -4,7 +4,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {Contract} from 'web3-eth-contract';
 import {convertToTokens} from '../utils/common.js';
-import {makeNames} from '@unique/tests/util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 import type {ContractImports} from '@unique/tests/eth/util/playgrounds/types.js';
 import type {RMRKNestableMintable} from './ABIGEN/index.js';
 
modifiedjs-packages/scripts/benchmarks/opsFee/index.tsdiffbeforeafterboth
--- a/js-packages/scripts/benchmarks/opsFee/index.ts
+++ b/js-packages/scripts/benchmarks/opsFee/index.ts
@@ -3,13 +3,13 @@
 import {readFile} from 'fs/promises';
 import {CollectionLimitField,  CreateCollectionData,  TokenPermissionField} from '@unique/tests/eth/util/playgrounds/types.js';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {UniqueFTCollection, UniqueNFTCollection} from '@unique/playgrounds/unique.js';
+import {UniqueFTCollection, UniqueNFTCollection} from '@unique-nft/playgrounds/unique.js';
 import {Contract} from 'web3-eth-contract';
 import {createObjectCsvWriter} from 'csv-writer';
 import {FunctionFeeVM} from '../utils/types.js';
 import type {IFunctionFee} from '../utils/types.js';
 import {convertToTokens, createCollectionForBenchmarks, PERMISSIONS, PROPERTIES, SUBS_PROPERTIES} from '../utils/common.js';
-import {makeNames} from '@unique/tests/util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 
 
 const {dirname} = makeNames(import.meta.url);
modifiedjs-packages/scripts/benchmarks/utils/common.tsdiffbeforeafterboth
--- a/js-packages/scripts/benchmarks/utils/common.ts
+++ b/js-packages/scripts/benchmarks/utils/common.ts
@@ -1,6 +1,6 @@
 import {EthUniqueHelper} from '@unique/tests/eth/util/playgrounds/unique.dev.js';
-import {UniqueNFTCollection, UniqueRFTCollection} from '@unique/playgrounds/unique.js';
-import type {ITokenPropertyPermission, TCollectionMode} from '@unique/playgrounds/types.js';
+import {UniqueNFTCollection, UniqueRFTCollection} from '@unique-nft/playgrounds/unique.js';
+import type {ITokenPropertyPermission, TCollectionMode} from '@unique-nft/playgrounds/types.js';
 import type {IKeyringPair} from '@polkadot/types/types';
 
 export const PROPERTIES = Array(40)
modifiedjs-packages/scripts/calibrateApply.tsdiffbeforeafterboth
--- a/js-packages/scripts/calibrateApply.ts
+++ b/js-packages/scripts/calibrateApply.ts
@@ -1,6 +1,6 @@
 import {readFile, writeFile} from 'fs/promises';
 import path from 'path';
-import {makeNames, usingPlaygrounds} from '@unique/tests/util/index.js';
+import {makeNames, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
addedjs-packages/scripts/createHrmp.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/scripts/createHrmp.ts
@@ -0,0 +1,37 @@
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
+import config from '../tests/config.js';
+
+const profile = process.argv[2];
+if(!profile) throw new Error('missing profile/relay argument');
+
+await usingPlaygrounds(async (helper, privateKey) => {
+  const bidirOpen = async (a: number, b: number) => {
+    console.log(`Opening ${a} <=> ${b}`);
+    await helper.getSudo().executeExtrinsic(alice, 'api.tx.hrmp.forceOpenHrmpChannel', [a, b, 8, 512]);
+    await helper.getSudo().executeExtrinsic(alice, 'api.tx.hrmp.forceOpenHrmpChannel', [b, a, 8, 512]);
+  };
+  const alice = await privateKey('//Alice');
+  switch (profile) {
+    case 'opal':
+      await bidirOpen(1001, 1002);
+      break;
+    case 'quartz':
+      await bidirOpen(1001, 1002);
+      await bidirOpen(1001, 1003);
+      await bidirOpen(1001, 1004);
+      await bidirOpen(1001, 1005);
+      break;
+    case 'unique':
+      await bidirOpen(1001, 1002);
+      await bidirOpen(1001, 1003);
+      await bidirOpen(1001, 1004);
+      await bidirOpen(1001, 1005);
+      await bidirOpen(1001, 1006);
+      await bidirOpen(1001, 1007);
+      break;
+    default:
+      throw new Error(`unknown hrmp config profile: ${profile}`);
+  }
+}, config.relayUrl);
+// We miss disconnect/unref somewhere.
+process.exit(0);
modifiedjs-packages/scripts/generateEnv.tsdiffbeforeafterboth
--- a/js-packages/scripts/generateEnv.ts
+++ b/js-packages/scripts/generateEnv.ts
@@ -1,7 +1,7 @@
 import {ApiPromise, WsProvider} from '@polkadot/api';
 import {readFile} from 'fs/promises';
 import {join} from 'path';
-import {makeNames} from '@unique/tests/util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
addedjs-packages/scripts/identitySetter.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/scripts/identitySetter.ts
@@ -0,0 +1,229 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Pulls identities and sub-identities from a chain and then makes a preimage to later force upload them into another.
+// Only changed or previously non-existent data are inserted.
+//
+// Usage: `yarn setIdentities [relay WS URL] [parachain WS URL] [user key]
+// Example: `yarn setIdentities wss://polkadot-rpc.dwellir.com ws://localhost:9944 escape pattern miracle train sudden cart adapt embark wedding alien lamp mesh`
+
+import {encodeAddress} from '@polkadot/keyring';
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, Pallets} from '@unique/test-utils/util.js';
+import {ChainHelperBase} from '@unique-nft/playgrounds/unique.js';
+
+const relayUrl = process.argv[2] ?? 'ws://localhost:9844';
+const paraUrl = process.argv[3] ?? 'ws://localhost:9944';
+const key = process.argv.length > 4 ? process.argv.slice(4).join(' ') : '//Alice';
+
+export function extractAccountId(key: any): string {
+  return (key as any).toHuman()[0];
+}
+
+export function extractIdentityInfo(value: any): object {
+  const heart = (value as any).unwrap();
+  const identity = heart.toJSON();
+  identity.judgements = heart.judgements.toHuman();
+  return identity;
+}
+
+export function extractIdentity(key: any, value: any): [string, any] {
+  return [extractAccountId(key), extractIdentityInfo(value)];
+}
+
+export async function getIdentities(helper: ChainHelperBase, noneCasePredicate?: (key: any, value: any) => void) {
+  const identities: [string, any][] = [];
+  for(const [key, v] of await helper.getApi().query.identity.identityOf.entries()) {
+    const value = v as any;
+    if(value.isNone) {
+      if(noneCasePredicate) noneCasePredicate(key, value);
+      continue;
+    }
+    identities.push(extractIdentity(key, value));
+  }
+  return identities;
+}
+
+// whether the existing chain data is more important than the coming
+function isCurrentChainDataPriority(helper: ChainHelperBase, currentChainId: number | undefined, relayChainId: number) {
+  if(!currentChainId) return false;
+  // information has come from local chain, and is automatically superior
+  if(currentChainId == helper.chain.getChainProperties().ss58Format) return true;
+  // the lower the id, the more important it is (Polkadot has ss58 prefix = 0, Kusama has ss58 prefix = 2)
+  return currentChainId < relayChainId;
+}
+
+// construct an object with all data necessary for insertion from storage query results
+export function constructSubInfo(identityAccount: string, subQuery: any, supers: any[], ss58?: number): [string, any] {
+  const deposit = subQuery.toJSON()[0];
+  const subIdentities = subQuery.toHuman()[1];
+  subIdentities.map((sub: string) => supers.find((sup: any) => sup[0] === sub));
+
+  return [
+    encodeAddress(identityAccount, ss58), [
+      deposit,
+      subIdentities.map((sub: string): [string, object] | null => {
+        const sup = supers.find((sup: any) => sup[0] === sub);
+        if(!sup) {
+          console.error(`Error: Could not find info on super for \nsub-identity account\t${sub} of \nsuper account \t\t${identityAccount}, skipping.`);
+          return null;
+        }
+        return [encodeAddress(sub, ss58), sup[1].toJSON()[1]];
+      }).filter((x: any) => x),
+    ],
+  ];
+}
+
+export async function getSubs(helper: ChainHelperBase) {
+  return (await helper.getApi().query.identity.subsOf.entries()).map(([key, value]) => [extractAccountId(key), value as any]);
+}
+
+export async function getSupers(helper: ChainHelperBase) {
+  return (await helper.getApi().query.identity.superOf.entries()).map(([key, value]) => [extractAccountId(key), value as any]);
+}
+
+async function uploadPreimage(helper: ChainHelperBase, preimageMaker: IKeyringPair, preimage: string) {
+  try {
+    await helper.executeExtrinsic(preimageMaker, 'api.tx.preimage.notePreimage', [preimage]);
+  } catch (e: any) {
+    if(e.message.includes('AlreadyNoted')) {
+      console.warn('Warning: The same preimage already exists on the chain. Nothing was uploaded.');
+    } else {
+      console.error(e);
+    }
+  }
+}
+
+// The utility for pulling identity and sub-identity data
+const forceInsertIdentities = async (): Promise<void> => {
+  let relaySS58Prefix = 0;
+  const identitiesOnRelay: any[] = [];
+  const subsOnRelay: any[] = [];
+  const identitiesToRemove: string[] = [];
+  await usingPlaygrounds(async helper => {
+    try {
+      relaySS58Prefix = helper.chain.getChainProperties().ss58Format;
+      // iterate over every identity
+      for(const [key, value] of await getIdentities(helper, (key, _value) => identitiesToRemove.push((key as any).toHuman()[0]))) {
+        // if any of the judgements resulted in a good confirmed outcome, keep this identity
+        let knownGood = false, reasonable = false;
+        for(const [_id, judgement] of value.judgements) {
+          if(judgement == 'KnownGood') knownGood = true;
+          if(judgement == 'Reasonable') reasonable = true;
+        }
+        if(!(reasonable || knownGood)) continue;
+        // replace the registrator id with the relay chain's ss58 format
+        value.judgements = [[helper.chain.getChainProperties().ss58Format, knownGood ? 'KnownGood' : 'Reasonable']];
+        identitiesOnRelay.push([key, value]);
+      }
+
+      const sublessIdentities = [...identitiesOnRelay];
+      const supersOfSubs = await getSupers(helper);
+
+      // iterate over every sub-identity
+      for(const [key, value] of await getSubs(helper)) {
+        // only get subs of the identities interesting to us
+        const identityIndex = sublessIdentities.findIndex((x: any) => x[0] == key);
+        if(identityIndex == -1) continue;
+        sublessIdentities.splice(identityIndex, 1);
+        subsOnRelay.push(constructSubInfo(key, value, supersOfSubs));
+      }
+
+      // mark the rest of sub-identities for deletion with empty arrays
+      /*for(const [account, _identity] of sublessIdentities) {
+        subsOnRelay.push([account, ['0', []]]);
+      }*/
+    } catch (error) {
+      console.error(error);
+      throw Error('Error during fetching identities');
+    }
+  }, relayUrl);
+
+  await usingPlaygrounds(async (helper, privateKey) => {
+    if(helper.fetchMissingPalletNames([Pallets.Identity]).length != 0) console.error('pallet-identity is not included in parachain.');
+    if(helper.fetchMissingPalletNames([Pallets.Preimage]).length != 0) console.error('pallet-preimage is not included in parachain.');
+
+    try {
+      const preimageMaker = await privateKey(key);
+      const ss58Format = helper.chain.getChainProperties().ss58Format;
+      const paraIdentities = await getIdentities(helper);
+      const identitiesToAdd: any[] = [];
+      const paraAccountsRegistrators: {[name: string]: number} = {};
+
+      // cross-reference every account for changes
+      for(const [key, value] of identitiesOnRelay) {
+        const encodedKey = encodeAddress(key, ss58Format);
+
+        // only update if the identity info does not exist or is changed
+        const identity = paraIdentities.find(i => i[0] === encodedKey);
+        if(identity) {
+          const registratorId = identity[1].judgements[0][0];
+          paraAccountsRegistrators[encodedKey] = registratorId;
+          if(isCurrentChainDataPriority(helper, registratorId, value.judgements[0][0])
+            || JSON.stringify(value.info) === JSON.stringify(identity[1].info)) {
+            continue;
+          }
+        }
+
+        identitiesToAdd.push([key, value]);
+        // exercise caution - in case we have an identity and the realy doesn't, it might mean one of two things:
+        // 1) it was deleted on the relay;
+        // 2) it is our own identity, we don't want to delete it.
+        // identitiesToRemove.push((key as any).toHuman()[0]);
+      }
+
+      if(identitiesToRemove.length != 0)
+        await uploadPreimage(
+          helper,
+          preimageMaker,
+          helper.constructApiCall('api.tx.identity.forceRemoveIdentities', [identitiesToRemove]).method.toHex(),
+        );
+      if(identitiesToAdd.length != 0)
+        await uploadPreimage(
+          helper,
+          preimageMaker,
+          helper.constructApiCall('api.tx.identity.forceInsertIdentities', [identitiesToAdd]).method.toHex(),
+        );
+
+      console.log(`Tried to push ${identitiesToAdd.length} identities`
+        + ` and found ${identitiesToRemove.length} identities for potential removal.`
+        + ` Currently there are ${(await helper.getApi().query.identity.identityOf.keys()).length} identities on the chain.`);
+
+      // fill sub-identities
+      const paraSubs = await getSubs(helper);
+      const supersOfSubs = await getSupers(helper);
+      const subsToUpdate: any[] = [];
+
+      for(const [key, value] of subsOnRelay) {
+        const encodedKey = encodeAddress(key, ss58Format);
+        const sub = paraSubs.find(i => i[0] === encodedKey);
+        if(sub) {
+          // only update if the sub-identity info does not exist or is changed
+          if(isCurrentChainDataPriority(helper, paraAccountsRegistrators[encodedKey], relaySS58Prefix)
+            || JSON.stringify(value) === JSON.stringify(constructSubInfo(sub[0], sub[1], supersOfSubs)[1])) {
+            continue;
+          }
+        } else if(value[1].length == 0)
+          continue;
+
+        subsToUpdate.push([key, value]);
+      }
+
+      if(subsToUpdate.length != 0)
+        await uploadPreimage(
+          helper,
+          preimageMaker,
+          helper.constructApiCall('api.tx.identity.forceSetSubs', [subsToUpdate]).method.toHex(),
+        );
+
+      console.log(`Also tried to push ${subsToUpdate.length} identities with their sub-identities.`
+        + ` Currently there are ${(await helper.getApi().query.identity.subsOf.keys()).length} identities with subs.`);
+    } catch (error) {
+      console.error(error);
+      throw Error('Error during setting identities');
+    }
+  }, paraUrl);
+};
+
+if(process.argv[1] === module.filename)
+  forceInsertIdentities().catch(() => process.exit(1));
addedjs-packages/scripts/relayIdentitiesChecker.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/scripts/relayIdentitiesChecker.ts
@@ -0,0 +1,114 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+//
+// Checks and reports the differences between identities and sub-identities on two chains.
+//
+// Usage: `yarn checkRelayIdentities [relay-1 WS URL] [relay-2 WS URL]`
+// Example: `yarn checkRelayIdentities wss://polkadot-rpc.dwellir.com wss://kusama-rpc.dwellir.com`
+
+import {encodeAddress} from '@polkadot/keyring';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
+import {getIdentities, getSubs, getSupers, constructSubInfo} from './identitySetter.js';
+
+const relay1Url = process.argv[2] ?? 'ws://localhost:9844';
+const relay2Url = process.argv[3] ?? 'ws://localhost:9844';
+
+async function pullIdentities(relayUrl: string): Promise<[any[], any[]]> {
+  const identities: any[] = [];
+  const subs: any[] = [];
+
+  await usingPlaygrounds(async helper => {
+    try {
+      // iterate over every identity
+      for(const [key, value] of await getIdentities(helper)) {
+        // if any of the judgements resulted in a good confirmed outcome, keep this identity
+        if(value.toHuman().judgements.filter((x: any) => x[1] == 'Reasonable' || x[1] == 'KnownGood').length == 0) continue;
+        identities.push([key, value]);
+      }
+
+      const supersOfSubs = await getSupers(helper);
+
+      // iterate over every sub-identity
+      for(const [key, value] of await getSubs(helper)) {
+        // only get subs of the identities interesting to us
+        if(identities.find((x: any) => x[0] == key) == -1) continue;
+        subs.push(constructSubInfo(key, value, supersOfSubs));
+      }
+    } catch (error) {
+      console.error(error);
+      throw Error(`Error during fetching identities on ${relayUrl}`);
+    }
+  }, relayUrl);
+
+  return [identities, subs];
+}
+
+// The utility for pulling identity and sub-identity data
+const checkRelayIdentities = async (): Promise<void> => {
+  const [identitiesOnRelay1, subIdentitiesOnRelay1] = await pullIdentities(relay1Url);
+  const [identitiesOnRelay2, subIdentitiesOnRelay2] = await pullIdentities(relay2Url);
+
+  console.log('identities counts:\t', identitiesOnRelay1.length, identitiesOnRelay2.length);
+  console.log('sub-identities counts:\t', subIdentitiesOnRelay1.length, subIdentitiesOnRelay2.length);
+  console.log();
+
+  try {
+    const matchingAddresses: string[] = [];
+    const inequalIdentities: {[name: string]: [any, any]} = {};
+
+    for(const [key1, value1] of identitiesOnRelay1) {
+      const address = encodeAddress(key1);
+      const identity2 = identitiesOnRelay2.find(([key2, _value2]) => address === encodeAddress(key2));
+      if(!identity2) continue;
+      matchingAddresses.push(address);
+
+      //const [[key2, value2]] = identitiesOnRelay2.splice(index2, 1);
+      const [_key2, value2] = identity2;
+      if(JSON.stringify(value1.info) === JSON.stringify(value2.info)) continue;
+      inequalIdentities[address] = [value1, value2];
+    }
+
+    /*for (const [v1, v2] of Object.values(inequalIdentities)) {
+      console.log(v1.toHuman().info);
+      console.log();
+      console.log(v2.toHuman().info);
+      await new Promise(resolve => setTimeout(resolve, 4000));
+    }*/
+
+    console.log(`Accounts with identities on both relays:\t${matchingAddresses.length}`);
+    console.log(`Sub-identities with conflicting information:\t${Object.entries(inequalIdentities).length}`);
+    console.log();
+
+    const inequalSubIdentities = [];
+    let matchesFound = 0;
+    for(const address of matchingAddresses) {
+      const sub1 = subIdentitiesOnRelay1.find(([key1, _value1]) => address === encodeAddress(key1));
+      if(!sub1) continue;
+      const sub2 = subIdentitiesOnRelay2.find(([key2, _value2]) => address === encodeAddress(key2));
+      if(!sub2) continue;
+
+      const [value1, value2] = [sub1[1], sub2[1]];
+      matchesFound++;
+
+      if(JSON.stringify(value1[1]) === JSON.stringify(value2[1])) {
+        continue;
+      }
+      inequalSubIdentities.push([value1, value2]);
+    }
+
+    /*for (const [v1, v2] of inequalSubIdentities) {
+      console.log(v1[1]);
+      console.log();
+      console.log(v2[1]);
+      await new Promise(resolve => setTimeout(resolve, 300));
+    }*/
+    console.log(`Accounts with sub-identities on both relays:\t${matchesFound}`);
+    console.log(`Of them, those with conflicting sub-identities:\t${inequalSubIdentities.length}`);
+  } catch (error) {
+    console.error(error);
+    throw Error('Error during comparison');
+  }
+};
+
+if(process.argv[1] === module.filename)
+  checkRelayIdentities().catch(() => process.exit(1));
addedjs-packages/scripts/setCode.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/scripts/setCode.ts
@@ -0,0 +1,15 @@
+import {readFile} from 'fs/promises';
+import {u8aToHex} from '@polkadot/util';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
+
+const codePath = process.argv[2];
+if(!codePath) throw new Error('missing code path argument');
+
+const code = await readFile(codePath);
+
+await usingPlaygrounds(async (helper, privateKey) => {
+  const alice = await privateKey('//Alice');
+  await helper.getSudo().executeExtrinsicUncheckedWeight(alice, 'api.tx.system.setCode', [u8aToHex(code)]);
+});
+// We miss disconnect/unref somewhere.
+process.exit(0);
modifiedjs-packages/scripts/transfer.nload.tsdiffbeforeafterboth
--- a/js-packages/scripts/transfer.nload.ts
+++ b/js-packages/scripts/transfer.nload.ts
@@ -17,8 +17,8 @@
 /* eslint-disable @typescript-eslint/no-floating-promises */
 import os from 'os';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds} from '@unique/tests/util/index.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
+import {UniqueHelper} from '@unique-nft/playgrounds/unique.js';
 import * as notReallyCluster from 'cluster'; // https://github.com/nodejs/node/issues/42271#issuecomment-1063415346
 const cluster = notReallyCluster as unknown as notReallyCluster.Cluster;
 
addedjs-packages/test-utils/globalSetup.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/test-utils/globalSetup.ts
@@ -0,0 +1,119 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+  usingPlaygrounds, Pallets, DONOR_FUNDING, MINIMUM_DONOR_FUND, LOCKING_PERIOD, UNLOCKING_PERIOD, makeNames,
+} from './util.js';
+import * as path from 'path';
+import {promises as fs} from 'fs';
+
+const {dirname} = makeNames(import.meta.url);
+
+// This function should be called before running test suites.
+const globalSetup = async (): Promise<void> => {
+  await usingPlaygrounds(async (helper, privateKey) => {
+    try {
+      // 1. Wait node producing blocks
+      await helper.wait.newBlocks(1, 600_000);
+
+      // 2. Create donors for test files
+      await fundFilenamesWithRetries(3)
+        .then((result) => {
+          if(!result) throw Error('Some problems with fundFilenamesWithRetries');
+        });
+
+      // 3. Configure App Promotion
+      const missingPallets = helper.fetchMissingPalletNames([Pallets.AppPromotion]);
+      if(missingPallets.length === 0) {
+        const superuser = await privateKey('//Alice');
+        const palletAddress = helper.arrange.calculatePalletAddress('appstake');
+        const palletAdmin = await privateKey('//PromotionAdmin');
+        const api = helper.getApi();
+        await helper.signTransaction(superuser, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})));
+        const nominal = helper.balance.getOneTokenNominal();
+        await helper.balance.transferToSubstrate(superuser, palletAdmin.address, 10000n * nominal);
+        await helper.balance.transferToSubstrate(superuser, palletAddress, 10000n * nominal);
+        await helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [api.tx.configuration
+          .setAppPromotionConfigurationOverride({
+            recalculationInterval: LOCKING_PERIOD,
+            pendingInterval: UNLOCKING_PERIOD})], true);
+      }
+    } catch (error) {
+      console.error(error);
+      throw Error('Error during globalSetup');
+    }
+  });
+};
+
+async function getFiles(rootPath: string): Promise<string[]> {
+  const files = await fs.readdir(rootPath, {withFileTypes: true});
+  const filenames: string[] = [];
+  for(const entry of files) {
+    const res = path.resolve(rootPath, entry.name);
+    if(entry.isDirectory()) {
+      filenames.push(...await getFiles(res));
+    } else {
+      filenames.push(res);
+    }
+  }
+  return filenames;
+}
+
+const fundFilenames = async () => {
+  await usingPlaygrounds(async (helper, privateKey) => {
+    const oneToken = helper.balance.getOneTokenNominal();
+    const alice = await privateKey('//Alice');
+    const nonce = await helper.chain.getNonce(alice.address);
+    const filenames = await getFiles(path.resolve(dirname, '..'));
+
+    // batching is actually undesireable, it takes away the time while all the transactions actually succeed
+    const batchSize = 300;
+    let balanceGrantedCounter = 0;
+    for(let b = 0; b < filenames.length; b += batchSize) {
+      const tx: Promise<boolean>[] = [];
+      let batchBalanceGrantedCounter = 0;
+      for(let i = 0; batchBalanceGrantedCounter < batchSize && b + i < filenames.length; i++) {
+        const f = filenames[b + i];
+        if(!f.endsWith('.test.ts') && !f.endsWith('seqtest.ts') || f.includes('.outdated')) continue;
+        const account = await privateKey({filename: f, ignoreFundsPresence: true});
+        const aliceBalance = await helper.balance.getSubstrate(account.address);
+
+        if(aliceBalance < MINIMUM_DONOR_FUND * oneToken) {
+          tx.push(helper.executeExtrinsic(
+            alice,
+            'api.tx.balances.transferKeepAlive',
+            [account.address, DONOR_FUNDING * oneToken],
+            true,
+            {nonce: nonce + balanceGrantedCounter++},
+          ).then(() => true).catch(() => {console.error(`Transaction to ${path.basename(f)} registered as failed. Strange.`); return false;}));
+          batchBalanceGrantedCounter++;
+        }
+      }
+
+      if(tx.length > 0) {
+        console.log(`Granting funds to ${batchBalanceGrantedCounter} filename accounts.`);
+        const result = await Promise.all(tx);
+        if(result && result.lastIndexOf(false) > -1) throw new Error('The transactions actually probably succeeded, should check the balances.');
+      }
+    }
+
+    if(balanceGrantedCounter == 0) console.log('No account needs additional funding.');
+  });
+};
+
+const fundFilenamesWithRetries = (retriesLeft: number): Promise<boolean> => {
+  if(retriesLeft <= 0) return Promise.resolve(false);
+  return fundFilenames()
+    .then(() => Promise.resolve(true))
+    .catch(e => {
+      console.error(e);
+      console.error(`Some transactions might have failed. ${retriesLeft > 1 ? 'Retrying...' : 'Something is wrong.'}\n`);
+      return fundFilenamesWithRetries(--retriesLeft);
+    });
+};
+
+globalSetup().catch(e => {
+  console.error('Setup error');
+  console.error(e);
+  process.exit(1);
+});
addedjs-packages/test-utils/governance.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/test-utils/governance.ts
@@ -0,0 +1,531 @@
+import {blake2AsHex} from '@polkadot/util-crypto';
+import type {PalletDemocracyConviction} from '@polkadot/types/lookup';
+import type {IPhasicEvent, TSigner} from '@unique-nft/playgrounds/types.js';
+import {HelperGroup, UniqueHelper} from '@unique-nft/playgrounds/unique.js';
+
+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);
+  }*/
+}
addedjs-packages/test-utils/index.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/test-utils/index.ts
@@ -0,0 +1,1646 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+import '@unique-nft/opal-testnet-types/augment-api.js';
+import '@unique-nft/opal-testnet-types/augment-types.js';
+import '@unique-nft/opal-testnet-types/types-lookup.js';
+
+import {stringToU8a} from '@polkadot/util';
+import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';
+import type {ChainHelperBaseConstructor, UniqueHelperConstructor} from '@unique-nft/playgrounds/unique.js';
+import {UniqueHelper, ChainHelperBase, HelperGroup} from '@unique-nft/playgrounds/unique.js';
+import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';
+import * as defs from '@unique-nft/opal-testnet-types/definitions.js';
+import type {IKeyringPair} from '@polkadot/types/types';
+import type {EventRecord} from '@polkadot/types/interfaces';
+import type {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from '@unique-nft/playgrounds/types.js';
+import type {FrameSystemEventRecord, StagingXcmV2TraitsError, StagingXcmV3TraitsOutcome} from '@polkadot/types/lookup';
+import type {SignerOptions, VoidFn} from '@polkadot/api/types';
+import {spawnSync} from 'child_process';
+import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup, HydraDxHelper} from './xcm/index.js';
+import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, RankedCollectiveGroup, ReferendaGroup} from './governance.js';
+import type {ICollectiveGroup, IFellowshipGroup} from './governance.js';
+
+export class SilentLogger {
+  log(_msg: any, _level: any): void { }
+  level = {
+    ERROR: 'ERROR' as const,
+    WARNING: 'WARNING' as const,
+    INFO: 'INFO' as const,
+  };
+}
+
+export class SilentConsole {
+  // TODO: Remove, this is temporary: Filter unneeded API output
+  // (Jaco promised it will be removed in the next version)
+  consoleErr: any;
+  consoleLog: any;
+  consoleWarn: any;
+
+  constructor() {
+    this.consoleErr = console.error;
+    this.consoleLog = console.log;
+    this.consoleWarn = console.warn;
+  }
+
+  enable() {
+    const outFn = (printer: any) => (...args: any[]) => {
+      for(const arg of args) {
+        if(typeof arg !== 'string')
+          continue;
+        const skippedWarnings = ['1000:: Normal connection closure', 'Not decorating unknown runtime apis:', 'RPC methods not decorated:', 'Not decorating runtime apis', 'Bad input data provided to validate_transaction', 'account balance too low', '1006:: Abnormal Closure'];
+        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);
+        if(needToSkip || arg === 'Normal connection closure')
+          return;
+      }
+      printer(...args);
+    };
+
+    console.error = outFn(this.consoleErr.bind(console));
+    console.log = outFn(this.consoleLog.bind(console));
+    console.warn = outFn(this.consoleWarn.bind(console));
+  }
+
+  disable() {
+    console.error = this.consoleErr;
+    console.log = this.consoleLog;
+    console.warn = this.consoleWarn;
+  }
+}
+
+export interface IEventHelper {
+  section(): string;
+
+  method(): string;
+
+  wrapEvent(data: any[]): any;
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {
+  const helperClass = class implements IEventHelper {
+    wrapEvent: (data: any[]) => any;
+    _section: string;
+    _method: string;
+
+    constructor() {
+      this.wrapEvent = wrapEvent;
+      this._section = section;
+      this._method = method;
+    }
+
+    section(): string {
+      return this._section;
+    }
+
+    method(): string {
+      return this._method;
+    }
+
+    filter(txres: ITransactionResult) {
+      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)
+        .map(e => this.wrapEvent(e.event.data));
+    }
+
+    find(txres: ITransactionResult) {
+      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);
+      return e ? this.wrapEvent(e.event.data) : null;
+    }
+
+    expect(txres: ITransactionResult) {
+      const e = this.find(txres);
+      if(e) {
+        return e;
+      } else {
+        throw Error(`Expected event ${section}.${method}`);
+      }
+    }
+  };
+
+  return helperClass;
+}
+
+function eventJsonData<T = any>(data: any[], index: number) {
+  return data[index].toJSON() as T;
+}
+
+function eventHumanData(data: any[], index: number) {
+  return data[index].toHuman();
+}
+
+function eventData<T = any>(data: any[], index: number) {
+  return data[index] as T;
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+function EventSection(section: string) {
+  return class Section {
+    static section = section;
+
+    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {
+      const helperClass = EventHelper(Section.section, name, wrapEvent);
+      return new helperClass();
+    }
+  };
+}
+
+function schedulerSection(schedulerInstance: string) {
+  return class extends EventSection(schedulerInstance) {
+    static Dispatched = this.Method('Dispatched', data => ({
+      task: eventJsonData(data, 0),
+      id: eventHumanData(data, 1),
+      result: data[2],
+    }));
+
+    static PriorityChanged = this.Method('PriorityChanged', data => ({
+      task: eventJsonData(data, 0),
+      priority: eventJsonData(data, 1),
+    }));
+  };
+}
+
+export class Event {
+  static Democracy = class extends EventSection('democracy') {
+    static Proposed = this.Method('Proposed', data => ({
+      proposalIndex: eventJsonData<number>(data, 0),
+    }));
+
+    static ExternalTabled = this.Method('ExternalTabled');
+
+    static Started = this.Method('Started', data => ({
+      referendumIndex: eventJsonData<number>(data, 0),
+      threshold: eventHumanData(data, 1),
+    }));
+
+    static Voted = this.Method('Voted', data => ({
+      voter: eventJsonData(data, 0),
+      referendumIndex: eventJsonData<number>(data, 1),
+      vote: eventJsonData(data, 2),
+    }));
+
+    static Passed = this.Method('Passed', data => ({
+      referendumIndex: eventJsonData<number>(data, 0),
+    }));
+
+    static ProposalCanceled = this.Method('ProposalCanceled', data => ({
+      propIndex: eventJsonData<number>(data, 0),
+    }));
+
+    static Cancelled = this.Method('Cancelled', data => ({
+      propIndex: eventJsonData<number>(data, 0),
+    }));
+
+    static Vetoed = this.Method('Vetoed', data => ({
+      who: eventHumanData(data, 0),
+      proposalHash: eventHumanData(data, 1),
+      until: eventJsonData<number>(data, 1),
+    }));
+  };
+
+  static Council = class extends EventSection('council') {
+    static Proposed = this.Method('Proposed', data => ({
+      account: eventHumanData(data, 0),
+      proposalIndex: eventJsonData<number>(data, 1),
+      proposalHash: eventHumanData(data, 2),
+      threshold: eventJsonData<number>(data, 3),
+    }));
+    static Closed = this.Method('Closed', data => ({
+      proposalHash: eventHumanData(data, 0),
+      yes: eventJsonData<number>(data, 1),
+      no: eventJsonData<number>(data, 2),
+    }));
+    static Executed = this.Method('Executed', data => ({
+      proposalHash: eventHumanData(data, 0),
+    }));
+  };
+
+  static TechnicalCommittee = class extends EventSection('technicalCommittee') {
+    static Proposed = this.Method('Proposed', data => ({
+      account: eventHumanData(data, 0),
+      proposalIndex: eventJsonData<number>(data, 1),
+      proposalHash: eventHumanData(data, 2),
+      threshold: eventJsonData<number>(data, 3),
+    }));
+    static Closed = this.Method('Closed', data => ({
+      proposalHash: eventHumanData(data, 0),
+      yes: eventJsonData<number>(data, 1),
+      no: eventJsonData<number>(data, 2),
+    }));
+    static Approved = this.Method('Approved', data => ({
+      proposalHash: eventHumanData(data, 0),
+    }));
+    static Executed = this.Method('Executed', data => ({
+      proposalHash: eventHumanData(data, 0),
+      result: eventHumanData(data, 1),
+    }));
+  };
+
+  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {
+    static Submitted = this.Method('Submitted', data => ({
+      referendumIndex: eventJsonData<number>(data, 0),
+      trackId: eventJsonData<number>(data, 1),
+      proposal: eventJsonData(data, 2),
+    }));
+
+    static Cancelled = this.Method('Cancelled', data => ({
+      index: eventJsonData<number>(data, 0),
+      tally: eventJsonData(data, 1),
+    }));
+  };
+
+  static UniqueScheduler = schedulerSection('uniqueScheduler');
+  static Scheduler = schedulerSection('scheduler');
+
+  static XcmpQueue = class extends EventSection('xcmpQueue') {
+    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({
+      messageHash: eventJsonData(data, 0),
+    }));
+
+    static Success = this.Method('Success', data => ({
+      messageHash: eventJsonData(data, 0),
+    }));
+
+    static Fail = this.Method('Fail', data => ({
+      messageHash: eventJsonData(data, 0),
+      outcome: eventData<StagingXcmV2TraitsError>(data, 2),
+    }));
+  };
+
+  static DmpQueue = class extends EventSection('dmpQueue') {
+    static ExecutedDownward = this.Method('ExecutedDownward', data => ({
+      outcome: eventData<StagingXcmV3TraitsOutcome>(data, 2),
+    }));
+  };
+}
+
+// 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);
+    }
+
+    override 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;
+    }
+    override 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
+   */
+  arrange: ArrangeGroup;
+  wait: WaitGroup;
+  admin: AdminGroup;
+  session: SessionGroup;
+  testUtils: TestUtilGroup;
+  foreignAssets: ForeignAssetsGroup;
+  xcm: XcmGroup<DevUniqueHelper>;
+  xTokens: XTokensGroup<DevUniqueHelper>;
+  tokens: TokensGroup<DevUniqueHelper>;
+  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;
+
+    super(logger, options);
+    this.arrange = new ArrangeGroup(this);
+    this.wait = new WaitGroup(this);
+    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);
+  }
+
+  override async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
+    if(!wsEndpoint) throw new Error('wsEndpoint was not set');
+    const wsProvider = new WsProvider(wsEndpoint);
+    this.api = new ApiPromise({
+      provider: wsProvider,
+      signedExtensions: {
+        ContractHelpers: {
+          extrinsic: {},
+          payload: {},
+        },
+        CheckMaintenance: {
+          extrinsic: {},
+          payload: {},
+        },
+        DisableIdentityCalls: {
+          extrinsic: {},
+          payload: {},
+        },
+        FakeTransactionFinalizer: {
+          extrinsic: {},
+          payload: {},
+        },
+      },
+      rpc: {
+        unique: defs.unique.rpc,
+        appPromotion: defs.appPromotion.rpc,
+        povinfo: defs.povinfo.rpc,
+        eth: {
+          feeHistory: {
+            description: 'Dummy',
+            params: [],
+            type: 'u8',
+          },
+          maxPriorityFeePerGas: {
+            description: 'Dummy',
+            params: [],
+            type: 'u8',
+          },
+        },
+      },
+    });
+    await this.api.isReadyOrError;
+    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 {
+  wait: WaitGroup;
+
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.helperBase = options.helperBase ?? DevRelayHelper;
+
+    super(logger, options);
+    this.wait = new WaitGroup(this);
+  }
+
+  getSudo() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as DevRelayHelper;
+  }
+}
+
+export class DevWestmintHelper extends WestmintHelper {
+  wait: WaitGroup;
+
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.helperBase = options.helperBase ?? DevWestmintHelper;
+
+    super(logger, options);
+    this.wait = new WaitGroup(this);
+  }
+}
+
+export class DevStatemineHelper extends DevWestmintHelper {}
+
+export class DevStatemintHelper extends DevWestmintHelper {}
+
+export class DevMoonbeamHelper extends MoonbeamHelper {
+  account: MoonbeamAccountGroup;
+  wait: WaitGroup;
+  fastDemocracy: MoonbeamFastDemocracyGroup;
+
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.helperBase = options.helperBase ?? DevMoonbeamHelper;
+    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';
+
+    super(logger, options);
+    this.account = new MoonbeamAccountGroup(this);
+    this.wait = new WaitGroup(this);
+    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);
+  }
+}
+
+export class DevMoonriverHelper extends DevMoonbeamHelper {
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';
+    super(logger, options);
+  }
+}
+
+export class DevAstarHelper extends AstarHelper {
+  wait: WaitGroup;
+
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.helperBase = options.helperBase ?? DevAstarHelper;
+
+    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;
+
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.helperBase = options.helperBase ?? DevAcalaHelper;
+
+    super(logger, options);
+    this.wait = new WaitGroup(this);
+  }
+  getSudo() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as DevAcalaHelper;
+  }
+}
+
+export class DevPolkadexHelper extends PolkadexHelper {
+  wait: WaitGroup;
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.helperBase = options.helperBase ?? PolkadexHelper;
+
+    super(logger, options);
+    this.wait = new WaitGroup(this);
+  }
+
+  getSudo() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as DevPolkadexHelper;
+  }
+}
+
+export class DevHydraDxHelper extends HydraDxHelper {
+  wait: WaitGroup;
+  fastDemocracy: HydraFastDemocracyGroup;
+
+  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
+    options.helperBase = options.helperBase ?? DevHydraDxHelper;
+
+    super(logger, options);
+
+    this.wait = new WaitGroup(this);
+    this.fastDemocracy = new HydraFastDemocracyGroup(this);
+  }
+}
+
+export class DevKaruraHelper extends DevAcalaHelper {}
+
+export class ArrangeGroup {
+  helper: DevUniqueHelper;
+
+  scheduledIdSlider = 0;
+
+  constructor(helper: DevUniqueHelper) {
+    this.helper = helper;
+  }
+
+  /**
+   * Generates accounts with the specified UNQ token balance
+   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.
+   * @param donor donor account for balances
+   * @returns array of newly created accounts
+   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);
+   */
+  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {
+    let nonce = await this.helper.chain.getNonce(donor.address);
+    const wait = new WaitGroup(this.helper);
+    const ss58Format = this.helper.chain.getChainProperties().ss58Format;
+    const tokenNominal = this.helper.balance.getOneTokenNominal();
+    const transactions = [];
+    const accounts: IKeyringPair[] = [];
+    for(const balance of balances) {
+      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);
+      accounts.push(recipient);
+      if(balance !== 0n) {
+        const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, balance * tokenNominal]);
+        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));
+        nonce++;
+      }
+    }
+
+    await Promise.all(transactions).catch(_e => {});
+
+    //#region TODO remove this region, when nonce problem will be solved
+    const checkBalances = async () => {
+      let isSuccess = true;
+      for(let i = 0; i < balances.length; i++) {
+        const balance = await this.helper.balance.getSubstrate(accounts[i].address);
+        if(balance !== balances[i] * tokenNominal) {
+          isSuccess = false;
+          break;
+        }
+      }
+      return isSuccess;
+    };
+
+    let accountsCreated = false;
+    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;
+    // checkBalances retry up to 5-50 blocks
+    for(let index = 0; index < maxBlocksChecked; index++) {
+      accountsCreated = await checkBalances();
+      if(accountsCreated) break;
+      await wait.newBlocks(1);
+    }
+
+    if(!accountsCreated) throw Error('Accounts generation failed');
+    //#endregion
+
+    return accounts;
+  };
+
+  // TODO combine this method and createAccounts into one
+  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {
+    const createAsManyAsCan = async () => {
+      let transactions: any = [];
+      const accounts: IKeyringPair[] = [];
+      let nonce = await this.helper.chain.getNonce(donor.address);
+      const tokenNominal = this.helper.balance.getOneTokenNominal();
+      const ss58Format = this.helper.chain.getChainProperties().ss58Format;
+      for(let i = 0; i < accountsToCreate; i++) {
+        if(i === 500) { // if there are too many accounts to create
+          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled
+          transactions = []; //
+          nonce = await this.helper.chain.getNonce(donor.address); // update nonce
+        }
+        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);
+        accounts.push(recipient);
+        if(withBalance !== 0n) {
+          const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, withBalance * tokenNominal]);
+          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));
+          nonce++;
+        }
+      }
+
+      const fullfilledAccounts = [];
+      await Promise.allSettled(transactions);
+      for(const account of accounts) {
+        const accountBalance = await this.helper.balance.getSubstrate(account.address);
+        if(accountBalance === withBalance * tokenNominal) {
+          fullfilledAccounts.push(account);
+        }
+      }
+      return fullfilledAccounts;
+    };
+
+
+    const crowd: IKeyringPair[] = [];
+    // do up to 5 retries
+    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {
+      const asManyAsCan = await createAsManyAsCan();
+      crowd.push(...asManyAsCan);
+      accountsToCreate -= asManyAsCan.length;
+    }
+
+    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);
+
+    return crowd;
+  };
+
+  /**
+   * Generates one account with zero balance
+   * @returns the newly generated account
+   * @example const account = await helper.arrange.createEmptyAccount();
+   */
+  createEmptyAccount = (): IKeyringPair => {
+    const ss58Format = this.helper.chain.getChainProperties().ss58Format;
+    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);
+  };
+
+  isDevNode = async () => {
+    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();
+    if(blockNumber == 0) {
+      await this.helper.wait.newBlocks(1);
+      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();
+    }
+    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);
+    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);
+    const findCreationDate = (block: any) => {
+      const humanBlock = block.toHuman();
+      let date;
+      humanBlock.block.extrinsics.forEach((ext: any) => {
+        if(ext.method.section === 'timestamp') {
+          date = Number(ext.method.args.now.replaceAll(',', ''));
+        }
+      });
+      return date;
+    };
+    const block1date = await findCreationDate(block1);
+    const block2date = await findCreationDate(block2);
+    if(block2date! - block1date! < 9000) return true;
+    return false;
+  };
+
+  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {
+    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);
+    let balance = await this.helper.balance.getSubstrate(address);
+
+    await promise();
+
+    balance -= await this.helper.balance.getSubstrate(address);
+
+    return balance;
+  }
+
+  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {
+    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);
+
+    const kvJson: {[key: string]: string} = {};
+
+    for(const kv of rawPovInfo.keyValues) {
+      kvJson[kv.key.toHex()] = kv.value.toHex();
+    }
+
+    const kvStr = JSON.stringify(kvJson);
+
+    const chainql = spawnSync(
+      'chainql',
+      [
+        `--tla-code=data=${kvStr}`,
+        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,
+      ],
+    );
+
+    if(!chainql.stdout) {
+      throw Error('unable to get an output from the `chainql`');
+    }
+
+    return {
+      proofSize: rawPovInfo.proofSize.toNumber(),
+      compactProofSize: rawPovInfo.compactProofSize.toNumber(),
+      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),
+      results: rawPovInfo.results,
+      kv: JSON.parse(chainql.stdout.toString()),
+    };
+  }
+
+  calculatePalletAddress(palletId: any) {
+    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));
+    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);
+  }
+
+  makeScheduledIds(num: number): string[] {
+    function makeId(slider: number) {
+      const scheduledIdSize = 64;
+      const hexId = slider.toString(16);
+      const prefixSize = scheduledIdSize - hexId.length;
+
+      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;
+
+      return scheduledId;
+    }
+
+    const ids = [];
+    for(let i = 0; i < num; i++) {
+      ids.push(makeId(this.scheduledIdSlider));
+      this.scheduledIdSlider += 1;
+    }
+
+    return ids;
+  }
+
+  makeScheduledId(): string {
+    return (this.makeScheduledIds(1))[0];
+  }
+
+  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {
+    const capture = new EventCapture(this.helper, eventSection, eventMethod);
+    await capture.startCapture();
+
+    return capture;
+  }
+
+  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {
+    return {
+      V2: [
+        {
+          WithdrawAsset: [
+            {
+              id,
+              fun: {
+                Fungible: amount,
+              },
+            },
+          ],
+        },
+        {
+          BuyExecution: {
+            fees: {
+              id,
+              fun: {
+                Fungible: amount,
+              },
+            },
+            weightLimit: 'Unlimited',
+          },
+        },
+        {
+          DepositAsset: {
+            assets: {
+              Wild: 'All',
+            },
+            maxAssets: 1,
+            beneficiary: {
+              parents: 0,
+              interior: {
+                X1: {
+                  AccountId32: {
+                    network: 'Any',
+                    id: beneficiary,
+                  },
+                },
+              },
+            },
+          },
+        },
+      ],
+    };
+  }
+
+  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {
+    return {
+      V2: [
+        {
+          ReserveAssetDeposited: [
+            {
+              id,
+              fun: {
+                Fungible: amount,
+              },
+            },
+          ],
+        },
+        {
+          BuyExecution: {
+            fees: {
+              id,
+              fun: {
+                Fungible: amount,
+              },
+            },
+            weightLimit: 'Unlimited',
+          },
+        },
+        {
+          DepositAsset: {
+            assets: {
+              Wild: 'All',
+            },
+            maxAssets: 1,
+            beneficiary: {
+              parents: 0,
+              interior: {
+                X1: {
+                  AccountId32: {
+                    network: 'Any',
+                    id: beneficiary,
+                  },
+                },
+              },
+            },
+          },
+        },
+      ],
+    };
+  }
+
+  makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {
+    return {
+      V3: [
+        {
+          UnpaidExecution: {
+            weightLimit: 'Unlimited',
+            checkOrigin: null,
+          },
+        },
+        {
+          Transact: {
+            originKind: 'Superuser',
+            requireWeightAtMost: {
+              refTime: info.weightMultiplier * 200000000,
+              proofSize: info.weightMultiplier * 3000,
+            },
+            call: {
+              encoded: info.call,
+            },
+          },
+        },
+      ],
+    };
+  }
+}
+
+class MoonbeamAccountGroup {
+  helper: MoonbeamHelper;
+
+  keyring: Keyring;
+  _alithAccount: IKeyringPair;
+  _baltatharAccount: IKeyringPair;
+  _dorothyAccount: IKeyringPair;
+
+  constructor(helper: MoonbeamHelper) {
+    this.helper = helper;
+
+    this.keyring = new Keyring({type: 'ethereum'});
+    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';
+    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';
+    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';
+
+    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');
+    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');
+    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');
+  }
+
+  alithAccount() {
+    return this._alithAccount;
+  }
+
+  baltatharAccount() {
+    return this._baltatharAccount;
+  }
+
+  dorothyAccount() {
+    return this._dorothyAccount;
+  }
+
+  create() {
+    return this.keyring.addFromUri(mnemonicGenerate());
+  }
+}
+
+class MoonbeamFastDemocracyGroup {
+  helper: DevMoonbeamHelper;
+
+  constructor(helper: DevMoonbeamHelper) {
+    this.helper = helper;
+  }
+
+  async executeProposal(proposalDesciption: string, encodedProposal: string) {
+    const proposalHash = blake2AsHex(encodedProposal);
+
+    const alithAccount = this.helper.account.alithAccount();
+    const baltatharAccount = this.helper.account.baltatharAccount();
+    const dorothyAccount = this.helper.account.dorothyAccount();
+
+    const councilVotingThreshold = 2;
+    const technicalCommitteeThreshold = 2;
+    const fastTrackVotingPeriod = 3;
+    const fastTrackDelayPeriod = 0;
+
+    console.log(`[democracy] executing '${proposalDesciption}' proposal`);
+
+    // >>> Propose external motion through council >>>
+    console.log('\t* Propose external motion through council.......');
+    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});
+    const encodedMotion = externalMotion?.method.toHex() || '';
+    const motionHash = blake2AsHex(encodedMotion);
+    console.log('\t* Motion hash is %s', motionHash);
+
+    await this.helper.collective.council.propose(
+      baltatharAccount,
+      councilVotingThreshold,
+      externalMotion,
+      externalMotion.encodedLength,
+    );
+
+    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;
+    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);
+    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);
+
+    await this.helper.collective.council.close(
+      dorothyAccount,
+      motionHash,
+      councilProposalIdx,
+      {
+        refTime: 1_000_000_000,
+        proofSize: 1_000_000,
+      },
+      externalMotion.encodedLength,
+    );
+    console.log('\t* Propose external motion through council.......DONE');
+    // <<< Propose external motion through council <<<
+
+    // >>> Fast track proposal through technical committee >>>
+    console.log('\t* Fast track proposal through technical committee.......');
+    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);
+    const encodedFastTrack = fastTrack?.method.toHex() || '';
+    const fastTrackHash = blake2AsHex(encodedFastTrack);
+    console.log('\t* FastTrack hash is %s', fastTrackHash);
+
+    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);
+
+    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;
+    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);
+    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);
+
+    await this.helper.collective.techCommittee.close(
+      baltatharAccount,
+      fastTrackHash,
+      techProposalIdx,
+      {
+        refTime: 1_000_000_000,
+        proofSize: 1_000_000,
+      },
+      fastTrack.encodedLength,
+    );
+    console.log('\t* Fast track proposal through technical committee.......DONE');
+    // <<< Fast track proposal through technical committee <<<
+
+    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);
+    const referendumIndex = democracyStarted.referendumIndex;
+
+    // >>> Referendum voting >>>
+    console.log(`\t* Referendum #${referendumIndex} voting.......`);
+    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {
+      balance: 10_000_000_000_000_000_000n,
+      vote: {aye: true, conviction: 1},
+    });
+    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);
+    // <<< Referendum voting <<<
+
+    // Wait the proposal to pass
+    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);
+
+    await this.helper.wait.newBlocks(1);
+
+    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);
+  }
+}
+
+class HydraFastDemocracyGroup {
+  helper: DevHydraDxHelper;
+
+  constructor(helper: DevHydraDxHelper) {
+    this.helper = helper;
+  }
+
+  async executeProposal(proposalDesciption: string, encodedProposal: string) {
+    const proposalHash = blake2AsHex(encodedProposal);
+    const aliceAccount = this.helper.util.fromSeed('//Alice');
+    const bobAccount = this.helper.util.fromSeed('//Bob');
+    const eveAccount = this.helper.util.fromSeed('//Eve');
+
+    const councilVotingThreshold = 1;
+    const technicalCommitteeThreshold = 3;
+    const fastTrackVotingPeriod = 3;
+    const fastTrackDelayPeriod = 0;
+
+    console.log(`[democracy] executing '${proposalDesciption}' proposal`);
+
+    // >>> Propose external motion through council >>>
+    console.log('\t* Propose external motion through council.......');
+    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});
+    const encodedMotion = externalMotion?.method.toHex() || '';
+    const motionHash = blake2AsHex(encodedMotion);
+    console.log('\t* Motion hash is %s', motionHash);
+
+    await this.helper.collective.council.propose(
+      aliceAccount,
+      councilVotingThreshold,
+      externalMotion,
+      externalMotion.encodedLength,
+    );
+
+    console.log('\t* Propose external motion through council.......DONE');
+    // <<< Propose external motion through council <<<
+
+    // >>> Fast track proposal through technical committee >>>
+    console.log('\t* Fast track proposal through technical committee.......');
+    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);
+    const encodedFastTrack = fastTrack?.method.toHex() || '';
+    const fastTrackHash = blake2AsHex(encodedFastTrack);
+    console.log('\t* FastTrack hash is %s', fastTrackHash);
+
+    await this.helper.collective.techCommittee.propose(aliceAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);
+
+    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;
+    await this.helper.collective.techCommittee.vote(aliceAccount, fastTrackHash, techProposalIdx, true);
+    await this.helper.collective.techCommittee.vote(bobAccount, fastTrackHash, techProposalIdx, true);
+    await this.helper.collective.techCommittee.vote(eveAccount, fastTrackHash, techProposalIdx, true);
+
+    await this.helper.collective.techCommittee.close(
+      bobAccount,
+      fastTrackHash,
+      techProposalIdx,
+      {
+        refTime: 1_000_000_000,
+        proofSize: 1_000_000,
+      },
+      fastTrack.encodedLength,
+    );
+    console.log('\t* Fast track proposal through technical committee.......DONE');
+    // <<< Fast track proposal through technical committee <<<
+
+    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);
+    const referendumIndex = democracyStarted.referendumIndex;
+
+    // >>> Referendum voting >>>
+    console.log(`\t* Referendum #${referendumIndex} voting.......`);
+    await this.helper.democracy.referendumVote(eveAccount, referendumIndex, {
+      balance: 10_000_000_000_000_000_000n,
+      vote: {aye: true, conviction: 1},
+    });
+    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);
+    // <<< Referendum voting <<<
+
+    // Wait the proposal to pass
+    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);
+
+    await this.helper.wait.newBlocks(1);
+
+    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);
+  }
+}
+
+class WaitGroup {
+  helper: ChainHelperBase;
+
+  constructor(helper: ChainHelperBase) {
+    this.helper = helper;
+  }
+
+  sleep(milliseconds: number) {
+    return new Promise((resolve) => setTimeout(resolve, milliseconds));
+  }
+
+  private async waitWithTimeout(promise: Promise<any>, timeout: number) {
+    let isBlock = false;
+    promise.then(() => isBlock = true).catch(() => isBlock = true);
+    let totalTime = 0;
+    const step = 100;
+    while(!isBlock) {
+      await this.sleep(step);
+      totalTime += step;
+      if(totalTime >= timeout) throw Error('Blocks production failed');
+    }
+    return promise;
+  }
+
+  /**
+   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.
+   * @param promise async operation to race against the timeout
+   * @param timeoutMS time after which to time out
+   * @param timeoutError error message to throw
+   * @returns promise of the same type the operation had
+   */
+  withTimeout<T>(
+    promise: Promise<T>,
+    timeoutMS = 30000,
+    timeoutError = 'The operation has timed out!',
+  ): Promise<T> {
+    const timeout = new Promise<never>((_, reject) => {
+      setTimeout(() => {
+        reject(new Error(timeoutError));
+      }, timeoutMS);
+    });
+
+    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});
+  }
+
+  /**
+   * Wait for specified number of blocks
+   * @param blocksCount number of blocks to wait
+   * @returns
+   */
+  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {
+    timeout = timeout ?? blocksCount * 60_000;
+    // eslint-disable-next-line no-async-promise-executor
+    const promise = new Promise<void>(async (resolve) => {
+      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {
+        if(blocksCount > 0) {
+          blocksCount--;
+        } else {
+          unsubscribe();
+          resolve();
+        }
+      });
+    });
+    await this.waitWithTimeout(promise, timeout);
+    return promise;
+  }
+
+  /**
+   * Wait for the specified number of sessions to pass.
+   * Only applicable if the Session pallet is turned on.
+   * @param sessionCount number of sessions to wait
+   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks
+   * @returns
+   */
+  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {
+    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`
+      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');
+
+    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;
+    let currentSessionIndex = -1;
+
+    while(currentSessionIndex < expectedSessionIndex) {
+      // eslint-disable-next-line no-async-promise-executor
+      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {
+        await this.newBlocks(1);
+        const res = await (this.helper as DevUniqueHelper).session.getIndex();
+        resolve(res);
+      }), blockTimeout, 'The chain has stopped producing blocks!');
+    }
+  }
+
+  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {
+    timeout = timeout ?? 30 * 60 * 1000;
+    // eslint-disable-next-line no-async-promise-executor
+    const promise = new Promise<void>(async (resolve) => {
+      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {
+        if(data.number.toNumber() >= blockNumber) {
+          unsubscribe();
+          resolve();
+        }
+      });
+    });
+    await this.waitWithTimeout(promise, timeout);
+    return promise;
+  }
+
+  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {
+    timeout = timeout ?? 30 * 60 * 1000;
+    // eslint-disable-next-line no-async-promise-executor
+    const promise = new Promise<void>(async (resolve) => {
+      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {
+        if(data.value.relayParentNumber.toNumber() >= blockNumber) {
+          // @ts-ignore
+          unsubscribe();
+          resolve();
+        }
+      });
+    });
+    await this.waitWithTimeout(promise, timeout);
+    return promise;
+  }
+
+  noScheduledTasks() {
+    const api = this.helper.getApi();
+
+    // eslint-disable-next-line no-async-promise-executor
+    const promise = new Promise<void>(async resolve => {
+      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {
+        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();
+
+        if(areThereScheduledTasks.length == 0) {
+          unsubscribe();
+          resolve();
+        }
+      });
+    });
+
+    return promise;
+  }
+
+  parachainBlockMultiplesOf(val: bigint) {
+    // eslint-disable-next-line no-async-promise-executor
+    const promise = new Promise<void>(async resolve => {
+      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {
+        if(data.number.toBigInt() % val == 0n) {
+          console.log(`from waiter: ${data.number.toBigInt()}`);
+          unsubscribe();
+          resolve();
+        }
+      });
+    });
+    return promise;
+  }
+
+  event<T extends IEventHelper>(
+    maxBlocksToWait: number,
+    eventHelper: T,
+    filter: (_: any) => boolean = () => true,
+  ): any {
+    // eslint-disable-next-line no-async-promise-executor
+    const promise = new Promise<T | null>(async (resolve) => {
+      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {
+        const blockNumber = header.number.toJSON();
+        const blockHash = header.hash;
+        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;
+        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;
+
+        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);
+
+        const apiAt = await this.helper.getApi().at(blockHash);
+        const eventRecords = (await apiAt.query.system.events()) as any;
+
+        const neededEvent = eventRecords.toArray()
+          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())
+          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))
+          .find(filter);
+
+        if(neededEvent) {
+          unsubscribe();
+          resolve(neededEvent);
+        } else if(maxBlocksToWait > 0) {
+          maxBlocksToWait--;
+        } else {
+          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.
+          The wait lasted until block ${blockNumber} inclusive`);
+          unsubscribe();
+          resolve(null);
+        }
+      });
+    });
+    return promise;
+  }
+
+  async expectEvent<T extends IEventHelper>(
+    maxBlocksToWait: number,
+    eventHelper: T,
+    filter: (e: any) => boolean = () => true,
+  ) {
+    const e = await this.event(maxBlocksToWait, eventHelper, filter);
+    if(e == null) {
+      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);
+    } else {
+      return e;
+    }
+  }
+}
+
+class SessionGroup {
+  helper: ChainHelperBase;
+
+  constructor(helper: ChainHelperBase) {
+    this.helper = helper;
+  }
+
+  //todo:collator documentation
+  async getIndex(): Promise<number> {
+    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();
+  }
+
+  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {
+    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);
+  }
+
+  setOwnKeys(signer: TSigner, key: string) {
+    return this.helper.executeExtrinsic(
+      signer,
+      'api.tx.session.setKeys',
+      [key, '0x0'],
+      true,
+    );
+  }
+
+  setOwnKeysFromAddress(signer: TSigner) {
+    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));
+  }
+}
+
+class TestUtilGroup {
+  helper: DevUniqueHelper;
+
+  constructor(helper: DevUniqueHelper) {
+    this.helper = helper;
+  }
+
+  async enable(testUtilsPalletName: string) {
+    if(this.helper.fetchMissingPalletNames([testUtilsPalletName]).length != 0) {
+      return;
+    }
+
+    const signer = this.helper.util.fromSeed('//Alice');
+    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);
+  }
+
+  async setTestValue(signer: TSigner, testVal: number) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);
+  }
+
+  async incTestValue(signer: TSigner) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);
+  }
+
+  async setTestValueAndRollback(signer: TSigner, testVal: number) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);
+  }
+
+  async testValue(blockIdx?: number) {
+    const api = blockIdx
+      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))
+      : this.helper.getApi();
+
+    return (await api.query.testUtils.testValue()).toJSON();
+  }
+
+  async justTakeFee(signer: TSigner) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);
+  }
+
+  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);
+  }
+}
+
+class EventCapture {
+  helper: DevUniqueHelper;
+  eventSection: string;
+  eventMethod: string;
+  events: EventRecord[] = [];
+  unsubscribe: VoidFn | null = null;
+
+  constructor(
+    helper: DevUniqueHelper,
+    eventSection: string,
+    eventMethod: string,
+  ) {
+    this.helper = helper;
+    this.eventSection = eventSection;
+    this.eventMethod = eventMethod;
+  }
+
+  async startCapture() {
+    this.stopCapture();
+    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {
+      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);
+
+      this.events.push(...newEvents);
+    })) as any;
+  }
+
+  stopCapture() {
+    if(this.unsubscribe !== null) {
+      this.unsubscribe();
+    }
+  }
+
+  extractCapturedEvents() {
+    return this.events;
+  }
+}
+
+class AdminGroup {
+  helper: UniqueHelper;
+
+  constructor(helper: UniqueHelper) {
+    this.helper = helper;
+  }
+
+  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {
+    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);
+    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({
+      staker: e.event.data[0].toString(),
+      stake: e.event.data[1].toBigInt(),
+      payout: e.event.data[2].toBigInt(),
+    }));
+  }
+}
+
+// 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;
+    }
+
+    override 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,
+      );
+    }
+  };
+}
addedjs-packages/test-utils/package.jsondiffbeforeafterboth
--- /dev/null
+++ b/js-packages/test-utils/package.json
@@ -0,0 +1,18 @@
+{
+  "author": "",
+  "license": "SEE LICENSE IN ../../../LICENSE",
+  "description": "Playground scripts",
+  "engines": {
+    "node": ">=16"
+  },
+  "name": "@unique/test-utils",
+  "type": "module",
+  "version": "1.0.0",
+  "main": "index.js",
+  "dependencies": {
+    "@polkadot/api": "10.10.1",
+    "@polkadot/util": "^12.5.1",
+    "@polkadot/util-crypto": "^12.5.1",
+    "@unique-nft/opal-testnet-types": "workspace:*"
+  }
+}
addedjs-packages/test-utils/util.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/test-utils/util.ts
@@ -0,0 +1,223 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+import * as path from 'path';
+import * as crypto from 'crypto';
+import type {IKeyringPair} from '@polkadot/types/types/interfaces';
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import chaiSubset from 'chai-subset';
+import {Context} from 'mocha';
+import config from '../tests/config.js';
+import {ChainHelperBase} from '@unique-nft/playgrounds/unique.js';
+import type {ILogger} from '@unique-nft/playgrounds/types.js';
+import {DevUniqueHelper, SilentLogger, SilentConsole, DevMoonbeamHelper, DevMoonriverHelper, DevAcalaHelper, DevKaruraHelper, DevRelayHelper, DevWestmintHelper, DevStatemineHelper, DevStatemintHelper, DevAstarHelper, DevShidenHelper, DevPolkadexHelper, DevHydraDxHelper} from '@unique/test-utils';
+import {dirname} from 'path';
+import {fileURLToPath} from 'url';
+
+chai.config.truncateThreshold = 0;
+chai.use(chaiAsPromised);
+chai.use(chaiSubset);
+export const expect = chai.expect;
+
+const getTestHash = (filename: string) => crypto.createHash('md5').update(filename).digest('hex');
+
+export const getTestSeed = (filename: string) => `//Alice+${getTestHash(filename)}`;
+
+async function usingPlaygroundsGeneral<T extends ChainHelperBase, R = void>(
+  helperType: new (logger: ILogger) => T,
+  url: string,
+  code: (helper: T, privateKey: (seed: string | { filename?: string, url?: string, ignoreFundsPresence?: boolean }) => Promise<IKeyringPair>) => Promise<R>,
+): Promise<R> {
+  const silentConsole = new SilentConsole();
+  silentConsole.enable();
+
+  const helper = new helperType(new SilentLogger());
+  let result;
+  try {
+    await helper.connect(url);
+    const ss58Format = helper.chain.getChainProperties().ss58Format;
+    const privateKey = async (seed: string | {filename?: string, url?: string, ignoreFundsPresence?: boolean}) => {
+      if(typeof seed === 'string') {
+        return helper.util.fromSeed(seed, ss58Format);
+      }
+      if(seed.url) {
+        const {filename} = makeNames(seed.url);
+        seed.filename = filename;
+      } else if(seed.filename) {
+        // Pass
+      } else {
+        throw new Error('no url nor filename set');
+      }
+      const actualSeed = getTestSeed(seed.filename);
+      let account = helper.util.fromSeed(actualSeed, ss58Format);
+      // here's to hoping that no
+      if(!seed.ignoreFundsPresence && ((helper as any)['balance'] == undefined || await (helper as any).balance.getSubstrate(account.address) < MINIMUM_DONOR_FUND)) {
+        console.warn(`${path.basename(seed.filename)}: Not enough funds present on the filename account. Using the default one as the donor instead.`);
+        account = helper.util.fromSeed('//Alice', ss58Format);
+      }
+      return account;
+    };
+    result = await code(helper, privateKey);
+  }
+  finally {
+    await helper.disconnect();
+    silentConsole.disable();
+  }
+  return result as any as R;
+}
+
+export const usingPlaygrounds = <R = void>(code: (helper: DevUniqueHelper, privateKey: (seed: string | {filename?: string, url?: string, ignoreFundsPresence?: boolean}) => Promise<IKeyringPair>) => Promise<R>, url: string = config.substrateUrl) => usingPlaygroundsGeneral<DevUniqueHelper, R>(DevUniqueHelper, url, code);
+
+export const usingWestmintPlaygrounds = (url: string, code: (helper: DevWestmintHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevWestmintHelper>(DevWestmintHelper, url, code);
+
+export const usingStateminePlaygrounds = (url: string, code: (helper: DevWestmintHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevStatemineHelper>(DevWestmintHelper, url, code);
+
+export const usingStatemintPlaygrounds = (url: string, code: (helper: DevWestmintHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevStatemintHelper>(DevWestmintHelper, url, code);
+
+export const usingRelayPlaygrounds = (url: string, code: (helper: DevRelayHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevRelayHelper>(DevRelayHelper, url, code);
+
+export const usingAcalaPlaygrounds = (url: string, code: (helper: DevAcalaHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevAcalaHelper>(DevAcalaHelper, url, code);
+
+export const usingKaruraPlaygrounds = (url: string, code: (helper: DevKaruraHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevKaruraHelper>(DevAcalaHelper, url, code);
+
+export const usingMoonbeamPlaygrounds = (url: string, code: (helper: DevMoonbeamHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevMoonbeamHelper>(DevMoonbeamHelper, url, code);
+
+export const usingMoonriverPlaygrounds = (url: string, code: (helper: DevMoonbeamHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevMoonriverHelper>(DevMoonriverHelper, url, code);
+
+export const usingAstarPlaygrounds = (url: string, code: (helper: DevAstarHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevAstarHelper>(DevAstarHelper, url, code);
+
+export const usingShidenPlaygrounds = (url: string, code: (helper: DevShidenHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevShidenHelper>(DevShidenHelper, url, code);
+
+export const usingPolkadexPlaygrounds = (url: string, code: (helper: DevPolkadexHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevPolkadexHelper>(DevPolkadexHelper, url, code);
+
+export const usingHydraDxPlaygrounds = (url: string, code: (helper: DevHydraDxHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevHydraDxHelper>(DevHydraDxHelper, url, code);
+
+export const MINIMUM_DONOR_FUND = 4_000_000n;
+export const DONOR_FUNDING = 4_000_000n;
+
+// App-promotion periods:
+export const LOCKING_PERIOD = 12n; // 12 blocks of relay
+export const UNLOCKING_PERIOD = 6n; // 6 blocks of parachain
+
+// Native contracts
+export const COLLECTION_HELPER = '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f';
+export const CONTRACT_HELPER = '0x842899ECF380553E8a4de75bF534cdf6fBF64049';
+
+export enum Pallets {
+  Inflation = 'inflation',
+  ReFungible = 'refungible',
+  Fungible = 'fungible',
+  NFT = 'nonfungible',
+  Scheduler = 'scheduler',
+  UniqueScheduler = 'uniqueScheduler',
+  AppPromotion = 'apppromotion',
+  CollatorSelection = 'collatorselection',
+  Session = 'session',
+  Identity = 'identity',
+  Democracy = 'democracy',
+  Council = 'council',
+  //CouncilMembership = 'councilmembership',
+  TechnicalCommittee = 'technicalcommittee',
+  Fellowship = 'fellowshipcollective',
+  Preimage = 'preimage',
+  Maintenance = 'maintenance',
+  TestUtils = 'testutils',
+}
+
+export function requirePalletsOrSkip(test: Context, helper: DevUniqueHelper, requiredPallets: readonly string[]) {
+  const missingPallets = helper.fetchMissingPalletNames(requiredPallets);
+
+  if(missingPallets.length > 0) {
+    const skipMsg = `\tSkipping test '${test.test?.title}'.\n\tThe following pallets are missing:\n\t- ${missingPallets.join('\n\t- ')}`;
+    console.warn('\x1b[38:5:208m%s\x1b[0m', skipMsg);
+    test.skip();
+  }
+}
+
+export function itSub(name: string, cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: readonly string[] } = {}) {
+  (opts.only ? it.only :
+    opts.skip ? it.skip : it)(name, async function () {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      if(opts.requiredPallets) {
+        requirePalletsOrSkip(this, helper, opts.requiredPallets);
+      }
+
+      await cb({helper, privateKey});
+    });
+  });
+}
+export function itSubIfWithPallet(name: string, required: readonly string[], cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: readonly string[] } = {}) {
+  return itSub(name, cb, {requiredPallets: required, ...opts});
+}
+itSub.only = (name: string, cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSub(name, cb, {only: true});
+itSub.skip = (name: string, cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSub(name, cb, {skip: true});
+
+itSubIfWithPallet.only = (name: string, required: readonly string[], cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSubIfWithPallet(name, required, cb, {only: true});
+itSubIfWithPallet.skip = (name: string, required: readonly string[], cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSubIfWithPallet(name, required, cb, {skip: true});
+itSub.ifWithPallets = itSubIfWithPallet;
+
+export type SchedKind = 'anon' | 'named';
+
+export function itSched(
+  name: string,
+  cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any,
+  opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {},
+) {
+  itSub(name + ' (anonymous scheduling)', (apis) => cb('anon', apis), opts);
+  itSub(name + ' (named scheduling)', (apis) => cb('named', apis), opts);
+}
+itSched.only = (name: string, cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSched(name, cb, {only: true});
+itSched.skip = (name: string, cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSched(name, cb, {skip: true});
+itSched.ifWithPallets = itSchedIfWithPallets;
+
+function itSchedIfWithPallets(name: string, required: string[], cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {}) {
+  return itSched(name, cb, {requiredPallets: required, ...opts});
+}
+
+export function describeXCM(title: string, fn: (this: Mocha.Suite) => void, opts: {skip?: boolean} = {}) {
+  (process.env.RUN_XCM_TESTS && !opts.skip
+    ? describe
+    : describe.skip)(title, fn);
+}
+
+describeXCM.skip = (name: string, fn: (this: Mocha.Suite) => void) => describeXCM(name, fn, {skip: true});
+
+export function describeGov(title: string, fn: (this: Mocha.Suite) => void, opts: {skip?: boolean} = {}) {
+  (process.env.RUN_GOV_TESTS && !opts.skip
+    ? describe
+    : describe.skip)(title, fn);
+}
+
+describeGov.skip = (name: string, fn: (this: Mocha.Suite) => void) => describeGov(name, fn, {skip: true});
+
+export function sizeOfInt(i: number) {
+  if(i < 0 || i > 0xffffffff) throw new Error('out of range');
+  if(i < 0b11_1111) {
+    return 1;
+  } else if(i < 0b11_1111_1111_1111) {
+    return 2;
+  } else if(i < 0b11_1111_1111_1111_1111_1111_1111_1111) {
+    return 4;
+  } else {
+    return 5;
+  }
+}
+
+const UTF8_ENCODER = new TextEncoder();
+export function sizeOfEncodedStr(v: string) {
+  const encoded = UTF8_ENCODER.encode(v);
+  return sizeOfInt(encoded.length) + encoded.length;
+}
+
+export function sizeOfProperty(prop: {key: string, value: string}) {
+  return sizeOfEncodedStr(prop.key) + sizeOfEncodedStr(prop.value);
+}
+
+export function makeNames(url: string) {
+  const filename = fileURLToPath(url);
+  return {
+    filename,
+    dirname: dirname(filename),
+  };
+}
addedjs-packages/test-utils/xcm/index.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/test-utils/xcm/index.ts
@@ -0,0 +1,449 @@
+import {ApiPromise, WsProvider} from '@polkadot/api';
+import type {IKeyringPair} from '@polkadot/types/types';
+import {ChainHelperBase, EthereumBalanceGroup, HelperGroup, SubstrateBalanceGroup, UniqueHelper} from '@unique-nft/playgrounds/unique.js';
+import type {ILogger, TSigner} from '@unique-nft/playgrounds/types.js';
+import type {AcalaAssetMetadata, DemocracyStandardAccountVote, MoonbeamAssetInfo} from './types.js';
+
+
+export class XcmChainHelper extends ChainHelperBase {
+  override 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 DemocracyGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  notePreimagePallet: string;
+
+  constructor(helper: T, 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 CollectiveGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  collective: string;
+
+  constructor(helper: T, palletName: string) {
+    super(helper);
+
+    this.collective = palletName;
+  }
+
+  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, assetId: any, name: string, tokenPrefix: string, mode: 'NFT' | { Fungible: number }) {
+    await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.foreignAssets.forceRegisterForeignAsset',
+      [{V3: assetId}, this.helper.util.str2vec(name), tokenPrefix, mode],
+      true,
+    );
+  }
+
+  async foreignCollectionId(assetId: any) {
+    return (await this.helper.callRpc('api.query.foreignAssets.foreignAssetToCollection', [assetId])).toJSON();
+  }
+}
+
+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 | bigint, admin: string, minimalBalance: bigint) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
+  }
+
+  async forceCreate(signer: TSigner, assetId: number | bigint, admin: string, minimalBalance: bigint, isSufficient = true) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.forceCreate', [assetId, admin, isSufficient, minimalBalance], true);
+  }
+
+  async setMetadata(signer: TSigner, assetId: number | bigint, 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 | bigint, beneficiary: string, amount: bigint) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
+  }
+
+  async assetInfo(assetId: number | bigint) {
+    return (await this.helper.callRpc('api.query.assets.asset', [assetId])).toJSON();
+  }
+
+  async account(assetId: string | number | bigint, 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);
+  }
+}
+
+export class HydraDxHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<HydraDxHelper>;
+  xcm: XcmGroup<HydraDxHelper>;
+  xTokens: XTokensGroup<HydraDxHelper>;
+  democracy: DemocracyGroup<HydraDxHelper>;
+  collective: {
+    council: CollectiveGroup<HydraDxHelper>,
+    techCommittee: CollectiveGroup<HydraDxHelper>,
+  };
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? HydraDxHelper);
+    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+    this.xTokens = new XTokensGroup(this);
+    this.democracy = new DemocracyGroup(this, options);
+    this.collective = {
+      council: new CollectiveGroup(this, 'council'),
+      techCommittee: new CollectiveGroup(this, 'technicalCommittee'),
+    };
+  }
+}
+
addedjs-packages/test-utils/xcm/types.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/test-utils/xcm/types.ts
@@ -0,0 +1,29 @@
+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,
+  },
+}
modifiedjs-packages/tests/addCollectionAdmin.test.tsdiffbeforeafterboth
--- a/js-packages/tests/addCollectionAdmin.test.ts
+++ b/js-packages/tests/addCollectionAdmin.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test addCollectionAdmin(collection_id, new_admin_id):', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/adminTransferAndBurn.test.tsdiffbeforeafterboth
--- a/js-packages/tests/adminTransferAndBurn.test.ts
+++ b/js-packages/tests/adminTransferAndBurn.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from './util/index.js';
+import {usingPlaygrounds, expect, itSub} from '@unique/test-utils/util.js';
 
 describe('Integration Test: ownerCanTransfer allows admins to use only transferFrom/burnFrom:', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/allowLists.test.tsdiffbeforeafterboth
--- a/js-packages/tests/allowLists.test.ts
+++ b/js-packages/tests/allowLists.test.ts
@@ -15,9 +15,9 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from './util/index.js';
-import type {ICollectionPermissions} from '@unique/playgrounds/types.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, expect, itSub} from '@unique/test-utils/util.js';
+import type {ICollectionPermissions} from '@unique-nft/playgrounds/types.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test ext. Allow list tests', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/apiConsts.test.tsdiffbeforeafterboth
--- a/js-packages/tests/apiConsts.test.ts
+++ b/js-packages/tests/apiConsts.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {ApiPromise} from '@polkadot/api';
-import {usingPlaygrounds, itSub, expect, COLLECTION_HELPER, CONTRACT_HELPER} from './util/index.js';
+import {usingPlaygrounds, itSub, expect, COLLECTION_HELPER, CONTRACT_HELPER} from '@unique/test-utils/util.js';
 
 
 const MAX_COLLECTION_DESCRIPTION_LENGTH = 256n;
modifiedjs-packages/tests/approve.test.tsdiffbeforeafterboth
--- a/js-packages/tests/approve.test.ts
+++ b/js-packages/tests/approve.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, usingPlaygrounds} from './util/index.js';
-import {CrossAccountId} from '@unique/playgrounds/unique.js';
+import {expect, itSub, Pallets, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {CrossAccountId} from '@unique-nft/playgrounds/unique.js';
 
 
 
modifiedjs-packages/tests/burnItem.test.tsdiffbeforeafterboth
--- a/js-packages/tests/burnItem.test.ts
+++ b/js-packages/tests/burnItem.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from './util/index.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 
 describe('integration test: ext. burnItem():', () => {
modifiedjs-packages/tests/change-collection-owner.test.tsdiffbeforeafterboth
--- a/js-packages/tests/change-collection-owner.test.ts
+++ b/js-packages/tests/change-collection-owner.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, expect, itSub} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test changeCollectionOwner(collection_id, new_owner):', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/confirmSponsorship.test.tsdiffbeforeafterboth
--- a/js-packages/tests/confirmSponsorship.test.ts
+++ b/js-packages/tests/confirmSponsorship.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub, Pallets} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, expect, itSub, Pallets} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 async function setSponsorHelper(collection: any, signer: IKeyringPair, sponsorAddress: string) {
   await collection.setSponsor(signer, sponsorAddress);
modifiedjs-packages/tests/connection.test.tsdiffbeforeafterboth
--- a/js-packages/tests/connection.test.ts
+++ b/js-packages/tests/connection.test.ts
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {itSub, expect, usingPlaygrounds} from './util/index.js';
+import {itSub, expect, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 describe('Connection smoke test', () => {
   itSub('Connection can be established', async ({helper}) => {
modifiedjs-packages/tests/createCollection.test.tsdiffbeforeafterboth
--- a/js-packages/tests/createCollection.test.ts
+++ b/js-packages/tests/createCollection.test.ts
@@ -15,10 +15,10 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub, Pallets} from './util/index.js';
-import {CollectionFlag} from '@unique/playgrounds/types.js';
-import type {ICollectionCreationOptions, IProperty} from '@unique/playgrounds/types.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
+import {usingPlaygrounds, expect, itSub, Pallets} from '@unique/test-utils/util.js';
+import {CollectionFlag} from '@unique-nft/playgrounds/types.js';
+import type {ICollectionCreationOptions, IProperty} from '@unique-nft/playgrounds/types.js';
+import {UniqueHelper} from '@unique-nft/playgrounds/unique.js';
 
 async function mintCollectionHelper(helper: UniqueHelper, signer: IKeyringPair, options: ICollectionCreationOptions, type?: 'nft' | 'fungible' | 'refungible') {
   let collection;
modifiedjs-packages/tests/createItem.test.tsdiffbeforeafterboth
--- a/js-packages/tests/createItem.test.ts
+++ b/js-packages/tests/createItem.test.ts
@@ -15,9 +15,9 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub, Pallets} from './util/index.js';
-import type {IProperty, ICrossAccountId} from '@unique/playgrounds/types.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
+import {usingPlaygrounds, expect, itSub, Pallets} from '@unique/test-utils/util.js';
+import type {IProperty, ICrossAccountId} from '@unique-nft/playgrounds/types.js';
+import {UniqueHelper} from '@unique-nft/playgrounds/unique.js';
 
 async function mintTokenHelper(helper: UniqueHelper, collection: any, signer: IKeyringPair, owner: ICrossAccountId, type: 'nft' | 'fungible' | 'refungible'='nft', properties?: IProperty[]) {
   let token;
modifiedjs-packages/tests/createMultipleItems.test.tsdiffbeforeafterboth
--- a/js-packages/tests/createMultipleItems.test.ts
+++ b/js-packages/tests/createMultipleItems.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, Pallets, itSub} from './util/index.js';
+import {usingPlaygrounds, expect, Pallets, itSub} from '@unique/test-utils/util.js';
 
 describe('Integration Test createMultipleItems(collection_id, owner, items_data):', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/createMultipleItemsEx.test.tsdiffbeforeafterboth
--- a/js-packages/tests/createMultipleItemsEx.test.ts
+++ b/js-packages/tests/createMultipleItemsEx.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, Pallets, itSub} from './util/index.js';
-import type {IProperty} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, expect, Pallets, itSub} from '@unique/test-utils/util.js';
+import type {IProperty} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test: createMultipleItemsEx', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/creditFeesToTreasury.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/creditFeesToTreasury.seqtest.ts
+++ b/js-packages/tests/creditFeesToTreasury.seqtest.ts
@@ -16,7 +16,7 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {ApiPromise} from '@polkadot/api';
-import {usingPlaygrounds, expect, itSub} from './util/index.js';
+import {usingPlaygrounds, expect, itSub} from '@unique/test-utils/util.js';
 import type {u32} from '@polkadot/types-codec';
 
 const TREASURY = '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z';
modifiedjs-packages/tests/destroyCollection.test.tsdiffbeforeafterboth
--- a/js-packages/tests/destroyCollection.test.ts
+++ b/js-packages/tests/destroyCollection.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, expect, usingPlaygrounds, Pallets} from './util/index.js';
+import {itSub, expect, usingPlaygrounds, Pallets} from '@unique/test-utils/util.js';
 
 describe('integration test: ext. destroyCollection():', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/enableDisableTransfer.test.tsdiffbeforeafterboth
--- a/js-packages/tests/enableDisableTransfer.test.ts
+++ b/js-packages/tests/enableDisableTransfer.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 describe('Enable/Disable Transfers', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/eth/allowlist.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/allowlist.test.ts
+++ b/js-packages/tests/eth/allowlist.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {Pallets} from '../util/index.js';
+import {Pallets} from '@unique/test-utils/util.js';
 import {itEth, usingEthPlaygrounds, expect, SponsoringMode} from './util/index.js';
 import {CreateCollectionData} from './util/playgrounds/types.js';
 
modifiedjs-packages/tests/eth/collectionAdmin.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/collectionAdmin.test.ts
+++ b/js-packages/tests/eth/collectionAdmin.test.ts
@@ -15,8 +15,8 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {expect} from 'chai';
-import {Pallets} from '../util/index.js';
-import type {IEthCrossAccountId} from '@unique/playgrounds/types.js';
+import {Pallets} from '@unique/test-utils/util.js';
+import type {IEthCrossAccountId} from '@unique-nft/playgrounds/types.js';
 import {usingEthPlaygrounds, itEth} from './util/index.js';
 import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';
 import {CreateCollectionData} from './util/playgrounds/types.js';
modifiedjs-packages/tests/eth/collectionHelperAddress.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/collectionHelperAddress.test.ts
+++ b/js-packages/tests/eth/collectionHelperAddress.test.ts
@@ -16,7 +16,7 @@
 
 import {itEth, usingEthPlaygrounds, expect} from './util/index.js';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {COLLECTION_HELPER, Pallets} from '../util/index.js';
+import {COLLECTION_HELPER, Pallets} from '@unique/test-utils/util.js';
 
 describe('[eth]CollectionHelperAddress test: ERC20/ERC721 ', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/eth/collectionLimits.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/collectionLimits.test.ts
+++ b/js-packages/tests/eth/collectionLimits.test.ts
@@ -1,5 +1,5 @@
 import type {IKeyringPair} from '@polkadot/types/types';
-import {Pallets} from '../util/index.js';
+import {Pallets} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 import {CollectionLimitField, CreateCollectionData} from './util/playgrounds/types.js';
 
modifiedjs-packages/tests/eth/collectionProperties.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/collectionProperties.test.ts
+++ b/js-packages/tests/eth/collectionProperties.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {itEth, usingEthPlaygrounds, expect} from './util/index.js';
-import {Pallets} from '../util/index.js';
-import type {IProperty, ITokenPropertyPermission} from '@unique/playgrounds/types.js';
+import {Pallets} from '@unique/test-utils/util.js';
+import type {IProperty, ITokenPropertyPermission} from '@unique-nft/playgrounds/types.js';
 import type {IKeyringPair} from '@polkadot/types/types';
 
 describe('EVM collection properties', () => {
modifiedjs-packages/tests/eth/collectionSponsoring.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/collectionSponsoring.test.ts
+++ b/js-packages/tests/eth/collectionSponsoring.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../util/index.js';
+import {Pallets, requirePalletsOrSkip, usingPlaygrounds} from '@unique/test-utils/util.js';
 import {itEth, expect} from './util/index.js';
 import {CollectionLimitField, TokenPermissionField} from './util/playgrounds/types.js';
 
modifiedjs-packages/tests/eth/contractSponsoring.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/contractSponsoring.test.ts
+++ b/js-packages/tests/eth/contractSponsoring.test.ts
@@ -17,7 +17,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';
 import {itEth, expect, SponsoringMode, usingEthPlaygrounds} from './util/index.js';
-import {usingPlaygrounds} from '../util/index.js';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
 import type {CompiledContract} from './util/playgrounds/types.js';
 
 describe('Sponsoring EVM contracts', () => {
modifiedjs-packages/tests/eth/createCollection.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/createCollection.test.ts
+++ b/js-packages/tests/eth/createCollection.test.ts
@@ -16,11 +16,11 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {evmToAddress} from '@polkadot/util-crypto';
-import {Pallets, requirePalletsOrSkip} from '../util/index.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 import {CREATE_COLLECTION_DATA_DEFAULTS, CollectionLimitField, CollectionMode, CreateCollectionData, TokenPermissionField, emptyAddress} from './util/playgrounds/types.js';
-import {CollectionFlag} from '@unique/playgrounds/types.js';
-import type {IEthCrossAccountId, TCollectionMode} from '@unique/playgrounds/types.js';
+import {CollectionFlag} from '@unique-nft/playgrounds/types.js';
+import type {IEthCrossAccountId, TCollectionMode} from '@unique-nft/playgrounds/types.js';
 
 const DECIMALS = 18;
 const CREATE_COLLECTION_DATA_DEFAULTS_ARRAY = [
modifiedjs-packages/tests/eth/createFTCollection.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/createFTCollection.seqtest.ts
+++ b/js-packages/tests/eth/createFTCollection.seqtest.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {Pallets, requirePalletsOrSkip} from '../util/index.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 
 const DECIMALS = 18;
modifiedjs-packages/tests/eth/createFTCollection.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/createFTCollection.test.ts
+++ b/js-packages/tests/eth/createFTCollection.test.ts
@@ -16,7 +16,7 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {evmToAddress} from '@polkadot/util-crypto';
-import {Pallets, requirePalletsOrSkip} from '../util/index.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 import {CollectionLimitField} from './util/playgrounds/types.js';
 
modifiedjs-packages/tests/eth/createRFTCollection.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/createRFTCollection.test.ts
+++ b/js-packages/tests/eth/createRFTCollection.test.ts
@@ -16,7 +16,7 @@
 
 import {evmToAddress} from '@polkadot/util-crypto';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {Pallets, requirePalletsOrSkip} from '../util/index.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 import {CollectionLimitField} from './util/playgrounds/types.js';
 
modifiedjs-packages/tests/eth/crossTransfer.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/crossTransfer.test.ts
+++ b/js-packages/tests/eth/crossTransfer.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {itEth, usingEthPlaygrounds} from './util/index.js';
-import {CrossAccountId} from '@unique/playgrounds/unique.js';
+import {CrossAccountId} from '@unique-nft/playgrounds/unique.js';
 import type {IKeyringPair} from '@polkadot/types/types';
 
 describe('Token transfer between substrate address and EVM address. Fungible', () => {
modifiedjs-packages/tests/eth/destroyCollection.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/destroyCollection.test.ts
+++ b/js-packages/tests/eth/destroyCollection.test.ts
@@ -15,9 +15,9 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {Pallets} from '../util/index.js';
+import {Pallets} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
-import type {TCollectionMode} from '@unique/playgrounds/types.js';
+import type {TCollectionMode} from '@unique-nft/playgrounds/types.js';
 import {CreateCollectionData} from './util/playgrounds/types.js';
 
 describe('Destroy Collection from EVM', function() {
modifiedjs-packages/tests/eth/events.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/events.test.ts
+++ b/js-packages/tests/eth/events.test.ts
@@ -18,8 +18,8 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {itEth, usingEthPlaygrounds} from './util/index.js';
 import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';
-import type {IEvent, TCollectionMode} from '@unique/playgrounds/types.js';
-import {Pallets, requirePalletsOrSkip} from '../util/index.js';
+import type {IEvent, TCollectionMode} from '@unique-nft/playgrounds/types.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {CollectionLimitField, TokenPermissionField, CreateCollectionData} from './util/playgrounds/types.js';
 import type {NormalizedEvent} from './util/playgrounds/types.js';
 
modifiedjs-packages/tests/eth/fractionalizer/fractionalizer.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/fractionalizer/fractionalizer.test.ts
+++ b/js-packages/tests/eth/fractionalizer/fractionalizer.test.ts
@@ -25,7 +25,7 @@
 import {usingEthPlaygrounds, expect, itEth} from '../util/index.js';
 import {EthUniqueHelper} from '../util/playgrounds/unique.dev.js';
 import type {CompiledContract} from '../util/playgrounds/types.js';
-import {requirePalletsOrSkip, Pallets, makeNames} from '../../util/index.js';
+import {requirePalletsOrSkip, Pallets, makeNames} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
modifiedjs-packages/tests/eth/getCode.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/getCode.test.ts
+++ b/js-packages/tests/eth/getCode.test.ts
@@ -16,7 +16,7 @@
 
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {COLLECTION_HELPER, CONTRACT_HELPER} from '../util/index.js';
+import {COLLECTION_HELPER, CONTRACT_HELPER} from '@unique/test-utils/util.js';
 
 describe('RPC eth_getCode', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/eth/marketplace-v2/marketplace.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/marketplace-v2/marketplace.test.ts
+++ b/js-packages/tests/eth/marketplace-v2/marketplace.test.ts
@@ -19,8 +19,7 @@
 import {readFile} from 'fs/promises';
 import {SponsoringMode, itEth, usingEthPlaygrounds} from '../util/index.js';
 import {EthUniqueHelper} from '../util/playgrounds/unique.dev.js';
-import {makeNames} from '../../util/index.js';
-import {expect} from 'chai';
+import {makeNames, expect} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
modifiedjs-packages/tests/eth/marketplace/marketplace.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/marketplace/marketplace.test.ts
+++ b/js-packages/tests/eth/marketplace/marketplace.test.ts
@@ -17,7 +17,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {readFile} from 'fs/promises';
 import {itEth, usingEthPlaygrounds, expect, SponsoringMode} from '../util/index.js';
-import {makeNames} from '../../util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
modifiedjs-packages/tests/eth/migration.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/migration.seqtest.ts
+++ b/js-packages/tests/eth/migration.seqtest.ts
@@ -18,7 +18,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {Struct} from '@polkadot/types';
 
-import type {IEvent} from '@unique/playgrounds/types.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 import type {InterfaceTypes} from '@polkadot/types/types/registry';
 import {ApiPromise} from '@polkadot/api';
 
modifiedjs-packages/tests/eth/nativeFungible.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/nativeFungible.test.ts
+++ b/js-packages/tests/eth/nativeFungible.test.ts
@@ -16,7 +16,7 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
+import {UniqueHelper} from '@unique-nft/playgrounds/unique.js';
 
 describe('NativeFungible: ERC20 calls', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/eth/nonFungible.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/nonFungible.test.ts
+++ b/js-packages/tests/eth/nonFungible.test.ts
@@ -18,7 +18,7 @@
 import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';
 import type {IKeyringPair} from '@polkadot/types/types';
 import {Contract} from 'web3-eth-contract';
-import type {ITokenPropertyPermission} from '@unique/playgrounds/types.js';
+import type {ITokenPropertyPermission} from '@unique-nft/playgrounds/types.js';
 import {CREATE_COLLECTION_DATA_DEFAULTS, TokenPermissionField} from './util/playgrounds/types.js';
 
 describe('Check ERC721 token URI for NFT', () => {
modifiedjs-packages/tests/eth/payable.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/payable.test.ts
+++ b/js-packages/tests/eth/payable.test.ts
@@ -18,7 +18,7 @@
 
 import {itEth, expect, usingEthPlaygrounds} from './util/index.js';
 import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';
-import {makeNames} from '../util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
modifiedjs-packages/tests/eth/proxy/fungibleProxy.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/proxy/fungibleProxy.test.ts
+++ b/js-packages/tests/eth/proxy/fungibleProxy.test.ts
@@ -19,7 +19,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {itEth, usingEthPlaygrounds} from '../util/index.js';
 import {EthUniqueHelper} from '../util/playgrounds/unique.dev.js';
-import {makeNames} from '../../util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
modifiedjs-packages/tests/eth/proxy/nonFungibleProxy.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/proxy/nonFungibleProxy.test.ts
+++ b/js-packages/tests/eth/proxy/nonFungibleProxy.test.ts
@@ -18,7 +18,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {itEth, usingEthPlaygrounds, expect} from '../util/index.js';
 import {EthUniqueHelper} from '../util/playgrounds/unique.dev.js';
-import {makeNames} from '../../util/index.js';
+import {makeNames} from '@unique/test-utils/util.js';
 
 const {dirname} = makeNames(import.meta.url);
 
modifiedjs-packages/tests/eth/reFungible.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/reFungible.test.ts
+++ b/js-packages/tests/eth/reFungible.test.ts
@@ -14,10 +14,10 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {Pallets, requirePalletsOrSkip} from '../util/index.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 import type {IKeyringPair} from '@polkadot/types/types';
-import type {ITokenPropertyPermission} from '@unique/playgrounds/types.js';
+import type {ITokenPropertyPermission} from '@unique-nft/playgrounds/types.js';
 import {CREATE_COLLECTION_DATA_DEFAULTS, TokenPermissionField} from './util/playgrounds/types.js';
 
 describe('Refungible: Plain calls', () => {
modifiedjs-packages/tests/eth/reFungibleToken.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/reFungibleToken.test.ts
+++ b/js-packages/tests/eth/reFungibleToken.test.ts
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {Pallets, requirePalletsOrSkip} from '../util/index.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from './util/index.js';
 import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';
 import type {IKeyringPair} from '@polkadot/types/types';
modifiedjs-packages/tests/eth/scheduling.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/scheduling.test.ts
+++ b/js-packages/tests/eth/scheduling.test.ts
@@ -17,7 +17,7 @@
 import {expect} from 'chai';
 import {itSchedEth} from './util/index.js';
 import {EthUniqueHelper} from './util/playgrounds/unique.dev.js';
-import {Pallets, usingPlaygrounds} from '../util/index.js';
+import {Pallets, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 describe('Scheduing EVM smart contracts', () => {
 
modifiedjs-packages/tests/eth/sponsoring.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/sponsoring.test.ts
+++ b/js-packages/tests/eth/sponsoring.test.ts
@@ -16,7 +16,7 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {itEth, expect, SponsoringMode} from './util/index.js';
-import {usingPlaygrounds} from '../util/index.js';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
 
 describe('EVM sponsoring', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/eth/tokenProperties.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/tokenProperties.test.ts
+++ b/js-packages/tests/eth/tokenProperties.test.ts
@@ -17,9 +17,9 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {Contract} from 'web3-eth-contract';
 import {itEth, usingEthPlaygrounds, expect} from './util/index.js';
-import type {ITokenPropertyPermission} from '@unique/playgrounds/types.js';
-import {Pallets} from '../util/index.js';
-import {UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection} from '@unique/playgrounds/unique.js';
+import type {ITokenPropertyPermission} from '@unique-nft/playgrounds/types.js';
+import {Pallets} from '@unique/test-utils/util.js';
+import {UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection} from '@unique-nft/playgrounds/unique.js';
 import {CreateCollectionData, TokenPermissionField} from './util/playgrounds/types.js';
 
 describe('EVM token properties', () => {
modifiedjs-packages/tests/eth/tokens/callMethodsERC20.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/tokens/callMethodsERC20.test.ts
+++ b/js-packages/tests/eth/tokens/callMethodsERC20.test.ts
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {Pallets, requirePalletsOrSkip} from '../../util/index.js';
+import {Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from '../util/index.js';
 import type {IKeyringPair} from '@polkadot/types/types';
 import {CreateCollectionData} from '../util/playgrounds/types.js';
modifiedjs-packages/tests/eth/tokens/callMethodsERC721.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/tokens/callMethodsERC721.test.ts
+++ b/js-packages/tests/eth/tokens/callMethodsERC721.test.ts
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {Pallets} from '../../util/index.js';
+import {Pallets} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from '../util/index.js';
 import type {IKeyringPair} from '@polkadot/types/types';
 import {CreateCollectionData} from '../util/playgrounds/types.js';
modifiedjs-packages/tests/eth/tokens/minting.test.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/tokens/minting.test.ts
+++ b/js-packages/tests/eth/tokens/minting.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {Pallets} from '../../util/index.js';
+import {Pallets} from '@unique/test-utils/util.js';
 import {expect, itEth, usingEthPlaygrounds} from '../util/index.js';
 import {CreateCollectionData} from '../util/playgrounds/types.js';
 
modifiedjs-packages/tests/eth/util/index.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/util/index.ts
+++ b/js-packages/tests/eth/util/index.ts
@@ -7,14 +7,13 @@
 import config from '../../config.js';
 
 import {EthUniqueHelper} from './playgrounds/unique.dev.js';
-import {SilentLogger, SilentConsole} from '@unique/playgrounds/unique.dev.js';
-import {makeNames} from '../../util/index.js';
-import type {SchedKind} from '../../util/index.js';
+import {SilentLogger, SilentConsole} from '@unique/test-utils';
+import type {SchedKind} from '@unique/test-utils/util.js';
 
 import chai from 'chai';
 import chaiAsPromised from 'chai-as-promised';
 import chaiLike from 'chai-like';
-import {getTestSeed, MINIMUM_DONOR_FUND, requirePalletsOrSkip} from '../../util/index.js';
+import {getTestSeed, MINIMUM_DONOR_FUND, requirePalletsOrSkip, makeNames} from '@unique/test-utils/util.js';
 
 chai.use(chaiAsPromised);
 chai.use(chaiLike);
modifiedjs-packages/tests/eth/util/playgrounds/types.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/util/playgrounds/types.ts
+++ b/js-packages/tests/eth/util/playgrounds/types.ts
@@ -1,5 +1,5 @@
-import {CollectionFlag} from '@unique/playgrounds/types.js';
-import type {TCollectionMode} from '@unique/playgrounds/types.js';
+import {CollectionFlag} from '@unique-nft/playgrounds/types.js';
+import type {TCollectionMode} from '@unique-nft/playgrounds/types.js';
 
 export interface ContractImports {
   solPath: string;
modifiedjs-packages/tests/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/js-packages/tests/eth/util/playgrounds/unique.dev.ts
+++ b/js-packages/tests/eth/util/playgrounds/unique.dev.ts
@@ -15,7 +15,7 @@
 import {evmToAddress} from '@polkadot/util-crypto';
 import type {IKeyringPair} from '@polkadot/types/types';
 
-import {ArrangeGroup, DevUniqueHelper} from '@unique/playgrounds/unique.dev.js';
+import {ArrangeGroup, DevUniqueHelper} from '@unique/test-utils/index.js';
 
 import type {ContractImports, CompiledContract, CrossAddress, NormalizedEvent, EthProperty} from './types.js';
 import {CollectionMode, CreateCollectionData} from './types.js';
@@ -32,7 +32,7 @@
 import refungibleTokenAbi from '../../abi/reFungibleToken.json' assert {type: 'json'};
 import refungibleTokenDeprecatedAbi from '../../abi/reFungibleTokenDeprecated.json' assert {type: 'json'};
 import contractHelpersAbi from '../../abi/contractHelpers.json' assert {type: 'json'};
-import type {ICrossAccountId, TEthereumAccount, TCollectionMode} from '@unique/playgrounds/types.js';
+import type {ICrossAccountId, TEthereumAccount, TCollectionMode} from '@unique-nft/playgrounds/types.js';
 
 class EthGroupBase {
   helper: EthUniqueHelper;
modifiedjs-packages/tests/fungible.test.tsdiffbeforeafterboth
--- a/js-packages/tests/fungible.test.ts
+++ b/js-packages/tests/fungible.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect, requirePalletsOrSkip, Pallets} from './util/index.js';
+import {itSub, usingPlaygrounds, expect, requirePalletsOrSkip, Pallets} from '@unique/test-utils/util.js';
 
 const U128_MAX = (1n << 128n) - 1n;
 
modifiedjs-packages/tests/getPropertiesRpc.test.tsdiffbeforeafterboth
--- a/js-packages/tests/getPropertiesRpc.test.ts
+++ b/js-packages/tests/getPropertiesRpc.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
-import {UniqueHelper, UniqueNFTCollection} from '@unique/playgrounds/unique.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {UniqueHelper, UniqueNFTCollection} from '@unique-nft/playgrounds/unique.js';
 
 const collectionProps = [
   {key: 'col-0', value: 'col-0-value'},
modifiedjs-packages/tests/inflation.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/inflation.seqtest.ts
+++ b/js-packages/tests/inflation.seqtest.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from './util/index.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 const TREASURY = '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z';
 
modifiedjs-packages/tests/limits.test.tsdiffbeforeafterboth
--- a/js-packages/tests/limits.test.ts
+++ b/js-packages/tests/limits.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util/index.js';
+import {expect, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 describe('Number of tokens per address (NFT)', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/maintenance.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/maintenance.seqtest.ts
+++ b/js-packages/tests/maintenance.seqtest.ts
@@ -16,7 +16,7 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {ApiPromise} from '@polkadot/api';
-import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util/index.js';
+import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from '@unique/test-utils/util.js';
 import {itEth} from './eth/util/index.js';
 import {main as correctState} from './migrations/correctStateAfterMaintenance.js';
 import type {PalletBalancesIdAmount} from '@polkadot/types/lookup';
modifiedjs-packages/tests/migrations/942057-appPromotion/index.tsdiffbeforeafterboth
--- a/js-packages/tests/migrations/942057-appPromotion/index.ts
+++ b/js-packages/tests/migrations/942057-appPromotion/index.ts
@@ -1,4 +1,4 @@
-import type {Migration} from '../../util/frankensteinMigrate.js';
+import type {Migration} from '../index.js';
 import {collectData} from './collectData.js';
 import {migrateLockedToFreeze} from './lockedToFreeze.js';
 
modifiedjs-packages/tests/migrations/942057-appPromotion/lockedToFreeze.tsdiffbeforeafterboth
--- a/js-packages/tests/migrations/942057-appPromotion/lockedToFreeze.ts
+++ b/js-packages/tests/migrations/942057-appPromotion/lockedToFreeze.ts
@@ -1,6 +1,6 @@
 // import { usingApi, privateKey, onlySign } from "./../../load/lib";
 import * as fs from 'fs';
-import {usingPlaygrounds} from '../../util/index.js';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
 import path, {dirname} from 'path';
 import {isInteger, parse} from 'lossless-json';
 import {fileURLToPath} from 'url';
modifiedjs-packages/tests/migrations/correctStateAfterMaintenance.tsdiffbeforeafterboth
--- a/js-packages/tests/migrations/correctStateAfterMaintenance.ts
+++ b/js-packages/tests/migrations/correctStateAfterMaintenance.ts
@@ -1,5 +1,5 @@
 import config from '../config.js';
-import {usingPlaygrounds} from '../util/index.js';
+import {usingPlaygrounds} from '@unique/test-utils/util.js';
 import type {u32} from '@polkadot/types-codec';
 
 const WS_ENDPOINT = config.substrateUrl;
addedjs-packages/tests/migrations/index.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/tests/migrations/index.ts
@@ -0,0 +1,9 @@
+import {migration as locksToFreezesMigration} from './942057-appPromotion/index.js';
+export interface Migration {
+  before: () => Promise<void>,
+  after: () => Promise<void>,
+}
+
+export const migrations: {[key: string]: Migration} = {
+  'v942057': locksToFreezesMigration,
+};
\ No newline at end of file
modifiedjs-packages/tests/nativeFungible.test.tsdiffbeforeafterboth
--- a/js-packages/tests/nativeFungible.test.ts
+++ b/js-packages/tests/nativeFungible.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from './util/index.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 describe('Native fungible', () => {
   let root: IKeyringPair;
modifiedjs-packages/tests/nextSponsoring.test.tsdiffbeforeafterboth
--- a/js-packages/tests/nextSponsoring.test.ts
+++ b/js-packages/tests/nextSponsoring.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, usingPlaygrounds} from './util/index.js';
+import {expect, itSub, Pallets, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 const SPONSORING_TIMEOUT = 5;
 
modifiedjs-packages/tests/package.jsondiffbeforeafterboth
--- a/js-packages/tests/package.json
+++ b/js-packages/tests/package.json
@@ -13,9 +13,9 @@
         "mocha": "^10.1.0"
     },
     "scripts": {
-        "setup": "yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm ./util/globalSetup.ts",
-        "setIdentities": "yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm ./util/identitySetter.ts",
-        "checkRelayIdentities": "yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm ./util/relayIdentitiesChecker.ts",
+        "setup": "yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm ../test-utils/globalSetup.ts",
+        "setIdentities": "yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm ../scripts/identitySetter.ts",
+        "checkRelayIdentities": "yarn node --no-warnings=ExperimentalWarning --loader ts-node/esm ../scripts/relayIdentitiesChecker.ts",
         "_test": "yarn setup && yarn mocha --timeout 9999999 --loader=ts-node/esm.mjs",
         "_testParallel": "yarn setup && yarn mocha --timeout 9999999 --parallel --loader=ts-node/esm.mjs",
         "test": "yarn _test './**/*.*test.ts'",
modifiedjs-packages/tests/pallet-presence.test.tsdiffbeforeafterboth
--- a/js-packages/tests/pallet-presence.test.ts
+++ b/js-packages/tests/pallet-presence.test.ts
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 // Pallets that must always be present
 const requiredPallets = [
modifiedjs-packages/tests/performance.seq.test.tsdiffbeforeafterboth
--- a/js-packages/tests/performance.seq.test.ts
+++ b/js-packages/tests/performance.seq.test.ts
@@ -16,9 +16,9 @@
 
 import {ApiPromise} from '@polkadot/api';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from './util/index.js';
-import type {ICrossAccountId, IProperty} from '@unique/playgrounds/types.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
+import type {ICrossAccountId, IProperty} from '@unique-nft/playgrounds/types.js';
+import {UniqueHelper} from '@unique-nft/playgrounds/unique.js';
 
 describe('Performace tests', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/refungible.test.tsdiffbeforeafterboth
--- a/js-packages/tests/refungible.test.ts
+++ b/js-packages/tests/refungible.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from './util/index.js';
+import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 const MAX_REFUNGIBLE_PIECES = 1_000_000_000_000_000_000_000n;
 
modifiedjs-packages/tests/removeCollectionAdmin.test.tsdiffbeforeafterboth
--- a/js-packages/tests/removeCollectionAdmin.test.ts
+++ b/js-packages/tests/removeCollectionAdmin.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test removeCollectionAdmin(collection_id, account_id):', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/removeCollectionSponsor.test.tsdiffbeforeafterboth
--- a/js-packages/tests/removeCollectionSponsor.test.ts
+++ b/js-packages/tests/removeCollectionSponsor.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('integration test: ext. removeCollectionSponsor():', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/rpc.test.tsdiffbeforeafterboth
--- a/js-packages/tests/rpc.test.ts
+++ b/js-packages/tests/rpc.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect} from './util/index.js';
-import {ICrossAccountId} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, itSub, expect} from '@unique/test-utils/util.js';
+import {ICrossAccountId} from '@unique-nft/playgrounds/types.js';
 
 describe('integration test: RPC methods', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/scheduler.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/scheduler.seqtest.ts
+++ b/js-packages/tests/scheduler.seqtest.ts
@@ -14,9 +14,9 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util/index.js';
+import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from '@unique/test-utils/util.js';
 import type {IKeyringPair} from '@polkadot/types/types';
-import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';
+import {DevUniqueHelper, Event} from '@unique/test-utils';
 
 describe('Scheduling token and balance transfers', () => {
   let superuser: IKeyringPair;
modifiedjs-packages/tests/setCollectionLimits.test.tsdiffbeforeafterboth
--- a/js-packages/tests/setCollectionLimits.test.ts
+++ b/js-packages/tests/setCollectionLimits.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 const accountTokenOwnershipLimit = 0;
 const sponsoredDataSize = 0;
modifiedjs-packages/tests/setCollectionSponsor.test.tsdiffbeforeafterboth
--- a/js-packages/tests/setCollectionSponsor.test.ts
+++ b/js-packages/tests/setCollectionSponsor.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect, Pallets} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect, Pallets} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('integration test: ext. setCollectionSponsor():', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/setPermissions.test.tsdiffbeforeafterboth
--- a/js-packages/tests/setPermissions.test.ts
+++ b/js-packages/tests/setPermissions.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test: Set Permissions', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/appPromotion/appPromotion.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/appPromotion/appPromotion.seqtest.ts
+++ b/js-packages/tests/sub/appPromotion/appPromotion.seqtest.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip} from '../../util/index.js';
+import {itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 import {expect} from '../../eth/util/index.js';
 
 let superuser: IKeyringPair;
modifiedjs-packages/tests/sub/appPromotion/appPromotion.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/appPromotion/appPromotion.test.ts
+++ b/js-packages/tests/sub/appPromotion/appPromotion.test.ts
@@ -17,8 +17,8 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {
   itSub, usingPlaygrounds, Pallets, requirePalletsOrSkip, LOCKING_PERIOD, UNLOCKING_PERIOD,
-} from '../../util/index.js';
-import {DevUniqueHelper} from '@unique/playgrounds/unique.dev.js';
+} from '@unique/test-utils/util.js';
+import {DevUniqueHelper} from '@unique/test-utils';
 import {itEth, expect, SponsoringMode} from '../../eth/util/index.js';
 
 let donor: IKeyringPair;
modifiedjs-packages/tests/sub/check-event/burnItemEvent.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/check-event/burnItemEvent.test.ts
+++ b/js-packages/tests/sub/check-event/burnItemEvent.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from '../../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, expect, itSub} from '@unique/test-utils/util.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 
 
 describe('Burn Item event ', () => {
modifiedjs-packages/tests/sub/check-event/createCollectionEvent.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/check-event/createCollectionEvent.test.ts
+++ b/js-packages/tests/sub/check-event/createCollectionEvent.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect} from '../../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, itSub, expect} from '@unique/test-utils/util.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 
 describe('Create collection event ', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/check-event/createItemEvent.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/check-event/createItemEvent.test.ts
+++ b/js-packages/tests/sub/check-event/createItemEvent.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from '../../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 
 describe('Create Item event ', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/check-event/createMultipleItemsEvent.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/check-event/createMultipleItemsEvent.test.ts
+++ b/js-packages/tests/sub/check-event/createMultipleItemsEvent.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect} from '../../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, itSub, expect} from '@unique/test-utils/util.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 
 describe('Create Multiple Items Event event ', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/check-event/destroyCollectionEvent.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/check-event/destroyCollectionEvent.test.ts
+++ b/js-packages/tests/sub/check-event/destroyCollectionEvent.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from '../../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 
 describe('Destroy collection event ', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/check-event/transferEvent.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/check-event/transferEvent.test.ts
+++ b/js-packages/tests/sub/check-event/transferEvent.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from '../../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, expect, itSub} from '@unique/test-utils/util.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 
 describe('Transfer event ', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/check-event/transferFromEvent.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/check-event/transferFromEvent.test.ts
+++ b/js-packages/tests/sub/check-event/transferFromEvent.test.ts
@@ -16,8 +16,8 @@
 
 // https://unique-network.readthedocs.io/en/latest/jsapi.html#setchainlimits
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub} from '../../util/index.js';
-import type {IEvent} from '@unique/playgrounds/types.js';
+import {usingPlaygrounds, expect, itSub} from '@unique/test-utils/util.js';
+import type {IEvent} from '@unique-nft/playgrounds/types.js';
 
 describe('Transfer event ', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/collator-selection/collatorSelection.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/collator-selection/collatorSelection.seqtest.ts
+++ b/js-packages/tests/sub/collator-selection/collatorSelection.seqtest.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../../util/index.js';
+import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
 
 async function nodeAddress(name: string) {
   // eslint-disable-next-line require-await
modifiedjs-packages/tests/sub/collator-selection/identity.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/collator-selection/identity.seqtest.ts
+++ b/js-packages/tests/sub/collator-selection/identity.seqtest.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '../../util/index.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
+import {usingPlaygrounds, expect, itSub, Pallets, requirePalletsOrSkip} from '@unique/test-utils/util.js';
+import {UniqueHelper} from '@unique-nft/playgrounds/unique.js';
 
 async function getIdentities(helper: UniqueHelper) {
   const identities: [string, any][] = [];
modifiedjs-packages/tests/sub/governance/council.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/council.test.ts
+++ b/js-packages/tests/sub/governance/council.test.ts
@@ -1,7 +1,7 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
+import {Event} from '@unique/test-utils';
 import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin} from './util.js';
 import type {ICounselors} from './util.js';
 
modifiedjs-packages/tests/sub/governance/democracy.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/democracy.test.ts
+++ b/js-packages/tests/sub/governance/democracy.test.ts
@@ -1,7 +1,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
 import {clearFellowship, democracyLaunchPeriod, democracyTrackMinRank, dummyProposalCall, fellowshipConfirmPeriod, fellowshipMinEnactPeriod, fellowshipPreparePeriod, fellowshipPropositionOrigin, initFellowship, voteUnanimouslyInFellowship} from './util.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
+import {Event} from '@unique/test-utils';
 
 describeGov('Governance: Democracy tests', () => {
   let regularUser: IKeyringPair;
modifiedjs-packages/tests/sub/governance/electsudo.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/electsudo.test.ts
+++ b/js-packages/tests/sub/governance/electsudo.test.ts
@@ -1,6 +1,6 @@
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
+import {Event} from '@unique/test-utils';
 import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, clearCouncil, clearTechComm, initTechComm, ITechComms} from './util.js';
 import type {ICounselors} from './util.js';
 
modifiedjs-packages/tests/sub/governance/fellowship.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/fellowship.test.ts
+++ b/js-packages/tests/sub/governance/fellowship.test.ts
@@ -1,6 +1,6 @@
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
-import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
+import {DevUniqueHelper, Event} from '@unique/test-utils';
 import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, voteUnanimouslyInFellowship, democracyTrackMinRank, fellowshipPreparePeriod, fellowshipConfirmPeriod, fellowshipMinEnactPeriod, democracyTrackId, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js';
 import type {ICounselors, ITechComms} from './util.js';
 
modifiedjs-packages/tests/sub/governance/init.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/init.test.ts
+++ b/js-packages/tests/sub/governance/init.test.ts
@@ -1,6 +1,6 @@
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
+import {Event} from '@unique/test-utils';
 import {democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, clearCouncil, clearTechComm, clearFellowship} from './util.js';
 import type {ICounselors, ITechComms} from './util.js';
 
modifiedjs-packages/tests/sub/governance/technicalCommittee.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/technicalCommittee.test.ts
+++ b/js-packages/tests/sub/governance/technicalCommittee.test.ts
@@ -1,6 +1,6 @@
 import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '../../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
+import {Event} from '@unique/test-utils';
 import {initCouncil, democracyLaunchPeriod, democracyFastTrackVotingPeriod, clearCouncil, clearTechComm, clearFellowship, defaultEnactmentMoment, dummyProposal, dummyProposalCall, fellowshipPropositionOrigin, initFellowship, initTechComm, hardResetFellowshipReferenda, hardResetDemocracy, hardResetGovScheduler} from './util.js';
 import type {ITechComms} from './util.js';
 
modifiedjs-packages/tests/sub/governance/util.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/util.ts
+++ b/js-packages/tests/sub/governance/util.ts
@@ -1,9 +1,9 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 import {xxhashAsHex} from '@polkadot/util-crypto';
 import type {u32} from '@polkadot/types-codec';
-import {usingPlaygrounds, expect} from '../../util/index.js';
-import {UniqueHelper} from '@unique/playgrounds/unique.js';
-import {DevUniqueHelper} from '@unique/playgrounds/unique.dev.js';
+import {usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {UniqueHelper} from '@unique-nft/playgrounds/unique.js';
+import {DevUniqueHelper} from '@unique/test-utils';
 
 export const democracyLaunchPeriod = 35;
 export const democracyVotingPeriod = 35;
modifiedjs-packages/tests/sub/nesting/admin.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/admin.test.ts
+++ b/js-packages/tests/sub/nesting/admin.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from '../../util/index.js';
-import {CrossAccountId} from '@unique/playgrounds/unique.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {CrossAccountId} from '@unique-nft/playgrounds/unique.js';
 
 describe('Nesting by collection admin', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/collectionProperties.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/collectionProperties.seqtest.ts
+++ b/js-packages/tests/sub/nesting/collectionProperties.seqtest.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../../util/index.js';
+import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '@unique/test-utils/util.js';
 
 describe('Integration Test: Collection Properties with sudo', () => {
   let superuser: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/collectionProperties.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/collectionProperties.test.ts
+++ b/js-packages/tests/sub/nesting/collectionProperties.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../../util/index.js';
+import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '@unique/test-utils/util.js';
 
 describe('Integration Test: Collection Properties', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/common.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/common.test.ts
+++ b/js-packages/tests/sub/nesting/common.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, usingPlaygrounds} from '../../util/index.js';
-import {CrossAccountId, UniqueNFTCollection, UniqueRFTCollection} from '@unique/playgrounds/unique.js';
+import {expect, itSub, Pallets, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {CrossAccountId, UniqueNFTCollection, UniqueRFTCollection} from '@unique-nft/playgrounds/unique.js';
 
 let alice: IKeyringPair;
 let bob: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/e2e.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/e2e.test.ts
+++ b/js-packages/tests/sub/nesting/e2e.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from '../../util/index.js';
-import {CrossAccountId} from '@unique/playgrounds/unique.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {CrossAccountId} from '@unique-nft/playgrounds/unique.js';
 
 describe('Composite nesting tests', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/graphs.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/graphs.test.ts
+++ b/js-packages/tests/sub/nesting/graphs.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from '../../util/index.js';
-import {UniqueHelper, UniqueNFTCollection, UniqueNFToken} from '@unique/playgrounds/unique.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {UniqueHelper, UniqueNFTCollection, UniqueNFToken} from '@unique-nft/playgrounds/unique.js';
 
 /**
  * ```dot
modifiedjs-packages/tests/sub/nesting/nesting.negative.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/nesting.negative.test.ts
+++ b/js-packages/tests/sub/nesting/nesting.negative.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, usingPlaygrounds} from '../../util/index.js';
-import {UniqueFTCollection, UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection, UniqueRFToken} from '@unique/playgrounds/unique.js';
+import {expect, itSub, Pallets, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {UniqueFTCollection, UniqueNFTCollection, UniqueNFToken, UniqueRFTCollection, UniqueRFToken} from '@unique-nft/playgrounds/unique.js';
 import {itEth} from '../../eth/util/index.js';
 
 let alice: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/propertyPermissions.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/propertyPermissions.test.ts
+++ b/js-packages/tests/sub/nesting/propertyPermissions.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect} from '../../util/index.js';
-import {UniqueNFTCollection, UniqueRFTCollection} from '@unique/playgrounds/unique.js';
+import {itSub, Pallets, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {UniqueNFTCollection, UniqueRFTCollection} from '@unique-nft/playgrounds/unique.js';
 
 describe('Integration Test: Access Rights to Token Properties', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/refungible.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/refungible.test.ts
+++ b/js-packages/tests/sub/nesting/refungible.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, usingPlaygrounds} from '../../util/index.js';
+import {expect, itSub, Pallets, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 describe('ReFungible-specific nesting tests', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/tokenProperties.seqtest.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/tokenProperties.seqtest.ts
+++ b/js-packages/tests/sub/nesting/tokenProperties.seqtest.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '../../util/index.js';
+import {itSub, Pallets, usingPlaygrounds, expect, requirePalletsOrSkip, sizeOfProperty} from '@unique/test-utils/util.js';
 
 describe('Integration Test: Token Properties with sudo', () => {
   let superuser: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/tokenProperties.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/tokenProperties.test.ts
+++ b/js-packages/tests/sub/nesting/tokenProperties.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect, sizeOfProperty} from '../../util/index.js';
-import {UniqueHelper, UniqueNFToken, UniqueRFToken} from '@unique/playgrounds/unique.js';
+import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect, sizeOfProperty} from '@unique/test-utils/util.js';
+import {UniqueHelper, UniqueNFToken, UniqueRFToken} from '@unique-nft/playgrounds/unique.js';
 
 describe('Integration Test: Token Properties', () => {
   let alice: IKeyringPair; // collection owner
modifiedjs-packages/tests/sub/nesting/unnest.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/unnest.test.ts
+++ b/js-packages/tests/sub/nesting/unnest.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, usingPlaygrounds} from '../../util/index.js';
-import {CrossAccountId, UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '@unique/playgrounds/unique.js';
+import {expect, itSub, Pallets, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {CrossAccountId, UniqueFTCollection, UniqueNFToken, UniqueRFToken} from '@unique-nft/playgrounds/unique.js';
 
 describe('Integration Test: Unnesting', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/nesting/unnesting.negative.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/nesting/unnesting.negative.test.ts
+++ b/js-packages/tests/sub/nesting/unnesting.negative.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, usingPlaygrounds} from '../../util/index.js';
-import {CrossAccountId} from '@unique/playgrounds/unique.js';
+import {expect, itSub, usingPlaygrounds} from '@unique/test-utils/util.js';
+import {CrossAccountId} from '@unique-nft/playgrounds/unique.js';
 
 describe('Negative Test: Unnesting', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/refungible/burn.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/refungible/burn.test.ts
+++ b/js-packages/tests/sub/refungible/burn.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../../util/index.js';
+import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 describe('Refungible: burn', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/sub/refungible/nesting.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/refungible/nesting.test.ts
+++ b/js-packages/tests/sub/refungible/nesting.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {expect, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from '../../util/index.js';
+import {expect, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from '@unique/test-utils/util.js';
 
 describe('Refungible nesting', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/sub/refungible/repartition.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/refungible/repartition.test.ts
+++ b/js-packages/tests/sub/refungible/repartition.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../../util/index.js';
+import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 describe('integration test: Refungible functionality:', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/sub/refungible/transfer.test.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/refungible/transfer.test.ts
+++ b/js-packages/tests/sub/refungible/transfer.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '../../util/index.js';
+import {itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 describe('Refungible transfer tests', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/transfer.test.tsdiffbeforeafterboth
--- a/js-packages/tests/transfer.test.ts
+++ b/js-packages/tests/transfer.test.ts
@@ -16,8 +16,8 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import {itEth, usingEthPlaygrounds} from './eth/util/index.js';
-import {itSub, Pallets, usingPlaygrounds, expect} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, Pallets, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test Transfer(recipient, collection_id, item_id, value)', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/transferFrom.test.tsdiffbeforeafterboth
--- a/js-packages/tests/transferFrom.test.ts
+++ b/js-packages/tests/transferFrom.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, Pallets, usingPlaygrounds, expect} from './util/index.js';
-import {NON_EXISTENT_COLLECTION_ID} from '@unique/playgrounds/types.js';
+import {itSub, Pallets, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
+import {NON_EXISTENT_COLLECTION_ID} from '@unique-nft/playgrounds/types.js';
 
 describe('Integration Test transferFrom(from, recipient, collection_id, item_id, value):', () => {
   let alice: IKeyringPair;
modifiedjs-packages/tests/tx-version-presence.test.tsdiffbeforeafterboth
--- a/js-packages/tests/tx-version-presence.test.ts
+++ b/js-packages/tests/tx-version-presence.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {Metadata} from '@polkadot/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 let metadata: Metadata;
 
deletedjs-packages/tests/util/authorizeEnactUpgrade.tsdiffbeforeafterboth
--- a/js-packages/tests/util/authorizeEnactUpgrade.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import {readFile} from 'fs/promises';
-import {u8aToHex} from '@polkadot/util';
-import {usingPlaygrounds} from './index.js';
-import {blake2AsHex} from '@polkadot/util-crypto';
-
-
-const codePath = process.argv[2];
-if(!codePath) throw new Error('missing code path argument');
-
-const code = await readFile(codePath);
-
-await usingPlaygrounds(async (helper, privateKey) => {
-  const alice = await privateKey('//Alice');
-  const hex = blake2AsHex(code);
-  await helper.getSudo().executeExtrinsic(alice, 'api.tx.parachainSystem.authorizeUpgrade', [hex, true]);
-  await helper.getSudo().executeExtrinsicUncheckedWeight(alice, 'api.tx.parachainSystem.enactAuthorizedUpgrade', [u8aToHex(code)]);
-});
-// We miss disconnect/unref somewhere.
-process.exit(0);
deletedjs-packages/tests/util/createHrmp.tsdiffbeforeafterboth
--- a/js-packages/tests/util/createHrmp.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import {usingPlaygrounds} from './index.js';
-import config from '../config.js';
-
-const profile = process.argv[2];
-if(!profile) throw new Error('missing profile/relay argument');
-
-await usingPlaygrounds(async (helper, privateKey) => {
-  const bidirOpen = async (a: number, b: number) => {
-    console.log(`Opening ${a} <=> ${b}`);
-    await helper.getSudo().executeExtrinsic(alice, 'api.tx.hrmp.forceOpenHrmpChannel', [a, b, 8, 512]);
-    await helper.getSudo().executeExtrinsic(alice, 'api.tx.hrmp.forceOpenHrmpChannel', [b, a, 8, 512]);
-  };
-  const alice = await privateKey('//Alice');
-  switch (profile) {
-    case 'opal':
-      await bidirOpen(1001, 1002);
-      break;
-    case 'quartz':
-      await bidirOpen(1001, 1002);
-      await bidirOpen(1001, 1003);
-      await bidirOpen(1001, 1004);
-      await bidirOpen(1001, 1005);
-      break;
-    case 'unique':
-      await bidirOpen(1001, 1002);
-      await bidirOpen(1001, 1003);
-      await bidirOpen(1001, 1004);
-      await bidirOpen(1001, 1005);
-      await bidirOpen(1001, 1006);
-      await bidirOpen(1001, 1007);
-      break;
-    default:
-      throw new Error(`unknown hrmp config profile: ${profile}`);
-  }
-}, config.relayUrl);
-// We miss disconnect/unref somewhere.
-process.exit(0);
deletedjs-packages/tests/util/frankensteinMigrate.tsdiffbeforeafterboth
--- a/js-packages/tests/util/frankensteinMigrate.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {migration as locksToFreezesMigration} from '../migrations/942057-appPromotion/index.js';
-export interface Migration {
-  before: () => Promise<void>,
-  after: () => Promise<void>,
-}
-
-export const migrations: {[key: string]: Migration} = {
-  'v942057': locksToFreezesMigration,
-};
deletedjs-packages/tests/util/globalSetup.tsdiffbeforeafterboth
--- a/js-packages/tests/util/globalSetup.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
-// SPDX-License-Identifier: Apache-2.0
-
-import {
-  usingPlaygrounds, Pallets, DONOR_FUNDING, MINIMUM_DONOR_FUND, LOCKING_PERIOD, UNLOCKING_PERIOD, makeNames,
-} from './index.js';
-import * as path from 'path';
-import {promises as fs} from 'fs';
-
-const {dirname} = makeNames(import.meta.url);
-
-// This function should be called before running test suites.
-const globalSetup = async (): Promise<void> => {
-  await usingPlaygrounds(async (helper, privateKey) => {
-    try {
-      // 1. Wait node producing blocks
-      await helper.wait.newBlocks(1, 600_000);
-
-      // 2. Create donors for test files
-      await fundFilenamesWithRetries(3)
-        .then((result) => {
-          if(!result) throw Error('Some problems with fundFilenamesWithRetries');
-        });
-
-      // 3. Configure App Promotion
-      const missingPallets = helper.fetchMissingPalletNames([Pallets.AppPromotion]);
-      if(missingPallets.length === 0) {
-        const superuser = await privateKey('//Alice');
-        const palletAddress = helper.arrange.calculatePalletAddress('appstake');
-        const palletAdmin = await privateKey('//PromotionAdmin');
-        const api = helper.getApi();
-        await helper.signTransaction(superuser, api.tx.sudo.sudo(api.tx.appPromotion.setAdminAddress({Substrate: palletAdmin.address})));
-        const nominal = helper.balance.getOneTokenNominal();
-        await helper.balance.transferToSubstrate(superuser, palletAdmin.address, 10000n * nominal);
-        await helper.balance.transferToSubstrate(superuser, palletAddress, 10000n * nominal);
-        await helper.executeExtrinsic(superuser, 'api.tx.sudo.sudo', [api.tx.configuration
-          .setAppPromotionConfigurationOverride({
-            recalculationInterval: LOCKING_PERIOD,
-            pendingInterval: UNLOCKING_PERIOD})], true);
-      }
-    } catch (error) {
-      console.error(error);
-      throw Error('Error during globalSetup');
-    }
-  });
-};
-
-async function getFiles(rootPath: string): Promise<string[]> {
-  const files = await fs.readdir(rootPath, {withFileTypes: true});
-  const filenames: string[] = [];
-  for(const entry of files) {
-    const res = path.resolve(rootPath, entry.name);
-    if(entry.isDirectory()) {
-      filenames.push(...await getFiles(res));
-    } else {
-      filenames.push(res);
-    }
-  }
-  return filenames;
-}
-
-const fundFilenames = async () => {
-  await usingPlaygrounds(async (helper, privateKey) => {
-    const oneToken = helper.balance.getOneTokenNominal();
-    const alice = await privateKey('//Alice');
-    const nonce = await helper.chain.getNonce(alice.address);
-    const filenames = await getFiles(path.resolve(dirname, '..'));
-
-    // batching is actually undesireable, it takes away the time while all the transactions actually succeed
-    const batchSize = 300;
-    let balanceGrantedCounter = 0;
-    for(let b = 0; b < filenames.length; b += batchSize) {
-      const tx: Promise<boolean>[] = [];
-      let batchBalanceGrantedCounter = 0;
-      for(let i = 0; batchBalanceGrantedCounter < batchSize && b + i < filenames.length; i++) {
-        const f = filenames[b + i];
-        if(!f.endsWith('.test.ts') && !f.endsWith('seqtest.ts') || f.includes('.outdated')) continue;
-        const account = await privateKey({filename: f, ignoreFundsPresence: true});
-        const aliceBalance = await helper.balance.getSubstrate(account.address);
-
-        if(aliceBalance < MINIMUM_DONOR_FUND * oneToken) {
-          tx.push(helper.executeExtrinsic(
-            alice,
-            'api.tx.balances.transferKeepAlive',
-            [account.address, DONOR_FUNDING * oneToken],
-            true,
-            {nonce: nonce + balanceGrantedCounter++},
-          ).then(() => true).catch(() => {console.error(`Transaction to ${path.basename(f)} registered as failed. Strange.`); return false;}));
-          batchBalanceGrantedCounter++;
-        }
-      }
-
-      if(tx.length > 0) {
-        console.log(`Granting funds to ${batchBalanceGrantedCounter} filename accounts.`);
-        const result = await Promise.all(tx);
-        if(result && result.lastIndexOf(false) > -1) throw new Error('The transactions actually probably succeeded, should check the balances.');
-      }
-    }
-
-    if(balanceGrantedCounter == 0) console.log('No account needs additional funding.');
-  });
-};
-
-const fundFilenamesWithRetries = (retriesLeft: number): Promise<boolean> => {
-  if(retriesLeft <= 0) return Promise.resolve(false);
-  return fundFilenames()
-    .then(() => Promise.resolve(true))
-    .catch(e => {
-      console.error(e);
-      console.error(`Some transactions might have failed. ${retriesLeft > 1 ? 'Retrying...' : 'Something is wrong.'}\n`);
-      return fundFilenamesWithRetries(--retriesLeft);
-    });
-};
-
-globalSetup().catch(e => {
-  console.error('Setup error');
-  console.error(e);
-  process.exit(1);
-});
deletedjs-packages/tests/util/identitySetter.tsdiffbeforeafterboth
--- a/js-packages/tests/util/identitySetter.ts
+++ /dev/null
@@ -1,229 +0,0 @@
-// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
-// SPDX-License-Identifier: Apache-2.0
-//
-// Pulls identities and sub-identities from a chain and then makes a preimage to later force upload them into another.
-// Only changed or previously non-existent data are inserted.
-//
-// Usage: `yarn setIdentities [relay WS URL] [parachain WS URL] [user key]
-// Example: `yarn setIdentities wss://polkadot-rpc.dwellir.com ws://localhost:9944 escape pattern miracle train sudden cart adapt embark wedding alien lamp mesh`
-
-import {encodeAddress} from '@polkadot/keyring';
-import type {IKeyringPair} from '@polkadot/types/types';
-import {usingPlaygrounds, Pallets} from './index.js';
-import {ChainHelperBase} from '@unique/playgrounds/unique.js';
-
-const relayUrl = process.argv[2] ?? 'ws://localhost:9844';
-const paraUrl = process.argv[3] ?? 'ws://localhost:9944';
-const key = process.argv.length > 4 ? process.argv.slice(4).join(' ') : '//Alice';
-
-export function extractAccountId(key: any): string {
-  return (key as any).toHuman()[0];
-}
-
-export function extractIdentityInfo(value: any): object {
-  const heart = (value as any).unwrap();
-  const identity = heart.toJSON();
-  identity.judgements = heart.judgements.toHuman();
-  return identity;
-}
-
-export function extractIdentity(key: any, value: any): [string, any] {
-  return [extractAccountId(key), extractIdentityInfo(value)];
-}
-
-export async function getIdentities(helper: ChainHelperBase, noneCasePredicate?: (key: any, value: any) => void) {
-  const identities: [string, any][] = [];
-  for(const [key, v] of await helper.getApi().query.identity.identityOf.entries()) {
-    const value = v as any;
-    if(value.isNone) {
-      if(noneCasePredicate) noneCasePredicate(key, value);
-      continue;
-    }
-    identities.push(extractIdentity(key, value));
-  }
-  return identities;
-}
-
-// whether the existing chain data is more important than the coming
-function isCurrentChainDataPriority(helper: ChainHelperBase, currentChainId: number | undefined, relayChainId: number) {
-  if(!currentChainId) return false;
-  // information has come from local chain, and is automatically superior
-  if(currentChainId == helper.chain.getChainProperties().ss58Format) return true;
-  // the lower the id, the more important it is (Polkadot has ss58 prefix = 0, Kusama has ss58 prefix = 2)
-  return currentChainId < relayChainId;
-}
-
-// construct an object with all data necessary for insertion from storage query results
-export function constructSubInfo(identityAccount: string, subQuery: any, supers: any[], ss58?: number): [string, any] {
-  const deposit = subQuery.toJSON()[0];
-  const subIdentities = subQuery.toHuman()[1];
-  subIdentities.map((sub: string) => supers.find((sup: any) => sup[0] === sub));
-
-  return [
-    encodeAddress(identityAccount, ss58), [
-      deposit,
-      subIdentities.map((sub: string): [string, object] | null => {
-        const sup = supers.find((sup: any) => sup[0] === sub);
-        if(!sup) {
-          console.error(`Error: Could not find info on super for \nsub-identity account\t${sub} of \nsuper account \t\t${identityAccount}, skipping.`);
-          return null;
-        }
-        return [encodeAddress(sub, ss58), sup[1].toJSON()[1]];
-      }).filter((x: any) => x),
-    ],
-  ];
-}
-
-export async function getSubs(helper: ChainHelperBase) {
-  return (await helper.getApi().query.identity.subsOf.entries()).map(([key, value]) => [extractAccountId(key), value as any]);
-}
-
-export async function getSupers(helper: ChainHelperBase) {
-  return (await helper.getApi().query.identity.superOf.entries()).map(([key, value]) => [extractAccountId(key), value as any]);
-}
-
-async function uploadPreimage(helper: ChainHelperBase, preimageMaker: IKeyringPair, preimage: string) {
-  try {
-    await helper.executeExtrinsic(preimageMaker, 'api.tx.preimage.notePreimage', [preimage]);
-  } catch (e: any) {
-    if(e.message.includes('AlreadyNoted')) {
-      console.warn('Warning: The same preimage already exists on the chain. Nothing was uploaded.');
-    } else {
-      console.error(e);
-    }
-  }
-}
-
-// The utility for pulling identity and sub-identity data
-const forceInsertIdentities = async (): Promise<void> => {
-  let relaySS58Prefix = 0;
-  const identitiesOnRelay: any[] = [];
-  const subsOnRelay: any[] = [];
-  const identitiesToRemove: string[] = [];
-  await usingPlaygrounds(async helper => {
-    try {
-      relaySS58Prefix = helper.chain.getChainProperties().ss58Format;
-      // iterate over every identity
-      for(const [key, value] of await getIdentities(helper, (key, _value) => identitiesToRemove.push((key as any).toHuman()[0]))) {
-        // if any of the judgements resulted in a good confirmed outcome, keep this identity
-        let knownGood = false, reasonable = false;
-        for(const [_id, judgement] of value.judgements) {
-          if(judgement == 'KnownGood') knownGood = true;
-          if(judgement == 'Reasonable') reasonable = true;
-        }
-        if(!(reasonable || knownGood)) continue;
-        // replace the registrator id with the relay chain's ss58 format
-        value.judgements = [[helper.chain.getChainProperties().ss58Format, knownGood ? 'KnownGood' : 'Reasonable']];
-        identitiesOnRelay.push([key, value]);
-      }
-
-      const sublessIdentities = [...identitiesOnRelay];
-      const supersOfSubs = await getSupers(helper);
-
-      // iterate over every sub-identity
-      for(const [key, value] of await getSubs(helper)) {
-        // only get subs of the identities interesting to us
-        const identityIndex = sublessIdentities.findIndex((x: any) => x[0] == key);
-        if(identityIndex == -1) continue;
-        sublessIdentities.splice(identityIndex, 1);
-        subsOnRelay.push(constructSubInfo(key, value, supersOfSubs));
-      }
-
-      // mark the rest of sub-identities for deletion with empty arrays
-      /*for(const [account, _identity] of sublessIdentities) {
-        subsOnRelay.push([account, ['0', []]]);
-      }*/
-    } catch (error) {
-      console.error(error);
-      throw Error('Error during fetching identities');
-    }
-  }, relayUrl);
-
-  await usingPlaygrounds(async (helper, privateKey) => {
-    if(helper.fetchMissingPalletNames([Pallets.Identity]).length != 0) console.error('pallet-identity is not included in parachain.');
-    if(helper.fetchMissingPalletNames([Pallets.Preimage]).length != 0) console.error('pallet-preimage is not included in parachain.');
-
-    try {
-      const preimageMaker = await privateKey(key);
-      const ss58Format = helper.chain.getChainProperties().ss58Format;
-      const paraIdentities = await getIdentities(helper);
-      const identitiesToAdd: any[] = [];
-      const paraAccountsRegistrators: {[name: string]: number} = {};
-
-      // cross-reference every account for changes
-      for(const [key, value] of identitiesOnRelay) {
-        const encodedKey = encodeAddress(key, ss58Format);
-
-        // only update if the identity info does not exist or is changed
-        const identity = paraIdentities.find(i => i[0] === encodedKey);
-        if(identity) {
-          const registratorId = identity[1].judgements[0][0];
-          paraAccountsRegistrators[encodedKey] = registratorId;
-          if(isCurrentChainDataPriority(helper, registratorId, value.judgements[0][0])
-            || JSON.stringify(value.info) === JSON.stringify(identity[1].info)) {
-            continue;
-          }
-        }
-
-        identitiesToAdd.push([key, value]);
-        // exercise caution - in case we have an identity and the realy doesn't, it might mean one of two things:
-        // 1) it was deleted on the relay;
-        // 2) it is our own identity, we don't want to delete it.
-        // identitiesToRemove.push((key as any).toHuman()[0]);
-      }
-
-      if(identitiesToRemove.length != 0)
-        await uploadPreimage(
-          helper,
-          preimageMaker,
-          helper.constructApiCall('api.tx.identity.forceRemoveIdentities', [identitiesToRemove]).method.toHex(),
-        );
-      if(identitiesToAdd.length != 0)
-        await uploadPreimage(
-          helper,
-          preimageMaker,
-          helper.constructApiCall('api.tx.identity.forceInsertIdentities', [identitiesToAdd]).method.toHex(),
-        );
-
-      console.log(`Tried to push ${identitiesToAdd.length} identities`
-        + ` and found ${identitiesToRemove.length} identities for potential removal.`
-        + ` Currently there are ${(await helper.getApi().query.identity.identityOf.keys()).length} identities on the chain.`);
-
-      // fill sub-identities
-      const paraSubs = await getSubs(helper);
-      const supersOfSubs = await getSupers(helper);
-      const subsToUpdate: any[] = [];
-
-      for(const [key, value] of subsOnRelay) {
-        const encodedKey = encodeAddress(key, ss58Format);
-        const sub = paraSubs.find(i => i[0] === encodedKey);
-        if(sub) {
-          // only update if the sub-identity info does not exist or is changed
-          if(isCurrentChainDataPriority(helper, paraAccountsRegistrators[encodedKey], relaySS58Prefix)
-            || JSON.stringify(value) === JSON.stringify(constructSubInfo(sub[0], sub[1], supersOfSubs)[1])) {
-            continue;
-          }
-        } else if(value[1].length == 0)
-          continue;
-
-        subsToUpdate.push([key, value]);
-      }
-
-      if(subsToUpdate.length != 0)
-        await uploadPreimage(
-          helper,
-          preimageMaker,
-          helper.constructApiCall('api.tx.identity.forceSetSubs', [subsToUpdate]).method.toHex(),
-        );
-
-      console.log(`Also tried to push ${subsToUpdate.length} identities with their sub-identities.`
-        + ` Currently there are ${(await helper.getApi().query.identity.subsOf.keys()).length} identities with subs.`);
-    } catch (error) {
-      console.error(error);
-      throw Error('Error during setting identities');
-    }
-  }, paraUrl);
-};
-
-if(process.argv[1] === module.filename)
-  forceInsertIdentities().catch(() => process.exit(1));
deletedjs-packages/tests/util/index.tsdiffbeforeafterboth
--- a/js-packages/tests/util/index.ts
+++ /dev/null
@@ -1,223 +0,0 @@
-// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
-// SPDX-License-Identifier: Apache-2.0
-
-import * as path from 'path';
-import * as crypto from 'crypto';
-import type {IKeyringPair} from '@polkadot/types/types/interfaces';
-import chai from 'chai';
-import chaiAsPromised from 'chai-as-promised';
-import chaiSubset from 'chai-subset';
-import {Context} from 'mocha';
-import config from '../config.js';
-import {ChainHelperBase} from '@unique/playgrounds/unique.js';
-import type {ILogger} from '@unique/playgrounds/types.js';
-import {DevUniqueHelper, SilentLogger, SilentConsole, DevMoonbeamHelper, DevMoonriverHelper, DevAcalaHelper, DevKaruraHelper, DevRelayHelper, DevWestmintHelper, DevStatemineHelper, DevStatemintHelper, DevAstarHelper, DevShidenHelper, DevPolkadexHelper, DevHydraDxHelper} from '@unique/playgrounds/unique.dev.js';
-import {dirname} from 'path';
-import {fileURLToPath} from 'url';
-
-chai.config.truncateThreshold = 0;
-chai.use(chaiAsPromised);
-chai.use(chaiSubset);
-export const expect = chai.expect;
-
-const getTestHash = (filename: string) => crypto.createHash('md5').update(filename).digest('hex');
-
-export const getTestSeed = (filename: string) => `//Alice+${getTestHash(filename)}`;
-
-async function usingPlaygroundsGeneral<T extends ChainHelperBase, R = void>(
-  helperType: new (logger: ILogger) => T,
-  url: string,
-  code: (helper: T, privateKey: (seed: string | { filename?: string, url?: string, ignoreFundsPresence?: boolean }) => Promise<IKeyringPair>) => Promise<R>,
-): Promise<R> {
-  const silentConsole = new SilentConsole();
-  silentConsole.enable();
-
-  const helper = new helperType(new SilentLogger());
-  let result;
-  try {
-    await helper.connect(url);
-    const ss58Format = helper.chain.getChainProperties().ss58Format;
-    const privateKey = async (seed: string | {filename?: string, url?: string, ignoreFundsPresence?: boolean}) => {
-      if(typeof seed === 'string') {
-        return helper.util.fromSeed(seed, ss58Format);
-      }
-      if(seed.url) {
-        const {filename} = makeNames(seed.url);
-        seed.filename = filename;
-      } else if(seed.filename) {
-        // Pass
-      } else {
-        throw new Error('no url nor filename set');
-      }
-      const actualSeed = getTestSeed(seed.filename);
-      let account = helper.util.fromSeed(actualSeed, ss58Format);
-      // here's to hoping that no
-      if(!seed.ignoreFundsPresence && ((helper as any)['balance'] == undefined || await (helper as any).balance.getSubstrate(account.address) < MINIMUM_DONOR_FUND)) {
-        console.warn(`${path.basename(seed.filename)}: Not enough funds present on the filename account. Using the default one as the donor instead.`);
-        account = helper.util.fromSeed('//Alice', ss58Format);
-      }
-      return account;
-    };
-    result = await code(helper, privateKey);
-  }
-  finally {
-    await helper.disconnect();
-    silentConsole.disable();
-  }
-  return result as any as R;
-}
-
-export const usingPlaygrounds = <R = void>(code: (helper: DevUniqueHelper, privateKey: (seed: string | {filename?: string, url?: string, ignoreFundsPresence?: boolean}) => Promise<IKeyringPair>) => Promise<R>, url: string = config.substrateUrl) => usingPlaygroundsGeneral<DevUniqueHelper, R>(DevUniqueHelper, url, code);
-
-export const usingWestmintPlaygrounds = (url: string, code: (helper: DevWestmintHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevWestmintHelper>(DevWestmintHelper, url, code);
-
-export const usingStateminePlaygrounds = (url: string, code: (helper: DevWestmintHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevStatemineHelper>(DevWestmintHelper, url, code);
-
-export const usingStatemintPlaygrounds = (url: string, code: (helper: DevWestmintHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevStatemintHelper>(DevWestmintHelper, url, code);
-
-export const usingRelayPlaygrounds = (url: string, code: (helper: DevRelayHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevRelayHelper>(DevRelayHelper, url, code);
-
-export const usingAcalaPlaygrounds = (url: string, code: (helper: DevAcalaHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevAcalaHelper>(DevAcalaHelper, url, code);
-
-export const usingKaruraPlaygrounds = (url: string, code: (helper: DevKaruraHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevKaruraHelper>(DevAcalaHelper, url, code);
-
-export const usingMoonbeamPlaygrounds = (url: string, code: (helper: DevMoonbeamHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevMoonbeamHelper>(DevMoonbeamHelper, url, code);
-
-export const usingMoonriverPlaygrounds = (url: string, code: (helper: DevMoonbeamHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevMoonriverHelper>(DevMoonriverHelper, url, code);
-
-export const usingAstarPlaygrounds = (url: string, code: (helper: DevAstarHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevAstarHelper>(DevAstarHelper, url, code);
-
-export const usingShidenPlaygrounds = (url: string, code: (helper: DevShidenHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevShidenHelper>(DevShidenHelper, url, code);
-
-export const usingPolkadexPlaygrounds = (url: string, code: (helper: DevPolkadexHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevPolkadexHelper>(DevPolkadexHelper, url, code);
-
-export const usingHydraDxPlaygrounds = (url: string, code: (helper: DevHydraDxHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevHydraDxHelper>(DevHydraDxHelper, url, code);
-
-export const MINIMUM_DONOR_FUND = 4_000_000n;
-export const DONOR_FUNDING = 4_000_000n;
-
-// App-promotion periods:
-export const LOCKING_PERIOD = 12n; // 12 blocks of relay
-export const UNLOCKING_PERIOD = 6n; // 6 blocks of parachain
-
-// Native contracts
-export const COLLECTION_HELPER = '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f';
-export const CONTRACT_HELPER = '0x842899ECF380553E8a4de75bF534cdf6fBF64049';
-
-export enum Pallets {
-  Inflation = 'inflation',
-  ReFungible = 'refungible',
-  Fungible = 'fungible',
-  NFT = 'nonfungible',
-  Scheduler = 'scheduler',
-  UniqueScheduler = 'uniqueScheduler',
-  AppPromotion = 'apppromotion',
-  CollatorSelection = 'collatorselection',
-  Session = 'session',
-  Identity = 'identity',
-  Democracy = 'democracy',
-  Council = 'council',
-  //CouncilMembership = 'councilmembership',
-  TechnicalCommittee = 'technicalcommittee',
-  Fellowship = 'fellowshipcollective',
-  Preimage = 'preimage',
-  Maintenance = 'maintenance',
-  TestUtils = 'testutils',
-}
-
-export function requirePalletsOrSkip(test: Context, helper: DevUniqueHelper, requiredPallets: readonly string[]) {
-  const missingPallets = helper.fetchMissingPalletNames(requiredPallets);
-
-  if(missingPallets.length > 0) {
-    const skipMsg = `\tSkipping test '${test.test?.title}'.\n\tThe following pallets are missing:\n\t- ${missingPallets.join('\n\t- ')}`;
-    console.warn('\x1b[38:5:208m%s\x1b[0m', skipMsg);
-    test.skip();
-  }
-}
-
-export function itSub(name: string, cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: readonly string[] } = {}) {
-  (opts.only ? it.only :
-    opts.skip ? it.skip : it)(name, async function () {
-    await usingPlaygrounds(async (helper, privateKey) => {
-      if(opts.requiredPallets) {
-        requirePalletsOrSkip(this, helper, opts.requiredPallets);
-      }
-
-      await cb({helper, privateKey});
-    });
-  });
-}
-export function itSubIfWithPallet(name: string, required: readonly string[], cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: readonly string[] } = {}) {
-  return itSub(name, cb, {requiredPallets: required, ...opts});
-}
-itSub.only = (name: string, cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSub(name, cb, {only: true});
-itSub.skip = (name: string, cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSub(name, cb, {skip: true});
-
-itSubIfWithPallet.only = (name: string, required: readonly string[], cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSubIfWithPallet(name, required, cb, {only: true});
-itSubIfWithPallet.skip = (name: string, required: readonly string[], cb: (apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSubIfWithPallet(name, required, cb, {skip: true});
-itSub.ifWithPallets = itSubIfWithPallet;
-
-export type SchedKind = 'anon' | 'named';
-
-export function itSched(
-  name: string,
-  cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any,
-  opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {},
-) {
-  itSub(name + ' (anonymous scheduling)', (apis) => cb('anon', apis), opts);
-  itSub(name + ' (named scheduling)', (apis) => cb('named', apis), opts);
-}
-itSched.only = (name: string, cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSched(name, cb, {only: true});
-itSched.skip = (name: string, cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any) => itSched(name, cb, {skip: true});
-itSched.ifWithPallets = itSchedIfWithPallets;
-
-function itSchedIfWithPallets(name: string, required: string[], cb: (schedKind: SchedKind, apis: { helper: DevUniqueHelper, privateKey: (seed: string) => Promise<IKeyringPair> }) => any, opts: { only?: boolean, skip?: boolean, requiredPallets?: string[] } = {}) {
-  return itSched(name, cb, {requiredPallets: required, ...opts});
-}
-
-export function describeXCM(title: string, fn: (this: Mocha.Suite) => void, opts: {skip?: boolean} = {}) {
-  (process.env.RUN_XCM_TESTS && !opts.skip
-    ? describe
-    : describe.skip)(title, fn);
-}
-
-describeXCM.skip = (name: string, fn: (this: Mocha.Suite) => void) => describeXCM(name, fn, {skip: true});
-
-export function describeGov(title: string, fn: (this: Mocha.Suite) => void, opts: {skip?: boolean} = {}) {
-  (process.env.RUN_GOV_TESTS && !opts.skip
-    ? describe
-    : describe.skip)(title, fn);
-}
-
-describeGov.skip = (name: string, fn: (this: Mocha.Suite) => void) => describeGov(name, fn, {skip: true});
-
-export function sizeOfInt(i: number) {
-  if(i < 0 || i > 0xffffffff) throw new Error('out of range');
-  if(i < 0b11_1111) {
-    return 1;
-  } else if(i < 0b11_1111_1111_1111) {
-    return 2;
-  } else if(i < 0b11_1111_1111_1111_1111_1111_1111_1111) {
-    return 4;
-  } else {
-    return 5;
-  }
-}
-
-const UTF8_ENCODER = new TextEncoder();
-export function sizeOfEncodedStr(v: string) {
-  const encoded = UTF8_ENCODER.encode(v);
-  return sizeOfInt(encoded.length) + encoded.length;
-}
-
-export function sizeOfProperty(prop: {key: string, value: string}) {
-  return sizeOfEncodedStr(prop.key) + sizeOfEncodedStr(prop.value);
-}
-
-export function makeNames(url: string) {
-  const filename = fileURLToPath(url);
-  return {
-    filename,
-    dirname: dirname(filename),
-  };
-}
deletedjs-packages/tests/util/relayIdentitiesChecker.tsdiffbeforeafterboth
--- a/js-packages/tests/util/relayIdentitiesChecker.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
-// SPDX-License-Identifier: Apache-2.0
-//
-// Checks and reports the differences between identities and sub-identities on two chains.
-//
-// Usage: `yarn checkRelayIdentities [relay-1 WS URL] [relay-2 WS URL]`
-// Example: `yarn checkRelayIdentities wss://polkadot-rpc.dwellir.com wss://kusama-rpc.dwellir.com`
-
-import {encodeAddress} from '@polkadot/keyring';
-import {usingPlaygrounds} from './index.js';
-import {getIdentities, getSubs, getSupers, constructSubInfo} from './identitySetter.js';
-
-const relay1Url = process.argv[2] ?? 'ws://localhost:9844';
-const relay2Url = process.argv[3] ?? 'ws://localhost:9844';
-
-async function pullIdentities(relayUrl: string): Promise<[any[], any[]]> {
-  const identities: any[] = [];
-  const subs: any[] = [];
-
-  await usingPlaygrounds(async helper => {
-    try {
-      // iterate over every identity
-      for(const [key, value] of await getIdentities(helper)) {
-        // if any of the judgements resulted in a good confirmed outcome, keep this identity
-        if(value.toHuman().judgements.filter((x: any) => x[1] == 'Reasonable' || x[1] == 'KnownGood').length == 0) continue;
-        identities.push([key, value]);
-      }
-
-      const supersOfSubs = await getSupers(helper);
-
-      // iterate over every sub-identity
-      for(const [key, value] of await getSubs(helper)) {
-        // only get subs of the identities interesting to us
-        if(identities.find((x: any) => x[0] == key) == -1) continue;
-        subs.push(constructSubInfo(key, value, supersOfSubs));
-      }
-    } catch (error) {
-      console.error(error);
-      throw Error(`Error during fetching identities on ${relayUrl}`);
-    }
-  }, relayUrl);
-
-  return [identities, subs];
-}
-
-// The utility for pulling identity and sub-identity data
-const checkRelayIdentities = async (): Promise<void> => {
-  const [identitiesOnRelay1, subIdentitiesOnRelay1] = await pullIdentities(relay1Url);
-  const [identitiesOnRelay2, subIdentitiesOnRelay2] = await pullIdentities(relay2Url);
-
-  console.log('identities counts:\t', identitiesOnRelay1.length, identitiesOnRelay2.length);
-  console.log('sub-identities counts:\t', subIdentitiesOnRelay1.length, subIdentitiesOnRelay2.length);
-  console.log();
-
-  try {
-    const matchingAddresses: string[] = [];
-    const inequalIdentities: {[name: string]: [any, any]} = {};
-
-    for(const [key1, value1] of identitiesOnRelay1) {
-      const address = encodeAddress(key1);
-      const identity2 = identitiesOnRelay2.find(([key2, _value2]) => address === encodeAddress(key2));
-      if(!identity2) continue;
-      matchingAddresses.push(address);
-
-      //const [[key2, value2]] = identitiesOnRelay2.splice(index2, 1);
-      const [_key2, value2] = identity2;
-      if(JSON.stringify(value1.info) === JSON.stringify(value2.info)) continue;
-      inequalIdentities[address] = [value1, value2];
-    }
-
-    /*for (const [v1, v2] of Object.values(inequalIdentities)) {
-      console.log(v1.toHuman().info);
-      console.log();
-      console.log(v2.toHuman().info);
-      await new Promise(resolve => setTimeout(resolve, 4000));
-    }*/
-
-    console.log(`Accounts with identities on both relays:\t${matchingAddresses.length}`);
-    console.log(`Sub-identities with conflicting information:\t${Object.entries(inequalIdentities).length}`);
-    console.log();
-
-    const inequalSubIdentities = [];
-    let matchesFound = 0;
-    for(const address of matchingAddresses) {
-      const sub1 = subIdentitiesOnRelay1.find(([key1, _value1]) => address === encodeAddress(key1));
-      if(!sub1) continue;
-      const sub2 = subIdentitiesOnRelay2.find(([key2, _value2]) => address === encodeAddress(key2));
-      if(!sub2) continue;
-
-      const [value1, value2] = [sub1[1], sub2[1]];
-      matchesFound++;
-
-      if(JSON.stringify(value1[1]) === JSON.stringify(value2[1])) {
-        continue;
-      }
-      inequalSubIdentities.push([value1, value2]);
-    }
-
-    /*for (const [v1, v2] of inequalSubIdentities) {
-      console.log(v1[1]);
-      console.log();
-      console.log(v2[1]);
-      await new Promise(resolve => setTimeout(resolve, 300));
-    }*/
-    console.log(`Accounts with sub-identities on both relays:\t${matchesFound}`);
-    console.log(`Of them, those with conflicting sub-identities:\t${inequalSubIdentities.length}`);
-  } catch (error) {
-    console.error(error);
-    throw Error('Error during comparison');
-  }
-};
-
-if(process.argv[1] === module.filename)
-  checkRelayIdentities().catch(() => process.exit(1));
deletedjs-packages/tests/util/setCode.tsdiffbeforeafterboth
--- a/js-packages/tests/util/setCode.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import {readFile} from 'fs/promises';
-import {u8aToHex} from '@polkadot/util';
-import {usingPlaygrounds} from './index.js';
-
-const codePath = process.argv[2];
-if(!codePath) throw new Error('missing code path argument');
-
-const code = await readFile(codePath);
-
-await usingPlaygrounds(async (helper, privateKey) => {
-  const alice = await privateKey('//Alice');
-  await helper.getSudo().executeExtrinsicUncheckedWeight(alice, 'api.tx.system.setCode', [u8aToHex(code)]);
-});
-// We miss disconnect/unref somewhere.
-process.exit(0);
modifiedjs-packages/tests/vesting.test.tsdiffbeforeafterboth
--- a/js-packages/tests/vesting.test.ts
+++ b/js-packages/tests/vesting.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, usingPlaygrounds, expect} from './util/index.js';
+import {itSub, usingPlaygrounds, expect} from '@unique/test-utils/util.js';
 
 describe('Vesting', () => {
   let donor: IKeyringPair;
modifiedjs-packages/tests/xcm/lowLevelXcmQuartz.test.tsdiffbeforeafterboth
--- a/js-packages/tests/xcm/lowLevelXcmQuartz.test.ts
+++ b/js-packages/tests/xcm/lowLevelXcmQuartz.test.ts
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds,  usingMoonriverPlaygrounds, usingShidenPlaygrounds, usingRelayPlaygrounds} from '../util/index.js';
+import {itSub, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds,  usingMoonriverPlaygrounds, usingShidenPlaygrounds} from '@unique/test-utils/util.js';
 import {QUARTZ_CHAIN,  QTZ_DECIMALS,  SHIDEN_DECIMALS, karuraUrl, moonriverUrl,  shidenUrl,  SAFE_XCM_VERSION, XcmTestHelper, TRANSFER_AMOUNT, SENDER_BUDGET, relayUrl} from './xcm.types.js';
 import {hexToString} from '@polkadot/util';
 
modifiedjs-packages/tests/xcm/lowLevelXcmUnique.test.tsdiffbeforeafterboth
after · js-packages/tests/xcm/lowLevelXcmUnique.test.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 type {IKeyringPair} from '@polkadot/types/types';18import config from '../config.js';19import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingHydraDxPlaygrounds} from '@unique/test-utils/util.js';20import {nToBigInt} from '@polkadot/util';21import {hexToString} from '@polkadot/util';22import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl,  hydraDxUrl,  moonbeamUrl, polkadexUrl, uniqueAssetId} from './xcm.types.js';2324const testHelper = new XcmTestHelper('unique');2526272829describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => {30  let alice: IKeyringPair;31  let randomAccount: IKeyringPair;3233  before(async () => {34    await usingPlaygrounds(async (helper, privateKey) => {35      alice = await privateKey('//Alice');36      console.log(config.acalaUrl);37      randomAccount = helper.arrange.createEmptyAccount();3839      // Set the default version to wrap the first message to other chains.40      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);41    });4243    await usingAcalaPlaygrounds(acalaUrl, async (helper) => {44      const destination = {45        V2: {46          parents: 1,47          interior: {48            X1: {49              Parachain: UNIQUE_CHAIN,50            },51          },52        },53      };5455      const metadata = {56        name: 'Unique Network',57        symbol: 'UNQ',58        decimals: 18,59        minimalBalance: 1250_000_000_000_000_000n,60      };61      const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v] : [any, any]) =>62        hexToString(v.toJSON()['symbol'])) as string[];6364      if(!assets.includes('UNQ')) {65        await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);66      } else {67        console.log('UNQ token already registered on Acala assetRegistry pallet');68      }69      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);70    });7172    await usingPlaygrounds(async (helper) => {73      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);74    });75  });7677  itSub('Should connect and send UNQ to Acala', async () => {78    await testHelper.sendUnqTo('acala', randomAccount);79  });8081  itSub('Should connect to Acala and send UNQ back', async () => {82    await testHelper.sendUnqBack('acala', alice, randomAccount);83  });8485  itSub('Acala can send only up to its balance', async () => {86    await testHelper.sendOnlyOwnedBalance('acala', alice);87  });8889  itSub('Should not accept reserve transfer of UNQ from Acala', async () => {90    await testHelper.rejectReserveTransferUNQfrom('acala', alice);91  });92});9394describeXCM('[XCMLL] Integration test: Exchanging tokens with Polkadex', () => {95  let alice: IKeyringPair;96  let randomAccount: IKeyringPair;9798  before(async () => {99    await usingPlaygrounds(async (helper, privateKey) => {100      alice = await privateKey('//Alice');101      randomAccount = helper.arrange.createEmptyAccount();102103      // Set the default version to wrap the first message to other chains.104      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);105    });106107    await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {108      const isWhitelisted = ((await helper.callRpc('api.query.xcmHelper.whitelistedTokens', []))109        .toJSON() as [])110        .map(nToBigInt).length != 0;111      /*112      Check whether the Unique token has been added113      to the whitelist, since an error will occur114      if it is added again. Needed for debugging115      when this test is run multiple times.116      */117      if(isWhitelisted) {118        console.log('UNQ token is already whitelisted on Polkadex');119      } else {120        await helper.getSudo().xcmHelper.whitelistToken(alice, uniqueAssetId);121      }122123      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);124    });125126    await usingPlaygrounds(async (helper) => {127      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);128    });129  });130131  itSub('Should connect and send UNQ to Polkadex', async () => {132    await testHelper.sendUnqTo('polkadex', randomAccount);133  });134135136  itSub('Should connect to Polkadex and send UNQ back', async () => {137    await testHelper.sendUnqBack('polkadex', alice, randomAccount);138  });139140  itSub('Polkadex can send only up to its balance', async () => {141    await testHelper.sendOnlyOwnedBalance('polkadex', alice);142  });143144  itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {145    await testHelper.rejectReserveTransferUNQfrom('polkadex', alice);146  });147});148149// These tests are relevant only when150// the the corresponding foreign assets are not registered151describeXCM('[XCMLL] Integration test: Unique rejects non-native tokens', () => {152  let alice: IKeyringPair;153154  before(async () => {155    await usingPlaygrounds(async (helper, privateKey) => {156      alice = await privateKey('//Alice');157158      // Set the default version to wrap the first message to other chains.159      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);160    });161  });162163  itSub('Unique rejects ACA tokens from Acala', async () => {164    await testHelper.rejectNativeTokensFrom('acala', alice);165  });166167  itSub('Unique rejects GLMR tokens from Moonbeam', async () => {168    await testHelper.rejectNativeTokensFrom('moonbeam', alice);169  });170171  itSub('Unique rejects ASTR tokens from Astar', async () => {172    await testHelper.rejectNativeTokensFrom('astar', alice);173  });174175  itSub('Unique rejects PDX tokens from Polkadex', async () => {176    await testHelper.rejectNativeTokensFrom('polkadex', alice);177  });178});179180describeXCM('[XCMLL] Integration test: Exchanging UNQ with Moonbeam', () => {181  // Unique constants182  let alice: IKeyringPair;183  let uniqueAssetLocation;184185  let randomAccountUnique: IKeyringPair;186  let randomAccountMoonbeam: IKeyringPair;187188  // Moonbeam constants189  let assetId: string;190191  const uniqueAssetMetadata = {192    name: 'xcUnique',193    symbol: 'xcUNQ',194    decimals: 18,195    isFrozen: false,196    minimalBalance: 1n,197  };198199200  before(async () => {201    await usingPlaygrounds(async (helper, privateKey) => {202      alice = await privateKey('//Alice');203      [randomAccountUnique] = await helper.arrange.createAccounts([0n], alice);204205206      // Set the default version to wrap the first message to other chains.207      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);208    });209210    await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => {211      const alithAccount = helper.account.alithAccount();212      const baltatharAccount = helper.account.baltatharAccount();213      const dorothyAccount = helper.account.dorothyAccount();214215      randomAccountMoonbeam = helper.account.create();216217      // >>> Sponsoring Dorothy >>>218      console.log('Sponsoring Dorothy.......');219      await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);220      console.log('Sponsoring Dorothy.......DONE');221      // <<< Sponsoring Dorothy <<<222      uniqueAssetLocation = {223        XCM: {224          parents: 1,225          interior: {X1: {Parachain: UNIQUE_CHAIN}},226        },227      };228      const existentialDeposit = 1n;229      const isSufficient = true;230      const unitsPerSecond = 1n;231      const numAssetsWeightHint = 0;232233      if((await helper.assetManager.assetTypeId(uniqueAssetLocation)).toJSON()) {234        console.log('Unique asset already registered on Moonbeam');235      } else {236        const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({237          location: uniqueAssetLocation,238          metadata: uniqueAssetMetadata,239          existentialDeposit,240          isSufficient,241          unitsPerSecond,242          numAssetsWeightHint,243        });244245        console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);246247        await helper.fastDemocracy.executeProposal('register UNQ foreign asset', encodedProposal);248      }249250      // >>> Acquire Unique AssetId Info on Moonbeam >>>251      console.log('Acquire Unique AssetId Info on Moonbeam.......');252253      assetId = (await helper.assetManager.assetTypeId(uniqueAssetLocation)).toString();254255      console.log('UNQ asset ID is %s', assetId);256      console.log('Acquire Unique AssetId Info on Moonbeam.......DONE');257258      // >>> Sponsoring random Account >>>259      console.log('Sponsoring random Account.......');260      await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonbeam.address, 11_000_000_000_000_000_000n);261      console.log('Sponsoring random Account.......DONE');262      // <<< Sponsoring random Account <<<263    });264265    await usingPlaygrounds(async (helper) => {266      await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);267    });268  });269270  itSub('Should connect and send UNQ to Moonbeam', async () => {271    await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);272  });273274  itSub('Should connect to Moonbeam and send UNQ back', async () => {275    await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique);276  });277278  itSub('Moonbeam can send only up to its balance', async () => {279    await testHelper.sendOnlyOwnedBalance('moonbeam', alice);280  });281282  itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {283    await testHelper.rejectReserveTransferUNQfrom('moonbeam', alice);284  });285});286287describeXCM('[XCMLL] Integration test: Exchanging tokens with Astar', () => {288  let alice: IKeyringPair;289  let randomAccount: IKeyringPair;290291  const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar292  const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar293294  // Unique -> Astar295  const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.296  const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar297298  before(async () => {299    await usingPlaygrounds(async (helper, privateKey) => {300      alice = await privateKey('//Alice');301      randomAccount = helper.arrange.createEmptyAccount();302      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);303      console.log('randomAccount', randomAccount.address);304305      // Set the default version to wrap the first message to other chains.306      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);307    });308309    await usingAstarPlaygrounds(astarUrl, async (helper) => {310      if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {311        console.log('1. Create foreign asset and metadata');312        await helper.getSudo().assets.forceCreate(313          alice,314          UNQ_ASSET_ID_ON_ASTAR,315          alice.address,316          UNQ_MINIMAL_BALANCE_ON_ASTAR,317        );318319        await helper.assets.setMetadata(320          alice,321          UNQ_ASSET_ID_ON_ASTAR,322          'Unique Network',323          'UNQ',324          Number(UNQ_DECIMALS),325        );326327        console.log('2. Register asset location on Astar');328        const assetLocation = {329          V2: {330            parents: 1,331            interior: {332              X1: {333                Parachain: UNIQUE_CHAIN,334              },335            },336          },337        };338339        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, UNQ_ASSET_ID_ON_ASTAR]);340341        console.log('3. Set UNQ payment for XCM execution on Astar');342        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);343      } else {344        console.log('UNQ is already registered on Astar');345      }346      console.log('4. Transfer 1 ASTR to recipient to create the account (needed due to existential balance)');347      await helper.balance.transferToSubstrate(alice, randomAccount.address, astarInitialBalance);348    });349  });350351  itSub('Should connect and send UNQ to Astar', async () => {352    await testHelper.sendUnqTo('astar', randomAccount);353  });354355  itSub('Should connect to Astar and send UNQ back', async () => {356    await testHelper.sendUnqBack('astar', alice, randomAccount);357  });358359  itSub('Astar can send only up to its balance', async () => {360    await testHelper.sendOnlyOwnedBalance('astar', alice);361  });362363  itSub('Should not accept reserve transfer of UNQ from Astar', async () => {364    await testHelper.rejectReserveTransferUNQfrom('astar', alice);365  });366});367368describeXCM('[XCMLL] Integration test: Exchanging tokens with HydraDx', () => {369  let alice: IKeyringPair;370  let randomAccount: IKeyringPair;371372  before(async () => {373    await usingPlaygrounds(async (helper, privateKey) => {374      alice = await privateKey('//Alice');375      console.log(config.acalaUrl);376      randomAccount = helper.arrange.createEmptyAccount();377378      // Set the default version to wrap the first message to other chains.379      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);380      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);381    });382383    await usingHydraDxPlaygrounds(hydraDxUrl, async (helper) => {384      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);385    });386  });387388  itSub('Should connect and send UNQ to HydraDx', async () => {389    await testHelper.sendUnqTo('hydraDx', randomAccount);390  });391392  itSub('Should connect to HydraDx and send UNQ back', async () => {393    await testHelper.sendUnqBack('hydraDx', alice, randomAccount);394  });395396  itSub('HydraDx can send only up to its balance', async () => {397    await testHelper.sendOnlyOwnedBalance('hydraDx', alice);398  });399400  itSub('Should not accept reserve transfer of UNQ from HydraDx', async () => {401    await testHelper.rejectReserveTransferUNQfrom('hydraDx', alice);402  });403});
modifiedjs-packages/tests/xcm/xcm.types.tsdiffbeforeafterboth
--- a/js-packages/tests/xcm/xcm.types.ts
+++ b/js-packages/tests/xcm/xcm.types.ts
@@ -1,7 +1,6 @@
 import type {IKeyringPair} from '@polkadot/types/types';
-import {hexToString} from '@polkadot/util';
-import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingHydraDxPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '../util/index.js';
-import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';
+import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingHydraDxPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '@unique/test-utils/util.js';
+import {DevUniqueHelper, Event} from '@unique/test-utils';
 import config from '../config.js';
 
 export const UNIQUE_CHAIN = +(process.env.RELAY_UNIQUE_ID || 2037);
modifiedjs-packages/tests/xcm/xcmOpal.test.tsdiffbeforeafterboth
--- a/js-packages/tests/xcm/xcmOpal.test.ts
+++ b/js-packages/tests/xcm/xcmOpal.test.ts
@@ -16,7 +16,7 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import config from '../config.js';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingWestmintPlaygrounds, usingRelayPlaygrounds} from '../util/index.js';
+import {itSub, expect, describeXCM, usingPlaygrounds, usingWestmintPlaygrounds, usingRelayPlaygrounds} from '@unique/test-utils/util.js';
 import {XcmTestHelper} from './xcm.types.js';
 
 const STATEMINE_CHAIN = +(process.env.RELAY_WESTMINT_ID || 1000);
modifiedjs-packages/tests/xcm/xcmQuartz.test.tsdiffbeforeafterboth
--- a/js-packages/tests/xcm/xcmQuartz.test.ts
+++ b/js-packages/tests/xcm/xcmQuartz.test.ts
@@ -15,8 +15,8 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import type {IKeyringPair} from '@polkadot/types/types';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util/index.js';
-import {DevUniqueHelper, Event} from '@unique/playgrounds/unique.dev.js';
+import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '@unique/test-utils/util.js';
+import {DevUniqueHelper, Event} from '@unique/test-utils';
 import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl} from './xcm.types.js';
 import {hexToString} from '@polkadot/util';
 import {XcmTestHelper} from './xcm.types.js';
modifiedjs-packages/tests/xcm/xcmUnique.test.tsdiffbeforeafterboth
--- a/js-packages/tests/xcm/xcmUnique.test.ts
+++ b/js-packages/tests/xcm/xcmUnique.test.ts
@@ -16,8 +16,8 @@
 
 import type {IKeyringPair} from '@polkadot/types/types';
 import config from '../config.js';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util/index.js';
-import {Event} from '@unique/playgrounds/unique.dev.js';
+import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '@unique/test-utils/util.js';
+import {Event} from '@unique/test-utils';
 import {hexToString, nToBigInt} from '@polkadot/util';
 import {ACALA_CHAIN, ASTAR_CHAIN, MOONBEAM_CHAIN, POLKADEX_CHAIN, SAFE_XCM_VERSION, STATEMINT_CHAIN, UNIQUE_CHAIN, expectFailedToTransact, expectUntrustedReserveLocationFail, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types.js';
 import {XcmTestHelper} from './xcm.types.js';
modifiedjs-packages/types/package.jsondiffbeforeafterboth
--- a/js-packages/types/package.json
+++ b/js-packages/types/package.json
@@ -5,7 +5,7 @@
     "engines": {
         "node": ">=16"
     },
-    "name": "@unique/opal-types",
+    "name": "@unique-nft/opal-testnet-types",
     "type": "module",
     "version": "1.0.0",
     "main": "index.js",
modifiedjs-packages/yarn.lockdiffbeforeafterboth
--- a/js-packages/yarn.lock
+++ b/js-packages/yarn.lock
@@ -1242,22 +1242,21 @@
   languageName: node
   linkType: hard
 
-"@unique/opal-types@workspace:*, @unique/opal-types@workspace:types":
+"@unique-nft/opal-testnet-types@workspace:*, @unique-nft/opal-testnet-types@workspace:types":
   version: 0.0.0-use.local
-  resolution: "@unique/opal-types@workspace:types"
+  resolution: "@unique-nft/opal-testnet-types@workspace:types"
   dependencies:
     "@polkadot/typegen": ^10.10.1
   languageName: unknown
   linkType: soft
 
-"@unique/playgrounds@workspace:*, @unique/playgrounds@workspace:playgrounds":
+"@unique-nft/playgrounds@workspace:*, @unique-nft/playgrounds@workspace:playgrounds":
   version: 0.0.0-use.local
-  resolution: "@unique/playgrounds@workspace:playgrounds"
+  resolution: "@unique-nft/playgrounds@workspace:playgrounds"
   dependencies:
     "@polkadot/api": 10.10.1
     "@polkadot/util": ^12.5.1
     "@polkadot/util-crypto": ^12.5.1
-    "@unique/opal-types": "workspace:*"
   languageName: unknown
   linkType: soft
 
@@ -1267,6 +1266,17 @@
   languageName: unknown
   linkType: soft
 
+"@unique/test-utils@workspace:*, @unique/test-utils@workspace:test-utils":
+  version: 0.0.0-use.local
+  resolution: "@unique/test-utils@workspace:test-utils"
+  dependencies:
+    "@polkadot/api": 10.10.1
+    "@polkadot/util": ^12.5.1
+    "@polkadot/util-crypto": ^12.5.1
+    "@unique-nft/opal-testnet-types": "workspace:*"
+  languageName: unknown
+  linkType: soft
+
 "@unique/tests@workspace:tests":
   version: 0.0.0-use.local
   resolution: "@unique/tests@workspace:tests"
@@ -6001,8 +6011,9 @@
     "@types/node": ^20.8.10
     "@typescript-eslint/eslint-plugin": ^6.10.0
     "@typescript-eslint/parser": ^6.10.0
-    "@unique/opal-types": "workspace:*"
-    "@unique/playgrounds": "workspace:*"
+    "@unique-nft/opal-testnet-types": "workspace:*"
+    "@unique-nft/playgrounds": "workspace:*"
+    "@unique/test-utils": "workspace:*"
     chai: ^4.3.10
     chai-as-promised: ^7.1.1
     chai-like: ^1.1.1