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

difftreelog

Tests/fin council (#1070)

Max Andreev2024-06-05parent: #c9b1f9a.patch.diff
in: master

7 files changed

modifiedjs-packages/.vscode/settings.jsondiffbeforeafterboth
--- a/js-packages/.vscode/settings.json
+++ b/js-packages/.vscode/settings.json
@@ -5,6 +5,8 @@
 	},
 	"mochaExplorer.files": "tests/**/*.test.ts",
 	"mochaExplorer.require": "ts-node/register",
+  "mochaExplorer.esmLoader": true,
+  "mochaExplorer.nodeArgv": ["--loader", "ts-node/esm"],
 	"eslint.format.enable": true,
 	"[javascript]": {
 		"editor.defaultFormatter": "dbaeumer.vscode-eslint"
modifiedjs-packages/test-utils/index.tsdiffbeforeafterboth
before · js-packages/test-utils/index.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import '@unique-nft/opal-testnet-types/augment-api.js';5import '@unique-nft/opal-testnet-types/augment-types.js';6import '@unique-nft/opal-testnet-types/types-lookup.js';78import {stringToU8a} from '@polkadot/util';9import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';10import type {ChainHelperBaseConstructor, UniqueHelperConstructor} from '@unique-nft/playgrounds/unique.js';11import {UniqueHelper, ChainHelperBase, HelperGroup} from '@unique-nft/playgrounds/unique.js';12import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';13import * as defs from '@unique-nft/opal-testnet-types/definitions.js';14import type {IKeyringPair} from '@polkadot/types/types';15import type {EventRecord} from '@polkadot/types/interfaces';16import type {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from '@unique-nft/playgrounds/types.js';17import type {FrameSystemEventRecord, StagingXcmV2TraitsError, StagingXcmV3TraitsOutcome} from '@polkadot/types/lookup';18import type {SignerOptions, VoidFn} from '@polkadot/api/types';19import {spawnSync} from 'child_process';20import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup, HydraDxHelper} from './xcm/index.js';21import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, RankedCollectiveGroup, ReferendaGroup} from './governance.js';22import type {ICollectiveGroup, IFellowshipGroup} from './governance.js';2324export class SilentLogger {25  log(_msg: any, _level: any): void { }26  level = {27    ERROR: 'ERROR' as const,28    WARNING: 'WARNING' as const,29    INFO: 'INFO' as const,30  };31}3233export class SilentConsole {34  // TODO: Remove, this is temporary: Filter unneeded API output35  // (Jaco promised it will be removed in the next version)36  consoleErr: any;37  consoleLog: any;38  consoleWarn: any;3940  constructor() {41    this.consoleErr = console.error;42    this.consoleLog = console.log;43    this.consoleWarn = console.warn;44  }4546  enable() {47    const outFn = (printer: any) => (...args: any[]) => {48      for(const arg of args) {49        if(typeof arg !== 'string')50          continue;51        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'];52        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);53        if(needToSkip || arg === 'Normal connection closure')54          return;55      }56      printer(...args);57    };5859    console.error = outFn(this.consoleErr.bind(console));60    console.log = outFn(this.consoleLog.bind(console));61    console.warn = outFn(this.consoleWarn.bind(console));62  }6364  disable() {65    console.error = this.consoleErr;66    console.log = this.consoleLog;67    console.warn = this.consoleWarn;68  }69}7071export interface IEventHelper {72  section(): string;7374  method(): string;7576  wrapEvent(data: any[]): any;77}7879// eslint-disable-next-line @typescript-eslint/naming-convention80function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {81  const helperClass = class implements IEventHelper {82    wrapEvent: (data: any[]) => any;83    _section: string;84    _method: string;8586    constructor() {87      this.wrapEvent = wrapEvent;88      this._section = section;89      this._method = method;90    }9192    section(): string {93      return this._section;94    }9596    method(): string {97      return this._method;98    }99100    filter(txres: ITransactionResult) {101      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)102        .map(e => this.wrapEvent(e.event.data));103    }104105    find(txres: ITransactionResult) {106      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);107      return e ? this.wrapEvent(e.event.data) : null;108    }109110    expect(txres: ITransactionResult) {111      const e = this.find(txres);112      if(e) {113        return e;114      } else {115        throw Error(`Expected event ${section}.${method}`);116      }117    }118  };119120  return helperClass;121}122123function eventJsonData<T = any>(data: any[], index: number) {124  return data[index].toJSON() as T;125}126127function eventHumanData(data: any[], index: number) {128  return data[index].toHuman();129}130131function eventData<T = any>(data: any[], index: number) {132  return data[index] as T;133}134135// eslint-disable-next-line @typescript-eslint/naming-convention136function EventSection(section: string) {137  return class Section {138    static section = section;139140    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {141      const helperClass = EventHelper(Section.section, name, wrapEvent);142      return new helperClass();143    }144  };145}146147function schedulerSection(schedulerInstance: string) {148  return class extends EventSection(schedulerInstance) {149    static Dispatched = this.Method('Dispatched', data => ({150      task: eventJsonData(data, 0),151      id: eventHumanData(data, 1),152      result: data[2],153    }));154155    static PriorityChanged = this.Method('PriorityChanged', data => ({156      task: eventJsonData(data, 0),157      priority: eventJsonData(data, 1),158    }));159  };160}161162export class Event {163  static Democracy = class extends EventSection('democracy') {164    static Proposed = this.Method('Proposed', data => ({165      proposalIndex: eventJsonData<number>(data, 0),166    }));167168    static ExternalTabled = this.Method('ExternalTabled');169170    static Started = this.Method('Started', data => ({171      referendumIndex: eventJsonData<number>(data, 0),172      threshold: eventHumanData(data, 1),173    }));174175    static Voted = this.Method('Voted', data => ({176      voter: eventJsonData(data, 0),177      referendumIndex: eventJsonData<number>(data, 1),178      vote: eventJsonData(data, 2),179    }));180181    static Passed = this.Method('Passed', data => ({182      referendumIndex: eventJsonData<number>(data, 0),183    }));184185    static ProposalCanceled = this.Method('ProposalCanceled', data => ({186      propIndex: eventJsonData<number>(data, 0),187    }));188189    static Cancelled = this.Method('Cancelled', data => ({190      propIndex: eventJsonData<number>(data, 0),191    }));192193    static Vetoed = this.Method('Vetoed', data => ({194      who: eventHumanData(data, 0),195      proposalHash: eventHumanData(data, 1),196      until: eventJsonData<number>(data, 1),197    }));198  };199200  static Council = class extends EventSection('council') {201    static Proposed = this.Method('Proposed', data => ({202      account: eventHumanData(data, 0),203      proposalIndex: eventJsonData<number>(data, 1),204      proposalHash: eventHumanData(data, 2),205      threshold: eventJsonData<number>(data, 3),206    }));207    static Closed = this.Method('Closed', data => ({208      proposalHash: eventHumanData(data, 0),209      yes: eventJsonData<number>(data, 1),210      no: eventJsonData<number>(data, 2),211    }));212    static Executed = this.Method('Executed', data => ({213      proposalHash: eventHumanData(data, 0),214    }));215  };216217  static TechnicalCommittee = class extends EventSection('technicalCommittee') {218    static Proposed = this.Method('Proposed', data => ({219      account: eventHumanData(data, 0),220      proposalIndex: eventJsonData<number>(data, 1),221      proposalHash: eventHumanData(data, 2),222      threshold: eventJsonData<number>(data, 3),223    }));224    static Closed = this.Method('Closed', data => ({225      proposalHash: eventHumanData(data, 0),226      yes: eventJsonData<number>(data, 1),227      no: eventJsonData<number>(data, 2),228    }));229    static Approved = this.Method('Approved', data => ({230      proposalHash: eventHumanData(data, 0),231    }));232    static Executed = this.Method('Executed', data => ({233      proposalHash: eventHumanData(data, 0),234      result: eventHumanData(data, 1),235    }));236  };237238  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {239    static Submitted = this.Method('Submitted', data => ({240      referendumIndex: eventJsonData<number>(data, 0),241      trackId: eventJsonData<number>(data, 1),242      proposal: eventJsonData(data, 2),243    }));244245    static Cancelled = this.Method('Cancelled', data => ({246      index: eventJsonData<number>(data, 0),247      tally: eventJsonData(data, 1),248    }));249  };250251  static UniqueScheduler = schedulerSection('uniqueScheduler');252  static Scheduler = schedulerSection('scheduler');253254  static XcmpQueue = class extends EventSection('xcmpQueue') {255    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({256      messageHash: eventJsonData(data, 0),257    }));258259    static Success = this.Method('Success', data => ({260      messageHash: eventJsonData(data, 0),261    }));262263    static Fail = this.Method('Fail', data => ({264      messageHash: eventJsonData(data, 0),265      outcome: eventData<StagingXcmV2TraitsError>(data, 2),266    }));267  };268269  static DmpQueue = class extends EventSection('dmpQueue') {270    static ExecutedDownward = this.Method('ExecutedDownward', data => ({271      outcome: eventData<StagingXcmV3TraitsOutcome>(data, 2),272    }));273  };274}275276// eslint-disable-next-line @typescript-eslint/naming-convention277export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {278  return class extends Base {279    constructor(...args: any[]) {280      super(...args);281    }282283    override async executeExtrinsic(284      sender: IKeyringPair,285      extrinsic: string,286      params: any[],287      expectSuccess?: boolean,288      options: Partial<SignerOptions> | null = null,289    ): Promise<ITransactionResult> {290      const call = this.constructApiCall(extrinsic, params);291      const result = await super.executeExtrinsic(292        sender,293        'api.tx.sudo.sudo',294        [call],295        expectSuccess,296        options,297      );298299      if(result.status === 'Fail') return result;300301      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;302      if(data.isErr) {303        if(data.asErr.isModule) {304          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;305          const metaError = super.getApi()?.registry.findMetaError(error);306          throw new Error(`${metaError.section}.${metaError.name}`);307        } else if(data.asErr.isToken) {308          throw new Error(`Token: ${data.asErr.asToken}`);309        }310        // May be [object Object] in case of unhandled non-unit enum311        throw new Error(`Misc: ${data.asErr.toHuman()}`);312      }313      return result;314    }315    override async executeExtrinsicUncheckedWeight(316      sender: IKeyringPair,317      extrinsic: string,318      params: any[],319      expectSuccess?: boolean,320      options: Partial<SignerOptions> | null = null,321    ): Promise<ITransactionResult> {322      const call = this.constructApiCall(extrinsic, params);323      const result = await super.executeExtrinsic(324        sender,325        'api.tx.sudo.sudoUncheckedWeight',326        [call, {refTime: 0, proofSize: 0}],327        expectSuccess,328        options,329      );330331      if(result.status === 'Fail') return result;332333      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;334      if(data.isErr) {335        if(data.asErr.isModule) {336          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;337          const metaError = super.getApi()?.registry.findMetaError(error);338          throw new Error(`${metaError.section}.${metaError.name}`);339        } else if(data.asErr.isToken) {340          throw new Error(`Token: ${data.asErr.asToken}`);341        }342        // May be [object Object] in case of unhandled non-unit enum343        throw new Error(`Misc: ${data.asErr.toHuman()}`);344      }345      return result;346    }347  };348}349350class SchedulerGroup extends HelperGroup<UniqueHelper> {351  constructor(helper: UniqueHelper) {352    super(helper);353  }354355  cancelScheduled(signer: TSigner, scheduledId: string) {356    return this.helper.executeExtrinsic(357      signer,358      'api.tx.scheduler.cancelNamed',359      [scheduledId],360      true,361    );362  }363364  changePriority(signer: TSigner, scheduledId: string, priority: number) {365    return this.helper.executeExtrinsic(366      signer,367      'api.tx.scheduler.changeNamedPriority',368      [scheduledId, priority],369      true,370    );371  }372373  scheduleAt<T extends DevUniqueHelper>(374    executionBlockNumber: number,375    options: ISchedulerOptions = {},376  ) {377    return this.schedule<T>('schedule', executionBlockNumber, options);378  }379380  scheduleAfter<T extends DevUniqueHelper>(381    blocksBeforeExecution: number,382    options: ISchedulerOptions = {},383  ) {384    return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);385  }386387  schedule<T extends UniqueHelper>(388    scheduleFn: 'schedule' | 'scheduleAfter',389    blocksNum: number,390    options: ISchedulerOptions = {},391  ) {392    // eslint-disable-next-line @typescript-eslint/naming-convention393    const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);394    return this.helper.clone(ScheduledHelperType, {395      scheduleFn,396      blocksNum,397      options,398    }) as T;399  }400}401402class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {403  //todo:collator documentation404  addInvulnerable(signer: TSigner, address: string) {405    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);406  }407408  removeInvulnerable(signer: TSigner, address: string) {409    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);410  }411412  async getInvulnerables(): Promise<string[]> {413    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());414  }415416  /** and also total max invulnerables */417  maxCollators(): number {418    return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);419  }420421  async getDesiredCollators(): Promise<number> {422    return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();423  }424425  setLicenseBond(signer: TSigner, amount: bigint) {426    return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);427  }428429  async getLicenseBond(): Promise<bigint> {430    return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();431  }432433  obtainLicense(signer: TSigner) {434    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);435  }436437  releaseLicense(signer: TSigner) {438    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);439  }440441  forceReleaseLicense(signer: TSigner, released: string) {442    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);443  }444445  async hasLicense(address: string): Promise<bigint> {446    return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();447  }448449  onboard(signer: TSigner) {450    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);451  }452453  offboard(signer: TSigner) {454    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);455  }456457  async getCandidates(): Promise<string[]> {458    return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());459  }460}461462export class DevUniqueHelper extends UniqueHelper {463  /**464   * Arrange methods for tests465   */466  arrange: ArrangeGroup;467  wait: WaitGroup;468  admin: AdminGroup;469  session: SessionGroup;470  testUtils: TestUtilGroup;471  foreignAssets: ForeignAssetsGroup;472  xcm: XcmGroup<DevUniqueHelper>;473  xTokens: XTokensGroup<DevUniqueHelper>;474  tokens: TokensGroup<DevUniqueHelper>;475  scheduler: SchedulerGroup;476  collatorSelection: CollatorSelectionGroup;477  council: ICollectiveGroup;478  technicalCommittee: ICollectiveGroup;479  fellowship: IFellowshipGroup;480  democracy: DemocracyGroup;481482  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {483    options.helperBase = options.helperBase ?? DevUniqueHelper;484485    super(logger, options);486    this.arrange = new ArrangeGroup(this);487    this.wait = new WaitGroup(this);488    this.admin = new AdminGroup(this);489    this.testUtils = new TestUtilGroup(this);490    this.session = new SessionGroup(this);491    this.foreignAssets = new ForeignAssetsGroup(this);492    this.xcm = new XcmGroup(this, 'polkadotXcm');493    this.xTokens = new XTokensGroup(this);494    this.tokens = new TokensGroup(this);495    this.scheduler = new SchedulerGroup(this);496    this.collatorSelection = new CollatorSelectionGroup(this);497    this.council = {498      collective: new CollectiveGroup(this, 'council'),499      membership: new CollectiveMembershipGroup(this, 'councilMembership'),500    };501    this.technicalCommittee = {502      collective: new CollectiveGroup(this, 'technicalCommittee'),503      membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),504    };505    this.fellowship = {506      collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),507      referenda: new ReferendaGroup(this, 'fellowshipReferenda'),508    };509    this.democracy = new DemocracyGroup(this);510  }511512  override async connect(wsEndpoint: string, _listeners?: any): Promise<void> {513    if(!wsEndpoint) throw new Error('wsEndpoint was not set');514    const wsProvider = new WsProvider(wsEndpoint);515    this.api = new ApiPromise({516      provider: wsProvider,517      signedExtensions: {518        ContractHelpers: {519          extrinsic: {},520          payload: {},521        },522        CheckMaintenance: {523          extrinsic: {},524          payload: {},525        },526        DisableIdentityCalls: {527          extrinsic: {},528          payload: {},529        },530        FakeTransactionFinalizer: {531          extrinsic: {},532          payload: {},533        },534      },535      rpc: {536        unique: defs.unique.rpc,537        appPromotion: defs.appPromotion.rpc,538        povinfo: defs.povinfo.rpc,539        eth: {540          feeHistory: {541            description: 'Dummy',542            params: [],543            type: 'u8',544          },545          maxPriorityFeePerGas: {546            description: 'Dummy',547            params: [],548            type: 'u8',549          },550        },551      },552    });553    await this.api.isReadyOrError;554    this.network = await UniqueHelper.detectNetwork(this.api);555    this.wsEndpoint = wsEndpoint;556  }557  getSudo<T extends DevUniqueHelper>() {558    // eslint-disable-next-line @typescript-eslint/naming-convention559    const SudoHelperType = SudoHelper(this.helperBase);560    return this.clone(SudoHelperType) as T;561  }562}563564export class DevRelayHelper extends RelayHelper {565  wait: WaitGroup;566567  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {568    options.helperBase = options.helperBase ?? DevRelayHelper;569570    super(logger, options);571    this.wait = new WaitGroup(this);572  }573574  getSudo() {575    // eslint-disable-next-line @typescript-eslint/naming-convention576    const SudoHelperType = SudoHelper(this.helperBase);577    return this.clone(SudoHelperType) as DevRelayHelper;578  }579}580581export class DevWestmintHelper extends WestmintHelper {582  wait: WaitGroup;583584  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {585    options.helperBase = options.helperBase ?? DevWestmintHelper;586587    super(logger, options);588    this.wait = new WaitGroup(this);589  }590}591592export class DevStatemineHelper extends DevWestmintHelper {}593594export class DevStatemintHelper extends DevWestmintHelper {}595596export class DevMoonbeamHelper extends MoonbeamHelper {597  account: MoonbeamAccountGroup;598  wait: WaitGroup;599  fastDemocracy: MoonbeamFastDemocracyGroup;600601  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {602    options.helperBase = options.helperBase ?? DevMoonbeamHelper;603    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';604605    super(logger, options);606    this.account = new MoonbeamAccountGroup(this);607    this.wait = new WaitGroup(this);608    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);609  }610}611612export class DevMoonriverHelper extends DevMoonbeamHelper {613  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {614    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';615    super(logger, options);616  }617}618619export class DevAstarHelper extends AstarHelper {620  wait: WaitGroup;621622  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {623    options.helperBase = options.helperBase ?? DevAstarHelper;624625    super(logger, options);626    this.wait = new WaitGroup(this);627  }628629  getSudo<T extends AstarHelper>() {630    // eslint-disable-next-line @typescript-eslint/naming-convention631    const SudoHelperType = SudoHelper(this.helperBase);632    return this.clone(SudoHelperType) as T;633  }634}635636export class DevShidenHelper extends DevAstarHelper { }637638export class DevAcalaHelper extends AcalaHelper {639  wait: WaitGroup;640641  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {642    options.helperBase = options.helperBase ?? DevAcalaHelper;643644    super(logger, options);645    this.wait = new WaitGroup(this);646  }647  getSudo() {648    // eslint-disable-next-line @typescript-eslint/naming-convention649    const SudoHelperType = SudoHelper(this.helperBase);650    return this.clone(SudoHelperType) as DevAcalaHelper;651  }652}653654export class DevPolkadexHelper extends PolkadexHelper {655  wait: WaitGroup;656  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {657    options.helperBase = options.helperBase ?? PolkadexHelper;658659    super(logger, options);660    this.wait = new WaitGroup(this);661  }662663  getSudo() {664    // eslint-disable-next-line @typescript-eslint/naming-convention665    const SudoHelperType = SudoHelper(this.helperBase);666    return this.clone(SudoHelperType) as DevPolkadexHelper;667  }668}669670export class DevHydraDxHelper extends HydraDxHelper {671  wait: WaitGroup;672  fastDemocracy: HydraFastDemocracyGroup;673674  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {675    options.helperBase = options.helperBase ?? DevHydraDxHelper;676677    super(logger, options);678679    this.wait = new WaitGroup(this);680    this.fastDemocracy = new HydraFastDemocracyGroup(this);681  }682}683684export class DevKaruraHelper extends DevAcalaHelper {}685686export class ArrangeGroup {687  helper: DevUniqueHelper;688689  scheduledIdSlider = 0;690691  constructor(helper: DevUniqueHelper) {692    this.helper = helper;693  }694695  /**696   * Generates accounts with the specified UNQ token balance697   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.698   * @param donor donor account for balances699   * @returns array of newly created accounts700   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);701   */702  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {703    let nonce = await this.helper.chain.getNonce(donor.address);704    const wait = new WaitGroup(this.helper);705    const ss58Format = this.helper.chain.getChainProperties().ss58Format;706    const tokenNominal = this.helper.balance.getOneTokenNominal();707    const transactions = [];708    const accounts: IKeyringPair[] = [];709    for(const balance of balances) {710      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);711      accounts.push(recipient);712      if(balance !== 0n) {713        const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, balance * tokenNominal]);714        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));715        nonce++;716      }717    }718719    await Promise.all(transactions).catch(_e => {});720721    //#region TODO remove this region, when nonce problem will be solved722    const checkBalances = async () => {723      let isSuccess = true;724      for(let i = 0; i < balances.length; i++) {725        const balance = await this.helper.balance.getSubstrate(accounts[i].address);726        if(balance !== balances[i] * tokenNominal) {727          isSuccess = false;728          break;729        }730      }731      return isSuccess;732    };733734    let accountsCreated = false;735    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;736    // checkBalances retry up to 5-50 blocks737    for(let index = 0; index < maxBlocksChecked; index++) {738      accountsCreated = await checkBalances();739      if(accountsCreated) break;740      await wait.newBlocks(1);741    }742743    if(!accountsCreated) throw Error('Accounts generation failed');744    //#endregion745746    return accounts;747  };748749  // TODO combine this method and createAccounts into one750  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {751    const createAsManyAsCan = async () => {752      let transactions: any = [];753      const accounts: IKeyringPair[] = [];754      let nonce = await this.helper.chain.getNonce(donor.address);755      const tokenNominal = this.helper.balance.getOneTokenNominal();756      const ss58Format = this.helper.chain.getChainProperties().ss58Format;757      for(let i = 0; i < accountsToCreate; i++) {758        if(i === 500) { // if there are too many accounts to create759          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled760          transactions = []; //761          nonce = await this.helper.chain.getNonce(donor.address); // update nonce762        }763        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);764        accounts.push(recipient);765        if(withBalance !== 0n) {766          const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, withBalance * tokenNominal]);767          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));768          nonce++;769        }770      }771772      const fullfilledAccounts = [];773      await Promise.allSettled(transactions);774      for(const account of accounts) {775        const accountBalance = await this.helper.balance.getSubstrate(account.address);776        if(accountBalance === withBalance * tokenNominal) {777          fullfilledAccounts.push(account);778        }779      }780      return fullfilledAccounts;781    };782783784    const crowd: IKeyringPair[] = [];785    // do up to 5 retries786    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {787      const asManyAsCan = await createAsManyAsCan();788      crowd.push(...asManyAsCan);789      accountsToCreate -= asManyAsCan.length;790    }791792    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);793794    return crowd;795  };796797  /**798   * Generates one account with zero balance799   * @returns the newly generated account800   * @example const account = await helper.arrange.createEmptyAccount();801   */802  createEmptyAccount = (): IKeyringPair => {803    const ss58Format = this.helper.chain.getChainProperties().ss58Format;804    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);805  };806807  isDevNode = async () => {808    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();809    if(blockNumber == 0) {810      await this.helper.wait.newBlocks(1);811      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();812    }813    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);814    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);815    const findCreationDate = (block: any) => {816      const humanBlock = block.toHuman();817      let date;818      humanBlock.block.extrinsics.forEach((ext: any) => {819        if(ext.method.section === 'timestamp') {820          date = Number(ext.method.args.now.replaceAll(',', ''));821        }822      });823      return date;824    };825    const block1date = await findCreationDate(block1);826    const block2date = await findCreationDate(block2);827    if(block2date! - block1date! < 9000) return true;828    return false;829  };830831  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {832    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);833    let balance = await this.helper.balance.getSubstrate(address);834835    await promise();836837    balance -= await this.helper.balance.getSubstrate(address);838839    return balance;840  }841842  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {843    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);844845    const kvJson: {[key: string]: string} = {};846847    for(const kv of rawPovInfo.keyValues) {848      kvJson[kv.key.toHex()] = kv.value.toHex();849    }850851    const kvStr = JSON.stringify(kvJson);852853    const chainql = spawnSync(854      'chainql',855      [856        `--tla-code=data=${kvStr}`,857        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,858      ],859    );860861    if(!chainql.stdout) {862      throw Error('unable to get an output from the `chainql`');863    }864865    return {866      proofSize: rawPovInfo.proofSize.toNumber(),867      compactProofSize: rawPovInfo.compactProofSize.toNumber(),868      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),869      results: rawPovInfo.results,870      kv: JSON.parse(chainql.stdout.toString()),871    };872  }873874  calculatePalletAddress(palletId: any) {875    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));876    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);877  }878879  makeScheduledIds(num: number): string[] {880    function makeId(slider: number) {881      const scheduledIdSize = 64;882      const hexId = slider.toString(16);883      const prefixSize = scheduledIdSize - hexId.length;884885      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;886887      return scheduledId;888    }889890    const ids = [];891    for(let i = 0; i < num; i++) {892      ids.push(makeId(this.scheduledIdSlider));893      this.scheduledIdSlider += 1;894    }895896    return ids;897  }898899  makeScheduledId(): string {900    return (this.makeScheduledIds(1))[0];901  }902903  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {904    const capture = new EventCapture(this.helper, eventSection, eventMethod);905    await capture.startCapture();906907    return capture;908  }909910  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {911    return {912      V2: [913        {914          WithdrawAsset: [915            {916              id,917              fun: {918                Fungible: amount,919              },920            },921          ],922        },923        {924          BuyExecution: {925            fees: {926              id,927              fun: {928                Fungible: amount,929              },930            },931            weightLimit: 'Unlimited',932          },933        },934        {935          DepositAsset: {936            assets: {937              Wild: 'All',938            },939            maxAssets: 1,940            beneficiary: {941              parents: 0,942              interior: {943                X1: {944                  AccountId32: {945                    network: 'Any',946                    id: beneficiary,947                  },948                },949              },950            },951          },952        },953      ],954    };955  }956957  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {958    return {959      V2: [960        {961          ReserveAssetDeposited: [962            {963              id,964              fun: {965                Fungible: amount,966              },967            },968          ],969        },970        {971          BuyExecution: {972            fees: {973              id,974              fun: {975                Fungible: amount,976              },977            },978            weightLimit: 'Unlimited',979          },980        },981        {982          DepositAsset: {983            assets: {984              Wild: 'All',985            },986            maxAssets: 1,987            beneficiary: {988              parents: 0,989              interior: {990                X1: {991                  AccountId32: {992                    network: 'Any',993                    id: beneficiary,994                  },995                },996              },997            },998          },999        },1000      ],1001    };1002  }10031004  makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {1005    return {1006      V3: [1007        {1008          UnpaidExecution: {1009            weightLimit: 'Unlimited',1010            checkOrigin: null,1011          },1012        },1013        {1014          Transact: {1015            originKind: 'Superuser',1016            requireWeightAtMost: {1017              refTime: info.weightMultiplier * 200000000,1018              proofSize: info.weightMultiplier * 3000,1019            },1020            call: {1021              encoded: info.call,1022            },1023          },1024        },1025      ],1026    };1027  }1028}10291030class MoonbeamAccountGroup {1031  helper: MoonbeamHelper;10321033  keyring: Keyring;1034  _alithAccount: IKeyringPair;1035  _baltatharAccount: IKeyringPair;1036  _dorothyAccount: IKeyringPair;10371038  constructor(helper: MoonbeamHelper) {1039    this.helper = helper;10401041    this.keyring = new Keyring({type: 'ethereum'});1042    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';1043    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';1044    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';10451046    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');1047    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');1048    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');1049  }10501051  alithAccount() {1052    return this._alithAccount;1053  }10541055  baltatharAccount() {1056    return this._baltatharAccount;1057  }10581059  dorothyAccount() {1060    return this._dorothyAccount;1061  }10621063  create() {1064    return this.keyring.addFromUri(mnemonicGenerate());1065  }1066}10671068class MoonbeamFastDemocracyGroup {1069  helper: DevMoonbeamHelper;10701071  constructor(helper: DevMoonbeamHelper) {1072    this.helper = helper;1073  }10741075  async executeProposal(proposalDesciption: string, encodedProposal: string) {1076    const proposalHash = blake2AsHex(encodedProposal);10771078    const alithAccount = this.helper.account.alithAccount();1079    const baltatharAccount = this.helper.account.baltatharAccount();1080    const dorothyAccount = this.helper.account.dorothyAccount();10811082    const councilVotingThreshold = 2;1083    const technicalCommitteeThreshold = 2;1084    const fastTrackVotingPeriod = 3;1085    const fastTrackDelayPeriod = 0;10861087    console.log(`[democracy] executing '${proposalDesciption}' proposal`);10881089    // >>> Propose external motion through council >>>1090    console.log('\t* Propose external motion through council.......');1091    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1092    const encodedMotion = externalMotion?.method.toHex() || '';1093    const motionHash = blake2AsHex(encodedMotion);1094    console.log('\t* Motion hash is %s', motionHash);10951096    await this.helper.collective.council.propose(1097      baltatharAccount,1098      councilVotingThreshold,1099      externalMotion,1100      externalMotion.encodedLength,1101    );11021103    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1104    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1105    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);11061107    await this.helper.collective.council.close(1108      dorothyAccount,1109      motionHash,1110      councilProposalIdx,1111      {1112        refTime: 1_000_000_000,1113        proofSize: 1_000_000,1114      },1115      externalMotion.encodedLength,1116    );1117    console.log('\t* Propose external motion through council.......DONE');1118    // <<< Propose external motion through council <<<11191120    // >>> Fast track proposal through technical committee >>>1121    console.log('\t* Fast track proposal through technical committee.......');1122    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1123    const encodedFastTrack = fastTrack?.method.toHex() || '';1124    const fastTrackHash = blake2AsHex(encodedFastTrack);1125    console.log('\t* FastTrack hash is %s', fastTrackHash);11261127    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);11281129    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1130    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1131    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);11321133    await this.helper.collective.techCommittee.close(1134      baltatharAccount,1135      fastTrackHash,1136      techProposalIdx,1137      {1138        refTime: 1_000_000_000,1139        proofSize: 1_000_000,1140      },1141      fastTrack.encodedLength,1142    );1143    console.log('\t* Fast track proposal through technical committee.......DONE');1144    // <<< Fast track proposal through technical committee <<<11451146    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1147    const referendumIndex = democracyStarted.referendumIndex;11481149    // >>> Referendum voting >>>1150    console.log(`\t* Referendum #${referendumIndex} voting.......`);1151    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1152      balance: 10_000_000_000_000_000_000n,1153      vote: {aye: true, conviction: 1},1154    });1155    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1156    // <<< Referendum voting <<<11571158    // Wait the proposal to pass1159    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11601161    await this.helper.wait.newBlocks(1);11621163    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1164  }1165}11661167class HydraFastDemocracyGroup {1168  helper: DevHydraDxHelper;11691170  constructor(helper: DevHydraDxHelper) {1171    this.helper = helper;1172  }11731174  async executeProposal(proposalDesciption: string, encodedProposal: string) {1175    const proposalHash = blake2AsHex(encodedProposal);1176    const aliceAccount = this.helper.util.fromSeed('//Alice');1177    const bobAccount = this.helper.util.fromSeed('//Bob');1178    const eveAccount = this.helper.util.fromSeed('//Eve');11791180    const councilVotingThreshold = 1;1181    const technicalCommitteeThreshold = 3;1182    const fastTrackVotingPeriod = 3;1183    const fastTrackDelayPeriod = 0;11841185    console.log(`[democracy] executing '${proposalDesciption}' proposal`);11861187    // >>> Propose external motion through council >>>1188    console.log('\t* Propose external motion through council.......');1189    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1190    const encodedMotion = externalMotion?.method.toHex() || '';1191    const motionHash = blake2AsHex(encodedMotion);1192    console.log('\t* Motion hash is %s', motionHash);11931194    await this.helper.collective.council.propose(1195      aliceAccount,1196      councilVotingThreshold,1197      externalMotion,1198      externalMotion.encodedLength,1199    );12001201    console.log('\t* Propose external motion through council.......DONE');1202    // <<< Propose external motion through council <<<12031204    // >>> Fast track proposal through technical committee >>>1205    console.log('\t* Fast track proposal through technical committee.......');1206    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1207    const encodedFastTrack = fastTrack?.method.toHex() || '';1208    const fastTrackHash = blake2AsHex(encodedFastTrack);1209    console.log('\t* FastTrack hash is %s', fastTrackHash);12101211    await this.helper.collective.techCommittee.propose(aliceAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);12121213    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1214    await this.helper.collective.techCommittee.vote(aliceAccount, fastTrackHash, techProposalIdx, true);1215    await this.helper.collective.techCommittee.vote(bobAccount, fastTrackHash, techProposalIdx, true);1216    await this.helper.collective.techCommittee.vote(eveAccount, fastTrackHash, techProposalIdx, true);12171218    await this.helper.collective.techCommittee.close(1219      bobAccount,1220      fastTrackHash,1221      techProposalIdx,1222      {1223        refTime: 1_000_000_000,1224        proofSize: 1_000_000,1225      },1226      fastTrack.encodedLength,1227    );1228    console.log('\t* Fast track proposal through technical committee.......DONE');1229    // <<< Fast track proposal through technical committee <<<12301231    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1232    const referendumIndex = democracyStarted.referendumIndex;12331234    // >>> Referendum voting >>>1235    console.log(`\t* Referendum #${referendumIndex} voting.......`);1236    await this.helper.democracy.referendumVote(eveAccount, referendumIndex, {1237      balance: 10_000_000_000_000_000_000n,1238      vote: {aye: true, conviction: 1},1239    });1240    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1241    // <<< Referendum voting <<<12421243    // Wait the proposal to pass1244    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);12451246    await this.helper.wait.newBlocks(1);12471248    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1249  }1250}12511252class WaitGroup {1253  helper: ChainHelperBase;12541255  constructor(helper: ChainHelperBase) {1256    this.helper = helper;1257  }12581259  sleep(milliseconds: number) {1260    return new Promise((resolve) => setTimeout(resolve, milliseconds));1261  }12621263  private async waitWithTimeout(promise: Promise<any>, timeout: number) {1264    let isBlock = false;1265    promise.then(() => isBlock = true).catch(() => isBlock = true);1266    let totalTime = 0;1267    const step = 100;1268    while(!isBlock) {1269      await this.sleep(step);1270      totalTime += step;1271      if(totalTime >= timeout) throw Error('Blocks production failed');1272    }1273    return promise;1274  }12751276  /**1277   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1278   * @param promise async operation to race against the timeout1279   * @param timeoutMS time after which to time out1280   * @param timeoutError error message to throw1281   * @returns promise of the same type the operation had1282   */1283  withTimeout<T>(1284    promise: Promise<T>,1285    timeoutMS = 30000,1286    timeoutError = 'The operation has timed out!',1287  ): Promise<T> {1288    const timeout = new Promise<never>((_, reject) => {1289      setTimeout(() => {1290        reject(new Error(timeoutError));1291      }, timeoutMS);1292    });12931294    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1295  }12961297  /**1298   * Wait for specified number of blocks1299   * @param blocksCount number of blocks to wait1300   * @returns1301   */1302  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1303    timeout = timeout ?? blocksCount * 60_000;1304    // eslint-disable-next-line no-async-promise-executor1305    const promise = new Promise<void>(async (resolve) => {1306      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1307        if(blocksCount > 0) {1308          blocksCount--;1309        } else {1310          unsubscribe();1311          resolve();1312        }1313      });1314    });1315    await this.waitWithTimeout(promise, timeout);1316    return promise;1317  }13181319  /**1320   * Wait for the specified number of sessions to pass.1321   * Only applicable if the Session pallet is turned on.1322   * @param sessionCount number of sessions to wait1323   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1324   * @returns1325   */1326  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1327    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`1328      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');13291330    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1331    let currentSessionIndex = -1;13321333    while(currentSessionIndex < expectedSessionIndex) {1334      // eslint-disable-next-line no-async-promise-executor1335      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1336        await this.newBlocks(1);1337        const res = await (this.helper as DevUniqueHelper).session.getIndex();1338        resolve(res);1339      }), blockTimeout, 'The chain has stopped producing blocks!');1340    }1341  }13421343  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1344    timeout = timeout ?? 30 * 60 * 1000;1345    // eslint-disable-next-line no-async-promise-executor1346    const promise = new Promise<void>(async (resolve) => {1347      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1348        if(data.number.toNumber() >= blockNumber) {1349          unsubscribe();1350          resolve();1351        }1352      });1353    });1354    await this.waitWithTimeout(promise, timeout);1355    return promise;1356  }13571358  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1359    timeout = timeout ?? 30 * 60 * 1000;1360    // eslint-disable-next-line no-async-promise-executor1361    const promise = new Promise<void>(async (resolve) => {1362      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1363        if(data.value.relayParentNumber.toNumber() >= blockNumber) {1364          // @ts-ignore1365          unsubscribe();1366          resolve();1367        }1368      });1369    });1370    await this.waitWithTimeout(promise, timeout);1371    return promise;1372  }13731374  noScheduledTasks() {1375    const api = this.helper.getApi();13761377    // eslint-disable-next-line no-async-promise-executor1378    const promise = new Promise<void>(async resolve => {1379      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1380        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();13811382        if(areThereScheduledTasks.length == 0) {1383          unsubscribe();1384          resolve();1385        }1386      });1387    });13881389    return promise;1390  }13911392  parachainBlockMultiplesOf(val: bigint) {1393    // eslint-disable-next-line no-async-promise-executor1394    const promise = new Promise<void>(async resolve => {1395      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1396        if(data.number.toBigInt() % val == 0n) {1397          console.log(`from waiter: ${data.number.toBigInt()}`);1398          unsubscribe();1399          resolve();1400        }1401      });1402    });1403    return promise;1404  }14051406  event<T extends IEventHelper>(1407    maxBlocksToWait: number,1408    eventHelper: T,1409    filter: (_: any) => boolean = () => true,1410  ): any {1411    // eslint-disable-next-line no-async-promise-executor1412    const promise = new Promise<T | null>(async (resolve) => {1413      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1414        const blockNumber = header.number.toJSON();1415        const blockHash = header.hash;1416        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1417        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;14181419        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);14201421        const apiAt = await this.helper.getApi().at(blockHash);1422        const eventRecords = (await apiAt.query.system.events()) as any;14231424        const neededEvent = eventRecords.toArray()1425          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1426          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1427          .find(filter);14281429        if(neededEvent) {1430          unsubscribe();1431          resolve(neededEvent);1432        } else if(maxBlocksToWait > 0) {1433          maxBlocksToWait--;1434        } else {1435          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1436          The wait lasted until block ${blockNumber} inclusive`);1437          unsubscribe();1438          resolve(null);1439        }1440      });1441    });1442    return promise;1443  }14441445  async expectEvent<T extends IEventHelper>(1446    maxBlocksToWait: number,1447    eventHelper: T,1448    filter: (e: any) => boolean = () => true,1449  ) {1450    const e = await this.event(maxBlocksToWait, eventHelper, filter);1451    if(e == null) {1452      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1453    } else {1454      return e;1455    }1456  }1457}14581459class SessionGroup {1460  helper: ChainHelperBase;14611462  constructor(helper: ChainHelperBase) {1463    this.helper = helper;1464  }14651466  //todo:collator documentation1467  async getIndex(): Promise<number> {1468    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1469  }14701471  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1472    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1473  }14741475  setOwnKeys(signer: TSigner, key: string) {1476    return this.helper.executeExtrinsic(1477      signer,1478      'api.tx.session.setKeys',1479      [key, '0x0'],1480      true,1481    );1482  }14831484  setOwnKeysFromAddress(signer: TSigner) {1485    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1486  }1487}14881489class TestUtilGroup {1490  helper: DevUniqueHelper;14911492  constructor(helper: DevUniqueHelper) {1493    this.helper = helper;1494  }14951496  async enable(testUtilsPalletName: string) {1497    if(this.helper.fetchMissingPalletNames([testUtilsPalletName]).length != 0) {1498      return;1499    }15001501    const signer = this.helper.util.fromSeed('//Alice');1502    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1503  }15041505  async setTestValue(signer: TSigner, testVal: number) {1506    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1507  }15081509  async incTestValue(signer: TSigner) {1510    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1511  }15121513  async setTestValueAndRollback(signer: TSigner, testVal: number) {1514    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1515  }15161517  async testValue(blockIdx?: number) {1518    const api = blockIdx1519      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1520      : this.helper.getApi();15211522    return (await api.query.testUtils.testValue()).toJSON();1523  }15241525  async justTakeFee(signer: TSigner) {1526    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1527  }15281529  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1530    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1531  }1532}15331534class EventCapture {1535  helper: DevUniqueHelper;1536  eventSection: string;1537  eventMethod: string;1538  events: EventRecord[] = [];1539  unsubscribe: VoidFn | null = null;15401541  constructor(1542    helper: DevUniqueHelper,1543    eventSection: string,1544    eventMethod: string,1545  ) {1546    this.helper = helper;1547    this.eventSection = eventSection;1548    this.eventMethod = eventMethod;1549  }15501551  async startCapture() {1552    this.stopCapture();1553    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1554      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);15551556      this.events.push(...newEvents);1557    })) as any;1558  }15591560  stopCapture() {1561    if(this.unsubscribe !== null) {1562      this.unsubscribe();1563    }1564  }15651566  extractCapturedEvents() {1567    return this.events;1568  }1569}15701571class AdminGroup {1572  helper: UniqueHelper;15731574  constructor(helper: UniqueHelper) {1575    this.helper = helper;1576  }15771578  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1579    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1580    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1581      staker: e.event.data[0].toString(),1582      stake: e.event.data[1].toBigInt(),1583      payout: e.event.data[2].toBigInt(),1584    }));1585  }1586}15871588// eslint-disable-next-line @typescript-eslint/naming-convention1589function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1590  return class extends Base {1591    scheduleFn: 'schedule' | 'scheduleAfter';1592    blocksNum: number;1593    options: ISchedulerOptions;15941595    constructor(...args: any[]) {1596      const logger = args[0] as ILogger;1597      const options = args[1] as {1598        scheduleFn: 'schedule' | 'scheduleAfter',1599        blocksNum: number,1600        options: ISchedulerOptions1601      };16021603      super(logger);16041605      this.scheduleFn = options.scheduleFn;1606      this.blocksNum = options.blocksNum;1607      this.options = options.options;1608    }16091610    override executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1611      const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);16121613      const mandatorySchedArgs = [1614        this.blocksNum,1615        this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1616        this.options.priority ?? null,1617        scheduledTx,1618      ];16191620      let schedArgs;1621      let scheduleFn;16221623      if(this.options.scheduledId) {1624        schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];16251626        if(this.scheduleFn == 'schedule') {1627          scheduleFn = 'scheduleNamed';1628        } else if(this.scheduleFn == 'scheduleAfter') {1629          scheduleFn = 'scheduleNamedAfter';1630        }1631      } else {1632        schedArgs = mandatorySchedArgs;1633        scheduleFn = this.scheduleFn;1634      }16351636      const extrinsic = 'api.tx.scheduler.' + scheduleFn;16371638      return super.executeExtrinsic(1639        sender,1640        extrinsic as any,1641        schedArgs,1642        expectSuccess,1643      );1644    }1645  };1646}
after · js-packages/test-utils/index.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import '@unique-nft/opal-testnet-types/augment-api.js';5import '@unique-nft/opal-testnet-types/augment-types.js';6import '@unique-nft/opal-testnet-types/types-lookup.js';78import {stringToU8a} from '@polkadot/util';9import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';10import type {ChainHelperBaseConstructor, UniqueHelperConstructor} from '@unique-nft/playgrounds/unique.js';11import {UniqueHelper, ChainHelperBase, HelperGroup} from '@unique-nft/playgrounds/unique.js';12import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';13import * as defs from '@unique-nft/opal-testnet-types/definitions.js';14import type {IKeyringPair} from '@polkadot/types/types';15import type {EventRecord} from '@polkadot/types/interfaces';16import type {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from '@unique-nft/playgrounds/types.js';17import type {FrameSystemEventRecord, StagingXcmV2TraitsError, StagingXcmV3TraitsOutcome} from '@polkadot/types/lookup';18import type {SignerOptions, VoidFn} from '@polkadot/api/types';19import {spawnSync} from 'child_process';20import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup, HydraDxHelper} from './xcm/index.js';21import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, RankedCollectiveGroup, ReferendaGroup} from './governance.js';22import type {ICollectiveGroup, IFellowshipGroup} from './governance.js';2324export class SilentLogger {25  log(_msg: any, _level: any): void { }26  level = {27    ERROR: 'ERROR' as const,28    WARNING: 'WARNING' as const,29    INFO: 'INFO' as const,30  };31}3233export class SilentConsole {34  // TODO: Remove, this is temporary: Filter unneeded API output35  // (Jaco promised it will be removed in the next version)36  consoleErr: any;37  consoleLog: any;38  consoleWarn: any;3940  constructor() {41    this.consoleErr = console.error;42    this.consoleLog = console.log;43    this.consoleWarn = console.warn;44  }4546  enable() {47    const outFn = (printer: any) => (...args: any[]) => {48      for(const arg of args) {49        if(typeof arg !== 'string')50          continue;51        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'];52        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);53        if(needToSkip || arg === 'Normal connection closure')54          return;55      }56      printer(...args);57    };5859    console.error = outFn(this.consoleErr.bind(console));60    console.log = outFn(this.consoleLog.bind(console));61    console.warn = outFn(this.consoleWarn.bind(console));62  }6364  disable() {65    console.error = this.consoleErr;66    console.log = this.consoleLog;67    console.warn = this.consoleWarn;68  }69}7071export interface IEventHelper {72  section(): string;7374  method(): string;7576  wrapEvent(data: any[]): any;77}7879// eslint-disable-next-line @typescript-eslint/naming-convention80function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {81  const helperClass = class implements IEventHelper {82    wrapEvent: (data: any[]) => any;83    _section: string;84    _method: string;8586    constructor() {87      this.wrapEvent = wrapEvent;88      this._section = section;89      this._method = method;90    }9192    section(): string {93      return this._section;94    }9596    method(): string {97      return this._method;98    }99100    filter(txres: ITransactionResult) {101      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)102        .map(e => this.wrapEvent(e.event.data));103    }104105    find(txres: ITransactionResult) {106      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);107      return e ? this.wrapEvent(e.event.data) : null;108    }109110    expect(txres: ITransactionResult) {111      const e = this.find(txres);112      if(e) {113        return e;114      } else {115        throw Error(`Expected event ${section}.${method}`);116      }117    }118  };119120  return helperClass;121}122123function eventJsonData<T = any>(data: any[], index: number) {124  return data[index].toJSON() as T;125}126127function eventHumanData(data: any[], index: number) {128  return data[index].toHuman();129}130131function eventData<T = any>(data: any[], index: number) {132  return data[index] as T;133}134135// eslint-disable-next-line @typescript-eslint/naming-convention136function EventSection(section: string) {137  return class Section {138    static section = section;139140    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {141      const helperClass = EventHelper(Section.section, name, wrapEvent);142      return new helperClass();143    }144  };145}146147function schedulerSection(schedulerInstance: string) {148  return class extends EventSection(schedulerInstance) {149    static Dispatched = this.Method('Dispatched', data => ({150      task: eventJsonData(data, 0),151      id: eventHumanData(data, 1),152      result: data[2],153    }));154155    static PriorityChanged = this.Method('PriorityChanged', data => ({156      task: eventJsonData(data, 0),157      priority: eventJsonData(data, 1),158    }));159  };160}161162export class Event {163  static Democracy = class extends EventSection('democracy') {164    static Proposed = this.Method('Proposed', data => ({165      proposalIndex: eventJsonData<number>(data, 0),166    }));167168    static ExternalTabled = this.Method('ExternalTabled');169170    static Started = this.Method('Started', data => ({171      referendumIndex: eventJsonData<number>(data, 0),172      threshold: eventHumanData(data, 1),173    }));174175    static Voted = this.Method('Voted', data => ({176      voter: eventJsonData(data, 0),177      referendumIndex: eventJsonData<number>(data, 1),178      vote: eventJsonData(data, 2),179    }));180181    static Passed = this.Method('Passed', data => ({182      referendumIndex: eventJsonData<number>(data, 0),183    }));184185    static ProposalCanceled = this.Method('ProposalCanceled', data => ({186      propIndex: eventJsonData<number>(data, 0),187    }));188189    static Cancelled = this.Method('Cancelled', data => ({190      propIndex: eventJsonData<number>(data, 0),191    }));192193    static Vetoed = this.Method('Vetoed', data => ({194      who: eventHumanData(data, 0),195      proposalHash: eventHumanData(data, 1),196      until: eventJsonData<number>(data, 1),197    }));198  };199200  static Council = class extends EventSection('council') {201    static Proposed = this.Method('Proposed', data => ({202      account: eventHumanData(data, 0),203      proposalIndex: eventJsonData<number>(data, 1),204      proposalHash: eventHumanData(data, 2),205      threshold: eventJsonData<number>(data, 3),206    }));207    static Closed = this.Method('Closed', data => ({208      proposalHash: eventHumanData(data, 0),209      yes: eventJsonData<number>(data, 1),210      no: eventJsonData<number>(data, 2),211    }));212    static Executed = this.Method('Executed', data => ({213      proposalHash: eventHumanData(data, 0),214    }));215  };216217  static FinCouncil = class extends EventSection('financialCouncil') {218    static Proposed = this.Method('Proposed', data => ({219      account: eventHumanData(data, 0),220      proposalIndex: eventJsonData<number>(data, 1),221      proposalHash: eventHumanData(data, 2),222      threshold: eventJsonData<number>(data, 3),223    }));224    static Closed = this.Method('Closed', data => ({225      proposalHash: eventHumanData(data, 0),226      yes: eventJsonData<number>(data, 1),227      no: eventJsonData<number>(data, 2),228    }));229    static Executed = this.Method('Executed', data => ({230      proposalHash: eventHumanData(data, 0),231    }));232  };233234  static TechnicalCommittee = class extends EventSection('technicalCommittee') {235    static Proposed = this.Method('Proposed', data => ({236      account: eventHumanData(data, 0),237      proposalIndex: eventJsonData<number>(data, 1),238      proposalHash: eventHumanData(data, 2),239      threshold: eventJsonData<number>(data, 3),240    }));241    static Closed = this.Method('Closed', data => ({242      proposalHash: eventHumanData(data, 0),243      yes: eventJsonData<number>(data, 1),244      no: eventJsonData<number>(data, 2),245    }));246    static Approved = this.Method('Approved', data => ({247      proposalHash: eventHumanData(data, 0),248    }));249    static Executed = this.Method('Executed', data => ({250      proposalHash: eventHumanData(data, 0),251      result: eventHumanData(data, 1),252    }));253  };254255  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {256    static Submitted = this.Method('Submitted', data => ({257      referendumIndex: eventJsonData<number>(data, 0),258      trackId: eventJsonData<number>(data, 1),259      proposal: eventJsonData(data, 2),260    }));261262    static Cancelled = this.Method('Cancelled', data => ({263      index: eventJsonData<number>(data, 0),264      tally: eventJsonData(data, 1),265    }));266  };267268  static UniqueScheduler = schedulerSection('uniqueScheduler');269  static Scheduler = schedulerSection('scheduler');270271  static XcmpQueue = class extends EventSection('xcmpQueue') {272    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({273      messageHash: eventJsonData(data, 0),274    }));275276    static Success = this.Method('Success', data => ({277      messageHash: eventJsonData(data, 0),278    }));279280    static Fail = this.Method('Fail', data => ({281      messageHash: eventJsonData(data, 0),282      outcome: eventData<StagingXcmV2TraitsError>(data, 2),283    }));284  };285286  static DmpQueue = class extends EventSection('dmpQueue') {287    static ExecutedDownward = this.Method('ExecutedDownward', data => ({288      outcome: eventData<StagingXcmV3TraitsOutcome>(data, 2),289    }));290  };291}292293// eslint-disable-next-line @typescript-eslint/naming-convention294export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {295  return class extends Base {296    constructor(...args: any[]) {297      super(...args);298    }299300    override async executeExtrinsic(301      sender: IKeyringPair,302      extrinsic: string,303      params: any[],304      expectSuccess?: boolean,305      options: Partial<SignerOptions> | null = null,306    ): Promise<ITransactionResult> {307      const call = this.constructApiCall(extrinsic, params);308      const result = await super.executeExtrinsic(309        sender,310        'api.tx.sudo.sudo',311        [call],312        expectSuccess,313        options,314      );315316      if(result.status === 'Fail') return result;317318      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;319      if(data.isErr) {320        if(data.asErr.isModule) {321          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;322          const metaError = super.getApi()?.registry.findMetaError(error);323          throw new Error(`${metaError.section}.${metaError.name}`);324        } else if(data.asErr.isToken) {325          throw new Error(`Token: ${data.asErr.asToken}`);326        }327        // May be [object Object] in case of unhandled non-unit enum328        throw new Error(`Misc: ${data.asErr.toHuman()}`);329      }330      return result;331    }332    override async executeExtrinsicUncheckedWeight(333      sender: IKeyringPair,334      extrinsic: string,335      params: any[],336      expectSuccess?: boolean,337      options: Partial<SignerOptions> | null = null,338    ): Promise<ITransactionResult> {339      const call = this.constructApiCall(extrinsic, params);340      const result = await super.executeExtrinsic(341        sender,342        'api.tx.sudo.sudoUncheckedWeight',343        [call, {refTime: 0, proofSize: 0}],344        expectSuccess,345        options,346      );347348      if(result.status === 'Fail') return result;349350      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;351      if(data.isErr) {352        if(data.asErr.isModule) {353          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;354          const metaError = super.getApi()?.registry.findMetaError(error);355          throw new Error(`${metaError.section}.${metaError.name}`);356        } else if(data.asErr.isToken) {357          throw new Error(`Token: ${data.asErr.asToken}`);358        }359        // May be [object Object] in case of unhandled non-unit enum360        throw new Error(`Misc: ${data.asErr.toHuman()}`);361      }362      return result;363    }364  };365}366367class SchedulerGroup extends HelperGroup<UniqueHelper> {368  constructor(helper: UniqueHelper) {369    super(helper);370  }371372  cancelScheduled(signer: TSigner, scheduledId: string) {373    return this.helper.executeExtrinsic(374      signer,375      'api.tx.scheduler.cancelNamed',376      [scheduledId],377      true,378    );379  }380381  changePriority(signer: TSigner, scheduledId: string, priority: number) {382    return this.helper.executeExtrinsic(383      signer,384      'api.tx.scheduler.changeNamedPriority',385      [scheduledId, priority],386      true,387    );388  }389390  scheduleAt<T extends DevUniqueHelper>(391    executionBlockNumber: number,392    options: ISchedulerOptions = {},393  ) {394    return this.schedule<T>('schedule', executionBlockNumber, options);395  }396397  scheduleAfter<T extends DevUniqueHelper>(398    blocksBeforeExecution: number,399    options: ISchedulerOptions = {},400  ) {401    return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);402  }403404  schedule<T extends UniqueHelper>(405    scheduleFn: 'schedule' | 'scheduleAfter',406    blocksNum: number,407    options: ISchedulerOptions = {},408  ) {409    // eslint-disable-next-line @typescript-eslint/naming-convention410    const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);411    return this.helper.clone(ScheduledHelperType, {412      scheduleFn,413      blocksNum,414      options,415    }) as T;416  }417}418419class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {420  //todo:collator documentation421  addInvulnerable(signer: TSigner, address: string) {422    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);423  }424425  removeInvulnerable(signer: TSigner, address: string) {426    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);427  }428429  async getInvulnerables(): Promise<string[]> {430    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());431  }432433  /** and also total max invulnerables */434  maxCollators(): number {435    return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);436  }437438  async getDesiredCollators(): Promise<number> {439    return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();440  }441442  setLicenseBond(signer: TSigner, amount: bigint) {443    return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);444  }445446  async getLicenseBond(): Promise<bigint> {447    return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();448  }449450  obtainLicense(signer: TSigner) {451    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);452  }453454  releaseLicense(signer: TSigner) {455    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);456  }457458  forceReleaseLicense(signer: TSigner, released: string) {459    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);460  }461462  async hasLicense(address: string): Promise<bigint> {463    return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();464  }465466  onboard(signer: TSigner) {467    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);468  }469470  offboard(signer: TSigner) {471    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);472  }473474  async getCandidates(): Promise<string[]> {475    return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());476  }477}478479export class DevUniqueHelper extends UniqueHelper {480  /**481   * Arrange methods for tests482   */483  arrange: ArrangeGroup;484  wait: WaitGroup;485  admin: AdminGroup;486  session: SessionGroup;487  testUtils: TestUtilGroup;488  foreignAssets: ForeignAssetsGroup;489  xcm: XcmGroup<DevUniqueHelper>;490  xTokens: XTokensGroup<DevUniqueHelper>;491  tokens: TokensGroup<DevUniqueHelper>;492  scheduler: SchedulerGroup;493  collatorSelection: CollatorSelectionGroup;494  council: ICollectiveGroup;495  finCouncil: ICollectiveGroup;496  technicalCommittee: ICollectiveGroup;497  fellowship: IFellowshipGroup;498  democracy: DemocracyGroup;499500  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {501    options.helperBase = options.helperBase ?? DevUniqueHelper;502503    super(logger, options);504    this.arrange = new ArrangeGroup(this);505    this.wait = new WaitGroup(this);506    this.admin = new AdminGroup(this);507    this.testUtils = new TestUtilGroup(this);508    this.session = new SessionGroup(this);509    this.foreignAssets = new ForeignAssetsGroup(this);510    this.xcm = new XcmGroup(this, 'polkadotXcm');511    this.xTokens = new XTokensGroup(this);512    this.tokens = new TokensGroup(this);513    this.scheduler = new SchedulerGroup(this);514    this.collatorSelection = new CollatorSelectionGroup(this);515    this.council = {516      collective: new CollectiveGroup(this, 'council'),517      membership: new CollectiveMembershipGroup(this, 'councilMembership'),518    };519    this.finCouncil = {520      collective: new CollectiveGroup(this, 'financialCouncil'),521      membership: new CollectiveMembershipGroup(this, 'financialCouncilMembership'),522    };523    this.technicalCommittee = {524      collective: new CollectiveGroup(this, 'technicalCommittee'),525      membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),526    };527    this.fellowship = {528      collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),529      referenda: new ReferendaGroup(this, 'fellowshipReferenda'),530    };531    this.democracy = new DemocracyGroup(this);532  }533534  override async connect(wsEndpoint: string, _listeners?: any): Promise<void> {535    if(!wsEndpoint) throw new Error('wsEndpoint was not set');536    const wsProvider = new WsProvider(wsEndpoint);537    this.api = new ApiPromise({538      provider: wsProvider,539      signedExtensions: {540        ContractHelpers: {541          extrinsic: {},542          payload: {},543        },544        CheckMaintenance: {545          extrinsic: {},546          payload: {},547        },548        DisableIdentityCalls: {549          extrinsic: {},550          payload: {},551        },552        FakeTransactionFinalizer: {553          extrinsic: {},554          payload: {},555        },556      },557      rpc: {558        unique: defs.unique.rpc,559        appPromotion: defs.appPromotion.rpc,560        povinfo: defs.povinfo.rpc,561        eth: {562          feeHistory: {563            description: 'Dummy',564            params: [],565            type: 'u8',566          },567          maxPriorityFeePerGas: {568            description: 'Dummy',569            params: [],570            type: 'u8',571          },572        },573      },574    });575    await this.api.isReadyOrError;576    this.network = await UniqueHelper.detectNetwork(this.api);577    this.wsEndpoint = wsEndpoint;578  }579  getSudo<T extends DevUniqueHelper>() {580    // eslint-disable-next-line @typescript-eslint/naming-convention581    const SudoHelperType = SudoHelper(this.helperBase);582    return this.clone(SudoHelperType) as T;583  }584}585586export class DevRelayHelper extends RelayHelper {587  wait: WaitGroup;588589  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {590    options.helperBase = options.helperBase ?? DevRelayHelper;591592    super(logger, options);593    this.wait = new WaitGroup(this);594  }595596  getSudo() {597    // eslint-disable-next-line @typescript-eslint/naming-convention598    const SudoHelperType = SudoHelper(this.helperBase);599    return this.clone(SudoHelperType) as DevRelayHelper;600  }601}602603export class DevWestmintHelper extends WestmintHelper {604  wait: WaitGroup;605606  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {607    options.helperBase = options.helperBase ?? DevWestmintHelper;608609    super(logger, options);610    this.wait = new WaitGroup(this);611  }612}613614export class DevStatemineHelper extends DevWestmintHelper {}615616export class DevStatemintHelper extends DevWestmintHelper {}617618export class DevMoonbeamHelper extends MoonbeamHelper {619  account: MoonbeamAccountGroup;620  wait: WaitGroup;621  fastDemocracy: MoonbeamFastDemocracyGroup;622623  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {624    options.helperBase = options.helperBase ?? DevMoonbeamHelper;625    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';626627    super(logger, options);628    this.account = new MoonbeamAccountGroup(this);629    this.wait = new WaitGroup(this);630    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);631  }632}633634export class DevMoonriverHelper extends DevMoonbeamHelper {635  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {636    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';637    super(logger, options);638  }639}640641export class DevAstarHelper extends AstarHelper {642  wait: WaitGroup;643644  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {645    options.helperBase = options.helperBase ?? DevAstarHelper;646647    super(logger, options);648    this.wait = new WaitGroup(this);649  }650651  getSudo<T extends AstarHelper>() {652    // eslint-disable-next-line @typescript-eslint/naming-convention653    const SudoHelperType = SudoHelper(this.helperBase);654    return this.clone(SudoHelperType) as T;655  }656}657658export class DevShidenHelper extends DevAstarHelper { }659660export class DevAcalaHelper extends AcalaHelper {661  wait: WaitGroup;662663  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {664    options.helperBase = options.helperBase ?? DevAcalaHelper;665666    super(logger, options);667    this.wait = new WaitGroup(this);668  }669  getSudo() {670    // eslint-disable-next-line @typescript-eslint/naming-convention671    const SudoHelperType = SudoHelper(this.helperBase);672    return this.clone(SudoHelperType) as DevAcalaHelper;673  }674}675676export class DevPolkadexHelper extends PolkadexHelper {677  wait: WaitGroup;678  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {679    options.helperBase = options.helperBase ?? PolkadexHelper;680681    super(logger, options);682    this.wait = new WaitGroup(this);683  }684685  getSudo() {686    // eslint-disable-next-line @typescript-eslint/naming-convention687    const SudoHelperType = SudoHelper(this.helperBase);688    return this.clone(SudoHelperType) as DevPolkadexHelper;689  }690}691692export class DevHydraDxHelper extends HydraDxHelper {693  wait: WaitGroup;694  fastDemocracy: HydraFastDemocracyGroup;695696  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {697    options.helperBase = options.helperBase ?? DevHydraDxHelper;698699    super(logger, options);700701    this.wait = new WaitGroup(this);702    this.fastDemocracy = new HydraFastDemocracyGroup(this);703  }704}705706export class DevKaruraHelper extends DevAcalaHelper {}707708export class ArrangeGroup {709  helper: DevUniqueHelper;710711  scheduledIdSlider = 0;712713  constructor(helper: DevUniqueHelper) {714    this.helper = helper;715  }716717  /**718   * Generates accounts with the specified UNQ token balance719   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.720   * @param donor donor account for balances721   * @returns array of newly created accounts722   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);723   */724  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {725    let nonce = await this.helper.chain.getNonce(donor.address);726    const wait = new WaitGroup(this.helper);727    const ss58Format = this.helper.chain.getChainProperties().ss58Format;728    const tokenNominal = this.helper.balance.getOneTokenNominal();729    const transactions = [];730    const accounts: IKeyringPair[] = [];731    for(const balance of balances) {732      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);733      accounts.push(recipient);734      if(balance !== 0n) {735        const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, balance * tokenNominal]);736        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));737        nonce++;738      }739    }740741    await Promise.all(transactions).catch(_e => {});742743    //#region TODO remove this region, when nonce problem will be solved744    const checkBalances = async () => {745      let isSuccess = true;746      for(let i = 0; i < balances.length; i++) {747        const balance = await this.helper.balance.getSubstrate(accounts[i].address);748        if(balance !== balances[i] * tokenNominal) {749          isSuccess = false;750          break;751        }752      }753      return isSuccess;754    };755756    let accountsCreated = false;757    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;758    // checkBalances retry up to 5-50 blocks759    for(let index = 0; index < maxBlocksChecked; index++) {760      accountsCreated = await checkBalances();761      if(accountsCreated) break;762      await wait.newBlocks(1);763    }764765    if(!accountsCreated) throw Error('Accounts generation failed');766    //#endregion767768    return accounts;769  };770771  // TODO combine this method and createAccounts into one772  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {773    const createAsManyAsCan = async () => {774      let transactions: any = [];775      const accounts: IKeyringPair[] = [];776      let nonce = await this.helper.chain.getNonce(donor.address);777      const tokenNominal = this.helper.balance.getOneTokenNominal();778      const ss58Format = this.helper.chain.getChainProperties().ss58Format;779      for(let i = 0; i < accountsToCreate; i++) {780        if(i === 500) { // if there are too many accounts to create781          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled782          transactions = []; //783          nonce = await this.helper.chain.getNonce(donor.address); // update nonce784        }785        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);786        accounts.push(recipient);787        if(withBalance !== 0n) {788          const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, withBalance * tokenNominal]);789          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));790          nonce++;791        }792      }793794      const fullfilledAccounts = [];795      await Promise.allSettled(transactions);796      for(const account of accounts) {797        const accountBalance = await this.helper.balance.getSubstrate(account.address);798        if(accountBalance === withBalance * tokenNominal) {799          fullfilledAccounts.push(account);800        }801      }802      return fullfilledAccounts;803    };804805806    const crowd: IKeyringPair[] = [];807    // do up to 5 retries808    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {809      const asManyAsCan = await createAsManyAsCan();810      crowd.push(...asManyAsCan);811      accountsToCreate -= asManyAsCan.length;812    }813814    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);815816    return crowd;817  };818819  /**820   * Generates one account with zero balance821   * @returns the newly generated account822   * @example const account = await helper.arrange.createEmptyAccount();823   */824  createEmptyAccount = (): IKeyringPair => {825    const ss58Format = this.helper.chain.getChainProperties().ss58Format;826    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);827  };828829  isDevNode = async () => {830    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();831    if(blockNumber == 0) {832      await this.helper.wait.newBlocks(1);833      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();834    }835    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);836    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);837    const findCreationDate = (block: any) => {838      const humanBlock = block.toHuman();839      let date;840      humanBlock.block.extrinsics.forEach((ext: any) => {841        if(ext.method.section === 'timestamp') {842          date = Number(ext.method.args.now.replaceAll(',', ''));843        }844      });845      return date;846    };847    const block1date = await findCreationDate(block1);848    const block2date = await findCreationDate(block2);849    if(block2date! - block1date! < 9000) return true;850    return false;851  };852853  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {854    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);855    let balance = await this.helper.balance.getSubstrate(address);856857    await promise();858859    balance -= await this.helper.balance.getSubstrate(address);860861    return balance;862  }863864  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {865    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);866867    const kvJson: {[key: string]: string} = {};868869    for(const kv of rawPovInfo.keyValues) {870      kvJson[kv.key.toHex()] = kv.value.toHex();871    }872873    const kvStr = JSON.stringify(kvJson);874875    const chainql = spawnSync(876      'chainql',877      [878        `--tla-code=data=${kvStr}`,879        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,880      ],881    );882883    if(!chainql.stdout) {884      throw Error('unable to get an output from the `chainql`');885    }886887    return {888      proofSize: rawPovInfo.proofSize.toNumber(),889      compactProofSize: rawPovInfo.compactProofSize.toNumber(),890      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),891      results: rawPovInfo.results,892      kv: JSON.parse(chainql.stdout.toString()),893    };894  }895896  calculatePalletAddress(palletId: any) {897    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));898    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);899  }900901  makeScheduledIds(num: number): string[] {902    function makeId(slider: number) {903      const scheduledIdSize = 64;904      const hexId = slider.toString(16);905      const prefixSize = scheduledIdSize - hexId.length;906907      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;908909      return scheduledId;910    }911912    const ids = [];913    for(let i = 0; i < num; i++) {914      ids.push(makeId(this.scheduledIdSlider));915      this.scheduledIdSlider += 1;916    }917918    return ids;919  }920921  makeScheduledId(): string {922    return (this.makeScheduledIds(1))[0];923  }924925  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {926    const capture = new EventCapture(this.helper, eventSection, eventMethod);927    await capture.startCapture();928929    return capture;930  }931932  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {933    return {934      V2: [935        {936          WithdrawAsset: [937            {938              id,939              fun: {940                Fungible: amount,941              },942            },943          ],944        },945        {946          BuyExecution: {947            fees: {948              id,949              fun: {950                Fungible: amount,951              },952            },953            weightLimit: 'Unlimited',954          },955        },956        {957          DepositAsset: {958            assets: {959              Wild: 'All',960            },961            maxAssets: 1,962            beneficiary: {963              parents: 0,964              interior: {965                X1: {966                  AccountId32: {967                    network: 'Any',968                    id: beneficiary,969                  },970                },971              },972            },973          },974        },975      ],976    };977  }978979  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {980    return {981      V2: [982        {983          ReserveAssetDeposited: [984            {985              id,986              fun: {987                Fungible: amount,988              },989            },990          ],991        },992        {993          BuyExecution: {994            fees: {995              id,996              fun: {997                Fungible: amount,998              },999            },1000            weightLimit: 'Unlimited',1001          },1002        },1003        {1004          DepositAsset: {1005            assets: {1006              Wild: 'All',1007            },1008            maxAssets: 1,1009            beneficiary: {1010              parents: 0,1011              interior: {1012                X1: {1013                  AccountId32: {1014                    network: 'Any',1015                    id: beneficiary,1016                  },1017                },1018              },1019            },1020          },1021        },1022      ],1023    };1024  }10251026  makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {1027    return {1028      V3: [1029        {1030          UnpaidExecution: {1031            weightLimit: 'Unlimited',1032            checkOrigin: null,1033          },1034        },1035        {1036          Transact: {1037            originKind: 'Superuser',1038            requireWeightAtMost: {1039              refTime: info.weightMultiplier * 200000000,1040              proofSize: info.weightMultiplier * 3000,1041            },1042            call: {1043              encoded: info.call,1044            },1045          },1046        },1047      ],1048    };1049  }1050}10511052class MoonbeamAccountGroup {1053  helper: MoonbeamHelper;10541055  keyring: Keyring;1056  _alithAccount: IKeyringPair;1057  _baltatharAccount: IKeyringPair;1058  _dorothyAccount: IKeyringPair;10591060  constructor(helper: MoonbeamHelper) {1061    this.helper = helper;10621063    this.keyring = new Keyring({type: 'ethereum'});1064    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';1065    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';1066    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';10671068    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');1069    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');1070    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');1071  }10721073  alithAccount() {1074    return this._alithAccount;1075  }10761077  baltatharAccount() {1078    return this._baltatharAccount;1079  }10801081  dorothyAccount() {1082    return this._dorothyAccount;1083  }10841085  create() {1086    return this.keyring.addFromUri(mnemonicGenerate());1087  }1088}10891090class MoonbeamFastDemocracyGroup {1091  helper: DevMoonbeamHelper;10921093  constructor(helper: DevMoonbeamHelper) {1094    this.helper = helper;1095  }10961097  async executeProposal(proposalDesciption: string, encodedProposal: string) {1098    const proposalHash = blake2AsHex(encodedProposal);10991100    const alithAccount = this.helper.account.alithAccount();1101    const baltatharAccount = this.helper.account.baltatharAccount();1102    const dorothyAccount = this.helper.account.dorothyAccount();11031104    const councilVotingThreshold = 2;1105    const technicalCommitteeThreshold = 2;1106    const fastTrackVotingPeriod = 3;1107    const fastTrackDelayPeriod = 0;11081109    console.log(`[democracy] executing '${proposalDesciption}' proposal`);11101111    // >>> Propose external motion through council >>>1112    console.log('\t* Propose external motion through council.......');1113    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1114    const encodedMotion = externalMotion?.method.toHex() || '';1115    const motionHash = blake2AsHex(encodedMotion);1116    console.log('\t* Motion hash is %s', motionHash);11171118    await this.helper.collective.council.propose(1119      baltatharAccount,1120      councilVotingThreshold,1121      externalMotion,1122      externalMotion.encodedLength,1123    );11241125    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1126    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1127    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);11281129    await this.helper.collective.council.close(1130      dorothyAccount,1131      motionHash,1132      councilProposalIdx,1133      {1134        refTime: 1_000_000_000,1135        proofSize: 1_000_000,1136      },1137      externalMotion.encodedLength,1138    );1139    console.log('\t* Propose external motion through council.......DONE');1140    // <<< Propose external motion through council <<<11411142    // >>> Fast track proposal through technical committee >>>1143    console.log('\t* Fast track proposal through technical committee.......');1144    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1145    const encodedFastTrack = fastTrack?.method.toHex() || '';1146    const fastTrackHash = blake2AsHex(encodedFastTrack);1147    console.log('\t* FastTrack hash is %s', fastTrackHash);11481149    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);11501151    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1152    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1153    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);11541155    await this.helper.collective.techCommittee.close(1156      baltatharAccount,1157      fastTrackHash,1158      techProposalIdx,1159      {1160        refTime: 1_000_000_000,1161        proofSize: 1_000_000,1162      },1163      fastTrack.encodedLength,1164    );1165    console.log('\t* Fast track proposal through technical committee.......DONE');1166    // <<< Fast track proposal through technical committee <<<11671168    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1169    const referendumIndex = democracyStarted.referendumIndex;11701171    // >>> Referendum voting >>>1172    console.log(`\t* Referendum #${referendumIndex} voting.......`);1173    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1174      balance: 10_000_000_000_000_000_000n,1175      vote: {aye: true, conviction: 1},1176    });1177    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1178    // <<< Referendum voting <<<11791180    // Wait the proposal to pass1181    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11821183    await this.helper.wait.newBlocks(1);11841185    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1186  }1187}11881189class HydraFastDemocracyGroup {1190  helper: DevHydraDxHelper;11911192  constructor(helper: DevHydraDxHelper) {1193    this.helper = helper;1194  }11951196  async executeProposal(proposalDesciption: string, encodedProposal: string) {1197    const proposalHash = blake2AsHex(encodedProposal);1198    const aliceAccount = this.helper.util.fromSeed('//Alice');1199    const bobAccount = this.helper.util.fromSeed('//Bob');1200    const eveAccount = this.helper.util.fromSeed('//Eve');12011202    const councilVotingThreshold = 1;1203    const technicalCommitteeThreshold = 3;1204    const fastTrackVotingPeriod = 3;1205    const fastTrackDelayPeriod = 0;12061207    console.log(`[democracy] executing '${proposalDesciption}' proposal`);12081209    // >>> Propose external motion through council >>>1210    console.log('\t* Propose external motion through council.......');1211    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1212    const encodedMotion = externalMotion?.method.toHex() || '';1213    const motionHash = blake2AsHex(encodedMotion);1214    console.log('\t* Motion hash is %s', motionHash);12151216    await this.helper.collective.council.propose(1217      aliceAccount,1218      councilVotingThreshold,1219      externalMotion,1220      externalMotion.encodedLength,1221    );12221223    console.log('\t* Propose external motion through council.......DONE');1224    // <<< Propose external motion through council <<<12251226    // >>> Fast track proposal through technical committee >>>1227    console.log('\t* Fast track proposal through technical committee.......');1228    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1229    const encodedFastTrack = fastTrack?.method.toHex() || '';1230    const fastTrackHash = blake2AsHex(encodedFastTrack);1231    console.log('\t* FastTrack hash is %s', fastTrackHash);12321233    await this.helper.collective.techCommittee.propose(aliceAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);12341235    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1236    await this.helper.collective.techCommittee.vote(aliceAccount, fastTrackHash, techProposalIdx, true);1237    await this.helper.collective.techCommittee.vote(bobAccount, fastTrackHash, techProposalIdx, true);1238    await this.helper.collective.techCommittee.vote(eveAccount, fastTrackHash, techProposalIdx, true);12391240    await this.helper.collective.techCommittee.close(1241      bobAccount,1242      fastTrackHash,1243      techProposalIdx,1244      {1245        refTime: 1_000_000_000,1246        proofSize: 1_000_000,1247      },1248      fastTrack.encodedLength,1249    );1250    console.log('\t* Fast track proposal through technical committee.......DONE');1251    // <<< Fast track proposal through technical committee <<<12521253    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1254    const referendumIndex = democracyStarted.referendumIndex;12551256    // >>> Referendum voting >>>1257    console.log(`\t* Referendum #${referendumIndex} voting.......`);1258    await this.helper.democracy.referendumVote(eveAccount, referendumIndex, {1259      balance: 10_000_000_000_000_000_000n,1260      vote: {aye: true, conviction: 1},1261    });1262    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1263    // <<< Referendum voting <<<12641265    // Wait the proposal to pass1266    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);12671268    await this.helper.wait.newBlocks(1);12691270    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1271  }1272}12731274class WaitGroup {1275  helper: ChainHelperBase;12761277  constructor(helper: ChainHelperBase) {1278    this.helper = helper;1279  }12801281  sleep(milliseconds: number) {1282    return new Promise((resolve) => setTimeout(resolve, milliseconds));1283  }12841285  private async waitWithTimeout(promise: Promise<any>, timeout: number) {1286    let isBlock = false;1287    promise.then(() => isBlock = true).catch(() => isBlock = true);1288    let totalTime = 0;1289    const step = 100;1290    while(!isBlock) {1291      await this.sleep(step);1292      totalTime += step;1293      if(totalTime >= timeout) throw Error('Blocks production failed');1294    }1295    return promise;1296  }12971298  /**1299   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1300   * @param promise async operation to race against the timeout1301   * @param timeoutMS time after which to time out1302   * @param timeoutError error message to throw1303   * @returns promise of the same type the operation had1304   */1305  withTimeout<T>(1306    promise: Promise<T>,1307    timeoutMS = 30000,1308    timeoutError = 'The operation has timed out!',1309  ): Promise<T> {1310    const timeout = new Promise<never>((_, reject) => {1311      setTimeout(() => {1312        reject(new Error(timeoutError));1313      }, timeoutMS);1314    });13151316    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1317  }13181319  /**1320   * Wait for specified number of blocks1321   * @param blocksCount number of blocks to wait1322   * @returns1323   */1324  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1325    timeout = timeout ?? blocksCount * 60_000;1326    // eslint-disable-next-line no-async-promise-executor1327    const promise = new Promise<void>(async (resolve) => {1328      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1329        if(blocksCount > 0) {1330          blocksCount--;1331        } else {1332          unsubscribe();1333          resolve();1334        }1335      });1336    });1337    await this.waitWithTimeout(promise, timeout);1338    return promise;1339  }13401341  /**1342   * Wait for the specified number of sessions to pass.1343   * Only applicable if the Session pallet is turned on.1344   * @param sessionCount number of sessions to wait1345   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1346   * @returns1347   */1348  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1349    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`1350      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');13511352    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1353    let currentSessionIndex = -1;13541355    while(currentSessionIndex < expectedSessionIndex) {1356      // eslint-disable-next-line no-async-promise-executor1357      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1358        await this.newBlocks(1);1359        const res = await (this.helper as DevUniqueHelper).session.getIndex();1360        resolve(res);1361      }), blockTimeout, 'The chain has stopped producing blocks!');1362    }1363  }13641365  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1366    timeout = timeout ?? 30 * 60 * 1000;1367    // eslint-disable-next-line no-async-promise-executor1368    const promise = new Promise<void>(async (resolve) => {1369      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1370        if(data.number.toNumber() >= blockNumber) {1371          unsubscribe();1372          resolve();1373        }1374      });1375    });1376    await this.waitWithTimeout(promise, timeout);1377    return promise;1378  }13791380  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1381    timeout = timeout ?? 30 * 60 * 1000;1382    // eslint-disable-next-line no-async-promise-executor1383    const promise = new Promise<void>(async (resolve) => {1384      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1385        if(data.value.relayParentNumber.toNumber() >= blockNumber) {1386          // @ts-ignore1387          unsubscribe();1388          resolve();1389        }1390      });1391    });1392    await this.waitWithTimeout(promise, timeout);1393    return promise;1394  }13951396  noScheduledTasks() {1397    const api = this.helper.getApi();13981399    // eslint-disable-next-line no-async-promise-executor1400    const promise = new Promise<void>(async resolve => {1401      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1402        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();14031404        if(areThereScheduledTasks.length == 0) {1405          unsubscribe();1406          resolve();1407        }1408      });1409    });14101411    return promise;1412  }14131414  parachainBlockMultiplesOf(val: bigint) {1415    // eslint-disable-next-line no-async-promise-executor1416    const promise = new Promise<void>(async resolve => {1417      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1418        if(data.number.toBigInt() % val == 0n) {1419          console.log(`from waiter: ${data.number.toBigInt()}`);1420          unsubscribe();1421          resolve();1422        }1423      });1424    });1425    return promise;1426  }14271428  event<T extends IEventHelper>(1429    maxBlocksToWait: number,1430    eventHelper: T,1431    filter: (_: any) => boolean = () => true,1432  ): any {1433    // eslint-disable-next-line no-async-promise-executor1434    const promise = new Promise<T | null>(async (resolve) => {1435      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1436        const blockNumber = header.number.toJSON();1437        const blockHash = header.hash;1438        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1439        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;14401441        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);14421443        const apiAt = await this.helper.getApi().at(blockHash);1444        const eventRecords = (await apiAt.query.system.events()) as any;14451446        const neededEvent = eventRecords.toArray()1447          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1448          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1449          .find(filter);14501451        if(neededEvent) {1452          unsubscribe();1453          resolve(neededEvent);1454        } else if(maxBlocksToWait > 0) {1455          maxBlocksToWait--;1456        } else {1457          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1458          The wait lasted until block ${blockNumber} inclusive`);1459          unsubscribe();1460          resolve(null);1461        }1462      });1463    });1464    return promise;1465  }14661467  async expectEvent<T extends IEventHelper>(1468    maxBlocksToWait: number,1469    eventHelper: T,1470    filter: (e: any) => boolean = () => true,1471  ) {1472    const e = await this.event(maxBlocksToWait, eventHelper, filter);1473    if(e == null) {1474      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1475    } else {1476      return e;1477    }1478  }1479}14801481class SessionGroup {1482  helper: ChainHelperBase;14831484  constructor(helper: ChainHelperBase) {1485    this.helper = helper;1486  }14871488  //todo:collator documentation1489  async getIndex(): Promise<number> {1490    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1491  }14921493  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1494    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1495  }14961497  setOwnKeys(signer: TSigner, key: string) {1498    return this.helper.executeExtrinsic(1499      signer,1500      'api.tx.session.setKeys',1501      [key, '0x0'],1502      true,1503    );1504  }15051506  setOwnKeysFromAddress(signer: TSigner) {1507    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1508  }1509}15101511class TestUtilGroup {1512  helper: DevUniqueHelper;15131514  constructor(helper: DevUniqueHelper) {1515    this.helper = helper;1516  }15171518  async enable(testUtilsPalletName: string) {1519    if(this.helper.fetchMissingPalletNames([testUtilsPalletName]).length != 0) {1520      return;1521    }15221523    const signer = this.helper.util.fromSeed('//Alice');1524    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1525  }15261527  async setTestValue(signer: TSigner, testVal: number) {1528    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1529  }15301531  async incTestValue(signer: TSigner) {1532    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1533  }15341535  async setTestValueAndRollback(signer: TSigner, testVal: number) {1536    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1537  }15381539  async testValue(blockIdx?: number) {1540    const api = blockIdx1541      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1542      : this.helper.getApi();15431544    return (await api.query.testUtils.testValue()).toJSON();1545  }15461547  async justTakeFee(signer: TSigner) {1548    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1549  }15501551  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1552    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1553  }1554}15551556class EventCapture {1557  helper: DevUniqueHelper;1558  eventSection: string;1559  eventMethod: string;1560  events: EventRecord[] = [];1561  unsubscribe: VoidFn | null = null;15621563  constructor(1564    helper: DevUniqueHelper,1565    eventSection: string,1566    eventMethod: string,1567  ) {1568    this.helper = helper;1569    this.eventSection = eventSection;1570    this.eventMethod = eventMethod;1571  }15721573  async startCapture() {1574    this.stopCapture();1575    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1576      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);15771578      this.events.push(...newEvents);1579    })) as any;1580  }15811582  stopCapture() {1583    if(this.unsubscribe !== null) {1584      this.unsubscribe();1585    }1586  }15871588  extractCapturedEvents() {1589    return this.events;1590  }1591}15921593class AdminGroup {1594  helper: UniqueHelper;15951596  constructor(helper: UniqueHelper) {1597    this.helper = helper;1598  }15991600  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1601    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1602    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1603      staker: e.event.data[0].toString(),1604      stake: e.event.data[1].toBigInt(),1605      payout: e.event.data[2].toBigInt(),1606    }));1607  }1608}16091610// eslint-disable-next-line @typescript-eslint/naming-convention1611function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1612  return class extends Base {1613    scheduleFn: 'schedule' | 'scheduleAfter';1614    blocksNum: number;1615    options: ISchedulerOptions;16161617    constructor(...args: any[]) {1618      const logger = args[0] as ILogger;1619      const options = args[1] as {1620        scheduleFn: 'schedule' | 'scheduleAfter',1621        blocksNum: number,1622        options: ISchedulerOptions1623      };16241625      super(logger);16261627      this.scheduleFn = options.scheduleFn;1628      this.blocksNum = options.blocksNum;1629      this.options = options.options;1630    }16311632    override executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1633      const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);16341635      const mandatorySchedArgs = [1636        this.blocksNum,1637        this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1638        this.options.priority ?? null,1639        scheduledTx,1640      ];16411642      let schedArgs;1643      let scheduleFn;16441645      if(this.options.scheduledId) {1646        schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];16471648        if(this.scheduleFn == 'schedule') {1649          scheduleFn = 'scheduleNamed';1650        } else if(this.scheduleFn == 'scheduleAfter') {1651          scheduleFn = 'scheduleNamedAfter';1652        }1653      } else {1654        schedArgs = mandatorySchedArgs;1655        scheduleFn = this.scheduleFn;1656      }16571658      const extrinsic = 'api.tx.scheduler.' + scheduleFn;16591660      return super.executeExtrinsic(1661        sender,1662        extrinsic as any,1663        schedArgs,1664        expectSuccess,1665      );1666    }1667  };1668}
modifiedjs-packages/test-utils/util.tsdiffbeforeafterboth
--- a/js-packages/test-utils/util.ts
+++ b/js-packages/test-utils/util.ts
@@ -117,6 +117,7 @@
   Identity = 'identity',
   Democracy = 'democracy',
   Council = 'council',
+  FinancialCouncil = 'financialcouncil',
   //CouncilMembership = 'councilmembership',
   TechnicalCommittee = 'technicalcommittee',
   Fellowship = 'fellowshipcollective',
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
@@ -2,7 +2,7 @@
 import type {IKeyringPair} from '@polkadot/types/types';
 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 {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin, initFinCouncil} from './util.js';
 import type {ICounselors} from './util.js';
 
 describeGov('Governance: Council tests', () => {
@@ -192,6 +192,25 @@
     expect(techCommMembers).to.not.contains(techComm.andy.address);
   });
 
+  itSub('Council can remove FinCouncil member', async ({helper}) => {
+    const finCouncil = await initFinCouncil(donor, sudoer);
+    const removeMemberPrpoposal = helper.finCouncil.membership.removeMemberCall(finCouncil.andy.address);
+    await proposalFromMoreThanHalfCouncil(removeMemberPrpoposal);
+
+    const finCouncilMembers = await helper.finCouncil.membership.getMembers();
+    expect(finCouncilMembers).to.not.contains(finCouncil.andy.address);
+  });
+
+  itSub('Council can add FinCouncil member', async ({helper}) => {
+    await initFinCouncil(donor, sudoer);
+    const newFinCouncilMember = helper.arrange.createEmptyAccount();
+    const addMemberPrpoposal = helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address);
+    await proposalFromMoreThanHalfCouncil(addMemberPrpoposal);
+
+    const finCouncilMembers = await helper.finCouncil.membership.getMembers();
+    expect(finCouncilMembers).to.contains(newFinCouncilMember.address);
+  });
+
   itSub.skip('Council member can add Fellowship member', async ({helper}) => {
     const newFellowshipMember = helper.arrange.createEmptyAccount();
     await expect(helper.council.collective.execute(
@@ -328,6 +347,22 @@
     )).to.be.rejectedWith('BadOrigin');
   });
 
+  itSub('[Negative] Council member can\'t add FinCouncil member', async ({helper}) => {
+    const newFinCouncilMember = helper.arrange.createEmptyAccount();
+    await expect(helper.council.collective.execute(
+      counselors.alex,
+      helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address),
+    )).rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] Council member can\'t remove FinCouncil member', async ({helper}) => {
+    const finCouncil = await initFinCouncil(donor, sudoer);
+    await expect(helper.council.collective.execute(
+      counselors.alex,
+      helper.finCouncil.membership.removeMemberCall(finCouncil.ildar.address),
+    )).rejectedWith('BadOrigin');
+  });
+
   itSub('[Negative] Council member cannot promote/demote a Fellowship member', async ({helper}) => {
     const fellowship = await initFellowship(donor, sudoer);
     const memberWithRankOne = fellowship[1][0];
addedjs-packages/tests/sub/governance/financialCouncil.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/js-packages/tests/sub/governance/financialCouncil.test.ts
@@ -0,0 +1,406 @@
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
+import {Event} from '@unique/test-utils';
+import {democracyFastTrackVotingPeriod, IFinCounselors, clearTechComm, dummyProposalCall, initFinCouncil, clearFinCouncil, democracyLaunchPeriod, initFellowship, dummyProposal, fellowshipPropositionOrigin, defaultEnactmentMoment, initCouncil, clearCouncil, clearFellowship} from './util.js';
+
+
+describeGov('Governance: Financial Council tests', () => {
+  let donor: IKeyringPair;
+  let finCounselors: IFinCounselors;
+  let sudoer: IKeyringPair;
+
+  const moreThanHalfCouncilThreshold = 2;
+
+  before(async function() {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      requirePalletsOrSkip(this, helper, [Pallets.FinancialCouncil]);
+      sudoer = await privateKey('//Alice');
+      donor = await privateKey({url: import.meta.url});
+    });
+  });
+
+  beforeEach(async () => {
+    finCounselors = await initFinCouncil(donor, sudoer);
+  });
+
+  afterEach(async () => {
+    await clearFinCouncil(sudoer);
+    await clearTechComm(sudoer);
+  });
+
+  async function proposalFromMoreThanHalfCouncil(proposal: any) {
+    return await usingPlaygrounds(async (helper) => {
+      expect((await helper.finCouncil.membership.getMembers()).length).to.be.equal(3);
+      const proposeResult = await helper.finCouncil.collective.propose(
+        finCounselors.ildar,
+        proposal,
+        moreThanHalfCouncilThreshold,
+      );
+
+      const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult);
+      const proposalIndex = councilProposedEvent.proposalIndex;
+      const proposalHash = councilProposedEvent.proposalHash;
+
+      await helper.finCouncil.collective.vote(finCounselors.greg, proposalHash, proposalIndex, true);
+      await helper.finCouncil.collective.vote(finCounselors.ildar, proposalHash, proposalIndex, true);
+
+      return await helper.finCouncil.collective.close(finCounselors.ildar, proposalHash, proposalIndex);
+    });
+  }
+
+  async function proposalFromAllCouncil(proposal: any) {
+    return await usingPlaygrounds(async (helper) => {
+      expect((await helper.finCouncil.membership.getMembers()).length).to.be.equal(3);
+      const proposeResult = await helper.finCouncil.collective.propose(
+        finCounselors.ildar,
+        proposal,
+        moreThanHalfCouncilThreshold,
+      );
+
+      const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult);
+      const proposalIndex = councilProposedEvent.proposalIndex;
+      const proposalHash = councilProposedEvent.proposalHash;
+
+      await helper.finCouncil.collective.vote(finCounselors.greg, proposalHash, proposalIndex, true);
+      await helper.finCouncil.collective.vote(finCounselors.ildar, proposalHash, proposalIndex, true);
+      await helper.finCouncil.collective.vote(finCounselors.andy, proposalHash, proposalIndex, true);
+
+      return await helper.finCouncil.collective.close(finCounselors.andy, proposalHash, proposalIndex);
+    });
+  }
+
+  itSub('FinCouncil member can register foreign asset', async ({helper}) => {
+    const location = {
+      parents: 1,
+      interior: {X3: [
+        {
+          Parachain: 1000,
+        },
+        {
+          PalletInstance: 50,
+        },
+        {
+          GeneralIndex: 1984,
+        },
+      ]},
+    };
+    const assetId = {Concrete: location};
+
+    const registerForeignAssetCall = helper.constructApiCall(
+      'api.tx.foreignAssets.forceRegisterForeignAsset',
+      [{V3: assetId}, helper.util.str2vec('New Asset'), 'NEW', {Fungible: 10}],
+    );
+
+    await helper.finCouncil.collective.execute(finCounselors.andy, registerForeignAssetCall);
+
+    const asset = await helper.foreignAssets.foreignCollectionId(location);
+    expect(asset).not.null;
+  });
+
+  itSub('[Negative] FinCouncil can\'t fast-track Democracy proposals', async ({helper}) => {
+    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+    await helper.wait.parachainBlockMultiplesOf(35n);
+
+    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+    await expect(proposalFromAllCouncil(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0)))
+      .rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot fast-track Democracy proposals', async ({helper}) => {
+    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil can\'t cancel Democracy proposals', async ({helper}) => {
+    const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+    const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+    await expect(proposalFromAllCouncil(helper.democracy.cancelProposalCall(proposalIndex)))
+      .rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot cancel Democracy proposals', async ({helper}) => {
+    const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+    const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.democracy.cancelProposalCall(proposalIndex),
+    ))
+      .to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil can\'t cancel ongoing Democracy referendums', async ({helper}) => {
+    await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+    const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+    const referendumIndex = startedEvent.referendumIndex;
+
+    await expect(proposalFromAllCouncil(helper.democracy.emergencyCancelCall(referendumIndex)))
+      .rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot cancel ongoing Democracy referendums', async ({helper}) => {
+    await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+    const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+    const referendumIndex = startedEvent.referendumIndex;
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.democracy.emergencyCancelCall(referendumIndex),
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member can\'t veto Democracy proposals', async ({helper}) => {
+    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.democracy.vetoExternalCall(preimageHash),
+    )).rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot blacklist Democracy proposals', async ({helper}) => {
+    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+    await expect(proposalFromAllCouncil(helper.democracy.blacklistCall(preimageHash))).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot blacklist Democracy proposals', async ({helper}) => {
+    const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+    await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.democracy.blacklistCall(preimageHash),
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil can\' cancel Fellowship referendums', async ({helper}) => {
+    const fellowship = await initFellowship(donor, sudoer);
+    const fellowshipProposer = fellowship[5][0];
+    const proposal = dummyProposal(helper);
+
+    const submitResult = await helper.fellowship.referenda.submit(
+      fellowshipProposer,
+      fellowshipPropositionOrigin,
+      proposal,
+      defaultEnactmentMoment,
+    );
+    const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+    await expect(proposalFromAllCouncil(helper.fellowship.referenda.cancelCall(referendumIndex)))
+      .rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] TechComm member cannot cancel Fellowship referendums', async ({helper}) => {
+    const fellowship = await initFellowship(donor, sudoer);
+    const fellowshipProposer = fellowship[5][0];
+    const proposal = dummyProposal(helper);
+
+    const submitResult = await helper.fellowship.referenda.submit(
+      fellowshipProposer,
+      fellowshipPropositionOrigin,
+      proposal,
+      defaultEnactmentMoment,
+    );
+
+    const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.fellowship.referenda.cancelCall(referendumIndex),
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member can\' add a Fellowship member', async ({helper}) => {
+    const newFellowshipMember = helper.arrange.createEmptyAccount();
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.fellowship.collective.addMemberCall(newFellowshipMember.address),
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot submit regular democracy proposal', async ({helper}) => {
+    const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
+
+    await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+    const commiteeProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
+
+    await expect(proposalFromAllCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot submit regular democracy proposal', async ({helper}) => {
+    const memberProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      memberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot externally propose SimpleMajority', async ({helper}) => {
+    const commiteeProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
+
+    await expect(proposalFromAllCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot externally propose SuperMajorityApprove', async ({helper}) => {
+    const commiteeProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
+
+    await expect(proposalFromAllCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+    const memberProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      memberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot externally propose SimpleMajority', async ({helper}) => {
+    const memberProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      memberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot externally propose SuperMajorityApprove', async ({helper}) => {
+    const memberProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      memberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot add/remove a Council member', async ({helper}) => {
+    const newCouncilMember = helper.arrange.createEmptyAccount();
+    const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
+    const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
+
+    await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+    await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot add/remove a Council member', async ({helper}) => {
+    const newCouncilMember = helper.arrange.createEmptyAccount();
+    const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
+    const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      addMemberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      removeMemberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot add/remove a FinCouncil member', async ({helper}) => {
+    const newCouncilMember = helper.arrange.createEmptyAccount();
+    const addMemberProposal = helper.finCouncil.membership.addMemberCall(newCouncilMember.address);
+    const removeMemberProposal = helper.finCouncil.membership.removeMemberCall(finCounselors.ildar.address);
+
+    await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+    await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot add/remove a FinCouncil member', async ({helper}) => {
+    const newCouncilMember = helper.arrange.createEmptyAccount();
+    const addMemberProposal = helper.finCouncil.membership.addMemberCall(newCouncilMember.address);
+    const removeMemberProposal = helper.finCouncil.membership.removeMemberCall(finCounselors.ildar.address);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      addMemberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      removeMemberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot set/clear Council prime member', async ({helper}) => {
+    const counselors = await initCouncil(donor, sudoer);
+    const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
+    const proposalForClear = await helper.council.membership.clearPrimeCall();
+
+    await expect(proposalFromAllCouncil(proposalForSet)).to.be.rejectedWith('BadOrigin');
+    await expect(proposalFromAllCouncil(proposalForClear)).to.be.rejectedWith('BadOrigin');
+    await clearCouncil(sudoer);
+  });
+
+  itSub('[Negative] FinCouncil member cannot set/clear Council prime member', async ({helper}) => {
+    const counselors = await initCouncil(donor, sudoer);
+    const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
+    const proposalForClear = await helper.council.membership.clearPrimeCall();
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      proposalForSet,
+    )).to.be.rejectedWith('BadOrigin');
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      proposalForClear,
+    )).to.be.rejectedWith('BadOrigin');
+    await clearCouncil(sudoer);
+  });
+
+  itSub('[Negative] FinCouncil cannot add/remove a TechComm member', async ({helper}) => {
+    const newCommMember = helper.arrange.createEmptyAccount();
+    const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
+    const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
+
+    await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+    await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil member cannot add/remove a TechComm member', async ({helper}) => {
+    const newCommMember = helper.arrange.createEmptyAccount();
+    const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
+    const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      addMemberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      removeMemberProposal,
+    )).to.be.rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] FinCouncil cannot remove a Fellowship member', async ({helper}) => {
+    const fellowship = await initFellowship(donor, sudoer);
+
+    await expect(proposalFromAllCouncil(helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5))).to.be.rejectedWith('BadOrigin');
+    await clearFellowship(sudoer);
+  });
+
+  itSub('[Negative] FinCouncil member cannot remove a Fellowship member', async ({helper}) => {
+    const fellowship = await initFellowship(donor, sudoer);
+
+    await expect(helper.finCouncil.collective.execute(
+      finCounselors.andy,
+      helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5),
+    )).to.be.rejectedWith('BadOrigin');
+    await clearFellowship(sudoer);
+  });
+
+
+});
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
@@ -130,6 +130,50 @@
     await clearFellowship(sudoer);
   });
 
+  itSub('[Negative] TechComm can\'t add FinCouncil member', async ({helper}) => {
+    const newFinCouncilMember = helper.arrange.createEmptyAccount();
+    const addMemberProposal = helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address);
+    await proposalFromAllCommittee(addMemberProposal);
+
+    const finCouncilMembers = await helper.finCouncil.membership.getMembers();
+    expect(finCouncilMembers).to.contains(newFinCouncilMember.address);
+  });
+
+
+  itSub('[Negative] TechComm member can\'t add FinCouncil member', async ({helper}) => {
+    const newFinCouncilMember = helper.arrange.createEmptyAccount();
+    await expect(helper.technicalCommittee.collective.execute(
+      techcomms.greg,
+      helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address),
+    )).rejectedWith('BadOrigin');
+  });
+
+  itSub('[Negative] TechComm member cannot register foreign asset', async ({helper}) => {
+    const location = {
+      parents: 1,
+      interior: {X3: [
+        {
+          Parachain: 1000,
+        },
+        {
+          PalletInstance: 50,
+        },
+        {
+          GeneralIndex: 1985,
+        },
+      ]},
+    };
+    const assetId = {Concrete: location};
+
+    const foreignAssetProposal = helper.constructApiCall(
+      'api.tx.foreignAssets.forceRegisterForeignAsset',
+      [{V3: assetId}, helper.util.str2vec('New Asset2'), 'NEW', {Fungible: 10}],
+    );
+
+    await expect(helper.technicalCommittee.collective.execute(techcomms.andy, foreignAssetProposal))
+      .to.be.rejectedWith('BadOrigin');
+  });
+
   itSub('[Negative] TechComm cannot submit regular democracy proposal', async ({helper}) => {
     const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
 
modifiedjs-packages/tests/sub/governance/util.tsdiffbeforeafterboth
--- a/js-packages/tests/sub/governance/util.ts
+++ b/js-packages/tests/sub/governance/util.ts
@@ -29,12 +29,62 @@
   filip: IKeyringPair;
   irina: IKeyringPair;
 }
+
+export interface IFinCounselors {
+  greg: IKeyringPair;
+  ildar: IKeyringPair;
+  andy: IKeyringPair;
+}
+
 export interface ITechComms {
     greg: IKeyringPair;
     andy: IKeyringPair;
     constantine: IKeyringPair;
 }
 
+export function initFinCouncil(donor: IKeyringPair, superuser: IKeyringPair): Promise<IFinCounselors> {
+  return usingPlaygrounds(async (helper) => {
+    const [greg, ildar, andy] = await helper.arrange.createAccounts([10_000n, 10_000n, 10_000n], donor);
+    const sudo = helper.getSudo();
+    {
+      const members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON() as [];
+      if(members.length != 0) {
+        await clearFinCouncil(superuser);
+      }
+    }
+    const expectedMembers = [greg, ildar, andy];
+    for(const member of expectedMembers) {
+      await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.addMember', [member.address]);
+    }
+    await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.setPrime', [greg.address]);
+    {
+      const members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON();
+      expect(members).to.containSubset(expectedMembers.map((x: IKeyringPair) => x.address));
+      expect(members.length).to.be.equal(expectedMembers.length);
+    }
+
+    return {
+      greg,
+      ildar,
+      andy,
+    };
+  });
+}
+
+export async function clearFinCouncil(superuser: IKeyringPair) {
+  await usingPlaygrounds(async (helper) => {
+    let members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON();
+    if(members.length) {
+      const sudo = helper.getSudo();
+      for(const address of members) {
+        await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.removeMember', [address]);
+      }
+      members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON();
+    }
+    expect(members).to.be.deep.equal([]);
+  });
+}
+
 export async function initCouncil(donor: IKeyringPair, superuser: IKeyringPair) {
   let counselors: IKeyringPair[] = [];