git.delta.rocks / unique-network / refs/commits / 383d45a29a75

difftreelog

source

tests/src/util/playgrounds/unique.dev.ts41.8 KiBsourcehistory
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, ChainHelperBase, ChainHelperBaseConstructor} 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 {SignerOptions, VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';16import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper} from './unique.xcm';1718export class SilentLogger {19  log(_msg: any, _level: any): void { }20  level = {21    ERROR: 'ERROR' as const,22    WARNING: 'WARNING' as const,23    INFO: 'INFO' as const,24  };25}2627export class SilentConsole {28  // TODO: Remove, this is temporary: Filter unneeded API output29  // (Jaco promised it will be removed in the next version)30  consoleErr: any;31  consoleLog: any;32  consoleWarn: any;3334  constructor() {35    this.consoleErr = console.error;36    this.consoleLog = console.log;37    this.consoleWarn = console.warn;38  }3940  enable() {41    const outFn = (printer: any) => (...args: any[]) => {42      for(const arg of args) {43        if(typeof arg !== 'string')44          continue;45        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'];46        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);47        if(needToSkip || arg === 'Normal connection closure')48          return;49      }50      printer(...args);51    };5253    console.error = outFn(this.consoleErr.bind(console));54    console.log = outFn(this.consoleLog.bind(console));55    console.warn = outFn(this.consoleWarn.bind(console));56  }5758  disable() {59    console.error = this.consoleErr;60    console.log = this.consoleLog;61    console.warn = this.consoleWarn;62  }63}6465export interface IEventHelper {66  section(): string;6768  method(): string;6970  wrapEvent(data: any[]): any;71}7273// eslint-disable-next-line @typescript-eslint/naming-convention74function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {75  const helperClass = class implements IEventHelper {76    wrapEvent: (data: any[]) => any;77    _section: string;78    _method: string;7980    constructor() {81      this.wrapEvent = wrapEvent;82      this._section = section;83      this._method = method;84    }8586    section(): string {87      return this._section;88    }8990    method(): string {91      return this._method;92    }9394    filter(txres: ITransactionResult) {95      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)96        .map(e => this.wrapEvent(e.event.data));97    }9899    find(txres: ITransactionResult) {100      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);101      return e ? this.wrapEvent(e.event.data) : null;102    }103104    expect(txres: ITransactionResult) {105      const e = this.find(txres);106      if(e) {107        return e;108      } else {109        throw Error(`Expected event ${section}.${method}`);110      }111    }112  };113114  return helperClass;115}116117function eventJsonData<T = any>(data: any[], index: number) {118  return data[index].toJSON() as T;119}120121function eventHumanData(data: any[], index: number) {122  return data[index].toHuman();123}124125function eventData<T = any>(data: any[], index: number) {126  return data[index] as T;127}128129// eslint-disable-next-line @typescript-eslint/naming-convention130function EventSection(section: string) {131  return class Section {132    static section = section;133134    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {135      const helperClass = EventHelper(Section.section, name, wrapEvent);136      return new helperClass();137    }138  };139}140141function schedulerSection(schedulerInstance: string) {142  return class extends EventSection(schedulerInstance) {143    static Dispatched = this.Method('Dispatched', data => ({144      task: eventJsonData(data, 0),145      id: eventHumanData(data, 1),146      result: data[2],147    }));148149    static PriorityChanged = this.Method('PriorityChanged', data => ({150      task: eventJsonData(data, 0),151      priority: eventJsonData(data, 1),152    }));153  };154}155156export class Event {157  static Democracy = class extends EventSection('democracy') {158    static Proposed = this.Method('Proposed', data => ({159      proposalIndex: eventJsonData<number>(data, 0),160    }));161162    static ExternalTabled = this.Method('ExternalTabled');163164    static Started = this.Method('Started', data => ({165      referendumIndex: eventJsonData<number>(data, 0),166      threshold: eventHumanData(data, 1),167    }));168169    static Voted = this.Method('Voted', data => ({170      voter: eventJsonData(data, 0),171      referendumIndex: eventJsonData<number>(data, 1),172      vote: eventJsonData(data, 2),173    }));174175    static Passed = this.Method('Passed', data => ({176      referendumIndex: eventJsonData<number>(data, 0),177    }));178179    static ProposalCanceled = this.Method('ProposalCanceled', data => ({180      propIndex: eventJsonData<number>(data, 0),181    }));182183    static Cancelled = this.Method('Cancelled', data => ({184      propIndex: eventJsonData<number>(data, 0),185    }));186187    static Vetoed = this.Method('Vetoed', data => ({188      who: eventHumanData(data, 0),189      proposalHash: eventHumanData(data, 1),190      until: eventJsonData<number>(data, 1),191    }));192  };193194  static Council = class extends EventSection('council') {195    static Proposed = this.Method('Proposed', data => ({196      account: eventHumanData(data, 0),197      proposalIndex: eventJsonData<number>(data, 1),198      proposalHash: eventHumanData(data, 2),199      threshold: eventJsonData<number>(data, 3),200    }));201    static Closed = this.Method('Closed', data => ({202      proposalHash: eventHumanData(data, 0),203      yes: eventJsonData<number>(data, 1),204      no: eventJsonData<number>(data, 2),205    }));206    static Executed = this.Method('Executed', data => ({207      proposalHash: eventHumanData(data, 0),208    }));209  };210211  static TechnicalCommittee = class extends EventSection('technicalCommittee') {212    static Proposed = this.Method('Proposed', data => ({213      account: eventHumanData(data, 0),214      proposalIndex: eventJsonData<number>(data, 1),215      proposalHash: eventHumanData(data, 2),216      threshold: eventJsonData<number>(data, 3),217    }));218    static Closed = this.Method('Closed', data => ({219      proposalHash: eventHumanData(data, 0),220      yes: eventJsonData<number>(data, 1),221      no: eventJsonData<number>(data, 2),222    }));223    static Approved = this.Method('Approved', data => ({224      proposalHash: eventHumanData(data, 0),225    }));226    static Executed = this.Method('Executed', data => ({227      proposalHash: eventHumanData(data, 0),228      result: eventHumanData(data, 1),229    }));230  };231232  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {233    static Submitted = this.Method('Submitted', data => ({234      referendumIndex: eventJsonData<number>(data, 0),235      trackId: eventJsonData<number>(data, 1),236      proposal: eventJsonData(data, 2),237    }));238239    static Cancelled = this.Method('Cancelled', data => ({240      index: eventJsonData<number>(data, 0),241      tally: eventJsonData(data, 1),242    }));243  };244245  static UniqueScheduler = schedulerSection('uniqueScheduler');246  static Scheduler = schedulerSection('scheduler');247248  static XcmpQueue = class extends EventSection('xcmpQueue') {249    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({250      messageHash: eventJsonData(data, 0),251    }));252253    static Success = this.Method('Success', data => ({254      messageHash: eventJsonData(data, 0),255    }));256257    static Fail = this.Method('Fail', data => ({258      messageHash: eventJsonData(data, 0),259      outcome: eventData<XcmV2TraitsError>(data, 1),260    }));261  };262}263264// eslint-disable-next-line @typescript-eslint/naming-convention265export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {266  return class extends Base {267    constructor(...args: any[]) {268      super(...args);269    }270271    async executeExtrinsic(272      sender: IKeyringPair,273      extrinsic: string,274      params: any[],275      expectSuccess?: boolean,276      options: Partial<SignerOptions> | null = null,277    ): Promise<ITransactionResult> {278      const call = this.constructApiCall(extrinsic, params);279      const result = await super.executeExtrinsic(280        sender,281        'api.tx.sudo.sudo',282        [call],283        expectSuccess,284        options,285      );286287      if(result.status === 'Fail') return result;288289      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;290      if(data.isErr) {291        if(data.asErr.isModule) {292          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;293          const metaError = super.getApi()?.registry.findMetaError(error);294          throw new Error(`${metaError.section}.${metaError.name}`);295        } else if(data.asErr.isToken) {296          throw new Error(`Token: ${data.asErr.asToken}`);297        }298        // May be [object Object] in case of unhandled non-unit enum299        throw new Error(`Misc: ${data.asErr.toHuman()}`);300      }301      return result;302    }303    async executeExtrinsicUncheckedWeight(304      sender: IKeyringPair,305      extrinsic: string,306      params: any[],307      expectSuccess?: boolean,308      options: Partial<SignerOptions> | null = null,309    ): Promise<ITransactionResult> {310      const call = this.constructApiCall(extrinsic, params);311      const result = await super.executeExtrinsic(312        sender,313        'api.tx.sudo.sudoUncheckedWeight',314        [call, {refTime: 0, proofSize: 0}],315        expectSuccess,316        options,317      );318319      if(result.status === 'Fail') return result;320321      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;322      if(data.isErr) {323        if(data.asErr.isModule) {324          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;325          const metaError = super.getApi()?.registry.findMetaError(error);326          throw new Error(`${metaError.section}.${metaError.name}`);327        } else if(data.asErr.isToken) {328          throw new Error(`Token: ${data.asErr.asToken}`);329        }330        // May be [object Object] in case of unhandled non-unit enum331        throw new Error(`Misc: ${data.asErr.toHuman()}`);332      }333      return result;334    }335  };336}337338export class DevUniqueHelper extends UniqueHelper {339  /**340   * Arrange methods for tests341   */342  arrange: ArrangeGroup;343  wait: WaitGroup;344  admin: AdminGroup;345  session: SessionGroup;346  testUtils: TestUtilGroup;347348  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {349    options.helperBase = options.helperBase ?? DevUniqueHelper;350351    super(logger, options);352    this.arrange = new ArrangeGroup(this);353    this.wait = new WaitGroup(this);354    this.admin = new AdminGroup(this);355    this.testUtils = new TestUtilGroup(this);356    this.session = new SessionGroup(this);357  }358359  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {360    if(!wsEndpoint) throw new Error('wsEndpoint was not set');361    const wsProvider = new WsProvider(wsEndpoint);362    this.api = new ApiPromise({363      provider: wsProvider,364      signedExtensions: {365        ContractHelpers: {366          extrinsic: {},367          payload: {},368        },369        CheckMaintenance: {370          extrinsic: {},371          payload: {},372        },373        DisableIdentityCalls: {374          extrinsic: {},375          payload: {},376        },377        FakeTransactionFinalizer: {378          extrinsic: {},379          payload: {},380        },381      },382      rpc: {383        unique: defs.unique.rpc,384        appPromotion: defs.appPromotion.rpc,385        povinfo: defs.povinfo.rpc,386        eth: {387          feeHistory: {388            description: 'Dummy',389            params: [],390            type: 'u8',391          },392          maxPriorityFeePerGas: {393            description: 'Dummy',394            params: [],395            type: 'u8',396          },397        },398      },399    });400    await this.api.isReadyOrError;401    this.network = await UniqueHelper.detectNetwork(this.api);402    this.wsEndpoint = wsEndpoint;403  }404  getSudo<T extends UniqueHelper>() {405    // eslint-disable-next-line @typescript-eslint/naming-convention406    const SudoHelperType = SudoHelper(this.helperBase);407    return this.clone(SudoHelperType) as T;408  }409}410411export class DevRelayHelper extends RelayHelper {412  wait: WaitGroup;413414  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {415    options.helperBase = options.helperBase ?? DevRelayHelper;416417    super(logger, options);418    this.wait = new WaitGroup(this);419  }420}421422export class DevWestmintHelper extends WestmintHelper {423  wait: WaitGroup;424425  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {426    options.helperBase = options.helperBase ?? DevWestmintHelper;427428    super(logger, options);429    this.wait = new WaitGroup(this);430  }431}432433export class DevStatemineHelper extends DevWestmintHelper {}434435export class DevStatemintHelper extends DevWestmintHelper {}436437export class DevMoonbeamHelper extends MoonbeamHelper {438  account: MoonbeamAccountGroup;439  wait: WaitGroup;440  fastDemocracy: MoonbeamFastDemocracyGroup;441442  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {443    options.helperBase = options.helperBase ?? DevMoonbeamHelper;444    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';445446    super(logger, options);447    this.account = new MoonbeamAccountGroup(this);448    this.wait = new WaitGroup(this);449    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);450  }451}452453export class DevMoonriverHelper extends DevMoonbeamHelper {454  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {455    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';456    super(logger, options);457  }458}459460export class DevAstarHelper extends AstarHelper {461  wait: WaitGroup;462463  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {464    options.helperBase = options.helperBase ?? DevAstarHelper;465466    super(logger, options);467    this.wait = new WaitGroup(this);468  }469470  getSudo<T extends UniqueHelper>() {471    // eslint-disable-next-line @typescript-eslint/naming-convention472    const SudoHelperType = SudoHelper(this.helperBase);473    return this.clone(SudoHelperType) as T;474  }475}476477export class DevShidenHelper extends AstarHelper {478  wait: WaitGroup;479480  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {481    options.helperBase = options.helperBase ?? DevShidenHelper;482483    super(logger, options);484    this.wait = new WaitGroup(this);485  }486}487488export class DevAcalaHelper extends AcalaHelper {489  wait: WaitGroup;490491  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {492    options.helperBase = options.helperBase ?? DevAcalaHelper;493494    super(logger, options);495    this.wait = new WaitGroup(this);496  }497  getSudo<T extends UniqueHelper>() {498    // eslint-disable-next-line @typescript-eslint/naming-convention499    const SudoHelperType = SudoHelper(this.helperBase);500    return this.clone(SudoHelperType) as T;501  }502}503504export class DevPolkadexHelper extends PolkadexHelper {505  wait: WaitGroup;506  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {507    options.helperBase = options.helperBase ?? PolkadexHelper;508509    super(logger, options);510    this.wait = new WaitGroup(this);511  }512513  getSudo<T extends PolkadexHelper>() {514    // eslint-disable-next-line @typescript-eslint/naming-convention515    const SudoHelperType = SudoHelper(this.helperBase);516    return this.clone(SudoHelperType) as T;517  }518}519520export class DevKaruraHelper extends DevAcalaHelper {}521522export class ArrangeGroup {523  helper: DevUniqueHelper;524525  scheduledIdSlider = 0;526527  constructor(helper: DevUniqueHelper) {528    this.helper = helper;529  }530531  /**532   * Generates accounts with the specified UNQ token balance533   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.534   * @param donor donor account for balances535   * @returns array of newly created accounts536   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);537   */538  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {539    let nonce = await this.helper.chain.getNonce(donor.address);540    const wait = new WaitGroup(this.helper);541    const ss58Format = this.helper.chain.getChainProperties().ss58Format;542    const tokenNominal = this.helper.balance.getOneTokenNominal();543    const transactions = [];544    const accounts: IKeyringPair[] = [];545    for(const balance of balances) {546      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);547      accounts.push(recipient);548      if(balance !== 0n) {549        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);550        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));551        nonce++;552      }553    }554555    await Promise.all(transactions).catch(_e => {});556557    //#region TODO remove this region, when nonce problem will be solved558    const checkBalances = async () => {559      let isSuccess = true;560      for(let i = 0; i < balances.length; i++) {561        const balance = await this.helper.balance.getSubstrate(accounts[i].address);562        if(balance !== balances[i] * tokenNominal) {563          isSuccess = false;564          break;565        }566      }567      return isSuccess;568    };569570    let accountsCreated = false;571    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;572    // checkBalances retry up to 5-50 blocks573    for(let index = 0; index < maxBlocksChecked; index++) {574      accountsCreated = await checkBalances();575      if(accountsCreated) break;576      await wait.newBlocks(1);577    }578579    if(!accountsCreated) throw Error('Accounts generation failed');580    //#endregion581582    return accounts;583  };584585  // TODO combine this method and createAccounts into one586  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {587    const createAsManyAsCan = async () => {588      let transactions: any = [];589      const accounts: IKeyringPair[] = [];590      let nonce = await this.helper.chain.getNonce(donor.address);591      const tokenNominal = this.helper.balance.getOneTokenNominal();592      const ss58Format = this.helper.chain.getChainProperties().ss58Format;593      for(let i = 0; i < accountsToCreate; i++) {594        if(i === 500) { // if there are too many accounts to create595          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled596          transactions = []; //597          nonce = await this.helper.chain.getNonce(donor.address); // update nonce598        }599        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);600        accounts.push(recipient);601        if(withBalance !== 0n) {602          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);603          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));604          nonce++;605        }606      }607608      const fullfilledAccounts = [];609      await Promise.allSettled(transactions);610      for(const account of accounts) {611        const accountBalance = await this.helper.balance.getSubstrate(account.address);612        if(accountBalance === withBalance * tokenNominal) {613          fullfilledAccounts.push(account);614        }615      }616      return fullfilledAccounts;617    };618619620    const crowd: IKeyringPair[] = [];621    // do up to 5 retries622    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {623      const asManyAsCan = await createAsManyAsCan();624      crowd.push(...asManyAsCan);625      accountsToCreate -= asManyAsCan.length;626    }627628    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);629630    return crowd;631  };632633  /**634   * Generates one account with zero balance635   * @returns the newly generated account636   * @example const account = await helper.arrange.createEmptyAccount();637   */638  createEmptyAccount = (): IKeyringPair => {639    const ss58Format = this.helper.chain.getChainProperties().ss58Format;640    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);641  };642643  isDevNode = async () => {644    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();645    if(blockNumber == 0) {646      await this.helper.wait.newBlocks(1);647      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();648    }649    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);650    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);651    const findCreationDate = (block: any) => {652      const humanBlock = block.toHuman();653      let date;654      humanBlock.block.extrinsics.forEach((ext: any) => {655        if(ext.method.section === 'timestamp') {656          date = Number(ext.method.args.now.replaceAll(',', ''));657        }658      });659      return date;660    };661    const block1date = await findCreationDate(block1);662    const block2date = await findCreationDate(block2);663    if(block2date! - block1date! < 9000) return true;664  };665666  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {667    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);668    let balance = await this.helper.balance.getSubstrate(address);669670    await promise();671672    balance -= await this.helper.balance.getSubstrate(address);673674    return balance;675  }676677  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {678    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);679680    const kvJson: {[key: string]: string} = {};681682    for(const kv of rawPovInfo.keyValues) {683      kvJson[kv.key.toHex()] = kv.value.toHex();684    }685686    const kvStr = JSON.stringify(kvJson);687688    const chainql = spawnSync(689      'chainql',690      [691        `--tla-code=data=${kvStr}`,692        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,693      ],694    );695696    if(!chainql.stdout) {697      throw Error('unable to get an output from the `chainql`');698    }699700    return {701      proofSize: rawPovInfo.proofSize.toNumber(),702      compactProofSize: rawPovInfo.compactProofSize.toNumber(),703      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),704      results: rawPovInfo.results,705      kv: JSON.parse(chainql.stdout.toString()),706    };707  }708709  calculatePalletAddress(palletId: any) {710    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));711    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);712  }713714  makeScheduledIds(num: number): string[] {715    function makeId(slider: number) {716      const scheduledIdSize = 64;717      const hexId = slider.toString(16);718      const prefixSize = scheduledIdSize - hexId.length;719720      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;721722      return scheduledId;723    }724725    const ids = [];726    for(let i = 0; i < num; i++) {727      ids.push(makeId(this.scheduledIdSlider));728      this.scheduledIdSlider += 1;729    }730731    return ids;732  }733734  makeScheduledId(): string {735    return (this.makeScheduledIds(1))[0];736  }737738  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {739    const capture = new EventCapture(this.helper, eventSection, eventMethod);740    await capture.startCapture();741742    return capture;743  }744745  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {746    return {747      V2: [748        {749          WithdrawAsset: [750            {751              id,752              fun: {753                Fungible: amount,754              },755            },756          ],757        },758        {759          BuyExecution: {760            fees: {761              id,762              fun: {763                Fungible: amount,764              },765            },766            weightLimit: 'Unlimited',767          },768        },769        {770          DepositAsset: {771            assets: {772              Wild: 'All',773            },774            maxAssets: 1,775            beneficiary: {776              parents: 0,777              interior: {778                X1: {779                  AccountId32: {780                    network: 'Any',781                    id: beneficiary,782                  },783                },784              },785            },786          },787        },788      ],789    };790  }791792  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {793    return {794      V2: [795        {796          ReserveAssetDeposited: [797            {798              id,799              fun: {800                Fungible: amount,801              },802            },803          ],804        },805        {806          BuyExecution: {807            fees: {808              id,809              fun: {810                Fungible: amount,811              },812            },813            weightLimit: 'Unlimited',814          },815        },816        {817          DepositAsset: {818            assets: {819              Wild: 'All',820            },821            maxAssets: 1,822            beneficiary: {823              parents: 0,824              interior: {825                X1: {826                  AccountId32: {827                    network: 'Any',828                    id: beneficiary,829                  },830                },831              },832            },833          },834        },835      ],836    };837  }838}839840class MoonbeamAccountGroup {841  helper: MoonbeamHelper;842843  keyring: Keyring;844  _alithAccount: IKeyringPair;845  _baltatharAccount: IKeyringPair;846  _dorothyAccount: IKeyringPair;847848  constructor(helper: MoonbeamHelper) {849    this.helper = helper;850851    this.keyring = new Keyring({type: 'ethereum'});852    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';853    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';854    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';855856    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');857    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');858    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');859  }860861  alithAccount() {862    return this._alithAccount;863  }864865  baltatharAccount() {866    return this._baltatharAccount;867  }868869  dorothyAccount() {870    return this._dorothyAccount;871  }872873  create() {874    return this.keyring.addFromUri(mnemonicGenerate());875  }876}877878class MoonbeamFastDemocracyGroup {879  helper: DevMoonbeamHelper;880881  constructor(helper: DevMoonbeamHelper) {882    this.helper = helper;883  }884885  async executeProposal(proposalDesciption: string, encodedProposal: string) {886    const proposalHash = blake2AsHex(encodedProposal);887888    const alithAccount = this.helper.account.alithAccount();889    const baltatharAccount = this.helper.account.baltatharAccount();890    const dorothyAccount = this.helper.account.dorothyAccount();891892    const councilVotingThreshold = 2;893    const technicalCommitteeThreshold = 2;894    const fastTrackVotingPeriod = 3;895    const fastTrackDelayPeriod = 0;896897    console.log(`[democracy] executing '${proposalDesciption}' proposal`);898899    // >>> Propose external motion through council >>>900    console.log('\t* Propose external motion through council.......');901    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});902    const encodedMotion = externalMotion?.method.toHex() || '';903    const motionHash = blake2AsHex(encodedMotion);904    console.log('\t* Motion hash is %s', motionHash);905906    await this.helper.collective.council.propose(907      baltatharAccount,908      councilVotingThreshold,909      externalMotion,910      externalMotion.encodedLength,911    );912913    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;914    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);915    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);916917    await this.helper.collective.council.close(918      dorothyAccount,919      motionHash,920      councilProposalIdx,921      {922        refTime: 1_000_000_000,923        proofSize: 1_000_000,924      },925      externalMotion.encodedLength,926    );927    console.log('\t* Propose external motion through council.......DONE');928    // <<< Propose external motion through council <<<929930    // >>> Fast track proposal through technical committee >>>931    console.log('\t* Fast track proposal through technical committee.......');932    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);933    const encodedFastTrack = fastTrack?.method.toHex() || '';934    const fastTrackHash = blake2AsHex(encodedFastTrack);935    console.log('\t* FastTrack hash is %s', fastTrackHash);936937    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);938939    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;940    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);941    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);942943    await this.helper.collective.techCommittee.close(944      baltatharAccount,945      fastTrackHash,946      techProposalIdx,947      {948        refTime: 1_000_000_000,949        proofSize: 1_000_000,950      },951      fastTrack.encodedLength,952    );953    console.log('\t* Fast track proposal through technical committee.......DONE');954    // <<< Fast track proposal through technical committee <<<955956    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);957    const referendumIndex = democracyStarted.referendumIndex;958959    // >>> Referendum voting >>>960    console.log(`\t* Referendum #${referendumIndex} voting.......`);961    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {962      balance: 10_000_000_000_000_000_000n,963      vote: {aye: true, conviction: 1},964    });965    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);966    // <<< Referendum voting <<<967968    // Wait the proposal to pass969    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);970971    await this.helper.wait.newBlocks(1);972973    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);974  }975}976977class WaitGroup {978  helper: ChainHelperBase;979980  constructor(helper: ChainHelperBase) {981    this.helper = helper;982  }983984  sleep(milliseconds: number) {985    return new Promise((resolve) => setTimeout(resolve, milliseconds));986  }987988  private async waitWithTimeout(promise: Promise<any>, timeout: number) {989    let isBlock = false;990    promise.then(() => isBlock = true).catch(() => isBlock = true);991    let totalTime = 0;992    const step = 100;993    while(!isBlock) {994      await this.sleep(step);995      totalTime += step;996      if(totalTime >= timeout) throw Error('Blocks production failed');997    }998    return promise;999  }10001001  /**1002   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1003   * @param promise async operation to race against the timeout1004   * @param timeoutMS time after which to time out1005   * @param timeoutError error message to throw1006   * @returns promise of the same type the operation had1007   */1008  withTimeout<T>(1009    promise: Promise<T>,1010    timeoutMS = 30000,1011    timeoutError = 'The operation has timed out!',1012  ): Promise<T> {1013    const timeout = new Promise<never>((_, reject) => {1014      setTimeout(() => {1015        reject(new Error(timeoutError));1016      }, timeoutMS);1017    });10181019    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1020  }10211022  /**1023   * Wait for specified number of blocks1024   * @param blocksCount number of blocks to wait1025   * @returns1026   */1027  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1028    timeout = timeout ?? blocksCount * 60_000;1029    // eslint-disable-next-line no-async-promise-executor1030    const promise = new Promise<void>(async (resolve) => {1031      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1032        if(blocksCount > 0) {1033          blocksCount--;1034        } else {1035          unsubscribe();1036          resolve();1037        }1038      });1039    });1040    await this.waitWithTimeout(promise, timeout);1041    return promise;1042  }10431044  /**1045   * Wait for the specified number of sessions to pass.1046   * Only applicable if the Session pallet is turned on.1047   * @param sessionCount number of sessions to wait1048   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1049   * @returns1050   */1051  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1052    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`1053      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');10541055    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1056    let currentSessionIndex = -1;10571058    while(currentSessionIndex < expectedSessionIndex) {1059      // eslint-disable-next-line no-async-promise-executor1060      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1061        await this.newBlocks(1);1062        const res = await (this.helper as DevUniqueHelper).session.getIndex();1063        resolve(res);1064      }), blockTimeout, 'The chain has stopped producing blocks!');1065    }1066  }10671068  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1069    timeout = timeout ?? 30 * 60 * 1000;1070    // eslint-disable-next-line no-async-promise-executor1071    const promise = new Promise<void>(async (resolve) => {1072      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1073        if(data.number.toNumber() >= blockNumber) {1074          unsubscribe();1075          resolve();1076        }1077      });1078    });1079    await this.waitWithTimeout(promise, timeout);1080    return promise;1081  }10821083  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1084    timeout = timeout ?? 30 * 60 * 1000;1085    // eslint-disable-next-line no-async-promise-executor1086    const promise = new Promise<void>(async (resolve) => {1087      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1088        if(data.value.relayParentNumber.toNumber() >= blockNumber) {1089          // @ts-ignore1090          unsubscribe();1091          resolve();1092        }1093      });1094    });1095    await this.waitWithTimeout(promise, timeout);1096    return promise;1097  }10981099  noScheduledTasks() {1100    const api = this.helper.getApi();11011102    // eslint-disable-next-line no-async-promise-executor1103    const promise = new Promise<void>(async resolve => {1104      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1105        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();11061107        if(areThereScheduledTasks.length == 0) {1108          unsubscribe();1109          resolve();1110        }1111      });1112    });11131114    return promise;1115  }11161117  parachainBlockMultiplesOf(val: bigint) {1118    // eslint-disable-next-line no-async-promise-executor1119    const promise = new Promise<void>(async resolve => {1120      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1121        if(data.number.toBigInt() % val == 0n) {1122          console.log(`from waiter: ${data.number.toBigInt()}`);1123          unsubscribe();1124          resolve();1125        }1126      });1127    });1128    return promise;1129  }11301131  event<T extends IEventHelper>(1132    maxBlocksToWait: number,1133    eventHelper: T,1134    filter: (_: any) => boolean = () => true,1135  ): any {1136    // eslint-disable-next-line no-async-promise-executor1137    const promise = new Promise<T | null>(async (resolve) => {1138      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1139        const blockNumber = header.number.toHuman();1140        const blockHash = header.hash;1141        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1142        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;11431144        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);11451146        const apiAt = await this.helper.getApi().at(blockHash);1147        const eventRecords = (await apiAt.query.system.events()) as any;11481149        const neededEvent = eventRecords.toArray()1150          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1151          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1152          .find(filter);11531154        if(neededEvent) {1155          unsubscribe();1156          resolve(neededEvent);1157        } else if(maxBlocksToWait > 0) {1158          maxBlocksToWait--;1159        } else {1160          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);1161          unsubscribe();1162          resolve(null);1163        }1164      });1165    });1166    return promise;1167  }11681169  async expectEvent<T extends IEventHelper>(1170    maxBlocksToWait: number,1171    eventHelper: T,1172    filter: (e: any) => boolean = () => true,1173  ) {1174    const e = await this.event(maxBlocksToWait, eventHelper, filter);1175    if(e == null) {1176      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1177    } else {1178      return e;1179    }1180  }1181}11821183class SessionGroup {1184  helper: ChainHelperBase;11851186  constructor(helper: ChainHelperBase) {1187    this.helper = helper;1188  }11891190  //todo:collator documentation1191  async getIndex(): Promise<number> {1192    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1193  }11941195  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1196    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1197  }11981199  setOwnKeys(signer: TSigner, key: string) {1200    return this.helper.executeExtrinsic(1201      signer,1202      'api.tx.session.setKeys',1203      [key, '0x0'],1204      true,1205    );1206  }12071208  setOwnKeysFromAddress(signer: TSigner) {1209    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1210  }1211}12121213class TestUtilGroup {1214  helper: DevUniqueHelper;12151216  constructor(helper: DevUniqueHelper) {1217    this.helper = helper;1218  }12191220  async enable() {1221    if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1222      return;1223    }12241225    const signer = this.helper.util.fromSeed('//Alice');1226    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1227  }12281229  async setTestValue(signer: TSigner, testVal: number) {1230    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1231  }12321233  async incTestValue(signer: TSigner) {1234    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1235  }12361237  async setTestValueAndRollback(signer: TSigner, testVal: number) {1238    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1239  }12401241  async testValue(blockIdx?: number) {1242    const api = blockIdx1243      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1244      : this.helper.getApi();12451246    return (await api.query.testUtils.testValue()).toJSON();1247  }12481249  async justTakeFee(signer: TSigner) {1250    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1251  }12521253  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1254    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1255  }1256}12571258class EventCapture {1259  helper: DevUniqueHelper;1260  eventSection: string;1261  eventMethod: string;1262  events: EventRecord[] = [];1263  unsubscribe: VoidFn | null = null;12641265  constructor(1266    helper: DevUniqueHelper,1267    eventSection: string,1268    eventMethod: string,1269  ) {1270    this.helper = helper;1271    this.eventSection = eventSection;1272    this.eventMethod = eventMethod;1273  }12741275  async startCapture() {1276    this.stopCapture();1277    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1278      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);12791280      this.events.push(...newEvents);1281    })) as any;1282  }12831284  stopCapture() {1285    if(this.unsubscribe !== null) {1286      this.unsubscribe();1287    }1288  }12891290  extractCapturedEvents() {1291    return this.events;1292  }1293}12941295class AdminGroup {1296  helper: UniqueHelper;12971298  constructor(helper: UniqueHelper) {1299    this.helper = helper;1300  }13011302  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1303    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1304    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1305      staker: e.event.data[0].toString(),1306      stake: e.event.data[1].toBigInt(),1307      payout: e.event.data[2].toBigInt(),1308    }));1309  }1310}