1234import {stringToU8a} from '@polkadot/util';5import {encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';6import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper} from './unique';7import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';8import * as defs from '../../interfaces/definitions';9import {IKeyringPair} from '@polkadot/types/types';10import {EventRecord} from '@polkadot/types/interfaces';11import {ICrossAccountId, TSigner} from './types';12import {FrameSystemEventRecord} from '@polkadot/types/lookup';13import {VoidFn} from '@polkadot/api/types';14import {Pallets} from '..';1516export class SilentLogger {17 log(_msg: any, _level: any): void { }18 level = {19 ERROR: 'ERROR' as const,20 WARNING: 'WARNING' as const,21 INFO: 'INFO' as const,22 };23}2425export class SilentConsole {26 27 28 consoleErr: any;29 consoleLog: any;30 consoleWarn: any;3132 constructor() {33 this.consoleErr = console.error;34 this.consoleLog = console.log;35 this.consoleWarn = console.warn;36 }3738 enable() { 39 const outFn = (printer: any) => (...args: any[]) => {40 for (const arg of args) {41 if (typeof arg !== 'string')42 continue;43 if (arg.includes('1000:: Normal connection closure') || arg.includes('Not decorating unknown runtime apis:') || arg.includes('RPC methods not decorated:') || arg === 'Normal connection closure')44 return;45 }46 printer(...args);47 };48 49 console.error = outFn(this.consoleErr.bind(console));50 console.log = outFn(this.consoleLog.bind(console));51 console.warn = outFn(this.consoleWarn.bind(console));52 }5354 disable() {55 console.error = this.consoleErr;56 console.log = this.consoleLog;57 console.warn = this.consoleWarn;58 }59}6061export class DevUniqueHelper extends UniqueHelper {62 636465 arrange: ArrangeGroup;66 wait: WaitGroup;67 admin: AdminGroup;68 session: SessionGroup;69 testUtils: TestUtilGroup;7071 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {72 options.helperBase = options.helperBase ?? DevUniqueHelper;7374 super(logger, options);75 this.arrange = new ArrangeGroup(this);76 this.wait = new WaitGroup(this);77 this.admin = new AdminGroup(this);78 this.testUtils = new TestUtilGroup(this);79 this.session = new SessionGroup(this);80 }8182 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {83 const wsProvider = new WsProvider(wsEndpoint);84 this.api = new ApiPromise({85 provider: wsProvider,86 signedExtensions: {87 ContractHelpers: {88 extrinsic: {},89 payload: {},90 },91 CheckMaintenance: {92 extrinsic: {},93 payload: {},94 },95 FilterIdentity: {96 extrinsic: {},97 payload: {},98 },99 FakeTransactionFinalizer: {100 extrinsic: {},101 payload: {},102 },103 },104 rpc: {105 unique: defs.unique.rpc,106 appPromotion: defs.appPromotion.rpc,107 rmrk: defs.rmrk.rpc,108 eth: {109 feeHistory: {110 description: 'Dummy',111 params: [],112 type: 'u8',113 },114 maxPriorityFeePerGas: {115 description: 'Dummy',116 params: [],117 type: 'u8',118 },119 },120 },121 });122 await this.api.isReadyOrError;123 this.network = await UniqueHelper.detectNetwork(this.api);124 }125}126127export class DevRelayHelper extends RelayHelper {}128129export class DevWestmintHelper extends WestmintHelper {130 wait: WaitGroup;131132 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {133 options.helperBase = options.helperBase ?? DevWestmintHelper;134135 super(logger, options);136 this.wait = new WaitGroup(this);137 }138}139140export class DevMoonbeamHelper extends MoonbeamHelper {141 account: MoonbeamAccountGroup;142 wait: WaitGroup;143144 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {145 options.helperBase = options.helperBase ?? DevMoonbeamHelper;146147 super(logger, options);148 this.account = new MoonbeamAccountGroup(this);149 this.wait = new WaitGroup(this);150 }151}152153export class DevMoonriverHelper extends DevMoonbeamHelper {}154155export class DevAcalaHelper extends AcalaHelper {156 wait: WaitGroup;157158 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {159 options.helperBase = options.helperBase ?? DevAcalaHelper;160161 super(logger, options);162 this.wait = new WaitGroup(this);163 }164}165166export class DevKaruraHelper extends DevAcalaHelper {}167168class ArrangeGroup {169 helper: DevUniqueHelper;170171 scheduledIdSlider = 0;172173 constructor(helper: DevUniqueHelper) {174 this.helper = helper;175 }176177 178179180181182183184 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {185 let nonce = await this.helper.chain.getNonce(donor.address);186 const wait = new WaitGroup(this.helper);187 const ss58Format = this.helper.chain.getChainProperties().ss58Format;188 const tokenNominal = this.helper.balance.getOneTokenNominal();189 const transactions = [];190 const accounts: IKeyringPair[] = [];191 for (const balance of balances) {192 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);193 accounts.push(recipient);194 if (balance !== 0n) {195 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recipient.address}, balance * tokenNominal]);196 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));197 nonce++;198 }199 }200201 await Promise.all(transactions).catch(_e => {});202 203 204 const checkBalances = async () => {205 let isSuccess = true;206 for (let i = 0; i < balances.length; i++) {207 const balance = await this.helper.balance.getSubstrate(accounts[i].address);208 if (balance !== balances[i] * tokenNominal) {209 isSuccess = false;210 break;211 }212 }213 return isSuccess;214 };215216 let accountsCreated = false;217 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;218 219 for (let index = 0; index < maxBlocksChecked; index++) {220 accountsCreated = await checkBalances();221 if(accountsCreated) break;222 await wait.newBlocks(1);223 }224225 if (!accountsCreated) throw Error('Accounts generation failed');226 227228 return accounts;229 };230231 232 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => { 233 const createAsManyAsCan = async () => {234 let transactions: any = [];235 const accounts: IKeyringPair[] = [];236 let nonce = await this.helper.chain.getNonce(donor.address);237 const tokenNominal = this.helper.balance.getOneTokenNominal();238 for (let i = 0; i < accountsToCreate; i++) {239 if (i === 500) { 240 await Promise.allSettled(transactions); 241 transactions = []; 242 nonce = await this.helper.chain.getNonce(donor.address); 243 }244 const recepient = this.helper.util.fromSeed(mnemonicGenerate());245 accounts.push(recepient);246 if (withBalance !== 0n) {247 const tx = this.helper.constructApiCall('api.tx.balances.transfer', [{Id: recepient.address}, withBalance * tokenNominal]);248 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));249 nonce++;250 }251 }252 253 const fullfilledAccounts = [];254 await Promise.allSettled(transactions);255 for (const account of accounts) {256 const accountBalance = await this.helper.balance.getSubstrate(account.address);257 if (accountBalance === withBalance * tokenNominal) {258 fullfilledAccounts.push(account);259 }260 }261 return fullfilledAccounts;262 };263264 265 const crowd: IKeyringPair[] = [];266 267 for (let index = 0; index < 5 && accountsToCreate !== 0; index++) {268 const asManyAsCan = await createAsManyAsCan();269 crowd.push(...asManyAsCan);270 accountsToCreate -= asManyAsCan.length;271 }272273 if (accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);274275 return crowd;276 };277278 isDevNode = async () => {279 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();280 if (blockNumber == 0) {281 await this.helper.wait.newBlocks(1); 282 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();283 }284 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);285 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);286 const findCreationDate = (block: any) => {287 const humanBlock = block.toHuman();288 let date;289 humanBlock.block.extrinsics.forEach((ext: any) => {290 if(ext.method.section === 'timestamp') {291 date = Number(ext.method.args.now.replaceAll(',', ''));292 }293 });294 return date;295 };296 const block1date = await findCreationDate(block1);297 const block2date = await findCreationDate(block2);298 if(block2date! - block1date! < 9000) return true;299 };300 301 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {302 const address = payer.Substrate ? payer.Substrate : await this.helper.address.ethToSubstrate(payer.Ethereum!);303 let balance = await this.helper.balance.getSubstrate(address); 304 305 await promise();306 307 balance -= await this.helper.balance.getSubstrate(address);308 309 return balance;310 }311312 calculatePalletAddress(palletId: any) {313 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));314 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);315 }316317 makeScheduledIds(num: number): string[] {318 function makeId(slider: number) {319 const scheduledIdSize = 64;320 const hexId = slider.toString(16);321 const prefixSize = scheduledIdSize - hexId.length;322323 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;324325 return scheduledId; 326 }327328 const ids = [];329 for (let i = 0; i < num; i++) {330 ids.push(makeId(this.scheduledIdSlider));331 this.scheduledIdSlider += 1;332 }333334 return ids;335 }336337 makeScheduledId(): string {338 return (this.makeScheduledIds(1))[0];339 }340341 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {342 const capture = new EventCapture(this.helper, eventSection, eventMethod);343 await capture.startCapture();344345 return capture;346 }347}348349class MoonbeamAccountGroup {350 helper: MoonbeamHelper;351352 keyring: Keyring;353 _alithAccount: IKeyringPair;354 _baltatharAccount: IKeyringPair;355 _dorothyAccount: IKeyringPair;356357 constructor(helper: MoonbeamHelper) {358 this.helper = helper;359360 this.keyring = new Keyring({type: 'ethereum'});361 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';362 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';363 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';364365 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');366 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');367 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');368 }369370 alithAccount() {371 return this._alithAccount;372 }373374 baltatharAccount() {375 return this._baltatharAccount;376 }377378 dorothyAccount() {379 return this._dorothyAccount;380 }381382 create() {383 return this.keyring.addFromUri(mnemonicGenerate());384 }385}386387class WaitGroup {388 helper: ChainHelperBase;389390 constructor(helper: ChainHelperBase) {391 this.helper = helper;392 }393394 sleep(milliseconds: number) {395 return new Promise((resolve) => setTimeout(resolve, milliseconds));396 }397398 private async waitWithTimeout(promise: Promise<any>, timeout: number) {399 let isBlock = false;400 promise.then(() => isBlock = true).catch(() => isBlock = true);401 let totalTime = 0;402 const step = 100;403 while(!isBlock) {404 await this.sleep(step);405 totalTime += step;406 if(totalTime >= timeout) throw Error('Blocks production failed');407 }408 return promise;409 }410411 412413414415416417418 async withTimeout<T>(419 promise: Promise<T>,420 timeoutMS = 30000,421 timeoutError = 'The operation has timed out!',422 ): Promise<T> {423 const timeout = new Promise<never>((_, reject) => {424 setTimeout(() => {425 reject(new Error(timeoutError));426 }, timeoutMS);427 });428 429 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});430 }431432 433434435436437 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {438 timeout = timeout ?? blocksCount * 60_000;439 440 const promise = new Promise<void>(async (resolve) => {441 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {442 if (blocksCount > 0) {443 blocksCount--;444 } else {445 unsubscribe();446 resolve();447 }448 });449 });450 await this.waitWithTimeout(promise, timeout);451 return promise;452 }453454 455456457458459460461 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {462 console.log(`Waiting for ${sessionCount} new session${sessionCount > 1 ? 's' : ''}.` 463 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');464465 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;466 let currentSessionIndex = -1;467468 while (currentSessionIndex < expectedSessionIndex) {469 470 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {471 await this.newBlocks(1);472 const res = await (this.helper as DevUniqueHelper).session.getIndex();473 resolve(res);474 }), blockTimeout, 'The chain has stopped producing blocks!');475 }476 }477478 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {479 timeout = timeout ?? 30 * 60 * 1000;480 481 const promise = new Promise<void>(async (resolve) => {482 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {483 if (data.number.toNumber() >= blockNumber) {484 unsubscribe();485 resolve();486 }487 });488 });489 await this.waitWithTimeout(promise, timeout);490 return promise;491 }492 493 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {494 timeout = timeout ?? 30 * 60 * 1000;495 496 const promise = new Promise<void>(async (resolve) => {497 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {498 if (data.value.relayParentNumber.toNumber() >= blockNumber) {499 500 unsubscribe();501 resolve();502 }503 });504 });505 await this.waitWithTimeout(promise, timeout);506 return promise;507 }508509 noScheduledTasks() {510 const api = this.helper.getApi();511 512 513 const promise = new Promise<void>(async resolve => {514 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {515 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();516517 if(areThereScheduledTasks.length == 0) {518 unsubscribe();519 resolve();520 }521 }); 522 });523524 return promise;525 }526527 event(maxBlocksToWait: number, eventSection: string, eventMethod: string) {528 529 const promise = new Promise<EventRecord | null>(async (resolve) => {530 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {531 const blockNumber = header.number.toHuman();532 const blockHash = header.hash;533 const eventIdStr = `${eventSection}.${eventMethod}`;534 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;535 536 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);537 538 const apiAt = await this.helper.getApi().at(blockHash);539 const eventRecords = (await apiAt.query.system.events()) as any;540 541 const neededEvent = eventRecords.toArray().find((r: FrameSystemEventRecord) => {542 return r.event.section == eventSection && r.event.method == eventMethod;543 });544 545 if (neededEvent) {546 unsubscribe();547 resolve(neededEvent);548 } else if (maxBlocksToWait > 0) {549 maxBlocksToWait--;550 } else {551 this.helper.logger.log(`Event \`${eventIdStr}\` is NOT found`);552 unsubscribe();553 resolve(null);554 }555 });556 });557 return promise;558 }559}560561class SessionGroup {562 helper: ChainHelperBase;563564 constructor(helper: ChainHelperBase) {565 this.helper = helper;566 }567 568 569 async getIndex(): Promise<number> {570 return (await this.helper.callRpc('api.query.session.currentIndex')).toNumber();571 }572573 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {574 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);575 }576577 setOwnKeys(signer: TSigner, key: string) {578 return this.helper.executeExtrinsic(579 signer,580 'api.tx.session.setKeys', 581 [key, '0x0'],582 true,583 );584 }585586 setOwnKeysFromAddress(signer: TSigner) {587 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));588 }589}590591class TestUtilGroup {592 helper: DevUniqueHelper;593594 constructor(helper: DevUniqueHelper) {595 this.helper = helper;596 }597598 async enable() {599 if (this.helper.fetchMissingPalletNames([Pallets.TestUtils]).length != 0) {600 return;601 }602603 const signer = this.helper.util.fromSeed('//Alice');604 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);605 }606607 async setTestValue(signer: TSigner, testVal: number) {608 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);609 }610611 async incTestValue(signer: TSigner) {612 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);613 }614615 async setTestValueAndRollback(signer: TSigner, testVal: number) {616 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);617 }618619 async testValue(blockIdx?: number) {620 const api = blockIdx621 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))622 : this.helper.getApi();623624 return (await api.query.testUtils.testValue()).toJSON();625 }626627 async justTakeFee(signer: TSigner) {628 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);629 }630631 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {632 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);633 }634}635636class EventCapture {637 helper: DevUniqueHelper;638 eventSection: string;639 eventMethod: string;640 events: EventRecord[] = [];641 unsubscribe: VoidFn | null = null;642643 constructor(644 helper: DevUniqueHelper,645 eventSection: string,646 eventMethod: string,647 ) {648 this.helper = helper;649 this.eventSection = eventSection;650 this.eventMethod = eventMethod;651 }652653 async startCapture() {654 this.stopCapture();655 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {656 const newEvents = eventRecords.filter(r => {657 return r.event.section == this.eventSection && r.event.method == this.eventMethod;658 });659660 this.events.push(...newEvents);661 })) as any;662 }663664 stopCapture() {665 if (this.unsubscribe !== null) {666 this.unsubscribe();667 }668 }669670 extractCapturedEvents() {671 return this.events;672 }673}674675class AdminGroup {676 helper: UniqueHelper;677678 constructor(helper: UniqueHelper) {679 this.helper = helper;680 }681682 async payoutStakers(signer: IKeyringPair, stakersToPayout: number) {683 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);684 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => {685 return {686 staker: e.event.data[0].toString(),687 stake: e.event.data[1].toBigInt(),688 payout: e.event.data[2].toBigInt(),689 };690 });691 }692}