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

difftreelog

Merge pull request #1003 from UniqueNetwork/feature/llxcm-qtz

Yaroslav Bolyukin2023-10-02parents: #22b45b5 #3107202.patch.diff
in: master
added `XcmTestHelper` + allow XCM Transact for Gov/Identity/System calls

14 files changed

modified.baedeker/.gitignorediffbeforeafterboth
--- a/.baedeker/.gitignore
+++ b/.baedeker/.gitignore
@@ -1,4 +1,5 @@
 /.bdk-env
-/rewrites.jsonnet
+/rewrites*.jsonnet
 /vendor
 /baedeker-library
+!/rewrites.example.jsonnet
\ No newline at end of file
modified.github/workflows/xcm.ymldiffbeforeafterboth
--- a/.github/workflows/xcm.yml
+++ b/.github/workflows/xcm.yml
@@ -38,7 +38,7 @@
         with:
           matrix: |
             network {opal}, relay_branch {${{ env.UNIQUEWEST_MAINNET_BRANCH }}}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.WESTMINT_BUILD_BRANCH }}}, astar_version {${{ env.ASTAR_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testXcmOpal}, runtime_features {opal-runtime}
-            network {quartz}, relay_branch {${{ env.KUSAMA_MAINNET_BRANCH }}}, acala_version {${{ env.KARURA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONRIVER_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINE_BUILD_BRANCH }}}, astar_version {${{ env.SHIDEN_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testXcmQuartz}, runtime_features {quartz-runtime}
+            network {quartz}, relay_branch {${{ env.KUSAMA_MAINNET_BRANCH }}}, acala_version {${{ env.KARURA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONRIVER_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINE_BUILD_BRANCH }}}, astar_version {${{ env.SHIDEN_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testFullXcmQuartz}, runtime_features {quartz-runtime}
             network {unique}, relay_branch {${{ env.POLKADOT_MAINNET_BRANCH }}}, acala_version {${{ env.ACALA_BUILD_BRANCH }}}, moonbeam_version {${{ env.MOONBEAM_BUILD_BRANCH }}}, cumulus_version {${{ env.STATEMINT_BUILD_BRANCH }}}, astar_version {${{ env.ASTAR_BUILD_BRANCH }}}, polkadex_version {${{ env.POLKADEX_BUILD_BRANCH }}}, runtest {testFullXcmUnique}, runtime_features {unique-runtime}
 
   xcm:
modifiedruntime/common/config/xcm/mod.rsdiffbeforeafterboth
--- a/runtime/common/config/xcm/mod.rs
+++ b/runtime/common/config/xcm/mod.rs
@@ -15,7 +15,7 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 use frame_support::{
-	traits::{Everything, Nothing, Get, ConstU32, ProcessMessageError},
+	traits::{Everything, Nothing, Get, ConstU32, ProcessMessageError, Contains},
 	parameter_types,
 };
 use frame_system::EnsureRoot;
@@ -162,6 +162,54 @@
 
 pub type Weigher = FixedWeightBounds<UnitWeightCost, RuntimeCall, MaxInstructions>;
 
+pub struct XcmCallFilter;
+impl XcmCallFilter {
+	fn allow_gov_and_sys_call(call: &RuntimeCall) -> bool {
+		match call {
+			RuntimeCall::System(..) => true,
+
+			#[cfg(feature = "governance")]
+			RuntimeCall::Identity(..)
+			| RuntimeCall::Preimage(..)
+			| RuntimeCall::Democracy(..)
+			| RuntimeCall::Council(..)
+			| RuntimeCall::TechnicalCommittee(..)
+			| RuntimeCall::CouncilMembership(..)
+			| RuntimeCall::TechnicalCommitteeMembership(..)
+			| RuntimeCall::FellowshipCollective(..)
+			| RuntimeCall::FellowshipReferenda(..) => true,
+			_ => false,
+		}
+	}
+
+	fn allow_utility_call(call: &RuntimeCall) -> bool {
+		match call {
+			RuntimeCall::Utility(pallet_utility::Call::batch { calls, .. }) => {
+				calls.iter().all(Self::allow_gov_and_sys_call)
+			}
+			RuntimeCall::Utility(pallet_utility::Call::batch_all { calls, .. }) => {
+				calls.iter().all(Self::allow_gov_and_sys_call)
+			}
+			RuntimeCall::Utility(pallet_utility::Call::as_derivative { call, .. }) => {
+				Self::allow_gov_and_sys_call(call)
+			}
+			RuntimeCall::Utility(pallet_utility::Call::dispatch_as { call, .. }) => {
+				Self::allow_gov_and_sys_call(call)
+			}
+			RuntimeCall::Utility(pallet_utility::Call::force_batch { calls, .. }) => {
+				calls.iter().all(Self::allow_gov_and_sys_call)
+			}
+			_ => false,
+		}
+	}
+}
+
+impl Contains<RuntimeCall> for XcmCallFilter {
+	fn contains(call: &RuntimeCall) -> bool {
+		Self::allow_gov_and_sys_call(call) || Self::allow_utility_call(call)
+	}
+}
+
 pub struct XcmExecutorConfig<T>(PhantomData<T>);
 impl<T> xcm_executor::Config for XcmExecutorConfig<T>
 where
@@ -191,9 +239,7 @@
 	type MessageExporter = ();
 	type UniversalAliases = Nothing;
 	type CallDispatcher = RuntimeCall;
-
-	// Deny all XCM Transacts.
-	type SafeCallFilter = Nothing;
+	type SafeCallFilter = XcmCallFilter;
 }
 
 #[cfg(feature = "runtime-benchmarks")]
modifiedruntime/opal/src/xcm_barrier.rsdiffbeforeafterboth
--- a/runtime/opal/src/xcm_barrier.rs
+++ b/runtime/opal/src/xcm_barrier.rs
@@ -14,7 +14,18 @@
 // You should have received a copy of the GNU General Public License
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
-use frame_support::traits::Everything;
-use xcm_builder::{AllowTopLevelPaidExecutionFrom, TakeWeightCredit};
+use frame_support::{match_types, traits::Everything};
+use xcm::latest::{Junctions::*, MultiLocation};
+use xcm_builder::{AllowTopLevelPaidExecutionFrom, TakeWeightCredit, AllowExplicitUnpaidExecutionFrom};
 
-pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom<Everything>);
+match_types! {
+	pub type ParentOnly: impl Contains<MultiLocation> = {
+		MultiLocation { parents: 1, interior: Here }
+	};
+}
+
+pub type Barrier = (
+	TakeWeightCredit,
+	AllowExplicitUnpaidExecutionFrom<ParentOnly>,
+	AllowTopLevelPaidExecutionFrom<Everything>,
+);
modifiedruntime/quartz/src/xcm_barrier.rsdiffbeforeafterboth
--- a/runtime/quartz/src/xcm_barrier.rs
+++ b/runtime/quartz/src/xcm_barrier.rs
@@ -18,12 +18,16 @@
 use xcm::latest::{Junctions::*, MultiLocation};
 use xcm_builder::{
 	AllowKnownQueryResponses, AllowSubscriptionsFrom, TakeWeightCredit,
-	AllowTopLevelPaidExecutionFrom,
+	AllowTopLevelPaidExecutionFrom, AllowExplicitUnpaidExecutionFrom,
 };
 
 use crate::PolkadotXcm;
 
 match_types! {
+	pub type ParentOnly: impl Contains<MultiLocation> = {
+		MultiLocation { parents: 1, interior: Here }
+	};
+
 	pub type ParentOrSiblings: impl Contains<MultiLocation> = {
 		MultiLocation { parents: 1, interior: Here } |
 		MultiLocation { parents: 1, interior: X1(_) }
@@ -32,6 +36,7 @@
 
 pub type Barrier = (
 	TakeWeightCredit,
+	AllowExplicitUnpaidExecutionFrom<ParentOnly>,
 	AllowTopLevelPaidExecutionFrom<Everything>,
 	// Expected responses are OK.
 	AllowKnownQueryResponses<PolkadotXcm>,
modifiedruntime/unique/src/xcm_barrier.rsdiffbeforeafterboth
--- a/runtime/unique/src/xcm_barrier.rs
+++ b/runtime/unique/src/xcm_barrier.rs
@@ -18,12 +18,16 @@
 use xcm::latest::{Junctions::*, MultiLocation};
 use xcm_builder::{
 	AllowKnownQueryResponses, AllowSubscriptionsFrom, TakeWeightCredit,
-	AllowTopLevelPaidExecutionFrom,
+	AllowTopLevelPaidExecutionFrom, AllowExplicitUnpaidExecutionFrom,
 };
 
 use crate::PolkadotXcm;
 
 match_types! {
+	pub type ParentOnly: impl Contains<MultiLocation> = {
+		MultiLocation { parents: 1, interior: Here }
+	};
+
 	pub type ParentOrSiblings: impl Contains<MultiLocation> = {
 		MultiLocation { parents: 1, interior: Here } |
 		MultiLocation { parents: 1, interior: X1(_) }
@@ -32,6 +36,7 @@
 
 pub type Barrier = (
 	TakeWeightCredit,
+	AllowExplicitUnpaidExecutionFrom<ParentOnly>,
 	AllowTopLevelPaidExecutionFrom<Everything>,
 	// Expected responses are OK.
 	AllowKnownQueryResponses<PolkadotXcm>,
modifiedtests/package.jsondiffbeforeafterboth
--- a/tests/package.json
+++ b/tests/package.json
@@ -117,6 +117,8 @@
     "testXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmUnique.test.ts",
     "testFullXcmUnique": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Unique.test.ts",
     "testXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmQuartz.test.ts",
+    "testLowLevelXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/lowLevelXcmQuartz.test.ts",
+    "testFullXcmQuartz": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/*Quartz.test.ts",
     "testXcmOpal": "RUN_XCM_TESTS=1 yarn _test ./**/xcm/xcmOpal.test.ts",
     "testXcmTransferAcala": "yarn _test ./**/xcm/xcmTransferAcala.test.ts acalaId=2000 uniqueId=5000",
     "testXcmTransferStatemine": "yarn _test ./**/xcm/xcmTransferStatemine.test.ts statemineId=1000 uniqueId=5000",
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, ChainHelperBase, ChainHelperBaseConstructor, HelperGroup, UniqueHelperConstructor} 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, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';13import {SignerOptions, VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';16import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup} from './unique.xcm';17import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, ICollectiveGroup, IFellowshipGroup, RankedCollectiveGroup, ReferendaGroup} from './unique.governance';1819export class SilentLogger {20  log(_msg: any, _level: any): void { }21  level = {22    ERROR: 'ERROR' as const,23    WARNING: 'WARNING' as const,24    INFO: 'INFO' as const,25  };26}2728export class SilentConsole {29  // TODO: Remove, this is temporary: Filter unneeded API output30  // (Jaco promised it will be removed in the next version)31  consoleErr: any;32  consoleLog: any;33  consoleWarn: any;3435  constructor() {36    this.consoleErr = console.error;37    this.consoleLog = console.log;38    this.consoleWarn = console.warn;39  }4041  enable() {42    const outFn = (printer: any) => (...args: any[]) => {43      for(const arg of args) {44        if(typeof arg !== 'string')45          continue;46        const skippedWarnings = ['1000:: Normal connection closure', 'Not decorating unknown runtime apis:', 'RPC methods not decorated:', 'Not decorating runtime apis', 'Bad input data provided to validate_transaction', 'account balance too low', '1006:: Abnormal Closure'];47        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);48        if(needToSkip || arg === 'Normal connection closure')49          return;50      }51      printer(...args);52    };5354    console.error = outFn(this.consoleErr.bind(console));55    console.log = outFn(this.consoleLog.bind(console));56    console.warn = outFn(this.consoleWarn.bind(console));57  }5859  disable() {60    console.error = this.consoleErr;61    console.log = this.consoleLog;62    console.warn = this.consoleWarn;63  }64}6566export interface IEventHelper {67  section(): string;6869  method(): string;7071  wrapEvent(data: any[]): any;72}7374// eslint-disable-next-line @typescript-eslint/naming-convention75function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {76  const helperClass = class implements IEventHelper {77    wrapEvent: (data: any[]) => any;78    _section: string;79    _method: string;8081    constructor() {82      this.wrapEvent = wrapEvent;83      this._section = section;84      this._method = method;85    }8687    section(): string {88      return this._section;89    }9091    method(): string {92      return this._method;93    }9495    filter(txres: ITransactionResult) {96      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)97        .map(e => this.wrapEvent(e.event.data));98    }99100    find(txres: ITransactionResult) {101      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);102      return e ? this.wrapEvent(e.event.data) : null;103    }104105    expect(txres: ITransactionResult) {106      const e = this.find(txres);107      if(e) {108        return e;109      } else {110        throw Error(`Expected event ${section}.${method}`);111      }112    }113  };114115  return helperClass;116}117118function eventJsonData<T = any>(data: any[], index: number) {119  return data[index].toJSON() as T;120}121122function eventHumanData(data: any[], index: number) {123  return data[index].toHuman();124}125126function eventData<T = any>(data: any[], index: number) {127  return data[index] as T;128}129130// eslint-disable-next-line @typescript-eslint/naming-convention131function EventSection(section: string) {132  return class Section {133    static section = section;134135    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {136      const helperClass = EventHelper(Section.section, name, wrapEvent);137      return new helperClass();138    }139  };140}141142function schedulerSection(schedulerInstance: string) {143  return class extends EventSection(schedulerInstance) {144    static Dispatched = this.Method('Dispatched', data => ({145      task: eventJsonData(data, 0),146      id: eventHumanData(data, 1),147      result: data[2],148    }));149150    static PriorityChanged = this.Method('PriorityChanged', data => ({151      task: eventJsonData(data, 0),152      priority: eventJsonData(data, 1),153    }));154  };155}156157export class Event {158  static Democracy = class extends EventSection('democracy') {159    static Proposed = this.Method('Proposed', data => ({160      proposalIndex: eventJsonData<number>(data, 0),161    }));162163    static ExternalTabled = this.Method('ExternalTabled');164165    static Started = this.Method('Started', data => ({166      referendumIndex: eventJsonData<number>(data, 0),167      threshold: eventHumanData(data, 1),168    }));169170    static Voted = this.Method('Voted', data => ({171      voter: eventJsonData(data, 0),172      referendumIndex: eventJsonData<number>(data, 1),173      vote: eventJsonData(data, 2),174    }));175176    static Passed = this.Method('Passed', data => ({177      referendumIndex: eventJsonData<number>(data, 0),178    }));179180    static ProposalCanceled = this.Method('ProposalCanceled', data => ({181      propIndex: eventJsonData<number>(data, 0),182    }));183184    static Cancelled = this.Method('Cancelled', data => ({185      propIndex: eventJsonData<number>(data, 0),186    }));187188    static Vetoed = this.Method('Vetoed', data => ({189      who: eventHumanData(data, 0),190      proposalHash: eventHumanData(data, 1),191      until: eventJsonData<number>(data, 1),192    }));193  };194195  static Council = class extends EventSection('council') {196    static Proposed = this.Method('Proposed', data => ({197      account: eventHumanData(data, 0),198      proposalIndex: eventJsonData<number>(data, 1),199      proposalHash: eventHumanData(data, 2),200      threshold: eventJsonData<number>(data, 3),201    }));202    static Closed = this.Method('Closed', data => ({203      proposalHash: eventHumanData(data, 0),204      yes: eventJsonData<number>(data, 1),205      no: eventJsonData<number>(data, 2),206    }));207    static Executed = this.Method('Executed', data => ({208      proposalHash: eventHumanData(data, 0),209    }));210  };211212  static TechnicalCommittee = class extends EventSection('technicalCommittee') {213    static Proposed = this.Method('Proposed', data => ({214      account: eventHumanData(data, 0),215      proposalIndex: eventJsonData<number>(data, 1),216      proposalHash: eventHumanData(data, 2),217      threshold: eventJsonData<number>(data, 3),218    }));219    static Closed = this.Method('Closed', data => ({220      proposalHash: eventHumanData(data, 0),221      yes: eventJsonData<number>(data, 1),222      no: eventJsonData<number>(data, 2),223    }));224    static Approved = this.Method('Approved', data => ({225      proposalHash: eventHumanData(data, 0),226    }));227    static Executed = this.Method('Executed', data => ({228      proposalHash: eventHumanData(data, 0),229      result: eventHumanData(data, 1),230    }));231  };232233  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {234    static Submitted = this.Method('Submitted', data => ({235      referendumIndex: eventJsonData<number>(data, 0),236      trackId: eventJsonData<number>(data, 1),237      proposal: eventJsonData(data, 2),238    }));239240    static Cancelled = this.Method('Cancelled', data => ({241      index: eventJsonData<number>(data, 0),242      tally: eventJsonData(data, 1),243    }));244  };245246  static UniqueScheduler = schedulerSection('uniqueScheduler');247  static Scheduler = schedulerSection('scheduler');248249  static XcmpQueue = class extends EventSection('xcmpQueue') {250    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({251      messageHash: eventJsonData(data, 0),252    }));253254    static Success = this.Method('Success', data => ({255      messageHash: eventJsonData(data, 0),256    }));257258    static Fail = this.Method('Fail', data => ({259      messageHash: eventJsonData(data, 0),260      outcome: eventData<XcmV2TraitsError>(data, 1),261    }));262  };263}264265// eslint-disable-next-line @typescript-eslint/naming-convention266export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {267  return class extends Base {268    constructor(...args: any[]) {269      super(...args);270    }271272    async executeExtrinsic(273      sender: IKeyringPair,274      extrinsic: string,275      params: any[],276      expectSuccess?: boolean,277      options: Partial<SignerOptions> | null = null,278    ): Promise<ITransactionResult> {279      const call = this.constructApiCall(extrinsic, params);280      const result = await super.executeExtrinsic(281        sender,282        'api.tx.sudo.sudo',283        [call],284        expectSuccess,285        options,286      );287288      if(result.status === 'Fail') return result;289290      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;291      if(data.isErr) {292        if(data.asErr.isModule) {293          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;294          const metaError = super.getApi()?.registry.findMetaError(error);295          throw new Error(`${metaError.section}.${metaError.name}`);296        } else if(data.asErr.isToken) {297          throw new Error(`Token: ${data.asErr.asToken}`);298        }299        // May be [object Object] in case of unhandled non-unit enum300        throw new Error(`Misc: ${data.asErr.toHuman()}`);301      }302      return result;303    }304    async executeExtrinsicUncheckedWeight(305      sender: IKeyringPair,306      extrinsic: string,307      params: any[],308      expectSuccess?: boolean,309      options: Partial<SignerOptions> | null = null,310    ): Promise<ITransactionResult> {311      const call = this.constructApiCall(extrinsic, params);312      const result = await super.executeExtrinsic(313        sender,314        'api.tx.sudo.sudoUncheckedWeight',315        [call, {refTime: 0, proofSize: 0}],316        expectSuccess,317        options,318      );319320      if(result.status === 'Fail') return result;321322      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;323      if(data.isErr) {324        if(data.asErr.isModule) {325          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;326          const metaError = super.getApi()?.registry.findMetaError(error);327          throw new Error(`${metaError.section}.${metaError.name}`);328        } else if(data.asErr.isToken) {329          throw new Error(`Token: ${data.asErr.asToken}`);330        }331        // May be [object Object] in case of unhandled non-unit enum332        throw new Error(`Misc: ${data.asErr.toHuman()}`);333      }334      return result;335    }336  };337}338339class SchedulerGroup extends HelperGroup<UniqueHelper> {340  constructor(helper: UniqueHelper) {341    super(helper);342  }343344  cancelScheduled(signer: TSigner, scheduledId: string) {345    return this.helper.executeExtrinsic(346      signer,347      'api.tx.scheduler.cancelNamed',348      [scheduledId],349      true,350    );351  }352353  changePriority(signer: TSigner, scheduledId: string, priority: number) {354    return this.helper.executeExtrinsic(355      signer,356      'api.tx.scheduler.changeNamedPriority',357      [scheduledId, priority],358      true,359    );360  }361362  scheduleAt<T extends DevUniqueHelper>(363    executionBlockNumber: number,364    options: ISchedulerOptions = {},365  ) {366    return this.schedule<T>('schedule', executionBlockNumber, options);367  }368369  scheduleAfter<T extends DevUniqueHelper>(370    blocksBeforeExecution: number,371    options: ISchedulerOptions = {},372  ) {373    return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);374  }375376  schedule<T extends UniqueHelper>(377    scheduleFn: 'schedule' | 'scheduleAfter',378    blocksNum: number,379    options: ISchedulerOptions = {},380  ) {381    // eslint-disable-next-line @typescript-eslint/naming-convention382    const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);383    return this.helper.clone(ScheduledHelperType, {384      scheduleFn,385      blocksNum,386      options,387    }) as T;388  }389}390391class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {392  //todo:collator documentation393  addInvulnerable(signer: TSigner, address: string) {394    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);395  }396397  removeInvulnerable(signer: TSigner, address: string) {398    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);399  }400401  async getInvulnerables(): Promise<string[]> {402    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());403  }404405  /** and also total max invulnerables */406  maxCollators(): number {407    return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);408  }409410  async getDesiredCollators(): Promise<number> {411    return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();412  }413414  setLicenseBond(signer: TSigner, amount: bigint) {415    return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);416  }417418  async getLicenseBond(): Promise<bigint> {419    return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();420  }421422  obtainLicense(signer: TSigner) {423    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);424  }425426  releaseLicense(signer: TSigner) {427    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);428  }429430  forceReleaseLicense(signer: TSigner, released: string) {431    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);432  }433434  async hasLicense(address: string): Promise<bigint> {435    return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();436  }437438  onboard(signer: TSigner) {439    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);440  }441442  offboard(signer: TSigner) {443    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);444  }445446  async getCandidates(): Promise<string[]> {447    return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());448  }449}450451export class DevUniqueHelper extends UniqueHelper {452  /**453   * Arrange methods for tests454   */455  arrange: ArrangeGroup;456  wait: WaitGroup;457  admin: AdminGroup;458  session: SessionGroup;459  testUtils: TestUtilGroup;460  foreignAssets: ForeignAssetsGroup;461  xcm: XcmGroup<DevUniqueHelper>;462  xTokens: XTokensGroup<DevUniqueHelper>;463  tokens: TokensGroup<DevUniqueHelper>;464  scheduler: SchedulerGroup;465  collatorSelection: CollatorSelectionGroup;466  council: ICollectiveGroup;467  technicalCommittee: ICollectiveGroup;468  fellowship: IFellowshipGroup;469  democracy: DemocracyGroup;470471  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {472    options.helperBase = options.helperBase ?? DevUniqueHelper;473474    super(logger, options);475    this.arrange = new ArrangeGroup(this);476    this.wait = new WaitGroup(this);477    this.admin = new AdminGroup(this);478    this.testUtils = new TestUtilGroup(this);479    this.session = new SessionGroup(this);480    this.foreignAssets = new ForeignAssetsGroup(this);481    this.xcm = new XcmGroup(this, 'polkadotXcm');482    this.xTokens = new XTokensGroup(this);483    this.tokens = new TokensGroup(this);484    this.scheduler = new SchedulerGroup(this);485    this.collatorSelection = new CollatorSelectionGroup(this);486    this.council = {487      collective: new CollectiveGroup(this, 'council'),488      membership: new CollectiveMembershipGroup(this, 'councilMembership'),489    };490    this.technicalCommittee = {491      collective: new CollectiveGroup(this, 'technicalCommittee'),492      membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),493    };494    this.fellowship = {495      collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),496      referenda: new ReferendaGroup(this, 'fellowshipReferenda'),497    };498    this.democracy = new DemocracyGroup(this);499  }500501  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {502    if(!wsEndpoint) throw new Error('wsEndpoint was not set');503    const wsProvider = new WsProvider(wsEndpoint);504    this.api = new ApiPromise({505      provider: wsProvider,506      signedExtensions: {507        ContractHelpers: {508          extrinsic: {},509          payload: {},510        },511        CheckMaintenance: {512          extrinsic: {},513          payload: {},514        },515        DisableIdentityCalls: {516          extrinsic: {},517          payload: {},518        },519        FakeTransactionFinalizer: {520          extrinsic: {},521          payload: {},522        },523      },524      rpc: {525        unique: defs.unique.rpc,526        appPromotion: defs.appPromotion.rpc,527        povinfo: defs.povinfo.rpc,528        eth: {529          feeHistory: {530            description: 'Dummy',531            params: [],532            type: 'u8',533          },534          maxPriorityFeePerGas: {535            description: 'Dummy',536            params: [],537            type: 'u8',538          },539        },540      },541    });542    await this.api.isReadyOrError;543    this.network = await UniqueHelper.detectNetwork(this.api);544    this.wsEndpoint = wsEndpoint;545  }546  getSudo<T extends DevUniqueHelper>() {547    // eslint-disable-next-line @typescript-eslint/naming-convention548    const SudoHelperType = SudoHelper(this.helperBase);549    return this.clone(SudoHelperType) as T;550  }551}552553export class DevRelayHelper extends RelayHelper {554  wait: WaitGroup;555556  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {557    options.helperBase = options.helperBase ?? DevRelayHelper;558559    super(logger, options);560    this.wait = new WaitGroup(this);561  }562}563564export class DevWestmintHelper extends WestmintHelper {565  wait: WaitGroup;566567  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {568    options.helperBase = options.helperBase ?? DevWestmintHelper;569570    super(logger, options);571    this.wait = new WaitGroup(this);572  }573}574575export class DevStatemineHelper extends DevWestmintHelper {}576577export class DevStatemintHelper extends DevWestmintHelper {}578579export class DevMoonbeamHelper extends MoonbeamHelper {580  account: MoonbeamAccountGroup;581  wait: WaitGroup;582  fastDemocracy: MoonbeamFastDemocracyGroup;583584  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {585    options.helperBase = options.helperBase ?? DevMoonbeamHelper;586    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';587588    super(logger, options);589    this.account = new MoonbeamAccountGroup(this);590    this.wait = new WaitGroup(this);591    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);592  }593}594595export class DevMoonriverHelper extends DevMoonbeamHelper {596  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {597    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';598    super(logger, options);599  }600}601602export class DevAstarHelper extends AstarHelper {603  wait: WaitGroup;604605  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {606    options.helperBase = options.helperBase ?? DevAstarHelper;607608    super(logger, options);609    this.wait = new WaitGroup(this);610  }611612  getSudo<T extends AstarHelper>() {613    // eslint-disable-next-line @typescript-eslint/naming-convention614    const SudoHelperType = SudoHelper(this.helperBase);615    return this.clone(SudoHelperType) as T;616  }617}618619export class DevShidenHelper extends DevAstarHelper { }620621export class DevAcalaHelper extends AcalaHelper {622  wait: WaitGroup;623624  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {625    options.helperBase = options.helperBase ?? DevAcalaHelper;626627    super(logger, options);628    this.wait = new WaitGroup(this);629  }630  getSudo() {631    // eslint-disable-next-line @typescript-eslint/naming-convention632    const SudoHelperType = SudoHelper(this.helperBase);633    return this.clone(SudoHelperType) as DevAcalaHelper;634  }635}636637export class DevPolkadexHelper extends PolkadexHelper {638  wait: WaitGroup;639  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {640    options.helperBase = options.helperBase ?? PolkadexHelper;641642    super(logger, options);643    this.wait = new WaitGroup(this);644  }645646  getSudo() {647    // eslint-disable-next-line @typescript-eslint/naming-convention648    const SudoHelperType = SudoHelper(this.helperBase);649    return this.clone(SudoHelperType) as DevPolkadexHelper;650  }651}652653export class DevKaruraHelper extends DevAcalaHelper {}654655export class ArrangeGroup {656  helper: DevUniqueHelper;657658  scheduledIdSlider = 0;659660  constructor(helper: DevUniqueHelper) {661    this.helper = helper;662  }663664  /**665   * Generates accounts with the specified UNQ token balance666   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.667   * @param donor donor account for balances668   * @returns array of newly created accounts669   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);670   */671  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {672    let nonce = await this.helper.chain.getNonce(donor.address);673    const wait = new WaitGroup(this.helper);674    const ss58Format = this.helper.chain.getChainProperties().ss58Format;675    const tokenNominal = this.helper.balance.getOneTokenNominal();676    const transactions = [];677    const accounts: IKeyringPair[] = [];678    for(const balance of balances) {679      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);680      accounts.push(recipient);681      if(balance !== 0n) {682        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);683        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));684        nonce++;685      }686    }687688    await Promise.all(transactions).catch(_e => {});689690    //#region TODO remove this region, when nonce problem will be solved691    const checkBalances = async () => {692      let isSuccess = true;693      for(let i = 0; i < balances.length; i++) {694        const balance = await this.helper.balance.getSubstrate(accounts[i].address);695        if(balance !== balances[i] * tokenNominal) {696          isSuccess = false;697          break;698        }699      }700      return isSuccess;701    };702703    let accountsCreated = false;704    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;705    // checkBalances retry up to 5-50 blocks706    for(let index = 0; index < maxBlocksChecked; index++) {707      accountsCreated = await checkBalances();708      if(accountsCreated) break;709      await wait.newBlocks(1);710    }711712    if(!accountsCreated) throw Error('Accounts generation failed');713    //#endregion714715    return accounts;716  };717718  // TODO combine this method and createAccounts into one719  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {720    const createAsManyAsCan = async () => {721      let transactions: any = [];722      const accounts: IKeyringPair[] = [];723      let nonce = await this.helper.chain.getNonce(donor.address);724      const tokenNominal = this.helper.balance.getOneTokenNominal();725      const ss58Format = this.helper.chain.getChainProperties().ss58Format;726      for(let i = 0; i < accountsToCreate; i++) {727        if(i === 500) { // if there are too many accounts to create728          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled729          transactions = []; //730          nonce = await this.helper.chain.getNonce(donor.address); // update nonce731        }732        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);733        accounts.push(recipient);734        if(withBalance !== 0n) {735          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);736          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));737          nonce++;738        }739      }740741      const fullfilledAccounts = [];742      await Promise.allSettled(transactions);743      for(const account of accounts) {744        const accountBalance = await this.helper.balance.getSubstrate(account.address);745        if(accountBalance === withBalance * tokenNominal) {746          fullfilledAccounts.push(account);747        }748      }749      return fullfilledAccounts;750    };751752753    const crowd: IKeyringPair[] = [];754    // do up to 5 retries755    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {756      const asManyAsCan = await createAsManyAsCan();757      crowd.push(...asManyAsCan);758      accountsToCreate -= asManyAsCan.length;759    }760761    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);762763    return crowd;764  };765766  /**767   * Generates one account with zero balance768   * @returns the newly generated account769   * @example const account = await helper.arrange.createEmptyAccount();770   */771  createEmptyAccount = (): IKeyringPair => {772    const ss58Format = this.helper.chain.getChainProperties().ss58Format;773    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);774  };775776  isDevNode = async () => {777    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();778    if(blockNumber == 0) {779      await this.helper.wait.newBlocks(1);780      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();781    }782    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);783    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);784    const findCreationDate = (block: any) => {785      const humanBlock = block.toHuman();786      let date;787      humanBlock.block.extrinsics.forEach((ext: any) => {788        if(ext.method.section === 'timestamp') {789          date = Number(ext.method.args.now.replaceAll(',', ''));790        }791      });792      return date;793    };794    const block1date = await findCreationDate(block1);795    const block2date = await findCreationDate(block2);796    if(block2date! - block1date! < 9000) return true;797  };798799  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {800    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);801    let balance = await this.helper.balance.getSubstrate(address);802803    await promise();804805    balance -= await this.helper.balance.getSubstrate(address);806807    return balance;808  }809810  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {811    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);812813    const kvJson: {[key: string]: string} = {};814815    for(const kv of rawPovInfo.keyValues) {816      kvJson[kv.key.toHex()] = kv.value.toHex();817    }818819    const kvStr = JSON.stringify(kvJson);820821    const chainql = spawnSync(822      'chainql',823      [824        `--tla-code=data=${kvStr}`,825        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,826      ],827    );828829    if(!chainql.stdout) {830      throw Error('unable to get an output from the `chainql`');831    }832833    return {834      proofSize: rawPovInfo.proofSize.toNumber(),835      compactProofSize: rawPovInfo.compactProofSize.toNumber(),836      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),837      results: rawPovInfo.results,838      kv: JSON.parse(chainql.stdout.toString()),839    };840  }841842  calculatePalletAddress(palletId: any) {843    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));844    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);845  }846847  makeScheduledIds(num: number): string[] {848    function makeId(slider: number) {849      const scheduledIdSize = 64;850      const hexId = slider.toString(16);851      const prefixSize = scheduledIdSize - hexId.length;852853      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;854855      return scheduledId;856    }857858    const ids = [];859    for(let i = 0; i < num; i++) {860      ids.push(makeId(this.scheduledIdSlider));861      this.scheduledIdSlider += 1;862    }863864    return ids;865  }866867  makeScheduledId(): string {868    return (this.makeScheduledIds(1))[0];869  }870871  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {872    const capture = new EventCapture(this.helper, eventSection, eventMethod);873    await capture.startCapture();874875    return capture;876  }877878  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {879    return {880      V2: [881        {882          WithdrawAsset: [883            {884              id,885              fun: {886                Fungible: amount,887              },888            },889          ],890        },891        {892          BuyExecution: {893            fees: {894              id,895              fun: {896                Fungible: amount,897              },898            },899            weightLimit: 'Unlimited',900          },901        },902        {903          DepositAsset: {904            assets: {905              Wild: 'All',906            },907            maxAssets: 1,908            beneficiary: {909              parents: 0,910              interior: {911                X1: {912                  AccountId32: {913                    network: 'Any',914                    id: beneficiary,915                  },916                },917              },918            },919          },920        },921      ],922    };923  }924925  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {926    return {927      V2: [928        {929          ReserveAssetDeposited: [930            {931              id,932              fun: {933                Fungible: amount,934              },935            },936          ],937        },938        {939          BuyExecution: {940            fees: {941              id,942              fun: {943                Fungible: amount,944              },945            },946            weightLimit: 'Unlimited',947          },948        },949        {950          DepositAsset: {951            assets: {952              Wild: 'All',953            },954            maxAssets: 1,955            beneficiary: {956              parents: 0,957              interior: {958                X1: {959                  AccountId32: {960                    network: 'Any',961                    id: beneficiary,962                  },963                },964              },965            },966          },967        },968      ],969    };970  }971}972973class MoonbeamAccountGroup {974  helper: MoonbeamHelper;975976  keyring: Keyring;977  _alithAccount: IKeyringPair;978  _baltatharAccount: IKeyringPair;979  _dorothyAccount: IKeyringPair;980981  constructor(helper: MoonbeamHelper) {982    this.helper = helper;983984    this.keyring = new Keyring({type: 'ethereum'});985    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';986    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';987    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';988989    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');990    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');991    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');992  }993994  alithAccount() {995    return this._alithAccount;996  }997998  baltatharAccount() {999    return this._baltatharAccount;1000  }10011002  dorothyAccount() {1003    return this._dorothyAccount;1004  }10051006  create() {1007    return this.keyring.addFromUri(mnemonicGenerate());1008  }1009}10101011class MoonbeamFastDemocracyGroup {1012  helper: DevMoonbeamHelper;10131014  constructor(helper: DevMoonbeamHelper) {1015    this.helper = helper;1016  }10171018  async executeProposal(proposalDesciption: string, encodedProposal: string) {1019    const proposalHash = blake2AsHex(encodedProposal);10201021    const alithAccount = this.helper.account.alithAccount();1022    const baltatharAccount = this.helper.account.baltatharAccount();1023    const dorothyAccount = this.helper.account.dorothyAccount();10241025    const councilVotingThreshold = 2;1026    const technicalCommitteeThreshold = 2;1027    const fastTrackVotingPeriod = 3;1028    const fastTrackDelayPeriod = 0;10291030    console.log(`[democracy] executing '${proposalDesciption}' proposal`);10311032    // >>> Propose external motion through council >>>1033    console.log('\t* Propose external motion through council.......');1034    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1035    const encodedMotion = externalMotion?.method.toHex() || '';1036    const motionHash = blake2AsHex(encodedMotion);1037    console.log('\t* Motion hash is %s', motionHash);10381039    await this.helper.collective.council.propose(1040      baltatharAccount,1041      councilVotingThreshold,1042      externalMotion,1043      externalMotion.encodedLength,1044    );10451046    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1047    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1048    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);10491050    await this.helper.collective.council.close(1051      dorothyAccount,1052      motionHash,1053      councilProposalIdx,1054      {1055        refTime: 1_000_000_000,1056        proofSize: 1_000_000,1057      },1058      externalMotion.encodedLength,1059    );1060    console.log('\t* Propose external motion through council.......DONE');1061    // <<< Propose external motion through council <<<10621063    // >>> Fast track proposal through technical committee >>>1064    console.log('\t* Fast track proposal through technical committee.......');1065    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1066    const encodedFastTrack = fastTrack?.method.toHex() || '';1067    const fastTrackHash = blake2AsHex(encodedFastTrack);1068    console.log('\t* FastTrack hash is %s', fastTrackHash);10691070    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);10711072    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1073    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1074    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);10751076    await this.helper.collective.techCommittee.close(1077      baltatharAccount,1078      fastTrackHash,1079      techProposalIdx,1080      {1081        refTime: 1_000_000_000,1082        proofSize: 1_000_000,1083      },1084      fastTrack.encodedLength,1085    );1086    console.log('\t* Fast track proposal through technical committee.......DONE');1087    // <<< Fast track proposal through technical committee <<<10881089    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1090    const referendumIndex = democracyStarted.referendumIndex;10911092    // >>> Referendum voting >>>1093    console.log(`\t* Referendum #${referendumIndex} voting.......`);1094    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1095      balance: 10_000_000_000_000_000_000n,1096      vote: {aye: true, conviction: 1},1097    });1098    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1099    // <<< Referendum voting <<<11001101    // Wait the proposal to pass1102    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11031104    await this.helper.wait.newBlocks(1);11051106    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1107  }1108}11091110class WaitGroup {1111  helper: ChainHelperBase;11121113  constructor(helper: ChainHelperBase) {1114    this.helper = helper;1115  }11161117  sleep(milliseconds: number) {1118    return new Promise((resolve) => setTimeout(resolve, milliseconds));1119  }11201121  private async waitWithTimeout(promise: Promise<any>, timeout: number) {1122    let isBlock = false;1123    promise.then(() => isBlock = true).catch(() => isBlock = true);1124    let totalTime = 0;1125    const step = 100;1126    while(!isBlock) {1127      await this.sleep(step);1128      totalTime += step;1129      if(totalTime >= timeout) throw Error('Blocks production failed');1130    }1131    return promise;1132  }11331134  /**1135   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1136   * @param promise async operation to race against the timeout1137   * @param timeoutMS time after which to time out1138   * @param timeoutError error message to throw1139   * @returns promise of the same type the operation had1140   */1141  withTimeout<T>(1142    promise: Promise<T>,1143    timeoutMS = 30000,1144    timeoutError = 'The operation has timed out!',1145  ): Promise<T> {1146    const timeout = new Promise<never>((_, reject) => {1147      setTimeout(() => {1148        reject(new Error(timeoutError));1149      }, timeoutMS);1150    });11511152    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1153  }11541155  /**1156   * Wait for specified number of blocks1157   * @param blocksCount number of blocks to wait1158   * @returns1159   */1160  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1161    timeout = timeout ?? blocksCount * 60_000;1162    // eslint-disable-next-line no-async-promise-executor1163    const promise = new Promise<void>(async (resolve) => {1164      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1165        if(blocksCount > 0) {1166          blocksCount--;1167        } else {1168          unsubscribe();1169          resolve();1170        }1171      });1172    });1173    await this.waitWithTimeout(promise, timeout);1174    return promise;1175  }11761177  /**1178   * Wait for the specified number of sessions to pass.1179   * Only applicable if the Session pallet is turned on.1180   * @param sessionCount number of sessions to wait1181   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1182   * @returns1183   */1184  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1185    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`1186      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');11871188    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1189    let currentSessionIndex = -1;11901191    while(currentSessionIndex < expectedSessionIndex) {1192      // eslint-disable-next-line no-async-promise-executor1193      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1194        await this.newBlocks(1);1195        const res = await (this.helper as DevUniqueHelper).session.getIndex();1196        resolve(res);1197      }), blockTimeout, 'The chain has stopped producing blocks!');1198    }1199  }12001201  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1202    timeout = timeout ?? 30 * 60 * 1000;1203    // eslint-disable-next-line no-async-promise-executor1204    const promise = new Promise<void>(async (resolve) => {1205      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1206        if(data.number.toNumber() >= blockNumber) {1207          unsubscribe();1208          resolve();1209        }1210      });1211    });1212    await this.waitWithTimeout(promise, timeout);1213    return promise;1214  }12151216  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1217    timeout = timeout ?? 30 * 60 * 1000;1218    // eslint-disable-next-line no-async-promise-executor1219    const promise = new Promise<void>(async (resolve) => {1220      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1221        if(data.value.relayParentNumber.toNumber() >= blockNumber) {1222          // @ts-ignore1223          unsubscribe();1224          resolve();1225        }1226      });1227    });1228    await this.waitWithTimeout(promise, timeout);1229    return promise;1230  }12311232  noScheduledTasks() {1233    const api = this.helper.getApi();12341235    // eslint-disable-next-line no-async-promise-executor1236    const promise = new Promise<void>(async resolve => {1237      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1238        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();12391240        if(areThereScheduledTasks.length == 0) {1241          unsubscribe();1242          resolve();1243        }1244      });1245    });12461247    return promise;1248  }12491250  parachainBlockMultiplesOf(val: bigint) {1251    // eslint-disable-next-line no-async-promise-executor1252    const promise = new Promise<void>(async resolve => {1253      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1254        if(data.number.toBigInt() % val == 0n) {1255          console.log(`from waiter: ${data.number.toBigInt()}`);1256          unsubscribe();1257          resolve();1258        }1259      });1260    });1261    return promise;1262  }12631264  event<T extends IEventHelper>(1265    maxBlocksToWait: number,1266    eventHelper: T,1267    filter: (_: any) => boolean = () => true,1268  ): any {1269    // eslint-disable-next-line no-async-promise-executor1270    const promise = new Promise<T | null>(async (resolve) => {1271      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1272        const blockNumber = header.number.toHuman();1273        const blockHash = header.hash;1274        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1275        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;12761277        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);12781279        const apiAt = await this.helper.getApi().at(blockHash);1280        const eventRecords = (await apiAt.query.system.events()) as any;12811282        const neededEvent = eventRecords.toArray()1283          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1284          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1285          .find(filter);12861287        if(neededEvent) {1288          unsubscribe();1289          resolve(neededEvent);1290        } else if(maxBlocksToWait > 0) {1291          maxBlocksToWait--;1292        } else {1293          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1294          The wait lasted until block ${blockNumber} inclusive`);1295          unsubscribe();1296          resolve(null);1297        }1298      });1299    });1300    return promise;1301  }13021303  async expectEvent<T extends IEventHelper>(1304    maxBlocksToWait: number,1305    eventHelper: T,1306    filter: (e: any) => boolean = () => true,1307  ) {1308    const e = await this.event(maxBlocksToWait, eventHelper, filter);1309    if(e == null) {1310      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1311    } else {1312      return e;1313    }1314  }1315}13161317class SessionGroup {1318  helper: ChainHelperBase;13191320  constructor(helper: ChainHelperBase) {1321    this.helper = helper;1322  }13231324  //todo:collator documentation1325  async getIndex(): Promise<number> {1326    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1327  }13281329  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1330    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1331  }13321333  setOwnKeys(signer: TSigner, key: string) {1334    return this.helper.executeExtrinsic(1335      signer,1336      'api.tx.session.setKeys',1337      [key, '0x0'],1338      true,1339    );1340  }13411342  setOwnKeysFromAddress(signer: TSigner) {1343    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1344  }1345}13461347class TestUtilGroup {1348  helper: DevUniqueHelper;13491350  constructor(helper: DevUniqueHelper) {1351    this.helper = helper;1352  }13531354  async enable() {1355    if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1356      return;1357    }13581359    const signer = this.helper.util.fromSeed('//Alice');1360    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1361  }13621363  async setTestValue(signer: TSigner, testVal: number) {1364    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1365  }13661367  async incTestValue(signer: TSigner) {1368    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1369  }13701371  async setTestValueAndRollback(signer: TSigner, testVal: number) {1372    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1373  }13741375  async testValue(blockIdx?: number) {1376    const api = blockIdx1377      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1378      : this.helper.getApi();13791380    return (await api.query.testUtils.testValue()).toJSON();1381  }13821383  async justTakeFee(signer: TSigner) {1384    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1385  }13861387  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1388    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1389  }1390}13911392class EventCapture {1393  helper: DevUniqueHelper;1394  eventSection: string;1395  eventMethod: string;1396  events: EventRecord[] = [];1397  unsubscribe: VoidFn | null = null;13981399  constructor(1400    helper: DevUniqueHelper,1401    eventSection: string,1402    eventMethod: string,1403  ) {1404    this.helper = helper;1405    this.eventSection = eventSection;1406    this.eventMethod = eventMethod;1407  }14081409  async startCapture() {1410    this.stopCapture();1411    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1412      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);14131414      this.events.push(...newEvents);1415    })) as any;1416  }14171418  stopCapture() {1419    if(this.unsubscribe !== null) {1420      this.unsubscribe();1421    }1422  }14231424  extractCapturedEvents() {1425    return this.events;1426  }1427}14281429class AdminGroup {1430  helper: UniqueHelper;14311432  constructor(helper: UniqueHelper) {1433    this.helper = helper;1434  }14351436  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1437    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1438    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1439      staker: e.event.data[0].toString(),1440      stake: e.event.data[1].toBigInt(),1441      payout: e.event.data[2].toBigInt(),1442    }));1443  }1444}14451446// eslint-disable-next-line @typescript-eslint/naming-convention1447function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1448  return class extends Base {1449    scheduleFn: 'schedule' | 'scheduleAfter';1450    blocksNum: number;1451    options: ISchedulerOptions;14521453    constructor(...args: any[]) {1454      const logger = args[0] as ILogger;1455      const options = args[1] as {1456        scheduleFn: 'schedule' | 'scheduleAfter',1457        blocksNum: number,1458        options: ISchedulerOptions1459      };14601461      super(logger);14621463      this.scheduleFn = options.scheduleFn;1464      this.blocksNum = options.blocksNum;1465      this.options = options.options;1466    }14671468    executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1469      const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);14701471      const mandatorySchedArgs = [1472        this.blocksNum,1473        this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1474        this.options.priority ?? null,1475        scheduledTx,1476      ];14771478      let schedArgs;1479      let scheduleFn;14801481      if(this.options.scheduledId) {1482        schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];14831484        if(this.scheduleFn == 'schedule') {1485          scheduleFn = 'scheduleNamed';1486        } else if(this.scheduleFn == 'scheduleAfter') {1487          scheduleFn = 'scheduleNamedAfter';1488        }1489      } else {1490        schedArgs = mandatorySchedArgs;1491        scheduleFn = this.scheduleFn;1492      }14931494      const extrinsic = 'api.tx.scheduler.' + scheduleFn;14951496      return super.executeExtrinsic(1497        sender,1498        extrinsic as any,1499        schedArgs,1500        expectSuccess,1501      );1502    }1503  };1504}
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, ChainHelperBase, ChainHelperBaseConstructor, HelperGroup, UniqueHelperConstructor} 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, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError, XcmV3TraitsOutcome} from '@polkadot/types/lookup';13import {SignerOptions, VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';16import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup} from './unique.xcm';17import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, ICollectiveGroup, IFellowshipGroup, RankedCollectiveGroup, ReferendaGroup} from './unique.governance';1819export class SilentLogger {20  log(_msg: any, _level: any): void { }21  level = {22    ERROR: 'ERROR' as const,23    WARNING: 'WARNING' as const,24    INFO: 'INFO' as const,25  };26}2728export class SilentConsole {29  // TODO: Remove, this is temporary: Filter unneeded API output30  // (Jaco promised it will be removed in the next version)31  consoleErr: any;32  consoleLog: any;33  consoleWarn: any;3435  constructor() {36    this.consoleErr = console.error;37    this.consoleLog = console.log;38    this.consoleWarn = console.warn;39  }4041  enable() {42    const outFn = (printer: any) => (...args: any[]) => {43      for(const arg of args) {44        if(typeof arg !== 'string')45          continue;46        const skippedWarnings = ['1000:: Normal connection closure', 'Not decorating unknown runtime apis:', 'RPC methods not decorated:', 'Not decorating runtime apis', 'Bad input data provided to validate_transaction', 'account balance too low', '1006:: Abnormal Closure'];47        const needToSkip = skippedWarnings.reduce((a,  b) => a || arg.includes(b), false);48        if(needToSkip || arg === 'Normal connection closure')49          return;50      }51      printer(...args);52    };5354    console.error = outFn(this.consoleErr.bind(console));55    console.log = outFn(this.consoleLog.bind(console));56    console.warn = outFn(this.consoleWarn.bind(console));57  }5859  disable() {60    console.error = this.consoleErr;61    console.log = this.consoleLog;62    console.warn = this.consoleWarn;63  }64}6566export interface IEventHelper {67  section(): string;6869  method(): string;7071  wrapEvent(data: any[]): any;72}7374// eslint-disable-next-line @typescript-eslint/naming-convention75function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {76  const helperClass = class implements IEventHelper {77    wrapEvent: (data: any[]) => any;78    _section: string;79    _method: string;8081    constructor() {82      this.wrapEvent = wrapEvent;83      this._section = section;84      this._method = method;85    }8687    section(): string {88      return this._section;89    }9091    method(): string {92      return this._method;93    }9495    filter(txres: ITransactionResult) {96      return txres.result.events.filter(e => e.event.section === section && e.event.method === method)97        .map(e => this.wrapEvent(e.event.data));98    }99100    find(txres: ITransactionResult) {101      const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);102      return e ? this.wrapEvent(e.event.data) : null;103    }104105    expect(txres: ITransactionResult) {106      const e = this.find(txres);107      if(e) {108        return e;109      } else {110        throw Error(`Expected event ${section}.${method}`);111      }112    }113  };114115  return helperClass;116}117118function eventJsonData<T = any>(data: any[], index: number) {119  return data[index].toJSON() as T;120}121122function eventHumanData(data: any[], index: number) {123  return data[index].toHuman();124}125126function eventData<T = any>(data: any[], index: number) {127  return data[index] as T;128}129130// eslint-disable-next-line @typescript-eslint/naming-convention131function EventSection(section: string) {132  return class Section {133    static section = section;134135    static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {136      const helperClass = EventHelper(Section.section, name, wrapEvent);137      return new helperClass();138    }139  };140}141142function schedulerSection(schedulerInstance: string) {143  return class extends EventSection(schedulerInstance) {144    static Dispatched = this.Method('Dispatched', data => ({145      task: eventJsonData(data, 0),146      id: eventHumanData(data, 1),147      result: data[2],148    }));149150    static PriorityChanged = this.Method('PriorityChanged', data => ({151      task: eventJsonData(data, 0),152      priority: eventJsonData(data, 1),153    }));154  };155}156157export class Event {158  static Democracy = class extends EventSection('democracy') {159    static Proposed = this.Method('Proposed', data => ({160      proposalIndex: eventJsonData<number>(data, 0),161    }));162163    static ExternalTabled = this.Method('ExternalTabled');164165    static Started = this.Method('Started', data => ({166      referendumIndex: eventJsonData<number>(data, 0),167      threshold: eventHumanData(data, 1),168    }));169170    static Voted = this.Method('Voted', data => ({171      voter: eventJsonData(data, 0),172      referendumIndex: eventJsonData<number>(data, 1),173      vote: eventJsonData(data, 2),174    }));175176    static Passed = this.Method('Passed', data => ({177      referendumIndex: eventJsonData<number>(data, 0),178    }));179180    static ProposalCanceled = this.Method('ProposalCanceled', data => ({181      propIndex: eventJsonData<number>(data, 0),182    }));183184    static Cancelled = this.Method('Cancelled', data => ({185      propIndex: eventJsonData<number>(data, 0),186    }));187188    static Vetoed = this.Method('Vetoed', data => ({189      who: eventHumanData(data, 0),190      proposalHash: eventHumanData(data, 1),191      until: eventJsonData<number>(data, 1),192    }));193  };194195  static Council = class extends EventSection('council') {196    static Proposed = this.Method('Proposed', data => ({197      account: eventHumanData(data, 0),198      proposalIndex: eventJsonData<number>(data, 1),199      proposalHash: eventHumanData(data, 2),200      threshold: eventJsonData<number>(data, 3),201    }));202    static Closed = this.Method('Closed', data => ({203      proposalHash: eventHumanData(data, 0),204      yes: eventJsonData<number>(data, 1),205      no: eventJsonData<number>(data, 2),206    }));207    static Executed = this.Method('Executed', data => ({208      proposalHash: eventHumanData(data, 0),209    }));210  };211212  static TechnicalCommittee = class extends EventSection('technicalCommittee') {213    static Proposed = this.Method('Proposed', data => ({214      account: eventHumanData(data, 0),215      proposalIndex: eventJsonData<number>(data, 1),216      proposalHash: eventHumanData(data, 2),217      threshold: eventJsonData<number>(data, 3),218    }));219    static Closed = this.Method('Closed', data => ({220      proposalHash: eventHumanData(data, 0),221      yes: eventJsonData<number>(data, 1),222      no: eventJsonData<number>(data, 2),223    }));224    static Approved = this.Method('Approved', data => ({225      proposalHash: eventHumanData(data, 0),226    }));227    static Executed = this.Method('Executed', data => ({228      proposalHash: eventHumanData(data, 0),229      result: eventHumanData(data, 1),230    }));231  };232233  static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {234    static Submitted = this.Method('Submitted', data => ({235      referendumIndex: eventJsonData<number>(data, 0),236      trackId: eventJsonData<number>(data, 1),237      proposal: eventJsonData(data, 2),238    }));239240    static Cancelled = this.Method('Cancelled', data => ({241      index: eventJsonData<number>(data, 0),242      tally: eventJsonData(data, 1),243    }));244  };245246  static UniqueScheduler = schedulerSection('uniqueScheduler');247  static Scheduler = schedulerSection('scheduler');248249  static XcmpQueue = class extends EventSection('xcmpQueue') {250    static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({251      messageHash: eventJsonData(data, 0),252    }));253254    static Success = this.Method('Success', data => ({255      messageHash: eventJsonData(data, 0),256    }));257258    static Fail = this.Method('Fail', data => ({259      messageHash: eventJsonData(data, 0),260      outcome: eventData<XcmV2TraitsError>(data, 1),261    }));262  };263264  static DmpQueue = class extends EventSection('dmpQueue') {265    static ExecutedDownward = this.Method('ExecutedDownward', data => ({266      outcome: eventData<XcmV3TraitsOutcome>(data, 1),267    }));268  };269}270271// eslint-disable-next-line @typescript-eslint/naming-convention272export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {273  return class extends Base {274    constructor(...args: any[]) {275      super(...args);276    }277278    async executeExtrinsic(279      sender: IKeyringPair,280      extrinsic: string,281      params: any[],282      expectSuccess?: boolean,283      options: Partial<SignerOptions> | null = null,284    ): Promise<ITransactionResult> {285      const call = this.constructApiCall(extrinsic, params);286      const result = await super.executeExtrinsic(287        sender,288        'api.tx.sudo.sudo',289        [call],290        expectSuccess,291        options,292      );293294      if(result.status === 'Fail') return result;295296      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;297      if(data.isErr) {298        if(data.asErr.isModule) {299          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;300          const metaError = super.getApi()?.registry.findMetaError(error);301          throw new Error(`${metaError.section}.${metaError.name}`);302        } else if(data.asErr.isToken) {303          throw new Error(`Token: ${data.asErr.asToken}`);304        }305        // May be [object Object] in case of unhandled non-unit enum306        throw new Error(`Misc: ${data.asErr.toHuman()}`);307      }308      return result;309    }310    async executeExtrinsicUncheckedWeight(311      sender: IKeyringPair,312      extrinsic: string,313      params: any[],314      expectSuccess?: boolean,315      options: Partial<SignerOptions> | null = null,316    ): Promise<ITransactionResult> {317      const call = this.constructApiCall(extrinsic, params);318      const result = await super.executeExtrinsic(319        sender,320        'api.tx.sudo.sudoUncheckedWeight',321        [call, {refTime: 0, proofSize: 0}],322        expectSuccess,323        options,324      );325326      if(result.status === 'Fail') return result;327328      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;329      if(data.isErr) {330        if(data.asErr.isModule) {331          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;332          const metaError = super.getApi()?.registry.findMetaError(error);333          throw new Error(`${metaError.section}.${metaError.name}`);334        } else if(data.asErr.isToken) {335          throw new Error(`Token: ${data.asErr.asToken}`);336        }337        // May be [object Object] in case of unhandled non-unit enum338        throw new Error(`Misc: ${data.asErr.toHuman()}`);339      }340      return result;341    }342  };343}344345class SchedulerGroup extends HelperGroup<UniqueHelper> {346  constructor(helper: UniqueHelper) {347    super(helper);348  }349350  cancelScheduled(signer: TSigner, scheduledId: string) {351    return this.helper.executeExtrinsic(352      signer,353      'api.tx.scheduler.cancelNamed',354      [scheduledId],355      true,356    );357  }358359  changePriority(signer: TSigner, scheduledId: string, priority: number) {360    return this.helper.executeExtrinsic(361      signer,362      'api.tx.scheduler.changeNamedPriority',363      [scheduledId, priority],364      true,365    );366  }367368  scheduleAt<T extends DevUniqueHelper>(369    executionBlockNumber: number,370    options: ISchedulerOptions = {},371  ) {372    return this.schedule<T>('schedule', executionBlockNumber, options);373  }374375  scheduleAfter<T extends DevUniqueHelper>(376    blocksBeforeExecution: number,377    options: ISchedulerOptions = {},378  ) {379    return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);380  }381382  schedule<T extends UniqueHelper>(383    scheduleFn: 'schedule' | 'scheduleAfter',384    blocksNum: number,385    options: ISchedulerOptions = {},386  ) {387    // eslint-disable-next-line @typescript-eslint/naming-convention388    const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);389    return this.helper.clone(ScheduledHelperType, {390      scheduleFn,391      blocksNum,392      options,393    }) as T;394  }395}396397class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {398  //todo:collator documentation399  addInvulnerable(signer: TSigner, address: string) {400    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);401  }402403  removeInvulnerable(signer: TSigner, address: string) {404    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);405  }406407  async getInvulnerables(): Promise<string[]> {408    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());409  }410411  /** and also total max invulnerables */412  maxCollators(): number {413    return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);414  }415416  async getDesiredCollators(): Promise<number> {417    return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();418  }419420  setLicenseBond(signer: TSigner, amount: bigint) {421    return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);422  }423424  async getLicenseBond(): Promise<bigint> {425    return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();426  }427428  obtainLicense(signer: TSigner) {429    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);430  }431432  releaseLicense(signer: TSigner) {433    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);434  }435436  forceReleaseLicense(signer: TSigner, released: string) {437    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);438  }439440  async hasLicense(address: string): Promise<bigint> {441    return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();442  }443444  onboard(signer: TSigner) {445    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);446  }447448  offboard(signer: TSigner) {449    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);450  }451452  async getCandidates(): Promise<string[]> {453    return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());454  }455}456457export class DevUniqueHelper extends UniqueHelper {458  /**459   * Arrange methods for tests460   */461  arrange: ArrangeGroup;462  wait: WaitGroup;463  admin: AdminGroup;464  session: SessionGroup;465  testUtils: TestUtilGroup;466  foreignAssets: ForeignAssetsGroup;467  xcm: XcmGroup<DevUniqueHelper>;468  xTokens: XTokensGroup<DevUniqueHelper>;469  tokens: TokensGroup<DevUniqueHelper>;470  scheduler: SchedulerGroup;471  collatorSelection: CollatorSelectionGroup;472  council: ICollectiveGroup;473  technicalCommittee: ICollectiveGroup;474  fellowship: IFellowshipGroup;475  democracy: DemocracyGroup;476477  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {478    options.helperBase = options.helperBase ?? DevUniqueHelper;479480    super(logger, options);481    this.arrange = new ArrangeGroup(this);482    this.wait = new WaitGroup(this);483    this.admin = new AdminGroup(this);484    this.testUtils = new TestUtilGroup(this);485    this.session = new SessionGroup(this);486    this.foreignAssets = new ForeignAssetsGroup(this);487    this.xcm = new XcmGroup(this, 'polkadotXcm');488    this.xTokens = new XTokensGroup(this);489    this.tokens = new TokensGroup(this);490    this.scheduler = new SchedulerGroup(this);491    this.collatorSelection = new CollatorSelectionGroup(this);492    this.council = {493      collective: new CollectiveGroup(this, 'council'),494      membership: new CollectiveMembershipGroup(this, 'councilMembership'),495    };496    this.technicalCommittee = {497      collective: new CollectiveGroup(this, 'technicalCommittee'),498      membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),499    };500    this.fellowship = {501      collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),502      referenda: new ReferendaGroup(this, 'fellowshipReferenda'),503    };504    this.democracy = new DemocracyGroup(this);505  }506507  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {508    if(!wsEndpoint) throw new Error('wsEndpoint was not set');509    const wsProvider = new WsProvider(wsEndpoint);510    this.api = new ApiPromise({511      provider: wsProvider,512      signedExtensions: {513        ContractHelpers: {514          extrinsic: {},515          payload: {},516        },517        CheckMaintenance: {518          extrinsic: {},519          payload: {},520        },521        DisableIdentityCalls: {522          extrinsic: {},523          payload: {},524        },525        FakeTransactionFinalizer: {526          extrinsic: {},527          payload: {},528        },529      },530      rpc: {531        unique: defs.unique.rpc,532        appPromotion: defs.appPromotion.rpc,533        povinfo: defs.povinfo.rpc,534        eth: {535          feeHistory: {536            description: 'Dummy',537            params: [],538            type: 'u8',539          },540          maxPriorityFeePerGas: {541            description: 'Dummy',542            params: [],543            type: 'u8',544          },545        },546      },547    });548    await this.api.isReadyOrError;549    this.network = await UniqueHelper.detectNetwork(this.api);550    this.wsEndpoint = wsEndpoint;551  }552  getSudo<T extends DevUniqueHelper>() {553    // eslint-disable-next-line @typescript-eslint/naming-convention554    const SudoHelperType = SudoHelper(this.helperBase);555    return this.clone(SudoHelperType) as T;556  }557}558559export class DevRelayHelper extends RelayHelper {560  wait: WaitGroup;561562  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {563    options.helperBase = options.helperBase ?? DevRelayHelper;564565    super(logger, options);566    this.wait = new WaitGroup(this);567  }568569  getSudo() {570    // eslint-disable-next-line @typescript-eslint/naming-convention571    const SudoHelperType = SudoHelper(this.helperBase);572    return this.clone(SudoHelperType) as DevRelayHelper;573  }574}575576export class DevWestmintHelper extends WestmintHelper {577  wait: WaitGroup;578579  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {580    options.helperBase = options.helperBase ?? DevWestmintHelper;581582    super(logger, options);583    this.wait = new WaitGroup(this);584  }585}586587export class DevStatemineHelper extends DevWestmintHelper {}588589export class DevStatemintHelper extends DevWestmintHelper {}590591export class DevMoonbeamHelper extends MoonbeamHelper {592  account: MoonbeamAccountGroup;593  wait: WaitGroup;594  fastDemocracy: MoonbeamFastDemocracyGroup;595596  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {597    options.helperBase = options.helperBase ?? DevMoonbeamHelper;598    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';599600    super(logger, options);601    this.account = new MoonbeamAccountGroup(this);602    this.wait = new WaitGroup(this);603    this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);604  }605}606607export class DevMoonriverHelper extends DevMoonbeamHelper {608  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {609    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';610    super(logger, options);611  }612}613614export class DevAstarHelper extends AstarHelper {615  wait: WaitGroup;616617  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {618    options.helperBase = options.helperBase ?? DevAstarHelper;619620    super(logger, options);621    this.wait = new WaitGroup(this);622  }623624  getSudo<T extends AstarHelper>() {625    // eslint-disable-next-line @typescript-eslint/naming-convention626    const SudoHelperType = SudoHelper(this.helperBase);627    return this.clone(SudoHelperType) as T;628  }629}630631export class DevShidenHelper extends DevAstarHelper { }632633export class DevAcalaHelper extends AcalaHelper {634  wait: WaitGroup;635636  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {637    options.helperBase = options.helperBase ?? DevAcalaHelper;638639    super(logger, options);640    this.wait = new WaitGroup(this);641  }642  getSudo() {643    // eslint-disable-next-line @typescript-eslint/naming-convention644    const SudoHelperType = SudoHelper(this.helperBase);645    return this.clone(SudoHelperType) as DevAcalaHelper;646  }647}648649export class DevPolkadexHelper extends PolkadexHelper {650  wait: WaitGroup;651  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {652    options.helperBase = options.helperBase ?? PolkadexHelper;653654    super(logger, options);655    this.wait = new WaitGroup(this);656  }657658  getSudo() {659    // eslint-disable-next-line @typescript-eslint/naming-convention660    const SudoHelperType = SudoHelper(this.helperBase);661    return this.clone(SudoHelperType) as DevPolkadexHelper;662  }663}664665export class DevKaruraHelper extends DevAcalaHelper {}666667export class ArrangeGroup {668  helper: DevUniqueHelper;669670  scheduledIdSlider = 0;671672  constructor(helper: DevUniqueHelper) {673    this.helper = helper;674  }675676  /**677   * Generates accounts with the specified UNQ token balance678   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.679   * @param donor donor account for balances680   * @returns array of newly created accounts681   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);682   */683  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {684    let nonce = await this.helper.chain.getNonce(donor.address);685    const wait = new WaitGroup(this.helper);686    const ss58Format = this.helper.chain.getChainProperties().ss58Format;687    const tokenNominal = this.helper.balance.getOneTokenNominal();688    const transactions = [];689    const accounts: IKeyringPair[] = [];690    for(const balance of balances) {691      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);692      accounts.push(recipient);693      if(balance !== 0n) {694        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);695        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));696        nonce++;697      }698    }699700    await Promise.all(transactions).catch(_e => {});701702    //#region TODO remove this region, when nonce problem will be solved703    const checkBalances = async () => {704      let isSuccess = true;705      for(let i = 0; i < balances.length; i++) {706        const balance = await this.helper.balance.getSubstrate(accounts[i].address);707        if(balance !== balances[i] * tokenNominal) {708          isSuccess = false;709          break;710        }711      }712      return isSuccess;713    };714715    let accountsCreated = false;716    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;717    // checkBalances retry up to 5-50 blocks718    for(let index = 0; index < maxBlocksChecked; index++) {719      accountsCreated = await checkBalances();720      if(accountsCreated) break;721      await wait.newBlocks(1);722    }723724    if(!accountsCreated) throw Error('Accounts generation failed');725    //#endregion726727    return accounts;728  };729730  // TODO combine this method and createAccounts into one731  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {732    const createAsManyAsCan = async () => {733      let transactions: any = [];734      const accounts: IKeyringPair[] = [];735      let nonce = await this.helper.chain.getNonce(donor.address);736      const tokenNominal = this.helper.balance.getOneTokenNominal();737      const ss58Format = this.helper.chain.getChainProperties().ss58Format;738      for(let i = 0; i < accountsToCreate; i++) {739        if(i === 500) { // if there are too many accounts to create740          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled741          transactions = []; //742          nonce = await this.helper.chain.getNonce(donor.address); // update nonce743        }744        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);745        accounts.push(recipient);746        if(withBalance !== 0n) {747          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);748          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));749          nonce++;750        }751      }752753      const fullfilledAccounts = [];754      await Promise.allSettled(transactions);755      for(const account of accounts) {756        const accountBalance = await this.helper.balance.getSubstrate(account.address);757        if(accountBalance === withBalance * tokenNominal) {758          fullfilledAccounts.push(account);759        }760      }761      return fullfilledAccounts;762    };763764765    const crowd: IKeyringPair[] = [];766    // do up to 5 retries767    for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {768      const asManyAsCan = await createAsManyAsCan();769      crowd.push(...asManyAsCan);770      accountsToCreate -= asManyAsCan.length;771    }772773    if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);774775    return crowd;776  };777778  /**779   * Generates one account with zero balance780   * @returns the newly generated account781   * @example const account = await helper.arrange.createEmptyAccount();782   */783  createEmptyAccount = (): IKeyringPair => {784    const ss58Format = this.helper.chain.getChainProperties().ss58Format;785    return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);786  };787788  isDevNode = async () => {789    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();790    if(blockNumber == 0) {791      await this.helper.wait.newBlocks(1);792      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();793    }794    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);795    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);796    const findCreationDate = (block: any) => {797      const humanBlock = block.toHuman();798      let date;799      humanBlock.block.extrinsics.forEach((ext: any) => {800        if(ext.method.section === 'timestamp') {801          date = Number(ext.method.args.now.replaceAll(',', ''));802        }803      });804      return date;805    };806    const block1date = await findCreationDate(block1);807    const block2date = await findCreationDate(block2);808    if(block2date! - block1date! < 9000) return true;809  };810811  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {812    const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);813    let balance = await this.helper.balance.getSubstrate(address);814815    await promise();816817    balance -= await this.helper.balance.getSubstrate(address);818819    return balance;820  }821822  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {823    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);824825    const kvJson: {[key: string]: string} = {};826827    for(const kv of rawPovInfo.keyValues) {828      kvJson[kv.key.toHex()] = kv.value.toHex();829    }830831    const kvStr = JSON.stringify(kvJson);832833    const chainql = spawnSync(834      'chainql',835      [836        `--tla-code=data=${kvStr}`,837        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,838      ],839    );840841    if(!chainql.stdout) {842      throw Error('unable to get an output from the `chainql`');843    }844845    return {846      proofSize: rawPovInfo.proofSize.toNumber(),847      compactProofSize: rawPovInfo.compactProofSize.toNumber(),848      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),849      results: rawPovInfo.results,850      kv: JSON.parse(chainql.stdout.toString()),851    };852  }853854  calculatePalletAddress(palletId: any) {855    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));856    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);857  }858859  makeScheduledIds(num: number): string[] {860    function makeId(slider: number) {861      const scheduledIdSize = 64;862      const hexId = slider.toString(16);863      const prefixSize = scheduledIdSize - hexId.length;864865      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;866867      return scheduledId;868    }869870    const ids = [];871    for(let i = 0; i < num; i++) {872      ids.push(makeId(this.scheduledIdSlider));873      this.scheduledIdSlider += 1;874    }875876    return ids;877  }878879  makeScheduledId(): string {880    return (this.makeScheduledIds(1))[0];881  }882883  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {884    const capture = new EventCapture(this.helper, eventSection, eventMethod);885    await capture.startCapture();886887    return capture;888  }889890  makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {891    return {892      V2: [893        {894          WithdrawAsset: [895            {896              id,897              fun: {898                Fungible: amount,899              },900            },901          ],902        },903        {904          BuyExecution: {905            fees: {906              id,907              fun: {908                Fungible: amount,909              },910            },911            weightLimit: 'Unlimited',912          },913        },914        {915          DepositAsset: {916            assets: {917              Wild: 'All',918            },919            maxAssets: 1,920            beneficiary: {921              parents: 0,922              interior: {923                X1: {924                  AccountId32: {925                    network: 'Any',926                    id: beneficiary,927                  },928                },929              },930            },931          },932        },933      ],934    };935  }936937  makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {938    return {939      V2: [940        {941          ReserveAssetDeposited: [942            {943              id,944              fun: {945                Fungible: amount,946              },947            },948          ],949        },950        {951          BuyExecution: {952            fees: {953              id,954              fun: {955                Fungible: amount,956              },957            },958            weightLimit: 'Unlimited',959          },960        },961        {962          DepositAsset: {963            assets: {964              Wild: 'All',965            },966            maxAssets: 1,967            beneficiary: {968              parents: 0,969              interior: {970                X1: {971                  AccountId32: {972                    network: 'Any',973                    id: beneficiary,974                  },975                },976              },977            },978          },979        },980      ],981    };982  }983984  makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {985    return {986      V3: [987        {988          UnpaidExecution: {989            weightLimit: 'Unlimited',990            checkOrigin: null,991          },992        },993        {994          Transact: {995            originKind: 'Superuser',996            requireWeightAtMost: {997              refTime: info.weightMultiplier * 200000000,998              proofSize: info.weightMultiplier * 3000,999            },1000            call: {1001              encoded: info.call,1002            },1003          },1004        },1005      ],1006    };1007  }1008}10091010class MoonbeamAccountGroup {1011  helper: MoonbeamHelper;10121013  keyring: Keyring;1014  _alithAccount: IKeyringPair;1015  _baltatharAccount: IKeyringPair;1016  _dorothyAccount: IKeyringPair;10171018  constructor(helper: MoonbeamHelper) {1019    this.helper = helper;10201021    this.keyring = new Keyring({type: 'ethereum'});1022    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';1023    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';1024    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';10251026    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');1027    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');1028    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');1029  }10301031  alithAccount() {1032    return this._alithAccount;1033  }10341035  baltatharAccount() {1036    return this._baltatharAccount;1037  }10381039  dorothyAccount() {1040    return this._dorothyAccount;1041  }10421043  create() {1044    return this.keyring.addFromUri(mnemonicGenerate());1045  }1046}10471048class MoonbeamFastDemocracyGroup {1049  helper: DevMoonbeamHelper;10501051  constructor(helper: DevMoonbeamHelper) {1052    this.helper = helper;1053  }10541055  async executeProposal(proposalDesciption: string, encodedProposal: string) {1056    const proposalHash = blake2AsHex(encodedProposal);10571058    const alithAccount = this.helper.account.alithAccount();1059    const baltatharAccount = this.helper.account.baltatharAccount();1060    const dorothyAccount = this.helper.account.dorothyAccount();10611062    const councilVotingThreshold = 2;1063    const technicalCommitteeThreshold = 2;1064    const fastTrackVotingPeriod = 3;1065    const fastTrackDelayPeriod = 0;10661067    console.log(`[democracy] executing '${proposalDesciption}' proposal`);10681069    // >>> Propose external motion through council >>>1070    console.log('\t* Propose external motion through council.......');1071    const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1072    const encodedMotion = externalMotion?.method.toHex() || '';1073    const motionHash = blake2AsHex(encodedMotion);1074    console.log('\t* Motion hash is %s', motionHash);10751076    await this.helper.collective.council.propose(1077      baltatharAccount,1078      councilVotingThreshold,1079      externalMotion,1080      externalMotion.encodedLength,1081    );10821083    const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1084    await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1085    await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);10861087    await this.helper.collective.council.close(1088      dorothyAccount,1089      motionHash,1090      councilProposalIdx,1091      {1092        refTime: 1_000_000_000,1093        proofSize: 1_000_000,1094      },1095      externalMotion.encodedLength,1096    );1097    console.log('\t* Propose external motion through council.......DONE');1098    // <<< Propose external motion through council <<<10991100    // >>> Fast track proposal through technical committee >>>1101    console.log('\t* Fast track proposal through technical committee.......');1102    const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1103    const encodedFastTrack = fastTrack?.method.toHex() || '';1104    const fastTrackHash = blake2AsHex(encodedFastTrack);1105    console.log('\t* FastTrack hash is %s', fastTrackHash);11061107    await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);11081109    const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1110    await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1111    await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);11121113    await this.helper.collective.techCommittee.close(1114      baltatharAccount,1115      fastTrackHash,1116      techProposalIdx,1117      {1118        refTime: 1_000_000_000,1119        proofSize: 1_000_000,1120      },1121      fastTrack.encodedLength,1122    );1123    console.log('\t* Fast track proposal through technical committee.......DONE');1124    // <<< Fast track proposal through technical committee <<<11251126    const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1127    const referendumIndex = democracyStarted.referendumIndex;11281129    // >>> Referendum voting >>>1130    console.log(`\t* Referendum #${referendumIndex} voting.......`);1131    await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1132      balance: 10_000_000_000_000_000_000n,1133      vote: {aye: true, conviction: 1},1134    });1135    console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1136    // <<< Referendum voting <<<11371138    // Wait the proposal to pass1139    await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11401141    await this.helper.wait.newBlocks(1);11421143    console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1144  }1145}11461147class WaitGroup {1148  helper: ChainHelperBase;11491150  constructor(helper: ChainHelperBase) {1151    this.helper = helper;1152  }11531154  sleep(milliseconds: number) {1155    return new Promise((resolve) => setTimeout(resolve, milliseconds));1156  }11571158  private async waitWithTimeout(promise: Promise<any>, timeout: number) {1159    let isBlock = false;1160    promise.then(() => isBlock = true).catch(() => isBlock = true);1161    let totalTime = 0;1162    const step = 100;1163    while(!isBlock) {1164      await this.sleep(step);1165      totalTime += step;1166      if(totalTime >= timeout) throw Error('Blocks production failed');1167    }1168    return promise;1169  }11701171  /**1172   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1173   * @param promise async operation to race against the timeout1174   * @param timeoutMS time after which to time out1175   * @param timeoutError error message to throw1176   * @returns promise of the same type the operation had1177   */1178  withTimeout<T>(1179    promise: Promise<T>,1180    timeoutMS = 30000,1181    timeoutError = 'The operation has timed out!',1182  ): Promise<T> {1183    const timeout = new Promise<never>((_, reject) => {1184      setTimeout(() => {1185        reject(new Error(timeoutError));1186      }, timeoutMS);1187    });11881189    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1190  }11911192  /**1193   * Wait for specified number of blocks1194   * @param blocksCount number of blocks to wait1195   * @returns1196   */1197  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1198    timeout = timeout ?? blocksCount * 60_000;1199    // eslint-disable-next-line no-async-promise-executor1200    const promise = new Promise<void>(async (resolve) => {1201      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1202        if(blocksCount > 0) {1203          blocksCount--;1204        } else {1205          unsubscribe();1206          resolve();1207        }1208      });1209    });1210    await this.waitWithTimeout(promise, timeout);1211    return promise;1212  }12131214  /**1215   * Wait for the specified number of sessions to pass.1216   * Only applicable if the Session pallet is turned on.1217   * @param sessionCount number of sessions to wait1218   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1219   * @returns1220   */1221  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1222    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`1223      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');12241225    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1226    let currentSessionIndex = -1;12271228    while(currentSessionIndex < expectedSessionIndex) {1229      // eslint-disable-next-line no-async-promise-executor1230      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1231        await this.newBlocks(1);1232        const res = await (this.helper as DevUniqueHelper).session.getIndex();1233        resolve(res);1234      }), blockTimeout, 'The chain has stopped producing blocks!');1235    }1236  }12371238  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1239    timeout = timeout ?? 30 * 60 * 1000;1240    // eslint-disable-next-line no-async-promise-executor1241    const promise = new Promise<void>(async (resolve) => {1242      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1243        if(data.number.toNumber() >= blockNumber) {1244          unsubscribe();1245          resolve();1246        }1247      });1248    });1249    await this.waitWithTimeout(promise, timeout);1250    return promise;1251  }12521253  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1254    timeout = timeout ?? 30 * 60 * 1000;1255    // eslint-disable-next-line no-async-promise-executor1256    const promise = new Promise<void>(async (resolve) => {1257      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1258        if(data.value.relayParentNumber.toNumber() >= blockNumber) {1259          // @ts-ignore1260          unsubscribe();1261          resolve();1262        }1263      });1264    });1265    await this.waitWithTimeout(promise, timeout);1266    return promise;1267  }12681269  noScheduledTasks() {1270    const api = this.helper.getApi();12711272    // eslint-disable-next-line no-async-promise-executor1273    const promise = new Promise<void>(async resolve => {1274      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1275        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();12761277        if(areThereScheduledTasks.length == 0) {1278          unsubscribe();1279          resolve();1280        }1281      });1282    });12831284    return promise;1285  }12861287  parachainBlockMultiplesOf(val: bigint) {1288    // eslint-disable-next-line no-async-promise-executor1289    const promise = new Promise<void>(async resolve => {1290      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1291        if(data.number.toBigInt() % val == 0n) {1292          console.log(`from waiter: ${data.number.toBigInt()}`);1293          unsubscribe();1294          resolve();1295        }1296      });1297    });1298    return promise;1299  }13001301  event<T extends IEventHelper>(1302    maxBlocksToWait: number,1303    eventHelper: T,1304    filter: (_: any) => boolean = () => true,1305  ): any {1306    // eslint-disable-next-line no-async-promise-executor1307    const promise = new Promise<T | null>(async (resolve) => {1308      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1309        const blockNumber = header.number.toHuman();1310        const blockHash = header.hash;1311        const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1312        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;13131314        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);13151316        const apiAt = await this.helper.getApi().at(blockHash);1317        const eventRecords = (await apiAt.query.system.events()) as any;13181319        const neededEvent = eventRecords.toArray()1320          .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1321          .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1322          .find(filter);13231324        if(neededEvent) {1325          unsubscribe();1326          resolve(neededEvent);1327        } else if(maxBlocksToWait > 0) {1328          maxBlocksToWait--;1329        } else {1330          this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1331          The wait lasted until block ${blockNumber} inclusive`);1332          unsubscribe();1333          resolve(null);1334        }1335      });1336    });1337    return promise;1338  }13391340  async expectEvent<T extends IEventHelper>(1341    maxBlocksToWait: number,1342    eventHelper: T,1343    filter: (e: any) => boolean = () => true,1344  ) {1345    const e = await this.event(maxBlocksToWait, eventHelper, filter);1346    if(e == null) {1347      throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1348    } else {1349      return e;1350    }1351  }1352}13531354class SessionGroup {1355  helper: ChainHelperBase;13561357  constructor(helper: ChainHelperBase) {1358    this.helper = helper;1359  }13601361  //todo:collator documentation1362  async getIndex(): Promise<number> {1363    return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1364  }13651366  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1367    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1368  }13691370  setOwnKeys(signer: TSigner, key: string) {1371    return this.helper.executeExtrinsic(1372      signer,1373      'api.tx.session.setKeys',1374      [key, '0x0'],1375      true,1376    );1377  }13781379  setOwnKeysFromAddress(signer: TSigner) {1380    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1381  }1382}13831384class TestUtilGroup {1385  helper: DevUniqueHelper;13861387  constructor(helper: DevUniqueHelper) {1388    this.helper = helper;1389  }13901391  async enable() {1392    if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1393      return;1394    }13951396    const signer = this.helper.util.fromSeed('//Alice');1397    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1398  }13991400  async setTestValue(signer: TSigner, testVal: number) {1401    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1402  }14031404  async incTestValue(signer: TSigner) {1405    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1406  }14071408  async setTestValueAndRollback(signer: TSigner, testVal: number) {1409    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1410  }14111412  async testValue(blockIdx?: number) {1413    const api = blockIdx1414      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1415      : this.helper.getApi();14161417    return (await api.query.testUtils.testValue()).toJSON();1418  }14191420  async justTakeFee(signer: TSigner) {1421    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1422  }14231424  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1425    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1426  }1427}14281429class EventCapture {1430  helper: DevUniqueHelper;1431  eventSection: string;1432  eventMethod: string;1433  events: EventRecord[] = [];1434  unsubscribe: VoidFn | null = null;14351436  constructor(1437    helper: DevUniqueHelper,1438    eventSection: string,1439    eventMethod: string,1440  ) {1441    this.helper = helper;1442    this.eventSection = eventSection;1443    this.eventMethod = eventMethod;1444  }14451446  async startCapture() {1447    this.stopCapture();1448    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1449      const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);14501451      this.events.push(...newEvents);1452    })) as any;1453  }14541455  stopCapture() {1456    if(this.unsubscribe !== null) {1457      this.unsubscribe();1458    }1459  }14601461  extractCapturedEvents() {1462    return this.events;1463  }1464}14651466class AdminGroup {1467  helper: UniqueHelper;14681469  constructor(helper: UniqueHelper) {1470    this.helper = helper;1471  }14721473  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {1474    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1475    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1476      staker: e.event.data[0].toString(),1477      stake: e.event.data[1].toBigInt(),1478      payout: e.event.data[2].toBigInt(),1479    }));1480  }1481}14821483// eslint-disable-next-line @typescript-eslint/naming-convention1484function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1485  return class extends Base {1486    scheduleFn: 'schedule' | 'scheduleAfter';1487    blocksNum: number;1488    options: ISchedulerOptions;14891490    constructor(...args: any[]) {1491      const logger = args[0] as ILogger;1492      const options = args[1] as {1493        scheduleFn: 'schedule' | 'scheduleAfter',1494        blocksNum: number,1495        options: ISchedulerOptions1496      };14971498      super(logger);14991500      this.scheduleFn = options.scheduleFn;1501      this.blocksNum = options.blocksNum;1502      this.options = options.options;1503    }15041505    executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1506      const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);15071508      const mandatorySchedArgs = [1509        this.blocksNum,1510        this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1511        this.options.priority ?? null,1512        scheduledTx,1513      ];15141515      let schedArgs;1516      let scheduleFn;15171518      if(this.options.scheduledId) {1519        schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];15201521        if(this.scheduleFn == 'schedule') {1522          scheduleFn = 'scheduleNamed';1523        } else if(this.scheduleFn == 'scheduleAfter') {1524          scheduleFn = 'scheduleNamedAfter';1525        }1526      } else {1527        schedArgs = mandatorySchedArgs;1528        scheduleFn = this.scheduleFn;1529      }15301531      const extrinsic = 'api.tx.scheduler.' + scheduleFn;15321533      return super.executeExtrinsic(1534        sender,1535        extrinsic as any,1536        schedArgs,1537        expectSuccess,1538      );1539    }1540  };1541}
modifiedtests/src/util/playgrounds/unique.xcm.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.xcm.ts
+++ b/tests/src/util/playgrounds/unique.xcm.ts
@@ -240,19 +240,19 @@
 }
 
 export class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {
-  async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) {
+  async create(signer: TSigner, assetId: number | bigint, admin: string, minimalBalance: bigint) {
     await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
   }
 
-  async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) {
+  async setMetadata(signer: TSigner, assetId: number | bigint, name: string, symbol: string, decimals: number) {
     await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true);
   }
 
-  async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) {
+  async mint(signer: TSigner, assetId: number | bigint, beneficiary: string, amount: bigint) {
     await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
   }
 
-  async account(assetId: string | number, address: string) {
+  async account(assetId: string | number | bigint, address: string) {
     const accountAsset = (
       await this.helper.callRpc('api.query.assets.account', [assetId, address])
     ).toJSON()! as any;
addedtests/src/xcm/lowLevelXcmQuartz.test.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/xcm/lowLevelXcmQuartz.test.ts
@@ -0,0 +1,364 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {IKeyringPair} from '@polkadot/types/types';
+import {itSub, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds,  usingMoonriverPlaygrounds, usingShidenPlaygrounds, usingRelayPlaygrounds} from '../util';
+import {QUARTZ_CHAIN,  QTZ_DECIMALS,  SHIDEN_DECIMALS, karuraUrl, moonriverUrl,  shidenUrl,  SAFE_XCM_VERSION, XcmTestHelper, TRANSFER_AMOUNT, SENDER_BUDGET, relayUrl} from './xcm.types';
+import {hexToString} from '@polkadot/util';
+
+const testHelper = new XcmTestHelper('quartz');
+
+describeXCM('[XCMLL] Integration test: Exchanging tokens with Karura', () => {
+  let alice: IKeyringPair;
+  let randomAccount: IKeyringPair;
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+      [randomAccount] = await helper.arrange.createAccounts([0n], alice);
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+
+    await usingKaruraPlaygrounds(karuraUrl, async (helper) => {
+      const destination = {
+        V2: {
+          parents: 1,
+          interior: {
+            X1: {
+              Parachain: QUARTZ_CHAIN,
+            },
+          },
+        },
+      };
+
+      const metadata = {
+        name: 'Quartz',
+        symbol: 'QTZ',
+        decimals: 18,
+        minimalBalance: 1000000000000000000n,
+      };
+
+      const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) =>
+        hexToString(v.toJSON()['symbol'])) as string[];
+
+      if(!assets.includes('QTZ')) {
+        await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);
+      } else {
+        console.log('QTZ token already registered on Karura assetRegistry pallet');
+      }
+      await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);
+    });
+
+    await usingPlaygrounds(async (helper) => {
+      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
+    });
+  });
+
+  itSub('Should connect and send QTZ to Karura', async () => {
+    await testHelper.sendUnqTo('karura', randomAccount);
+  });
+
+  itSub('Should connect to Karura and send QTZ back', async () => {
+    await testHelper.sendUnqBack('karura', alice, randomAccount);
+  });
+
+  itSub('Karura can send only up to its balance', async () => {
+    await testHelper.sendOnlyOwnedBalance('karura', alice);
+  });
+});
+// These tests are relevant only when
+// the the corresponding foreign assets are not registered
+describeXCM('[XCMLL] Integration test: Quartz rejects non-native tokens', () => {
+  let alice: IKeyringPair;
+
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+
+
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+  });
+
+  itSub('Quartz rejects KAR tokens from Karura', async () => {
+    await testHelper.rejectNativeTokensFrom('karura', alice);
+  });
+
+  itSub('Quartz rejects MOVR tokens from Moonriver', async () => {
+    await testHelper.rejectNativeTokensFrom('moonriver', alice);
+  });
+
+  itSub('Quartz rejects SDN tokens from Shiden', async () => {
+    await testHelper.rejectNativeTokensFrom('shiden', alice);
+  });
+});
+
+describeXCM('[XCMLL] Integration test: Exchanging QTZ with Moonriver', () => {
+  // Quartz constants
+  let alice: IKeyringPair;
+  let quartzAssetLocation;
+
+  let randomAccountQuartz: IKeyringPair;
+  let randomAccountMoonriver: IKeyringPair;
+
+  // Moonriver constants
+  let assetId: string;
+
+  const quartzAssetMetadata = {
+    name: 'xcQuartz',
+    symbol: 'xcQTZ',
+    decimals: 18,
+    isFrozen: false,
+    minimalBalance: 1n,
+  };
+
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+      [randomAccountQuartz] = await helper.arrange.createAccounts([0n], alice);
+
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+
+    await usingMoonriverPlaygrounds(moonriverUrl, async (helper) => {
+      const alithAccount = helper.account.alithAccount();
+      const baltatharAccount = helper.account.baltatharAccount();
+      const dorothyAccount = helper.account.dorothyAccount();
+
+      randomAccountMoonriver = helper.account.create();
+
+      // >>> Sponsoring Dorothy >>>
+      console.log('Sponsoring Dorothy.......');
+      await helper.balance.transferToEthereum(alithAccount, dorothyAccount.address, 11_000_000_000_000_000_000n);
+      console.log('Sponsoring Dorothy.......DONE');
+      // <<< Sponsoring Dorothy <<<
+
+      quartzAssetLocation = {
+        XCM: {
+          parents: 1,
+          interior: {X1: {Parachain: QUARTZ_CHAIN}},
+        },
+      };
+      const existentialDeposit = 1n;
+      const isSufficient = true;
+      const unitsPerSecond = 1n;
+      const numAssetsWeightHint = 0;
+      if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) {
+        console.log('Quartz asset already registered on Moonriver');
+      } else {
+        const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({
+          location: quartzAssetLocation,
+          metadata: quartzAssetMetadata,
+          existentialDeposit,
+          isSufficient,
+          unitsPerSecond,
+          numAssetsWeightHint,
+        });
+
+        console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);
+
+        await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);
+      }
+      // >>> Acquire Quartz AssetId Info on Moonriver >>>
+      console.log('Acquire Quartz AssetId Info on Moonriver.......');
+
+      assetId = (await helper.assetManager.assetTypeId(quartzAssetLocation)).toString();
+
+      console.log('QTZ asset ID is %s', assetId);
+      console.log('Acquire Quartz AssetId Info on Moonriver.......DONE');
+      // >>> Acquire Quartz AssetId Info on Moonriver >>>
+
+      // >>> Sponsoring random Account >>>
+      console.log('Sponsoring random Account.......');
+      await helper.balance.transferToEthereum(baltatharAccount, randomAccountMoonriver.address, 11_000_000_000_000_000_000n);
+      console.log('Sponsoring random Account.......DONE');
+      // <<< Sponsoring random Account <<<
+    });
+
+    await usingPlaygrounds(async (helper) => {
+      await helper.balance.transferToSubstrate(alice, randomAccountQuartz.address, 10n * TRANSFER_AMOUNT);
+    });
+  });
+
+  itSub('Should connect and send QTZ to Moonriver', async () => {
+    await testHelper.sendUnqTo('moonriver', randomAccountQuartz, randomAccountMoonriver);
+  });
+
+  itSub('Should connect to Moonriver and send QTZ back', async () => {
+    await testHelper.sendUnqBack('moonriver', alice, randomAccountQuartz);
+  });
+
+  itSub('Moonriver can send only up to its balance', async () => {
+    await testHelper.sendOnlyOwnedBalance('moonriver', alice);
+  });
+
+  itSub('Should not accept reserve transfer of QTZ from Moonriver', async () => {
+    await testHelper.rejectReserveTransferUNQfrom('moonriver', alice);
+  });
+});
+
+describeXCM('[XCMLL] Integration test: Exchanging tokens with Shiden', () => {
+  let alice: IKeyringPair;
+  let randomAccount: IKeyringPair;
+
+  const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden
+  const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden
+
+  // Quartz -> Shiden
+  const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden
+  const unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+      randomAccount = helper.arrange.createEmptyAccount();
+      await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
+      console.log('sender: ', randomAccount.address);
+
+      // Set the default version to wrap the first message to other chains.
+      await helper.getSudo().xcm.setSafeXcmVersion(alice, SAFE_XCM_VERSION);
+    });
+
+    await usingShidenPlaygrounds(shidenUrl, async (helper) => {
+      if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) {
+        console.log('1. Create foreign asset and metadata');
+        await helper.assets.create(
+          alice,
+          QTZ_ASSET_ID_ON_SHIDEN,
+          alice.address,
+          QTZ_MINIMAL_BALANCE_ON_SHIDEN,
+        );
+
+        await helper.assets.setMetadata(
+          alice,
+          QTZ_ASSET_ID_ON_SHIDEN,
+          'Quartz',
+          'QTZ',
+          Number(QTZ_DECIMALS),
+        );
+
+        console.log('2. Register asset location on Shiden');
+        const assetLocation = {
+          V2: {
+            parents: 1,
+            interior: {
+              X1: {
+                Parachain: QUARTZ_CHAIN,
+              },
+            },
+          },
+        };
+
+        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);
+
+        console.log('3. Set QTZ payment for XCM execution on Shiden');
+        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
+      } else {
+        console.log('QTZ is already registered on Shiden');
+      }
+      console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');
+      await helper.balance.transferToSubstrate(alice, randomAccount.address, shidenInitialBalance);
+    });
+  });
+
+  itSub('Should connect and send QTZ to Shiden', async () => {
+    await testHelper.sendUnqTo('shiden', randomAccount);
+  });
+
+  itSub('Should connect to Shiden and send QTZ back', async () => {
+    await testHelper.sendUnqBack('shiden', alice, randomAccount);
+  });
+
+  itSub('Shiden can send only up to its balance', async () => {
+    await testHelper.sendOnlyOwnedBalance('shiden', alice);
+  });
+
+  itSub('Should not accept reserve transfer of QTZ from Shiden', async () => {
+    await testHelper.rejectReserveTransferUNQfrom('shiden', alice);
+  });
+});
+
+describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => {
+  let sudoer: IKeyringPair;
+
+  before(async function () {
+    await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => {
+      sudoer = await privateKey('//Alice');
+    });
+  });
+
+  // At the moment there is no reliable way
+  // to establish the correspondence between the `ExecutedDownward` event
+  // and the relay's sent message due to `SetTopic` instruction
+  // containing an unpredictable topic silently added by the relay's messages on the router level.
+  // This changes the message hash on arrival to our chain.
+  //
+  // See:
+  // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83
+  // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36
+  //
+  // Because of this, we insert time gaps between tests so
+  // different `ExecutedDownward` events won't interfere with each other.
+  afterEach(async () => {
+    await usingPlaygrounds(async (helper) => {
+      await helper.wait.newBlocks(3);
+    });
+  });
+
+  itSub('The relay can set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain');
+  });
+
+  itSub('The relay can batch set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch');
+  });
+
+  itSub('The relay can batchAll set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll');
+  });
+
+  itSub('The relay can forceBatch set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch');
+  });
+
+  itSub('[negative] The relay cannot set balance', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain');
+  });
+
+  itSub('[negative] The relay cannot set balance via batch', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch');
+  });
+
+  itSub('[negative] The relay cannot set balance via batchAll', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll');
+  });
+
+  itSub('[negative] The relay cannot set balance via forceBatch', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch');
+  });
+
+  itSub('[negative] The relay cannot set balance via dispatchAs', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs');
+  });
+});
modifiedtests/src/xcm/lowLevelXcmUnique.test.tsdiffbeforeafterboth
--- a/tests/src/xcm/lowLevelXcmUnique.test.ts
+++ b/tests/src/xcm/lowLevelXcmUnique.test.ts
@@ -16,317 +16,14 @@
 
 import {IKeyringPair} from '@polkadot/types/types';
 import config from '../config';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util';
-import {Event} from '../util/playgrounds/unique.dev';
+import {itSub, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingMoonbeamPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds} from '../util';
 import {nToBigInt} from '@polkadot/util';
 import {hexToString} from '@polkadot/util';
-import {ASTAR_DECIMALS, NETWORKS, SAFE_XCM_VERSION, UNIQUE_CHAIN, UNQ_DECIMALS, acalaUrl, astarUrl, expectFailedToTransact, expectUntrustedReserveLocationFail, getDevPlayground, mapToChainId, mapToChainUrl, maxWaitBlocks, moonbeamUrl, polkadexUrl, uniqueAssetId, uniqueVersionedMultilocation} from './xcm.types';
-
-
-const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
-const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;
-const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;
-const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;
-const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;
-
-let balanceUniqueTokenInit: bigint;
-let balanceUniqueTokenMiddle: bigint;
-let balanceUniqueTokenFinal: bigint;
-let unqFees: bigint;
-
-
-async function genericSendUnqTo(
-  networkName: keyof typeof NETWORKS,
-  randomAccount: IKeyringPair,
-  randomAccountOnTargetChain = randomAccount,
-) {
-  const networkUrl = mapToChainUrl(networkName);
-  const targetPlayground = getDevPlayground(networkName);
-  await usingPlaygrounds(async (helper) => {
-    balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
-    const destination = {
-      V2: {
-        parents: 1,
-        interior: {
-          X1: {
-            Parachain: mapToChainId(networkName),
-          },
-        },
-      },
-    };
-
-    const beneficiary = {
-      V2: {
-        parents: 0,
-        interior: {
-          X1: (
-            networkName == 'moonbeam' ?
-              {
-                AccountKey20: {
-                  network: 'Any',
-                  key: randomAccountOnTargetChain.address,
-                },
-              }
-              :
-              {
-                AccountId32: {
-                  network: 'Any',
-                  id: randomAccountOnTargetChain.addressRaw,
-                },
-              }
-          ),
-        },
-      },
-    };
-
-    const assets = {
-      V2: [
-        {
-          id: {
-            Concrete: {
-              parents: 0,
-              interior: 'Here',
-            },
-          },
-          fun: {
-            Fungible: TRANSFER_AMOUNT,
-          },
-        },
-      ],
-    };
-    const feeAssetItem = 0;
-
-    await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
-    const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-    balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);
-
-    unqFees = balanceUniqueTokenInit - balanceUniqueTokenMiddle - TRANSFER_AMOUNT;
-    console.log('[Unique -> %s] transaction fees on Unique: %s UNQ', networkName, helper.util.bigIntToDecimals(unqFees));
-    expect(unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true;
-
-    await targetPlayground(networkUrl, async (helper) => {
-      /*
-        Since only the parachain part of the Polkadex
-        infrastructure is launched (without their
-        solochain validators), processing incoming
-        assets will lead to an error.
-        This error indicates that the Polkadex chain
-        received a message from the Unique network,
-        since the hash is being checked to ensure
-        it matches what was sent.
-      */
-      if(networkName == 'polkadex') {
-        await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
-      } else {
-        await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);
-      }
-    });
-
-  });
-}
-
-async function genericSendUnqBack(
-  networkName: keyof typeof NETWORKS,
-  sudoer: IKeyringPair,
-  randomAccountOnUnq: IKeyringPair,
-) {
-  const networkUrl = mapToChainUrl(networkName);
-
-  const targetPlayground = getDevPlayground(networkName);
-  await usingPlaygrounds(async (helper) => {
-
-    const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
-      randomAccountOnUnq.addressRaw,
-      uniqueAssetId,
-      SENDBACK_AMOUNT,
-    );
-
-    let xcmProgramSent: any;
-
-
-    await targetPlayground(networkUrl, async (helper) => {
-      if('getSudo' in helper) {
-        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, xcmProgram);
-        xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      } else if('fastDemocracy' in helper) {
-        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, xcmProgram]);
-        // Needed to bypass the call filter.
-        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
-        await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
-        xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      }
-    });
-
-    await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);
-
-    balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);
-
-    expect(balanceUniqueTokenFinal).to.be.equal(balanceUniqueTokenInit - unqFees - STAYED_ON_TARGET_CHAIN);
-
-  });
-}
-
-async function genericSendOnlyOwnedBalance(
-  networkName: keyof typeof NETWORKS,
-  sudoer: IKeyringPair,
-) {
-  const networkUrl = mapToChainUrl(networkName);
-  const targetPlayground = getDevPlayground(networkName);
-
-  const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);
-
-  await usingPlaygrounds(async (helper) => {
-    const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));
-    await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);
-    const moreThanTargetChainHas = 2n * targetChainBalance;
-
-    const targetAccount = helper.arrange.createEmptyAccount();
-
-    const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
-      targetAccount.addressRaw,
-      {
-        Concrete: {
-          parents: 0,
-          interior: 'Here',
-        },
-      },
-      moreThanTargetChainHas,
-    );
-
-    let maliciousXcmProgramSent: any;
-
-
-    await targetPlayground(networkUrl, async (helper) => {
-      if('getSudo' in helper) {
-        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgram);
-        maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      } else if('fastDemocracy' in helper) {
-        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgram]);
-        // Needed to bypass the call filter.
-        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
-        await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
-        maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      }
-    });
-
-    await expectFailedToTransact(helper, maliciousXcmProgramSent);
-
-    const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
-    expect(targetAccountBalance).to.be.equal(0n);
-  });
-}
-
-async function genericReserveTransferUNQfrom(netwokrName: keyof typeof NETWORKS, sudoer: IKeyringPair) {
-  const networkUrl = mapToChainUrl(netwokrName);
-  const targetPlayground = getDevPlayground(netwokrName);
-
-  await usingPlaygrounds(async (helper) => {
-    const testAmount = 10_000n * (10n ** UNQ_DECIMALS);
-    const targetAccount = helper.arrange.createEmptyAccount();
-
-    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
-      targetAccount.addressRaw,
-      uniqueAssetId,
-      testAmount,
-    );
-
-    const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(
-      targetAccount.addressRaw,
-      {
-        Concrete: {
-          parents: 0,
-          interior: 'Here',
-        },
-      },
-      testAmount,
-    );
-
-    let maliciousXcmProgramFullIdSent: any;
-    let maliciousXcmProgramHereIdSent: any;
-    const maxWaitBlocks = 3;
-
-    // Try to trick Unique using full UNQ identification
-    await targetPlayground(networkUrl, async (helper) => {
-      if('getSudo' in helper) {
-        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
-        maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      }
-      // Moonbeam case
-      else if('fastDemocracy' in helper) {
-        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
-        // Needed to bypass the call filter.
-        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
-        await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using path asset identification`,batchCall);
-
-        maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      }
-    });
-
-
-    await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent);
-
-    let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
-    expect(accountBalance).to.be.equal(0n);
-
-    // Try to trick Unique using shortened UNQ identification
-    await targetPlayground(networkUrl, async (helper) => {
-      if('getSudo' in helper) {
-        await helper.getSudo().xcm.send(sudoer, uniqueVersionedMultilocation, maliciousXcmProgramHereId);
-        maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      }
-      else if('fastDemocracy' in helper) {
-        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramHereId]);
-        // Needed to bypass the call filter.
-        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
-        await helper.fastDemocracy.executeProposal(`${netwokrName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);
-
-        maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      }
-    });
-
-    await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);
+import {ASTAR_DECIMALS, SAFE_XCM_VERSION, SENDER_BUDGET, UNIQUE_CHAIN, UNQ_DECIMALS, XcmTestHelper, acalaUrl, astarUrl,  moonbeamUrl, polkadexUrl, relayUrl, uniqueAssetId} from './xcm.types';
 
-    accountBalance = await helper.balance.getSubstrate(targetAccount.address);
-    expect(accountBalance).to.be.equal(0n);
-  });
-}
+const testHelper = new XcmTestHelper('unique');
 
-async function genericRejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {
-  const networkUrl = mapToChainUrl(networkName);
-  const targetPlayground = getDevPlayground(networkName);
-  let messageSent: any;
 
-  await usingPlaygrounds(async (helper) => {
-    const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
-      helper.arrange.createEmptyAccount().addressRaw,
-      {
-        Concrete: {
-          parents: 1,
-          interior: {
-            X1: {
-              Parachain: mapToChainId(networkName),
-            },
-          },
-        },
-      },
-      TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,
-    );
-    await targetPlayground(networkUrl, async (helper) => {
-      if('getSudo' in helper) {
-        await helper.getSudo().xcm.send(sudoerOnTargetChain, uniqueVersionedMultilocation, maliciousXcmProgramFullId);
-        messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      } else if('fastDemocracy' in helper) {
-        const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [uniqueVersionedMultilocation, maliciousXcmProgramFullId]);
-        // Needed to bypass the call filter.
-        const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
-        await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall);
-
-        messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
-      }
-    });
-    await expectFailedToTransact(helper, messageSent);
-  });
-}
 
 
 describeXCM('[XCMLL] Integration test: Exchanging tokens with Acala', () => {
@@ -374,24 +71,23 @@
 
     await usingPlaygrounds(async (helper) => {
       await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
-      balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
     });
   });
 
   itSub('Should connect and send UNQ to Acala', async () => {
-    await genericSendUnqTo('acala', randomAccount);
+    await testHelper.sendUnqTo('acala', randomAccount);
   });
 
   itSub('Should connect to Acala and send UNQ back', async () => {
-    await genericSendUnqBack('acala', alice, randomAccount);
+    await testHelper.sendUnqBack('acala', alice, randomAccount);
   });
 
   itSub('Acala can send only up to its balance', async () => {
-    await genericSendOnlyOwnedBalance('acala', alice);
+    await testHelper.sendOnlyOwnedBalance('acala', alice);
   });
 
   itSub('Should not accept reserve transfer of UNQ from Acala', async () => {
-    await genericReserveTransferUNQfrom('acala', alice);
+    await testHelper.rejectReserveTransferUNQfrom('acala', alice);
   });
 });
 
@@ -429,25 +125,24 @@
 
     await usingPlaygrounds(async (helper) => {
       await helper.balance.transferToSubstrate(alice, randomAccount.address, SENDER_BUDGET);
-      balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
     });
   });
 
   itSub('Should connect and send UNQ to Polkadex', async () => {
-    await genericSendUnqTo('polkadex', randomAccount);
+    await testHelper.sendUnqTo('polkadex', randomAccount);
   });
 
 
   itSub('Should connect to Polkadex and send UNQ back', async () => {
-    await genericSendUnqBack('polkadex', alice, randomAccount);
+    await testHelper.sendUnqBack('polkadex', alice, randomAccount);
   });
 
   itSub('Polkadex can send only up to its balance', async () => {
-    await genericSendOnlyOwnedBalance('polkadex', alice);
+    await testHelper.sendOnlyOwnedBalance('polkadex', alice);
   });
 
   itSub('Should not accept reserve transfer of UNQ from Polkadex', async () => {
-    await genericReserveTransferUNQfrom('polkadex', alice);
+    await testHelper.rejectReserveTransferUNQfrom('polkadex', alice);
   });
 });
 
@@ -466,19 +161,19 @@
   });
 
   itSub('Unique rejects ACA tokens from Acala', async () => {
-    await genericRejectNativeTokensFrom('acala', alice);
+    await testHelper.rejectNativeTokensFrom('acala', alice);
   });
 
   itSub('Unique rejects GLMR tokens from Moonbeam', async () => {
-    await genericRejectNativeTokensFrom('moonbeam', alice);
+    await testHelper.rejectNativeTokensFrom('moonbeam', alice);
   });
 
   itSub('Unique rejects ASTR tokens from Astar', async () => {
-    await genericRejectNativeTokensFrom('astar', alice);
+    await testHelper.rejectNativeTokensFrom('astar', alice);
   });
 
   itSub('Unique rejects PDX tokens from Polkadex', async () => {
-    await genericRejectNativeTokensFrom('polkadex', alice);
+    await testHelper.rejectNativeTokensFrom('polkadex', alice);
   });
 });
 
@@ -569,24 +264,23 @@
 
     await usingPlaygrounds(async (helper) => {
       await helper.balance.transferToSubstrate(alice, randomAccountUnique.address, SENDER_BUDGET);
-      balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccountUnique.address);
     });
   });
 
   itSub('Should connect and send UNQ to Moonbeam', async () => {
-    await genericSendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
+    await testHelper.sendUnqTo('moonbeam', randomAccountUnique, randomAccountMoonbeam);
   });
 
   itSub('Should connect to Moonbeam and send UNQ back', async () => {
-    await genericSendUnqBack('moonbeam', alice, randomAccountUnique);
+    await testHelper.sendUnqBack('moonbeam', alice, randomAccountUnique);
   });
 
   itSub('Moonbeam can send only up to its balance', async () => {
-    await genericSendOnlyOwnedBalance('moonbeam', alice);
+    await testHelper.sendOnlyOwnedBalance('moonbeam', alice);
   });
 
   itSub('Should not accept reserve transfer of UNQ from Moonbeam', async () => {
-    await genericReserveTransferUNQfrom('moonbeam', alice);
+    await testHelper.rejectReserveTransferUNQfrom('moonbeam', alice);
   });
 });
 
@@ -594,12 +288,12 @@
   let alice: IKeyringPair;
   let randomAccount: IKeyringPair;
 
-  const UNQ_ASSET_ID_ON_ASTAR = 1;
-  const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n;
+  const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar
+  const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar
 
   // Unique -> Astar
   const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.
-  const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+  const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar
 
   before(async () => {
     await usingPlaygrounds(async (helper, privateKey) => {
@@ -615,7 +309,6 @@
     await usingAstarPlaygrounds(astarUrl, async (helper) => {
       if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {
         console.log('1. Create foreign asset and metadata');
-        // TODO update metadata with values from production
         await helper.assets.create(
           alice,
           UNQ_ASSET_ID_ON_ASTAR,
@@ -626,8 +319,8 @@
         await helper.assets.setMetadata(
           alice,
           UNQ_ASSET_ID_ON_ASTAR,
-          'Cross chain UNQ',
-          'xcUNQ',
+          'Unique Network',
+          'UNQ',
           Number(UNQ_DECIMALS),
         );
 
@@ -656,18 +349,82 @@
   });
 
   itSub('Should connect and send UNQ to Astar', async () => {
-    await genericSendUnqTo('astar', randomAccount);
+    await testHelper.sendUnqTo('astar', randomAccount);
   });
 
   itSub('Should connect to Astar and send UNQ back', async () => {
-    await genericSendUnqBack('astar', alice, randomAccount);
+    await testHelper.sendUnqBack('astar', alice, randomAccount);
   });
 
   itSub('Astar can send only up to its balance', async () => {
-    await genericSendOnlyOwnedBalance('astar', alice);
+    await testHelper.sendOnlyOwnedBalance('astar', alice);
   });
 
   itSub('Should not accept reserve transfer of UNQ from Astar', async () => {
-    await genericReserveTransferUNQfrom('astar', alice);
+    await testHelper.rejectReserveTransferUNQfrom('astar', alice);
+  });
+});
+
+describeXCM('[XCMLL] Integration test: The relay can do some root ops', () => {
+  let sudoer: IKeyringPair;
+
+  before(async function () {
+    await usingRelayPlaygrounds(relayUrl, async (_, privateKey) => {
+      sudoer = await privateKey('//Alice');
+    });
+  });
+
+  // At the moment there is no reliable way
+  // to establish the correspondence between the `ExecutedDownward` event
+  // and the relay's sent message due to `SetTopic` instruction
+  // containing an unpredictable topic silently added by the relay's messages on the router level.
+  // This changes the message hash on arrival to our chain.
+  //
+  // See:
+  // * The relay's router: https://github.com/paritytech/polkadot-sdk/blob/f60318f68687e601c47de5ad5ca88e2c3f8139a7/polkadot/runtime/westend/src/xcm_config.rs#L83
+  // * The `WithUniqueTopic` helper: https://github.com/paritytech/polkadot-sdk/blob/945ebbbcf66646be13d5b1d1bc26c8b0d3296d9e/polkadot/xcm/xcm-builder/src/routing.rs#L36
+  //
+  // Because of this, we insert time gaps between tests so
+  // different `ExecutedDownward` events won't interfere with each other.
+  afterEach(async () => {
+    await usingPlaygrounds(async (helper) => {
+      await helper.wait.newBlocks(3);
+    });
+  });
+
+  itSub('The relay can set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'plain');
+  });
+
+  itSub('The relay can batch set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'batch');
+  });
+
+  itSub('The relay can batchAll set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'batchAll');
+  });
+
+  itSub('The relay can forceBatch set storage', async () => {
+    await testHelper.relayIsPermittedToSetStorage(sudoer, 'forceBatch');
+  });
+
+  itSub('[negative] The relay cannot set balance', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'plain');
+  });
+
+  itSub('[negative] The relay cannot set balance via batch', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batch');
+  });
+
+  itSub('[negative] The relay cannot set balance via batchAll', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'batchAll');
+  });
+
+  itSub('[negative] The relay cannot set balance via forceBatch', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'forceBatch');
+  });
+
+  itSub('[negative] The relay cannot set balance via dispatchAs', async () => {
+    await testHelper.relayIsNotPermittedToSetBalance(sudoer, 'dispatchAs');
   });
 });
modifiedtests/src/xcm/xcm.types.tsdiffbeforeafterboth
--- a/tests/src/xcm/xcm.types.ts
+++ b/tests/src/xcm/xcm.types.ts
@@ -1,4 +1,6 @@
-import {usingAcalaPlaygrounds, usingAstarPlaygrounds, usingMoonbeamPlaygrounds, usingPolkadexPlaygrounds} from '../util';
+import {IKeyringPair} from '@polkadot/types/types';
+import {hexToString} from '@polkadot/util';
+import {expect, usingAcalaPlaygrounds, usingAstarPlaygrounds, usingKaruraPlaygrounds, usingMoonbeamPlaygrounds, usingMoonriverPlaygrounds, usingPlaygrounds, usingPolkadexPlaygrounds, usingRelayPlaygrounds, usingShidenPlaygrounds} from '../util';
 import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
 import config from '../config';
 
@@ -9,19 +11,39 @@
 export const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006);
 export const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040);
 
+export const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);
+export const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);
+export const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);
+export const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);
+export const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);
+
+export const relayUrl = config.relayUrl;
+export const statemintUrl = config.statemintUrl;
+export const statemineUrl = config.statemineUrl;
+
 export const acalaUrl = config.acalaUrl;
 export const moonbeamUrl = config.moonbeamUrl;
 export const astarUrl = config.astarUrl;
 export const polkadexUrl = config.polkadexUrl;
 
+export const karuraUrl = config.karuraUrl;
+export const moonriverUrl = config.moonriverUrl;
+export const shidenUrl = config.shidenUrl;
+
 export const SAFE_XCM_VERSION = 3;
 
-export const maxWaitBlocks = 6;
 
+export const RELAY_DECIMALS = 12;
+export const STATEMINE_DECIMALS = 12;
+export const KARURA_DECIMALS = 12;
+export const SHIDEN_DECIMALS = 18n;
+export const QTZ_DECIMALS = 18n;
 
 export const ASTAR_DECIMALS = 18n;
 export const UNQ_DECIMALS = 18n;
 
+export const maxWaitBlocks = 6;
+
 export const uniqueMultilocation = {
   parents: 1,
   interior: {
@@ -47,14 +69,30 @@
          && event.outcome.isUntrustedReserveLocation);
 };
 
+export const expectDownwardXcmNoPermission = async (helper: DevUniqueHelper) => {
+  // The correct messageHash for downward messages can't be reliably obtained
+  await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.asIncomplete[1].isNoPermission);
+};
+
+export const expectDownwardXcmComplete = async (helper: DevUniqueHelper) => {
+  // The correct messageHash for downward messages can't be reliably obtained
+  await helper.wait.expectEvent(maxWaitBlocks, Event.DmpQueue.ExecutedDownward, event => event.outcome.isComplete);
+};
+
 export const NETWORKS = {
   acala: usingAcalaPlaygrounds,
   astar: usingAstarPlaygrounds,
   polkadex: usingPolkadexPlaygrounds,
   moonbeam: usingMoonbeamPlaygrounds,
+  moonriver: usingMoonriverPlaygrounds,
+  karura: usingKaruraPlaygrounds,
+  shiden: usingShidenPlaygrounds,
 } as const;
+type NetworkNames = keyof typeof NETWORKS;
+
+type NativeRuntime = 'opal' | 'quartz' | 'unique';
 
-export function mapToChainId(networkName: keyof typeof NETWORKS) {
+export function mapToChainId(networkName: keyof typeof NETWORKS): number {
   switch (networkName) {
     case 'acala':
       return ACALA_CHAIN;
@@ -64,10 +102,16 @@
       return MOONBEAM_CHAIN;
     case 'polkadex':
       return POLKADEX_CHAIN;
+    case 'moonriver':
+      return MOONRIVER_CHAIN;
+    case 'karura':
+      return KARURA_CHAIN;
+    case 'shiden':
+      return SHIDEN_CHAIN;
   }
 }
 
-export function mapToChainUrl(networkName: keyof typeof NETWORKS): string {
+export function mapToChainUrl(networkName: NetworkNames): string {
   switch (networkName) {
     case 'acala':
       return acalaUrl;
@@ -77,9 +121,501 @@
       return moonbeamUrl;
     case 'polkadex':
       return polkadexUrl;
+    case 'moonriver':
+      return moonriverUrl;
+    case 'karura':
+      return karuraUrl;
+    case 'shiden':
+      return shidenUrl;
   }
 }
 
-export function getDevPlayground<T extends keyof typeof NETWORKS>(name: T) {
+export function getDevPlayground(name: NetworkNames) {
   return NETWORKS[name];
-}
\ No newline at end of file
+}
+
+export const TRANSFER_AMOUNT = 2000000_000_000_000_000_000_000n;
+export const SENDER_BUDGET = 2n * TRANSFER_AMOUNT;
+export const SENDBACK_AMOUNT = TRANSFER_AMOUNT / 2n;
+export const STAYED_ON_TARGET_CHAIN = TRANSFER_AMOUNT - SENDBACK_AMOUNT;
+export const TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT = 100_000_000_000n;
+
+export class XcmTestHelper {
+  private _balanceUniqueTokenInit: bigint = 0n;
+  private _balanceUniqueTokenMiddle: bigint = 0n;
+  private _balanceUniqueTokenFinal: bigint = 0n;
+  private _unqFees: bigint = 0n;
+  private _nativeRuntime: NativeRuntime;
+
+  constructor(runtime: NativeRuntime) {
+    this._nativeRuntime = runtime;
+  }
+
+  private _getNativeId() {
+    switch (this._nativeRuntime) {
+      case 'opal':
+        // To-Do
+        return 1001;
+      case 'quartz':
+        return QUARTZ_CHAIN;
+      case 'unique':
+        return UNIQUE_CHAIN;
+    }
+  }
+
+  private _isAddress20FormatFor(network: NetworkNames) {
+    switch (network) {
+      case 'moonbeam':
+      case 'moonriver':
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  private _runtimeVersionedMultilocation() {
+    return {
+      V3: {
+        parents: 1,
+        interior: {
+          X1: {
+            Parachain: this._getNativeId(),
+          },
+        },
+      },
+    };
+  }
+
+  private _uniqueChainMultilocationForRelay() {
+    return {
+      V3: {
+        parents: 0,
+        interior: {
+          X1: {Parachain: this._getNativeId()},
+        },
+      },
+    };
+  }
+
+  async sendUnqTo(
+    networkName: keyof typeof NETWORKS,
+    randomAccount: IKeyringPair,
+    randomAccountOnTargetChain = randomAccount,
+  ) {
+    const networkUrl = mapToChainUrl(networkName);
+    const targetPlayground = getDevPlayground(networkName);
+    await usingPlaygrounds(async (helper) => {
+      this._balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
+      const destination = {
+        V2: {
+          parents: 1,
+          interior: {
+            X1: {
+              Parachain: mapToChainId(networkName),
+            },
+          },
+        },
+      };
+
+      const beneficiary = {
+        V2: {
+          parents: 0,
+          interior: {
+            X1: (
+              this._isAddress20FormatFor(networkName) ?
+                {
+                  AccountKey20: {
+                    network: 'Any',
+                    key: randomAccountOnTargetChain.address,
+                  },
+                }
+                :
+                {
+                  AccountId32: {
+                    network: 'Any',
+                    id: randomAccountOnTargetChain.addressRaw,
+                  },
+                }
+            ),
+          },
+        },
+      };
+
+      const assets = {
+        V2: [
+          {
+            id: {
+              Concrete: {
+                parents: 0,
+                interior: 'Here',
+              },
+            },
+            fun: {
+              Fungible: TRANSFER_AMOUNT,
+            },
+          },
+        ],
+      };
+      const feeAssetItem = 0;
+
+      await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
+      const messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+      this._balanceUniqueTokenMiddle = await helper.balance.getSubstrate(randomAccount.address);
+
+      this._unqFees = this._balanceUniqueTokenInit - this._balanceUniqueTokenMiddle - TRANSFER_AMOUNT;
+      console.log('[%s -> %s] transaction fees: %s', this._nativeRuntime, networkName, helper.util.bigIntToDecimals(this._unqFees));
+      expect(this._unqFees > 0n, 'Negative fees, looks like nothing was transferred').to.be.true;
+
+      await targetPlayground(networkUrl, async (helper) => {
+      /*
+        Since only the parachain part of the Polkadex
+        infrastructure is launched (without their
+        solochain validators), processing incoming
+        assets will lead to an error.
+        This error indicates that the Polkadex chain
+        received a message from the Unique network,
+        since the hash is being checked to ensure
+        it matches what was sent.
+      */
+        if(networkName == 'polkadex') {
+          await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
+        } else {
+          await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == messageSent.messageHash);
+        }
+      });
+
+    });
+  }
+
+  async sendUnqBack(
+    networkName: keyof typeof NETWORKS,
+    sudoer: IKeyringPair,
+    randomAccountOnUnq: IKeyringPair,
+  ) {
+    const networkUrl = mapToChainUrl(networkName);
+
+    const targetPlayground = getDevPlayground(networkName);
+    await usingPlaygrounds(async (helper) => {
+
+      const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+        randomAccountOnUnq.addressRaw,
+        {
+          Concrete: {
+            parents: 1,
+            interior: {
+              X1: {Parachain: this._getNativeId()},
+            },
+          },
+        },
+        SENDBACK_AMOUNT,
+      );
+
+      let xcmProgramSent: any;
+
+
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), xcmProgram);
+          xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        } else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), xcmProgram]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
+          xcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+      await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Success, event => event.messageHash == xcmProgramSent.messageHash);
+
+      this._balanceUniqueTokenFinal = await helper.balance.getSubstrate(randomAccountOnUnq.address);
+
+      expect(this._balanceUniqueTokenFinal).to.be.equal(this._balanceUniqueTokenInit - this._unqFees - STAYED_ON_TARGET_CHAIN);
+
+    });
+  }
+
+  async sendOnlyOwnedBalance(
+    networkName: keyof typeof NETWORKS,
+    sudoer: IKeyringPair,
+  ) {
+    const networkUrl = mapToChainUrl(networkName);
+    const targetPlayground = getDevPlayground(networkName);
+
+    const targetChainBalance = 10000n * (10n ** UNQ_DECIMALS);
+
+    await usingPlaygrounds(async (helper) => {
+      const targetChainSovereignAccount = helper.address.paraSiblingSovereignAccount(mapToChainId(networkName));
+      await helper.getSudo().balance.setBalanceSubstrate(sudoer, targetChainSovereignAccount, targetChainBalance);
+      const moreThanTargetChainHas = 2n * targetChainBalance;
+
+      const targetAccount = helper.arrange.createEmptyAccount();
+
+      const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+        targetAccount.addressRaw,
+        {
+          Concrete: {
+            parents: 0,
+            interior: 'Here',
+          },
+        },
+        moreThanTargetChainHas,
+      );
+
+      let maliciousXcmProgramSent: any;
+
+
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgram);
+          maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        } else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgram]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`sending ${networkName} -> Unique via XCM program`, batchCall);
+          maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+      await expectFailedToTransact(helper, maliciousXcmProgramSent);
+
+      const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
+      expect(targetAccountBalance).to.be.equal(0n);
+    });
+  }
+
+  async rejectReserveTransferUNQfrom(networkName: keyof typeof NETWORKS, sudoer: IKeyringPair) {
+    const networkUrl = mapToChainUrl(networkName);
+    const targetPlayground = getDevPlayground(networkName);
+
+    await usingPlaygrounds(async (helper) => {
+      const testAmount = 10_000n * (10n ** UNQ_DECIMALS);
+      const targetAccount = helper.arrange.createEmptyAccount();
+
+      const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+        targetAccount.addressRaw,
+        {
+          Concrete: {
+            parents: 1,
+            interior: {
+              X1: {
+                Parachain: this._getNativeId(),
+              },
+            },
+          },
+        },
+        testAmount,
+      );
+
+      const maliciousXcmProgramHereId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+        targetAccount.addressRaw,
+        {
+          Concrete: {
+            parents: 0,
+            interior: 'Here',
+          },
+        },
+        testAmount,
+      );
+
+      let maliciousXcmProgramFullIdSent: any;
+      let maliciousXcmProgramHereIdSent: any;
+      const maxWaitBlocks = 3;
+
+      // Try to trick Unique using full UNQ identification
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId);
+          maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+        // Moonbeam case
+        else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`${networkName} try to act like a reserve location for UNQ using path asset identification`,batchCall);
+
+          maliciousXcmProgramFullIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+
+      await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramFullIdSent);
+
+      let accountBalance = await helper.balance.getSubstrate(targetAccount.address);
+      expect(accountBalance).to.be.equal(0n);
+
+      // Try to trick Unique using shortened UNQ identification
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoer, this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId);
+          maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+        else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramHereId]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`${networkName} try to act like a reserve location for UNQ using "here" asset identification`, batchCall);
+
+          maliciousXcmProgramHereIdSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+
+      await expectUntrustedReserveLocationFail(helper, maliciousXcmProgramHereIdSent);
+
+      accountBalance = await helper.balance.getSubstrate(targetAccount.address);
+      expect(accountBalance).to.be.equal(0n);
+    });
+  }
+
+  async rejectNativeTokensFrom(networkName: keyof typeof NETWORKS, sudoerOnTargetChain: IKeyringPair) {
+    const networkUrl = mapToChainUrl(networkName);
+    const targetPlayground = getDevPlayground(networkName);
+    let messageSent: any;
+
+    await usingPlaygrounds(async (helper) => {
+      const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+        helper.arrange.createEmptyAccount().addressRaw,
+        {
+          Concrete: {
+            parents: 1,
+            interior: {
+              X1: {
+                Parachain: mapToChainId(networkName),
+              },
+            },
+          },
+        },
+        TARGET_CHAIN_TOKEN_TRANSFER_AMOUNT,
+      );
+      await targetPlayground(networkUrl, async (helper) => {
+        if('getSudo' in helper) {
+          await helper.getSudo().xcm.send(sudoerOnTargetChain, this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId);
+          messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        } else if('fastDemocracy' in helper) {
+          const xcmSend = helper.constructApiCall('api.tx.polkadotXcm.send', [this._runtimeVersionedMultilocation(), maliciousXcmProgramFullId]);
+          // Needed to bypass the call filter.
+          const batchCall = helper.encodeApiCall('api.tx.utility.batch', [[xcmSend]]);
+          await helper.fastDemocracy.executeProposal(`${networkName} sending native tokens to the Unique via fast democracy`, batchCall);
+
+          messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+        }
+      });
+      await expectFailedToTransact(helper, messageSent);
+    });
+  }
+
+  private async _relayXcmTransactSetStorage(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') {
+    // eslint-disable-next-line require-await
+    return await usingPlaygrounds(async (helper) => {
+      const relayForceKV = () => {
+        const random = Math.random();
+        const key = `relay-forced-key (instance: ${random})`;
+        const val = `relay-forced-value (instance: ${random})`;
+        const call = helper.constructApiCall('api.tx.system.setStorage', [[[key, val]]]).method.toHex();
+
+        return {
+          call,
+          key,
+          val,
+        };
+      };
+
+      if(variant == 'plain') {
+        const kv = relayForceKV();
+        return {
+          program: helper.arrange.makeUnpaidSudoTransactProgram({
+            weightMultiplier: 1,
+            call: kv.call,
+          }),
+          kvs: [kv],
+        };
+      } else {
+        const kv0 = relayForceKV();
+        const kv1 = relayForceKV();
+
+        const batchCall = helper.constructApiCall(`api.tx.utility.${variant}`, [[kv0.call, kv1.call]]).method.toHex();
+        return {
+          program: helper.arrange.makeUnpaidSudoTransactProgram({
+            weightMultiplier: 2,
+            call: batchCall,
+          }),
+          kvs: [kv0, kv1],
+        };
+      }
+    });
+  }
+
+  async relayIsPermittedToSetStorage(relaySudoer: IKeyringPair, variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch') {
+    const {program, kvs} = await this._relayXcmTransactSetStorage(variant);
+
+    await usingRelayPlaygrounds(relayUrl, async (helper) => {
+      await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [
+        this._uniqueChainMultilocationForRelay(),
+        program,
+      ]);
+    });
+
+    await usingPlaygrounds(async (helper) => {
+      await expectDownwardXcmComplete(helper);
+
+      for(const kv of kvs) {
+        const forcedValue = await helper.callRpc('api.rpc.state.getStorage', [kv.key]);
+        expect(hexToString(forcedValue.toHex())).to.be.equal(kv.val);
+      }
+    });
+  }
+
+  private async _relayXcmTransactSetBalance(variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs') {
+    // eslint-disable-next-line require-await
+    return await usingPlaygrounds(async (helper) => {
+      const emptyAccount = helper.arrange.createEmptyAccount().address;
+
+      const forceSetBalanceCall = helper.constructApiCall('api.tx.balances.forceSetBalance', [emptyAccount, 10_000n]).method.toHex();
+
+      let call;
+
+      if(variant == 'plain') {
+        call = forceSetBalanceCall;
+
+      } else if(variant == 'dispatchAs') {
+        call = helper.constructApiCall('api.tx.utility.dispatchAs', [
+          {
+            system: 'Root',
+          },
+          forceSetBalanceCall,
+        ]).method.toHex();
+      } else {
+        call = helper.constructApiCall(`api.tx.utility.${variant}`, [[forceSetBalanceCall]]).method.toHex();
+      }
+
+      return {
+        program: helper.arrange.makeUnpaidSudoTransactProgram({
+          weightMultiplier: 1,
+          call,
+        }),
+        emptyAccount,
+      };
+    });
+  }
+
+  async relayIsNotPermittedToSetBalance(
+    relaySudoer: IKeyringPair,
+    variant: 'plain' | 'batch' | 'batchAll' | 'forceBatch' | 'dispatchAs',
+  ) {
+    const {program, emptyAccount} = await this._relayXcmTransactSetBalance(variant);
+
+    await usingRelayPlaygrounds(relayUrl, async (helper) => {
+      await helper.getSudo().executeExtrinsic(relaySudoer, 'api.tx.xcmPallet.send', [
+        this._uniqueChainMultilocationForRelay(),
+        program,
+      ]);
+    });
+
+    await usingPlaygrounds(async (helper) => {
+      await expectDownwardXcmNoPermission(helper);
+      expect(await helper.balance.getSubstrate(emptyAccount)).to.be.equal(0n);
+    });
+  }
+}
modifiedtests/src/xcm/xcmQuartz.test.tsdiffbeforeafterboth
--- a/tests/src/xcm/xcmQuartz.test.ts
+++ b/tests/src/xcm/xcmQuartz.test.ts
@@ -15,30 +15,15 @@
 // along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
 
 import {IKeyringPair} from '@polkadot/types/types';
-import config from '../config';
 import {itSub, expect, describeXCM, usingPlaygrounds, usingKaruraPlaygrounds, usingRelayPlaygrounds, usingMoonriverPlaygrounds, usingStateminePlaygrounds, usingShidenPlaygrounds} from '../util';
 import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
+import {STATEMINE_CHAIN, QUARTZ_CHAIN, KARURA_CHAIN, MOONRIVER_CHAIN, SHIDEN_CHAIN, STATEMINE_DECIMALS, KARURA_DECIMALS, QTZ_DECIMALS, RELAY_DECIMALS, SHIDEN_DECIMALS, karuraUrl, moonriverUrl, relayUrl, shidenUrl, statemineUrl} from './xcm.types';
+import {hexToString} from '@polkadot/util';
 
-const QUARTZ_CHAIN = +(process.env.RELAY_QUARTZ_ID || 2095);
-const STATEMINE_CHAIN = +(process.env.RELAY_STATEMINE_ID || 1000);
-const KARURA_CHAIN = +(process.env.RELAY_KARURA_ID || 2000);
-const MOONRIVER_CHAIN = +(process.env.RELAY_MOONRIVER_ID || 2023);
-const SHIDEN_CHAIN = +(process.env.RELAY_SHIDEN_ID || 2007);
 
-const STATEMINE_PALLET_INSTANCE = 50;
 
-const relayUrl = config.relayUrl;
-const statemineUrl = config.statemineUrl;
-const karuraUrl = config.karuraUrl;
-const moonriverUrl = config.moonriverUrl;
-const shidenUrl = config.shidenUrl;
+const STATEMINE_PALLET_INSTANCE = 50;
 
-const RELAY_DECIMALS = 12;
-const STATEMINE_DECIMALS = 12;
-const KARURA_DECIMALS = 12;
-const SHIDEN_DECIMALS = 18n;
-const QTZ_DECIMALS = 18n;
-
 const TRANSFER_AMOUNT = 2000000000000000000000000n;
 
 const FUNDING_AMOUNT = 3_500_000_0000_000_000n;
@@ -499,7 +484,14 @@
         minimalBalance: 1000000000000000000n,
       };
 
-      await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);
+      const assets = (await (helper.callRpc('api.query.assetRegistry.assetMetadatas.entries'))).map(([_k, v]: [any, any]) =>
+        hexToString(v.toJSON()['symbol'])) as string[];
+
+      if(!assets.includes('QTZ')) {
+        await helper.getSudo().assetRegistry.registerForeignAsset(alice, destination, metadata);
+      } else {
+        console.log('QTZ token already registered on Karura assetRegistry pallet');
+      }
       await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);
       balanceKaruraTokenInit = await helper.balance.getSubstrate(randomAccount.address);
       balanceQuartzForeignTokenInit = await helper.tokens.accounts(randomAccount.address, {ForeignAsset: 0});
@@ -985,19 +977,22 @@
       const unitsPerSecond = 1n;
       const numAssetsWeightHint = 0;
 
-      const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({
-        location: quartzAssetLocation,
-        metadata: quartzAssetMetadata,
-        existentialDeposit,
-        isSufficient,
-        unitsPerSecond,
-        numAssetsWeightHint,
-      });
+      if((await helper.assetManager.assetTypeId(quartzAssetLocation)).toJSON()) {
+        console.log('Quartz asset already registered on Moonriver');
+      } else {
+        const encodedProposal = helper.assetManager.makeRegisterForeignAssetProposal({
+          location: quartzAssetLocation,
+          metadata: quartzAssetMetadata,
+          existentialDeposit,
+          isSufficient,
+          unitsPerSecond,
+          numAssetsWeightHint,
+        });
 
-      console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);
-
-      await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);
+        console.log('Encoded proposal for registerForeignAsset & setAssetUnitsPerSecond is %s', encodedProposal);
 
+        await helper.fastDemocracy.executeProposal('register QTZ foreign asset', encodedProposal);
+      }
       // >>> Acquire Quartz AssetId Info on Moonriver >>>
       console.log('Acquire Quartz AssetId Info on Moonriver.......');
 
@@ -1288,12 +1283,12 @@
   let alice: IKeyringPair;
   let sender: IKeyringPair;
 
-  const QTZ_ASSET_ID_ON_SHIDEN = 1;
-  const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n;
+  const QTZ_ASSET_ID_ON_SHIDEN = 18_446_744_073_709_551_633n; // The value is taken from the live Shiden
+  const QTZ_MINIMAL_BALANCE_ON_SHIDEN = 1n; // The value is taken from the live Shiden
 
   // Quartz -> Shiden
   const shidenInitialBalance = 1n * (10n ** SHIDEN_DECIMALS); // 1 SHD, existential deposit required to actually create the account on Shiden
-  const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+  const unitsPerSecond = 500_451_000_000_000_000_000n; // The value is taken from the live Shiden
   const qtzToShidenTransferred = 10n * (10n ** QTZ_DECIMALS); // 10 QTZ
   const qtzToShidenArrived = 9_999_999_999_088_000_000n; // 9.999 ... QTZ, Shiden takes a commision in foreign tokens
 
@@ -1314,40 +1309,42 @@
     });
 
     await usingShidenPlaygrounds(shidenUrl, async (helper) => {
-      console.log('1. Create foreign asset and metadata');
-      // TODO update metadata with values from production
-      await helper.assets.create(
-        alice,
-        QTZ_ASSET_ID_ON_SHIDEN,
-        alice.address,
-        QTZ_MINIMAL_BALANCE_ON_SHIDEN,
-      );
+      if(!(await helper.callRpc('api.query.assets.asset', [QTZ_ASSET_ID_ON_SHIDEN])).toJSON()) {
+        console.log('1. Create foreign asset and metadata');
+        await helper.assets.create(
+          alice,
+          QTZ_ASSET_ID_ON_SHIDEN,
+          alice.address,
+          QTZ_MINIMAL_BALANCE_ON_SHIDEN,
+        );
 
-      await helper.assets.setMetadata(
-        alice,
-        QTZ_ASSET_ID_ON_SHIDEN,
-        'Cross chain QTZ',
-        'xcQTZ',
-        Number(QTZ_DECIMALS),
-      );
+        await helper.assets.setMetadata(
+          alice,
+          QTZ_ASSET_ID_ON_SHIDEN,
+          'Quartz',
+          'QTZ',
+          Number(QTZ_DECIMALS),
+        );
 
-      console.log('2. Register asset location on Shiden');
-      const assetLocation = {
-        V2: {
-          parents: 1,
-          interior: {
-            X1: {
-              Parachain: QUARTZ_CHAIN,
+        console.log('2. Register asset location on Shiden');
+        const assetLocation = {
+          V2: {
+            parents: 1,
+            interior: {
+              X1: {
+                Parachain: QUARTZ_CHAIN,
+              },
             },
           },
-        },
-      };
+        };
 
-      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);
+        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, QTZ_ASSET_ID_ON_SHIDEN]);
 
-      console.log('3. Set QTZ payment for XCM execution on Shiden');
-      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
-
+        console.log('3. Set QTZ payment for XCM execution on Shiden');
+        await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
+      } else {
+        console.log('QTZ is already registered on Shiden');
+      }
       console.log('4. Transfer 1 SDN to recipient to create the account (needed due to existential balance)');
       await helper.balance.transferToSubstrate(alice, sender.address, shidenInitialBalance);
     });
modifiedtests/src/xcm/xcmUnique.test.tsdiffbeforeafterboth
--- a/tests/src/xcm/xcmUnique.test.ts
+++ b/tests/src/xcm/xcmUnique.test.ts
@@ -1511,12 +1511,12 @@
   let alice: IKeyringPair;
   let randomAccount: IKeyringPair;
 
-  const UNQ_ASSET_ID_ON_ASTAR = 1;
-  const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n;
+  const UNQ_ASSET_ID_ON_ASTAR = 18_446_744_073_709_551_631n; // The value is taken from the live Astar
+  const UNQ_MINIMAL_BALANCE_ON_ASTAR = 1n; // The value is taken from the live Astar
 
   // Unique -> Astar
   const astarInitialBalance = 1n * (10n ** ASTAR_DECIMALS); // 1 ASTR, existential deposit required to actually create the account on Astar.
-  const unitsPerSecond = 228_000_000_000n; // This is Phala's value. What will be ours?
+  const unitsPerSecond = 9_451_000_000_000_000_000n; // The value is taken from the live Astar
   const unqToAstarTransferred = 10n * (10n ** UNQ_DECIMALS); // 10 UNQ
   const unqToAstarArrived = 9_999_999_999_088_000_000n; // 9.999 ... UNQ, Astar takes a commision in foreign tokens
 
@@ -1539,7 +1539,6 @@
     await usingAstarPlaygrounds(astarUrl, async (helper) => {
       if(!(await helper.callRpc('api.query.assets.asset', [UNQ_ASSET_ID_ON_ASTAR])).toJSON()) {
         console.log('1. Create foreign asset and metadata');
-        // TODO update metadata with values from production
         await helper.assets.create(
           alice,
           UNQ_ASSET_ID_ON_ASTAR,
@@ -1550,8 +1549,8 @@
         await helper.assets.setMetadata(
           alice,
           UNQ_ASSET_ID_ON_ASTAR,
-          'Cross chain UNQ',
-          'xcUNQ',
+          'Unique Network',
+          'UNQ',
           Number(UNQ_DECIMALS),
         );