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

difftreelog

feat add sudo/scheduler support to playgrounds, several minor additions

Daniel Shiposha2022-09-16parent: #b36ccba.patch.diff
in: master

2 files changed

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 {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} from './types';12import {FrameSystemEventRecord} from '@polkadot/types/lookup';1314export class SilentLogger {15  log(_msg: any, _level: any): void { }16  level = {17    ERROR: 'ERROR' as const,18    WARNING: 'WARNING' as const,19    INFO: 'INFO' as const,20  };21}2223export class SilentConsole {24  // TODO: Remove, this is temporary: Filter unneeded API output25  // (Jaco promised it will be removed in the next version)26  consoleErr: any;27  consoleLog: any;28  consoleWarn: any;2930  constructor() {31    this.consoleErr = console.error;32    this.consoleLog = console.log;33    this.consoleWarn = console.warn;34  }3536  enable() {  37    const outFn = (printer: any) => (...args: any[]) => {38      for (const arg of args) {39        if (typeof arg !== 'string')40          continue;41        if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')42          return;43      }44      printer(...args);45    };46  47    console.error = outFn(this.consoleErr.bind(console));48    console.log = outFn(this.consoleLog.bind(console));49    console.warn = outFn(this.consoleWarn.bind(console));50  }5152  disable() {53    console.error = this.consoleErr;54    console.log = this.consoleLog;55    console.warn = this.consoleWarn;56  }57}5859export class DevUniqueHelper extends UniqueHelper {60  /**61   * Arrange methods for tests62   */63  arrange: ArrangeGroup;64  wait: WaitGroup;65  admin: AdminGroup;6667  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {68    options.helperBase = options.helperBase ?? DevUniqueHelper;6970    super(logger, options);71    this.arrange = new ArrangeGroup(this);72    this.wait = new WaitGroup(this);73    this.admin = new AdminGroup(this);74  }7576  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {77    const wsProvider = new WsProvider(wsEndpoint);78    this.api = new ApiPromise({79      provider: wsProvider,80      signedExtensions: {81        ContractHelpers: {82          extrinsic: {},83          payload: {},84        },85        FakeTransactionFinalizer: {86          extrinsic: {},87          payload: {},88        },89      },90      rpc: {91        unique: defs.unique.rpc,92        appPromotion: defs.appPromotion.rpc,93        rmrk: defs.rmrk.rpc,94        eth: {95          feeHistory: {96            description: 'Dummy',97            params: [],98            type: 'u8',99          },100          maxPriorityFeePerGas: {101            description: 'Dummy',102            params: [],103            type: 'u8',104          },105        },106      },107    });108    await this.api.isReadyOrError;109    this.network = await UniqueHelper.detectNetwork(this.api);110  }111}112113export class DevRelayHelper extends RelayHelper {}114115export class DevWestmintHelper extends WestmintHelper {116  wait: WaitGroup;117118  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {119    options.helperBase = options.helperBase ?? DevWestmintHelper;120121    super(logger, options);122    this.wait = new WaitGroup(this);123  }124}125126export class DevMoonbeamHelper extends MoonbeamHelper {127  account: MoonbeamAccountGroup;128  wait: WaitGroup;129130  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {131    options.helperBase = options.helperBase ?? DevMoonbeamHelper;132133    super(logger, options);134    this.account = new MoonbeamAccountGroup(this);135    this.wait = new WaitGroup(this);136  }137}138139export class DevMoonriverHelper extends DevMoonbeamHelper {}140141export class DevAcalaHelper extends AcalaHelper {142  wait: WaitGroup;143144  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {145    options.helperBase = options.helperBase ?? DevAcalaHelper;146147    super(logger, options);148    this.wait = new WaitGroup(this);149  }150}151152export class DevKaruraHelper extends DevAcalaHelper {}153154class ArrangeGroup {155  helper: DevUniqueHelper;156157  constructor(helper: DevUniqueHelper) {158    this.helper = helper;159  }160161  /**162   * Generates accounts with the specified UNQ token balance 163   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.164   * @param donor donor account for balances165   * @returns array of newly created accounts166   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 167   */168  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {169    let nonce = await this.helper.chain.getNonce(donor.address);170    const wait = new WaitGroup(this.helper);171    const ss58Format = this.helper.chain.getChainProperties().ss58Format;172    const tokenNominal = this.helper.balance.getOneTokenNominal();173    const transactions = [];174    const accounts: IKeyringPair[] = [];175    for (const balance of balances) {176      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);177      accounts.push(recipient);178      if (balance !== 0n) {179        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);180        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));181        nonce++;182      }183    }184185    await Promise.all(transactions).catch(_e => {});186    187    //#region TODO remove this region, when nonce problem will be solved188    const checkBalances = async () => {189      let isSuccess = true;190      for (let i = 0; i < balances.length; i++) {191        const balance = await this.helper.balance.getSubstrate(accounts[i].address);192        if (balance !== balances[i] * tokenNominal) {193          isSuccess = false;194          break;195        }196      }197      return isSuccess;198    };199200    let accountsCreated = false;201    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;202    // checkBalances retry up to 5-50 blocks203    for (let index = 0; index < maxBlocksChecked; index++) {204      accountsCreated = await checkBalances();205      if(accountsCreated) break;206      await wait.newBlocks(1);207    }208209    if (!accountsCreated) throw Error('Accounts generation failed');210    //#endregion211212    return accounts;213  };214215  // TODO combine this method and createAccounts into one216  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {  217    const createAsManyAsCan = async () => {218      let transactions: any = [];219      const accounts: IKeyringPair[] = [];220      let nonce = await this.helper.chain.getNonce(donor.address);221      const tokenNominal = this.helper.balance.getOneTokenNominal();222      for (let i = 0; i < accountsToCreate; i++) {223        if (i === 500) { // if there are too many accounts to create224          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 225          transactions = []; //226          nonce = await this.helper.chain.getNonce(donor.address); // update nonce 227        }228        const recepient = this.helper.util.fromSeed(mnemonicGenerate());229        accounts.push(recepient);230        if (withBalance !== 0n) {231          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);232          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));233          nonce++;234        }235      }236      237      const fullfilledAccounts = [];238      await Promise.allSettled(transactions);239      for (const account of accounts) {240        const accountBalance = await this.helper.balance.getSubstrate(account.address);241        if (accountBalance === withBalance * tokenNominal) {242          fullfilledAccounts.push(account);243        }244      }245      return fullfilledAccounts;246    };247248    249    const crowd: IKeyringPair[] = [];250    // do up to 5 retries251    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {252      const asManyAsCan = await createAsManyAsCan();253      crowd.push(...asManyAsCan);254      accountsToCreate -= asManyAsCan.length;255    }256257    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);258259    return crowd;260  };261262  isDevNode = async () => {263    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();264    if (blockNumber == 0) {265      await this.helper.wait.newBlocks(1); 266      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();267    }268    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);269    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);270    const findCreationDate = async (block: any) => {271      const humanBlock = block.toHuman();272      let date;273      humanBlock.block.extrinsics.forEach((ext: any) => {274        if(ext.method.section === 'timestamp') {275          date = Number(ext.method.args.now.replaceAll(',', ''));276        }277      });278      return date;279    };280    const block1date = await findCreationDate(block1);281    const block2date = await findCreationDate(block2);282    if(block2date! - block1date! < 9000) return true;283  };284  285  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {286    const address = payer.Substrate ? payer.Substrate : await this.helper.address.ethToSubstrate(payer.Ethereum!);287    let balance = await this.helper.balance.getSubstrate(address); 288    289    await promise();290    291    balance -= await this.helper.balance.getSubstrate(address);292    293    return balance;294  }295296  calculatePalletAddress(palletId: any) {297    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));298    return encodeAddress(address);299  }300}301302class MoonbeamAccountGroup {303  helper: MoonbeamHelper;304305  keyring: Keyring;306  _alithAccount: IKeyringPair;307  _baltatharAccount: IKeyringPair;308  _dorothyAccount: IKeyringPair;309310  constructor(helper: MoonbeamHelper) {311    this.helper = helper;312313    this.keyring = new Keyring({type: 'ethereum'});314    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';315    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';316    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';317318    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');319    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');320    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');321  }322323  alithAccount() {324    return this._alithAccount;325  }326327  baltatharAccount() {328    return this._baltatharAccount;329  }330331  dorothyAccount() {332    return this._dorothyAccount;333  }334335  create() {336    return this.keyring.addFromUri(mnemonicGenerate());337  }338}339340class WaitGroup {341  helper: ChainHelperBase;342343  constructor(helper: ChainHelperBase) {344    this.helper = helper;345  }346347  /**348   * Wait for specified number of blocks349   * @param blocksCount number of blocks to wait350   * @returns 351   */352  async newBlocks(blocksCount = 1): Promise<void> {353    // eslint-disable-next-line no-async-promise-executor354    const promise = new Promise<void>(async (resolve) => {355      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {356        if (blocksCount > 0) {357          blocksCount--;358        } else {359          unsubscribe();360          resolve();361        }362      });363    });364    return promise;365  }366367  async forParachainBlockNumber(blockNumber: bigint) {368    // eslint-disable-next-line no-async-promise-executor369    return new Promise<void>(async (resolve) => {370      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async (data: any) => {371        if (data.number.toNumber() >= blockNumber) {372          unsubscribe();373          resolve();374        }375      });376    });377  }378  379  async forRelayBlockNumber(blockNumber: bigint) {380    // eslint-disable-next-line no-async-promise-executor381    return new Promise<void>(async (resolve) => {382      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData(async (data: any) => {383        if (data.value.relayParentNumber.toNumber() >= blockNumber) {384          // @ts-ignore385          unsubscribe();386          resolve();387        }388      });389    });390  }391392  async event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {393    // eslint-disable-next-line no-async-promise-executor394    const promise = new Promise<EventRecord | null>(async (resolve) => {395      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {396        const blockNumber = header.number.toHuman();397        const blockHash = header.hash;398        const eventIdStr = `${eventSection}.${eventMethod}`;399        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;400  401        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);402  403        const apiAt = await this.helper.getApi().at(blockHash);404        const eventRecords = (await apiAt.query.system.events()) as any;405  406        const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {407          return r.event.section == eventSection && r.event.method == eventMethod;408        });409  410        if (neededEvent) {411          unsubscribe();412          resolve(neededEvent);413        } else if (maxBlocksToWait > 0) {414          maxBlocksToWait--;415        } else {416          this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);417  418          unsubscribe();419          resolve(null);420        }421      });422    });423    return promise;424  }425}426427class AdminGroup {428  helper: UniqueHelper;429430  constructor(helper: UniqueHelper) {431    this.helper = helper;432  }433434  async payoutStakers(signer: IKeyringPair, stakersToPayout: number) {435    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);436    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {437      return {438        staker: e.event.data[0].toString(),439        stake: e.event.data[1].toBigInt(),440        payout: e.event.data[2].toBigInt(),441      };442    });443  }444}
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 {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} from './types';12import {FrameSystemEventRecord} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';1415export class SilentLogger {16  log(_msg: any, _level: any): void { }17  level = {18    ERROR: 'ERROR' as const,19    WARNING: 'WARNING' as const,20    INFO: 'INFO' as const,21  };22}2324export class SilentConsole {25  // TODO: Remove, this is temporary: Filter unneeded API output26  // (Jaco promised it will be removed in the next version)27  consoleErr: any;28  consoleLog: any;29  consoleWarn: any;3031  constructor() {32    this.consoleErr = console.error;33    this.consoleLog = console.log;34    this.consoleWarn = console.warn;35  }3637  enable() {  38    const outFn = (printer: any) => (...args: any[]) => {39      for (const arg of args) {40        if (typeof arg !== 'string')41          continue;42        if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')43          return;44      }45      printer(...args);46    };47  48    console.error = outFn(this.consoleErr.bind(console));49    console.log = outFn(this.consoleLog.bind(console));50    console.warn = outFn(this.consoleWarn.bind(console));51  }5253  disable() {54    console.error = this.consoleErr;55    console.log = this.consoleLog;56    console.warn = this.consoleWarn;57  }58}5960export class DevUniqueHelper extends UniqueHelper {61  /**62   * Arrange methods for tests63   */64  arrange: ArrangeGroup;65  wait: WaitGroup;66  admin: AdminGroup;6768  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {69    options.helperBase = options.helperBase ?? DevUniqueHelper;7071    super(logger, options);72    this.arrange = new ArrangeGroup(this);73    this.wait = new WaitGroup(this);74    this.admin = new AdminGroup(this);75  }7677  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {78    const wsProvider = new WsProvider(wsEndpoint);79    this.api = new ApiPromise({80      provider: wsProvider,81      signedExtensions: {82        ContractHelpers: {83          extrinsic: {},84          payload: {},85        },86        FakeTransactionFinalizer: {87          extrinsic: {},88          payload: {},89        },90      },91      rpc: {92        unique: defs.unique.rpc,93        appPromotion: defs.appPromotion.rpc,94        rmrk: defs.rmrk.rpc,95        eth: {96          feeHistory: {97            description: 'Dummy',98            params: [],99            type: 'u8',100          },101          maxPriorityFeePerGas: {102            description: 'Dummy',103            params: [],104            type: 'u8',105          },106        },107      },108    });109    await this.api.isReadyOrError;110    this.network = await UniqueHelper.detectNetwork(this.api);111  }112}113114export class DevRelayHelper extends RelayHelper {}115116export class DevWestmintHelper extends WestmintHelper {117  wait: WaitGroup;118119  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {120    options.helperBase = options.helperBase ?? DevWestmintHelper;121122    super(logger, options);123    this.wait = new WaitGroup(this);124  }125}126127export class DevMoonbeamHelper extends MoonbeamHelper {128  account: MoonbeamAccountGroup;129  wait: WaitGroup;130131  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {132    options.helperBase = options.helperBase ?? DevMoonbeamHelper;133134    super(logger, options);135    this.account = new MoonbeamAccountGroup(this);136    this.wait = new WaitGroup(this);137  }138}139140export class DevMoonriverHelper extends DevMoonbeamHelper {}141142export class DevAcalaHelper extends AcalaHelper {143  wait: WaitGroup;144145  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {146    options.helperBase = options.helperBase ?? DevAcalaHelper;147148    super(logger, options);149    this.wait = new WaitGroup(this);150  }151}152153export class DevKaruraHelper extends DevAcalaHelper {}154155class ArrangeGroup {156  helper: DevUniqueHelper;157158  scheduledIdSlider = 0;159160  constructor(helper: DevUniqueHelper) {161    this.helper = helper;162  }163164  /**165   * Generates accounts with the specified UNQ token balance 166   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.167   * @param donor donor account for balances168   * @returns array of newly created accounts169   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor); 170   */171  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {172    let nonce = await this.helper.chain.getNonce(donor.address);173    const wait = new WaitGroup(this.helper);174    const ss58Format = this.helper.chain.getChainProperties().ss58Format;175    const tokenNominal = this.helper.balance.getOneTokenNominal();176    const transactions = [];177    const accounts: IKeyringPair[] = [];178    for (const balance of balances) {179      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);180      accounts.push(recipient);181      if (balance !== 0n) {182        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);183        transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));184        nonce++;185      }186    }187188    await Promise.all(transactions).catch(_e => {});189    190    //#region TODO remove this region, when nonce problem will be solved191    const checkBalances = async () => {192      let isSuccess = true;193      for (let i = 0; i < balances.length; i++) {194        const balance = await this.helper.balance.getSubstrate(accounts[i].address);195        if (balance !== balances[i] * tokenNominal) {196          isSuccess = false;197          break;198        }199      }200      return isSuccess;201    };202203    let accountsCreated = false;204    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;205    // checkBalances retry up to 5-50 blocks206    for (let index = 0; index < maxBlocksChecked; index++) {207      accountsCreated = await checkBalances();208      if(accountsCreated) break;209      await wait.newBlocks(1);210    }211212    if (!accountsCreated) throw Error('Accounts generation failed');213    //#endregion214215    return accounts;216  };217218  // TODO combine this method and createAccounts into one219  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {  220    const createAsManyAsCan = async () => {221      let transactions: any = [];222      const accounts: IKeyringPair[] = [];223      let nonce = await this.helper.chain.getNonce(donor.address);224      const tokenNominal = this.helper.balance.getOneTokenNominal();225      for (let i = 0; i < accountsToCreate; i++) {226        if (i === 500) { // if there are too many accounts to create227          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled 228          transactions = []; //229          nonce = await this.helper.chain.getNonce(donor.address); // update nonce 230        }231        const recepient = this.helper.util.fromSeed(mnemonicGenerate());232        accounts.push(recepient);233        if (withBalance !== 0n) {234          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);235          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));236          nonce++;237        }238      }239      240      const fullfilledAccounts = [];241      await Promise.allSettled(transactions);242      for (const account of accounts) {243        const accountBalance = await this.helper.balance.getSubstrate(account.address);244        if (accountBalance === withBalance * tokenNominal) {245          fullfilledAccounts.push(account);246        }247      }248      return fullfilledAccounts;249    };250251    252    const crowd: IKeyringPair[] = [];253    // do up to 5 retries254    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {255      const asManyAsCan = await createAsManyAsCan();256      crowd.push(...asManyAsCan);257      accountsToCreate -= asManyAsCan.length;258    }259260    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);261262    return crowd;263  };264265  isDevNode = async () => {266    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();267    if (blockNumber == 0) {268      await this.helper.wait.newBlocks(1); 269      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();270    }271    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);272    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);273    const findCreationDate = async (block: any) => {274      const humanBlock = block.toHuman();275      let date;276      humanBlock.block.extrinsics.forEach((ext: any) => {277        if(ext.method.section === 'timestamp') {278          date = Number(ext.method.args.now.replaceAll(',', ''));279        }280      });281      return date;282    };283    const block1date = await findCreationDate(block1);284    const block2date = await findCreationDate(block2);285    if(block2date! - block1date! < 9000) return true;286  };287  288  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {289    const address = payer.Substrate ? payer.Substrate : await this.helper.address.ethToSubstrate(payer.Ethereum!);290    let balance = await this.helper.balance.getSubstrate(address); 291    292    await promise();293    294    balance -= await this.helper.balance.getSubstrate(address);295    296    return balance;297  }298299  calculatePalletAddress(palletId: any) {300    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));301    return encodeAddress(address);302  }303304  async makeScheduledIds(num: number): Promise<string[]> {305    await this.helper.wait.noScheduledTasks();306307    function makeId(slider: number) {308      const scheduledIdSize = 32;309      const hexId = slider.toString(16);310      const prefixSize = scheduledIdSize - hexId.length;311312      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;313314      return scheduledId;  315    }316317    const ids = [];318    for (let i = 0; i < num; i++) {319      ids.push(makeId(this.scheduledIdSlider));320      this.scheduledIdSlider += 1;321    }322323    return ids;324  }325326  async makeScheduledId(): Promise<string> {327    return (await this.makeScheduledIds(1))[0];328  }329330  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {331    const capture = new EventCapture(this.helper, eventSection, eventMethod);332    await capture.startCapture();333334    return capture;335  }336}337338class MoonbeamAccountGroup {339  helper: MoonbeamHelper;340341  keyring: Keyring;342  _alithAccount: IKeyringPair;343  _baltatharAccount: IKeyringPair;344  _dorothyAccount: IKeyringPair;345346  constructor(helper: MoonbeamHelper) {347    this.helper = helper;348349    this.keyring = new Keyring({type: 'ethereum'});350    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';351    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';352    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';353354    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');355    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');356    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');357  }358359  alithAccount() {360    return this._alithAccount;361  }362363  baltatharAccount() {364    return this._baltatharAccount;365  }366367  dorothyAccount() {368    return this._dorothyAccount;369  }370371  create() {372    return this.keyring.addFromUri(mnemonicGenerate());373  }374}375376class WaitGroup {377  helper: ChainHelperBase;378379  constructor(helper: ChainHelperBase) {380    this.helper = helper;381  }382383  /**384   * Wait for specified number of blocks385   * @param blocksCount number of blocks to wait386   * @returns 387   */388  async newBlocks(blocksCount = 1): Promise<void> {389    // eslint-disable-next-line no-async-promise-executor390    const promise = new Promise<void>(async (resolve) => {391      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {392        if (blocksCount > 0) {393          blocksCount--;394        } else {395          unsubscribe();396          resolve();397        }398      });399    });400    return promise;401  }402403  async forParachainBlockNumber(blockNumber: bigint) {404    // eslint-disable-next-line no-async-promise-executor405    return new Promise<void>(async (resolve) => {406      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async (data: any) => {407        if (data.number.toNumber() >= blockNumber) {408          unsubscribe();409          resolve();410        }411      });412    });413  }414  415  async forRelayBlockNumber(blockNumber: bigint) {416    // eslint-disable-next-line no-async-promise-executor417    return new Promise<void>(async (resolve) => {418      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData(async (data: any) => {419        if (data.value.relayParentNumber.toNumber() >= blockNumber) {420          // @ts-ignore421          unsubscribe();422          resolve();423        }424      });425    });426  }427428  async noScheduledTasks() {429    const api = this.helper.getApi();430    431    // eslint-disable-next-line no-async-promise-executor432    const promise = new Promise<void>(async resolve => {433      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {434        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();435436        if(areThereScheduledTasks.length == 0) {437          unsubscribe();438          resolve();439        }440      }); 441    });442443    return promise;444  }445446  async event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {447    // eslint-disable-next-line no-async-promise-executor448    const promise = new Promise<EventRecord | null>(async (resolve) => {449      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {450        const blockNumber = header.number.toHuman();451        const blockHash = header.hash;452        const eventIdStr = `${eventSection}.${eventMethod}`;453        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;454  455        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);456  457        const apiAt = await this.helper.getApi().at(blockHash);458        const eventRecords = (await apiAt.query.system.events()) as any;459  460        const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {461          return r.event.section == eventSection && r.event.method == eventMethod;462        });463  464        if (neededEvent) {465          unsubscribe();466          resolve(neededEvent);467        } else if (maxBlocksToWait > 0) {468          maxBlocksToWait--;469        } else {470          this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);471          unsubscribe();472          resolve(null);473        }474      });475    });476    return promise;477  }478}479480class EventCapture {481  helper: DevUniqueHelper;482  eventSection: string;483  eventMethod: string;484  events: EventRecord[] = [];485  unsubscribe: VoidFn | null = null;486487  constructor(488    helper: DevUniqueHelper,489    eventSection: string,490    eventMethod: string,491  ) {492    this.helper = helper;493    this.eventSection = eventSection;494    this.eventMethod = eventMethod;495  }496497  async startCapture() {498    this.stopCapture();499    this.unsubscribe = await this.helper.getApi().query.system.events(eventRecords => {500      const newEvents = eventRecords.filter(r => {501        return r.event.section == this.eventSection && r.event.method == this.eventMethod;502      });503504      this.events.push(...newEvents);505    });506  }507508  stopCapture() {509    if (this.unsubscribe !== null) {510      this.unsubscribe();511    }512  }513514  extractCapturedEvents() {515    return this.events;516  }517}518519class AdminGroup {520  helper: UniqueHelper;521522  constructor(helper: UniqueHelper) {523    this.helper = helper;524  }525526  async payoutStakers(signer: IKeyringPair, stakersToPayout: number) {527    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);528    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {529      return {530        staker: e.event.data[0].toString(),531        stake: e.event.data[1].toBigInt(),532        payout: e.event.data[2].toBigInt(),533      };534    });535  }536}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -534,25 +534,27 @@
   }
 
   async getPaymentInfo(signer: TSigner, tx: any, len: number | null) {
-    const signingInfo = await this.api!.derive.tx.signingInfo(signer.address);
+    const api = this.getApi();
+    const signingInfo = await api.derive.tx.signingInfo(signer.address);
 
     // We need to sign the tx because
     // unsigned transactions does not have an inclusion fee
     tx.sign(signer, {
-      blockHash: this.api!.genesisHash,
-      genesisHash: this.api!.genesisHash,
-      runtimeVersion: this.api!.runtimeVersion,
+      blockHash: api.genesisHash,
+      genesisHash: api.genesisHash,
+      runtimeVersion: api.runtimeVersion,
       nonce: signingInfo.nonce,
     });
 
     if (len === null) {
       return (await this.callRpc('api.rpc.payment.queryInfo', [tx.toHex()])) as RuntimeDispatchInfo;
     } else {
-      return (await this.api!.call.transactionPaymentApi.queryInfo(tx, len)) as RuntimeDispatchInfo;
+      return (await api.call.transactionPaymentApi.queryInfo(tx, len)) as RuntimeDispatchInfo;
     }
   }
 
   constructApiCall(apiCall: string, params: any[]) {
+    if(this.api === null) throw Error('API not initialized');
     if(!apiCall.startsWith('api.')) throw Error(`Invalid api call: ${apiCall}`);
     let call = this.getApi() as any;
     for(const part of apiCall.slice(4).split('.')) {
@@ -2274,6 +2276,25 @@
   async transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise<boolean> {
     return this.subBalanceGroup.transferToSubstrate(signer, address, amount);
   }
+
+  async forceTransferToSubstrate(signer: TSigner, from: TSubstrateAccount, to: TSubstrateAccount, amount: bigint | string): Promise<boolean> {
+    const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.forceTransfer', [from, to, amount], true);
+
+    let transfer = {from: null, to: null, amount: 0n} as any;
+    result.result.events.forEach(({event: {data, method, section}}) => {
+      if ((section === 'balances') && (method === 'Transfer')) {
+        transfer = {
+          from: this.helper.address.normalizeSubstrate(data[0]),
+          to: this.helper.address.normalizeSubstrate(data[1]),
+          amount: BigInt(data[2]),
+        };
+      }
+    });
+    let isSuccess = this.helper.address.normalizeSubstrate(from) === transfer.from;
+    isSuccess = isSuccess && this.helper.address.normalizeSubstrate(to) === transfer.to;
+    isSuccess = isSuccess && BigInt(amount) === transfer.amount;
+    return isSuccess;
+  }
 }
 
 class AddressGroup extends HelperGroup<ChainHelperBase> {
@@ -2418,52 +2439,6 @@
 }
 
 class SchedulerGroup extends HelperGroup<UniqueHelper> {
-  scheduledIdSlider = 0;
-
-  async waitNoScheduledTasks() {
-    const api = this.helper.api!;
-    
-    // eslint-disable-next-line no-async-promise-executor
-    const promise = new Promise<void>(async resolve => {
-      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {
-        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();
-
-        if(areThereScheduledTasks.length == 0) {
-          unsubscribe();
-          resolve();
-        }
-      }); 
-    });
-
-    return promise;
-  }
-
-  async makeScheduledIds(num: number): Promise<string[]> {
-    await this.waitNoScheduledTasks();
-
-    function makeId(slider: number) {
-      const scheduledIdSize = 32;
-      const hexId = slider.toString(16);
-      const prefixSize = scheduledIdSize - hexId.length;
-
-      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;
-
-      return scheduledId;  
-    }
-
-    const ids = [];
-    for (let i = 0; i < num; i++) {
-      ids.push(makeId(this.scheduledIdSlider));
-      this.scheduledIdSlider += 1;
-    }
-
-    return ids;
-  }
-
-  async makeScheduledId(): Promise<string> {
-    return (await this.makeScheduledIds(1))[0];
-  }
-
   async cancelScheduled(signer: TSigner, scheduledId: string) {
     return this.helper.executeExtrinsic(
       signer,
@@ -2990,6 +2965,10 @@
   getSudo<T extends UniqueHelper>() {
     return new UniqueBaseCollection(this.collectionId, this.helper.getSudo<T>());
   }
+
+  getSudo() {
+    return new UniqueCollectionBase(this.collectionId, this.helper.getSudo());
+  }
 }