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

difftreelog

source

tests/src/util/playgrounds/unique.dev.ts23.9 KiBsourcehistory
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper} 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, TSigner} from './types';12import {FrameSystemEventRecord} 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        if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')45          return;46      }47      printer(...args);48    };4950    console.error = outFn(this.consoleErr.bind(console));51    console.log = outFn(this.consoleLog.bind(console));52    console.warn = outFn(this.consoleWarn.bind(console));53  }5455  disable() {56    console.error = this.consoleErr;57    console.log = this.consoleLog;58    console.warn = this.consoleWarn;59  }60}6162export class DevUniqueHelper extends UniqueHelper {63  /**64   * Arrange methods for tests65   */66  arrange: ArrangeGroup;67  wait: WaitGroup;68  admin: AdminGroup;69  session: SessionGroup;70  testUtils: TestUtilGroup;7172  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {73    options.helperBase = options.helperBase ?? DevUniqueHelper;7475    super(logger, options);76    this.arrange = new ArrangeGroup(this);77    this.wait = new WaitGroup(this);78    this.admin = new AdminGroup(this);79    this.testUtils = new TestUtilGroup(this);80    this.session = new SessionGroup(this);81  }8283  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {84    const wsProvider = new WsProvider(wsEndpoint);85    this.api = new ApiPromise({86      provider: wsProvider,87      signedExtensions: {88        ContractHelpers: {89          extrinsic: {},90          payload: {},91        },92        CheckMaintenance: {93          extrinsic: {},94          payload: {},95        },96        DisableIdentityCalls: {97          extrinsic: {},98          payload: {},99        },100        FakeTransactionFinalizer: {101          extrinsic: {},102          payload: {},103        },104      },105      rpc: {106        unique: defs.unique.rpc,107        appPromotion: defs.appPromotion.rpc,108        povinfo: defs.povinfo.rpc,109        rmrk: defs.rmrk.rpc,110        eth: {111          feeHistory: {112            description: 'Dummy',113            params: [],114            type: 'u8',115          },116          maxPriorityFeePerGas: {117            description: 'Dummy',118            params: [],119            type: 'u8',120          },121        },122      },123    });124    await this.api.isReadyOrError;125    this.network = await UniqueHelper.detectNetwork(this.api);126    this.wsEndpoint = wsEndpoint;127  }128}129130export class DevRelayHelper extends RelayHelper {131  wait: WaitGroup;132133  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {134    options.helperBase = options.helperBase ?? DevRelayHelper;135136    super(logger, options);137    this.wait = new WaitGroup(this);138  }139}140141export class DevWestmintHelper extends WestmintHelper {142  wait: WaitGroup;143144  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {145    options.helperBase = options.helperBase ?? DevWestmintHelper;146147    super(logger, options);148    this.wait = new WaitGroup(this);149  }150}151152export class DevStatemineHelper extends DevWestmintHelper {}153154export class DevStatemintHelper extends DevWestmintHelper {}155156export class DevMoonbeamHelper extends MoonbeamHelper {157  account: MoonbeamAccountGroup;158  wait: WaitGroup;159160  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {161    options.helperBase = options.helperBase ?? DevMoonbeamHelper;162    options.notePreimagePallet = options.notePreimagePallet ?? 'democracy';163164    super(logger, options);165    this.account = new MoonbeamAccountGroup(this);166    this.wait = new WaitGroup(this);167  }168}169170export class DevMoonriverHelper extends DevMoonbeamHelper {171  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {172    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';173    super(logger, options);174  }175}176177export class DevAcalaHelper extends AcalaHelper {178  wait: WaitGroup;179180  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {181    options.helperBase = options.helperBase ?? DevAcalaHelper;182183    super(logger, options);184    this.wait = new WaitGroup(this);185  }186}187188export class DevKaruraHelper extends DevAcalaHelper {}189190class ArrangeGroup {191  helper: DevUniqueHelper;192193  scheduledIdSlider = 0;194195  constructor(helper: DevUniqueHelper) {196    this.helper = helper;197  }198199  /**200   * Generates accounts with the specified UNQ token balance201   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.202   * @param donor donor account for balances203   * @returns array of newly created accounts204   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);205   */206  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {207    let nonce = await this.helper.chain.getNonce(donor.address);208    const wait = new WaitGroup(this.helper);209    const ss58Format = this.helper.chain.getChainProperties().ss58Format;210    const tokenNominal = this.helper.balance.getOneTokenNominal();211    const transactions = [];212    const accounts: IKeyringPair[] = [];213    for (const balance of balances) {214      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);215      accounts.push(recipient);216      if (balance !== 0n) {217        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);218        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));219        nonce++;220      }221    }222223    await Promise.all(transactions).catch(_e => {});224225    //#region TODO remove this region, when nonce problem will be solved226    const checkBalances = async () => {227      let isSuccess = true;228      for (let i = 0; i < balances.length; i++) {229        const balance = await this.helper.balance.getSubstrate(accounts[i].address);230        if (balance !== balances[i] * tokenNominal) {231          isSuccess = false;232          break;233        }234      }235      return isSuccess;236    };237238    let accountsCreated = false;239    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;240    // checkBalances retry up to 5-50 blocks241    for (let index = 0; index < maxBlocksChecked; index++) {242      accountsCreated = await checkBalances();243      if(accountsCreated) break;244      await wait.newBlocks(1);245    }246247    if (!accountsCreated) throw Error('Accounts generation failed');248    //#endregion249250    return accounts;251  };252253  // TODO combine this method and createAccounts into one254  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {255    const createAsManyAsCan = async () => {256      let transactions: any = [];257      const accounts: IKeyringPair[] = [];258      let nonce = await this.helper.chain.getNonce(donor.address);259      const tokenNominal = this.helper.balance.getOneTokenNominal();260      const ss58Format = this.helper.chain.getChainProperties().ss58Format;261      for (let i = 0; i < accountsToCreate; i++) {262        if (i === 500) { // if there are too many accounts to create263          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled264          transactions = []; //265          nonce = await this.helper.chain.getNonce(donor.address); // update nonce266        }267        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);268        accounts.push(recipient);269        if (withBalance !== 0n) {270          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);271          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));272          nonce++;273        }274      }275276      const fullfilledAccounts = [];277      await Promise.allSettled(transactions);278      for (const account of accounts) {279        const accountBalance = await this.helper.balance.getSubstrate(account.address);280        if (accountBalance === withBalance * tokenNominal) {281          fullfilledAccounts.push(account);282        }283      }284      return fullfilledAccounts;285    };286287288    const crowd: IKeyringPair[] = [];289    // do up to 5 retries290    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {291      const asManyAsCan = await createAsManyAsCan();292      crowd.push(...asManyAsCan);293      accountsToCreate -= asManyAsCan.length;294    }295296    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);297298    return crowd;299  };300301  isDevNode = async () => {302    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();303    if (blockNumber == 0) {304      await this.helper.wait.newBlocks(1);305      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();306    }307    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);308    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);309    const findCreationDate = (block: any) => {310      const humanBlock = block.toHuman();311      let date;312      humanBlock.block.extrinsics.forEach((ext: any) => {313        if(ext.method.section === 'timestamp') {314          date = Number(ext.method.args.now.replaceAll(',', ''));315        }316      });317      return date;318    };319    const block1date = await findCreationDate(block1);320    const block2date = await findCreationDate(block2);321    if(block2date! - block1date! < 9000) return true;322  };323324  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {325    const address = payer.Substrate ? payer.Substrate : await this.helper.address.ethToSubstrate(payer.Ethereum!);326    let balance = await this.helper.balance.getSubstrate(address);327328    await promise();329330    balance -= await this.helper.balance.getSubstrate(address);331332    return balance;333  }334335  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {336    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);337338    const kvJson: {[key: string]: string} = {};339340    for (const kv of rawPovInfo.keyValues) {341      kvJson[kv.key.toHex()] = kv.value.toHex();342    }343344    const kvStr = JSON.stringify(kvJson);345346    const chainql = spawnSync(347      'chainql',348      [349        `--tla-code=data=${kvStr}`,350        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,351      ],352    );353354    if (!chainql.stdout) {355      throw Error('unable to get an output from the `chainql`');356    }357358    return {359      proofSize: rawPovInfo.proofSize.toNumber(),360      compactProofSize: rawPovInfo.compactProofSize.toNumber(),361      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),362      results: rawPovInfo.results,363      kv: JSON.parse(chainql.stdout.toString()),364    };365  }366367  calculatePalletAddress(palletId: any) {368    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));369    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);370  }371372  makeScheduledIds(num: number): string[] {373    function makeId(slider: number) {374      const scheduledIdSize = 64;375      const hexId = slider.toString(16);376      const prefixSize = scheduledIdSize - hexId.length;377378      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;379380      return scheduledId;381    }382383    const ids = [];384    for (let i = 0; i < num; i++) {385      ids.push(makeId(this.scheduledIdSlider));386      this.scheduledIdSlider += 1;387    }388389    return ids;390  }391392  makeScheduledId(): string {393    return (this.makeScheduledIds(1))[0];394  }395396  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {397    const capture = new EventCapture(this.helper, eventSection, eventMethod);398    await capture.startCapture();399400    return capture;401  }402}403404class MoonbeamAccountGroup {405  helper: MoonbeamHelper;406407  keyring: Keyring;408  _alithAccount: IKeyringPair;409  _baltatharAccount: IKeyringPair;410  _dorothyAccount: IKeyringPair;411412  constructor(helper: MoonbeamHelper) {413    this.helper = helper;414415    this.keyring = new Keyring({type: 'ethereum'});416    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';417    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';418    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';419420    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');421    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');422    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');423  }424425  alithAccount() {426    return this._alithAccount;427  }428429  baltatharAccount() {430    return this._baltatharAccount;431  }432433  dorothyAccount() {434    return this._dorothyAccount;435  }436437  create() {438    return this.keyring.addFromUri(mnemonicGenerate());439  }440}441442class WaitGroup {443  helper: ChainHelperBase;444445  constructor(helper: ChainHelperBase) {446    this.helper = helper;447  }448449  sleep(milliseconds: number) {450    return new Promise((resolve) => setTimeout(resolve, milliseconds));451  }452453  private async waitWithTimeout(promise: Promise<any>, timeout: number) {454    let isBlock = false;455    promise.then(() => isBlock = true).catch(() => isBlock = true);456    let totalTime = 0;457    const step = 100;458    while(!isBlock) {459      await this.sleep(step);460      totalTime += step;461      if(totalTime >= timeout) throw Error('Blocks production failed');462    }463    return promise;464  }465466  /**467   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.468   * @param promise async operation to race against the timeout469   * @param timeoutMS time after which to time out470   * @param timeoutError error message to throw471   * @returns promise of the same type the operation had472   */473  withTimeout<T>(474    promise: Promise<T>,475    timeoutMS = 30000,476    timeoutError = 'The operation has timed out!',477  ): Promise<T> {478    const timeout = new Promise<never>((_, reject) => {479      setTimeout(() => {480        reject(new Error(timeoutError));481      }, timeoutMS);482    });483484    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});485  }486487  /**488   * Wait for specified number of blocks489   * @param blocksCount number of blocks to wait490   * @returns491   */492  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {493    timeout = timeout ?? blocksCount * 60_000;494    // eslint-disable-next-line no-async-promise-executor495    const promise = new Promise<void>(async (resolve) => {496      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {497        if (blocksCount > 0) {498          blocksCount--;499        } else {500          unsubscribe();501          resolve();502        }503      });504    });505    await this.waitWithTimeout(promise, timeout);506    return promise;507  }508509  /**510   * Wait for the specified number of sessions to pass.511   * Only applicable if the Session pallet is turned on.512   * @param sessionCount number of sessions to wait513   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks514   * @returns515   */516  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {517    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`518      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');519520    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;521    let currentSessionIndex = -1;522523    while (currentSessionIndex < expectedSessionIndex) {524      // eslint-disable-next-line no-async-promise-executor525      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {526        await this.newBlocks(1);527        const res = await (this.helper as DevUniqueHelper).session.getIndex();528        resolve(res);529      }), blockTimeout, 'The chain has stopped producing blocks!');530    }531  }532533  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {534    timeout = timeout ?? 30 * 60 * 1000;535    // eslint-disable-next-line no-async-promise-executor536    const promise = new Promise<void>(async (resolve) => {537      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {538        if (data.number.toNumber() >= blockNumber) {539          unsubscribe();540          resolve();541        }542      });543    });544    await this.waitWithTimeout(promise, timeout);545    return promise;546  }547548  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {549    timeout = timeout ?? 30 * 60 * 1000;550    // eslint-disable-next-line no-async-promise-executor551    const promise = new Promise<void>(async (resolve) => {552      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {553        if (data.value.relayParentNumber.toNumber() >= blockNumber) {554          // @ts-ignore555          unsubscribe();556          resolve();557        }558      });559    });560    await this.waitWithTimeout(promise, timeout);561    return promise;562  }563564  noScheduledTasks() {565    const api = this.helper.getApi();566567    // eslint-disable-next-line no-async-promise-executor568    const promise = new Promise<void>(async resolve => {569      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {570        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();571572        if(areThereScheduledTasks.length == 0) {573          unsubscribe();574          resolve();575        }576      });577    });578579    return promise;580  }581582  event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {583    // eslint-disable-next-line no-async-promise-executor584    const promise = new Promise<EventRecord | null>(async (resolve) => {585      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {586        const blockNumber = header.number.toHuman();587        const blockHash = header.hash;588        const eventIdStr = `${eventSection}.${eventMethod}`;589        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;590591        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);592593        const apiAt = await this.helper.getApi().at(blockHash);594        const eventRecords = (await apiAt.query.system.events()) as any;595596        const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {597          return r.event.section == eventSection && r.event.method == eventMethod;598        });599600        if (neededEvent) {601          unsubscribe();602          resolve(neededEvent);603        } else if (maxBlocksToWait > 0) {604          maxBlocksToWait--;605        } else {606          this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);607          unsubscribe();608          resolve(null);609        }610      });611    });612    return promise;613  }614}615616class SessionGroup {617  helper: ChainHelperBase;618619  constructor(helper: ChainHelperBase) {620    this.helper = helper;621  }622623  //todo:collator documentation624  async getIndex(): Promise<number> {625    return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();626  }627628  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {629    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);630  }631632  setOwnKeys(signer: TSigner, key: string) {633    return this.helper.executeExtrinsic(634      signer,635      'api.tx.session.setKeys',636      [key, '0x0'],637      true,638    );639  }640641  setOwnKeysFromAddress(signer: TSigner) {642    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));643  }644}645646class TestUtilGroup {647  helper: DevUniqueHelper;648649  constructor(helper: DevUniqueHelper) {650    this.helper = helper;651  }652653  async enable() {654    if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {655      return;656    }657658    const signer = this.helper.util.fromSeed('//Alice');659    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);660  }661662  async setTestValue(signer: TSigner, testVal: number) {663    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);664  }665666  async incTestValue(signer: TSigner) {667    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);668  }669670  async setTestValueAndRollback(signer: TSigner, testVal: number) {671    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);672  }673674  async testValue(blockIdx?: number) {675    const api = blockIdx676      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))677      : this.helper.getApi();678679    return (await api.query.testUtils.testValue()).toJSON();680  }681682  async justTakeFee(signer: TSigner) {683    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);684  }685686  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {687    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);688  }689}690691class EventCapture {692  helper: DevUniqueHelper;693  eventSection: string;694  eventMethod: string;695  events: EventRecord[] = [];696  unsubscribe: VoidFn | null = null;697698  constructor(699    helper: DevUniqueHelper,700    eventSection: string,701    eventMethod: string,702  ) {703    this.helper = helper;704    this.eventSection = eventSection;705    this.eventMethod = eventMethod;706  }707708  async startCapture() {709    this.stopCapture();710    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {711      const newEvents = eventRecords.filter(r => {712        return r.event.section == this.eventSection && r.event.method == this.eventMethod;713      });714715      this.events.push(...newEvents);716    })) as any;717  }718719  stopCapture() {720    if (this.unsubscribe !== null) {721      this.unsubscribe();722    }723  }724725  extractCapturedEvents() {726    return this.events;727  }728}729730class AdminGroup {731  helper: UniqueHelper;732733  constructor(helper: UniqueHelper) {734    this.helper = helper;735  }736737  async payoutStakers(signer: IKeyringPair, stakersToPayout: number) {738    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);739    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {740      return {741        staker: e.event.data[0].toString(),742        stake: e.event.data[1].toBigInt(),743        payout: e.event.data[2].toBigInt(),744      };745    });746  }747}