git.delta.rocks / unique-network / refs/commits / 8a18c9958995

difftreelog

refactor event helpers

Daniel Shiposha2023-04-21parent: #6e3cf5e.patch.diff
in: master

4 files changed

modifiedtests/src/scheduler.seqtest.tsdiffbeforeafterboth
--- a/tests/src/scheduler.seqtest.ts
+++ b/tests/src/scheduler.seqtest.ts
@@ -16,7 +16,7 @@
 
 import {expect, itSched, itSub, Pallets, requirePalletsOrSkip, usingPlaygrounds} from './util';
 import {IKeyringPair} from '@polkadot/types/types';
-import {DevUniqueHelper} from './util/playgrounds/unique.dev';
+import {DevUniqueHelper, Event} from './util/playgrounds/unique.dev';
 
 describe('Scheduling token and balance transfers', () => {
   let superuser: IKeyringPair;
@@ -411,19 +411,13 @@
     const priority = 112;
     await helper.getSudo().scheduler.changePriority(superuser, scheduledId, priority);
 
-    const priorityChanged = await helper.wait.event(
-      waitForBlocks,
-      'scheduler',
-      'PriorityChanged',
-    );
-
-    expect(priorityChanged !== null).to.be.true;
+    const priorityChanged = await helper.wait.expectEvent(waitForBlocks, Event.Scheduler.PriorityChanged);
 
-    const [blockNumber, index] = priorityChanged!.event.data[0].toJSON() as any[];
+    const [blockNumber, index] = priorityChanged.task();
     expect(blockNumber).to.be.equal(executionBlock);
     expect(index).to.be.equal(0);
 
-    expect(priorityChanged!.event.data[1].toString()).to.be.equal(priority.toString());
+    expect(priorityChanged.priority().toString()).to.be.equal(priority.toString());
   });
 
   itSub('Prioritized operations execute in valid order', async ({helper}) => {
@@ -668,13 +662,7 @@
     await expect(helper.scheduler.changePriority(alice, scheduledId, priority))
       .to.be.rejectedWith(/BadOrigin/);
 
-    const priorityChanged = await helper.wait.event(
-      waitForBlocks,
-      'scheduler',
-      'PriorityChanged',
-    );
-
-    expect(priorityChanged === null).to.be.true;
+    await helper.wait.expectEvent(waitForBlocks, Event.Scheduler.PriorityChanged);
   });
 });
 
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
before · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, IPovInfo, 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        eth: {110          feeHistory: {111            description: 'Dummy',112            params: [],113            type: 'u8',114          },115          maxPriorityFeePerGas: {116            description: 'Dummy',117            params: [],118            type: 'u8',119          },120        },121      },122    });123    await this.api.isReadyOrError;124    this.network = await UniqueHelper.detectNetwork(this.api);125    this.wsEndpoint = wsEndpoint;126  }127}128129export class DevRelayHelper extends RelayHelper {130  wait: WaitGroup;131132  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {133    options.helperBase = options.helperBase ?? DevRelayHelper;134135    super(logger, options);136    this.wait = new WaitGroup(this);137  }138}139140export class DevWestmintHelper extends WestmintHelper {141  wait: WaitGroup;142143  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {144    options.helperBase = options.helperBase ?? DevWestmintHelper;145146    super(logger, options);147    this.wait = new WaitGroup(this);148  }149}150151export class DevStatemineHelper extends DevWestmintHelper {}152153export class DevStatemintHelper extends DevWestmintHelper {}154155export class DevMoonbeamHelper extends MoonbeamHelper {156  account: MoonbeamAccountGroup;157  wait: WaitGroup;158  fastDemocracy: MoonbeamFastDemocracyGroup;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 ?? 'preimage';163164    super(logger, options);165    this.account = new MoonbeamAccountGroup(this);166    this.wait = new WaitGroup(this);167    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);168  }169}170171export class DevMoonriverHelper extends DevMoonbeamHelper {172  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {173    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';174    super(logger, options);175  }176}177178export class DevAstarHelper extends AstarHelper {179  wait: WaitGroup;180181  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {182    options.helperBase = options.helperBase ?? DevAstarHelper;183184    super(logger, options);185    this.wait = new WaitGroup(this);186  }187}188189export class DevShidenHelper extends AstarHelper {190  wait: WaitGroup;191192  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {193    options.helperBase = options.helperBase ?? DevShidenHelper;194195    super(logger, options);196    this.wait = new WaitGroup(this);197  }198}199200export class DevAcalaHelper extends AcalaHelper {201  wait: WaitGroup;202203  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {204    options.helperBase = options.helperBase ?? DevAcalaHelper;205206    super(logger, options);207    this.wait = new WaitGroup(this);208  }209}210211export class DevKaruraHelper extends DevAcalaHelper {}212213export class ArrangeGroup {214  helper: DevUniqueHelper;215216  scheduledIdSlider = 0;217218  constructor(helper: DevUniqueHelper) {219    this.helper = helper;220  }221222  /**223   * Generates accounts with the specified UNQ token balance224   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.225   * @param donor donor account for balances226   * @returns array of newly created accounts227   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);228   */229  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {230    let nonce = await this.helper.chain.getNonce(donor.address);231    const wait = new WaitGroup(this.helper);232    const ss58Format = this.helper.chain.getChainProperties().ss58Format;233    const tokenNominal = this.helper.balance.getOneTokenNominal();234    const transactions = [];235    const accounts: IKeyringPair[] = [];236    for (const balance of balances) {237      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);238      accounts.push(recipient);239      if (balance !== 0n) {240        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);241        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));242        nonce++;243      }244    }245246    await Promise.all(transactions).catch(_e => {});247248    //#region TODO remove this region, when nonce problem will be solved249    const checkBalances = async () => {250      let isSuccess = true;251      for (let i = 0; i < balances.length; i++) {252        const balance = await this.helper.balance.getSubstrate(accounts[i].address);253        if (balance !== balances[i] * tokenNominal) {254          isSuccess = false;255          break;256        }257      }258      return isSuccess;259    };260261    let accountsCreated = false;262    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;263    // checkBalances retry up to 5-50 blocks264    for (let index = 0; index < maxBlocksChecked; index++) {265      accountsCreated = await checkBalances();266      if(accountsCreated) break;267      await wait.newBlocks(1);268    }269270    if (!accountsCreated) throw Error('Accounts generation failed');271    //#endregion272273    return accounts;274  };275276  // TODO combine this method and createAccounts into one277  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {278    const createAsManyAsCan = async () => {279      let transactions: any = [];280      const accounts: IKeyringPair[] = [];281      let nonce = await this.helper.chain.getNonce(donor.address);282      const tokenNominal = this.helper.balance.getOneTokenNominal();283      const ss58Format = this.helper.chain.getChainProperties().ss58Format;284      for (let i = 0; i < accountsToCreate; i++) {285        if (i === 500) { // if there are too many accounts to create286          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled287          transactions = []; //288          nonce = await this.helper.chain.getNonce(donor.address); // update nonce289        }290        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);291        accounts.push(recipient);292        if (withBalance !== 0n) {293          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);294          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));295          nonce++;296        }297      }298299      const fullfilledAccounts = [];300      await Promise.allSettled(transactions);301      for (const account of accounts) {302        const accountBalance = await this.helper.balance.getSubstrate(account.address);303        if (accountBalance === withBalance * tokenNominal) {304          fullfilledAccounts.push(account);305        }306      }307      return fullfilledAccounts;308    };309310311    const crowd: IKeyringPair[] = [];312    // do up to 5 retries313    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {314      const asManyAsCan = await createAsManyAsCan();315      crowd.push(...asManyAsCan);316      accountsToCreate -= asManyAsCan.length;317    }318319    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);320321    return crowd;322  };323324  isDevNode = async () => {325    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();326    if (blockNumber == 0) {327      await this.helper.wait.newBlocks(1);328      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();329    }330    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);331    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);332    const findCreationDate = (block: any) => {333      const humanBlock = block.toHuman();334      let date;335      humanBlock.block.extrinsics.forEach((ext: any) => {336        if(ext.method.section === 'timestamp') {337          date = Number(ext.method.args.now.replaceAll(',', ''));338        }339      });340      return date;341    };342    const block1date = await findCreationDate(block1);343    const block2date = await findCreationDate(block2);344    if(block2date! - block1date! < 9000) return true;345  };346347  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {348    const address = payer.Substrate ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum!);349    let balance = await this.helper.balance.getSubstrate(address);350351    await promise();352353    balance -= await this.helper.balance.getSubstrate(address);354355    return balance;356  }357358  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {359    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);360361    const kvJson: {[key: string]: string} = {};362363    for (const kv of rawPovInfo.keyValues) {364      kvJson[kv.key.toHex()] = kv.value.toHex();365    }366367    const kvStr = JSON.stringify(kvJson);368369    const chainql = spawnSync(370      'chainql',371      [372        `--tla-code=data=${kvStr}`,373        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,374      ],375    );376377    if (!chainql.stdout) {378      throw Error('unable to get an output from the `chainql`');379    }380381    return {382      proofSize: rawPovInfo.proofSize.toNumber(),383      compactProofSize: rawPovInfo.compactProofSize.toNumber(),384      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),385      results: rawPovInfo.results,386      kv: JSON.parse(chainql.stdout.toString()),387    };388  }389390  calculatePalletAddress(palletId: any) {391    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));392    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);393  }394395  makeScheduledIds(num: number): string[] {396    function makeId(slider: number) {397      const scheduledIdSize = 64;398      const hexId = slider.toString(16);399      const prefixSize = scheduledIdSize - hexId.length;400401      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;402403      return scheduledId;404    }405406    const ids = [];407    for (let i = 0; i < num; i++) {408      ids.push(makeId(this.scheduledIdSlider));409      this.scheduledIdSlider += 1;410    }411412    return ids;413  }414415  makeScheduledId(): string {416    return (this.makeScheduledIds(1))[0];417  }418419  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {420    const capture = new EventCapture(this.helper, eventSection, eventMethod);421    await capture.startCapture();422423    return capture;424  }425426  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {427    return {428      V2: [429        {430          WithdrawAsset: [431            {432              id,433              fun: {434                Fungible: amount,435              },436            },437          ],438        },439        {440          BuyExecution: {441            fees: {442              id,443              fun: {444                Fungible: amount,445              },446            },447            weightLimit: 'Unlimited',448          },449        },450        {451          DepositAsset: {452            assets: {453              Wild: 'All',454            },455            maxAssets: 1,456            beneficiary: {457              parents: 0,458              interior: {459                X1: {460                  AccountId32: {461                    network: 'Any',462                    id: beneficiary,463                  },464                },465              },466            },467          },468        },469      ],470    };471  }472473  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {474    return {475      V2: [476        {477          ReserveAssetDeposited: [478            {479              id,480              fun: {481                Fungible: amount,482              },483            },484          ],485        },486        {487          BuyExecution: {488            fees: {489              id,490              fun: {491                Fungible: amount,492              },493            },494            weightLimit: 'Unlimited',495          },496        },497        {498          DepositAsset: {499            assets: {500              Wild: 'All',501            },502            maxAssets: 1,503            beneficiary: {504              parents: 0,505              interior: {506                X1: {507                  AccountId32: {508                    network: 'Any',509                    id: beneficiary,510                  },511                },512              },513            },514          },515        },516      ],517    };518  }519}520521class MoonbeamAccountGroup {522  helper: MoonbeamHelper;523524  keyring: Keyring;525  _alithAccount: IKeyringPair;526  _baltatharAccount: IKeyringPair;527  _dorothyAccount: IKeyringPair;528529  constructor(helper: MoonbeamHelper) {530    this.helper = helper;531532    this.keyring = new Keyring({type: 'ethereum'});533    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';534    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';535    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';536537    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');538    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');539    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');540  }541542  alithAccount() {543    return this._alithAccount;544  }545546  baltatharAccount() {547    return this._baltatharAccount;548  }549550  dorothyAccount() {551    return this._dorothyAccount;552  }553554  create() {555    return this.keyring.addFromUri(mnemonicGenerate());556  }557}558559class MoonbeamFastDemocracyGroup {560  helper: DevMoonbeamHelper;561562  constructor(helper: DevMoonbeamHelper) {563    this.helper = helper;564  }565566  async executeProposal(proposalDesciption: string, encodedProposal: string) {567    const proposalHash = blake2AsHex(encodedProposal);568569    const alithAccount = this.helper.account.alithAccount();570    const baltatharAccount = this.helper.account.baltatharAccount();571    const dorothyAccount = this.helper.account.dorothyAccount();572573    const councilVotingThreshold = 2;574    const technicalCommitteeThreshold = 2;575    const fastTrackVotingPeriod = 3;576    const fastTrackDelayPeriod = 0;577578    console.log(`[democracy] executing '${proposalDesciption}' proposal`);579580    // >>> Propose external motion through council >>>581    console.log('\t* Propose external motion through council.......');582    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});583    const encodedMotion = externalMotion?.method.toHex() || '';584    const motionHash = blake2AsHex(encodedMotion);585    console.log('\t* Motion hash is %s', motionHash);586587    await this.helper.collective.council.propose(588      baltatharAccount,589      councilVotingThreshold,590      externalMotion,591      externalMotion.encodedLength,592    );593594    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;595    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);596    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);597598    await this.helper.collective.council.close(599      dorothyAccount,600      motionHash,601      councilProposalIdx,602      {603        refTime: 1_000_000_000,604        proofSize: 1_000_000,605      },606      externalMotion.encodedLength,607    );608    console.log('\t* Propose external motion through council.......DONE');609    // <<< Propose external motion through council <<<610611    // >>> Fast track proposal through technical committee >>>612    console.log('\t* Fast track proposal through technical committee.......');613    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);614    const encodedFastTrack = fastTrack?.method.toHex() || '';615    const fastTrackHash = blake2AsHex(encodedFastTrack);616    console.log('\t* FastTrack hash is %s', fastTrackHash);617618    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);619620    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;621    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);622    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);623624    await this.helper.collective.techCommittee.close(625      baltatharAccount,626      fastTrackHash,627      techProposalIdx,628      {629        refTime: 1_000_000_000,630        proofSize: 1_000_000,631      },632      fastTrack.encodedLength,633    );634    console.log('\t* Fast track proposal through technical committee.......DONE');635    // <<< Fast track proposal through technical committee <<<636637    const refIndexField = 0;638    const referendumIndex = await this.helper.wait.eventData<number>(3, 'democracy', 'Started', refIndexField);639640    // >>> Referendum voting >>>641    console.log(`\t* Referendum #${referendumIndex} voting.......`);642    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex!, {643      balance: 10_000_000_000_000_000_000n,644      vote: {aye: true, conviction: 1},645    });646    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);647    // <<< Referendum voting <<<648649    // Wait the proposal to pass650    await this.helper.wait.event(3, 'democracy', 'Passed');651    await this.helper.wait.newBlocks(1);652653    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);654  }655}656657class WaitGroup {658  helper: ChainHelperBase;659660  constructor(helper: ChainHelperBase) {661    this.helper = helper;662  }663664  sleep(milliseconds: number) {665    return new Promise((resolve) => setTimeout(resolve, milliseconds));666  }667668  private async waitWithTimeout(promise: Promise<any>, timeout: number) {669    let isBlock = false;670    promise.then(() => isBlock = true).catch(() => isBlock = true);671    let totalTime = 0;672    const step = 100;673    while(!isBlock) {674      await this.sleep(step);675      totalTime += step;676      if(totalTime >= timeout) throw Error('Blocks production failed');677    }678    return promise;679  }680681  /**682   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.683   * @param promise async operation to race against the timeout684   * @param timeoutMS time after which to time out685   * @param timeoutError error message to throw686   * @returns promise of the same type the operation had687   */688  withTimeout<T>(689    promise: Promise<T>,690    timeoutMS = 30000,691    timeoutError = 'The operation has timed out!',692  ): Promise<T> {693    const timeout = new Promise<never>((_, reject) => {694      setTimeout(() => {695        reject(new Error(timeoutError));696      }, timeoutMS);697    });698699    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});700  }701702  /**703   * Wait for specified number of blocks704   * @param blocksCount number of blocks to wait705   * @returns706   */707  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {708    timeout = timeout ?? blocksCount * 60_000;709    // eslint-disable-next-line no-async-promise-executor710    const promise = new Promise<void>(async (resolve) => {711      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {712        if (blocksCount > 0) {713          blocksCount--;714        } else {715          unsubscribe();716          resolve();717        }718      });719    });720    await this.waitWithTimeout(promise, timeout);721    return promise;722  }723724  /**725   * Wait for the specified number of sessions to pass.726   * Only applicable if the Session pallet is turned on.727   * @param sessionCount number of sessions to wait728   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks729   * @returns730   */731  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {732    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`733      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');734735    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;736    let currentSessionIndex = -1;737738    while (currentSessionIndex < expectedSessionIndex) {739      // eslint-disable-next-line no-async-promise-executor740      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {741        await this.newBlocks(1);742        const res = await (this.helper as DevUniqueHelper).session.getIndex();743        resolve(res);744      }), blockTimeout, 'The chain has stopped producing blocks!');745    }746  }747748  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {749    timeout = timeout ?? 30 * 60 * 1000;750    // eslint-disable-next-line no-async-promise-executor751    const promise = new Promise<void>(async (resolve) => {752      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {753        if (data.number.toNumber() >= blockNumber) {754          unsubscribe();755          resolve();756        }757      });758    });759    await this.waitWithTimeout(promise, timeout);760    return promise;761  }762763  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {764    timeout = timeout ?? 30 * 60 * 1000;765    // eslint-disable-next-line no-async-promise-executor766    const promise = new Promise<void>(async (resolve) => {767      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {768        if (data.value.relayParentNumber.toNumber() >= blockNumber) {769          // @ts-ignore770          unsubscribe();771          resolve();772        }773      });774    });775    await this.waitWithTimeout(promise, timeout);776    return promise;777  }778779  noScheduledTasks() {780    const api = this.helper.getApi();781782    // eslint-disable-next-line no-async-promise-executor783    const promise = new Promise<void>(async resolve => {784      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {785        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();786787        if(areThereScheduledTasks.length == 0) {788          unsubscribe();789          resolve();790        }791      });792    });793794    return promise;795  }796797  event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {798    // eslint-disable-next-line no-async-promise-executor799    const promise = new Promise<EventRecord | null>(async (resolve) => {800      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {801        const blockNumber = header.number.toHuman();802        const blockHash = header.hash;803        const eventIdStr = `${eventSection}.${eventMethod}`;804        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;805806        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);807808        const apiAt = await this.helper.getApi().at(blockHash);809        const eventRecords = (await apiAt.query.system.events()) as any;810811        const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {812          return r.event.section == eventSection && r.event.method == eventMethod;813        });814815        if (neededEvent) {816          unsubscribe();817          resolve(neededEvent);818        } else if (maxBlocksToWait > 0) {819          maxBlocksToWait--;820        } else {821          this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);822          unsubscribe();823          resolve(null);824        }825      });826    });827    return promise;828  }829830  async eventData<EventT>(maxBlocksToWait: number, eventSection: string, eventMethod: string, fieldIndex: number) {831    const eventRecord = await this.event(maxBlocksToWait, eventSection, eventMethod);832833    if (eventRecord == null) {834      return null;835    }836837    const event = eventRecord!.event;838    const data = event.data[fieldIndex] as EventT;839840    return data;841  }842}843844class SessionGroup {845  helper: ChainHelperBase;846847  constructor(helper: ChainHelperBase) {848    this.helper = helper;849  }850851  //todo:collator documentation852  async getIndex(): Promise<number> {853    return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();854  }855856  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {857    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);858  }859860  setOwnKeys(signer: TSigner, key: string) {861    return this.helper.executeExtrinsic(862      signer,863      'api.tx.session.setKeys',864      [key, '0x0'],865      true,866    );867  }868869  setOwnKeysFromAddress(signer: TSigner) {870    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));871  }872}873874class TestUtilGroup {875  helper: DevUniqueHelper;876877  constructor(helper: DevUniqueHelper) {878    this.helper = helper;879  }880881  async enable() {882    if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {883      return;884    }885886    const signer = this.helper.util.fromSeed('//Alice');887    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);888  }889890  async setTestValue(signer: TSigner, testVal: number) {891    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);892  }893894  async incTestValue(signer: TSigner) {895    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);896  }897898  async setTestValueAndRollback(signer: TSigner, testVal: number) {899    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);900  }901902  async testValue(blockIdx?: number) {903    const api = blockIdx904      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))905      : this.helper.getApi();906907    return (await api.query.testUtils.testValue()).toJSON();908  }909910  async justTakeFee(signer: TSigner) {911    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);912  }913914  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {915    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);916  }917}918919class EventCapture {920  helper: DevUniqueHelper;921  eventSection: string;922  eventMethod: string;923  events: EventRecord[] = [];924  unsubscribe: VoidFn | null = null;925926  constructor(927    helper: DevUniqueHelper,928    eventSection: string,929    eventMethod: string,930  ) {931    this.helper = helper;932    this.eventSection = eventSection;933    this.eventMethod = eventMethod;934  }935936  async startCapture() {937    this.stopCapture();938    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {939      const newEvents = eventRecords.filter(r => {940        return r.event.section == this.eventSection && r.event.method == this.eventMethod;941      });942943      this.events.push(...newEvents);944    })) as any;945  }946947  stopCapture() {948    if (this.unsubscribe !== null) {949      this.unsubscribe();950    }951  }952953  extractCapturedEvents() {954    return this.events;955  }956}957958class AdminGroup {959  helper: UniqueHelper;960961  constructor(helper: UniqueHelper) {962    this.helper = helper;963  }964965  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {966    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);967    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {968      return {969        staker: e.event.data[0].toString(),970        stake: e.event.data[1].toBigInt(),971        payout: e.event.data[2].toBigInt(),972      };973    });974  }975}
after · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, IPovInfo, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';1617export class SilentLogger {18  log(_msg: any, _level: any): void { }19  level = {20    ERROR: 'ERROR' as const,21    WARNING: 'WARNING' as const,22    INFO: 'INFO' as const,23  };24}2526export class SilentConsole {27  // TODO: Remove, this is temporary: Filter unneeded API output28  // (Jaco promised it will be removed in the next version)29  consoleErr: any;30  consoleLog: any;31  consoleWarn: any;3233  constructor() {34    this.consoleErr = console.error;35    this.consoleLog = console.log;36    this.consoleWarn = console.warn;37  }3839  enable() {40    const outFn = (printer: any) => (...args: any[]) => {41      for (const arg of args) {42        if (typeof arg !== 'string')43          continue;44        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 interface IEventHelper {63  section(): string;6465  method(): string;6667  bindEventRecord(e: FrameSystemEventRecord): void;6869  raw(): FrameSystemEventRecord;70}7172// eslint-disable-next-line @typescript-eslint/naming-convention73function EventHelper(section: string, method: string) {74  return class implements IEventHelper {75    eventRecord: FrameSystemEventRecord | null;76    _section: string;77    _method: string;7879    constructor() {80      this.eventRecord = null;81      this._section = section;82      this._method = method;83    }8485    section(): string {86      return this._section;87    }8889    method(): string {90      return this._method;91    }9293    bindEventRecord(e: FrameSystemEventRecord) {94      this.eventRecord = e;95    }9697    raw() {98      return this.eventRecord!;99    }100101    eventJsonData<T = any>(index: number) {102      return this.raw().event.data[index].toJSON() as T;103    }104105    eventData<T>(index: number) {106      return this.raw().event.data[index] as T;107    }108  };109}110111// eslint-disable-next-line @typescript-eslint/naming-convention112function EventSection(section: string) {113  return class Section {114    static section = section;115116    static Method(name: string) {117      return EventHelper(Section.section, name);118    }119  };120}121122export class Event {123  static Democracy = class extends EventSection('democracy') {124    static Started = class extends this.Method('Started') {125      referendumIndex() {126        return this.eventJsonData<number>(0);127      }128129      threshold() {130        return this.eventJsonData(1);131      }132    };133134    static Voted = class extends this.Method('Voted') {135      voter() {136        return this.eventJsonData(0);137      }138139      referendumIndex() {140        return this.eventJsonData<number>(1);141      }142143      vote() {144        return this.eventJsonData(2);145      }146    };147148    static Passed = class extends this.Method('Passed') {149      referendumIndex() {150        return this.eventJsonData<number>(0);151      }152    };153  };154155  static Scheduler = class extends EventSection('scheduler') {156    static PriorityChanged = class extends this.Method('PriorityChanged') {157      task() {158        return this.eventJsonData(0);159      }160161      priority() {162        return this.eventJsonData(1);163      }164    };165  };166167  static XcmpQueue = class extends EventSection('xcmpQueue') {168    static XcmpMessageSent = class extends this.Method('XcmpMessageSent') {169      messageHash() {170        return this.eventJsonData(0);171      }172    };173174    static Fail = class extends this.Method('Fail') {175      messageHash() {176        return this.eventJsonData(0);177      }178179      outcome() {180        return this.eventData<XcmV2TraitsError>(1);181      }182    };183  };184}185186export class DevUniqueHelper extends UniqueHelper {187  /**188   * Arrange methods for tests189   */190  arrange: ArrangeGroup;191  wait: WaitGroup;192  admin: AdminGroup;193  session: SessionGroup;194  testUtils: TestUtilGroup;195196  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {197    options.helperBase = options.helperBase ?? DevUniqueHelper;198199    super(logger, options);200    this.arrange = new ArrangeGroup(this);201    this.wait = new WaitGroup(this);202    this.admin = new AdminGroup(this);203    this.testUtils = new TestUtilGroup(this);204    this.session = new SessionGroup(this);205  }206207  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {208    const wsProvider = new WsProvider(wsEndpoint);209    this.api = new ApiPromise({210      provider: wsProvider,211      signedExtensions: {212        ContractHelpers: {213          extrinsic: {},214          payload: {},215        },216        CheckMaintenance: {217          extrinsic: {},218          payload: {},219        },220        DisableIdentityCalls: {221          extrinsic: {},222          payload: {},223        },224        FakeTransactionFinalizer: {225          extrinsic: {},226          payload: {},227        },228      },229      rpc: {230        unique: defs.unique.rpc,231        appPromotion: defs.appPromotion.rpc,232        povinfo: defs.povinfo.rpc,233        eth: {234          feeHistory: {235            description: 'Dummy',236            params: [],237            type: 'u8',238          },239          maxPriorityFeePerGas: {240            description: 'Dummy',241            params: [],242            type: 'u8',243          },244        },245      },246    });247    await this.api.isReadyOrError;248    this.network = await UniqueHelper.detectNetwork(this.api);249    this.wsEndpoint = wsEndpoint;250  }251}252253export class DevRelayHelper extends RelayHelper {254  wait: WaitGroup;255256  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {257    options.helperBase = options.helperBase ?? DevRelayHelper;258259    super(logger, options);260    this.wait = new WaitGroup(this);261  }262}263264export class DevWestmintHelper extends WestmintHelper {265  wait: WaitGroup;266267  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {268    options.helperBase = options.helperBase ?? DevWestmintHelper;269270    super(logger, options);271    this.wait = new WaitGroup(this);272  }273}274275export class DevStatemineHelper extends DevWestmintHelper {}276277export class DevStatemintHelper extends DevWestmintHelper {}278279export class DevMoonbeamHelper extends MoonbeamHelper {280  account: MoonbeamAccountGroup;281  wait: WaitGroup;282  fastDemocracy: MoonbeamFastDemocracyGroup;283284  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {285    options.helperBase = options.helperBase ?? DevMoonbeamHelper;286    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';287288    super(logger, options);289    this.account = new MoonbeamAccountGroup(this);290    this.wait = new WaitGroup(this);291    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);292  }293}294295export class DevMoonriverHelper extends DevMoonbeamHelper {296  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {297    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';298    super(logger, options);299  }300}301302export class DevAstarHelper extends AstarHelper {303  wait: WaitGroup;304305  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {306    options.helperBase = options.helperBase ?? DevAstarHelper;307308    super(logger, options);309    this.wait = new WaitGroup(this);310  }311}312313export class DevShidenHelper extends AstarHelper {314  wait: WaitGroup;315316  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {317    options.helperBase = options.helperBase ?? DevShidenHelper;318319    super(logger, options);320    this.wait = new WaitGroup(this);321  }322}323324export class DevAcalaHelper extends AcalaHelper {325  wait: WaitGroup;326327  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {328    options.helperBase = options.helperBase ?? DevAcalaHelper;329330    super(logger, options);331    this.wait = new WaitGroup(this);332  }333}334335export class DevKaruraHelper extends DevAcalaHelper {}336337export class ArrangeGroup {338  helper: DevUniqueHelper;339340  scheduledIdSlider = 0;341342  constructor(helper: DevUniqueHelper) {343    this.helper = helper;344  }345346  /**347   * Generates accounts with the specified UNQ token balance348   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.349   * @param donor donor account for balances350   * @returns array of newly created accounts351   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);352   */353  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {354    let nonce = await this.helper.chain.getNonce(donor.address);355    const wait = new WaitGroup(this.helper);356    const ss58Format = this.helper.chain.getChainProperties().ss58Format;357    const tokenNominal = this.helper.balance.getOneTokenNominal();358    const transactions = [];359    const accounts: IKeyringPair[] = [];360    for (const balance of balances) {361      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);362      accounts.push(recipient);363      if (balance !== 0n) {364        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);365        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));366        nonce++;367      }368    }369370    await Promise.all(transactions).catch(_e => {});371372    //#region TODO remove this region, when nonce problem will be solved373    const checkBalances = async () => {374      let isSuccess = true;375      for (let i = 0; i < balances.length; i++) {376        const balance = await this.helper.balance.getSubstrate(accounts[i].address);377        if (balance !== balances[i] * tokenNominal) {378          isSuccess = false;379          break;380        }381      }382      return isSuccess;383    };384385    let accountsCreated = false;386    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;387    // checkBalances retry up to 5-50 blocks388    for (let index = 0; index < maxBlocksChecked; index++) {389      accountsCreated = await checkBalances();390      if(accountsCreated) break;391      await wait.newBlocks(1);392    }393394    if (!accountsCreated) throw Error('Accounts generation failed');395    //#endregion396397    return accounts;398  };399400  // TODO combine this method and createAccounts into one401  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {402    const createAsManyAsCan = async () => {403      let transactions: any = [];404      const accounts: IKeyringPair[] = [];405      let nonce = await this.helper.chain.getNonce(donor.address);406      const tokenNominal = this.helper.balance.getOneTokenNominal();407      const ss58Format = this.helper.chain.getChainProperties().ss58Format;408      for (let i = 0; i < accountsToCreate; i++) {409        if (i === 500) { // if there are too many accounts to create410          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled411          transactions = []; //412          nonce = await this.helper.chain.getNonce(donor.address); // update nonce413        }414        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);415        accounts.push(recipient);416        if (withBalance !== 0n) {417          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);418          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));419          nonce++;420        }421      }422423      const fullfilledAccounts = [];424      await Promise.allSettled(transactions);425      for (const account of accounts) {426        const accountBalance = await this.helper.balance.getSubstrate(account.address);427        if (accountBalance === withBalance * tokenNominal) {428          fullfilledAccounts.push(account);429        }430      }431      return fullfilledAccounts;432    };433434435    const crowd: IKeyringPair[] = [];436    // do up to 5 retries437    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {438      const asManyAsCan = await createAsManyAsCan();439      crowd.push(...asManyAsCan);440      accountsToCreate -= asManyAsCan.length;441    }442443    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);444445    return crowd;446  };447448  isDevNode = async () => {449    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();450    if (blockNumber == 0) {451      await this.helper.wait.newBlocks(1);452      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();453    }454    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);455    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);456    const findCreationDate = (block: any) => {457      const humanBlock = block.toHuman();458      let date;459      humanBlock.block.extrinsics.forEach((ext: any) => {460        if(ext.method.section === 'timestamp') {461          date = Number(ext.method.args.now.replaceAll(',', ''));462        }463      });464      return date;465    };466    const block1date = await findCreationDate(block1);467    const block2date = await findCreationDate(block2);468    if(block2date! - block1date! < 9000) return true;469  };470471  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {472    const address = payer.Substrate ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum!);473    let balance = await this.helper.balance.getSubstrate(address);474475    await promise();476477    balance -= await this.helper.balance.getSubstrate(address);478479    return balance;480  }481482  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {483    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);484485    const kvJson: {[key: string]: string} = {};486487    for (const kv of rawPovInfo.keyValues) {488      kvJson[kv.key.toHex()] = kv.value.toHex();489    }490491    const kvStr = JSON.stringify(kvJson);492493    const chainql = spawnSync(494      'chainql',495      [496        `--tla-code=data=${kvStr}`,497        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,498      ],499    );500501    if (!chainql.stdout) {502      throw Error('unable to get an output from the `chainql`');503    }504505    return {506      proofSize: rawPovInfo.proofSize.toNumber(),507      compactProofSize: rawPovInfo.compactProofSize.toNumber(),508      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),509      results: rawPovInfo.results,510      kv: JSON.parse(chainql.stdout.toString()),511    };512  }513514  calculatePalletAddress(palletId: any) {515    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));516    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);517  }518519  makeScheduledIds(num: number): string[] {520    function makeId(slider: number) {521      const scheduledIdSize = 64;522      const hexId = slider.toString(16);523      const prefixSize = scheduledIdSize - hexId.length;524525      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;526527      return scheduledId;528    }529530    const ids = [];531    for (let i = 0; i < num; i++) {532      ids.push(makeId(this.scheduledIdSlider));533      this.scheduledIdSlider += 1;534    }535536    return ids;537  }538539  makeScheduledId(): string {540    return (this.makeScheduledIds(1))[0];541  }542543  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {544    const capture = new EventCapture(this.helper, eventSection, eventMethod);545    await capture.startCapture();546547    return capture;548  }549550  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {551    return {552      V2: [553        {554          WithdrawAsset: [555            {556              id,557              fun: {558                Fungible: amount,559              },560            },561          ],562        },563        {564          BuyExecution: {565            fees: {566              id,567              fun: {568                Fungible: amount,569              },570            },571            weightLimit: 'Unlimited',572          },573        },574        {575          DepositAsset: {576            assets: {577              Wild: 'All',578            },579            maxAssets: 1,580            beneficiary: {581              parents: 0,582              interior: {583                X1: {584                  AccountId32: {585                    network: 'Any',586                    id: beneficiary,587                  },588                },589              },590            },591          },592        },593      ],594    };595  }596597  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {598    return {599      V2: [600        {601          ReserveAssetDeposited: [602            {603              id,604              fun: {605                Fungible: amount,606              },607            },608          ],609        },610        {611          BuyExecution: {612            fees: {613              id,614              fun: {615                Fungible: amount,616              },617            },618            weightLimit: 'Unlimited',619          },620        },621        {622          DepositAsset: {623            assets: {624              Wild: 'All',625            },626            maxAssets: 1,627            beneficiary: {628              parents: 0,629              interior: {630                X1: {631                  AccountId32: {632                    network: 'Any',633                    id: beneficiary,634                  },635                },636              },637            },638          },639        },640      ],641    };642  }643}644645class MoonbeamAccountGroup {646  helper: MoonbeamHelper;647648  keyring: Keyring;649  _alithAccount: IKeyringPair;650  _baltatharAccount: IKeyringPair;651  _dorothyAccount: IKeyringPair;652653  constructor(helper: MoonbeamHelper) {654    this.helper = helper;655656    this.keyring = new Keyring({type: 'ethereum'});657    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';658    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';659    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';660661    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');662    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');663    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');664  }665666  alithAccount() {667    return this._alithAccount;668  }669670  baltatharAccount() {671    return this._baltatharAccount;672  }673674  dorothyAccount() {675    return this._dorothyAccount;676  }677678  create() {679    return this.keyring.addFromUri(mnemonicGenerate());680  }681}682683class MoonbeamFastDemocracyGroup {684  helper: DevMoonbeamHelper;685686  constructor(helper: DevMoonbeamHelper) {687    this.helper = helper;688  }689690  async executeProposal(proposalDesciption: string, encodedProposal: string) {691    const proposalHash = blake2AsHex(encodedProposal);692693    const alithAccount = this.helper.account.alithAccount();694    const baltatharAccount = this.helper.account.baltatharAccount();695    const dorothyAccount = this.helper.account.dorothyAccount();696697    const councilVotingThreshold = 2;698    const technicalCommitteeThreshold = 2;699    const fastTrackVotingPeriod = 3;700    const fastTrackDelayPeriod = 0;701702    console.log(`[democracy] executing '${proposalDesciption}' proposal`);703704    // >>> Propose external motion through council >>>705    console.log('\t* Propose external motion through council.......');706    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});707    const encodedMotion = externalMotion?.method.toHex() || '';708    const motionHash = blake2AsHex(encodedMotion);709    console.log('\t* Motion hash is %s', motionHash);710711    await this.helper.collective.council.propose(712      baltatharAccount,713      councilVotingThreshold,714      externalMotion,715      externalMotion.encodedLength,716    );717718    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;719    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);720    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);721722    await this.helper.collective.council.close(723      dorothyAccount,724      motionHash,725      councilProposalIdx,726      {727        refTime: 1_000_000_000,728        proofSize: 1_000_000,729      },730      externalMotion.encodedLength,731    );732    console.log('\t* Propose external motion through council.......DONE');733    // <<< Propose external motion through council <<<734735    // >>> Fast track proposal through technical committee >>>736    console.log('\t* Fast track proposal through technical committee.......');737    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);738    const encodedFastTrack = fastTrack?.method.toHex() || '';739    const fastTrackHash = blake2AsHex(encodedFastTrack);740    console.log('\t* FastTrack hash is %s', fastTrackHash);741742    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);743744    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;745    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);746    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);747748    await this.helper.collective.techCommittee.close(749      baltatharAccount,750      fastTrackHash,751      techProposalIdx,752      {753        refTime: 1_000_000_000,754        proofSize: 1_000_000,755      },756      fastTrack.encodedLength,757    );758    console.log('\t* Fast track proposal through technical committee.......DONE');759    // <<< Fast track proposal through technical committee <<<760761    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);762    const referendumIndex = democracyStarted.referendumIndex();763764    // >>> Referendum voting >>>765    console.log(`\t* Referendum #${referendumIndex} voting.......`);766    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {767      balance: 10_000_000_000_000_000_000n,768      vote: {aye: true, conviction: 1},769    });770    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);771    // <<< Referendum voting <<<772773    // Wait the proposal to pass774    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => {775      return event.referendumIndex() == referendumIndex;776    });777778    await this.helper.wait.newBlocks(1);779780    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);781  }782}783784class WaitGroup {785  helper: ChainHelperBase;786787  constructor(helper: ChainHelperBase) {788    this.helper = helper;789  }790791  sleep(milliseconds: number) {792    return new Promise((resolve) => setTimeout(resolve, milliseconds));793  }794795  private async waitWithTimeout(promise: Promise<any>, timeout: number) {796    let isBlock = false;797    promise.then(() => isBlock = true).catch(() => isBlock = true);798    let totalTime = 0;799    const step = 100;800    while(!isBlock) {801      await this.sleep(step);802      totalTime += step;803      if(totalTime >= timeout) throw Error('Blocks production failed');804    }805    return promise;806  }807808  /**809   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.810   * @param promise async operation to race against the timeout811   * @param timeoutMS time after which to time out812   * @param timeoutError error message to throw813   * @returns promise of the same type the operation had814   */815  withTimeout<T>(816    promise: Promise<T>,817    timeoutMS = 30000,818    timeoutError = 'The operation has timed out!',819  ): Promise<T> {820    const timeout = new Promise<never>((_, reject) => {821      setTimeout(() => {822        reject(new Error(timeoutError));823      }, timeoutMS);824    });825826    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});827  }828829  /**830   * Wait for specified number of blocks831   * @param blocksCount number of blocks to wait832   * @returns833   */834  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {835    timeout = timeout ?? blocksCount * 60_000;836    // eslint-disable-next-line no-async-promise-executor837    const promise = new Promise<void>(async (resolve) => {838      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {839        if (blocksCount > 0) {840          blocksCount--;841        } else {842          unsubscribe();843          resolve();844        }845      });846    });847    await this.waitWithTimeout(promise, timeout);848    return promise;849  }850851  /**852   * Wait for the specified number of sessions to pass.853   * Only applicable if the Session pallet is turned on.854   * @param sessionCount number of sessions to wait855   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks856   * @returns857   */858  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {859    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`860      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');861862    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;863    let currentSessionIndex = -1;864865    while (currentSessionIndex < expectedSessionIndex) {866      // eslint-disable-next-line no-async-promise-executor867      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {868        await this.newBlocks(1);869        const res = await (this.helper as DevUniqueHelper).session.getIndex();870        resolve(res);871      }), blockTimeout, 'The chain has stopped producing blocks!');872    }873  }874875  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {876    timeout = timeout ?? 30 * 60 * 1000;877    // eslint-disable-next-line no-async-promise-executor878    const promise = new Promise<void>(async (resolve) => {879      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {880        if (data.number.toNumber() >= blockNumber) {881          unsubscribe();882          resolve();883        }884      });885    });886    await this.waitWithTimeout(promise, timeout);887    return promise;888  }889890  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {891    timeout = timeout ?? 30 * 60 * 1000;892    // eslint-disable-next-line no-async-promise-executor893    const promise = new Promise<void>(async (resolve) => {894      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {895        if (data.value.relayParentNumber.toNumber() >= blockNumber) {896          // @ts-ignore897          unsubscribe();898          resolve();899        }900      });901    });902    await this.waitWithTimeout(promise, timeout);903    return promise;904  }905906  noScheduledTasks() {907    const api = this.helper.getApi();908909    // eslint-disable-next-line no-async-promise-executor910    const promise = new Promise<void>(async resolve => {911      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {912        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();913914        if(areThereScheduledTasks.length == 0) {915          unsubscribe();916          resolve();917        }918      });919    });920921    return promise;922  }923924  event<T extends IEventHelper>(925    maxBlocksToWait: number,926    eventHelperType: new () => T,927    filter: (_: T) => boolean = () => { return true; },928  ) {929    // eslint-disable-next-line no-async-promise-executor930    const promise = new Promise<T | null>(async (resolve) => {931      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {932        const eventHelper = new eventHelperType();933        const blockNumber = header.number.toHuman();934        const blockHash = header.hash;935        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;936        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;937938        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);939940        const apiAt = await this.helper.getApi().at(blockHash);941        const eventRecords = (await apiAt.query.system.events()) as any;942943        const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {944          if (945            r.event.section == eventHelper.section()946            && r.event.method == eventHelper.method()947          ) {948            eventHelper.bindEventRecord(r);949            return filter(eventHelper);950          } else {951            return false;952          }953        });954955        if (neededEvent) {956          unsubscribe();957          resolve(eventHelper);958        } else if (maxBlocksToWait > 0) {959          maxBlocksToWait--;960        } else {961          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);962          unsubscribe();963          resolve(null);964        }965      });966    });967    return promise;968  }969970  async expectEvent<T extends IEventHelper>(971    maxBlocksToWait: number,972    eventHelperType: new () => T,973    filter: (e: T) => boolean = () => { return true; },974  ) {975    const e = await this.event(maxBlocksToWait, eventHelperType, filter);976    if (e == null) {977      const eventHelper = new eventHelperType();978      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);979    } else {980      return e;981    }982  }983}984985class SessionGroup {986  helper: ChainHelperBase;987988  constructor(helper: ChainHelperBase) {989    this.helper = helper;990  }991992  //todo:collator documentation993  async getIndex(): Promise<number> {994    return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();995  }996997  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {998    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);999  }10001001  setOwnKeys(signer: TSigner, key: string) {1002    return this.helper.executeExtrinsic(1003      signer,1004      'api.tx.session.setKeys',1005      [key, '0x0'],1006      true,1007    );1008  }10091010  setOwnKeysFromAddress(signer: TSigner) {1011    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1012  }1013}10141015class TestUtilGroup {1016  helper: DevUniqueHelper;10171018  constructor(helper: DevUniqueHelper) {1019    this.helper = helper;1020  }10211022  async enable() {1023    if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1024      return;1025    }10261027    const signer = this.helper.util.fromSeed('//Alice');1028    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1029  }10301031  async setTestValue(signer: TSigner, testVal: number) {1032    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1033  }10341035  async incTestValue(signer: TSigner) {1036    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1037  }10381039  async setTestValueAndRollback(signer: TSigner, testVal: number) {1040    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1041  }10421043  async testValue(blockIdx?: number) {1044    const api = blockIdx1045      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1046      : this.helper.getApi();10471048    return (await api.query.testUtils.testValue()).toJSON();1049  }10501051  async justTakeFee(signer: TSigner) {1052    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1053  }10541055  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1056    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1057  }1058}10591060class EventCapture {1061  helper: DevUniqueHelper;1062  eventSection: string;1063  eventMethod: string;1064  events: EventRecord[] = [];1065  unsubscribe: VoidFn | null = null;10661067  constructor(1068    helper: DevUniqueHelper,1069    eventSection: string,1070    eventMethod: string,1071  ) {1072    this.helper = helper;1073    this.eventSection = eventSection;1074    this.eventMethod = eventMethod;1075  }10761077  async startCapture() {1078    this.stopCapture();1079    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1080      const newEvents = eventRecords.filter(r => {1081        return r.event.section == this.eventSection && r.event.method == this.eventMethod;1082      });10831084      this.events.push(...newEvents);1085    })) as any;1086  }10871088  stopCapture() {1089    if (this.unsubscribe !== null) {1090      this.unsubscribe();1091    }1092  }10931094  extractCapturedEvents() {1095    return this.events;1096  }1097}10981099class AdminGroup {1100  helper: UniqueHelper;11011102  constructor(helper: UniqueHelper) {1103    this.helper = helper;1104  }11051106  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1107    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1108    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {1109      return {1110        staker: e.event.data[0].toString(),1111        stake: e.event.data[1].toBigInt(),1112        payout: e.event.data[2].toBigInt(),1113      };1114    });1115  }1116}
modifiedtests/src/xcm/xcmQuartz.test.tsdiffbeforeafterboth
--- a/tests/src/xcm/xcmQuartz.test.ts
+++ b/tests/src/xcm/xcmQuartz.test.ts
@@ -16,9 +16,8 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import config from '../config';
-import {XcmV2TraitsError} from '../interfaces';
 import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util';
-import {DevUniqueHelper} from '../util/playgrounds/unique.dev';
+import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
 
 const QUARTZ_CHAIN = 2095;
 const STATEMINE_CHAIN = 1000;
@@ -673,30 +672,20 @@
       moreThanKaruraHas,
     );
 
+    let maliciousXcmProgramSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Quartz
     await usingKaruraPlaygrounds(karuraUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);
+
+      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
 
     targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(targetAccountBalance).to.be.equal(0n);
@@ -765,30 +754,21 @@
       testAmount,
     );
 
+    let maliciousXcmProgramFullIdSent: any;
+    let maliciousXcmProgramHereIdSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Quartz using full QTZ identification
     await usingKaruraPlaygrounds(karuraUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);
-    });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    let xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
+      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramFullIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -796,24 +776,14 @@
     // Try to trick Quartz using shortened QTZ identification
     await usingKaruraPlaygrounds(karuraUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);
-    });
 
-    xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramHereIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -834,6 +804,10 @@
   let quartzAccountMultilocation: any;
   let quartzCombinedMultilocation: any;
 
+  let messageSent: any;
+
+  const maxWaitBlocks = 3;
+
   before(async () => {
     await usingPlaygrounds(async (helper, privateKey) => {
       alice = await privateKey('//Alice');
@@ -883,26 +857,11 @@
     });
   });
 
-  const expectFailedToTransact = async (network: string, helper: DevUniqueHelper) => {
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      `[reject ${network} tokens] 'xcmpQueue.FailEvent' event is expected`,
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `[reject ${network} tokens] The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+  const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == messageSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
   };
 
   itSub('Quartz rejects KAR tokens from Karura', async ({helper}) => {
@@ -912,9 +871,11 @@
       };
       const destination = quartzCombinedMultilocation;
       await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited');
+
+      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    await expectFailedToTransact('KAR', helper);
+    await expectFailedToTransact(helper, messageSent);
   });
 
   itSub('Quartz rejects MOVR tokens from Moonriver', async ({helper}) => {
@@ -922,9 +883,11 @@
       const id = 'SelfReserve';
       const destination = quartzCombinedMultilocation;
       await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited');
+
+      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    await expectFailedToTransact('MOVR', helper);
+    await expectFailedToTransact(helper, messageSent);
   });
 
   itSub('Quartz rejects SDN tokens from Shiden', async ({helper}) => {
@@ -952,9 +915,11 @@
         assets,
         feeAssetItem,
       ]);
+
+      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    await expectFailedToTransact('SDN', helper);
+    await expectFailedToTransact(helper, messageSent);
   });
 });
 
@@ -1193,6 +1158,9 @@
       moreThanMoonriverHas,
     );
 
+    let maliciousXcmProgramSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Quartz
     await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {
       const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgram]);
@@ -1200,27 +1168,14 @@
       // Needed to bypass the call filter.
       const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
       await helper.fastDemocracy.executeProposal('try to spend more QTZ than Moonriver has', batchCall);
-    });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
 
     targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(targetAccountBalance).to.be.equal(0n);
@@ -1293,6 +1248,10 @@
       testAmount,
     );
 
+    let maliciousXcmProgramFullIdSent: any;
+    let maliciousXcmProgramHereIdSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Quartz using full QTZ identification
     await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {
       const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [quartzMultilocation, maliciousXcmProgramFullId]);
@@ -1300,27 +1259,14 @@
       // Needed to bypass the call filter.
       const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
       await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using path asset identification', batchCall);
-    });
-
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
 
-    let xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'isUntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramFullIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -1332,24 +1278,14 @@
       // Needed to bypass the call filter.
       const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
       await helper.fastDemocracy.executeProposal('try to act like a reserve location for QTZ using "here" asset identification', batchCall);
-    });
 
-    xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'isUntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramHereIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -1598,31 +1534,21 @@
       moreThanShidenHas,
     );
 
+    let maliciousXcmProgramSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Quartz
     await usingShidenPlaygrounds(shidenUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgram);
+
+      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
 
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
-
     targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(targetAccountBalance).to.be.equal(0n);
 
@@ -1690,30 +1616,21 @@
       testAmount,
     );
 
+    let maliciousXcmProgramFullIdSent: any;
+    let maliciousXcmProgramHereIdSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Quartz using full QTZ identification
     await usingShidenPlaygrounds(shidenUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramFullId);
-    });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
+      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    let xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'isUntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramFullIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -1721,24 +1638,14 @@
     // Try to trick Quartz using shortened QTZ identification
     await usingShidenPlaygrounds(shidenUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, quartzMultilocation, maliciousXcmProgramHereId);
-    });
 
-    xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'isUntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramHereIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
modifiedtests/src/xcm/xcmUnique.test.tsdiffbeforeafterboth
--- a/tests/src/xcm/xcmUnique.test.ts
+++ b/tests/src/xcm/xcmUnique.test.ts
@@ -16,9 +16,8 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import config from '../config';
-import {XcmV2TraitsError} from '../interfaces';
 import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds} from '../util';
-import {DevUniqueHelper} from '../util/playgrounds/unique.dev';
+import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
 
 const UNIQUE_CHAIN = 2037;
 const STATEMINT_CHAIN = 1000;
@@ -675,30 +674,20 @@
       moreThanAcalaHas,
     );
 
+    let maliciousXcmProgramSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Unique
     await usingAcalaPlaygrounds(acalaUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram);
+
+      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
 
     targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(targetAccountBalance).to.be.equal(0n);
@@ -767,30 +756,21 @@
       testAmount,
     );
 
+    let maliciousXcmProgramFullIdSent: any;
+    let maliciousXcmProgramHereIdSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Unique using full UNQ identification
     await usingAcalaPlaygrounds(acalaUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramFullId);
-    });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    let xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
+      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramFullIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -798,24 +778,14 @@
     // Try to trick Unique using shortened UNQ identification
     await usingAcalaPlaygrounds(acalaUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramHereId);
-    });
 
-    xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramHereIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -836,6 +806,10 @@
   let uniqueAccountMultilocation: any;
   let uniqueCombinedMultilocation: any;
 
+  let messageSent: any;
+
+  const maxWaitBlocks = 3;
+
   before(async () => {
     await usingPlaygrounds(async (helper, privateKey) => {
       alice = await privateKey('//Alice');
@@ -885,26 +859,11 @@
     });
   });
 
-  const expectFailedToTransact = async (network: string, helper: DevUniqueHelper) => {
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      `[reject ${network} tokens] 'xcmpQueue.FailEvent' event is expected`,
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `[reject ${network} tokens] The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+  const expectFailedToTransact = async (helper: DevUniqueHelper, messageSent: any) => {
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == messageSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
   };
 
   itSub('Unique rejects ACA tokens from Acala', async ({helper}) => {
@@ -914,9 +873,11 @@
       };
       const destination = uniqueCombinedMultilocation;
       await helper.xTokens.transfer(alice, id, testAmount, destination, 'Unlimited');
+
+      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    await expectFailedToTransact('ACA', helper);
+    await expectFailedToTransact(helper, messageSent);
   });
 
   itSub('Unique rejects GLMR tokens from Moonbeam', async ({helper}) => {
@@ -924,9 +885,11 @@
       const id = 'SelfReserve';
       const destination = uniqueCombinedMultilocation;
       await helper.xTokens.transfer(alith, id, testAmount, destination, 'Unlimited');
+
+      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    await expectFailedToTransact('GLMR', helper);
+    await expectFailedToTransact(helper, messageSent);
   });
 
   itSub('Unique rejects ASTR tokens from Astar', async ({helper}) => {
@@ -954,9 +917,11 @@
         assets,
         feeAssetItem,
       ]);
+
+      messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    await expectFailedToTransact('ASTR', helper);
+    await expectFailedToTransact(helper, messageSent);
   });
 });
 
@@ -1196,6 +1161,9 @@
       moreThanMoonbeamHas,
     );
 
+    let maliciousXcmProgramSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Unique
     await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => {
       const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueMultilocation, maliciousXcmProgram]);
@@ -1203,27 +1171,14 @@
       // Needed to bypass the call filter.
       const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
       await helper.fastDemocracy.executeProposal('try to spend more UNQ than Moonbeam has', batchCall);
-    });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
 
     targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(targetAccountBalance).to.be.equal(0n);
@@ -1296,6 +1251,10 @@
       testAmount,
     );
 
+    let maliciousXcmProgramFullIdSent: any;
+    let maliciousXcmProgramHereIdSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Unique using full UNQ identification
     await usingMoonbeamPlaygrounds(moonbeamUrl, async (helper) => {
       const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueMultilocation, maliciousXcmProgramFullId]);
@@ -1303,27 +1262,14 @@
       // Needed to bypass the call filter.
       const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
       await helper.fastDemocracy.executeProposal('try to act like a reserve location for UNQ using path asset identification', batchCall);
-    });
-
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
 
-    let xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramFullIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -1335,24 +1281,14 @@
       // Needed to bypass the call filter.
       const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
       await helper.fastDemocracy.executeProposal('try to act like a reserve location for UNQ using "here" asset identification', batchCall);
-    });
 
-    xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramHereIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -1600,31 +1536,21 @@
       moreThanAstarHas,
     );
 
+    let maliciousXcmProgramSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Unique
     await usingAstarPlaygrounds(astarUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram);
+
+      maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
     });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
-
-    const xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramSent.messageHash()
+        && event.outcome().isFailedToTransactAsset;
+    });
 
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isFailedToTransactAsset,
-      `The XCM error should be 'FailedToTransactAsset', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
-
     targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(targetAccountBalance).to.be.equal(0n);
 
@@ -1692,30 +1618,21 @@
       testAmount,
     );
 
+    let maliciousXcmProgramFullIdSent: any;
+    let maliciousXcmProgramHereIdSent: any;
+    const maxWaitBlocks = 3;
+
     // Try to trick Unique using full UNQ identification
     await usingAstarPlaygrounds(astarUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramFullId);
-    });
 
-    const maxWaitBlocks = 3;
-    const outcomeField = 1;
+      maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    let xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
-
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramFullIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);
@@ -1723,24 +1640,14 @@
     // Try to trick Unique using shortened UNQ identification
     await usingAstarPlaygrounds(astarUrl, async (helper) => {
       await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgramHereId);
-    });
 
-    xcmpQueueFailEvent = await helper.wait.eventData<XcmV2TraitsError>(
-      maxWaitBlocks,
-      'xcmpQueue',
-      'Fail',
-      outcomeField,
-    );
-
-    expect(
-      xcmpQueueFailEvent != null,
-      '\'xcmpQueue.FailEvent\' event is expected',
-    ).to.be.true;
+      maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+    });
 
-    expect(
-      xcmpQueueFailEvent!.isUntrustedReserveLocation,
-      `The XCM error should be 'UntrustedReserveLocation', got '${xcmpQueueFailEvent!.toHuman()}'`,
-    ).to.be.true;
+    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => {
+      return event.messageHash() == maliciousXcmProgramHereIdSent.messageHash()
+        && event.outcome().isUntrustedReserveLocation;
+    });
 
     accountBalance = await helper.balance.getSubstrate(targetAccount.address);
     expect(accountBalance).to.be.equal(0n);