1234import {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, PalletSchedulerEvent} 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 28 29 consoleErr: any;30 consoleLog: any;31 consoleWarn: any;3233 constructor() {34 this.consoleErr = console.error;35 this.consoleLog = console.log;36 this.consoleWarn = console.warn;37 }3839 enable() {40 const outFn = (printer: any) => (...args: any[]) => {41 for(const arg of args) {42 if(typeof arg !== 'string')43 continue;44 if(arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')45 return;46 }47 printer(...args);48 };4950 console.error = outFn(this.consoleErr.bind(console));51 console.log = outFn(this.consoleLog.bind(console));52 console.warn = outFn(this.consoleWarn.bind(console));53 }5455 disable() {56 console.error = this.consoleErr;57 console.log = this.consoleLog;58 console.warn = this.consoleWarn;59 }60}6162export interface IEventHelper {63 section(): string;6465 method(): string;6667 wrapEvent(data: any[]): any;68}697071function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {72 const helperClass = class implements IEventHelper {73 wrapEvent: (data: any[]) => any;74 _section: string;75 _method: string;7677 constructor() {78 this.wrapEvent = wrapEvent;79 this._section = section;80 this._method = method;81 }8283 section(): string {84 return this._section;85 }8687 method(): string {88 return this._method;89 }9091 filter(txres: ITransactionResult) {92 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)93 .map(e => this.wrapEvent(e.event.data));94 }9596 find(txres: ITransactionResult) {97 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);98 return e ? this.wrapEvent(e.event.data) : null;99 }100101 expect(txres: ITransactionResult) {102 const e = this.find(txres);103 if(e) {104 return e;105 } else {106 throw Error(`Expected event ${section}.${method}`);107 }108 }109 };110111 return helperClass;112}113114function eventJsonData<T = any>(data: any[], index: number) {115 return data[index].toJSON() as T;116}117118function eventHumanData(data: any[], index: number) {119 return data[index].toHuman();120}121122function eventData<T = any>(data: any[], index: number) {123 return data[index] as T;124}125126127function EventSection(section: string) {128 return class Section {129 static section = section;130131 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {132 const helperClass = EventHelper(Section.section, name, wrapEvent);133 return new helperClass();134 }135 };136}137138function schedulerSection(schedulerInstance: string) {139 return class extends EventSection(schedulerInstance) {140 static Dispatched = this.Method('Dispatched', data => ({141 task: eventJsonData(data, 0),142 id: eventHumanData(data, 1),143 result: data[2],144 }));145146 static PriorityChanged = this.Method('PriorityChanged', data => ({147 task: eventJsonData(data, 0),148 priority: eventJsonData(data, 1),149 }));150 };151}152153export class Event {154 static Democracy = class extends EventSection('democracy') {155 static Proposed = this.Method('Proposed', data => ({156 proposalIndex: eventJsonData<number>(data, 0),157 }));158159 static ExternalTabled = this.Method('ExternalTabled');160161 static Started = this.Method('Started', data => ({162 referendumIndex: eventJsonData<number>(data, 0),163 threshold: eventHumanData(data, 1),164 }));165166 static Voted = this.Method('Voted', data => ({167 voter: eventJsonData(data, 0),168 referendumIndex: eventJsonData<number>(data, 1),169 vote: eventJsonData(data, 2),170 }));171172 static Passed = this.Method('Passed', data => ({173 referendumIndex: eventJsonData<number>(data, 0),174 }));175 };176177 static Council = class extends EventSection('council') {178 static Proposed = this.Method('Proposed', data => ({179 account: eventHumanData(data, 0),180 proposalIndex: eventJsonData<number>(data, 1),181 proposalHash: eventHumanData(data, 2),182 threshold: eventJsonData<number>(data, 3),183 }));184 static Closed = this.Method('Closed', data => ({185 proposalHash: eventHumanData(data, 0),186 yes: eventJsonData<number>(data, 1),187 no: eventJsonData<number>(data, 2),188 }));189 };190191 static TechnicalCommittee = class extends EventSection('technicalCommittee') {192 static Proposed = this.Method('Proposed', data => ({193 account: eventHumanData(data, 0),194 proposalIndex: eventJsonData<number>(data, 1),195 proposalHash: eventHumanData(data, 2),196 threshold: eventJsonData<number>(data, 3),197 }));198 static Closed = this.Method('Closed', data => ({199 proposalHash: eventHumanData(data, 0),200 yes: eventJsonData<number>(data, 1),201 no: eventJsonData<number>(data, 2),202 }));203 };204205 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {206 static Submitted = this.Method('Submitted', data => ({207 referendumIndex: eventJsonData<number>(data, 0),208 trackId: eventJsonData<number>(data, 1),209 proposal: eventJsonData(data, 2),210 }));211 };212213 static UniqueScheduler = schedulerSection('uniqueScheduler');214 static Scheduler = schedulerSection('scheduler');215216 static XcmpQueue = class extends EventSection('xcmpQueue') {217 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({218 messageHash: eventJsonData(data, 0),219 }));220221 static Fail = this.Method('Fail', data => ({222 messageHash: eventJsonData(data, 0),223 outcome: eventData<XcmV2TraitsError>(data, 1),224 }));225 };226}227228export class DevUniqueHelper extends UniqueHelper {229 230231232 arrange: ArrangeGroup;233 wait: WaitGroup;234 admin: AdminGroup;235 session: SessionGroup;236 testUtils: TestUtilGroup;237238 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {239 options.helperBase = options.helperBase ?? DevUniqueHelper;240241 super(logger, options);242 this.arrange = new ArrangeGroup(this);243 this.wait = new WaitGroup(this);244 this.admin = new AdminGroup(this);245 this.testUtils = new TestUtilGroup(this);246 this.session = new SessionGroup(this);247 }248249 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {250 const wsProvider = new WsProvider(wsEndpoint);251 this.api = new ApiPromise({252 provider: wsProvider,253 signedExtensions: {254 ContractHelpers: {255 extrinsic: {},256 payload: {},257 },258 CheckMaintenance: {259 extrinsic: {},260 payload: {},261 },262 DisableIdentityCalls: {263 extrinsic: {},264 payload: {},265 },266 FakeTransactionFinalizer: {267 extrinsic: {},268 payload: {},269 },270 },271 rpc: {272 unique: defs.unique.rpc,273 appPromotion: defs.appPromotion.rpc,274 povinfo: defs.povinfo.rpc,275 eth: {276 feeHistory: {277 description: 'Dummy',278 params: [],279 type: 'u8',280 },281 maxPriorityFeePerGas: {282 description: 'Dummy',283 params: [],284 type: 'u8',285 },286 },287 },288 });289 await this.api.isReadyOrError;290 this.network = await UniqueHelper.detectNetwork(this.api);291 this.wsEndpoint = wsEndpoint;292 }293}294295export class DevRelayHelper extends RelayHelper {296 wait: WaitGroup;297298 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {299 options.helperBase = options.helperBase ?? DevRelayHelper;300301 super(logger, options);302 this.wait = new WaitGroup(this);303 }304}305306export class DevWestmintHelper extends WestmintHelper {307 wait: WaitGroup;308309 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {310 options.helperBase = options.helperBase ?? DevWestmintHelper;311312 super(logger, options);313 this.wait = new WaitGroup(this);314 }315}316317export class DevStatemineHelper extends DevWestmintHelper {}318319export class DevStatemintHelper extends DevWestmintHelper {}320321export class DevMoonbeamHelper extends MoonbeamHelper {322 account: MoonbeamAccountGroup;323 wait: WaitGroup;324 fastDemocracy: MoonbeamFastDemocracyGroup;325326 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {327 options.helperBase = options.helperBase ?? DevMoonbeamHelper;328 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';329330 super(logger, options);331 this.account = new MoonbeamAccountGroup(this);332 this.wait = new WaitGroup(this);333 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);334 }335}336337export class DevMoonriverHelper extends DevMoonbeamHelper {338 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {339 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';340 super(logger, options);341 }342}343344export class DevAstarHelper extends AstarHelper {345 wait: WaitGroup;346347 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {348 options.helperBase = options.helperBase ?? DevAstarHelper;349350 super(logger, options);351 this.wait = new WaitGroup(this);352 }353}354355export class DevShidenHelper extends AstarHelper {356 wait: WaitGroup;357358 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {359 options.helperBase = options.helperBase ?? DevShidenHelper;360361 super(logger, options);362 this.wait = new WaitGroup(this);363 }364}365366export class DevAcalaHelper extends AcalaHelper {367 wait: WaitGroup;368369 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {370 options.helperBase = options.helperBase ?? DevAcalaHelper;371372 super(logger, options);373 this.wait = new WaitGroup(this);374 }375}376377export class DevKaruraHelper extends DevAcalaHelper {}378379export class ArrangeGroup {380 helper: DevUniqueHelper;381382 scheduledIdSlider = 0;383384 constructor(helper: DevUniqueHelper) {385 this.helper = helper;386 }387388 389390391392393394395 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {396 let nonce = await this.helper.chain.getNonce(donor.address);397 const wait = new WaitGroup(this.helper);398 const ss58Format = this.helper.chain.getChainProperties().ss58Format;399 const tokenNominal = this.helper.balance.getOneTokenNominal();400 const transactions = [];401 const accounts: IKeyringPair[] = [];402 for(const balance of balances) {403 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);404 accounts.push(recipient);405 if(balance !== 0n) {406 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);407 transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));408 nonce++;409 }410 }411412 await Promise.all(transactions).catch(_e => {});413414 415 const checkBalances = async () => {416 let isSuccess = true;417 for(let i = 0; i < balances.length; i++) {418 const balance = await this.helper.balance.getSubstrate(accounts[i].address);419 if(balance !== balances[i] * tokenNominal) {420 isSuccess = false;421 break;422 }423 }424 return isSuccess;425 };426427 let accountsCreated = false;428 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;429 430 for(let index = 0; index < maxBlocksChecked; index++) {431 accountsCreated = await checkBalances();432 if(accountsCreated) break;433 await wait.newBlocks(1);434 }435436 if(!accountsCreated) throw Error('Accounts generation failed');437 438439 return accounts;440 };441442 443 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {444 const createAsManyAsCan = async () => {445 let transactions: any = [];446 const accounts: IKeyringPair[] = [];447 let nonce = await this.helper.chain.getNonce(donor.address);448 const tokenNominal = this.helper.balance.getOneTokenNominal();449 const ss58Format = this.helper.chain.getChainProperties().ss58Format;450 for(let i = 0; i < accountsToCreate; i++) {451 if(i === 500) { 452 await Promise.allSettled(transactions); 453 transactions = []; 454 nonce = await this.helper.chain.getNonce(donor.address); 455 }456 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);457 accounts.push(recipient);458 if(withBalance !== 0n) {459 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);460 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));461 nonce++;462 }463 }464465 const fullfilledAccounts = [];466 await Promise.allSettled(transactions);467 for(const account of accounts) {468 const accountBalance = await this.helper.balance.getSubstrate(account.address);469 if(accountBalance === withBalance * tokenNominal) {470 fullfilledAccounts.push(account);471 }472 }473 return fullfilledAccounts;474 };475476477 const crowd: IKeyringPair[] = [];478 479 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {480 const asManyAsCan = await createAsManyAsCan();481 crowd.push(...asManyAsCan);482 accountsToCreate -= asManyAsCan.length;483 }484485 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);486487 return crowd;488 };489490 491492493494495 createEmptyAccount = (): IKeyringPair => {496 const ss58Format = this.helper.chain.getChainProperties().ss58Format;497 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);498 };499500 isDevNode = async () => {501 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();502 if(blockNumber == 0) {503 await this.helper.wait.newBlocks(1);504 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();505 }506 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);507 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);508 const findCreationDate = (block: any) => {509 const humanBlock = block.toHuman();510 let date;511 humanBlock.block.extrinsics.forEach((ext: any) => {512 if(ext.method.section === 'timestamp') {513 date = Number(ext.method.args.now.replaceAll(',', ''));514 }515 });516 return date;517 };518 const block1date = await findCreationDate(block1);519 const block2date = await findCreationDate(block2);520 if(block2date! - block1date! < 9000) return true;521 };522523 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {524 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);525 let balance = await this.helper.balance.getSubstrate(address);526527 await promise();528529 balance -= await this.helper.balance.getSubstrate(address);530531 return balance;532 }533534 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {535 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);536537 const kvJson: {[key: string]: string} = {};538539 for(const kv of rawPovInfo.keyValues) {540 kvJson[kv.key.toHex()] = kv.value.toHex();541 }542543 const kvStr = JSON.stringify(kvJson);544545 const chainql = spawnSync(546 'chainql',547 [548 `--tla-code=data=${kvStr}`,549 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,550 ],551 );552553 if(!chainql.stdout) {554 throw Error('unable to get an output from the `chainql`');555 }556557 return {558 proofSize: rawPovInfo.proofSize.toNumber(),559 compactProofSize: rawPovInfo.compactProofSize.toNumber(),560 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),561 results: rawPovInfo.results,562 kv: JSON.parse(chainql.stdout.toString()),563 };564 }565566 calculatePalletAddress(palletId: any) {567 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));568 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);569 }570571 makeScheduledIds(num: number): string[] {572 function makeId(slider: number) {573 const scheduledIdSize = 64;574 const hexId = slider.toString(16);575 const prefixSize = scheduledIdSize - hexId.length;576577 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;578579 return scheduledId;580 }581582 const ids = [];583 for(let i = 0; i < num; i++) {584 ids.push(makeId(this.scheduledIdSlider));585 this.scheduledIdSlider += 1;586 }587588 return ids;589 }590591 makeScheduledId(): string {592 return (this.makeScheduledIds(1))[0];593 }594595 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {596 const capture = new EventCapture(this.helper, eventSection, eventMethod);597 await capture.startCapture();598599 return capture;600 }601602 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {603 return {604 V2: [605 {606 WithdrawAsset: [607 {608 id,609 fun: {610 Fungible: amount,611 },612 },613 ],614 },615 {616 BuyExecution: {617 fees: {618 id,619 fun: {620 Fungible: amount,621 },622 },623 weightLimit: 'Unlimited',624 },625 },626 {627 DepositAsset: {628 assets: {629 Wild: 'All',630 },631 maxAssets: 1,632 beneficiary: {633 parents: 0,634 interior: {635 X1: {636 AccountId32: {637 network: 'Any',638 id: beneficiary,639 },640 },641 },642 },643 },644 },645 ],646 };647 }648649 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {650 return {651 V2: [652 {653 ReserveAssetDeposited: [654 {655 id,656 fun: {657 Fungible: amount,658 },659 },660 ],661 },662 {663 BuyExecution: {664 fees: {665 id,666 fun: {667 Fungible: amount,668 },669 },670 weightLimit: 'Unlimited',671 },672 },673 {674 DepositAsset: {675 assets: {676 Wild: 'All',677 },678 maxAssets: 1,679 beneficiary: {680 parents: 0,681 interior: {682 X1: {683 AccountId32: {684 network: 'Any',685 id: beneficiary,686 },687 },688 },689 },690 },691 },692 ],693 };694 }695}696697class MoonbeamAccountGroup {698 helper: MoonbeamHelper;699700 keyring: Keyring;701 _alithAccount: IKeyringPair;702 _baltatharAccount: IKeyringPair;703 _dorothyAccount: IKeyringPair;704705 constructor(helper: MoonbeamHelper) {706 this.helper = helper;707708 this.keyring = new Keyring({type: 'ethereum'});709 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';710 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';711 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';712713 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');714 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');715 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');716 }717718 alithAccount() {719 return this._alithAccount;720 }721722 baltatharAccount() {723 return this._baltatharAccount;724 }725726 dorothyAccount() {727 return this._dorothyAccount;728 }729730 create() {731 return this.keyring.addFromUri(mnemonicGenerate());732 }733}734735class MoonbeamFastDemocracyGroup {736 helper: DevMoonbeamHelper;737738 constructor(helper: DevMoonbeamHelper) {739 this.helper = helper;740 }741742 async executeProposal(proposalDesciption: string, encodedProposal: string) {743 const proposalHash = blake2AsHex(encodedProposal);744745 const alithAccount = this.helper.account.alithAccount();746 const baltatharAccount = this.helper.account.baltatharAccount();747 const dorothyAccount = this.helper.account.dorothyAccount();748749 const councilVotingThreshold = 2;750 const technicalCommitteeThreshold = 2;751 const fastTrackVotingPeriod = 3;752 const fastTrackDelayPeriod = 0;753754 console.log(`[democracy] executing '${proposalDesciption}' proposal`);755756 757 console.log('\t* Propose external motion through council.......');758 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});759 const encodedMotion = externalMotion?.method.toHex() || '';760 const motionHash = blake2AsHex(encodedMotion);761 console.log('\t* Motion hash is %s', motionHash);762763 await this.helper.collective.council.propose(764 baltatharAccount,765 councilVotingThreshold,766 externalMotion,767 externalMotion.encodedLength,768 );769770 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;771 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);772 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);773774 await this.helper.collective.council.close(775 dorothyAccount,776 motionHash,777 councilProposalIdx,778 {779 refTime: 1_000_000_000,780 proofSize: 1_000_000,781 },782 externalMotion.encodedLength,783 );784 console.log('\t* Propose external motion through council.......DONE');785 786787 788 console.log('\t* Fast track proposal through technical committee.......');789 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);790 const encodedFastTrack = fastTrack?.method.toHex() || '';791 const fastTrackHash = blake2AsHex(encodedFastTrack);792 console.log('\t* FastTrack hash is %s', fastTrackHash);793794 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);795796 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;797 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);798 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);799800 await this.helper.collective.techCommittee.close(801 baltatharAccount,802 fastTrackHash,803 techProposalIdx,804 {805 refTime: 1_000_000_000,806 proofSize: 1_000_000,807 },808 fastTrack.encodedLength,809 );810 console.log('\t* Fast track proposal through technical committee.......DONE');811 812813 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);814 const referendumIndex = democracyStarted.referendumIndex;815816 817 console.log(`\t* Referendum #${referendumIndex} voting.......`);818 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {819 balance: 10_000_000_000_000_000_000n,820 vote: {aye: true, conviction: 1},821 });822 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);823 824825 826 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);827828 await this.helper.wait.newBlocks(1);829830 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);831 }832}833834class WaitGroup {835 helper: ChainHelperBase;836837 constructor(helper: ChainHelperBase) {838 this.helper = helper;839 }840841 sleep(milliseconds: number) {842 return new Promise((resolve) => setTimeout(resolve, milliseconds));843 }844845 private async waitWithTimeout(promise: Promise<any>, timeout: number) {846 let isBlock = false;847 promise.then(() => isBlock = true).catch(() => isBlock = true);848 let totalTime = 0;849 const step = 100;850 while(!isBlock) {851 await this.sleep(step);852 totalTime += step;853 if(totalTime >= timeout) throw Error('Blocks production failed');854 }855 return promise;856 }857858 859860861862863864865 withTimeout<T>(866 promise: Promise<T>,867 timeoutMS = 30000,868 timeoutError = 'The operation has timed out!',869 ): Promise<T> {870 const timeout = new Promise<never>((_, reject) => {871 setTimeout(() => {872 reject(new Error(timeoutError));873 }, timeoutMS);874 });875876 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});877 }878879 880881882883884 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {885 timeout = timeout ?? blocksCount * 60_000;886 887 const promise = new Promise<void>(async (resolve) => {888 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {889 if(blocksCount > 0) {890 blocksCount--;891 } else {892 unsubscribe();893 resolve();894 }895 });896 });897 await this.waitWithTimeout(promise, timeout);898 return promise;899 }900901 902903904905906907908 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {909 console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`910 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');911912 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;913 let currentSessionIndex = -1;914915 while(currentSessionIndex < expectedSessionIndex) {916 917 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {918 await this.newBlocks(1);919 const res = await (this.helper as DevUniqueHelper).session.getIndex();920 resolve(res);921 }), blockTimeout, 'The chain has stopped producing blocks!');922 }923 }924925 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {926 timeout = timeout ?? 30 * 60 * 1000;927 928 const promise = new Promise<void>(async (resolve) => {929 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {930 if(data.number.toNumber() >= blockNumber) {931 unsubscribe();932 resolve();933 }934 });935 });936 await this.waitWithTimeout(promise, timeout);937 return promise;938 }939940 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {941 timeout = timeout ?? 30 * 60 * 1000;942 943 const promise = new Promise<void>(async (resolve) => {944 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {945 if(data.value.relayParentNumber.toNumber() >= blockNumber) {946 947 unsubscribe();948 resolve();949 }950 });951 });952 await this.waitWithTimeout(promise, timeout);953 return promise;954 }955956 noScheduledTasks() {957 const api = this.helper.getApi();958959 960 const promise = new Promise<void>(async resolve => {961 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {962 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();963964 if(areThereScheduledTasks.length == 0) {965 unsubscribe();966 resolve();967 }968 });969 });970971 return promise;972 }973974 parachainBlockMultiplesOf(val: bigint) {975 976 const promise = new Promise<void>(async resolve => {977 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {978 if(data.number.toBigInt() % val == 0n) {979 console.log(`from waiter: ${data.number.toBigInt()}`);980 unsubscribe();981 resolve();982 }983 });984 });985 return promise;986 }987988 event<T extends IEventHelper>(989 maxBlocksToWait: number,990 eventHelper: T,991 filter: (_: any) => boolean = () => true,992 ): any {993 994 const promise = new Promise<T | null>(async (resolve) => {995 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {996 const blockNumber = header.number.toHuman();997 const blockHash = header.hash;998 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;999 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;10001001 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);10021003 const apiAt = await this.helper.getApi().at(blockHash);1004 const eventRecords = (await apiAt.query.system.events()) as any;10051006 const neededEvent = eventRecords.toArray()1007 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1008 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1009 .find(filter);10101011 if(neededEvent) {1012 unsubscribe();1013 resolve(neededEvent);1014 } else if(maxBlocksToWait > 0) {1015 maxBlocksToWait--;1016 } else {1017 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);1018 unsubscribe();1019 resolve(null);1020 }1021 });1022 });1023 return promise;1024 }10251026 async expectEvent<T extends IEventHelper>(1027 maxBlocksToWait: number,1028 eventHelper: T,1029 filter: (e: any) => boolean = () => true,1030 ) {1031 const e = await this.event(maxBlocksToWait, eventHelper, filter);1032 if(e == null) {1033 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1034 } else {1035 return e;1036 }1037 }1038}10391040class SessionGroup {1041 helper: ChainHelperBase;10421043 constructor(helper: ChainHelperBase) {1044 this.helper = helper;1045 }10461047 1048 async getIndex(): Promise<number> {1049 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1050 }10511052 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1053 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1054 }10551056 setOwnKeys(signer: TSigner, key: string) {1057 return this.helper.executeExtrinsic(1058 signer,1059 'api.tx.session.setKeys',1060 [key, '0x0'],1061 true,1062 );1063 }10641065 setOwnKeysFromAddress(signer: TSigner) {1066 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1067 }1068}10691070class TestUtilGroup {1071 helper: DevUniqueHelper;10721073 constructor(helper: DevUniqueHelper) {1074 this.helper = helper;1075 }10761077 async enable() {1078 if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1079 return;1080 }10811082 const signer = this.helper.util.fromSeed('//Alice');1083 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1084 }10851086 async setTestValue(signer: TSigner, testVal: number) {1087 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1088 }10891090 async incTestValue(signer: TSigner) {1091 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1092 }10931094 async setTestValueAndRollback(signer: TSigner, testVal: number) {1095 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1096 }10971098 async testValue(blockIdx?: number) {1099 const api = blockIdx1100 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1101 : this.helper.getApi();11021103 return (await api.query.testUtils.testValue()).toJSON();1104 }11051106 async justTakeFee(signer: TSigner) {1107 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1108 }11091110 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1111 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1112 }1113}11141115class EventCapture {1116 helper: DevUniqueHelper;1117 eventSection: string;1118 eventMethod: string;1119 events: EventRecord[] = [];1120 unsubscribe: VoidFn | null = null;11211122 constructor(1123 helper: DevUniqueHelper,1124 eventSection: string,1125 eventMethod: string,1126 ) {1127 this.helper = helper;1128 this.eventSection = eventSection;1129 this.eventMethod = eventMethod;1130 }11311132 async startCapture() {1133 this.stopCapture();1134 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1135 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);11361137 this.events.push(...newEvents);1138 })) as any;1139 }11401141 stopCapture() {1142 if(this.unsubscribe !== null) {1143 this.unsubscribe();1144 }1145 }11461147 extractCapturedEvents() {1148 return this.events;1149 }1150}11511152class AdminGroup {1153 helper: UniqueHelper;11541155 constructor(helper: UniqueHelper) {1156 this.helper = helper;1157 }11581159 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1160 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1161 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1162 staker: e.event.data[0].toString(),1163 stake: e.event.data[1].toBigInt(),1164 payout: e.event.data[2].toBigInt(),1165 }));1166 }1167}