difftreelog
test added CI for the Polkadex network
in: master
7 files changed
tests/.mocha.envdiffbeforeafterboth--- /dev/null
+++ b/tests/.mocha.env
@@ -0,0 +1,20 @@
+RELAY_ACALA_ID=1002
+RELAY_ASTAR_ID=1005
+RELAY_MOONBEAM_ID=1003
+RELAY_POLKADEX_ID=1006
+RELAY_STATEMINT_ID=1004
+RELAY_UNIQUE_ID=1001
+RELAY_HTTP_URL=http://127.0.0.1:9699/relay/
+RELAY_ACALA_HTTP_URL=http://127.0.0.1:9699/relay-acala/
+RELAY_ASTAR_HTTP_URL=http://127.0.0.1:9699/relay-astar/
+RELAY_MOONBEAM_HTTP_URL=http://127.0.0.1:9699/relay-moonbeam/
+RELAY_POLKADEX_HTTP_URL=http://127.0.0.1:9699/relay-polkadex/
+RELAY_STATEMINT_HTTP_URL=http://127.0.0.1:9699/relay-statemint/
+RELAY_UNIQUE_HTTP_URL=http://127.0.0.1:9699/relay-unique/
+RELAY_URL=ws://127.0.0.1:9699/relay/
+RELAY_ACALA_URL=ws://127.0.0.1:9699/relay-acala/
+RELAY_ASTAR_URL=ws://127.0.0.1:9699/relay-astar/
+RELAY_MOONBEAM_URL=ws://127.0.0.1:9699/relay-moonbeam/
+RELAY_POLKADEX_URL=ws://127.0.0.1:9699/relay-polkadex/
+RELAY_STATEMINT_URL=ws://127.0.0.1:9699/relay-statemint/
+RELAY_UNIQUE_URL=ws://127.0.0.1:9699/relay-unique/
tests/.vscode/settings.jsondiffbeforeafterboth--- a/tests/.vscode/settings.json
+++ b/tests/.vscode/settings.json
@@ -1,6 +1,10 @@
{
- "mocha.enabled": true,
- "mochaExplorer.files": "**/*.test.ts",
+ "mochaExplorer.env": {
+ "RUN_GOV_TESTS": "1",
+ "RUN_XCM_TESTS": "1"
+ },
+ "mochaExplorer.files": "src/**/*.test.ts",
+ "mochaExplorer.envPath": ".mocha.env",
"mochaExplorer.require": "ts-node/register",
"eslint.format.enable": true,
"[javascript]": {
tests/src/config.tsdiffbeforeafterboth--- a/tests/src/config.ts
+++ b/tests/src/config.ts
@@ -28,6 +28,7 @@
westmintUrl: process.env.RELAY_WESTMINT_URL || 'ws://127.0.0.1:9948',
statemineUrl: process.env.RELAY_STATEMINE_URL || 'ws://127.0.0.1:9948',
statemintUrl: process.env.RELAY_STATEMINT_URL || 'ws://127.0.0.1:9948',
+ polkadexUrl: process.env.RELAY_POLKADEX_URL || 'ws://127.0.0.1:9950',
};
export default config;
tests/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, DevAstarHelper, DevShidenHelper} from './playgrounds/unique.dev';
+import {DevUniqueHelper, SilentLogger, SilentConsole, DevMoonbeamHelper, DevMoonriverHelper, DevAcalaHelper, DevKaruraHelper, DevRelayHelper, DevWestmintHelper, DevStatemineHelper, DevStatemintHelper, DevAstarHelper, DevShidenHelper, DevPolkadexHelper} from './playgrounds/unique.dev';
import {dirname} from 'path';
import {fileURLToPath} from 'url';
@@ -89,6 +89,8 @@
export const usingShidenPlaygrounds = (url: string, code: (helper: DevShidenHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevShidenHelper>(DevShidenHelper, url, code);
+export const usingPolkadexPlaygrounds = (url: string, code: (helper: DevPolkadexHelper, privateKey: (seed: string) => Promise<IKeyringPair>) => Promise<void>) => usingPlaygroundsGeneral<DevPolkadexHelper>(DevPolkadexHelper, url, code);
+
export const MINIMUM_DONOR_FUND = 4_000_000n;
export const DONOR_FUNDING = 4_000_000n;
tests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, IPovInfo, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';1617export class SilentLogger {18 log(_msg: any, _level: any): void { }19 level = {20 ERROR: 'ERROR' as const,21 WARNING: 'WARNING' as const,22 INFO: 'INFO' as const,23 };24}2526export class SilentConsole {27 // TODO: Remove, this is temporary: Filter unneeded API output28 // (Jaco promised it will be removed in the next version)29 consoleErr: any;30 consoleLog: any;31 consoleWarn: any;3233 constructor() {34 this.consoleErr = console.error;35 this.consoleLog = console.log;36 this.consoleWarn = console.warn;37 }3839 enable() {40 const outFn = (printer: any) => (...args: any[]) => {41 for(const arg of args) {42 if(typeof arg !== 'string')43 continue;44 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'];45 const needToSkip = skippedWarnings.reduce((a, b) => a || arg.includes(b), false);46 if(needToSkip || arg === 'Normal connection closure')47 return;48 }49 printer(...args);50 };5152 console.error = outFn(this.consoleErr.bind(console));53 console.log = outFn(this.consoleLog.bind(console));54 console.warn = outFn(this.consoleWarn.bind(console));55 }5657 disable() {58 console.error = this.consoleErr;59 console.log = this.consoleLog;60 console.warn = this.consoleWarn;61 }62}6364export interface IEventHelper {65 section(): string;6667 method(): string;6869 wrapEvent(data: any[]): any;70}7172// eslint-disable-next-line @typescript-eslint/naming-convention73function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {74 const helperClass = class implements IEventHelper {75 wrapEvent: (data: any[]) => any;76 _section: string;77 _method: string;7879 constructor() {80 this.wrapEvent = wrapEvent;81 this._section = section;82 this._method = method;83 }8485 section(): string {86 return this._section;87 }8889 method(): string {90 return this._method;91 }9293 filter(txres: ITransactionResult) {94 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)95 .map(e => this.wrapEvent(e.event.data));96 }9798 find(txres: ITransactionResult) {99 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);100 return e ? this.wrapEvent(e.event.data) : null;101 }102103 expect(txres: ITransactionResult) {104 const e = this.find(txres);105 if(e) {106 return e;107 } else {108 throw Error(`Expected event ${section}.${method}`);109 }110 }111 };112113 return helperClass;114}115116function eventJsonData<T = any>(data: any[], index: number) {117 return data[index].toJSON() as T;118}119120function eventHumanData(data: any[], index: number) {121 return data[index].toHuman();122}123124function eventData<T = any>(data: any[], index: number) {125 return data[index] as T;126}127128// eslint-disable-next-line @typescript-eslint/naming-convention129function EventSection(section: string) {130 return class Section {131 static section = section;132133 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {134 const helperClass = EventHelper(Section.section, name, wrapEvent);135 return new helperClass();136 }137 };138}139140function schedulerSection(schedulerInstance: string) {141 return class extends EventSection(schedulerInstance) {142 static Dispatched = this.Method('Dispatched', data => ({143 task: eventJsonData(data, 0),144 id: eventHumanData(data, 1),145 result: data[2],146 }));147148 static PriorityChanged = this.Method('PriorityChanged', data => ({149 task: eventJsonData(data, 0),150 priority: eventJsonData(data, 1),151 }));152 };153}154155export class Event {156 static Democracy = class extends EventSection('democracy') {157 static Proposed = this.Method('Proposed', data => ({158 proposalIndex: eventJsonData<number>(data, 0),159 }));160161 static ExternalTabled = this.Method('ExternalTabled');162163 static Started = this.Method('Started', data => ({164 referendumIndex: eventJsonData<number>(data, 0),165 threshold: eventHumanData(data, 1),166 }));167168 static Voted = this.Method('Voted', data => ({169 voter: eventJsonData(data, 0),170 referendumIndex: eventJsonData<number>(data, 1),171 vote: eventJsonData(data, 2),172 }));173174 static Passed = this.Method('Passed', data => ({175 referendumIndex: eventJsonData<number>(data, 0),176 }));177178 static ProposalCanceled = this.Method('ProposalCanceled', data => ({179 propIndex: eventJsonData<number>(data, 0),180 }));181182 static Cancelled = this.Method('Cancelled', data => ({183 propIndex: eventJsonData<number>(data, 0),184 }));185186 static Vetoed = this.Method('Vetoed', data => ({187 who: eventHumanData(data, 0),188 proposalHash: eventHumanData(data, 1),189 until: eventJsonData<number>(data, 1),190 }));191 };192193 static Council = class extends EventSection('council') {194 static Proposed = this.Method('Proposed', data => ({195 account: eventHumanData(data, 0),196 proposalIndex: eventJsonData<number>(data, 1),197 proposalHash: eventHumanData(data, 2),198 threshold: eventJsonData<number>(data, 3),199 }));200 static Closed = this.Method('Closed', data => ({201 proposalHash: eventHumanData(data, 0),202 yes: eventJsonData<number>(data, 1),203 no: eventJsonData<number>(data, 2),204 }));205 static Executed = this.Method('Executed', data => ({206 proposalHash: eventHumanData(data, 0),207 }));208 };209210 static TechnicalCommittee = class extends EventSection('technicalCommittee') {211 static Proposed = this.Method('Proposed', data => ({212 account: eventHumanData(data, 0),213 proposalIndex: eventJsonData<number>(data, 1),214 proposalHash: eventHumanData(data, 2),215 threshold: eventJsonData<number>(data, 3),216 }));217 static Closed = this.Method('Closed', data => ({218 proposalHash: eventHumanData(data, 0),219 yes: eventJsonData<number>(data, 1),220 no: eventJsonData<number>(data, 2),221 }));222 static Approved = this.Method('Approved', data => ({223 proposalHash: eventHumanData(data, 0),224 }));225 static Executed = this.Method('Executed', data => ({226 proposalHash: eventHumanData(data, 0),227 result: eventHumanData(data, 1),228 }));229 };230231 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {232 static Submitted = this.Method('Submitted', data => ({233 referendumIndex: eventJsonData<number>(data, 0),234 trackId: eventJsonData<number>(data, 1),235 proposal: eventJsonData(data, 2),236 }));237238 static Cancelled = this.Method('Cancelled', data => ({239 index: eventJsonData<number>(data, 0),240 tally: eventJsonData(data, 1),241 }));242 };243244 static UniqueScheduler = schedulerSection('uniqueScheduler');245 static Scheduler = schedulerSection('scheduler');246247 static XcmpQueue = class extends EventSection('xcmpQueue') {248 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({249 messageHash: eventJsonData(data, 0),250 }));251252 static Fail = this.Method('Fail', data => ({253 messageHash: eventJsonData(data, 0),254 outcome: eventData<XcmV2TraitsError>(data, 1),255 }));256 };257}258259export class DevUniqueHelper extends UniqueHelper {260 /**261 * Arrange methods for tests262 */263 arrange: ArrangeGroup;264 wait: WaitGroup;265 admin: AdminGroup;266 session: SessionGroup;267 testUtils: TestUtilGroup;268269 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {270 options.helperBase = options.helperBase ?? DevUniqueHelper;271272 super(logger, options);273 this.arrange = new ArrangeGroup(this);274 this.wait = new WaitGroup(this);275 this.admin = new AdminGroup(this);276 this.testUtils = new TestUtilGroup(this);277 this.session = new SessionGroup(this);278 }279280 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {281 if(!wsEndpoint) throw new Error('wsEndpoint was not set');282 const wsProvider = new WsProvider(wsEndpoint);283 this.api = new ApiPromise({284 provider: wsProvider,285 signedExtensions: {286 ContractHelpers: {287 extrinsic: {},288 payload: {},289 },290 CheckMaintenance: {291 extrinsic: {},292 payload: {},293 },294 DisableIdentityCalls: {295 extrinsic: {},296 payload: {},297 },298 FakeTransactionFinalizer: {299 extrinsic: {},300 payload: {},301 },302 },303 rpc: {304 unique: defs.unique.rpc,305 appPromotion: defs.appPromotion.rpc,306 povinfo: defs.povinfo.rpc,307 eth: {308 feeHistory: {309 description: 'Dummy',310 params: [],311 type: 'u8',312 },313 maxPriorityFeePerGas: {314 description: 'Dummy',315 params: [],316 type: 'u8',317 },318 },319 },320 });321 await this.api.isReadyOrError;322 this.network = await UniqueHelper.detectNetwork(this.api);323 this.wsEndpoint = wsEndpoint;324 }325}326327export class DevRelayHelper extends RelayHelper {328 wait: WaitGroup;329330 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {331 options.helperBase = options.helperBase ?? DevRelayHelper;332333 super(logger, options);334 this.wait = new WaitGroup(this);335 }336}337338export class DevWestmintHelper extends WestmintHelper {339 wait: WaitGroup;340341 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {342 options.helperBase = options.helperBase ?? DevWestmintHelper;343344 super(logger, options);345 this.wait = new WaitGroup(this);346 }347}348349export class DevStatemineHelper extends DevWestmintHelper {}350351export class DevStatemintHelper extends DevWestmintHelper {}352353export class DevMoonbeamHelper extends MoonbeamHelper {354 account: MoonbeamAccountGroup;355 wait: WaitGroup;356 fastDemocracy: MoonbeamFastDemocracyGroup;357358 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {359 options.helperBase = options.helperBase ?? DevMoonbeamHelper;360 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';361362 super(logger, options);363 this.account = new MoonbeamAccountGroup(this);364 this.wait = new WaitGroup(this);365 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);366 }367}368369export class DevMoonriverHelper extends DevMoonbeamHelper {370 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {371 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';372 super(logger, options);373 }374}375376export class DevAstarHelper extends AstarHelper {377 wait: WaitGroup;378379 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {380 options.helperBase = options.helperBase ?? DevAstarHelper;381382 super(logger, options);383 this.wait = new WaitGroup(this);384 }385}386387export class DevShidenHelper extends AstarHelper {388 wait: WaitGroup;389390 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {391 options.helperBase = options.helperBase ?? DevShidenHelper;392393 super(logger, options);394 this.wait = new WaitGroup(this);395 }396}397398export class DevAcalaHelper extends AcalaHelper {399 wait: WaitGroup;400401 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {402 options.helperBase = options.helperBase ?? DevAcalaHelper;403404 super(logger, options);405 this.wait = new WaitGroup(this);406 }407}408409export class DevKaruraHelper extends DevAcalaHelper {}410411export class ArrangeGroup {412 helper: DevUniqueHelper;413414 scheduledIdSlider = 0;415416 constructor(helper: DevUniqueHelper) {417 this.helper = helper;418 }419420 /**421 * Generates accounts with the specified UNQ token balance422 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.423 * @param donor donor account for balances424 * @returns array of newly created accounts425 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);426 */427 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {428 let nonce = await this.helper.chain.getNonce(donor.address);429 const wait = new WaitGroup(this.helper);430 const ss58Format = this.helper.chain.getChainProperties().ss58Format;431 const tokenNominal = this.helper.balance.getOneTokenNominal();432 const transactions = [];433 const accounts: IKeyringPair[] = [];434 for(const balance of balances) {435 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);436 accounts.push(recipient);437 if(balance !== 0n) {438 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);439 transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));440 nonce++;441 }442 }443444 await Promise.all(transactions).catch(_e => {});445446 //#region TODO remove this region, when nonce problem will be solved447 const checkBalances = async () => {448 let isSuccess = true;449 for(let i = 0; i < balances.length; i++) {450 const balance = await this.helper.balance.getSubstrate(accounts[i].address);451 if(balance !== balances[i] * tokenNominal) {452 isSuccess = false;453 break;454 }455 }456 return isSuccess;457 };458459 let accountsCreated = false;460 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;461 // checkBalances retry up to 5-50 blocks462 for(let index = 0; index < maxBlocksChecked; index++) {463 accountsCreated = await checkBalances();464 if(accountsCreated) break;465 await wait.newBlocks(1);466 }467468 if(!accountsCreated) throw Error('Accounts generation failed');469 //#endregion470471 return accounts;472 };473474 // TODO combine this method and createAccounts into one475 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {476 const createAsManyAsCan = async () => {477 let transactions: any = [];478 const accounts: IKeyringPair[] = [];479 let nonce = await this.helper.chain.getNonce(donor.address);480 const tokenNominal = this.helper.balance.getOneTokenNominal();481 const ss58Format = this.helper.chain.getChainProperties().ss58Format;482 for(let i = 0; i < accountsToCreate; i++) {483 if(i === 500) { // if there are too many accounts to create484 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled485 transactions = []; //486 nonce = await this.helper.chain.getNonce(donor.address); // update nonce487 }488 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);489 accounts.push(recipient);490 if(withBalance !== 0n) {491 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);492 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));493 nonce++;494 }495 }496497 const fullfilledAccounts = [];498 await Promise.allSettled(transactions);499 for(const account of accounts) {500 const accountBalance = await this.helper.balance.getSubstrate(account.address);501 if(accountBalance === withBalance * tokenNominal) {502 fullfilledAccounts.push(account);503 }504 }505 return fullfilledAccounts;506 };507508509 const crowd: IKeyringPair[] = [];510 // do up to 5 retries511 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {512 const asManyAsCan = await createAsManyAsCan();513 crowd.push(...asManyAsCan);514 accountsToCreate -= asManyAsCan.length;515 }516517 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);518519 return crowd;520 };521522 /**523 * Generates one account with zero balance524 * @returns the newly generated account525 * @example const account = await helper.arrange.createEmptyAccount();526 */527 createEmptyAccount = (): IKeyringPair => {528 const ss58Format = this.helper.chain.getChainProperties().ss58Format;529 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);530 };531532 isDevNode = async () => {533 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();534 if(blockNumber == 0) {535 await this.helper.wait.newBlocks(1);536 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();537 }538 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);539 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);540 const findCreationDate = (block: any) => {541 const humanBlock = block.toHuman();542 let date;543 humanBlock.block.extrinsics.forEach((ext: any) => {544 if(ext.method.section === 'timestamp') {545 date = Number(ext.method.args.now.replaceAll(',', ''));546 }547 });548 return date;549 };550 const block1date = await findCreationDate(block1);551 const block2date = await findCreationDate(block2);552 if(block2date! - block1date! < 9000) return true;553 };554555 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {556 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);557 let balance = await this.helper.balance.getSubstrate(address);558559 await promise();560561 balance -= await this.helper.balance.getSubstrate(address);562563 return balance;564 }565566 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {567 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);568569 const kvJson: {[key: string]: string} = {};570571 for(const kv of rawPovInfo.keyValues) {572 kvJson[kv.key.toHex()] = kv.value.toHex();573 }574575 const kvStr = JSON.stringify(kvJson);576577 const chainql = spawnSync(578 'chainql',579 [580 `--tla-code=data=${kvStr}`,581 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,582 ],583 );584585 if(!chainql.stdout) {586 throw Error('unable to get an output from the `chainql`');587 }588589 return {590 proofSize: rawPovInfo.proofSize.toNumber(),591 compactProofSize: rawPovInfo.compactProofSize.toNumber(),592 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),593 results: rawPovInfo.results,594 kv: JSON.parse(chainql.stdout.toString()),595 };596 }597598 calculatePalletAddress(palletId: any) {599 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));600 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);601 }602603 makeScheduledIds(num: number): string[] {604 function makeId(slider: number) {605 const scheduledIdSize = 64;606 const hexId = slider.toString(16);607 const prefixSize = scheduledIdSize - hexId.length;608609 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;610611 return scheduledId;612 }613614 const ids = [];615 for(let i = 0; i < num; i++) {616 ids.push(makeId(this.scheduledIdSlider));617 this.scheduledIdSlider += 1;618 }619620 return ids;621 }622623 makeScheduledId(): string {624 return (this.makeScheduledIds(1))[0];625 }626627 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {628 const capture = new EventCapture(this.helper, eventSection, eventMethod);629 await capture.startCapture();630631 return capture;632 }633634 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {635 return {636 V2: [637 {638 WithdrawAsset: [639 {640 id,641 fun: {642 Fungible: amount,643 },644 },645 ],646 },647 {648 BuyExecution: {649 fees: {650 id,651 fun: {652 Fungible: amount,653 },654 },655 weightLimit: 'Unlimited',656 },657 },658 {659 DepositAsset: {660 assets: {661 Wild: 'All',662 },663 maxAssets: 1,664 beneficiary: {665 parents: 0,666 interior: {667 X1: {668 AccountId32: {669 network: 'Any',670 id: beneficiary,671 },672 },673 },674 },675 },676 },677 ],678 };679 }680681 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {682 return {683 V2: [684 {685 ReserveAssetDeposited: [686 {687 id,688 fun: {689 Fungible: amount,690 },691 },692 ],693 },694 {695 BuyExecution: {696 fees: {697 id,698 fun: {699 Fungible: amount,700 },701 },702 weightLimit: 'Unlimited',703 },704 },705 {706 DepositAsset: {707 assets: {708 Wild: 'All',709 },710 maxAssets: 1,711 beneficiary: {712 parents: 0,713 interior: {714 X1: {715 AccountId32: {716 network: 'Any',717 id: beneficiary,718 },719 },720 },721 },722 },723 },724 ],725 };726 }727}728729class MoonbeamAccountGroup {730 helper: MoonbeamHelper;731732 keyring: Keyring;733 _alithAccount: IKeyringPair;734 _baltatharAccount: IKeyringPair;735 _dorothyAccount: IKeyringPair;736737 constructor(helper: MoonbeamHelper) {738 this.helper = helper;739740 this.keyring = new Keyring({type: 'ethereum'});741 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';742 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';743 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';744745 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');746 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');747 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');748 }749750 alithAccount() {751 return this._alithAccount;752 }753754 baltatharAccount() {755 return this._baltatharAccount;756 }757758 dorothyAccount() {759 return this._dorothyAccount;760 }761762 create() {763 return this.keyring.addFromUri(mnemonicGenerate());764 }765}766767class MoonbeamFastDemocracyGroup {768 helper: DevMoonbeamHelper;769770 constructor(helper: DevMoonbeamHelper) {771 this.helper = helper;772 }773774 async executeProposal(proposalDesciption: string, encodedProposal: string) {775 const proposalHash = blake2AsHex(encodedProposal);776777 const alithAccount = this.helper.account.alithAccount();778 const baltatharAccount = this.helper.account.baltatharAccount();779 const dorothyAccount = this.helper.account.dorothyAccount();780781 const councilVotingThreshold = 2;782 const technicalCommitteeThreshold = 2;783 const fastTrackVotingPeriod = 3;784 const fastTrackDelayPeriod = 0;785786 console.log(`[democracy] executing '${proposalDesciption}' proposal`);787788 // >>> Propose external motion through council >>>789 console.log('\t* Propose external motion through council.......');790 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});791 const encodedMotion = externalMotion?.method.toHex() || '';792 const motionHash = blake2AsHex(encodedMotion);793 console.log('\t* Motion hash is %s', motionHash);794795 await this.helper.collective.council.propose(796 baltatharAccount,797 councilVotingThreshold,798 externalMotion,799 externalMotion.encodedLength,800 );801802 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;803 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);804 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);805806 await this.helper.collective.council.close(807 dorothyAccount,808 motionHash,809 councilProposalIdx,810 {811 refTime: 1_000_000_000,812 proofSize: 1_000_000,813 },814 externalMotion.encodedLength,815 );816 console.log('\t* Propose external motion through council.......DONE');817 // <<< Propose external motion through council <<<818819 // >>> Fast track proposal through technical committee >>>820 console.log('\t* Fast track proposal through technical committee.......');821 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);822 const encodedFastTrack = fastTrack?.method.toHex() || '';823 const fastTrackHash = blake2AsHex(encodedFastTrack);824 console.log('\t* FastTrack hash is %s', fastTrackHash);825826 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);827828 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;829 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);830 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);831832 await this.helper.collective.techCommittee.close(833 baltatharAccount,834 fastTrackHash,835 techProposalIdx,836 {837 refTime: 1_000_000_000,838 proofSize: 1_000_000,839 },840 fastTrack.encodedLength,841 );842 console.log('\t* Fast track proposal through technical committee.......DONE');843 // <<< Fast track proposal through technical committee <<<844845 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);846 const referendumIndex = democracyStarted.referendumIndex;847848 // >>> Referendum voting >>>849 console.log(`\t* Referendum #${referendumIndex} voting.......`);850 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {851 balance: 10_000_000_000_000_000_000n,852 vote: {aye: true, conviction: 1},853 });854 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);855 // <<< Referendum voting <<<856857 // Wait the proposal to pass858 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);859860 await this.helper.wait.newBlocks(1);861862 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);863 }864}865866class WaitGroup {867 helper: ChainHelperBase;868869 constructor(helper: ChainHelperBase) {870 this.helper = helper;871 }872873 sleep(milliseconds: number) {874 return new Promise((resolve) => setTimeout(resolve, milliseconds));875 }876877 private async waitWithTimeout(promise: Promise<any>, timeout: number) {878 let isBlock = false;879 promise.then(() => isBlock = true).catch(() => isBlock = true);880 let totalTime = 0;881 const step = 100;882 while(!isBlock) {883 await this.sleep(step);884 totalTime += step;885 if(totalTime >= timeout) throw Error('Blocks production failed');886 }887 return promise;888 }889890 /**891 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.892 * @param promise async operation to race against the timeout893 * @param timeoutMS time after which to time out894 * @param timeoutError error message to throw895 * @returns promise of the same type the operation had896 */897 withTimeout<T>(898 promise: Promise<T>,899 timeoutMS = 30000,900 timeoutError = 'The operation has timed out!',901 ): Promise<T> {902 const timeout = new Promise<never>((_, reject) => {903 setTimeout(() => {904 reject(new Error(timeoutError));905 }, timeoutMS);906 });907908 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});909 }910911 /**912 * Wait for specified number of blocks913 * @param blocksCount number of blocks to wait914 * @returns915 */916 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {917 timeout = timeout ?? blocksCount * 60_000;918 // eslint-disable-next-line no-async-promise-executor919 const promise = new Promise<void>(async (resolve) => {920 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {921 if(blocksCount > 0) {922 blocksCount--;923 } else {924 unsubscribe();925 resolve();926 }927 });928 });929 await this.waitWithTimeout(promise, timeout);930 return promise;931 }932933 /**934 * Wait for the specified number of sessions to pass.935 * Only applicable if the Session pallet is turned on.936 * @param sessionCount number of sessions to wait937 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks938 * @returns939 */940 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {941 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.`942 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');943944 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;945 let currentSessionIndex = -1;946947 while(currentSessionIndex < expectedSessionIndex) {948 // eslint-disable-next-line no-async-promise-executor949 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {950 await this.newBlocks(1);951 const res = await (this.helper as DevUniqueHelper).session.getIndex();952 resolve(res);953 }), blockTimeout, 'The chain has stopped producing blocks!');954 }955 }956957 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {958 timeout = timeout ?? 30 * 60 * 1000;959 // eslint-disable-next-line no-async-promise-executor960 const promise = new Promise<void>(async (resolve) => {961 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {962 if(data.number.toNumber() >= blockNumber) {963 unsubscribe();964 resolve();965 }966 });967 });968 await this.waitWithTimeout(promise, timeout);969 return promise;970 }971972 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {973 timeout = timeout ?? 30 * 60 * 1000;974 // eslint-disable-next-line no-async-promise-executor975 const promise = new Promise<void>(async (resolve) => {976 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {977 if(data.value.relayParentNumber.toNumber() >= blockNumber) {978 // @ts-ignore979 unsubscribe();980 resolve();981 }982 });983 });984 await this.waitWithTimeout(promise, timeout);985 return promise;986 }987988 noScheduledTasks() {989 const api = this.helper.getApi();990991 // eslint-disable-next-line no-async-promise-executor992 const promise = new Promise<void>(async resolve => {993 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {994 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();995996 if(areThereScheduledTasks.length == 0) {997 unsubscribe();998 resolve();999 }1000 });1001 });10021003 return promise;1004 }10051006 parachainBlockMultiplesOf(val: bigint) {1007 // eslint-disable-next-line no-async-promise-executor1008 const promise = new Promise<void>(async resolve => {1009 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1010 if(data.number.toBigInt() % val == 0n) {1011 console.log(`from waiter: ${data.number.toBigInt()}`);1012 unsubscribe();1013 resolve();1014 }1015 });1016 });1017 return promise;1018 }10191020 event<T extends IEventHelper>(1021 maxBlocksToWait: number,1022 eventHelper: T,1023 filter: (_: any) => boolean = () => true,1024 ): any {1025 // eslint-disable-next-line no-async-promise-executor1026 const promise = new Promise<T | null>(async (resolve) => {1027 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1028 const blockNumber = header.number.toHuman();1029 const blockHash = header.hash;1030 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1031 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;10321033 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);10341035 const apiAt = await this.helper.getApi().at(blockHash);1036 const eventRecords = (await apiAt.query.system.events()) as any;10371038 const neededEvent = eventRecords.toArray()1039 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1040 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1041 .find(filter);10421043 if(neededEvent) {1044 unsubscribe();1045 resolve(neededEvent);1046 } else if(maxBlocksToWait > 0) {1047 maxBlocksToWait--;1048 } else {1049 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);1050 unsubscribe();1051 resolve(null);1052 }1053 });1054 });1055 return promise;1056 }10571058 async expectEvent<T extends IEventHelper>(1059 maxBlocksToWait: number,1060 eventHelper: T,1061 filter: (e: any) => boolean = () => true,1062 ) {1063 const e = await this.event(maxBlocksToWait, eventHelper, filter);1064 if(e == null) {1065 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1066 } else {1067 return e;1068 }1069 }1070}10711072class SessionGroup {1073 helper: ChainHelperBase;10741075 constructor(helper: ChainHelperBase) {1076 this.helper = helper;1077 }10781079 //todo:collator documentation1080 async getIndex(): Promise<number> {1081 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1082 }10831084 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1085 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1086 }10871088 setOwnKeys(signer: TSigner, key: string) {1089 return this.helper.executeExtrinsic(1090 signer,1091 'api.tx.session.setKeys',1092 [key, '0x0'],1093 true,1094 );1095 }10961097 setOwnKeysFromAddress(signer: TSigner) {1098 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1099 }1100}11011102class TestUtilGroup {1103 helper: DevUniqueHelper;11041105 constructor(helper: DevUniqueHelper) {1106 this.helper = helper;1107 }11081109 async enable() {1110 if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1111 return;1112 }11131114 const signer = this.helper.util.fromSeed('//Alice');1115 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1116 }11171118 async setTestValue(signer: TSigner, testVal: number) {1119 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1120 }11211122 async incTestValue(signer: TSigner) {1123 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1124 }11251126 async setTestValueAndRollback(signer: TSigner, testVal: number) {1127 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1128 }11291130 async testValue(blockIdx?: number) {1131 const api = blockIdx1132 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1133 : this.helper.getApi();11341135 return (await api.query.testUtils.testValue()).toJSON();1136 }11371138 async justTakeFee(signer: TSigner) {1139 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1140 }11411142 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1143 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1144 }1145}11461147class EventCapture {1148 helper: DevUniqueHelper;1149 eventSection: string;1150 eventMethod: string;1151 events: EventRecord[] = [];1152 unsubscribe: VoidFn | null = null;11531154 constructor(1155 helper: DevUniqueHelper,1156 eventSection: string,1157 eventMethod: string,1158 ) {1159 this.helper = helper;1160 this.eventSection = eventSection;1161 this.eventMethod = eventMethod;1162 }11631164 async startCapture() {1165 this.stopCapture();1166 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1167 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);11681169 this.events.push(...newEvents);1170 })) as any;1171 }11721173 stopCapture() {1174 if(this.unsubscribe !== null) {1175 this.unsubscribe();1176 }1177 }11781179 extractCapturedEvents() {1180 return this.events;1181 }1182}11831184class AdminGroup {1185 helper: UniqueHelper;11861187 constructor(helper: UniqueHelper) {1188 this.helper = helper;1189 }11901191 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1192 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1193 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1194 staker: e.event.data[0].toString(),1195 stake: e.event.data[1].toBigInt(),1196 payout: e.event.data[2].toBigInt(),1197 }));1198 }1199}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper, PolkadexHelper} 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, ITransactionResult, TSigner} from './types';12import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';1617export class SilentLogger {18 log(_msg: any, _level: any): void { }19 level = {20 ERROR: 'ERROR' as const,21 WARNING: 'WARNING' as const,22 INFO: 'INFO' as const,23 };24}2526export class SilentConsole {27 // TODO: Remove, this is temporary: Filter unneeded API output28 // (Jaco promised it will be removed in the next version)29 consoleErr: any;30 consoleLog: any;31 consoleWarn: any;3233 constructor() {34 this.consoleErr = console.error;35 this.consoleLog = console.log;36 this.consoleWarn = console.warn;37 }3839 enable() {40 const outFn = (printer: any) => (...args: any[]) => {41 for(const arg of args) {42 if(typeof arg !== 'string')43 continue;44 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'];45 const needToSkip = skippedWarnings.reduce((a, b) => a || arg.includes(b), false);46 if(needToSkip || arg === 'Normal connection closure')47 return;48 }49 printer(...args);50 };5152 console.error = outFn(this.consoleErr.bind(console));53 console.log = outFn(this.consoleLog.bind(console));54 console.warn = outFn(this.consoleWarn.bind(console));55 }5657 disable() {58 console.error = this.consoleErr;59 console.log = this.consoleLog;60 console.warn = this.consoleWarn;61 }62}6364export interface IEventHelper {65 section(): string;6667 method(): string;6869 wrapEvent(data: any[]): any;70}7172// eslint-disable-next-line @typescript-eslint/naming-convention73function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {74 const helperClass = class implements IEventHelper {75 wrapEvent: (data: any[]) => any;76 _section: string;77 _method: string;7879 constructor() {80 this.wrapEvent = wrapEvent;81 this._section = section;82 this._method = method;83 }8485 section(): string {86 return this._section;87 }8889 method(): string {90 return this._method;91 }9293 filter(txres: ITransactionResult) {94 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)95 .map(e => this.wrapEvent(e.event.data));96 }9798 find(txres: ITransactionResult) {99 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);100 return e ? this.wrapEvent(e.event.data) : null;101 }102103 expect(txres: ITransactionResult) {104 const e = this.find(txres);105 if(e) {106 return e;107 } else {108 throw Error(`Expected event ${section}.${method}`);109 }110 }111 };112113 return helperClass;114}115116function eventJsonData<T = any>(data: any[], index: number) {117 return data[index].toJSON() as T;118}119120function eventHumanData(data: any[], index: number) {121 return data[index].toHuman();122}123124function eventData<T = any>(data: any[], index: number) {125 return data[index] as T;126}127128// eslint-disable-next-line @typescript-eslint/naming-convention129function EventSection(section: string) {130 return class Section {131 static section = section;132133 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {134 const helperClass = EventHelper(Section.section, name, wrapEvent);135 return new helperClass();136 }137 };138}139140function schedulerSection(schedulerInstance: string) {141 return class extends EventSection(schedulerInstance) {142 static Dispatched = this.Method('Dispatched', data => ({143 task: eventJsonData(data, 0),144 id: eventHumanData(data, 1),145 result: data[2],146 }));147148 static PriorityChanged = this.Method('PriorityChanged', data => ({149 task: eventJsonData(data, 0),150 priority: eventJsonData(data, 1),151 }));152 };153}154155export class Event {156 static Democracy = class extends EventSection('democracy') {157 static Proposed = this.Method('Proposed', data => ({158 proposalIndex: eventJsonData<number>(data, 0),159 }));160161 static ExternalTabled = this.Method('ExternalTabled');162163 static Started = this.Method('Started', data => ({164 referendumIndex: eventJsonData<number>(data, 0),165 threshold: eventHumanData(data, 1),166 }));167168 static Voted = this.Method('Voted', data => ({169 voter: eventJsonData(data, 0),170 referendumIndex: eventJsonData<number>(data, 1),171 vote: eventJsonData(data, 2),172 }));173174 static Passed = this.Method('Passed', data => ({175 referendumIndex: eventJsonData<number>(data, 0),176 }));177178 static ProposalCanceled = this.Method('ProposalCanceled', data => ({179 propIndex: eventJsonData<number>(data, 0),180 }));181182 static Cancelled = this.Method('Cancelled', data => ({183 propIndex: eventJsonData<number>(data, 0),184 }));185186 static Vetoed = this.Method('Vetoed', data => ({187 who: eventHumanData(data, 0),188 proposalHash: eventHumanData(data, 1),189 until: eventJsonData<number>(data, 1),190 }));191 };192193 static Council = class extends EventSection('council') {194 static Proposed = this.Method('Proposed', data => ({195 account: eventHumanData(data, 0),196 proposalIndex: eventJsonData<number>(data, 1),197 proposalHash: eventHumanData(data, 2),198 threshold: eventJsonData<number>(data, 3),199 }));200 static Closed = this.Method('Closed', data => ({201 proposalHash: eventHumanData(data, 0),202 yes: eventJsonData<number>(data, 1),203 no: eventJsonData<number>(data, 2),204 }));205 static Executed = this.Method('Executed', data => ({206 proposalHash: eventHumanData(data, 0),207 }));208 };209210 static TechnicalCommittee = class extends EventSection('technicalCommittee') {211 static Proposed = this.Method('Proposed', data => ({212 account: eventHumanData(data, 0),213 proposalIndex: eventJsonData<number>(data, 1),214 proposalHash: eventHumanData(data, 2),215 threshold: eventJsonData<number>(data, 3),216 }));217 static Closed = this.Method('Closed', data => ({218 proposalHash: eventHumanData(data, 0),219 yes: eventJsonData<number>(data, 1),220 no: eventJsonData<number>(data, 2),221 }));222 static Approved = this.Method('Approved', data => ({223 proposalHash: eventHumanData(data, 0),224 }));225 static Executed = this.Method('Executed', data => ({226 proposalHash: eventHumanData(data, 0),227 result: eventHumanData(data, 1),228 }));229 };230231 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {232 static Submitted = this.Method('Submitted', data => ({233 referendumIndex: eventJsonData<number>(data, 0),234 trackId: eventJsonData<number>(data, 1),235 proposal: eventJsonData(data, 2),236 }));237238 static Cancelled = this.Method('Cancelled', data => ({239 index: eventJsonData<number>(data, 0),240 tally: eventJsonData(data, 1),241 }));242 };243244 static UniqueScheduler = schedulerSection('uniqueScheduler');245 static Scheduler = schedulerSection('scheduler');246247 static XcmpQueue = class extends EventSection('xcmpQueue') {248 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({249 messageHash: eventJsonData(data, 0),250 }));251252 static Success = this.Method('Success', data => ({253 messageHash: eventJsonData(data, 0),254 }));255256 static Fail = this.Method('Fail', data => ({257 messageHash: eventJsonData(data, 0),258 outcome: eventData<XcmV2TraitsError>(data, 1),259 }));260 };261}262263export class DevUniqueHelper extends UniqueHelper {264 /**265 * Arrange methods for tests266 */267 arrange: ArrangeGroup;268 wait: WaitGroup;269 admin: AdminGroup;270 session: SessionGroup;271 testUtils: TestUtilGroup;272273 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {274 options.helperBase = options.helperBase ?? DevUniqueHelper;275276 super(logger, options);277 this.arrange = new ArrangeGroup(this);278 this.wait = new WaitGroup(this);279 this.admin = new AdminGroup(this);280 this.testUtils = new TestUtilGroup(this);281 this.session = new SessionGroup(this);282 }283284 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {285 if(!wsEndpoint) throw new Error('wsEndpoint was not set');286 const wsProvider = new WsProvider(wsEndpoint);287 this.api = new ApiPromise({288 provider: wsProvider,289 signedExtensions: {290 ContractHelpers: {291 extrinsic: {},292 payload: {},293 },294 CheckMaintenance: {295 extrinsic: {},296 payload: {},297 },298 DisableIdentityCalls: {299 extrinsic: {},300 payload: {},301 },302 FakeTransactionFinalizer: {303 extrinsic: {},304 payload: {},305 },306 },307 rpc: {308 unique: defs.unique.rpc,309 appPromotion: defs.appPromotion.rpc,310 povinfo: defs.povinfo.rpc,311 eth: {312 feeHistory: {313 description: 'Dummy',314 params: [],315 type: 'u8',316 },317 maxPriorityFeePerGas: {318 description: 'Dummy',319 params: [],320 type: 'u8',321 },322 },323 },324 });325 await this.api.isReadyOrError;326 this.network = await UniqueHelper.detectNetwork(this.api);327 this.wsEndpoint = wsEndpoint;328 }329}330331export class DevRelayHelper extends RelayHelper {332 wait: WaitGroup;333334 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {335 options.helperBase = options.helperBase ?? DevRelayHelper;336337 super(logger, options);338 this.wait = new WaitGroup(this);339 }340}341342export class DevWestmintHelper extends WestmintHelper {343 wait: WaitGroup;344345 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {346 options.helperBase = options.helperBase ?? DevWestmintHelper;347348 super(logger, options);349 this.wait = new WaitGroup(this);350 }351}352353export class DevStatemineHelper extends DevWestmintHelper {}354355export class DevStatemintHelper extends DevWestmintHelper {}356357export class DevMoonbeamHelper extends MoonbeamHelper {358 account: MoonbeamAccountGroup;359 wait: WaitGroup;360 fastDemocracy: MoonbeamFastDemocracyGroup;361362 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {363 options.helperBase = options.helperBase ?? DevMoonbeamHelper;364 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';365366 super(logger, options);367 this.account = new MoonbeamAccountGroup(this);368 this.wait = new WaitGroup(this);369 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);370 }371}372373export class DevMoonriverHelper extends DevMoonbeamHelper {374 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {375 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';376 super(logger, options);377 }378}379380export class DevAstarHelper extends AstarHelper {381 wait: WaitGroup;382383 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {384 options.helperBase = options.helperBase ?? DevAstarHelper;385386 super(logger, options);387 this.wait = new WaitGroup(this);388 }389}390391export class DevShidenHelper extends AstarHelper {392 wait: WaitGroup;393394 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {395 options.helperBase = options.helperBase ?? DevShidenHelper;396397 super(logger, options);398 this.wait = new WaitGroup(this);399 }400}401402export class DevAcalaHelper extends AcalaHelper {403 wait: WaitGroup;404405 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {406 options.helperBase = options.helperBase ?? DevAcalaHelper;407408 super(logger, options);409 this.wait = new WaitGroup(this);410 }411}412413export class DevPolkadexHelper extends PolkadexHelper {414 wait: WaitGroup;415 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {416 options.helperBase = options.helperBase ?? PolkadexHelper;417418 super(logger, options);419 this.wait = new WaitGroup(this);420 }421}422423export class DevKaruraHelper extends DevAcalaHelper {}424425export class ArrangeGroup {426 helper: DevUniqueHelper;427428 scheduledIdSlider = 0;429430 constructor(helper: DevUniqueHelper) {431 this.helper = helper;432 }433434 /**435 * Generates accounts with the specified UNQ token balance436 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.437 * @param donor donor account for balances438 * @returns array of newly created accounts439 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);440 */441 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {442 let nonce = await this.helper.chain.getNonce(donor.address);443 const wait = new WaitGroup(this.helper);444 const ss58Format = this.helper.chain.getChainProperties().ss58Format;445 const tokenNominal = this.helper.balance.getOneTokenNominal();446 const transactions = [];447 const accounts: IKeyringPair[] = [];448 for(const balance of balances) {449 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);450 accounts.push(recipient);451 if(balance !== 0n) {452 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);453 transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));454 nonce++;455 }456 }457458 await Promise.all(transactions).catch(_e => {});459460 //#region TODO remove this region, when nonce problem will be solved461 const checkBalances = async () => {462 let isSuccess = true;463 for(let i = 0; i < balances.length; i++) {464 const balance = await this.helper.balance.getSubstrate(accounts[i].address);465 if(balance !== balances[i] * tokenNominal) {466 isSuccess = false;467 break;468 }469 }470 return isSuccess;471 };472473 let accountsCreated = false;474 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;475 // checkBalances retry up to 5-50 blocks476 for(let index = 0; index < maxBlocksChecked; index++) {477 accountsCreated = await checkBalances();478 if(accountsCreated) break;479 await wait.newBlocks(1);480 }481482 if(!accountsCreated) throw Error('Accounts generation failed');483 //#endregion484485 return accounts;486 };487488 // TODO combine this method and createAccounts into one489 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {490 const createAsManyAsCan = async () => {491 let transactions: any = [];492 const accounts: IKeyringPair[] = [];493 let nonce = await this.helper.chain.getNonce(donor.address);494 const tokenNominal = this.helper.balance.getOneTokenNominal();495 const ss58Format = this.helper.chain.getChainProperties().ss58Format;496 for(let i = 0; i < accountsToCreate; i++) {497 if(i === 500) { // if there are too many accounts to create498 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled499 transactions = []; //500 nonce = await this.helper.chain.getNonce(donor.address); // update nonce501 }502 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);503 accounts.push(recipient);504 if(withBalance !== 0n) {505 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);506 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));507 nonce++;508 }509 }510511 const fullfilledAccounts = [];512 await Promise.allSettled(transactions);513 for(const account of accounts) {514 const accountBalance = await this.helper.balance.getSubstrate(account.address);515 if(accountBalance === withBalance * tokenNominal) {516 fullfilledAccounts.push(account);517 }518 }519 return fullfilledAccounts;520 };521522523 const crowd: IKeyringPair[] = [];524 // do up to 5 retries525 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {526 const asManyAsCan = await createAsManyAsCan();527 crowd.push(...asManyAsCan);528 accountsToCreate -= asManyAsCan.length;529 }530531 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);532533 return crowd;534 };535536 /**537 * Generates one account with zero balance538 * @returns the newly generated account539 * @example const account = await helper.arrange.createEmptyAccount();540 */541 createEmptyAccount = (): IKeyringPair => {542 const ss58Format = this.helper.chain.getChainProperties().ss58Format;543 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);544 };545546 isDevNode = async () => {547 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();548 if(blockNumber == 0) {549 await this.helper.wait.newBlocks(1);550 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();551 }552 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);553 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);554 const findCreationDate = (block: any) => {555 const humanBlock = block.toHuman();556 let date;557 humanBlock.block.extrinsics.forEach((ext: any) => {558 if(ext.method.section === 'timestamp') {559 date = Number(ext.method.args.now.replaceAll(',', ''));560 }561 });562 return date;563 };564 const block1date = await findCreationDate(block1);565 const block2date = await findCreationDate(block2);566 if(block2date! - block1date! < 9000) return true;567 };568569 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {570 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);571 let balance = await this.helper.balance.getSubstrate(address);572573 await promise();574575 balance -= await this.helper.balance.getSubstrate(address);576577 return balance;578 }579580 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {581 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);582583 const kvJson: {[key: string]: string} = {};584585 for(const kv of rawPovInfo.keyValues) {586 kvJson[kv.key.toHex()] = kv.value.toHex();587 }588589 const kvStr = JSON.stringify(kvJson);590591 const chainql = spawnSync(592 'chainql',593 [594 `--tla-code=data=${kvStr}`,595 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,596 ],597 );598599 if(!chainql.stdout) {600 throw Error('unable to get an output from the `chainql`');601 }602603 return {604 proofSize: rawPovInfo.proofSize.toNumber(),605 compactProofSize: rawPovInfo.compactProofSize.toNumber(),606 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),607 results: rawPovInfo.results,608 kv: JSON.parse(chainql.stdout.toString()),609 };610 }611612 calculatePalletAddress(palletId: any) {613 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));614 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);615 }616617 makeScheduledIds(num: number): string[] {618 function makeId(slider: number) {619 const scheduledIdSize = 64;620 const hexId = slider.toString(16);621 const prefixSize = scheduledIdSize - hexId.length;622623 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;624625 return scheduledId;626 }627628 const ids = [];629 for(let i = 0; i < num; i++) {630 ids.push(makeId(this.scheduledIdSlider));631 this.scheduledIdSlider += 1;632 }633634 return ids;635 }636637 makeScheduledId(): string {638 return (this.makeScheduledIds(1))[0];639 }640641 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {642 const capture = new EventCapture(this.helper, eventSection, eventMethod);643 await capture.startCapture();644645 return capture;646 }647648 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {649 return {650 V2: [651 {652 WithdrawAsset: [653 {654 id,655 fun: {656 Fungible: amount,657 },658 },659 ],660 },661 {662 BuyExecution: {663 fees: {664 id,665 fun: {666 Fungible: amount,667 },668 },669 weightLimit: 'Unlimited',670 },671 },672 {673 DepositAsset: {674 assets: {675 Wild: 'All',676 },677 maxAssets: 1,678 beneficiary: {679 parents: 0,680 interior: {681 X1: {682 AccountId32: {683 network: 'Any',684 id: beneficiary,685 },686 },687 },688 },689 },690 },691 ],692 };693 }694695 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {696 return {697 V2: [698 {699 ReserveAssetDeposited: [700 {701 id,702 fun: {703 Fungible: amount,704 },705 },706 ],707 },708 {709 BuyExecution: {710 fees: {711 id,712 fun: {713 Fungible: amount,714 },715 },716 weightLimit: 'Unlimited',717 },718 },719 {720 DepositAsset: {721 assets: {722 Wild: 'All',723 },724 maxAssets: 1,725 beneficiary: {726 parents: 0,727 interior: {728 X1: {729 AccountId32: {730 network: 'Any',731 id: beneficiary,732 },733 },734 },735 },736 },737 },738 ],739 };740 }741}742743class MoonbeamAccountGroup {744 helper: MoonbeamHelper;745746 keyring: Keyring;747 _alithAccount: IKeyringPair;748 _baltatharAccount: IKeyringPair;749 _dorothyAccount: IKeyringPair;750751 constructor(helper: MoonbeamHelper) {752 this.helper = helper;753754 this.keyring = new Keyring({type: 'ethereum'});755 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';756 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';757 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';758759 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');760 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');761 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');762 }763764 alithAccount() {765 return this._alithAccount;766 }767768 baltatharAccount() {769 return this._baltatharAccount;770 }771772 dorothyAccount() {773 return this._dorothyAccount;774 }775776 create() {777 return this.keyring.addFromUri(mnemonicGenerate());778 }779}780781class MoonbeamFastDemocracyGroup {782 helper: DevMoonbeamHelper;783784 constructor(helper: DevMoonbeamHelper) {785 this.helper = helper;786 }787788 async executeProposal(proposalDesciption: string, encodedProposal: string) {789 const proposalHash = blake2AsHex(encodedProposal);790791 const alithAccount = this.helper.account.alithAccount();792 const baltatharAccount = this.helper.account.baltatharAccount();793 const dorothyAccount = this.helper.account.dorothyAccount();794795 const councilVotingThreshold = 2;796 const technicalCommitteeThreshold = 2;797 const fastTrackVotingPeriod = 3;798 const fastTrackDelayPeriod = 0;799800 console.log(`[democracy] executing '${proposalDesciption}' proposal`);801802 // >>> Propose external motion through council >>>803 console.log('\t* Propose external motion through council.......');804 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});805 const encodedMotion = externalMotion?.method.toHex() || '';806 const motionHash = blake2AsHex(encodedMotion);807 console.log('\t* Motion hash is %s', motionHash);808809 await this.helper.collective.council.propose(810 baltatharAccount,811 councilVotingThreshold,812 externalMotion,813 externalMotion.encodedLength,814 );815816 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;817 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);818 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);819820 await this.helper.collective.council.close(821 dorothyAccount,822 motionHash,823 councilProposalIdx,824 {825 refTime: 1_000_000_000,826 proofSize: 1_000_000,827 },828 externalMotion.encodedLength,829 );830 console.log('\t* Propose external motion through council.......DONE');831 // <<< Propose external motion through council <<<832833 // >>> Fast track proposal through technical committee >>>834 console.log('\t* Fast track proposal through technical committee.......');835 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);836 const encodedFastTrack = fastTrack?.method.toHex() || '';837 const fastTrackHash = blake2AsHex(encodedFastTrack);838 console.log('\t* FastTrack hash is %s', fastTrackHash);839840 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);841842 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;843 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);844 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);845846 await this.helper.collective.techCommittee.close(847 baltatharAccount,848 fastTrackHash,849 techProposalIdx,850 {851 refTime: 1_000_000_000,852 proofSize: 1_000_000,853 },854 fastTrack.encodedLength,855 );856 console.log('\t* Fast track proposal through technical committee.......DONE');857 // <<< Fast track proposal through technical committee <<<858859 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);860 const referendumIndex = democracyStarted.referendumIndex;861862 // >>> Referendum voting >>>863 console.log(`\t* Referendum #${referendumIndex} voting.......`);864 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {865 balance: 10_000_000_000_000_000_000n,866 vote: {aye: true, conviction: 1},867 });868 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);869 // <<< Referendum voting <<<870871 // Wait the proposal to pass872 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);873874 await this.helper.wait.newBlocks(1);875876 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);877 }878}879880class WaitGroup {881 helper: ChainHelperBase;882883 constructor(helper: ChainHelperBase) {884 this.helper = helper;885 }886887 sleep(milliseconds: number) {888 return new Promise((resolve) => setTimeout(resolve, milliseconds));889 }890891 private async waitWithTimeout(promise: Promise<any>, timeout: number) {892 let isBlock = false;893 promise.then(() => isBlock = true).catch(() => isBlock = true);894 let totalTime = 0;895 const step = 100;896 while(!isBlock) {897 await this.sleep(step);898 totalTime += step;899 if(totalTime >= timeout) throw Error('Blocks production failed');900 }901 return promise;902 }903904 /**905 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.906 * @param promise async operation to race against the timeout907 * @param timeoutMS time after which to time out908 * @param timeoutError error message to throw909 * @returns promise of the same type the operation had910 */911 withTimeout<T>(912 promise: Promise<T>,913 timeoutMS = 30000,914 timeoutError = 'The operation has timed out!',915 ): Promise<T> {916 const timeout = new Promise<never>((_, reject) => {917 setTimeout(() => {918 reject(new Error(timeoutError));919 }, timeoutMS);920 });921922 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});923 }924925 /**926 * Wait for specified number of blocks927 * @param blocksCount number of blocks to wait928 * @returns929 */930 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {931 timeout = timeout ?? blocksCount * 60_000;932 // eslint-disable-next-line no-async-promise-executor933 const promise = new Promise<void>(async (resolve) => {934 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {935 if(blocksCount > 0) {936 blocksCount--;937 } else {938 unsubscribe();939 resolve();940 }941 });942 });943 await this.waitWithTimeout(promise, timeout);944 return promise;945 }946947 /**948 * Wait for the specified number of sessions to pass.949 * Only applicable if the Session pallet is turned on.950 * @param sessionCount number of sessions to wait951 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks952 * @returns953 */954 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {955 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.`956 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');957958 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;959 let currentSessionIndex = -1;960961 while(currentSessionIndex < expectedSessionIndex) {962 // eslint-disable-next-line no-async-promise-executor963 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {964 await this.newBlocks(1);965 const res = await (this.helper as DevUniqueHelper).session.getIndex();966 resolve(res);967 }), blockTimeout, 'The chain has stopped producing blocks!');968 }969 }970971 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {972 timeout = timeout ?? 30 * 60 * 1000;973 // eslint-disable-next-line no-async-promise-executor974 const promise = new Promise<void>(async (resolve) => {975 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {976 if(data.number.toNumber() >= blockNumber) {977 unsubscribe();978 resolve();979 }980 });981 });982 await this.waitWithTimeout(promise, timeout);983 return promise;984 }985986 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {987 timeout = timeout ?? 30 * 60 * 1000;988 // eslint-disable-next-line no-async-promise-executor989 const promise = new Promise<void>(async (resolve) => {990 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {991 if(data.value.relayParentNumber.toNumber() >= blockNumber) {992 // @ts-ignore993 unsubscribe();994 resolve();995 }996 });997 });998 await this.waitWithTimeout(promise, timeout);999 return promise;1000 }10011002 noScheduledTasks() {1003 const api = this.helper.getApi();10041005 // eslint-disable-next-line no-async-promise-executor1006 const promise = new Promise<void>(async resolve => {1007 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1008 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();10091010 if(areThereScheduledTasks.length == 0) {1011 unsubscribe();1012 resolve();1013 }1014 });1015 });10161017 return promise;1018 }10191020 parachainBlockMultiplesOf(val: bigint) {1021 // eslint-disable-next-line no-async-promise-executor1022 const promise = new Promise<void>(async resolve => {1023 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1024 if(data.number.toBigInt() % val == 0n) {1025 console.log(`from waiter: ${data.number.toBigInt()}`);1026 unsubscribe();1027 resolve();1028 }1029 });1030 });1031 return promise;1032 }10331034 event<T extends IEventHelper>(1035 maxBlocksToWait: number,1036 eventHelper: T,1037 filter: (_: any) => boolean = () => true,1038 ): any {1039 // eslint-disable-next-line no-async-promise-executor1040 const promise = new Promise<T | null>(async (resolve) => {1041 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1042 const blockNumber = header.number.toHuman();1043 const blockHash = header.hash;1044 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1045 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;10461047 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);10481049 const apiAt = await this.helper.getApi().at(blockHash);1050 const eventRecords = (await apiAt.query.system.events()) as any;10511052 const neededEvent = eventRecords.toArray()1053 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1054 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1055 .find(filter);10561057 if(neededEvent) {1058 unsubscribe();1059 resolve(neededEvent);1060 } else if(maxBlocksToWait > 0) {1061 maxBlocksToWait--;1062 } else {1063 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);1064 unsubscribe();1065 resolve(null);1066 }1067 });1068 });1069 return promise;1070 }10711072 async expectEvent<T extends IEventHelper>(1073 maxBlocksToWait: number,1074 eventHelper: T,1075 filter: (e: any) => boolean = () => true,1076 ) {1077 const e = await this.event(maxBlocksToWait, eventHelper, filter);1078 if(e == null) {1079 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1080 } else {1081 return e;1082 }1083 }1084}10851086class SessionGroup {1087 helper: ChainHelperBase;10881089 constructor(helper: ChainHelperBase) {1090 this.helper = helper;1091 }10921093 //todo:collator documentation1094 async getIndex(): Promise<number> {1095 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1096 }10971098 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1099 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1100 }11011102 setOwnKeys(signer: TSigner, key: string) {1103 return this.helper.executeExtrinsic(1104 signer,1105 'api.tx.session.setKeys',1106 [key, '0x0'],1107 true,1108 );1109 }11101111 setOwnKeysFromAddress(signer: TSigner) {1112 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1113 }1114}11151116class TestUtilGroup {1117 helper: DevUniqueHelper;11181119 constructor(helper: DevUniqueHelper) {1120 this.helper = helper;1121 }11221123 async enable() {1124 if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1125 return;1126 }11271128 const signer = this.helper.util.fromSeed('//Alice');1129 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1130 }11311132 async setTestValue(signer: TSigner, testVal: number) {1133 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1134 }11351136 async incTestValue(signer: TSigner) {1137 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1138 }11391140 async setTestValueAndRollback(signer: TSigner, testVal: number) {1141 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1142 }11431144 async testValue(blockIdx?: number) {1145 const api = blockIdx1146 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1147 : this.helper.getApi();11481149 return (await api.query.testUtils.testValue()).toJSON();1150 }11511152 async justTakeFee(signer: TSigner) {1153 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1154 }11551156 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1157 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1158 }1159}11601161class EventCapture {1162 helper: DevUniqueHelper;1163 eventSection: string;1164 eventMethod: string;1165 events: EventRecord[] = [];1166 unsubscribe: VoidFn | null = null;11671168 constructor(1169 helper: DevUniqueHelper,1170 eventSection: string,1171 eventMethod: string,1172 ) {1173 this.helper = helper;1174 this.eventSection = eventSection;1175 this.eventMethod = eventMethod;1176 }11771178 async startCapture() {1179 this.stopCapture();1180 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1181 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);11821183 this.events.push(...newEvents);1184 })) as any;1185 }11861187 stopCapture() {1188 if(this.unsubscribe !== null) {1189 this.unsubscribe();1190 }1191 }11921193 extractCapturedEvents() {1194 return this.events;1195 }1196}11971198class AdminGroup {1199 helper: UniqueHelper;12001201 constructor(helper: UniqueHelper) {1202 this.helper = helper;1203 }12041205 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1206 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1207 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1208 staker: e.event.data[0].toString(),1209 stake: e.event.data[1].toBigInt(),1210 payout: e.event.data[2].toBigInt(),1211 }));1212 }1213}tests/src/util/playgrounds/unique.tsdiffbeforeafterboth--- a/tests/src/util/playgrounds/unique.ts
+++ b/tests/src/util/playgrounds/unique.ts
@@ -3694,6 +3694,12 @@
}
}
+class PolkadexXcmHelperGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+ async whitelistToken(signer: TSigner, assetId: any) {
+ await this.helper.executeExtrinsic(signer, 'api.tx.xcmHelper.whitelistToken', [assetId], true);
+ }
+}
+
class TokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
async accounts(address: string, currencyId: any) {
const {free} = (await this.helper.callRpc('api.query.tokens.accounts', [address, currencyId])).toJSON() as any;
@@ -3977,6 +3983,30 @@
}
}
+export class PolkadexHelper extends XcmChainHelper {
+ assets: AssetsGroup<PolkadexHelper>;
+ balance: SubstrateBalanceGroup<PolkadexHelper>;
+ xTokens: XTokensGroup<PolkadexHelper>;
+ xcm: XcmGroup<PolkadexHelper>;
+ xcmHelper: PolkadexXcmHelperGroup<PolkadexHelper>;
+
+ constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+ super(logger, options.helperBase ?? PolkadexHelper);
+
+ this.assets = new AssetsGroup(this);
+ this.balance = new SubstrateBalanceGroup(this);
+ this.xTokens = new XTokensGroup(this);
+ this.xcm = new XcmGroup(this, 'polkadotXcm');
+ this.xcmHelper = new PolkadexXcmHelperGroup(this);
+ }
+
+ getSudo<T extends PolkadexHelper>() {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ const SudoHelperType = SudoHelper(this.helperBase);
+ return this.clone(SudoHelperType) as T;
+ }
+}
+
// eslint-disable-next-line @typescript-eslint/naming-convention
function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {
return class extends Base {
tests/src/xcm/xcmUnique.test.tsdiffbeforeafterboth--- a/tests/src/xcm/xcmUnique.test.ts
+++ b/tests/src/xcm/xcmUnique.test.ts
@@ -16,14 +16,16 @@
import {IKeyringPair} from '@polkadot/types/types';
import config from '../config';
-import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds} from '../util';
+import {itSub, expect, describeXCM, usingPlaygrounds, usingAcalaPlaygrounds, usingRelayPlaygrounds, usingMoonbeamPlaygrounds, usingStatemintPlaygrounds, usingAstarPlaygrounds, usingPolkadexPlaygrounds} from '../util';
import {DevUniqueHelper, Event} from '../util/playgrounds/unique.dev';
+import {nToBigInt} from '@polkadot/util';
const UNIQUE_CHAIN = +(process.env.RELAY_UNIQUE_ID || 2037);
const STATEMINT_CHAIN = +(process.env.RELAY_STATEMINT_ID || 1000);
const ACALA_CHAIN = +(process.env.RELAY_ACALA_ID || 2000);
const MOONBEAM_CHAIN = +(process.env.RELAY_MOONBEAM_ID || 2004);
const ASTAR_CHAIN = +(process.env.RELAY_ASTAR_ID || 2006);
+const POLKADEX_CHAIN = +(process.env.RELAY_POLKADEX_ID || 2040);
const STATEMINT_PALLET_INSTANCE = 50;
@@ -32,6 +34,7 @@
const acalaUrl = config.acalaUrl;
const moonbeamUrl = config.moonbeamUrl;
const astarUrl = config.astarUrl;
+const polkadexUrl = config.polkadexUrl;
const RELAY_DECIMALS = 12;
const STATEMINT_DECIMALS = 12;
@@ -791,6 +794,193 @@
});
});
+describeXCM('[XCM] Integration test: Exchanging tokens with Polkadex', () => {
+ let alice: IKeyringPair;
+ let randomAccount: IKeyringPair;
+ let unqFees: bigint;
+ let balanceUniqueTokenInit: bigint;
+ let balanceUniqueTokenMiddle: bigint;
+ let balanceUniqueTokenFinal: bigint;
+ const maxWaitBlocks = 6;
+
+ const uniqueAssetId = {
+ Concrete: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: UNIQUE_CHAIN,
+ },
+ },
+ },
+ };
+
+ 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 usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {
+ const isWhitelisted = ((await helper.callRpc('api.query.xcmHelper.whitelistedTokens', [])).toJSON() as [])
+ .map(nToBigInt).length != 0;
+
+ if(!isWhitelisted) {
+ await helper.getSudo().xcmHelper.whitelistToken(alice, uniqueAssetId);
+ }
+
+ await helper.balance.transferToSubstrate(alice, randomAccount.address, 10000000000000n);
+ });
+
+ await usingPlaygrounds(async (helper) => {
+ await helper.balance.transferToSubstrate(alice, randomAccount.address, 10n * TRANSFER_AMOUNT);
+ balanceUniqueTokenInit = await helper.balance.getSubstrate(randomAccount.address);
+ });
+ });
+
+ itSub('Should connect and send UNQ to Polkadex', async ({helper}) => {
+
+ const destination = {
+ V2: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: POLKADEX_CHAIN,
+ },
+ },
+ },
+ };
+
+ const beneficiary = {
+ V2: {
+ parents: 0,
+ interior: {
+ X1: {
+ AccountId32: {
+ network: 'Any',
+ id: randomAccount.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 -> Polkadex] transaction fees on Unique: %s UNQ', helper.util.bigIntToDecimals(unqFees));
+ expect(unqFees > 0n, 'Negative fees UNQ, looks like nothing was transferred').to.be.true;
+
+ await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {
+ await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == messageSent.messageHash);
+ });
+ });
+
+
+ itSub('Should connect to Polkadex and send UNQ back', async ({helper}) => {
+
+ const uniqueMultilocation = {
+ V2: {
+ parents: 1,
+ interior: {
+ X1: {Parachain: UNIQUE_CHAIN},
+ },
+ },
+ };
+
+ const xcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+ randomAccount.addressRaw,
+ {
+ Concrete: {
+ parents: 0,
+ interior: 'Here',
+ },
+ },
+ TRANSFER_AMOUNT,
+ );
+
+ let xcmProgramSent: any;
+
+
+ await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {
+ await helper.getSudo().xcm.send(alice, uniqueMultilocation, xcmProgram);
+
+ 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(randomAccount.address);
+
+ expect(balanceUniqueTokenFinal).to.be.equal(balanceUniqueTokenInit - unqFees);
+ });
+
+ itSub('Polkadex can send only up to its balance', async ({helper}) => {
+ const polkadexBalance = 10000n * (10n ** UNQ_DECIMALS);
+ const polkadexSovereignAccount = helper.address.paraSiblingSovereignAccount(POLKADEX_CHAIN);
+ await helper.getSudo().balance.setBalanceSubstrate(alice, polkadexSovereignAccount, polkadexBalance);
+ const moreThanPolkadexHas = 2n * polkadexBalance;
+
+ const targetAccount = helper.arrange.createEmptyAccount();
+
+ const uniqueMultilocation = {
+ V2: {
+ parents: 1,
+ interior: {
+ X1: {Parachain: UNIQUE_CHAIN},
+ },
+ },
+ };
+
+ const maliciousXcmProgram = helper.arrange.makeXcmProgramWithdrawDeposit(
+ targetAccount.addressRaw,
+ {
+ Concrete: {
+ parents: 0,
+ interior: 'Here',
+ },
+ },
+ moreThanPolkadexHas,
+ );
+
+ let maliciousXcmProgramSent: any;
+
+
+ await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {
+ await helper.getSudo().xcm.send(alice, uniqueMultilocation, maliciousXcmProgram);
+
+ maliciousXcmProgramSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ });
+
+ await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.Fail, event => event.messageHash == maliciousXcmProgramSent.messageHash);
+
+ const targetAccountBalance = await helper.balance.getSubstrate(targetAccount.address);
+ expect(targetAccountBalance).to.be.equal(0n);
+ });
+});
+
// These tests are relevant only when
// the the corresponding foreign assets are not registered
describeXCM('[XCM] Integration test: Unique rejects non-native tokens', () => {
@@ -930,6 +1120,31 @@
await expectFailedToTransact(helper, messageSent);
});
+
+ itSub('Unique rejects PDX tokens from Polkadex', async ({helper}) => {
+
+ const maliciousXcmProgramFullId = helper.arrange.makeXcmProgramReserveAssetDeposited(
+ helper.arrange.createEmptyAccount().addressRaw,
+ {
+ Concrete: {
+ parents: 1,
+ interior: {
+ X1: {
+ Parachain: POLKADEX_CHAIN,
+ },
+ },
+ },
+ },
+ testAmount,
+ );
+
+ await usingPolkadexPlaygrounds(polkadexUrl, async (helper) => {
+ await helper.getSudo().xcm.send(alice, uniqueParachainMultilocation, maliciousXcmProgramFullId);
+ messageSent = await helper.wait.expectEvent(maxWaitBlocks, Event.XcmpQueue.XcmpMessageSent);
+ });
+
+ await expectFailedToTransact(helper, messageSent);
+ });
});
describeXCM('[XCM] Integration test: Exchanging UNQ with Moonbeam', () => {