git.delta.rocks / unique-network / refs/commits / 9812643539bc

difftreelog

Add Unique-Astar integration tests

Max Andreev2023-03-28parent: #f628606.patch.diff
in: master

5 files changed

modifiedtests/src/config.tsdiffbeforeafterboth
--- a/tests/src/config.ts
+++ b/tests/src/config.ts
@@ -24,6 +24,8 @@
   karuraUrl: process.env.acalaUrl || 'ws://127.0.0.1:9946',
   moonbeamUrl: process.env.moonbeamUrl || 'ws://127.0.0.1:9947',
   moonriverUrl: process.env.moonbeamUrl || 'ws://127.0.0.1:9947',
+  astarUrl: process.env.astarUrl || 'ws://127.0.0.1:9949',
+  shidenUrl: process.env.shidenUrl || 'ws://127.0.0.1:9949',
   westmintUrl: process.env.westmintUrl || 'ws://127.0.0.1:9948',
   statemineUrl: process.env.statemineUrl || 'ws://127.0.0.1:9948',
   statemintUrl: process.env.statemintUrl || 'ws://127.0.0.1:9948',
modifiedtests/src/util/index.tsdiffbeforeafterboth
--- a/tests/src/util/index.ts
+++ b/tests/src/util/index.ts
@@ -11,7 +11,7 @@
 import config from '../config';
 import {ChainHelperBase} from './playgrounds/unique';
 import {ILogger} from './playgrounds/types';
-import {DevUniqueHelper, SilentLogger, SilentConsole, DevMoonbeamHelper, DevMoonriverHelper, DevAcalaHelper, DevKaruraHelper, DevRelayHelper, DevWestmintHelper, DevStatemineHelper, DevStatemintHelper} from './playgrounds/unique.dev';
+import {DevUniqueHelper, SilentLogger, SilentConsole, DevMoonbeamHelper, DevMoonriverHelper, DevAcalaHelper, DevKaruraHelper, DevRelayHelper, DevWestmintHelper, DevStatemineHelper, DevStatemintHelper, DevAstarHelper} from './playgrounds/unique.dev';
 import {dirname} from 'path';
 import {fileURLToPath} from 'url';
 
@@ -101,6 +101,11 @@
   return usingPlaygroundsGeneral<DevMoonriverHelper>(DevMoonriverHelper, url, code);
 };
 
+export const usingAstarPlaygrounds = (url: string, code: (helper: DevAstarHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => {
+  return usingPlaygroundsGeneral<DevAstarHelper>(DevAstarHelper, url, code);
+};
+
+
 export const MINIMUM_DONOR_FUND = 100_000n;
 export const DONOR_FUNDING = 2_000_000n;
 
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
before · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, IPovInfo, TSigner} from './types';12import {FrameSystemEventRecord} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';1617export class SilentLogger {18  log(_msg: any, _level: any): void { }19  level = {20    ERROR: 'ERROR' as const,21    WARNING: 'WARNING' as const,22    INFO: 'INFO' as const,23  };24}2526export class SilentConsole {27  // TODO: Remove, this is temporary: Filter unneeded API output28  // (Jaco promised it will be removed in the next version)29  consoleErr: any;30  consoleLog: any;31  consoleWarn: any;3233  constructor() {34    this.consoleErr = console.error;35    this.consoleLog = console.log;36    this.consoleWarn = console.warn;37  }3839  enable() {40    const outFn = (printer: any) => (...args: any[]) => {41      for (const arg of args) {42        if (typeof arg !== 'string')43          continue;44        if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')45          return;46      }47      printer(...args);48    };4950    console.error = outFn(this.consoleErr.bind(console));51    console.log = outFn(this.consoleLog.bind(console));52    console.warn = outFn(this.consoleWarn.bind(console));53  }5455  disable() {56    console.error = this.consoleErr;57    console.log = this.consoleLog;58    console.warn = this.consoleWarn;59  }60}6162export class DevUniqueHelper extends UniqueHelper {63  /**64   * Arrange methods for tests65   */66  arrange: ArrangeGroup;67  wait: WaitGroup;68  admin: AdminGroup;69  session: SessionGroup;70  testUtils: TestUtilGroup;7172  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {73    options.helperBase = options.helperBase ?? DevUniqueHelper;7475    super(logger, options);76    this.arrange = new ArrangeGroup(this);77    this.wait = new WaitGroup(this);78    this.admin = new AdminGroup(this);79    this.testUtils = new TestUtilGroup(this);80    this.session = new SessionGroup(this);81  }8283  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {84    const wsProvider = new WsProvider(wsEndpoint);85    this.api = new ApiPromise({86      provider: wsProvider,87      signedExtensions: {88        ContractHelpers: {89          extrinsic: {},90          payload: {},91        },92        CheckMaintenance: {93          extrinsic: {},94          payload: {},95        },96        DisableIdentityCalls: {97          extrinsic: {},98          payload: {},99        },100        FakeTransactionFinalizer: {101          extrinsic: {},102          payload: {},103        },104      },105      rpc: {106        unique: defs.unique.rpc,107        appPromotion: defs.appPromotion.rpc,108        povinfo: defs.povinfo.rpc,109        eth: {110          feeHistory: {111            description: 'Dummy',112            params: [],113            type: 'u8',114          },115          maxPriorityFeePerGas: {116            description: 'Dummy',117            params: [],118            type: 'u8',119          },120        },121      },122    });123    await this.api.isReadyOrError;124    this.network = await UniqueHelper.detectNetwork(this.api);125    this.wsEndpoint = wsEndpoint;126  }127}128129export class DevRelayHelper extends RelayHelper {130  wait: WaitGroup;131132  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {133    options.helperBase = options.helperBase ?? DevRelayHelper;134135    super(logger, options);136    this.wait = new WaitGroup(this);137  }138}139140export class DevWestmintHelper extends WestmintHelper {141  wait: WaitGroup;142143  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {144    options.helperBase = options.helperBase ?? DevWestmintHelper;145146    super(logger, options);147    this.wait = new WaitGroup(this);148  }149}150151export class DevStatemineHelper extends DevWestmintHelper {}152153export class DevStatemintHelper extends DevWestmintHelper {}154155export class DevMoonbeamHelper extends MoonbeamHelper {156  account: MoonbeamAccountGroup;157  wait: WaitGroup;158159  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {160    options.helperBase = options.helperBase ?? DevMoonbeamHelper;161    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';162163    super(logger, options);164    this.account = new MoonbeamAccountGroup(this);165    this.wait = new WaitGroup(this);166  }167}168169export class DevMoonriverHelper extends DevMoonbeamHelper {170  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {171    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';172    super(logger, options);173  }174}175176export class DevAcalaHelper extends AcalaHelper {177  wait: WaitGroup;178179  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {180    options.helperBase = options.helperBase ?? DevAcalaHelper;181182    super(logger, options);183    this.wait = new WaitGroup(this);184  }185}186187export class DevKaruraHelper extends DevAcalaHelper {}188189export class ArrangeGroup {190  helper: DevUniqueHelper;191192  scheduledIdSlider = 0;193194  constructor(helper: DevUniqueHelper) {195    this.helper = helper;196  }197198  /**199   * Generates accounts with the specified UNQ token balance200   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.201   * @param donor donor account for balances202   * @returns array of newly created accounts203   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);204   */205  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {206    let nonce = await this.helper.chain.getNonce(donor.address);207    const wait = new WaitGroup(this.helper);208    const ss58Format = this.helper.chain.getChainProperties().ss58Format;209    const tokenNominal = this.helper.balance.getOneTokenNominal();210    const transactions = [];211    const accounts: IKeyringPair[] = [];212    for (const balance of balances) {213      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);214      accounts.push(recipient);215      if (balance !== 0n) {216        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);217        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));218        nonce++;219      }220    }221222    await Promise.all(transactions).catch(_e => {});223224    //#region TODO remove this region, when nonce problem will be solved225    const checkBalances = async () => {226      let isSuccess = true;227      for (let i = 0; i < balances.length; i++) {228        const balance = await this.helper.balance.getSubstrate(accounts[i].address);229        if (balance !== balances[i] * tokenNominal) {230          isSuccess = false;231          break;232        }233      }234      return isSuccess;235    };236237    let accountsCreated = false;238    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;239    // checkBalances retry up to 5-50 blocks240    for (let index = 0; index < maxBlocksChecked; index++) {241      accountsCreated = await checkBalances();242      if(accountsCreated) break;243      await wait.newBlocks(1);244    }245246    if (!accountsCreated) throw Error('Accounts generation failed');247    //#endregion248249    return accounts;250  };251252  // TODO combine this method and createAccounts into one253  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {254    const createAsManyAsCan = async () => {255      let transactions: any = [];256      const accounts: IKeyringPair[] = [];257      let nonce = await this.helper.chain.getNonce(donor.address);258      const tokenNominal = this.helper.balance.getOneTokenNominal();259      const ss58Format = this.helper.chain.getChainProperties().ss58Format;260      for (let i = 0; i < accountsToCreate; i++) {261        if (i === 500) { // if there are too many accounts to create262          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled263          transactions = []; //264          nonce = await this.helper.chain.getNonce(donor.address); // update nonce265        }266        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);267        accounts.push(recipient);268        if (withBalance !== 0n) {269          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);270          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));271          nonce++;272        }273      }274275      const fullfilledAccounts = [];276      await Promise.allSettled(transactions);277      for (const account of accounts) {278        const accountBalance = await this.helper.balance.getSubstrate(account.address);279        if (accountBalance === withBalance * tokenNominal) {280          fullfilledAccounts.push(account);281        }282      }283      return fullfilledAccounts;284    };285286287    const crowd: IKeyringPair[] = [];288    // do up to 5 retries289    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {290      const asManyAsCan = await createAsManyAsCan();291      crowd.push(...asManyAsCan);292      accountsToCreate -= asManyAsCan.length;293    }294295    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);296297    return crowd;298  };299300  isDevNode = async () => {301    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();302    if (blockNumber == 0) {303      await this.helper.wait.newBlocks(1);304      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();305    }306    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);307    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);308    const findCreationDate = (block: any) => {309      const humanBlock = block.toHuman();310      let date;311      humanBlock.block.extrinsics.forEach((ext: any) => {312        if(ext.method.section === 'timestamp') {313          date = Number(ext.method.args.now.replaceAll(',', ''));314        }315      });316      return date;317    };318    const block1date = await findCreationDate(block1);319    const block2date = await findCreationDate(block2);320    if(block2date! - block1date! < 9000) return true;321  };322323  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {324    const address = payer.Substrate ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum!);325    let balance = await this.helper.balance.getSubstrate(address);326327    await promise();328329    balance -= await this.helper.balance.getSubstrate(address);330331    return balance;332  }333334  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {335    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);336337    const kvJson: {[key: string]: string} = {};338339    for (const kv of rawPovInfo.keyValues) {340      kvJson[kv.key.toHex()] = kv.value.toHex();341    }342343    const kvStr = JSON.stringify(kvJson);344345    const chainql = spawnSync(346      'chainql',347      [348        `--tla-code=data=${kvStr}`,349        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,350      ],351    );352353    if (!chainql.stdout) {354      throw Error('unable to get an output from the `chainql`');355    }356357    return {358      proofSize: rawPovInfo.proofSize.toNumber(),359      compactProofSize: rawPovInfo.compactProofSize.toNumber(),360      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),361      results: rawPovInfo.results,362      kv: JSON.parse(chainql.stdout.toString()),363    };364  }365366  calculatePalletAddress(palletId: any) {367    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));368    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);369  }370371  makeScheduledIds(num: number): string[] {372    function makeId(slider: number) {373      const scheduledIdSize = 64;374      const hexId = slider.toString(16);375      const prefixSize = scheduledIdSize - hexId.length;376377      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;378379      return scheduledId;380    }381382    const ids = [];383    for (let i = 0; i < num; i++) {384      ids.push(makeId(this.scheduledIdSlider));385      this.scheduledIdSlider += 1;386    }387388    return ids;389  }390391  makeScheduledId(): string {392    return (this.makeScheduledIds(1))[0];393  }394395  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {396    const capture = new EventCapture(this.helper, eventSection, eventMethod);397    await capture.startCapture();398399    return capture;400  }401}402403class MoonbeamAccountGroup {404  helper: MoonbeamHelper;405406  keyring: Keyring;407  _alithAccount: IKeyringPair;408  _baltatharAccount: IKeyringPair;409  _dorothyAccount: IKeyringPair;410411  constructor(helper: MoonbeamHelper) {412    this.helper = helper;413414    this.keyring = new Keyring({type: 'ethereum'});415    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';416    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';417    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';418419    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');420    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');421    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');422  }423424  alithAccount() {425    return this._alithAccount;426  }427428  baltatharAccount() {429    return this._baltatharAccount;430  }431432  dorothyAccount() {433    return this._dorothyAccount;434  }435436  create() {437    return this.keyring.addFromUri(mnemonicGenerate());438  }439}440441class WaitGroup {442  helper: ChainHelperBase;443444  constructor(helper: ChainHelperBase) {445    this.helper = helper;446  }447448  sleep(milliseconds: number) {449    return new Promise((resolve) => setTimeout(resolve, milliseconds));450  }451452  private async waitWithTimeout(promise: Promise<any>, timeout: number) {453    let isBlock = false;454    promise.then(() => isBlock = true).catch(() => isBlock = true);455    let totalTime = 0;456    const step = 100;457    while(!isBlock) {458      await this.sleep(step);459      totalTime += step;460      if(totalTime >= timeout) throw Error('Blocks production failed');461    }462    return promise;463  }464465  /**466   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.467   * @param promise async operation to race against the timeout468   * @param timeoutMS time after which to time out469   * @param timeoutError error message to throw470   * @returns promise of the same type the operation had471   */472  withTimeout<T>(473    promise: Promise<T>,474    timeoutMS = 30000,475    timeoutError = 'The operation has timed out!',476  ): Promise<T> {477    const timeout = new Promise<never>((_, reject) => {478      setTimeout(() => {479        reject(new Error(timeoutError));480      }, timeoutMS);481    });482483    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});484  }485486  /**487   * Wait for specified number of blocks488   * @param blocksCount number of blocks to wait489   * @returns490   */491  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {492    timeout = timeout ?? blocksCount * 60_000;493    // eslint-disable-next-line no-async-promise-executor494    const promise = new Promise<void>(async (resolve) => {495      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {496        if (blocksCount > 0) {497          blocksCount--;498        } else {499          unsubscribe();500          resolve();501        }502      });503    });504    await this.waitWithTimeout(promise, timeout);505    return promise;506  }507508  /**509   * Wait for the specified number of sessions to pass.510   * Only applicable if the Session pallet is turned on.511   * @param sessionCount number of sessions to wait512   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks513   * @returns514   */515  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {516    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`517      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');518519    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;520    let currentSessionIndex = -1;521522    while (currentSessionIndex < expectedSessionIndex) {523      // eslint-disable-next-line no-async-promise-executor524      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {525        await this.newBlocks(1);526        const res = await (this.helper as DevUniqueHelper).session.getIndex();527        resolve(res);528      }), blockTimeout, 'The chain has stopped producing blocks!');529    }530  }531532  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {533    timeout = timeout ?? 30 * 60 * 1000;534    // eslint-disable-next-line no-async-promise-executor535    const promise = new Promise<void>(async (resolve) => {536      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {537        if (data.number.toNumber() >= blockNumber) {538          unsubscribe();539          resolve();540        }541      });542    });543    await this.waitWithTimeout(promise, timeout);544    return promise;545  }546547  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {548    timeout = timeout ?? 30 * 60 * 1000;549    // eslint-disable-next-line no-async-promise-executor550    const promise = new Promise<void>(async (resolve) => {551      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {552        if (data.value.relayParentNumber.toNumber() >= blockNumber) {553          // @ts-ignore554          unsubscribe();555          resolve();556        }557      });558    });559    await this.waitWithTimeout(promise, timeout);560    return promise;561  }562563  noScheduledTasks() {564    const api = this.helper.getApi();565566    // eslint-disable-next-line no-async-promise-executor567    const promise = new Promise<void>(async resolve => {568      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {569        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();570571        if(areThereScheduledTasks.length == 0) {572          unsubscribe();573          resolve();574        }575      });576    });577578    return promise;579  }580581  event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {582    // eslint-disable-next-line no-async-promise-executor583    const promise = new Promise<EventRecord | null>(async (resolve) => {584      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {585        const blockNumber = header.number.toHuman();586        const blockHash = header.hash;587        const eventIdStr = `${eventSection}.${eventMethod}`;588        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;589590        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);591592        const apiAt = await this.helper.getApi().at(blockHash);593        const eventRecords = (await apiAt.query.system.events()) as any;594595        const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {596          return r.event.section == eventSection && r.event.method == eventMethod;597        });598599        if (neededEvent) {600          unsubscribe();601          resolve(neededEvent);602        } else if (maxBlocksToWait > 0) {603          maxBlocksToWait--;604        } else {605          this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);606          unsubscribe();607          resolve(null);608        }609      });610    });611    return promise;612  }613}614615class SessionGroup {616  helper: ChainHelperBase;617618  constructor(helper: ChainHelperBase) {619    this.helper = helper;620  }621622  //todo:collator documentation623  async getIndex(): Promise<number> {624    return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();625  }626627  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {628    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);629  }630631  setOwnKeys(signer: TSigner, key: string) {632    return this.helper.executeExtrinsic(633      signer,634      'api.tx.session.setKeys',635      [key, '0x0'],636      true,637    );638  }639640  setOwnKeysFromAddress(signer: TSigner) {641    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));642  }643}644645class TestUtilGroup {646  helper: DevUniqueHelper;647648  constructor(helper: DevUniqueHelper) {649    this.helper = helper;650  }651652  async enable() {653    if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {654      return;655    }656657    const signer = this.helper.util.fromSeed('//Alice');658    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);659  }660661  async setTestValue(signer: TSigner, testVal: number) {662    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);663  }664665  async incTestValue(signer: TSigner) {666    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);667  }668669  async setTestValueAndRollback(signer: TSigner, testVal: number) {670    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);671  }672673  async testValue(blockIdx?: number) {674    const api = blockIdx675      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))676      : this.helper.getApi();677678    return (await api.query.testUtils.testValue()).toJSON();679  }680681  async justTakeFee(signer: TSigner) {682    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);683  }684685  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {686    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);687  }688}689690class EventCapture {691  helper: DevUniqueHelper;692  eventSection: string;693  eventMethod: string;694  events: EventRecord[] = [];695  unsubscribe: VoidFn | null = null;696697  constructor(698    helper: DevUniqueHelper,699    eventSection: string,700    eventMethod: string,701  ) {702    this.helper = helper;703    this.eventSection = eventSection;704    this.eventMethod = eventMethod;705  }706707  async startCapture() {708    this.stopCapture();709    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {710      const newEvents = eventRecords.filter(r => {711        return r.event.section == this.eventSection && r.event.method == this.eventMethod;712      });713714      this.events.push(...newEvents);715    })) as any;716  }717718  stopCapture() {719    if (this.unsubscribe !== null) {720      this.unsubscribe();721    }722  }723724  extractCapturedEvents() {725    return this.events;726  }727}728729class AdminGroup {730  helper: UniqueHelper;731732  constructor(helper: UniqueHelper) {733    this.helper = helper;734  }735736  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {737    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);738    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {739      return {740        staker: e.event.data[0].toString(),741        stake: e.event.data[1].toBigInt(),742        payout: e.event.data[2].toBigInt(),743      };744    });745  }746}
after · tests/src/util/playgrounds/unique.dev.ts
1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, IPovInfo, TSigner} from './types';12import {FrameSystemEventRecord} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';1617export class SilentLogger {18  log(_msg: any, _level: any): void { }19  level = {20    ERROR: 'ERROR' as const,21    WARNING: 'WARNING' as const,22    INFO: 'INFO' as const,23  };24}2526export class SilentConsole {27  // TODO: Remove, this is temporary: Filter unneeded API output28  // (Jaco promised it will be removed in the next version)29  consoleErr: any;30  consoleLog: any;31  consoleWarn: any;3233  constructor() {34    this.consoleErr = console.error;35    this.consoleLog = console.log;36    this.consoleWarn = console.warn;37  }3839  enable() {40    const outFn = (printer: any) => (...args: any[]) => {41      for (const arg of args) {42        if (typeof arg !== 'string')43          continue;44        if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')45          return;46      }47      printer(...args);48    };4950    console.error = outFn(this.consoleErr.bind(console));51    console.log = outFn(this.consoleLog.bind(console));52    console.warn = outFn(this.consoleWarn.bind(console));53  }5455  disable() {56    console.error = this.consoleErr;57    console.log = this.consoleLog;58    console.warn = this.consoleWarn;59  }60}6162export class DevUniqueHelper extends UniqueHelper {63  /**64   * Arrange methods for tests65   */66  arrange: ArrangeGroup;67  wait: WaitGroup;68  admin: AdminGroup;69  session: SessionGroup;70  testUtils: TestUtilGroup;7172  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {73    options.helperBase = options.helperBase ?? DevUniqueHelper;7475    super(logger, options);76    this.arrange = new ArrangeGroup(this);77    this.wait = new WaitGroup(this);78    this.admin = new AdminGroup(this);79    this.testUtils = new TestUtilGroup(this);80    this.session = new SessionGroup(this);81  }8283  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {84    const wsProvider = new WsProvider(wsEndpoint);85    this.api = new ApiPromise({86      provider: wsProvider,87      signedExtensions: {88        ContractHelpers: {89          extrinsic: {},90          payload: {},91        },92        CheckMaintenance: {93          extrinsic: {},94          payload: {},95        },96        DisableIdentityCalls: {97          extrinsic: {},98          payload: {},99        },100        FakeTransactionFinalizer: {101          extrinsic: {},102          payload: {},103        },104      },105      rpc: {106        unique: defs.unique.rpc,107        appPromotion: defs.appPromotion.rpc,108        povinfo: defs.povinfo.rpc,109        eth: {110          feeHistory: {111            description: 'Dummy',112            params: [],113            type: 'u8',114          },115          maxPriorityFeePerGas: {116            description: 'Dummy',117            params: [],118            type: 'u8',119          },120        },121      },122    });123    await this.api.isReadyOrError;124    this.network = await UniqueHelper.detectNetwork(this.api);125    this.wsEndpoint = wsEndpoint;126  }127}128129export class DevRelayHelper extends RelayHelper {130  wait: WaitGroup;131132  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {133    options.helperBase = options.helperBase ?? DevRelayHelper;134135    super(logger, options);136    this.wait = new WaitGroup(this);137  }138}139140export class DevWestmintHelper extends WestmintHelper {141  wait: WaitGroup;142143  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {144    options.helperBase = options.helperBase ?? DevWestmintHelper;145146    super(logger, options);147    this.wait = new WaitGroup(this);148  }149}150151export class DevStatemineHelper extends DevWestmintHelper {}152153export class DevStatemintHelper extends DevWestmintHelper {}154155export class DevMoonbeamHelper extends MoonbeamHelper {156  account: MoonbeamAccountGroup;157  wait: WaitGroup;158159  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {160    options.helperBase = options.helperBase ?? DevMoonbeamHelper;161    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';162163    super(logger, options);164    this.account = new MoonbeamAccountGroup(this);165    this.wait = new WaitGroup(this);166  }167}168169export class DevMoonriverHelper extends DevMoonbeamHelper {170  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {171    options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';172    super(logger, options);173  }174}175176export class DevAstarHelper extends AstarHelper {177  wait: WaitGroup;178179  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {180    options.helperBase = options.helperBase ?? DevAstarHelper;181182    super(logger, options);183    this.wait = new WaitGroup(this);184  }185}186187export class DevAcalaHelper extends AcalaHelper {188  wait: WaitGroup;189190  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {191    options.helperBase = options.helperBase ?? DevAcalaHelper;192193    super(logger, options);194    this.wait = new WaitGroup(this);195  }196}197198export class DevKaruraHelper extends DevAcalaHelper {}199200export class ArrangeGroup {201  helper: DevUniqueHelper;202203  scheduledIdSlider = 0;204205  constructor(helper: DevUniqueHelper) {206    this.helper = helper;207  }208209  /**210   * Generates accounts with the specified UNQ token balance211   * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.212   * @param donor donor account for balances213   * @returns array of newly created accounts214   * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);215   */216  createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {217    let nonce = await this.helper.chain.getNonce(donor.address);218    const wait = new WaitGroup(this.helper);219    const ss58Format = this.helper.chain.getChainProperties().ss58Format;220    const tokenNominal = this.helper.balance.getOneTokenNominal();221    const transactions = [];222    const accounts: IKeyringPair[] = [];223    for (const balance of balances) {224      const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);225      accounts.push(recipient);226      if (balance !== 0n) {227        const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);228        transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));229        nonce++;230      }231    }232233    await Promise.all(transactions).catch(_e => {});234235    //#region TODO remove this region, when nonce problem will be solved236    const checkBalances = async () => {237      let isSuccess = true;238      for (let i = 0; i < balances.length; i++) {239        const balance = await this.helper.balance.getSubstrate(accounts[i].address);240        if (balance !== balances[i] * tokenNominal) {241          isSuccess = false;242          break;243        }244      }245      return isSuccess;246    };247248    let accountsCreated = false;249    const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;250    // checkBalances retry up to 5-50 blocks251    for (let index = 0; index < maxBlocksChecked; index++) {252      accountsCreated = await checkBalances();253      if(accountsCreated) break;254      await wait.newBlocks(1);255    }256257    if (!accountsCreated) throw Error('Accounts generation failed');258    //#endregion259260    return accounts;261  };262263  // TODO combine this method and createAccounts into one264  createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {265    const createAsManyAsCan = async () => {266      let transactions: any = [];267      const accounts: IKeyringPair[] = [];268      let nonce = await this.helper.chain.getNonce(donor.address);269      const tokenNominal = this.helper.balance.getOneTokenNominal();270      const ss58Format = this.helper.chain.getChainProperties().ss58Format;271      for (let i = 0; i < accountsToCreate; i++) {272        if (i === 500) { // if there are too many accounts to create273          await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled274          transactions = []; //275          nonce = await this.helper.chain.getNonce(donor.address); // update nonce276        }277        const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);278        accounts.push(recipient);279        if (withBalance !== 0n) {280          const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);281          transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));282          nonce++;283        }284      }285286      const fullfilledAccounts = [];287      await Promise.allSettled(transactions);288      for (const account of accounts) {289        const accountBalance = await this.helper.balance.getSubstrate(account.address);290        if (accountBalance === withBalance * tokenNominal) {291          fullfilledAccounts.push(account);292        }293      }294      return fullfilledAccounts;295    };296297298    const crowd: IKeyringPair[] = [];299    // do up to 5 retries300    for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {301      const asManyAsCan = await createAsManyAsCan();302      crowd.push(...asManyAsCan);303      accountsToCreate -= asManyAsCan.length;304    }305306    if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);307308    return crowd;309  };310311  isDevNode = async () => {312    let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();313    if (blockNumber == 0) {314      await this.helper.wait.newBlocks(1);315      blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();316    }317    const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);318    const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);319    const findCreationDate = (block: any) => {320      const humanBlock = block.toHuman();321      let date;322      humanBlock.block.extrinsics.forEach((ext: any) => {323        if(ext.method.section === 'timestamp') {324          date = Number(ext.method.args.now.replaceAll(',', ''));325        }326      });327      return date;328    };329    const block1date = await findCreationDate(block1);330    const block2date = await findCreationDate(block2);331    if(block2date! - block1date! < 9000) return true;332  };333334  async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {335    const address = payer.Substrate ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum!);336    let balance = await this.helper.balance.getSubstrate(address);337338    await promise();339340    balance -= await this.helper.balance.getSubstrate(address);341342    return balance;343  }344345  async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {346    const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);347348    const kvJson: {[key: string]: string} = {};349350    for (const kv of rawPovInfo.keyValues) {351      kvJson[kv.key.toHex()] = kv.value.toHex();352    }353354    const kvStr = JSON.stringify(kvJson);355356    const chainql = spawnSync(357      'chainql',358      [359        `--tla-code=data=${kvStr}`,360        '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,361      ],362    );363364    if (!chainql.stdout) {365      throw Error('unable to get an output from the `chainql`');366    }367368    return {369      proofSize: rawPovInfo.proofSize.toNumber(),370      compactProofSize: rawPovInfo.compactProofSize.toNumber(),371      compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),372      results: rawPovInfo.results,373      kv: JSON.parse(chainql.stdout.toString()),374    };375  }376377  calculatePalletAddress(palletId: any) {378    const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));379    return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);380  }381382  makeScheduledIds(num: number): string[] {383    function makeId(slider: number) {384      const scheduledIdSize = 64;385      const hexId = slider.toString(16);386      const prefixSize = scheduledIdSize - hexId.length;387388      const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;389390      return scheduledId;391    }392393    const ids = [];394    for (let i = 0; i < num; i++) {395      ids.push(makeId(this.scheduledIdSlider));396      this.scheduledIdSlider += 1;397    }398399    return ids;400  }401402  makeScheduledId(): string {403    return (this.makeScheduledIds(1))[0];404  }405406  async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {407    const capture = new EventCapture(this.helper, eventSection, eventMethod);408    await capture.startCapture();409410    return capture;411  }412}413414class MoonbeamAccountGroup {415  helper: MoonbeamHelper;416417  keyring: Keyring;418  _alithAccount: IKeyringPair;419  _baltatharAccount: IKeyringPair;420  _dorothyAccount: IKeyringPair;421422  constructor(helper: MoonbeamHelper) {423    this.helper = helper;424425    this.keyring = new Keyring({type: 'ethereum'});426    const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';427    const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';428    const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';429430    this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');431    this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');432    this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');433  }434435  alithAccount() {436    return this._alithAccount;437  }438439  baltatharAccount() {440    return this._baltatharAccount;441  }442443  dorothyAccount() {444    return this._dorothyAccount;445  }446447  create() {448    return this.keyring.addFromUri(mnemonicGenerate());449  }450}451452class WaitGroup {453  helper: ChainHelperBase;454455  constructor(helper: ChainHelperBase) {456    this.helper = helper;457  }458459  sleep(milliseconds: number) {460    return new Promise((resolve) => setTimeout(resolve, milliseconds));461  }462463  private async waitWithTimeout(promise: Promise<any>, timeout: number) {464    let isBlock = false;465    promise.then(() => isBlock = true).catch(() => isBlock = true);466    let totalTime = 0;467    const step = 100;468    while(!isBlock) {469      await this.sleep(step);470      totalTime += step;471      if(totalTime >= timeout) throw Error('Blocks production failed');472    }473    return promise;474  }475476  /**477   * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.478   * @param promise async operation to race against the timeout479   * @param timeoutMS time after which to time out480   * @param timeoutError error message to throw481   * @returns promise of the same type the operation had482   */483  withTimeout<T>(484    promise: Promise<T>,485    timeoutMS = 30000,486    timeoutError = 'The operation has timed out!',487  ): Promise<T> {488    const timeout = new Promise<never>((_, reject) => {489      setTimeout(() => {490        reject(new Error(timeoutError));491      }, timeoutMS);492    });493494    return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});495  }496497  /**498   * Wait for specified number of blocks499   * @param blocksCount number of blocks to wait500   * @returns501   */502  async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {503    timeout = timeout ?? blocksCount * 60_000;504    // eslint-disable-next-line no-async-promise-executor505    const promise = new Promise<void>(async (resolve) => {506      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {507        if (blocksCount > 0) {508          blocksCount--;509        } else {510          unsubscribe();511          resolve();512        }513      });514    });515    await this.waitWithTimeout(promise, timeout);516    return promise;517  }518519  /**520   * Wait for the specified number of sessions to pass.521   * Only applicable if the Session pallet is turned on.522   * @param sessionCount number of sessions to wait523   * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks524   * @returns525   */526  async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {527    console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`528      + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');529530    const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;531    let currentSessionIndex = -1;532533    while (currentSessionIndex < expectedSessionIndex) {534      // eslint-disable-next-line no-async-promise-executor535      currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {536        await this.newBlocks(1);537        const res = await (this.helper as DevUniqueHelper).session.getIndex();538        resolve(res);539      }), blockTimeout, 'The chain has stopped producing blocks!');540    }541  }542543  async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {544    timeout = timeout ?? 30 * 60 * 1000;545    // eslint-disable-next-line no-async-promise-executor546    const promise = new Promise<void>(async (resolve) => {547      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {548        if (data.number.toNumber() >= blockNumber) {549          unsubscribe();550          resolve();551        }552      });553    });554    await this.waitWithTimeout(promise, timeout);555    return promise;556  }557558  async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {559    timeout = timeout ?? 30 * 60 * 1000;560    // eslint-disable-next-line no-async-promise-executor561    const promise = new Promise<void>(async (resolve) => {562      const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {563        if (data.value.relayParentNumber.toNumber() >= blockNumber) {564          // @ts-ignore565          unsubscribe();566          resolve();567        }568      });569    });570    await this.waitWithTimeout(promise, timeout);571    return promise;572  }573574  noScheduledTasks() {575    const api = this.helper.getApi();576577    // eslint-disable-next-line no-async-promise-executor578    const promise = new Promise<void>(async resolve => {579      const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {580        const areThereScheduledTasks = await api.query.scheduler.lookup.entries();581582        if(areThereScheduledTasks.length == 0) {583          unsubscribe();584          resolve();585        }586      });587    });588589    return promise;590  }591592  event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {593    // eslint-disable-next-line no-async-promise-executor594    const promise = new Promise<EventRecord | null>(async (resolve) => {595      const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {596        const blockNumber = header.number.toHuman();597        const blockHash = header.hash;598        const eventIdStr = `${eventSection}.${eventMethod}`;599        const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;600601        this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);602603        const apiAt = await this.helper.getApi().at(blockHash);604        const eventRecords = (await apiAt.query.system.events()) as any;605606        const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {607          return r.event.section == eventSection && r.event.method == eventMethod;608        });609610        if (neededEvent) {611          unsubscribe();612          resolve(neededEvent);613        } else if (maxBlocksToWait > 0) {614          maxBlocksToWait--;615        } else {616          this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);617          unsubscribe();618          resolve(null);619        }620      });621    });622    return promise;623  }624}625626class SessionGroup {627  helper: ChainHelperBase;628629  constructor(helper: ChainHelperBase) {630    this.helper = helper;631  }632633  //todo:collator documentation634  async getIndex(): Promise<number> {635    return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();636  }637638  newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {639    return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);640  }641642  setOwnKeys(signer: TSigner, key: string) {643    return this.helper.executeExtrinsic(644      signer,645      'api.tx.session.setKeys',646      [key, '0x0'],647      true,648    );649  }650651  setOwnKeysFromAddress(signer: TSigner) {652    return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));653  }654}655656class TestUtilGroup {657  helper: DevUniqueHelper;658659  constructor(helper: DevUniqueHelper) {660    this.helper = helper;661  }662663  async enable() {664    if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {665      return;666    }667668    const signer = this.helper.util.fromSeed('//Alice');669    await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);670  }671672  async setTestValue(signer: TSigner, testVal: number) {673    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);674  }675676  async incTestValue(signer: TSigner) {677    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);678  }679680  async setTestValueAndRollback(signer: TSigner, testVal: number) {681    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);682  }683684  async testValue(blockIdx?: number) {685    const api = blockIdx686      ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))687      : this.helper.getApi();688689    return (await api.query.testUtils.testValue()).toJSON();690  }691692  async justTakeFee(signer: TSigner) {693    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);694  }695696  async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {697    await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);698  }699}700701class EventCapture {702  helper: DevUniqueHelper;703  eventSection: string;704  eventMethod: string;705  events: EventRecord[] = [];706  unsubscribe: VoidFn | null = null;707708  constructor(709    helper: DevUniqueHelper,710    eventSection: string,711    eventMethod: string,712  ) {713    this.helper = helper;714    this.eventSection = eventSection;715    this.eventMethod = eventMethod;716  }717718  async startCapture() {719    this.stopCapture();720    this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {721      const newEvents = eventRecords.filter(r => {722        return r.event.section == this.eventSection && r.event.method == this.eventMethod;723      });724725      this.events.push(...newEvents);726    })) as any;727  }728729  stopCapture() {730    if (this.unsubscribe !== null) {731      this.unsubscribe();732    }733  }734735  extractCapturedEvents() {736    return this.events;737  }738}739740class AdminGroup {741  helper: UniqueHelper;742743  constructor(helper: UniqueHelper) {744    this.helper = helper;745  }746747  async payoutStakers(signer: IKeyringPair, stakersToPayout: number):  Promise<{staker: string, stake: bigint, payout: bigint}[]> {748    const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);749    return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {750      return {751        staker: e.event.data[0].toString(),752        stake: e.event.data[1].toBigInt(),753        payout: e.event.data[2].toBigInt(),754      };755    });756  }757}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -3243,6 +3243,26 @@
   }
 }
 
+export class AstarHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<AstarHelper>;
+  assets: AssetsGroup<AstarHelper>;
+  xcm: XcmGroup<AstarHelper>;
+
+  constructor(logger?: ILogger, options: {[key: string]: any} = {}) {
+    super(logger, options.helperBase ?? AstarHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.assets = new AssetsGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+  }
+
+  getSudo<T extends UniqueHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
+  }
+}
+
 export class AcalaHelper extends XcmChainHelper {
   balance: SubstrateBalanceGroup<AcalaHelper>;
   assetRegistry: AcalaAssetRegistryGroup;
modifiedtests/src/xcm/xcmUnique.test.tsdiffbeforeafterboth
--- a/tests/src/xcm/xcmUnique.test.ts
+++ b/tests/src/xcm/xcmUnique.test.ts
@@ -18,12 +18,13 @@
 import {blake2AsHex} from '@polkadot/util-crypto';
 import config from '../config';
 import {XcmV2TraitsError} from '../interfaces';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds} from '../util';
+import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds} from '../util';
 
 const UNIQUE_CHAIN = 2037;
 const STATEMINT_CHAIN = 1000;
 const ACALA_CHAIN = 2000;
 const MOONBEAM_CHAIN = 2004;
+const ASTAR_CHAIN = 2006;
 
 const STATEMINT_PALLET_INSTANCE = 50;
 
@@ -31,6 +32,7 @@
 const statemintUrl = config.statemintUrl;
 const acalaUrl = config.acalaUrl;
 const moonbeamUrl = config.moonbeamUrl;
+const astarUrl = config.astarUrl;
 
 const RELAY_DECIMALS = 12;
 const STATEMINT_DECIMALS = 12;
@@ -980,3 +982,189 @@
     expect(unqFees == 0n).to.be.true;
   });
 });
+
+describeXCM('[XCM] Integration test: Exchanging tokens with Astar', () => {
+  let alice: IKeyringPair;
+  let randomAccount: IKeyringPair;
+
+  const astarInitialBalance = 1n * (10n ** 18n);
+  const unqToAstarAmount = 10n * (10n ** 18n);
+
+  before(async () => {
+    await usingPlaygrounds(async (helper, privateKey) => {
+      alice = await privateKey('//Alice');
+      [randomAccount] = await helper.arrange.createAccounts([100n], alice);
+      console.log('randomAccount', randomAccount.address);
+    });
+
+    await usingAstarPlaygrounds(astarUrl, async (helper) => {
+      console.log('1. Create foreign asset and metadata');
+      await helper.assets.create(
+        alice,
+        1,
+        alice.address,
+        1n, // TODO set correct minimal balance
+      );
+
+      await helper.assets.setMetadata(
+        alice,
+        1,
+        'Cross chain UNQ',
+        'xcUNQ',
+        18,
+      );
+
+      console.log('2. Register asset location');
+      const assetLocation = {
+        V1: {
+          parents: 1,
+          interior: {
+            X1: {
+              Parachain: UNIQUE_CHAIN,
+            },
+          },
+        },
+      };
+
+      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.registerAssetLocation', [assetLocation, 1]);
+
+      console.log('3. Set payment for computation');
+      // TODO this is Phala's price, what price will be for Unique?
+      const unitsPerSecond = 228_000_000_000n;
+      await helper.getSudo().executeExtrinsic(alice, 'api.tx.xcAssetConfig.setAssetUnitsPerSecond', [assetLocation, unitsPerSecond]);
+
+      console.log('4. Transfer 1 ASTAR for recepient');
+      await helper.balance.transferToSubstrate(alice, randomAccount.address, astarInitialBalance);
+    });
+  });
+
+  itSub.only('Should connect and send UNQ to Astar', async ({helper}) => {
+    const destination = {
+      V1: {
+        parents: 1,
+        interior: {
+          X1: {
+            Parachain: ASTAR_CHAIN,
+          },
+        },
+      },
+    };
+
+    const beneficiary = {
+      V1: {
+        parents: 0,
+        interior: {
+          X1: {
+            AccountId32: {
+              network: 'Any',
+              id: randomAccount.addressRaw,
+            },
+          },
+        },
+      },
+    };
+
+    const assets = {
+      V1: [
+        {
+          id: {
+            Concrete: {
+              parents: 0,
+              interior: 'Here',
+            },
+          },
+          fun: {
+            Fungible: unqToAstarAmount,
+          },
+        },
+      ],
+    };
+
+    // Initial balance is 100 UNQ
+    expect(await helper.balance.getSubstrate(randomAccount.address)).to.eq(100n * (10n ** 18n));
+
+    const feeAssetItem = 0;
+    await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
+
+    // Balance after reserve transfer is less than 90
+    expect(await helper.balance.getSubstrate(randomAccount.address)).to.eq(89_941967662676666465n);
+
+    await usingAstarPlaygrounds(astarUrl, async (helper) => {
+      await helper.wait.newBlocks(3);
+      const xcUNQbalance = await helper.assets.account(1, randomAccount.address);
+      const astarBalance = await helper.balance.getSubstrate(randomAccount.address);
+
+      expect(xcUNQbalance).to.eq(9_999_999_999_088_000_000n);
+      // Astar balance does not changed
+      expect(astarBalance).to.eq(1_000_000_000_000_000_000n);
+    });
+  });
+
+  itSub.only('Should connect to Astar and send UNQ back', async ({helper}) => {
+    await usingAstarPlaygrounds(astarUrl, async (helper) => {
+      const destination = {
+        V1: {
+          parents: 1,
+          interior: {
+            X1: {
+              Parachain: UNIQUE_CHAIN,
+            },
+          },
+        },
+      };
+
+      const beneficiary = {
+        V1: {
+          parents: 0,
+          interior: {
+            X1: {
+              AccountId32: {
+                network: 'Any',
+                id: randomAccount.addressRaw,
+              },
+            },
+          },
+        },
+      };
+
+      const assets = {
+        V1: [
+          {
+            id: {
+              Concrete: {
+                parents: 1,
+                interior: {
+                  X1: {
+                    Parachain: UNIQUE_CHAIN,
+                  },
+                },
+              },
+            },
+            fun: {
+              Fungible: 5_000_000_000_000_000_000n, // TODO set another value
+            },
+          },
+        ],
+      };
+
+      // Initial balance is 1 ASTAR
+      expect(await helper.balance.getSubstrate(randomAccount.address)).to.eq(1_000_000_000_000_000_000n);
+
+      const feeAssetItem = 0;
+      await helper.xcm.limitedReserveTransferAssets(randomAccount, destination, beneficiary, assets, feeAssetItem, 'Unlimited');
+
+      // Balance after reserve transfer is less than 1 ASTAR
+      const xcUNQbalance = await helper.assets.account(1, randomAccount.address);
+      const balanceAstar = await helper.balance.getSubstrate(randomAccount.address);
+
+      // xcUNQ balance decreased
+      expect(xcUNQbalance).to.eq(4_999_999_999_088_000_000n);
+      // Astar balance is 0.997...
+      expect(balanceAstar / (10n ** 15n)).to.eq(997n);
+    });
+
+    await helper.wait.newBlocks(3);
+    const balanceUNQ = await helper.balance.getSubstrate(randomAccount.address);
+    expect(balanceUNQ).to.eq(89_941967662676666465n + 5_000_000_000_000_000_000n);
+  });
+});