git.delta.rocks / unique-network / refs/commits / 2fcc0ad3d6a7

difftreelog

test forbid creating ApiPromise without set endpoint

Yaroslav Bolyukin2023-09-11parent: #348c8c0.patch.diff
in: master

4 files changed

modifiedtests/src/.outdated/substrate/substrate-api.tsdiffbeforeafterboth
--- a/tests/src/.outdated/substrate/substrate-api.ts
+++ b/tests/src/.outdated/substrate/substrate-api.ts
@@ -71,6 +71,7 @@
 
 export async function getApiConnection(settings: ApiOptions | undefined = undefined): Promise<ApiPromise> {
   settings = settings || defaultApiOptions();
+  if(!settings.provider) throw new Error('provider was not set');
   const api = new ApiPromise(settings);
 
   if (api) {
modifiedtests/src/eth/createCollection.test.tsdiffbeforeafterboth
--- a/tests/src/eth/createCollection.test.ts
+++ b/tests/src/eth/createCollection.test.ts
@@ -217,7 +217,8 @@
       const collection = helper.nft.getCollectionObject(collectionId);
       const data = (await collection.getData())!;
 
-      expect(collectionCountAfter - collectionCountBefore).to.be.eq(1);
+      // Parallel test safety
+      expect(collectionCountAfter - collectionCountBefore).to.be.gte(1);
       expect(collectionId).to.be.eq(collectionCountAfter);
       expect(data.name).to.be.eq(name);
       expect(data.description).to.be.eq(description);
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
before · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, IPovInfo, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';1617export class SilentLogger {18  log(_msg: any, _level: any): void { }19  level = {20    ERROR: 'ERROR' as const,21    WARNING: 'WARNING' as const,22    INFO: 'INFO' as const,23  };24}2526export class SilentConsole {27  // TODO: Remove, this is temporary: Filter unneeded API output28  // (Jaco promised it will be removed in the next version)29  consoleErr: any;30  consoleLog: any;31  consoleWarn: any;3233  constructor() {34    this.consoleErr = console.error;35    this.consoleLog = console.log;36    this.consoleWarn = console.warn;37  }3839  enable() {40    const outFn = (printer: any) => (...args: any[]) => {41      for(const arg of args) {42        if(typeof arg !== 'string')43          continue;44        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'];45        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);46        if(needToSkip || arg === 'Normal connection closure')47          return;48      }49      printer(...args);50    };5152    console.error = outFn(this.consoleErr.bind(console));53    console.log = outFn(this.consoleLog.bind(console));54    console.warn = outFn(this.consoleWarn.bind(console));55  }5657  disable() {58    console.error = this.consoleErr;59    console.log = this.consoleLog;60    console.warn = this.consoleWarn;61  }62}6364export interface IEventHelper {65  section(): string;6667  method(): string;6869  wrapEvent(data: any[]): any;70}7172// eslint-disable-next-line @typescript-eslint/naming-convention73function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {74  const helperClass = class implements IEventHelper {75    wrapEvent: (data: any[]) => any;76    _section: string;77    _method: string;7879    constructor() {80      this.wrapEvent = wrapEvent;81      this._section = section;82      this._method = method;83    }8485    section(): string {86      return this._section;87    }8889    method(): string {90      return this._method;91    }9293    filter(txres: ITransactionResult) {94      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)95        .map(e => this.wrapEvent(e.event.data));96    }9798    find(txres: ITransactionResult) {99      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);100      return e ? this.wrapEvent(e.event.data) : null;101    }102103    expect(txres: ITransactionResult) {104      const e = this.find(txres);105      if(e) {106        return e;107      } else {108        throw Error(`Expected event ${section}.${method}`);109      }110    }111  };112113  return helperClass;114}115116function eventJsonData<T = any>(data: any[], index: number) {117  return data[index].toJSON() as T;118}119120function eventHumanData(data: any[], index: number) {121  return data[index].toHuman();122}123124function eventData<T = any>(data: any[], index: number) {125  return data[index] as T;126}127128// eslint-disable-next-line @typescript-eslint/naming-convention129function EventSection(section: string) {130  return class Section {131    static section = section;132133    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {134      const helperClass = EventHelper(Section.section, name, wrapEvent);135      return new helperClass();136    }137  };138}139140function schedulerSection(schedulerInstance: string) {141  return class extends EventSection(schedulerInstance) {142    static Dispatched = this.Method('Dispatched', data => ({143      task: eventJsonData(data, 0),144      id: eventHumanData(data, 1),145      result: data[2],146    }));147148    static PriorityChanged = this.Method('PriorityChanged', data => ({149      task: eventJsonData(data, 0),150      priority: eventJsonData(data, 1),151    }));152  };153}154155export class Event {156  static Democracy = class extends EventSection('democracy') {157    static Proposed = this.Method('Proposed', data => ({158      proposalIndex: eventJsonData<number>(data, 0),159    }));160161    static ExternalTabled = this.Method('ExternalTabled');162163    static Started = this.Method('Started', data => ({164      referendumIndex: eventJsonData<number>(data, 0),165      threshold: eventHumanData(data, 1),166    }));167168    static Voted = this.Method('Voted', data => ({169      voter: eventJsonData(data, 0),170      referendumIndex: eventJsonData<number>(data, 1),171      vote: eventJsonData(data, 2),172    }));173174    static Passed = this.Method('Passed', data => ({175      referendumIndex: eventJsonData<number>(data, 0),176    }));177178    static ProposalCanceled = this.Method('ProposalCanceled', data => ({179      propIndex: eventJsonData<number>(data, 0),180    }));181182    static Cancelled = this.Method('Cancelled', data => ({183      propIndex: eventJsonData<number>(data, 0),184    }));185186    static Vetoed = this.Method('Vetoed', data => ({187      who: eventHumanData(data, 0),188      proposalHash: eventHumanData(data, 1),189      until: eventJsonData<number>(data, 1),190    }));191  };192193  static Council = class extends EventSection('council') {194    static Proposed = this.Method('Proposed', data => ({195      account: eventHumanData(data, 0),196      proposalIndex: eventJsonData<number>(data, 1),197      proposalHash: eventHumanData(data, 2),198      threshold: eventJsonData<number>(data, 3),199    }));200    static Closed = this.Method('Closed', data => ({201      proposalHash: eventHumanData(data, 0),202      yes: eventJsonData<number>(data, 1),203      no: eventJsonData<number>(data, 2),204    }));205    static Executed = this.Method('Executed', data => ({206      proposalHash: eventHumanData(data, 0),207    }));208  };209210  static TechnicalCommittee = class extends EventSection('technicalCommittee') {211    static Proposed = this.Method('Proposed', data => ({212      account: eventHumanData(data, 0),213      proposalIndex: eventJsonData<number>(data, 1),214      proposalHash: eventHumanData(data, 2),215      threshold: eventJsonData<number>(data, 3),216    }));217    static Closed = this.Method('Closed', data => ({218      proposalHash: eventHumanData(data, 0),219      yes: eventJsonData<number>(data, 1),220      no: eventJsonData<number>(data, 2),221    }));222    static Approved = this.Method('Approved', data => ({223      proposalHash: eventHumanData(data, 0),224    }));225    static Executed = this.Method('Executed', data => ({226      proposalHash: eventHumanData(data, 0),227      result: eventHumanData(data, 1),228    }));229  };230231  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {232    static Submitted = this.Method('Submitted', data => ({233      referendumIndex: eventJsonData<number>(data, 0),234      trackId: eventJsonData<number>(data, 1),235      proposal: eventJsonData(data, 2),236    }));237238    static Cancelled = this.Method('Cancelled', data => ({239      index: eventJsonData<number>(data, 0),240      tally: eventJsonData(data, 1),241    }));242  };243244  static UniqueScheduler = schedulerSection('uniqueScheduler');245  static Scheduler = schedulerSection('scheduler');246247  static XcmpQueue = class extends EventSection('xcmpQueue') {248    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({249      messageHash: eventJsonData(data, 0),250    }));251252    static Fail = this.Method('Fail', data => ({253      messageHash: eventJsonData(data, 0),254      outcome: eventData<XcmV2TraitsError>(data, 1),255    }));256  };257}258259export class DevUniqueHelper extends UniqueHelper {260  /**261   * Arrange methods for tests262   */263  arrange: ArrangeGroup;264  wait: WaitGroup;265  admin: AdminGroup;266  session: SessionGroup;267  testUtils: TestUtilGroup;268269  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {270    options.helperBase = options.helperBase ?? DevUniqueHelper;271272    super(logger, options);273    this.arrange = new ArrangeGroup(this);274    this.wait = new WaitGroup(this);275    this.admin = new AdminGroup(this);276    this.testUtils = new TestUtilGroup(this);277    this.session = new SessionGroup(this);278  }279280  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {281    const wsProvider = new WsProvider(wsEndpoint);282    this.api = new ApiPromise({283      provider: wsProvider,284      signedExtensions: {285        ContractHelpers: {286          extrinsic: {},287          payload: {},288        },289        CheckMaintenance: {290          extrinsic: {},291          payload: {},292        },293        DisableIdentityCalls: {294          extrinsic: {},295          payload: {},296        },297        FakeTransactionFinalizer: {298          extrinsic: {},299          payload: {},300        },301      },302      rpc: {303        unique: defs.unique.rpc,304        appPromotion: defs.appPromotion.rpc,305        povinfo: defs.povinfo.rpc,306        eth: {307          feeHistory: {308            description: 'Dummy',309            params: [],310            type: 'u8',311          },312          maxPriorityFeePerGas: {313            description: 'Dummy',314            params: [],315            type: 'u8',316          },317        },318      },319    });320    await this.api.isReadyOrError;321    this.network = await UniqueHelper.detectNetwork(this.api);322    this.wsEndpoint = wsEndpoint;323  }324}325326export class DevRelayHelper extends RelayHelper {327  wait: WaitGroup;328329  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {330    options.helperBase = options.helperBase ?? DevRelayHelper;331332    super(logger, options);333    this.wait = new WaitGroup(this);334  }335}336337export class DevWestmintHelper extends WestmintHelper {338  wait: WaitGroup;339340  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {341    options.helperBase = options.helperBase ?? DevWestmintHelper;342343    super(logger, options);344    this.wait = new WaitGroup(this);345  }346}347348export class DevStatemineHelper extends DevWestmintHelper {}349350export class DevStatemintHelper extends DevWestmintHelper {}351352export class DevMoonbeamHelper extends MoonbeamHelper {353  account: MoonbeamAccountGroup;354  wait: WaitGroup;355  fastDemocracy: MoonbeamFastDemocracyGroup;356357  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {358    options.helperBase = options.helperBase ?? DevMoonbeamHelper;359    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';360361    super(logger, options);362    this.account = new MoonbeamAccountGroup(this);363    this.wait = new WaitGroup(this);364    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);365  }366}367368export class DevMoonriverHelper extends DevMoonbeamHelper {369  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {370    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';371    super(logger, options);372  }373}374375export class DevAstarHelper extends AstarHelper {376  wait: WaitGroup;377378  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {379    options.helperBase = options.helperBase ?? DevAstarHelper;380381    super(logger, options);382    this.wait = new WaitGroup(this);383  }384}385386export class DevShidenHelper extends AstarHelper {387  wait: WaitGroup;388389  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {390    options.helperBase = options.helperBase ?? DevShidenHelper;391392    super(logger, options);393    this.wait = new WaitGroup(this);394  }395}396397export class DevAcalaHelper extends AcalaHelper {398  wait: WaitGroup;399400  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {401    options.helperBase = options.helperBase ?? DevAcalaHelper;402403    super(logger, options);404    this.wait = new WaitGroup(this);405  }406}407408export class DevKaruraHelper extends DevAcalaHelper {}409410export class ArrangeGroup {411  helper: DevUniqueHelper;412413  scheduledIdSlider = 0;414415  constructor(helper: DevUniqueHelper) {416    this.helper = helper;417  }418419  /**420   * Generates accounts with the specified UNQ token balance421   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.422   * @param donor donor account for balances423   * @returns array of newly created accounts424   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);425   */426  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {427    let nonce = await this.helper.chain.getNonce(donor.address);428    const wait = new WaitGroup(this.helper);429    const ss58Format = this.helper.chain.getChainProperties().ss58Format;430    const tokenNominal = this.helper.balance.getOneTokenNominal();431    const transactions = [];432    const accounts: IKeyringPair[] = [];433    for(const balance of balances) {434      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);435      accounts.push(recipient);436      if(balance !== 0n) {437        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);438        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));439        nonce++;440      }441    }442443    await Promise.all(transactions).catch(_e => {});444445    //#region TODO remove this region, when nonce problem will be solved446    const checkBalances = async () => {447      let isSuccess = true;448      for(let i = 0; i < balances.length; i++) {449        const balance = await this.helper.balance.getSubstrate(accounts[i].address);450        if(balance !== balances[i] * tokenNominal) {451          isSuccess = false;452          break;453        }454      }455      return isSuccess;456    };457458    let accountsCreated = false;459    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;460    // checkBalances retry up to 5-50 blocks461    for(let index = 0; index < maxBlocksChecked; index++) {462      accountsCreated = await checkBalances();463      if(accountsCreated) break;464      await wait.newBlocks(1);465    }466467    if(!accountsCreated) throw Error('Accounts generation failed');468    //#endregion469470    return accounts;471  };472473  // TODO combine this method and createAccounts into one474  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {475    const createAsManyAsCan = async () => {476      let transactions: any = [];477      const accounts: IKeyringPair[] = [];478      let nonce = await this.helper.chain.getNonce(donor.address);479      const tokenNominal = this.helper.balance.getOneTokenNominal();480      const ss58Format = this.helper.chain.getChainProperties().ss58Format;481      for(let i = 0; i < accountsToCreate; i++) {482        if(i === 500) { // if there are too many accounts to create483          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled484          transactions = []; //485          nonce = await this.helper.chain.getNonce(donor.address); // update nonce486        }487        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);488        accounts.push(recipient);489        if(withBalance !== 0n) {490          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);491          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));492          nonce++;493        }494      }495496      const fullfilledAccounts = [];497      await Promise.allSettled(transactions);498      for(const account of accounts) {499        const accountBalance = await this.helper.balance.getSubstrate(account.address);500        if(accountBalance === withBalance * tokenNominal) {501          fullfilledAccounts.push(account);502        }503      }504      return fullfilledAccounts;505    };506507508    const crowd: IKeyringPair[] = [];509    // do up to 5 retries510    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {511      const asManyAsCan = await createAsManyAsCan();512      crowd.push(...asManyAsCan);513      accountsToCreate -= asManyAsCan.length;514    }515516    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);517518    return crowd;519  };520521  /**522   * Generates one account with zero balance523   * @returns the newly generated account524   * @example const account = await helper.arrange.createEmptyAccount();525   */526  createEmptyAccount = (): IKeyringPair => {527    const ss58Format = this.helper.chain.getChainProperties().ss58Format;528    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);529  };530531  isDevNode = async () => {532    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();533    if(blockNumber == 0) {534      await this.helper.wait.newBlocks(1);535      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();536    }537    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);538    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);539    const findCreationDate = (block: any) => {540      const humanBlock = block.toHuman();541      let date;542      humanBlock.block.extrinsics.forEach((ext: any) => {543        if(ext.method.section === 'timestamp') {544          date = Number(ext.method.args.now.replaceAll(',', ''));545        }546      });547      return date;548    };549    const block1date = await findCreationDate(block1);550    const block2date = await findCreationDate(block2);551    if(block2date! - block1date! < 9000) return true;552  };553554  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {555    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);556    let balance = await this.helper.balance.getSubstrate(address);557558    await promise();559560    balance -= await this.helper.balance.getSubstrate(address);561562    return balance;563  }564565  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {566    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);567568    const kvJson: {[key: string]: string} = {};569570    for(const kv of rawPovInfo.keyValues) {571      kvJson[kv.key.toHex()] = kv.value.toHex();572    }573574    const kvStr = JSON.stringify(kvJson);575576    const chainql = spawnSync(577      'chainql',578      [579        `--tla-code=data=${kvStr}`,580        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,581      ],582    );583584    if(!chainql.stdout) {585      throw Error('unable to get an output from the `chainql`');586    }587588    return {589      proofSize: rawPovInfo.proofSize.toNumber(),590      compactProofSize: rawPovInfo.compactProofSize.toNumber(),591      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),592      results: rawPovInfo.results,593      kv: JSON.parse(chainql.stdout.toString()),594    };595  }596597  calculatePalletAddress(palletId: any) {598    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));599    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);600  }601602  makeScheduledIds(num: number): string[] {603    function makeId(slider: number) {604      const scheduledIdSize = 64;605      const hexId = slider.toString(16);606      const prefixSize = scheduledIdSize - hexId.length;607608      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;609610      return scheduledId;611    }612613    const ids = [];614    for(let i = 0; i < num; i++) {615      ids.push(makeId(this.scheduledIdSlider));616      this.scheduledIdSlider += 1;617    }618619    return ids;620  }621622  makeScheduledId(): string {623    return (this.makeScheduledIds(1))[0];624  }625626  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {627    const capture = new EventCapture(this.helper, eventSection, eventMethod);628    await capture.startCapture();629630    return capture;631  }632633  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {634    return {635      V2: [636        {637          WithdrawAsset: [638            {639              id,640              fun: {641                Fungible: amount,642              },643            },644          ],645        },646        {647          BuyExecution: {648            fees: {649              id,650              fun: {651                Fungible: amount,652              },653            },654            weightLimit: 'Unlimited',655          },656        },657        {658          DepositAsset: {659            assets: {660              Wild: 'All',661            },662            maxAssets: 1,663            beneficiary: {664              parents: 0,665              interior: {666                X1: {667                  AccountId32: {668                    network: 'Any',669                    id: beneficiary,670                  },671                },672              },673            },674          },675        },676      ],677    };678  }679680  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {681    return {682      V2: [683        {684          ReserveAssetDeposited: [685            {686              id,687              fun: {688                Fungible: amount,689              },690            },691          ],692        },693        {694          BuyExecution: {695            fees: {696              id,697              fun: {698                Fungible: amount,699              },700            },701            weightLimit: 'Unlimited',702          },703        },704        {705          DepositAsset: {706            assets: {707              Wild: 'All',708            },709            maxAssets: 1,710            beneficiary: {711              parents: 0,712              interior: {713                X1: {714                  AccountId32: {715                    network: 'Any',716                    id: beneficiary,717                  },718                },719              },720            },721          },722        },723      ],724    };725  }726}727728class MoonbeamAccountGroup {729  helper: MoonbeamHelper;730731  keyring: Keyring;732  _alithAccount: IKeyringPair;733  _baltatharAccount: IKeyringPair;734  _dorothyAccount: IKeyringPair;735736  constructor(helper: MoonbeamHelper) {737    this.helper = helper;738739    this.keyring = new Keyring({type: 'ethereum'});740    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';741    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';742    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';743744    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');745    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');746    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');747  }748749  alithAccount() {750    return this._alithAccount;751  }752753  baltatharAccount() {754    return this._baltatharAccount;755  }756757  dorothyAccount() {758    return this._dorothyAccount;759  }760761  create() {762    return this.keyring.addFromUri(mnemonicGenerate());763  }764}765766class MoonbeamFastDemocracyGroup {767  helper: DevMoonbeamHelper;768769  constructor(helper: DevMoonbeamHelper) {770    this.helper = helper;771  }772773  async executeProposal(proposalDesciption: string, encodedProposal: string) {774    const proposalHash = blake2AsHex(encodedProposal);775776    const alithAccount = this.helper.account.alithAccount();777    const baltatharAccount = this.helper.account.baltatharAccount();778    const dorothyAccount = this.helper.account.dorothyAccount();779780    const councilVotingThreshold = 2;781    const technicalCommitteeThreshold = 2;782    const fastTrackVotingPeriod = 3;783    const fastTrackDelayPeriod = 0;784785    console.log(`[democracy] executing '${proposalDesciption}' proposal`);786787    // >>> Propose external motion through council >>>788    console.log('\t* Propose external motion through council.......');789    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});790    const encodedMotion = externalMotion?.method.toHex() || '';791    const motionHash = blake2AsHex(encodedMotion);792    console.log('\t* Motion hash is %s', motionHash);793794    await this.helper.collective.council.propose(795      baltatharAccount,796      councilVotingThreshold,797      externalMotion,798      externalMotion.encodedLength,799    );800801    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;802    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);803    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);804805    await this.helper.collective.council.close(806      dorothyAccount,807      motionHash,808      councilProposalIdx,809      {810        refTime: 1_000_000_000,811        proofSize: 1_000_000,812      },813      externalMotion.encodedLength,814    );815    console.log('\t* Propose external motion through council.......DONE');816    // <<< Propose external motion through council <<<817818    // >>> Fast track proposal through technical committee >>>819    console.log('\t* Fast track proposal through technical committee.......');820    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);821    const encodedFastTrack = fastTrack?.method.toHex() || '';822    const fastTrackHash = blake2AsHex(encodedFastTrack);823    console.log('\t* FastTrack hash is %s', fastTrackHash);824825    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);826827    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;828    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);829    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);830831    await this.helper.collective.techCommittee.close(832      baltatharAccount,833      fastTrackHash,834      techProposalIdx,835      {836        refTime: 1_000_000_000,837        proofSize: 1_000_000,838      },839      fastTrack.encodedLength,840    );841    console.log('\t* Fast track proposal through technical committee.......DONE');842    // <<< Fast track proposal through technical committee <<<843844    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);845    const referendumIndex = democracyStarted.referendumIndex;846847    // >>> Referendum voting >>>848    console.log(`\t* Referendum #${referendumIndex} voting.......`);849    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {850      balance: 10_000_000_000_000_000_000n,851      vote: {aye: true, conviction: 1},852    });853    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);854    // <<< Referendum voting <<<855856    // Wait the proposal to pass857    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);858859    await this.helper.wait.newBlocks(1);860861    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);862  }863}864865class WaitGroup {866  helper: ChainHelperBase;867868  constructor(helper: ChainHelperBase) {869    this.helper = helper;870  }871872  sleep(milliseconds: number) {873    return new Promise((resolve) => setTimeout(resolve, milliseconds));874  }875876  private async waitWithTimeout(promise: Promise<any>, timeout: number) {877    let isBlock = false;878    promise.then(() => isBlock = true).catch(() => isBlock = true);879    let totalTime = 0;880    const step = 100;881    while(!isBlock) {882      await this.sleep(step);883      totalTime += step;884      if(totalTime >= timeout) throw Error('Blocks production failed');885    }886    return promise;887  }888889  /**890   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.891   * @param promise async operation to race against the timeout892   * @param timeoutMS time after which to time out893   * @param timeoutError error message to throw894   * @returns promise of the same type the operation had895   */896  withTimeout<T>(897    promise: Promise<T>,898    timeoutMS = 30000,899    timeoutError = 'The operation has timed out!',900  ): Promise<T> {901    const timeout = new Promise<never>((_, reject) => {902      setTimeout(() => {903        reject(new Error(timeoutError));904      }, timeoutMS);905    });906907    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});908  }909910  /**911   * Wait for specified number of blocks912   * @param blocksCount number of blocks to wait913   * @returns914   */915  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {916    timeout = timeout ?? blocksCount * 60_000;917    // eslint-disable-next-line no-async-promise-executor918    const promise = new Promise<void>(async (resolve) => {919      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {920        if(blocksCount > 0) {921          blocksCount--;922        } else {923          unsubscribe();924          resolve();925        }926      });927    });928    await this.waitWithTimeout(promise, timeout);929    return promise;930  }931932  /**933   * Wait for the specified number of sessions to pass.934   * Only applicable if the Session pallet is turned on.935   * @param sessionCount number of sessions to wait936   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks937   * @returns938   */939  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {940    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`941      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');942943    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;944    let currentSessionIndex = -1;945946    while(currentSessionIndex < expectedSessionIndex) {947      // eslint-disable-next-line no-async-promise-executor948      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {949        await this.newBlocks(1);950        const res = await (this.helper as DevUniqueHelper).session.getIndex();951        resolve(res);952      }), blockTimeout, 'The chain has stopped producing blocks!');953    }954  }955956  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {957    timeout = timeout ?? 30 * 60 * 1000;958    // eslint-disable-next-line no-async-promise-executor959    const promise = new Promise<void>(async (resolve) => {960      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {961        if(data.number.toNumber() >= blockNumber) {962          unsubscribe();963          resolve();964        }965      });966    });967    await this.waitWithTimeout(promise, timeout);968    return promise;969  }970971  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {972    timeout = timeout ?? 30 * 60 * 1000;973    // eslint-disable-next-line no-async-promise-executor974    const promise = new Promise<void>(async (resolve) => {975      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {976        if(data.value.relayParentNumber.toNumber() >= blockNumber) {977          // @ts-ignore978          unsubscribe();979          resolve();980        }981      });982    });983    await this.waitWithTimeout(promise, timeout);984    return promise;985  }986987  noScheduledTasks() {988    const api = this.helper.getApi();989990    // eslint-disable-next-line no-async-promise-executor991    const promise = new Promise<void>(async resolve => {992      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {993        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();994995        if(areThereScheduledTasks.length == 0) {996          unsubscribe();997          resolve();998        }999      });1000    });10011002    return promise;1003  }10041005  parachainBlockMultiplesOf(val: bigint) {1006    // eslint-disable-next-line no-async-promise-executor1007    const promise = new Promise<void>(async resolve => {1008      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1009        if(data.number.toBigInt() % val == 0n) {1010          console.log(`from waiter: ${data.number.toBigInt()}`);1011          unsubscribe();1012          resolve();1013        }1014      });1015    });1016    return promise;1017  }10181019  event<T extends IEventHelper>(1020    maxBlocksToWait: number,1021    eventHelper: T,1022    filter: (_: any) => boolean = () => true,1023  ): any {1024    // eslint-disable-next-line no-async-promise-executor1025    const promise = new Promise<T | null>(async (resolve) => {1026      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1027        const blockNumber = header.number.toHuman();1028        const blockHash = header.hash;1029        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1030        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;10311032        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);10331034        const apiAt = await this.helper.getApi().at(blockHash);1035        const eventRecords = (await apiAt.query.system.events()) as any;10361037        const neededEvent = eventRecords.toArray()1038          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1039          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1040          .find(filter);10411042        if(neededEvent) {1043          unsubscribe();1044          resolve(neededEvent);1045        } else if(maxBlocksToWait > 0) {1046          maxBlocksToWait--;1047        } else {1048          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);1049          unsubscribe();1050          resolve(null);1051        }1052      });1053    });1054    return promise;1055  }10561057  async expectEvent<T extends IEventHelper>(1058    maxBlocksToWait: number,1059    eventHelper: T,1060    filter: (e: any) => boolean = () => true,1061  ) {1062    const e = await this.event(maxBlocksToWait, eventHelper, filter);1063    if(e == null) {1064      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1065    } else {1066      return e;1067    }1068  }1069}10701071class SessionGroup {1072  helper: ChainHelperBase;10731074  constructor(helper: ChainHelperBase) {1075    this.helper = helper;1076  }10771078  //todo:collator documentation1079  async getIndex(): Promise<number> {1080    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1081  }10821083  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1084    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1085  }10861087  setOwnKeys(signer: TSigner, key: string) {1088    return this.helper.executeExtrinsic(1089      signer,1090      'api.tx.session.setKeys',1091      [key, '0x0'],1092      true,1093    );1094  }10951096  setOwnKeysFromAddress(signer: TSigner) {1097    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1098  }1099}11001101class TestUtilGroup {1102  helper: DevUniqueHelper;11031104  constructor(helper: DevUniqueHelper) {1105    this.helper = helper;1106  }11071108  async enable() {1109    if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1110      return;1111    }11121113    const signer = this.helper.util.fromSeed('//Alice');1114    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1115  }11161117  async setTestValue(signer: TSigner, testVal: number) {1118    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1119  }11201121  async incTestValue(signer: TSigner) {1122    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1123  }11241125  async setTestValueAndRollback(signer: TSigner, testVal: number) {1126    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1127  }11281129  async testValue(blockIdx?: number) {1130    const api = blockIdx1131      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1132      : this.helper.getApi();11331134    return (await api.query.testUtils.testValue()).toJSON();1135  }11361137  async justTakeFee(signer: TSigner) {1138    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1139  }11401141  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1142    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1143  }1144}11451146class EventCapture {1147  helper: DevUniqueHelper;1148  eventSection: string;1149  eventMethod: string;1150  events: EventRecord[] = [];1151  unsubscribe: VoidFn | null = null;11521153  constructor(1154    helper: DevUniqueHelper,1155    eventSection: string,1156    eventMethod: string,1157  ) {1158    this.helper = helper;1159    this.eventSection = eventSection;1160    this.eventMethod = eventMethod;1161  }11621163  async startCapture() {1164    this.stopCapture();1165    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1166      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);11671168      this.events.push(...newEvents);1169    })) as any;1170  }11711172  stopCapture() {1173    if(this.unsubscribe !== null) {1174      this.unsubscribe();1175    }1176  }11771178  extractCapturedEvents() {1179    return this.events;1180  }1181}11821183class AdminGroup {1184  helper: UniqueHelper;11851186  constructor(helper: UniqueHelper) {1187    this.helper = helper;1188  }11891190  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1191    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1192    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1193      staker: e.event.data[0].toString(),1194      stake: e.event.data[1].toBigInt(),1195      payout: e.event.data[2].toBigInt(),1196    }));1197  }1198}
after · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, IPovInfo, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';1617export class SilentLogger {18  log(_msg: any, _level: any): void { }19  level = {20    ERROR: 'ERROR' as const,21    WARNING: 'WARNING' as const,22    INFO: 'INFO' as const,23  };24}2526export class SilentConsole {27  // TODO: Remove, this is temporary: Filter unneeded API output28  // (Jaco promised it will be removed in the next version)29  consoleErr: any;30  consoleLog: any;31  consoleWarn: any;3233  constructor() {34    this.consoleErr = console.error;35    this.consoleLog = console.log;36    this.consoleWarn = console.warn;37  }3839  enable() {40    const outFn = (printer: any) => (...args: any[]) => {41      for(const arg of args) {42        if(typeof arg !== 'string')43          continue;44        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'];45        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);46        if(needToSkip || arg === 'Normal connection closure')47          return;48      }49      printer(...args);50    };5152    console.error = outFn(this.consoleErr.bind(console));53    console.log = outFn(this.consoleLog.bind(console));54    console.warn = outFn(this.consoleWarn.bind(console));55  }5657  disable() {58    console.error = this.consoleErr;59    console.log = this.consoleLog;60    console.warn = this.consoleWarn;61  }62}6364export interface IEventHelper {65  section(): string;6667  method(): string;6869  wrapEvent(data: any[]): any;70}7172// eslint-disable-next-line @typescript-eslint/naming-convention73function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {74  const helperClass = class implements IEventHelper {75    wrapEvent: (data: any[]) => any;76    _section: string;77    _method: string;7879    constructor() {80      this.wrapEvent = wrapEvent;81      this._section = section;82      this._method = method;83    }8485    section(): string {86      return this._section;87    }8889    method(): string {90      return this._method;91    }9293    filter(txres: ITransactionResult) {94      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)95        .map(e => this.wrapEvent(e.event.data));96    }9798    find(txres: ITransactionResult) {99      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);100      return e ? this.wrapEvent(e.event.data) : null;101    }102103    expect(txres: ITransactionResult) {104      const e = this.find(txres);105      if(e) {106        return e;107      } else {108        throw Error(`Expected event ${section}.${method}`);109      }110    }111  };112113  return helperClass;114}115116function eventJsonData<T = any>(data: any[], index: number) {117  return data[index].toJSON() as T;118}119120function eventHumanData(data: any[], index: number) {121  return data[index].toHuman();122}123124function eventData<T = any>(data: any[], index: number) {125  return data[index] as T;126}127128// eslint-disable-next-line @typescript-eslint/naming-convention129function EventSection(section: string) {130  return class Section {131    static section = section;132133    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {134      const helperClass = EventHelper(Section.section, name, wrapEvent);135      return new helperClass();136    }137  };138}139140function schedulerSection(schedulerInstance: string) {141  return class extends EventSection(schedulerInstance) {142    static Dispatched = this.Method('Dispatched', data => ({143      task: eventJsonData(data, 0),144      id: eventHumanData(data, 1),145      result: data[2],146    }));147148    static PriorityChanged = this.Method('PriorityChanged', data => ({149      task: eventJsonData(data, 0),150      priority: eventJsonData(data, 1),151    }));152  };153}154155export class Event {156  static Democracy = class extends EventSection('democracy') {157    static Proposed = this.Method('Proposed', data => ({158      proposalIndex: eventJsonData<number>(data, 0),159    }));160161    static ExternalTabled = this.Method('ExternalTabled');162163    static Started = this.Method('Started', data => ({164      referendumIndex: eventJsonData<number>(data, 0),165      threshold: eventHumanData(data, 1),166    }));167168    static Voted = this.Method('Voted', data => ({169      voter: eventJsonData(data, 0),170      referendumIndex: eventJsonData<number>(data, 1),171      vote: eventJsonData(data, 2),172    }));173174    static Passed = this.Method('Passed', data => ({175      referendumIndex: eventJsonData<number>(data, 0),176    }));177178    static ProposalCanceled = this.Method('ProposalCanceled', data => ({179      propIndex: eventJsonData<number>(data, 0),180    }));181182    static Cancelled = this.Method('Cancelled', data => ({183      propIndex: eventJsonData<number>(data, 0),184    }));185186    static Vetoed = this.Method('Vetoed', data => ({187      who: eventHumanData(data, 0),188      proposalHash: eventHumanData(data, 1),189      until: eventJsonData<number>(data, 1),190    }));191  };192193  static Council = class extends EventSection('council') {194    static Proposed = this.Method('Proposed', data => ({195      account: eventHumanData(data, 0),196      proposalIndex: eventJsonData<number>(data, 1),197      proposalHash: eventHumanData(data, 2),198      threshold: eventJsonData<number>(data, 3),199    }));200    static Closed = this.Method('Closed', data => ({201      proposalHash: eventHumanData(data, 0),202      yes: eventJsonData<number>(data, 1),203      no: eventJsonData<number>(data, 2),204    }));205    static Executed = this.Method('Executed', data => ({206      proposalHash: eventHumanData(data, 0),207    }));208  };209210  static TechnicalCommittee = class extends EventSection('technicalCommittee') {211    static Proposed = this.Method('Proposed', data => ({212      account: eventHumanData(data, 0),213      proposalIndex: eventJsonData<number>(data, 1),214      proposalHash: eventHumanData(data, 2),215      threshold: eventJsonData<number>(data, 3),216    }));217    static Closed = this.Method('Closed', data => ({218      proposalHash: eventHumanData(data, 0),219      yes: eventJsonData<number>(data, 1),220      no: eventJsonData<number>(data, 2),221    }));222    static Approved = this.Method('Approved', data => ({223      proposalHash: eventHumanData(data, 0),224    }));225    static Executed = this.Method('Executed', data => ({226      proposalHash: eventHumanData(data, 0),227      result: eventHumanData(data, 1),228    }));229  };230231  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {232    static Submitted = this.Method('Submitted', data => ({233      referendumIndex: eventJsonData<number>(data, 0),234      trackId: eventJsonData<number>(data, 1),235      proposal: eventJsonData(data, 2),236    }));237238    static Cancelled = this.Method('Cancelled', data => ({239      index: eventJsonData<number>(data, 0),240      tally: eventJsonData(data, 1),241    }));242  };243244  static UniqueScheduler = schedulerSection('uniqueScheduler');245  static Scheduler = schedulerSection('scheduler');246247  static XcmpQueue = class extends EventSection('xcmpQueue') {248    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({249      messageHash: eventJsonData(data, 0),250    }));251252    static Fail = this.Method('Fail', data => ({253      messageHash: eventJsonData(data, 0),254      outcome: eventData<XcmV2TraitsError>(data, 1),255    }));256  };257}258259export class DevUniqueHelper extends UniqueHelper {260  /**261   * Arrange methods for tests262   */263  arrange: ArrangeGroup;264  wait: WaitGroup;265  admin: AdminGroup;266  session: SessionGroup;267  testUtils: TestUtilGroup;268269  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {270    options.helperBase = options.helperBase ?? DevUniqueHelper;271272    super(logger, options);273    this.arrange = new ArrangeGroup(this);274    this.wait = new WaitGroup(this);275    this.admin = new AdminGroup(this);276    this.testUtils = new TestUtilGroup(this);277    this.session = new SessionGroup(this);278  }279280  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {281    if(!wsEndpoint) throw new Error('wsEndpoint was not set');282    const wsProvider = new WsProvider(wsEndpoint);283    this.api = new ApiPromise({284      provider: wsProvider,285      signedExtensions: {286        ContractHelpers: {287          extrinsic: {},288          payload: {},289        },290        CheckMaintenance: {291          extrinsic: {},292          payload: {},293        },294        DisableIdentityCalls: {295          extrinsic: {},296          payload: {},297        },298        FakeTransactionFinalizer: {299          extrinsic: {},300          payload: {},301        },302      },303      rpc: {304        unique: defs.unique.rpc,305        appPromotion: defs.appPromotion.rpc,306        povinfo: defs.povinfo.rpc,307        eth: {308          feeHistory: {309            description: 'Dummy',310            params: [],311            type: 'u8',312          },313          maxPriorityFeePerGas: {314            description: 'Dummy',315            params: [],316            type: 'u8',317          },318        },319      },320    });321    await this.api.isReadyOrError;322    this.network = await UniqueHelper.detectNetwork(this.api);323    this.wsEndpoint = wsEndpoint;324  }325}326327export class DevRelayHelper extends RelayHelper {328  wait: WaitGroup;329330  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {331    options.helperBase = options.helperBase ?? DevRelayHelper;332333    super(logger, options);334    this.wait = new WaitGroup(this);335  }336}337338export class DevWestmintHelper extends WestmintHelper {339  wait: WaitGroup;340341  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {342    options.helperBase = options.helperBase ?? DevWestmintHelper;343344    super(logger, options);345    this.wait = new WaitGroup(this);346  }347}348349export class DevStatemineHelper extends DevWestmintHelper {}350351export class DevStatemintHelper extends DevWestmintHelper {}352353export class DevMoonbeamHelper extends MoonbeamHelper {354  account: MoonbeamAccountGroup;355  wait: WaitGroup;356  fastDemocracy: MoonbeamFastDemocracyGroup;357358  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {359    options.helperBase = options.helperBase ?? DevMoonbeamHelper;360    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';361362    super(logger, options);363    this.account = new MoonbeamAccountGroup(this);364    this.wait = new WaitGroup(this);365    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);366  }367}368369export class DevMoonriverHelper extends DevMoonbeamHelper {370  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {371    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';372    super(logger, options);373  }374}375376export class DevAstarHelper extends AstarHelper {377  wait: WaitGroup;378379  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {380    options.helperBase = options.helperBase ?? DevAstarHelper;381382    super(logger, options);383    this.wait = new WaitGroup(this);384  }385}386387export class DevShidenHelper extends AstarHelper {388  wait: WaitGroup;389390  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {391    options.helperBase = options.helperBase ?? DevShidenHelper;392393    super(logger, options);394    this.wait = new WaitGroup(this);395  }396}397398export class DevAcalaHelper extends AcalaHelper {399  wait: WaitGroup;400401  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {402    options.helperBase = options.helperBase ?? DevAcalaHelper;403404    super(logger, options);405    this.wait = new WaitGroup(this);406  }407}408409export class DevKaruraHelper extends DevAcalaHelper {}410411export class ArrangeGroup {412  helper: DevUniqueHelper;413414  scheduledIdSlider = 0;415416  constructor(helper: DevUniqueHelper) {417    this.helper = helper;418  }419420  /**421   * Generates accounts with the specified UNQ token balance422   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.423   * @param donor donor account for balances424   * @returns array of newly created accounts425   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);426   */427  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {428    let nonce = await this.helper.chain.getNonce(donor.address);429    const wait = new WaitGroup(this.helper);430    const ss58Format = this.helper.chain.getChainProperties().ss58Format;431    const tokenNominal = this.helper.balance.getOneTokenNominal();432    const transactions = [];433    const accounts: IKeyringPair[] = [];434    for(const balance of balances) {435      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);436      accounts.push(recipient);437      if(balance !== 0n) {438        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);439        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));440        nonce++;441      }442    }443444    await Promise.all(transactions).catch(_e => {});445446    //#region TODO remove this region, when nonce problem will be solved447    const checkBalances = async () => {448      let isSuccess = true;449      for(let i = 0; i < balances.length; i++) {450        const balance = await this.helper.balance.getSubstrate(accounts[i].address);451        if(balance !== balances[i] * tokenNominal) {452          isSuccess = false;453          break;454        }455      }456      return isSuccess;457    };458459    let accountsCreated = false;460    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;461    // checkBalances retry up to 5-50 blocks462    for(let index = 0; index < maxBlocksChecked; index++) {463      accountsCreated = await checkBalances();464      if(accountsCreated) break;465      await wait.newBlocks(1);466    }467468    if(!accountsCreated) throw Error('Accounts generation failed');469    //#endregion470471    return accounts;472  };473474  // TODO combine this method and createAccounts into one475  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {476    const createAsManyAsCan = async () => {477      let transactions: any = [];478      const accounts: IKeyringPair[] = [];479      let nonce = await this.helper.chain.getNonce(donor.address);480      const tokenNominal = this.helper.balance.getOneTokenNominal();481      const ss58Format = this.helper.chain.getChainProperties().ss58Format;482      for(let i = 0; i < accountsToCreate; i++) {483        if(i === 500) { // if there are too many accounts to create484          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled485          transactions = []; //486          nonce = await this.helper.chain.getNonce(donor.address); // update nonce487        }488        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);489        accounts.push(recipient);490        if(withBalance !== 0n) {491          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);492          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));493          nonce++;494        }495      }496497      const fullfilledAccounts = [];498      await Promise.allSettled(transactions);499      for(const account of accounts) {500        const accountBalance = await this.helper.balance.getSubstrate(account.address);501        if(accountBalance === withBalance * tokenNominal) {502          fullfilledAccounts.push(account);503        }504      }505      return fullfilledAccounts;506    };507508509    const crowd: IKeyringPair[] = [];510    // do up to 5 retries511    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {512      const asManyAsCan = await createAsManyAsCan();513      crowd.push(...asManyAsCan);514      accountsToCreate -= asManyAsCan.length;515    }516517    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);518519    return crowd;520  };521522  /**523   * Generates one account with zero balance524   * @returns the newly generated account525   * @example const account = await helper.arrange.createEmptyAccount();526   */527  createEmptyAccount = (): IKeyringPair => {528    const ss58Format = this.helper.chain.getChainProperties().ss58Format;529    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);530  };531532  isDevNode = async () => {533    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();534    if(blockNumber == 0) {535      await this.helper.wait.newBlocks(1);536      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();537    }538    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);539    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);540    const findCreationDate = (block: any) => {541      const humanBlock = block.toHuman();542      let date;543      humanBlock.block.extrinsics.forEach((ext: any) => {544        if(ext.method.section === 'timestamp') {545          date = Number(ext.method.args.now.replaceAll(',', ''));546        }547      });548      return date;549    };550    const block1date = await findCreationDate(block1);551    const block2date = await findCreationDate(block2);552    if(block2date! - block1date! < 9000) return true;553  };554555  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {556    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);557    let balance = await this.helper.balance.getSubstrate(address);558559    await promise();560561    balance -= await this.helper.balance.getSubstrate(address);562563    return balance;564  }565566  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {567    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);568569    const kvJson: {[key: string]: string} = {};570571    for(const kv of rawPovInfo.keyValues) {572      kvJson[kv.key.toHex()] = kv.value.toHex();573    }574575    const kvStr = JSON.stringify(kvJson);576577    const chainql = spawnSync(578      'chainql',579      [580        `--tla-code=data=${kvStr}`,581        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,582      ],583    );584585    if(!chainql.stdout) {586      throw Error('unable to get an output from the `chainql`');587    }588589    return {590      proofSize: rawPovInfo.proofSize.toNumber(),591      compactProofSize: rawPovInfo.compactProofSize.toNumber(),592      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),593      results: rawPovInfo.results,594      kv: JSON.parse(chainql.stdout.toString()),595    };596  }597598  calculatePalletAddress(palletId: any) {599    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));600    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);601  }602603  makeScheduledIds(num: number): string[] {604    function makeId(slider: number) {605      const scheduledIdSize = 64;606      const hexId = slider.toString(16);607      const prefixSize = scheduledIdSize - hexId.length;608609      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;610611      return scheduledId;612    }613614    const ids = [];615    for(let i = 0; i < num; i++) {616      ids.push(makeId(this.scheduledIdSlider));617      this.scheduledIdSlider += 1;618    }619620    return ids;621  }622623  makeScheduledId(): string {624    return (this.makeScheduledIds(1))[0];625  }626627  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {628    const capture = new EventCapture(this.helper, eventSection, eventMethod);629    await capture.startCapture();630631    return capture;632  }633634  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {635    return {636      V2: [637        {638          WithdrawAsset: [639            {640              id,641              fun: {642                Fungible: amount,643              },644            },645          ],646        },647        {648          BuyExecution: {649            fees: {650              id,651              fun: {652                Fungible: amount,653              },654            },655            weightLimit: 'Unlimited',656          },657        },658        {659          DepositAsset: {660            assets: {661              Wild: 'All',662            },663            maxAssets: 1,664            beneficiary: {665              parents: 0,666              interior: {667                X1: {668                  AccountId32: {669                    network: 'Any',670                    id: beneficiary,671                  },672                },673              },674            },675          },676        },677      ],678    };679  }680681  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {682    return {683      V2: [684        {685          ReserveAssetDeposited: [686            {687              id,688              fun: {689                Fungible: amount,690              },691            },692          ],693        },694        {695          BuyExecution: {696            fees: {697              id,698              fun: {699                Fungible: amount,700              },701            },702            weightLimit: 'Unlimited',703          },704        },705        {706          DepositAsset: {707            assets: {708              Wild: 'All',709            },710            maxAssets: 1,711            beneficiary: {712              parents: 0,713              interior: {714                X1: {715                  AccountId32: {716                    network: 'Any',717                    id: beneficiary,718                  },719                },720              },721            },722          },723        },724      ],725    };726  }727}728729class MoonbeamAccountGroup {730  helper: MoonbeamHelper;731732  keyring: Keyring;733  _alithAccount: IKeyringPair;734  _baltatharAccount: IKeyringPair;735  _dorothyAccount: IKeyringPair;736737  constructor(helper: MoonbeamHelper) {738    this.helper = helper;739740    this.keyring = new Keyring({type: 'ethereum'});741    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';742    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';743    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';744745    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');746    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');747    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');748  }749750  alithAccount() {751    return this._alithAccount;752  }753754  baltatharAccount() {755    return this._baltatharAccount;756  }757758  dorothyAccount() {759    return this._dorothyAccount;760  }761762  create() {763    return this.keyring.addFromUri(mnemonicGenerate());764  }765}766767class MoonbeamFastDemocracyGroup {768  helper: DevMoonbeamHelper;769770  constructor(helper: DevMoonbeamHelper) {771    this.helper = helper;772  }773774  async executeProposal(proposalDesciption: string, encodedProposal: string) {775    const proposalHash = blake2AsHex(encodedProposal);776777    const alithAccount = this.helper.account.alithAccount();778    const baltatharAccount = this.helper.account.baltatharAccount();779    const dorothyAccount = this.helper.account.dorothyAccount();780781    const councilVotingThreshold = 2;782    const technicalCommitteeThreshold = 2;783    const fastTrackVotingPeriod = 3;784    const fastTrackDelayPeriod = 0;785786    console.log(`[democracy] executing '${proposalDesciption}' proposal`);787788    // >>> Propose external motion through council >>>789    console.log('\t* Propose external motion through council.......');790    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});791    const encodedMotion = externalMotion?.method.toHex() || '';792    const motionHash = blake2AsHex(encodedMotion);793    console.log('\t* Motion hash is %s', motionHash);794795    await this.helper.collective.council.propose(796      baltatharAccount,797      councilVotingThreshold,798      externalMotion,799      externalMotion.encodedLength,800    );801802    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;803    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);804    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);805806    await this.helper.collective.council.close(807      dorothyAccount,808      motionHash,809      councilProposalIdx,810      {811        refTime: 1_000_000_000,812        proofSize: 1_000_000,813      },814      externalMotion.encodedLength,815    );816    console.log('\t* Propose external motion through council.......DONE');817    // <<< Propose external motion through council <<<818819    // >>> Fast track proposal through technical committee >>>820    console.log('\t* Fast track proposal through technical committee.......');821    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);822    const encodedFastTrack = fastTrack?.method.toHex() || '';823    const fastTrackHash = blake2AsHex(encodedFastTrack);824    console.log('\t* FastTrack hash is %s', fastTrackHash);825826    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);827828    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;829    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);830    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);831832    await this.helper.collective.techCommittee.close(833      baltatharAccount,834      fastTrackHash,835      techProposalIdx,836      {837        refTime: 1_000_000_000,838        proofSize: 1_000_000,839      },840      fastTrack.encodedLength,841    );842    console.log('\t* Fast track proposal through technical committee.......DONE');843    // <<< Fast track proposal through technical committee <<<844845    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);846    const referendumIndex = democracyStarted.referendumIndex;847848    // >>> Referendum voting >>>849    console.log(`\t* Referendum #${referendumIndex} voting.......`);850    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {851      balance: 10_000_000_000_000_000_000n,852      vote: {aye: true, conviction: 1},853    });854    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);855    // <<< Referendum voting <<<856857    // Wait the proposal to pass858    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);859860    await this.helper.wait.newBlocks(1);861862    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);863  }864}865866class WaitGroup {867  helper: ChainHelperBase;868869  constructor(helper: ChainHelperBase) {870    this.helper = helper;871  }872873  sleep(milliseconds: number) {874    return new Promise((resolve) => setTimeout(resolve, milliseconds));875  }876877  private async waitWithTimeout(promise: Promise<any>, timeout: number) {878    let isBlock = false;879    promise.then(() => isBlock = true).catch(() => isBlock = true);880    let totalTime = 0;881    const step = 100;882    while(!isBlock) {883      await this.sleep(step);884      totalTime += step;885      if(totalTime >= timeout) throw Error('Blocks production failed');886    }887    return promise;888  }889890  /**891   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.892   * @param promise async operation to race against the timeout893   * @param timeoutMS time after which to time out894   * @param timeoutError error message to throw895   * @returns promise of the same type the operation had896   */897  withTimeout<T>(898    promise: Promise<T>,899    timeoutMS = 30000,900    timeoutError = 'The operation has timed out!',901  ): Promise<T> {902    const timeout = new Promise<never>((_, reject) => {903      setTimeout(() => {904        reject(new Error(timeoutError));905      }, timeoutMS);906    });907908    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});909  }910911  /**912   * Wait for specified number of blocks913   * @param blocksCount number of blocks to wait914   * @returns915   */916  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {917    timeout = timeout ?? blocksCount * 60_000;918    // eslint-disable-next-line no-async-promise-executor919    const promise = new Promise<void>(async (resolve) => {920      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {921        if(blocksCount > 0) {922          blocksCount--;923        } else {924          unsubscribe();925          resolve();926        }927      });928    });929    await this.waitWithTimeout(promise, timeout);930    return promise;931  }932933  /**934   * Wait for the specified number of sessions to pass.935   * Only applicable if the Session pallet is turned on.936   * @param sessionCount number of sessions to wait937   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks938   * @returns939   */940  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {941    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`942      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');943944    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;945    let currentSessionIndex = -1;946947    while(currentSessionIndex < expectedSessionIndex) {948      // eslint-disable-next-line no-async-promise-executor949      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {950        await this.newBlocks(1);951        const res = await (this.helper as DevUniqueHelper).session.getIndex();952        resolve(res);953      }), blockTimeout, 'The chain has stopped producing blocks!');954    }955  }956957  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {958    timeout = timeout ?? 30 * 60 * 1000;959    // eslint-disable-next-line no-async-promise-executor960    const promise = new Promise<void>(async (resolve) => {961      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {962        if(data.number.toNumber() >= blockNumber) {963          unsubscribe();964          resolve();965        }966      });967    });968    await this.waitWithTimeout(promise, timeout);969    return promise;970  }971972  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {973    timeout = timeout ?? 30 * 60 * 1000;974    // eslint-disable-next-line no-async-promise-executor975    const promise = new Promise<void>(async (resolve) => {976      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {977        if(data.value.relayParentNumber.toNumber() >= blockNumber) {978          // @ts-ignore979          unsubscribe();980          resolve();981        }982      });983    });984    await this.waitWithTimeout(promise, timeout);985    return promise;986  }987988  noScheduledTasks() {989    const api = this.helper.getApi();990991    // eslint-disable-next-line no-async-promise-executor992    const promise = new Promise<void>(async resolve => {993      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {994        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();995996        if(areThereScheduledTasks.length == 0) {997          unsubscribe();998          resolve();999        }1000      });1001    });10021003    return promise;1004  }10051006  parachainBlockMultiplesOf(val: bigint) {1007    // eslint-disable-next-line no-async-promise-executor1008    const promise = new Promise<void>(async resolve => {1009      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1010        if(data.number.toBigInt() % val == 0n) {1011          console.log(`from waiter: ${data.number.toBigInt()}`);1012          unsubscribe();1013          resolve();1014        }1015      });1016    });1017    return promise;1018  }10191020  event<T extends IEventHelper>(1021    maxBlocksToWait: number,1022    eventHelper: T,1023    filter: (_: any) => boolean = () => true,1024  ): any {1025    // eslint-disable-next-line no-async-promise-executor1026    const promise = new Promise<T | null>(async (resolve) => {1027      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1028        const blockNumber = header.number.toHuman();1029        const blockHash = header.hash;1030        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1031        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;10321033        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);10341035        const apiAt = await this.helper.getApi().at(blockHash);1036        const eventRecords = (await apiAt.query.system.events()) as any;10371038        const neededEvent = eventRecords.toArray()1039          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1040          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1041          .find(filter);10421043        if(neededEvent) {1044          unsubscribe();1045          resolve(neededEvent);1046        } else if(maxBlocksToWait > 0) {1047          maxBlocksToWait--;1048        } else {1049          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);1050          unsubscribe();1051          resolve(null);1052        }1053      });1054    });1055    return promise;1056  }10571058  async expectEvent<T extends IEventHelper>(1059    maxBlocksToWait: number,1060    eventHelper: T,1061    filter: (e: any) => boolean = () => true,1062  ) {1063    const e = await this.event(maxBlocksToWait, eventHelper, filter);1064    if(e == null) {1065      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1066    } else {1067      return e;1068    }1069  }1070}10711072class SessionGroup {1073  helper: ChainHelperBase;10741075  constructor(helper: ChainHelperBase) {1076    this.helper = helper;1077  }10781079  //todo:collator documentation1080  async getIndex(): Promise<number> {1081    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1082  }10831084  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1085    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1086  }10871088  setOwnKeys(signer: TSigner, key: string) {1089    return this.helper.executeExtrinsic(1090      signer,1091      'api.tx.session.setKeys',1092      [key, '0x0'],1093      true,1094    );1095  }10961097  setOwnKeysFromAddress(signer: TSigner) {1098    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1099  }1100}11011102class TestUtilGroup {1103  helper: DevUniqueHelper;11041105  constructor(helper: DevUniqueHelper) {1106    this.helper = helper;1107  }11081109  async enable() {1110    if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1111      return;1112    }11131114    const signer = this.helper.util.fromSeed('//Alice');1115    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1116  }11171118  async setTestValue(signer: TSigner, testVal: number) {1119    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1120  }11211122  async incTestValue(signer: TSigner) {1123    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1124  }11251126  async setTestValueAndRollback(signer: TSigner, testVal: number) {1127    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1128  }11291130  async testValue(blockIdx?: number) {1131    const api = blockIdx1132      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1133      : this.helper.getApi();11341135    return (await api.query.testUtils.testValue()).toJSON();1136  }11371138  async justTakeFee(signer: TSigner) {1139    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1140  }11411142  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1143    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1144  }1145}11461147class EventCapture {1148  helper: DevUniqueHelper;1149  eventSection: string;1150  eventMethod: string;1151  events: EventRecord[] = [];1152  unsubscribe: VoidFn | null = null;11531154  constructor(1155    helper: DevUniqueHelper,1156    eventSection: string,1157    eventMethod: string,1158  ) {1159    this.helper = helper;1160    this.eventSection = eventSection;1161    this.eventMethod = eventMethod;1162  }11631164  async startCapture() {1165    this.stopCapture();1166    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1167      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);11681169      this.events.push(...newEvents);1170    })) as any;1171  }11721173  stopCapture() {1174    if(this.unsubscribe !== null) {1175      this.unsubscribe();1176    }1177  }11781179  extractCapturedEvents() {1180    return this.events;1181  }1182}11831184class AdminGroup {1185  helper: UniqueHelper;11861187  constructor(helper: UniqueHelper) {1188    this.helper = helper;1189  }11901191  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1192    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1193    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1194      staker: e.event.data[0].toString(),1195      stake: e.event.data[1].toBigInt(),1196      payout: e.event.data[2].toBigInt(),1197    }));1198  }1199}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -492,6 +492,7 @@
   }
 
   static async detectNetworkByWsEndpoint(wsEndpoint: string): Promise<TNetworks> {
+    if(!wsEndpoint) throw new Error('wsEndpoint was not set');
     const api = new ApiPromise({provider: new WsProvider(wsEndpoint)});
     await api.isReady;
 
@@ -507,6 +508,7 @@
     network: TNetworks;
   }> {
     if(typeof network === 'undefined' || network === null) network = 'opal';
+    if(!wsEndpoint) throw new Error('wsEndpoint was not set');
     const supportedRPC = {
       opal: {
         unique: require('@unique-nft/opal-testnet-types/definitions').unique.rpc,