1234import {stringToU8a} from '@polkadot/util';5import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, ChainHelperBase, ChainHelperBaseConstructor} 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 {SignerOptions, VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';15import {spawnSync} from 'child_process';16import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper} from './unique.xcm';1718export class SilentLogger {19 log(_msg: any, _level: any): void { }20 level = {21 ERROR: 'ERROR' as const,22 WARNING: 'WARNING' as const,23 INFO: 'INFO' as const,24 };25}2627export class SilentConsole {28 29 30 consoleErr: any;31 consoleLog: any;32 consoleWarn: any;3334 constructor() {35 this.consoleErr = console.error;36 this.consoleLog = console.log;37 this.consoleWarn = console.warn;38 }3940 enable() {41 const outFn = (printer: any) => (...args: any[]) => {42 for(const arg of args) {43 if(typeof arg !== 'string')44 continue;45 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'];46 const needToSkip = skippedWarnings.reduce((a, b) => a || arg.includes(b), false);47 if(needToSkip || arg === 'Normal connection closure')48 return;49 }50 printer(...args);51 };5253 console.error = outFn(this.consoleErr.bind(console));54 console.log = outFn(this.consoleLog.bind(console));55 console.warn = outFn(this.consoleWarn.bind(console));56 }5758 disable() {59 console.error = this.consoleErr;60 console.log = this.consoleLog;61 console.warn = this.consoleWarn;62 }63}6465export interface IEventHelper {66 section(): string;6768 method(): string;6970 wrapEvent(data: any[]): any;71}727374function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {75 const helperClass = class implements IEventHelper {76 wrapEvent: (data: any[]) => any;77 _section: string;78 _method: string;7980 constructor() {81 this.wrapEvent = wrapEvent;82 this._section = section;83 this._method = method;84 }8586 section(): string {87 return this._section;88 }8990 method(): string {91 return this._method;92 }9394 filter(txres: ITransactionResult) {95 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)96 .map(e => this.wrapEvent(e.event.data));97 }9899 find(txres: ITransactionResult) {100 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);101 return e ? this.wrapEvent(e.event.data) : null;102 }103104 expect(txres: ITransactionResult) {105 const e = this.find(txres);106 if(e) {107 return e;108 } else {109 throw Error(`Expected event ${section}.${method}`);110 }111 }112 };113114 return helperClass;115}116117function eventJsonData<T = any>(data: any[], index: number) {118 return data[index].toJSON() as T;119}120121function eventHumanData(data: any[], index: number) {122 return data[index].toHuman();123}124125function eventData<T = any>(data: any[], index: number) {126 return data[index] as T;127}128129130function EventSection(section: string) {131 return class Section {132 static section = section;133134 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {135 const helperClass = EventHelper(Section.section, name, wrapEvent);136 return new helperClass();137 }138 };139}140141function schedulerSection(schedulerInstance: string) {142 return class extends EventSection(schedulerInstance) {143 static Dispatched = this.Method('Dispatched', data => ({144 task: eventJsonData(data, 0),145 id: eventHumanData(data, 1),146 result: data[2],147 }));148149 static PriorityChanged = this.Method('PriorityChanged', data => ({150 task: eventJsonData(data, 0),151 priority: eventJsonData(data, 1),152 }));153 };154}155156export class Event {157 static Democracy = class extends EventSection('democracy') {158 static Proposed = this.Method('Proposed', data => ({159 proposalIndex: eventJsonData<number>(data, 0),160 }));161162 static ExternalTabled = this.Method('ExternalTabled');163164 static Started = this.Method('Started', data => ({165 referendumIndex: eventJsonData<number>(data, 0),166 threshold: eventHumanData(data, 1),167 }));168169 static Voted = this.Method('Voted', data => ({170 voter: eventJsonData(data, 0),171 referendumIndex: eventJsonData<number>(data, 1),172 vote: eventJsonData(data, 2),173 }));174175 static Passed = this.Method('Passed', data => ({176 referendumIndex: eventJsonData<number>(data, 0),177 }));178179 static ProposalCanceled = this.Method('ProposalCanceled', data => ({180 propIndex: eventJsonData<number>(data, 0),181 }));182183 static Cancelled = this.Method('Cancelled', data => ({184 propIndex: eventJsonData<number>(data, 0),185 }));186187 static Vetoed = this.Method('Vetoed', data => ({188 who: eventHumanData(data, 0),189 proposalHash: eventHumanData(data, 1),190 until: eventJsonData<number>(data, 1),191 }));192 };193194 static Council = class extends EventSection('council') {195 static Proposed = this.Method('Proposed', data => ({196 account: eventHumanData(data, 0),197 proposalIndex: eventJsonData<number>(data, 1),198 proposalHash: eventHumanData(data, 2),199 threshold: eventJsonData<number>(data, 3),200 }));201 static Closed = this.Method('Closed', data => ({202 proposalHash: eventHumanData(data, 0),203 yes: eventJsonData<number>(data, 1),204 no: eventJsonData<number>(data, 2),205 }));206 static Executed = this.Method('Executed', data => ({207 proposalHash: eventHumanData(data, 0),208 }));209 };210211 static TechnicalCommittee = class extends EventSection('technicalCommittee') {212 static Proposed = this.Method('Proposed', data => ({213 account: eventHumanData(data, 0),214 proposalIndex: eventJsonData<number>(data, 1),215 proposalHash: eventHumanData(data, 2),216 threshold: eventJsonData<number>(data, 3),217 }));218 static Closed = this.Method('Closed', data => ({219 proposalHash: eventHumanData(data, 0),220 yes: eventJsonData<number>(data, 1),221 no: eventJsonData<number>(data, 2),222 }));223 static Approved = this.Method('Approved', data => ({224 proposalHash: eventHumanData(data, 0),225 }));226 static Executed = this.Method('Executed', data => ({227 proposalHash: eventHumanData(data, 0),228 result: eventHumanData(data, 1),229 }));230 };231232 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {233 static Submitted = this.Method('Submitted', data => ({234 referendumIndex: eventJsonData<number>(data, 0),235 trackId: eventJsonData<number>(data, 1),236 proposal: eventJsonData(data, 2),237 }));238239 static Cancelled = this.Method('Cancelled', data => ({240 index: eventJsonData<number>(data, 0),241 tally: eventJsonData(data, 1),242 }));243 };244245 static UniqueScheduler = schedulerSection('uniqueScheduler');246 static Scheduler = schedulerSection('scheduler');247248 static XcmpQueue = class extends EventSection('xcmpQueue') {249 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({250 messageHash: eventJsonData(data, 0),251 }));252253 static Success = this.Method('Success', data => ({254 messageHash: eventJsonData(data, 0),255 }));256257 static Fail = this.Method('Fail', data => ({258 messageHash: eventJsonData(data, 0),259 outcome: eventData<XcmV2TraitsError>(data, 1),260 }));261 };262}263264265export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {266 return class extends Base {267 constructor(...args: any[]) {268 super(...args);269 }270271 async executeExtrinsic(272 sender: IKeyringPair,273 extrinsic: string,274 params: any[],275 expectSuccess?: boolean,276 options: Partial<SignerOptions> | null = null,277 ): Promise<ITransactionResult> {278 const call = this.constructApiCall(extrinsic, params);279 const result = await super.executeExtrinsic(280 sender,281 'api.tx.sudo.sudo',282 [call],283 expectSuccess,284 options,285 );286287 if(result.status === 'Fail') return result;288289 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;290 if(data.isErr) {291 if(data.asErr.isModule) {292 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;293 const metaError = super.getApi()?.registry.findMetaError(error);294 throw new Error(`${metaError.section}.${metaError.name}`);295 } else if(data.asErr.isToken) {296 throw new Error(`Token: ${data.asErr.asToken}`);297 }298 299 throw new Error(`Misc: ${data.asErr.toHuman()}`);300 }301 return result;302 }303 async executeExtrinsicUncheckedWeight(304 sender: IKeyringPair,305 extrinsic: string,306 params: any[],307 expectSuccess?: boolean,308 options: Partial<SignerOptions> | null = null,309 ): Promise<ITransactionResult> {310 const call = this.constructApiCall(extrinsic, params);311 const result = await super.executeExtrinsic(312 sender,313 'api.tx.sudo.sudoUncheckedWeight',314 [call, {refTime: 0, proofSize: 0}],315 expectSuccess,316 options,317 );318319 if(result.status === 'Fail') return result;320321 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;322 if(data.isErr) {323 if(data.asErr.isModule) {324 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;325 const metaError = super.getApi()?.registry.findMetaError(error);326 throw new Error(`${metaError.section}.${metaError.name}`);327 } else if(data.asErr.isToken) {328 throw new Error(`Token: ${data.asErr.asToken}`);329 }330 331 throw new Error(`Misc: ${data.asErr.toHuman()}`);332 }333 return result;334 }335 };336}337338export class DevUniqueHelper extends UniqueHelper {339 340341342 arrange: ArrangeGroup;343 wait: WaitGroup;344 admin: AdminGroup;345 session: SessionGroup;346 testUtils: TestUtilGroup;347348 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {349 options.helperBase = options.helperBase ?? DevUniqueHelper;350351 super(logger, options);352 this.arrange = new ArrangeGroup(this);353 this.wait = new WaitGroup(this);354 this.admin = new AdminGroup(this);355 this.testUtils = new TestUtilGroup(this);356 this.session = new SessionGroup(this);357 }358359 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {360 if(!wsEndpoint) throw new Error('wsEndpoint was not set');361 const wsProvider = new WsProvider(wsEndpoint);362 this.api = new ApiPromise({363 provider: wsProvider,364 signedExtensions: {365 ContractHelpers: {366 extrinsic: {},367 payload: {},368 },369 CheckMaintenance: {370 extrinsic: {},371 payload: {},372 },373 DisableIdentityCalls: {374 extrinsic: {},375 payload: {},376 },377 FakeTransactionFinalizer: {378 extrinsic: {},379 payload: {},380 },381 },382 rpc: {383 unique: defs.unique.rpc,384 appPromotion: defs.appPromotion.rpc,385 povinfo: defs.povinfo.rpc,386 eth: {387 feeHistory: {388 description: 'Dummy',389 params: [],390 type: 'u8',391 },392 maxPriorityFeePerGas: {393 description: 'Dummy',394 params: [],395 type: 'u8',396 },397 },398 },399 });400 await this.api.isReadyOrError;401 this.network = await UniqueHelper.detectNetwork(this.api);402 this.wsEndpoint = wsEndpoint;403 }404 getSudo<T extends UniqueHelper>() {405 406 const SudoHelperType = SudoHelper(this.helperBase);407 return this.clone(SudoHelperType) as T;408 }409}410411export class DevRelayHelper extends RelayHelper {412 wait: WaitGroup;413414 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {415 options.helperBase = options.helperBase ?? DevRelayHelper;416417 super(logger, options);418 this.wait = new WaitGroup(this);419 }420}421422export class DevWestmintHelper extends WestmintHelper {423 wait: WaitGroup;424425 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {426 options.helperBase = options.helperBase ?? DevWestmintHelper;427428 super(logger, options);429 this.wait = new WaitGroup(this);430 }431}432433export class DevStatemineHelper extends DevWestmintHelper {}434435export class DevStatemintHelper extends DevWestmintHelper {}436437export class DevMoonbeamHelper extends MoonbeamHelper {438 account: MoonbeamAccountGroup;439 wait: WaitGroup;440 fastDemocracy: MoonbeamFastDemocracyGroup;441442 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {443 options.helperBase = options.helperBase ?? DevMoonbeamHelper;444 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';445446 super(logger, options);447 this.account = new MoonbeamAccountGroup(this);448 this.wait = new WaitGroup(this);449 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);450 }451}452453export class DevMoonriverHelper extends DevMoonbeamHelper {454 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {455 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';456 super(logger, options);457 }458}459460export class DevAstarHelper extends AstarHelper {461 wait: WaitGroup;462463 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {464 options.helperBase = options.helperBase ?? DevAstarHelper;465466 super(logger, options);467 this.wait = new WaitGroup(this);468 }469470 getSudo<T extends UniqueHelper>() {471 472 const SudoHelperType = SudoHelper(this.helperBase);473 return this.clone(SudoHelperType) as T;474 }475}476477export class DevShidenHelper extends AstarHelper {478 wait: WaitGroup;479480 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {481 options.helperBase = options.helperBase ?? DevShidenHelper;482483 super(logger, options);484 this.wait = new WaitGroup(this);485 }486}487488export class DevAcalaHelper extends AcalaHelper {489 wait: WaitGroup;490491 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {492 options.helperBase = options.helperBase ?? DevAcalaHelper;493494 super(logger, options);495 this.wait = new WaitGroup(this);496 }497 getSudo<T extends UniqueHelper>() {498 499 const SudoHelperType = SudoHelper(this.helperBase);500 return this.clone(SudoHelperType) as T;501 }502}503504export class DevPolkadexHelper extends PolkadexHelper {505 wait: WaitGroup;506 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {507 options.helperBase = options.helperBase ?? PolkadexHelper;508509 super(logger, options);510 this.wait = new WaitGroup(this);511 }512513 getSudo<T extends PolkadexHelper>() {514 515 const SudoHelperType = SudoHelper(this.helperBase);516 return this.clone(SudoHelperType) as T;517 }518}519520export class DevKaruraHelper extends DevAcalaHelper {}521522export class ArrangeGroup {523 helper: DevUniqueHelper;524525 scheduledIdSlider = 0;526527 constructor(helper: DevUniqueHelper) {528 this.helper = helper;529 }530531 532533534535536537538 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {539 let nonce = await this.helper.chain.getNonce(donor.address);540 const wait = new WaitGroup(this.helper);541 const ss58Format = this.helper.chain.getChainProperties().ss58Format;542 const tokenNominal = this.helper.balance.getOneTokenNominal();543 const transactions = [];544 const accounts: IKeyringPair[] = [];545 for(const balance of balances) {546 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);547 accounts.push(recipient);548 if(balance !== 0n) {549 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);550 transactions.push(this.helper.signTransaction(donor, tx, {nonce, era: 0}, 'account generation'));551 nonce++;552 }553 }554555 await Promise.all(transactions).catch(_e => {});556557 558 const checkBalances = async () => {559 let isSuccess = true;560 for(let i = 0; i < balances.length; i++) {561 const balance = await this.helper.balance.getSubstrate(accounts[i].address);562 if(balance !== balances[i] * tokenNominal) {563 isSuccess = false;564 break;565 }566 }567 return isSuccess;568 };569570 let accountsCreated = false;571 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;572 573 for(let index = 0; index < maxBlocksChecked; index++) {574 accountsCreated = await checkBalances();575 if(accountsCreated) break;576 await wait.newBlocks(1);577 }578579 if(!accountsCreated) throw Error('Accounts generation failed');580 581582 return accounts;583 };584585 586 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {587 const createAsManyAsCan = async () => {588 let transactions: any = [];589 const accounts: IKeyringPair[] = [];590 let nonce = await this.helper.chain.getNonce(donor.address);591 const tokenNominal = this.helper.balance.getOneTokenNominal();592 const ss58Format = this.helper.chain.getChainProperties().ss58Format;593 for(let i = 0; i < accountsToCreate; i++) {594 if(i === 500) { 595 await Promise.allSettled(transactions); 596 transactions = []; 597 nonce = await this.helper.chain.getNonce(donor.address); 598 }599 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);600 accounts.push(recipient);601 if(withBalance !== 0n) {602 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, withBalance * tokenNominal]);603 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));604 nonce++;605 }606 }607608 const fullfilledAccounts = [];609 await Promise.allSettled(transactions);610 for(const account of accounts) {611 const accountBalance = await this.helper.balance.getSubstrate(account.address);612 if(accountBalance === withBalance * tokenNominal) {613 fullfilledAccounts.push(account);614 }615 }616 return fullfilledAccounts;617 };618619620 const crowd: IKeyringPair[] = [];621 622 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {623 const asManyAsCan = await createAsManyAsCan();624 crowd.push(...asManyAsCan);625 accountsToCreate -= asManyAsCan.length;626 }627628 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);629630 return crowd;631 };632633 634635636637638 createEmptyAccount = (): IKeyringPair => {639 const ss58Format = this.helper.chain.getChainProperties().ss58Format;640 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);641 };642643 isDevNode = async () => {644 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();645 if(blockNumber == 0) {646 await this.helper.wait.newBlocks(1);647 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();648 }649 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);650 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);651 const findCreationDate = (block: any) => {652 const humanBlock = block.toHuman();653 let date;654 humanBlock.block.extrinsics.forEach((ext: any) => {655 if(ext.method.section === 'timestamp') {656 date = Number(ext.method.args.now.replaceAll(',', ''));657 }658 });659 return date;660 };661 const block1date = await findCreationDate(block1);662 const block2date = await findCreationDate(block2);663 if(block2date! - block1date! < 9000) return true;664 };665666 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {667 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);668 let balance = await this.helper.balance.getSubstrate(address);669670 await promise();671672 balance -= await this.helper.balance.getSubstrate(address);673674 return balance;675 }676677 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {678 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);679680 const kvJson: {[key: string]: string} = {};681682 for(const kv of rawPovInfo.keyValues) {683 kvJson[kv.key.toHex()] = kv.value.toHex();684 }685686 const kvStr = JSON.stringify(kvJson);687688 const chainql = spawnSync(689 'chainql',690 [691 `--tla-code=data=${kvStr}`,692 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,693 ],694 );695696 if(!chainql.stdout) {697 throw Error('unable to get an output from the `chainql`');698 }699700 return {701 proofSize: rawPovInfo.proofSize.toNumber(),702 compactProofSize: rawPovInfo.compactProofSize.toNumber(),703 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),704 results: rawPovInfo.results,705 kv: JSON.parse(chainql.stdout.toString()),706 };707 }708709 calculatePalletAddress(palletId: any) {710 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));711 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);712 }713714 makeScheduledIds(num: number): string[] {715 function makeId(slider: number) {716 const scheduledIdSize = 64;717 const hexId = slider.toString(16);718 const prefixSize = scheduledIdSize - hexId.length;719720 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;721722 return scheduledId;723 }724725 const ids = [];726 for(let i = 0; i < num; i++) {727 ids.push(makeId(this.scheduledIdSlider));728 this.scheduledIdSlider += 1;729 }730731 return ids;732 }733734 makeScheduledId(): string {735 return (this.makeScheduledIds(1))[0];736 }737738 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {739 const capture = new EventCapture(this.helper, eventSection, eventMethod);740 await capture.startCapture();741742 return capture;743 }744745 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {746 return {747 V2: [748 {749 WithdrawAsset: [750 {751 id,752 fun: {753 Fungible: amount,754 },755 },756 ],757 },758 {759 BuyExecution: {760 fees: {761 id,762 fun: {763 Fungible: amount,764 },765 },766 weightLimit: 'Unlimited',767 },768 },769 {770 DepositAsset: {771 assets: {772 Wild: 'All',773 },774 maxAssets: 1,775 beneficiary: {776 parents: 0,777 interior: {778 X1: {779 AccountId32: {780 network: 'Any',781 id: beneficiary,782 },783 },784 },785 },786 },787 },788 ],789 };790 }791792 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {793 return {794 V2: [795 {796 ReserveAssetDeposited: [797 {798 id,799 fun: {800 Fungible: amount,801 },802 },803 ],804 },805 {806 BuyExecution: {807 fees: {808 id,809 fun: {810 Fungible: amount,811 },812 },813 weightLimit: 'Unlimited',814 },815 },816 {817 DepositAsset: {818 assets: {819 Wild: 'All',820 },821 maxAssets: 1,822 beneficiary: {823 parents: 0,824 interior: {825 X1: {826 AccountId32: {827 network: 'Any',828 id: beneficiary,829 },830 },831 },832 },833 },834 },835 ],836 };837 }838}839840class MoonbeamAccountGroup {841 helper: MoonbeamHelper;842843 keyring: Keyring;844 _alithAccount: IKeyringPair;845 _baltatharAccount: IKeyringPair;846 _dorothyAccount: IKeyringPair;847848 constructor(helper: MoonbeamHelper) {849 this.helper = helper;850851 this.keyring = new Keyring({type: 'ethereum'});852 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';853 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';854 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';855856 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');857 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');858 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');859 }860861 alithAccount() {862 return this._alithAccount;863 }864865 baltatharAccount() {866 return this._baltatharAccount;867 }868869 dorothyAccount() {870 return this._dorothyAccount;871 }872873 create() {874 return this.keyring.addFromUri(mnemonicGenerate());875 }876}877878class MoonbeamFastDemocracyGroup {879 helper: DevMoonbeamHelper;880881 constructor(helper: DevMoonbeamHelper) {882 this.helper = helper;883 }884885 async executeProposal(proposalDesciption: string, encodedProposal: string) {886 const proposalHash = blake2AsHex(encodedProposal);887888 const alithAccount = this.helper.account.alithAccount();889 const baltatharAccount = this.helper.account.baltatharAccount();890 const dorothyAccount = this.helper.account.dorothyAccount();891892 const councilVotingThreshold = 2;893 const technicalCommitteeThreshold = 2;894 const fastTrackVotingPeriod = 3;895 const fastTrackDelayPeriod = 0;896897 console.log(`[democracy] executing '${proposalDesciption}' proposal`);898899 900 console.log('\t* Propose external motion through council.......');901 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});902 const encodedMotion = externalMotion?.method.toHex() || '';903 const motionHash = blake2AsHex(encodedMotion);904 console.log('\t* Motion hash is %s', motionHash);905906 await this.helper.collective.council.propose(907 baltatharAccount,908 councilVotingThreshold,909 externalMotion,910 externalMotion.encodedLength,911 );912913 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;914 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);915 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);916917 await this.helper.collective.council.close(918 dorothyAccount,919 motionHash,920 councilProposalIdx,921 {922 refTime: 1_000_000_000,923 proofSize: 1_000_000,924 },925 externalMotion.encodedLength,926 );927 console.log('\t* Propose external motion through council.......DONE');928 929930 931 console.log('\t* Fast track proposal through technical committee.......');932 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);933 const encodedFastTrack = fastTrack?.method.toHex() || '';934 const fastTrackHash = blake2AsHex(encodedFastTrack);935 console.log('\t* FastTrack hash is %s', fastTrackHash);936937 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);938939 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;940 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);941 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);942943 await this.helper.collective.techCommittee.close(944 baltatharAccount,945 fastTrackHash,946 techProposalIdx,947 {948 refTime: 1_000_000_000,949 proofSize: 1_000_000,950 },951 fastTrack.encodedLength,952 );953 console.log('\t* Fast track proposal through technical committee.......DONE');954 955956 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);957 const referendumIndex = democracyStarted.referendumIndex;958959 960 console.log(`\t* Referendum #${referendumIndex} voting.......`);961 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {962 balance: 10_000_000_000_000_000_000n,963 vote: {aye: true, conviction: 1},964 });965 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);966 967968 969 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);970971 await this.helper.wait.newBlocks(1);972973 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);974 }975}976977class WaitGroup {978 helper: ChainHelperBase;979980 constructor(helper: ChainHelperBase) {981 this.helper = helper;982 }983984 sleep(milliseconds: number) {985 return new Promise((resolve) => setTimeout(resolve, milliseconds));986 }987988 private async waitWithTimeout(promise: Promise<any>, timeout: number) {989 let isBlock = false;990 promise.then(() => isBlock = true).catch(() => isBlock = true);991 let totalTime = 0;992 const step = 100;993 while(!isBlock) {994 await this.sleep(step);995 totalTime += step;996 if(totalTime >= timeout) throw Error('Blocks production failed');997 }998 return promise;999 }10001001 1002100310041005100610071008 withTimeout<T>(1009 promise: Promise<T>,1010 timeoutMS = 30000,1011 timeoutError = 'The operation has timed out!',1012 ): Promise<T> {1013 const timeout = new Promise<never>((_, reject) => {1014 setTimeout(() => {1015 reject(new Error(timeoutError));1016 }, timeoutMS);1017 });10181019 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1020 }10211022 10231024102510261027 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1028 timeout = timeout ?? blocksCount * 60_000;1029 1030 const promise = new Promise<void>(async (resolve) => {1031 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1032 if(blocksCount > 0) {1033 blocksCount--;1034 } else {1035 unsubscribe();1036 resolve();1037 }1038 });1039 });1040 await this.waitWithTimeout(promise, timeout);1041 return promise;1042 }10431044 1045104610471048104910501051 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1052 console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.`1053 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');10541055 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1056 let currentSessionIndex = -1;10571058 while(currentSessionIndex < expectedSessionIndex) {1059 1060 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1061 await this.newBlocks(1);1062 const res = await (this.helper as DevUniqueHelper).session.getIndex();1063 resolve(res);1064 }), blockTimeout, 'The chain has stopped producing blocks!');1065 }1066 }10671068 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1069 timeout = timeout ?? 30 * 60 * 1000;1070 1071 const promise = new Promise<void>(async (resolve) => {1072 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1073 if(data.number.toNumber() >= blockNumber) {1074 unsubscribe();1075 resolve();1076 }1077 });1078 });1079 await this.waitWithTimeout(promise, timeout);1080 return promise;1081 }10821083 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1084 timeout = timeout ?? 30 * 60 * 1000;1085 1086 const promise = new Promise<void>(async (resolve) => {1087 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1088 if(data.value.relayParentNumber.toNumber() >= blockNumber) {1089 1090 unsubscribe();1091 resolve();1092 }1093 });1094 });1095 await this.waitWithTimeout(promise, timeout);1096 return promise;1097 }10981099 noScheduledTasks() {1100 const api = this.helper.getApi();11011102 1103 const promise = new Promise<void>(async resolve => {1104 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1105 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();11061107 if(areThereScheduledTasks.length == 0) {1108 unsubscribe();1109 resolve();1110 }1111 });1112 });11131114 return promise;1115 }11161117 parachainBlockMultiplesOf(val: bigint) {1118 1119 const promise = new Promise<void>(async resolve => {1120 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1121 if(data.number.toBigInt() % val == 0n) {1122 console.log(`from waiter: ${data.number.toBigInt()}`);1123 unsubscribe();1124 resolve();1125 }1126 });1127 });1128 return promise;1129 }11301131 event<T extends IEventHelper>(1132 maxBlocksToWait: number,1133 eventHelper: T,1134 filter: (_: any) => boolean = () => true,1135 ): any {1136 1137 const promise = new Promise<T | null>(async (resolve) => {1138 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1139 const blockNumber = header.number.toHuman();1140 const blockHash = header.hash;1141 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1142 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;11431144 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);11451146 const apiAt = await this.helper.getApi().at(blockHash);1147 const eventRecords = (await apiAt.query.system.events()) as any;11481149 const neededEvent = eventRecords.toArray()1150 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1151 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1152 .find(filter);11531154 if(neededEvent) {1155 unsubscribe();1156 resolve(neededEvent);1157 } else if(maxBlocksToWait > 0) {1158 maxBlocksToWait--;1159 } else {1160 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found`);1161 unsubscribe();1162 resolve(null);1163 }1164 });1165 });1166 return promise;1167 }11681169 async expectEvent<T extends IEventHelper>(1170 maxBlocksToWait: number,1171 eventHelper: T,1172 filter: (e: any) => boolean = () => true,1173 ) {1174 const e = await this.event(maxBlocksToWait, eventHelper, filter);1175 if(e == null) {1176 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1177 } else {1178 return e;1179 }1180 }1181}11821183class SessionGroup {1184 helper: ChainHelperBase;11851186 constructor(helper: ChainHelperBase) {1187 this.helper = helper;1188 }11891190 1191 async getIndex(): Promise<number> {1192 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1193 }11941195 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1196 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1197 }11981199 setOwnKeys(signer: TSigner, key: string) {1200 return this.helper.executeExtrinsic(1201 signer,1202 'api.tx.session.setKeys',1203 [key, '0x0'],1204 true,1205 );1206 }12071208 setOwnKeysFromAddress(signer: TSigner) {1209 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1210 }1211}12121213class TestUtilGroup {1214 helper: DevUniqueHelper;12151216 constructor(helper: DevUniqueHelper) {1217 this.helper = helper;1218 }12191220 async enable() {1221 if(this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {1222 return;1223 }12241225 const signer = this.helper.util.fromSeed('//Alice');1226 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1227 }12281229 async setTestValue(signer: TSigner, testVal: number) {1230 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1231 }12321233 async incTestValue(signer: TSigner) {1234 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1235 }12361237 async setTestValueAndRollback(signer: TSigner, testVal: number) {1238 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1239 }12401241 async testValue(blockIdx?: number) {1242 const api = blockIdx1243 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1244 : this.helper.getApi();12451246 return (await api.query.testUtils.testValue()).toJSON();1247 }12481249 async justTakeFee(signer: TSigner) {1250 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1251 }12521253 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1254 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1255 }1256}12571258class EventCapture {1259 helper: DevUniqueHelper;1260 eventSection: string;1261 eventMethod: string;1262 events: EventRecord[] = [];1263 unsubscribe: VoidFn | null = null;12641265 constructor(1266 helper: DevUniqueHelper,1267 eventSection: string,1268 eventMethod: string,1269 ) {1270 this.helper = helper;1271 this.eventSection = eventSection;1272 this.eventMethod = eventMethod;1273 }12741275 async startCapture() {1276 this.stopCapture();1277 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1278 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);12791280 this.events.push(...newEvents);1281 })) as any;1282 }12831284 stopCapture() {1285 if(this.unsubscribe !== null) {1286 this.unsubscribe();1287 }1288 }12891290 extractCapturedEvents() {1291 return this.events;1292 }1293}12941295class AdminGroup {1296 helper: UniqueHelper;12971298 constructor(helper: UniqueHelper) {1299 this.helper = helper;1300 }13011302 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1303 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1304 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1305 staker: e.event.data[0].toString(),1306 stake: e.event.data[1].toBigInt(),1307 payout: e.event.data[2].toBigInt(),1308 }));1309 }1310}