difftreelog
Tests/fin council (#1070)
in: master
7 files changed
js-packages/.vscode/settings.jsondiffbeforeafterboth--- a/js-packages/.vscode/settings.json
+++ b/js-packages/.vscode/settings.json
@@ -5,6 +5,8 @@
},
"mochaExplorer.files": "tests/**/*.test.ts",
"mochaExplorer.require": "ts-node/register",
+ "mochaExplorer.esmLoader": true,
+ "mochaExplorer.nodeArgv": ["--loader", "ts-node/esm"],
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
js-packages/test-utils/index.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import '@unique-nft/opal-testnet-types/augment-api.js';5import '@unique-nft/opal-testnet-types/augment-types.js';6import '@unique-nft/opal-testnet-types/types-lookup.js';78import {stringToU8a} from '@polkadot/util';9import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';10import type {ChainHelperBaseConstructor, UniqueHelperConstructor} from '@unique-nft/playgrounds/unique.js';11import {UniqueHelper, ChainHelperBase, HelperGroup} from '@unique-nft/playgrounds/unique.js';12import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';13import * as defs from '@unique-nft/opal-testnet-types/definitions.js';14import type {IKeyringPair} from '@polkadot/types/types';15import type {EventRecord} from '@polkadot/types/interfaces';16import type {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from '@unique-nft/playgrounds/types.js';17import type {FrameSystemEventRecord, StagingXcmV2TraitsError, StagingXcmV3TraitsOutcome} from '@polkadot/types/lookup';18import type {SignerOptions, VoidFn} from '@polkadot/api/types';19import {spawnSync} from 'child_process';20import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup, HydraDxHelper} from './xcm/index.js';21import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, RankedCollectiveGroup, ReferendaGroup} from './governance.js';22import type {ICollectiveGroup, IFellowshipGroup} from './governance.js';2324export class SilentLogger {25 log(_msg: any, _level: any): void { }26 level = {27 ERROR: 'ERROR' as const,28 WARNING: 'WARNING' as const,29 INFO: 'INFO' as const,30 };31}3233export class SilentConsole {34 // TODO: Remove, this is temporary: Filter unneeded API output35 // (Jaco promised it will be removed in the next version)36 consoleErr: any;37 consoleLog: any;38 consoleWarn: any;3940 constructor() {41 this.consoleErr = console.error;42 this.consoleLog = console.log;43 this.consoleWarn = console.warn;44 }4546 enable() {47 const outFn = (printer: any) => (...args: any[]) => {48 for(const arg of args) {49 if(typeof arg !== 'string')50 continue;51 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'];52 const needToSkip = skippedWarnings.reduce((a, b) => a || arg.includes(b), false);53 if(needToSkip || arg === 'Normal connection closure')54 return;55 }56 printer(...args);57 };5859 console.error = outFn(this.consoleErr.bind(console));60 console.log = outFn(this.consoleLog.bind(console));61 console.warn = outFn(this.consoleWarn.bind(console));62 }6364 disable() {65 console.error = this.consoleErr;66 console.log = this.consoleLog;67 console.warn = this.consoleWarn;68 }69}7071export interface IEventHelper {72 section(): string;7374 method(): string;7576 wrapEvent(data: any[]): any;77}7879// eslint-disable-next-line @typescript-eslint/naming-convention80function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {81 const helperClass = class implements IEventHelper {82 wrapEvent: (data: any[]) => any;83 _section: string;84 _method: string;8586 constructor() {87 this.wrapEvent = wrapEvent;88 this._section = section;89 this._method = method;90 }9192 section(): string {93 return this._section;94 }9596 method(): string {97 return this._method;98 }99100 filter(txres: ITransactionResult) {101 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)102 .map(e => this.wrapEvent(e.event.data));103 }104105 find(txres: ITransactionResult) {106 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);107 return e ? this.wrapEvent(e.event.data) : null;108 }109110 expect(txres: ITransactionResult) {111 const e = this.find(txres);112 if(e) {113 return e;114 } else {115 throw Error(`Expected event ${section}.${method}`);116 }117 }118 };119120 return helperClass;121}122123function eventJsonData<T = any>(data: any[], index: number) {124 return data[index].toJSON() as T;125}126127function eventHumanData(data: any[], index: number) {128 return data[index].toHuman();129}130131function eventData<T = any>(data: any[], index: number) {132 return data[index] as T;133}134135// eslint-disable-next-line @typescript-eslint/naming-convention136function EventSection(section: string) {137 return class Section {138 static section = section;139140 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {141 const helperClass = EventHelper(Section.section, name, wrapEvent);142 return new helperClass();143 }144 };145}146147function schedulerSection(schedulerInstance: string) {148 return class extends EventSection(schedulerInstance) {149 static Dispatched = this.Method('Dispatched', data => ({150 task: eventJsonData(data, 0),151 id: eventHumanData(data, 1),152 result: data[2],153 }));154155 static PriorityChanged = this.Method('PriorityChanged', data => ({156 task: eventJsonData(data, 0),157 priority: eventJsonData(data, 1),158 }));159 };160}161162export class Event {163 static Democracy = class extends EventSection('democracy') {164 static Proposed = this.Method('Proposed', data => ({165 proposalIndex: eventJsonData<number>(data, 0),166 }));167168 static ExternalTabled = this.Method('ExternalTabled');169170 static Started = this.Method('Started', data => ({171 referendumIndex: eventJsonData<number>(data, 0),172 threshold: eventHumanData(data, 1),173 }));174175 static Voted = this.Method('Voted', data => ({176 voter: eventJsonData(data, 0),177 referendumIndex: eventJsonData<number>(data, 1),178 vote: eventJsonData(data, 2),179 }));180181 static Passed = this.Method('Passed', data => ({182 referendumIndex: eventJsonData<number>(data, 0),183 }));184185 static ProposalCanceled = this.Method('ProposalCanceled', data => ({186 propIndex: eventJsonData<number>(data, 0),187 }));188189 static Cancelled = this.Method('Cancelled', data => ({190 propIndex: eventJsonData<number>(data, 0),191 }));192193 static Vetoed = this.Method('Vetoed', data => ({194 who: eventHumanData(data, 0),195 proposalHash: eventHumanData(data, 1),196 until: eventJsonData<number>(data, 1),197 }));198 };199200 static Council = class extends EventSection('council') {201 static Proposed = this.Method('Proposed', data => ({202 account: eventHumanData(data, 0),203 proposalIndex: eventJsonData<number>(data, 1),204 proposalHash: eventHumanData(data, 2),205 threshold: eventJsonData<number>(data, 3),206 }));207 static Closed = this.Method('Closed', data => ({208 proposalHash: eventHumanData(data, 0),209 yes: eventJsonData<number>(data, 1),210 no: eventJsonData<number>(data, 2),211 }));212 static Executed = this.Method('Executed', data => ({213 proposalHash: eventHumanData(data, 0),214 }));215 };216217 static TechnicalCommittee = class extends EventSection('technicalCommittee') {218 static Proposed = this.Method('Proposed', data => ({219 account: eventHumanData(data, 0),220 proposalIndex: eventJsonData<number>(data, 1),221 proposalHash: eventHumanData(data, 2),222 threshold: eventJsonData<number>(data, 3),223 }));224 static Closed = this.Method('Closed', data => ({225 proposalHash: eventHumanData(data, 0),226 yes: eventJsonData<number>(data, 1),227 no: eventJsonData<number>(data, 2),228 }));229 static Approved = this.Method('Approved', data => ({230 proposalHash: eventHumanData(data, 0),231 }));232 static Executed = this.Method('Executed', data => ({233 proposalHash: eventHumanData(data, 0),234 result: eventHumanData(data, 1),235 }));236 };237238 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {239 static Submitted = this.Method('Submitted', data => ({240 referendumIndex: eventJsonData<number>(data, 0),241 trackId: eventJsonData<number>(data, 1),242 proposal: eventJsonData(data, 2),243 }));244245 static Cancelled = this.Method('Cancelled', data => ({246 index: eventJsonData<number>(data, 0),247 tally: eventJsonData(data, 1),248 }));249 };250251 static UniqueScheduler = schedulerSection('uniqueScheduler');252 static Scheduler = schedulerSection('scheduler');253254 static XcmpQueue = class extends EventSection('xcmpQueue') {255 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({256 messageHash: eventJsonData(data, 0),257 }));258259 static Success = this.Method('Success', data => ({260 messageHash: eventJsonData(data, 0),261 }));262263 static Fail = this.Method('Fail', data => ({264 messageHash: eventJsonData(data, 0),265 outcome: eventData<StagingXcmV2TraitsError>(data, 2),266 }));267 };268269 static DmpQueue = class extends EventSection('dmpQueue') {270 static ExecutedDownward = this.Method('ExecutedDownward', data => ({271 outcome: eventData<StagingXcmV3TraitsOutcome>(data, 2),272 }));273 };274}275276// eslint-disable-next-line @typescript-eslint/naming-convention277export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {278 return class extends Base {279 constructor(...args: any[]) {280 super(...args);281 }282283 override async executeExtrinsic(284 sender: IKeyringPair,285 extrinsic: string,286 params: any[],287 expectSuccess?: boolean,288 options: Partial<SignerOptions> | null = null,289 ): Promise<ITransactionResult> {290 const call = this.constructApiCall(extrinsic, params);291 const result = await super.executeExtrinsic(292 sender,293 'api.tx.sudo.sudo',294 [call],295 expectSuccess,296 options,297 );298299 if(result.status === 'Fail') return result;300301 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;302 if(data.isErr) {303 if(data.asErr.isModule) {304 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;305 const metaError = super.getApi()?.registry.findMetaError(error);306 throw new Error(`${metaError.section}.${metaError.name}`);307 } else if(data.asErr.isToken) {308 throw new Error(`Token: ${data.asErr.asToken}`);309 }310 // May be [object Object] in case of unhandled non-unit enum311 throw new Error(`Misc: ${data.asErr.toHuman()}`);312 }313 return result;314 }315 override async executeExtrinsicUncheckedWeight(316 sender: IKeyringPair,317 extrinsic: string,318 params: any[],319 expectSuccess?: boolean,320 options: Partial<SignerOptions> | null = null,321 ): Promise<ITransactionResult> {322 const call = this.constructApiCall(extrinsic, params);323 const result = await super.executeExtrinsic(324 sender,325 'api.tx.sudo.sudoUncheckedWeight',326 [call, {refTime: 0, proofSize: 0}],327 expectSuccess,328 options,329 );330331 if(result.status === 'Fail') return result;332333 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;334 if(data.isErr) {335 if(data.asErr.isModule) {336 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;337 const metaError = super.getApi()?.registry.findMetaError(error);338 throw new Error(`${metaError.section}.${metaError.name}`);339 } else if(data.asErr.isToken) {340 throw new Error(`Token: ${data.asErr.asToken}`);341 }342 // May be [object Object] in case of unhandled non-unit enum343 throw new Error(`Misc: ${data.asErr.toHuman()}`);344 }345 return result;346 }347 };348}349350class SchedulerGroup extends HelperGroup<UniqueHelper> {351 constructor(helper: UniqueHelper) {352 super(helper);353 }354355 cancelScheduled(signer: TSigner, scheduledId: string) {356 return this.helper.executeExtrinsic(357 signer,358 'api.tx.scheduler.cancelNamed',359 [scheduledId],360 true,361 );362 }363364 changePriority(signer: TSigner, scheduledId: string, priority: number) {365 return this.helper.executeExtrinsic(366 signer,367 'api.tx.scheduler.changeNamedPriority',368 [scheduledId, priority],369 true,370 );371 }372373 scheduleAt<T extends DevUniqueHelper>(374 executionBlockNumber: number,375 options: ISchedulerOptions = {},376 ) {377 return this.schedule<T>('schedule', executionBlockNumber, options);378 }379380 scheduleAfter<T extends DevUniqueHelper>(381 blocksBeforeExecution: number,382 options: ISchedulerOptions = {},383 ) {384 return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);385 }386387 schedule<T extends UniqueHelper>(388 scheduleFn: 'schedule' | 'scheduleAfter',389 blocksNum: number,390 options: ISchedulerOptions = {},391 ) {392 // eslint-disable-next-line @typescript-eslint/naming-convention393 const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);394 return this.helper.clone(ScheduledHelperType, {395 scheduleFn,396 blocksNum,397 options,398 }) as T;399 }400}401402class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {403 //todo:collator documentation404 addInvulnerable(signer: TSigner, address: string) {405 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);406 }407408 removeInvulnerable(signer: TSigner, address: string) {409 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);410 }411412 async getInvulnerables(): Promise<string[]> {413 return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());414 }415416 /** and also total max invulnerables */417 maxCollators(): number {418 return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);419 }420421 async getDesiredCollators(): Promise<number> {422 return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();423 }424425 setLicenseBond(signer: TSigner, amount: bigint) {426 return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);427 }428429 async getLicenseBond(): Promise<bigint> {430 return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();431 }432433 obtainLicense(signer: TSigner) {434 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);435 }436437 releaseLicense(signer: TSigner) {438 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);439 }440441 forceReleaseLicense(signer: TSigner, released: string) {442 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);443 }444445 async hasLicense(address: string): Promise<bigint> {446 return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();447 }448449 onboard(signer: TSigner) {450 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);451 }452453 offboard(signer: TSigner) {454 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);455 }456457 async getCandidates(): Promise<string[]> {458 return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());459 }460}461462export class DevUniqueHelper extends UniqueHelper {463 /**464 * Arrange methods for tests465 */466 arrange: ArrangeGroup;467 wait: WaitGroup;468 admin: AdminGroup;469 session: SessionGroup;470 testUtils: TestUtilGroup;471 foreignAssets: ForeignAssetsGroup;472 xcm: XcmGroup<DevUniqueHelper>;473 xTokens: XTokensGroup<DevUniqueHelper>;474 tokens: TokensGroup<DevUniqueHelper>;475 scheduler: SchedulerGroup;476 collatorSelection: CollatorSelectionGroup;477 council: ICollectiveGroup;478 technicalCommittee: ICollectiveGroup;479 fellowship: IFellowshipGroup;480 democracy: DemocracyGroup;481482 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {483 options.helperBase = options.helperBase ?? DevUniqueHelper;484485 super(logger, options);486 this.arrange = new ArrangeGroup(this);487 this.wait = new WaitGroup(this);488 this.admin = new AdminGroup(this);489 this.testUtils = new TestUtilGroup(this);490 this.session = new SessionGroup(this);491 this.foreignAssets = new ForeignAssetsGroup(this);492 this.xcm = new XcmGroup(this, 'polkadotXcm');493 this.xTokens = new XTokensGroup(this);494 this.tokens = new TokensGroup(this);495 this.scheduler = new SchedulerGroup(this);496 this.collatorSelection = new CollatorSelectionGroup(this);497 this.council = {498 collective: new CollectiveGroup(this, 'council'),499 membership: new CollectiveMembershipGroup(this, 'councilMembership'),500 };501 this.technicalCommittee = {502 collective: new CollectiveGroup(this, 'technicalCommittee'),503 membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),504 };505 this.fellowship = {506 collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),507 referenda: new ReferendaGroup(this, 'fellowshipReferenda'),508 };509 this.democracy = new DemocracyGroup(this);510 }511512 override async connect(wsEndpoint: string, _listeners?: any): Promise<void> {513 if(!wsEndpoint) throw new Error('wsEndpoint was not set');514 const wsProvider = new WsProvider(wsEndpoint);515 this.api = new ApiPromise({516 provider: wsProvider,517 signedExtensions: {518 ContractHelpers: {519 extrinsic: {},520 payload: {},521 },522 CheckMaintenance: {523 extrinsic: {},524 payload: {},525 },526 DisableIdentityCalls: {527 extrinsic: {},528 payload: {},529 },530 FakeTransactionFinalizer: {531 extrinsic: {},532 payload: {},533 },534 },535 rpc: {536 unique: defs.unique.rpc,537 appPromotion: defs.appPromotion.rpc,538 povinfo: defs.povinfo.rpc,539 eth: {540 feeHistory: {541 description: 'Dummy',542 params: [],543 type: 'u8',544 },545 maxPriorityFeePerGas: {546 description: 'Dummy',547 params: [],548 type: 'u8',549 },550 },551 },552 });553 await this.api.isReadyOrError;554 this.network = await UniqueHelper.detectNetwork(this.api);555 this.wsEndpoint = wsEndpoint;556 }557 getSudo<T extends DevUniqueHelper>() {558 // eslint-disable-next-line @typescript-eslint/naming-convention559 const SudoHelperType = SudoHelper(this.helperBase);560 return this.clone(SudoHelperType) as T;561 }562}563564export class DevRelayHelper extends RelayHelper {565 wait: WaitGroup;566567 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {568 options.helperBase = options.helperBase ?? DevRelayHelper;569570 super(logger, options);571 this.wait = new WaitGroup(this);572 }573574 getSudo() {575 // eslint-disable-next-line @typescript-eslint/naming-convention576 const SudoHelperType = SudoHelper(this.helperBase);577 return this.clone(SudoHelperType) as DevRelayHelper;578 }579}580581export class DevWestmintHelper extends WestmintHelper {582 wait: WaitGroup;583584 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {585 options.helperBase = options.helperBase ?? DevWestmintHelper;586587 super(logger, options);588 this.wait = new WaitGroup(this);589 }590}591592export class DevStatemineHelper extends DevWestmintHelper {}593594export class DevStatemintHelper extends DevWestmintHelper {}595596export class DevMoonbeamHelper extends MoonbeamHelper {597 account: MoonbeamAccountGroup;598 wait: WaitGroup;599 fastDemocracy: MoonbeamFastDemocracyGroup;600601 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {602 options.helperBase = options.helperBase ?? DevMoonbeamHelper;603 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';604605 super(logger, options);606 this.account = new MoonbeamAccountGroup(this);607 this.wait = new WaitGroup(this);608 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);609 }610}611612export class DevMoonriverHelper extends DevMoonbeamHelper {613 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {614 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';615 super(logger, options);616 }617}618619export class DevAstarHelper extends AstarHelper {620 wait: WaitGroup;621622 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {623 options.helperBase = options.helperBase ?? DevAstarHelper;624625 super(logger, options);626 this.wait = new WaitGroup(this);627 }628629 getSudo<T extends AstarHelper>() {630 // eslint-disable-next-line @typescript-eslint/naming-convention631 const SudoHelperType = SudoHelper(this.helperBase);632 return this.clone(SudoHelperType) as T;633 }634}635636export class DevShidenHelper extends DevAstarHelper { }637638export class DevAcalaHelper extends AcalaHelper {639 wait: WaitGroup;640641 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {642 options.helperBase = options.helperBase ?? DevAcalaHelper;643644 super(logger, options);645 this.wait = new WaitGroup(this);646 }647 getSudo() {648 // eslint-disable-next-line @typescript-eslint/naming-convention649 const SudoHelperType = SudoHelper(this.helperBase);650 return this.clone(SudoHelperType) as DevAcalaHelper;651 }652}653654export class DevPolkadexHelper extends PolkadexHelper {655 wait: WaitGroup;656 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {657 options.helperBase = options.helperBase ?? PolkadexHelper;658659 super(logger, options);660 this.wait = new WaitGroup(this);661 }662663 getSudo() {664 // eslint-disable-next-line @typescript-eslint/naming-convention665 const SudoHelperType = SudoHelper(this.helperBase);666 return this.clone(SudoHelperType) as DevPolkadexHelper;667 }668}669670export class DevHydraDxHelper extends HydraDxHelper {671 wait: WaitGroup;672 fastDemocracy: HydraFastDemocracyGroup;673674 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {675 options.helperBase = options.helperBase ?? DevHydraDxHelper;676677 super(logger, options);678679 this.wait = new WaitGroup(this);680 this.fastDemocracy = new HydraFastDemocracyGroup(this);681 }682}683684export class DevKaruraHelper extends DevAcalaHelper {}685686export class ArrangeGroup {687 helper: DevUniqueHelper;688689 scheduledIdSlider = 0;690691 constructor(helper: DevUniqueHelper) {692 this.helper = helper;693 }694695 /**696 * Generates accounts with the specified UNQ token balance697 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.698 * @param donor donor account for balances699 * @returns array of newly created accounts700 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);701 */702 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {703 let nonce = await this.helper.chain.getNonce(donor.address);704 const wait = new WaitGroup(this.helper);705 const ss58Format = this.helper.chain.getChainProperties().ss58Format;706 const tokenNominal = this.helper.balance.getOneTokenNominal();707 const transactions = [];708 const accounts: IKeyringPair[] = [];709 for(const balance of balances) {710 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);711 accounts.push(recipient);712 if(balance !== 0n) {713 const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, balance * tokenNominal]);714 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));715 nonce++;716 }717 }718719 await Promise.all(transactions).catch(_e => {});720721 //#region TODO remove this region, when nonce problem will be solved722 const checkBalances = async () => {723 let isSuccess = true;724 for(let i = 0; i < balances.length; i++) {725 const balance = await this.helper.balance.getSubstrate(accounts[i].address);726 if(balance !== balances[i] * tokenNominal) {727 isSuccess = false;728 break;729 }730 }731 return isSuccess;732 };733734 let accountsCreated = false;735 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;736 // checkBalances retry up to 5-50 blocks737 for(let index = 0; index < maxBlocksChecked; index++) {738 accountsCreated = await checkBalances();739 if(accountsCreated) break;740 await wait.newBlocks(1);741 }742743 if(!accountsCreated) throw Error('Accounts generation failed');744 //#endregion745746 return accounts;747 };748749 // TODO combine this method and createAccounts into one750 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {751 const createAsManyAsCan = async () => {752 let transactions: any = [];753 const accounts: IKeyringPair[] = [];754 let nonce = await this.helper.chain.getNonce(donor.address);755 const tokenNominal = this.helper.balance.getOneTokenNominal();756 const ss58Format = this.helper.chain.getChainProperties().ss58Format;757 for(let i = 0; i < accountsToCreate; i++) {758 if(i === 500) { // if there are too many accounts to create759 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled760 transactions = []; //761 nonce = await this.helper.chain.getNonce(donor.address); // update nonce762 }763 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);764 accounts.push(recipient);765 if(withBalance !== 0n) {766 const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, withBalance * tokenNominal]);767 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));768 nonce++;769 }770 }771772 const fullfilledAccounts = [];773 await Promise.allSettled(transactions);774 for(const account of accounts) {775 const accountBalance = await this.helper.balance.getSubstrate(account.address);776 if(accountBalance === withBalance * tokenNominal) {777 fullfilledAccounts.push(account);778 }779 }780 return fullfilledAccounts;781 };782783784 const crowd: IKeyringPair[] = [];785 // do up to 5 retries786 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {787 const asManyAsCan = await createAsManyAsCan();788 crowd.push(...asManyAsCan);789 accountsToCreate -= asManyAsCan.length;790 }791792 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);793794 return crowd;795 };796797 /**798 * Generates one account with zero balance799 * @returns the newly generated account800 * @example const account = await helper.arrange.createEmptyAccount();801 */802 createEmptyAccount = (): IKeyringPair => {803 const ss58Format = this.helper.chain.getChainProperties().ss58Format;804 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);805 };806807 isDevNode = async () => {808 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();809 if(blockNumber == 0) {810 await this.helper.wait.newBlocks(1);811 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();812 }813 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);814 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);815 const findCreationDate = (block: any) => {816 const humanBlock = block.toHuman();817 let date;818 humanBlock.block.extrinsics.forEach((ext: any) => {819 if(ext.method.section === 'timestamp') {820 date = Number(ext.method.args.now.replaceAll(',', ''));821 }822 });823 return date;824 };825 const block1date = await findCreationDate(block1);826 const block2date = await findCreationDate(block2);827 if(block2date! - block1date! < 9000) return true;828 return false;829 };830831 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {832 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);833 let balance = await this.helper.balance.getSubstrate(address);834835 await promise();836837 balance -= await this.helper.balance.getSubstrate(address);838839 return balance;840 }841842 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {843 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);844845 const kvJson: {[key: string]: string} = {};846847 for(const kv of rawPovInfo.keyValues) {848 kvJson[kv.key.toHex()] = kv.value.toHex();849 }850851 const kvStr = JSON.stringify(kvJson);852853 const chainql = spawnSync(854 'chainql',855 [856 `--tla-code=data=${kvStr}`,857 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,858 ],859 );860861 if(!chainql.stdout) {862 throw Error('unable to get an output from the `chainql`');863 }864865 return {866 proofSize: rawPovInfo.proofSize.toNumber(),867 compactProofSize: rawPovInfo.compactProofSize.toNumber(),868 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),869 results: rawPovInfo.results,870 kv: JSON.parse(chainql.stdout.toString()),871 };872 }873874 calculatePalletAddress(palletId: any) {875 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));876 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);877 }878879 makeScheduledIds(num: number): string[] {880 function makeId(slider: number) {881 const scheduledIdSize = 64;882 const hexId = slider.toString(16);883 const prefixSize = scheduledIdSize - hexId.length;884885 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;886887 return scheduledId;888 }889890 const ids = [];891 for(let i = 0; i < num; i++) {892 ids.push(makeId(this.scheduledIdSlider));893 this.scheduledIdSlider += 1;894 }895896 return ids;897 }898899 makeScheduledId(): string {900 return (this.makeScheduledIds(1))[0];901 }902903 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {904 const capture = new EventCapture(this.helper, eventSection, eventMethod);905 await capture.startCapture();906907 return capture;908 }909910 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {911 return {912 V2: [913 {914 WithdrawAsset: [915 {916 id,917 fun: {918 Fungible: amount,919 },920 },921 ],922 },923 {924 BuyExecution: {925 fees: {926 id,927 fun: {928 Fungible: amount,929 },930 },931 weightLimit: 'Unlimited',932 },933 },934 {935 DepositAsset: {936 assets: {937 Wild: 'All',938 },939 maxAssets: 1,940 beneficiary: {941 parents: 0,942 interior: {943 X1: {944 AccountId32: {945 network: 'Any',946 id: beneficiary,947 },948 },949 },950 },951 },952 },953 ],954 };955 }956957 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {958 return {959 V2: [960 {961 ReserveAssetDeposited: [962 {963 id,964 fun: {965 Fungible: amount,966 },967 },968 ],969 },970 {971 BuyExecution: {972 fees: {973 id,974 fun: {975 Fungible: amount,976 },977 },978 weightLimit: 'Unlimited',979 },980 },981 {982 DepositAsset: {983 assets: {984 Wild: 'All',985 },986 maxAssets: 1,987 beneficiary: {988 parents: 0,989 interior: {990 X1: {991 AccountId32: {992 network: 'Any',993 id: beneficiary,994 },995 },996 },997 },998 },999 },1000 ],1001 };1002 }10031004 makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {1005 return {1006 V3: [1007 {1008 UnpaidExecution: {1009 weightLimit: 'Unlimited',1010 checkOrigin: null,1011 },1012 },1013 {1014 Transact: {1015 originKind: 'Superuser',1016 requireWeightAtMost: {1017 refTime: info.weightMultiplier * 200000000,1018 proofSize: info.weightMultiplier * 3000,1019 },1020 call: {1021 encoded: info.call,1022 },1023 },1024 },1025 ],1026 };1027 }1028}10291030class MoonbeamAccountGroup {1031 helper: MoonbeamHelper;10321033 keyring: Keyring;1034 _alithAccount: IKeyringPair;1035 _baltatharAccount: IKeyringPair;1036 _dorothyAccount: IKeyringPair;10371038 constructor(helper: MoonbeamHelper) {1039 this.helper = helper;10401041 this.keyring = new Keyring({type: 'ethereum'});1042 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';1043 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';1044 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';10451046 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');1047 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');1048 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');1049 }10501051 alithAccount() {1052 return this._alithAccount;1053 }10541055 baltatharAccount() {1056 return this._baltatharAccount;1057 }10581059 dorothyAccount() {1060 return this._dorothyAccount;1061 }10621063 create() {1064 return this.keyring.addFromUri(mnemonicGenerate());1065 }1066}10671068class MoonbeamFastDemocracyGroup {1069 helper: DevMoonbeamHelper;10701071 constructor(helper: DevMoonbeamHelper) {1072 this.helper = helper;1073 }10741075 async executeProposal(proposalDesciption: string, encodedProposal: string) {1076 const proposalHash = blake2AsHex(encodedProposal);10771078 const alithAccount = this.helper.account.alithAccount();1079 const baltatharAccount = this.helper.account.baltatharAccount();1080 const dorothyAccount = this.helper.account.dorothyAccount();10811082 const councilVotingThreshold = 2;1083 const technicalCommitteeThreshold = 2;1084 const fastTrackVotingPeriod = 3;1085 const fastTrackDelayPeriod = 0;10861087 console.log(`[democracy] executing '${proposalDesciption}' proposal`);10881089 // >>> Propose external motion through council >>>1090 console.log('\t* Propose external motion through council.......');1091 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1092 const encodedMotion = externalMotion?.method.toHex() || '';1093 const motionHash = blake2AsHex(encodedMotion);1094 console.log('\t* Motion hash is %s', motionHash);10951096 await this.helper.collective.council.propose(1097 baltatharAccount,1098 councilVotingThreshold,1099 externalMotion,1100 externalMotion.encodedLength,1101 );11021103 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1104 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1105 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);11061107 await this.helper.collective.council.close(1108 dorothyAccount,1109 motionHash,1110 councilProposalIdx,1111 {1112 refTime: 1_000_000_000,1113 proofSize: 1_000_000,1114 },1115 externalMotion.encodedLength,1116 );1117 console.log('\t* Propose external motion through council.......DONE');1118 // <<< Propose external motion through council <<<11191120 // >>> Fast track proposal through technical committee >>>1121 console.log('\t* Fast track proposal through technical committee.......');1122 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1123 const encodedFastTrack = fastTrack?.method.toHex() || '';1124 const fastTrackHash = blake2AsHex(encodedFastTrack);1125 console.log('\t* FastTrack hash is %s', fastTrackHash);11261127 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);11281129 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1130 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1131 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);11321133 await this.helper.collective.techCommittee.close(1134 baltatharAccount,1135 fastTrackHash,1136 techProposalIdx,1137 {1138 refTime: 1_000_000_000,1139 proofSize: 1_000_000,1140 },1141 fastTrack.encodedLength,1142 );1143 console.log('\t* Fast track proposal through technical committee.......DONE');1144 // <<< Fast track proposal through technical committee <<<11451146 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1147 const referendumIndex = democracyStarted.referendumIndex;11481149 // >>> Referendum voting >>>1150 console.log(`\t* Referendum #${referendumIndex} voting.......`);1151 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1152 balance: 10_000_000_000_000_000_000n,1153 vote: {aye: true, conviction: 1},1154 });1155 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1156 // <<< Referendum voting <<<11571158 // Wait the proposal to pass1159 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11601161 await this.helper.wait.newBlocks(1);11621163 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1164 }1165}11661167class HydraFastDemocracyGroup {1168 helper: DevHydraDxHelper;11691170 constructor(helper: DevHydraDxHelper) {1171 this.helper = helper;1172 }11731174 async executeProposal(proposalDesciption: string, encodedProposal: string) {1175 const proposalHash = blake2AsHex(encodedProposal);1176 const aliceAccount = this.helper.util.fromSeed('//Alice');1177 const bobAccount = this.helper.util.fromSeed('//Bob');1178 const eveAccount = this.helper.util.fromSeed('//Eve');11791180 const councilVotingThreshold = 1;1181 const technicalCommitteeThreshold = 3;1182 const fastTrackVotingPeriod = 3;1183 const fastTrackDelayPeriod = 0;11841185 console.log(`[democracy] executing '${proposalDesciption}' proposal`);11861187 // >>> Propose external motion through council >>>1188 console.log('\t* Propose external motion through council.......');1189 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1190 const encodedMotion = externalMotion?.method.toHex() || '';1191 const motionHash = blake2AsHex(encodedMotion);1192 console.log('\t* Motion hash is %s', motionHash);11931194 await this.helper.collective.council.propose(1195 aliceAccount,1196 councilVotingThreshold,1197 externalMotion,1198 externalMotion.encodedLength,1199 );12001201 console.log('\t* Propose external motion through council.......DONE');1202 // <<< Propose external motion through council <<<12031204 // >>> Fast track proposal through technical committee >>>1205 console.log('\t* Fast track proposal through technical committee.......');1206 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1207 const encodedFastTrack = fastTrack?.method.toHex() || '';1208 const fastTrackHash = blake2AsHex(encodedFastTrack);1209 console.log('\t* FastTrack hash is %s', fastTrackHash);12101211 await this.helper.collective.techCommittee.propose(aliceAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);12121213 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1214 await this.helper.collective.techCommittee.vote(aliceAccount, fastTrackHash, techProposalIdx, true);1215 await this.helper.collective.techCommittee.vote(bobAccount, fastTrackHash, techProposalIdx, true);1216 await this.helper.collective.techCommittee.vote(eveAccount, fastTrackHash, techProposalIdx, true);12171218 await this.helper.collective.techCommittee.close(1219 bobAccount,1220 fastTrackHash,1221 techProposalIdx,1222 {1223 refTime: 1_000_000_000,1224 proofSize: 1_000_000,1225 },1226 fastTrack.encodedLength,1227 );1228 console.log('\t* Fast track proposal through technical committee.......DONE');1229 // <<< Fast track proposal through technical committee <<<12301231 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1232 const referendumIndex = democracyStarted.referendumIndex;12331234 // >>> Referendum voting >>>1235 console.log(`\t* Referendum #${referendumIndex} voting.......`);1236 await this.helper.democracy.referendumVote(eveAccount, referendumIndex, {1237 balance: 10_000_000_000_000_000_000n,1238 vote: {aye: true, conviction: 1},1239 });1240 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1241 // <<< Referendum voting <<<12421243 // Wait the proposal to pass1244 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);12451246 await this.helper.wait.newBlocks(1);12471248 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1249 }1250}12511252class WaitGroup {1253 helper: ChainHelperBase;12541255 constructor(helper: ChainHelperBase) {1256 this.helper = helper;1257 }12581259 sleep(milliseconds: number) {1260 return new Promise((resolve) => setTimeout(resolve, milliseconds));1261 }12621263 private async waitWithTimeout(promise: Promise<any>, timeout: number) {1264 let isBlock = false;1265 promise.then(() => isBlock = true).catch(() => isBlock = true);1266 let totalTime = 0;1267 const step = 100;1268 while(!isBlock) {1269 await this.sleep(step);1270 totalTime += step;1271 if(totalTime >= timeout) throw Error('Blocks production failed');1272 }1273 return promise;1274 }12751276 /**1277 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1278 * @param promise async operation to race against the timeout1279 * @param timeoutMS time after which to time out1280 * @param timeoutError error message to throw1281 * @returns promise of the same type the operation had1282 */1283 withTimeout<T>(1284 promise: Promise<T>,1285 timeoutMS = 30000,1286 timeoutError = 'The operation has timed out!',1287 ): Promise<T> {1288 const timeout = new Promise<never>((_, reject) => {1289 setTimeout(() => {1290 reject(new Error(timeoutError));1291 }, timeoutMS);1292 });12931294 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1295 }12961297 /**1298 * Wait for specified number of blocks1299 * @param blocksCount number of blocks to wait1300 * @returns1301 */1302 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1303 timeout = timeout ?? blocksCount * 60_000;1304 // eslint-disable-next-line no-async-promise-executor1305 const promise = new Promise<void>(async (resolve) => {1306 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1307 if(blocksCount > 0) {1308 blocksCount--;1309 } else {1310 unsubscribe();1311 resolve();1312 }1313 });1314 });1315 await this.waitWithTimeout(promise, timeout);1316 return promise;1317 }13181319 /**1320 * Wait for the specified number of sessions to pass.1321 * Only applicable if the Session pallet is turned on.1322 * @param sessionCount number of sessions to wait1323 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1324 * @returns1325 */1326 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1327 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.`1328 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');13291330 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1331 let currentSessionIndex = -1;13321333 while(currentSessionIndex < expectedSessionIndex) {1334 // eslint-disable-next-line no-async-promise-executor1335 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1336 await this.newBlocks(1);1337 const res = await (this.helper as DevUniqueHelper).session.getIndex();1338 resolve(res);1339 }), blockTimeout, 'The chain has stopped producing blocks!');1340 }1341 }13421343 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1344 timeout = timeout ?? 30 * 60 * 1000;1345 // eslint-disable-next-line no-async-promise-executor1346 const promise = new Promise<void>(async (resolve) => {1347 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1348 if(data.number.toNumber() >= blockNumber) {1349 unsubscribe();1350 resolve();1351 }1352 });1353 });1354 await this.waitWithTimeout(promise, timeout);1355 return promise;1356 }13571358 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1359 timeout = timeout ?? 30 * 60 * 1000;1360 // eslint-disable-next-line no-async-promise-executor1361 const promise = new Promise<void>(async (resolve) => {1362 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1363 if(data.value.relayParentNumber.toNumber() >= blockNumber) {1364 // @ts-ignore1365 unsubscribe();1366 resolve();1367 }1368 });1369 });1370 await this.waitWithTimeout(promise, timeout);1371 return promise;1372 }13731374 noScheduledTasks() {1375 const api = this.helper.getApi();13761377 // eslint-disable-next-line no-async-promise-executor1378 const promise = new Promise<void>(async resolve => {1379 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1380 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();13811382 if(areThereScheduledTasks.length == 0) {1383 unsubscribe();1384 resolve();1385 }1386 });1387 });13881389 return promise;1390 }13911392 parachainBlockMultiplesOf(val: bigint) {1393 // eslint-disable-next-line no-async-promise-executor1394 const promise = new Promise<void>(async resolve => {1395 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1396 if(data.number.toBigInt() % val == 0n) {1397 console.log(`from waiter: ${data.number.toBigInt()}`);1398 unsubscribe();1399 resolve();1400 }1401 });1402 });1403 return promise;1404 }14051406 event<T extends IEventHelper>(1407 maxBlocksToWait: number,1408 eventHelper: T,1409 filter: (_: any) => boolean = () => true,1410 ): any {1411 // eslint-disable-next-line no-async-promise-executor1412 const promise = new Promise<T | null>(async (resolve) => {1413 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1414 const blockNumber = header.number.toJSON();1415 const blockHash = header.hash;1416 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1417 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;14181419 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);14201421 const apiAt = await this.helper.getApi().at(blockHash);1422 const eventRecords = (await apiAt.query.system.events()) as any;14231424 const neededEvent = eventRecords.toArray()1425 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1426 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1427 .find(filter);14281429 if(neededEvent) {1430 unsubscribe();1431 resolve(neededEvent);1432 } else if(maxBlocksToWait > 0) {1433 maxBlocksToWait--;1434 } else {1435 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1436 The wait lasted until block ${blockNumber} inclusive`);1437 unsubscribe();1438 resolve(null);1439 }1440 });1441 });1442 return promise;1443 }14441445 async expectEvent<T extends IEventHelper>(1446 maxBlocksToWait: number,1447 eventHelper: T,1448 filter: (e: any) => boolean = () => true,1449 ) {1450 const e = await this.event(maxBlocksToWait, eventHelper, filter);1451 if(e == null) {1452 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1453 } else {1454 return e;1455 }1456 }1457}14581459class SessionGroup {1460 helper: ChainHelperBase;14611462 constructor(helper: ChainHelperBase) {1463 this.helper = helper;1464 }14651466 //todo:collator documentation1467 async getIndex(): Promise<number> {1468 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1469 }14701471 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1472 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1473 }14741475 setOwnKeys(signer: TSigner, key: string) {1476 return this.helper.executeExtrinsic(1477 signer,1478 'api.tx.session.setKeys',1479 [key, '0x0'],1480 true,1481 );1482 }14831484 setOwnKeysFromAddress(signer: TSigner) {1485 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1486 }1487}14881489class TestUtilGroup {1490 helper: DevUniqueHelper;14911492 constructor(helper: DevUniqueHelper) {1493 this.helper = helper;1494 }14951496 async enable(testUtilsPalletName: string) {1497 if(this.helper.fetchMissingPalletNames([testUtilsPalletName]).length != 0) {1498 return;1499 }15001501 const signer = this.helper.util.fromSeed('//Alice');1502 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1503 }15041505 async setTestValue(signer: TSigner, testVal: number) {1506 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1507 }15081509 async incTestValue(signer: TSigner) {1510 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1511 }15121513 async setTestValueAndRollback(signer: TSigner, testVal: number) {1514 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1515 }15161517 async testValue(blockIdx?: number) {1518 const api = blockIdx1519 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1520 : this.helper.getApi();15211522 return (await api.query.testUtils.testValue()).toJSON();1523 }15241525 async justTakeFee(signer: TSigner) {1526 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1527 }15281529 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1530 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1531 }1532}15331534class EventCapture {1535 helper: DevUniqueHelper;1536 eventSection: string;1537 eventMethod: string;1538 events: EventRecord[] = [];1539 unsubscribe: VoidFn | null = null;15401541 constructor(1542 helper: DevUniqueHelper,1543 eventSection: string,1544 eventMethod: string,1545 ) {1546 this.helper = helper;1547 this.eventSection = eventSection;1548 this.eventMethod = eventMethod;1549 }15501551 async startCapture() {1552 this.stopCapture();1553 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1554 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);15551556 this.events.push(...newEvents);1557 })) as any;1558 }15591560 stopCapture() {1561 if(this.unsubscribe !== null) {1562 this.unsubscribe();1563 }1564 }15651566 extractCapturedEvents() {1567 return this.events;1568 }1569}15701571class AdminGroup {1572 helper: UniqueHelper;15731574 constructor(helper: UniqueHelper) {1575 this.helper = helper;1576 }15771578 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1579 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1580 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1581 staker: e.event.data[0].toString(),1582 stake: e.event.data[1].toBigInt(),1583 payout: e.event.data[2].toBigInt(),1584 }));1585 }1586}15871588// eslint-disable-next-line @typescript-eslint/naming-convention1589function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1590 return class extends Base {1591 scheduleFn: 'schedule' | 'scheduleAfter';1592 blocksNum: number;1593 options: ISchedulerOptions;15941595 constructor(...args: any[]) {1596 const logger = args[0] as ILogger;1597 const options = args[1] as {1598 scheduleFn: 'schedule' | 'scheduleAfter',1599 blocksNum: number,1600 options: ISchedulerOptions1601 };16021603 super(logger);16041605 this.scheduleFn = options.scheduleFn;1606 this.blocksNum = options.blocksNum;1607 this.options = options.options;1608 }16091610 override executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1611 const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);16121613 const mandatorySchedArgs = [1614 this.blocksNum,1615 this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1616 this.options.priority ?? null,1617 scheduledTx,1618 ];16191620 let schedArgs;1621 let scheduleFn;16221623 if(this.options.scheduledId) {1624 schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];16251626 if(this.scheduleFn == 'schedule') {1627 scheduleFn = 'scheduleNamed';1628 } else if(this.scheduleFn == 'scheduleAfter') {1629 scheduleFn = 'scheduleNamedAfter';1630 }1631 } else {1632 schedArgs = mandatorySchedArgs;1633 scheduleFn = this.scheduleFn;1634 }16351636 const extrinsic = 'api.tx.scheduler.' + scheduleFn;16371638 return super.executeExtrinsic(1639 sender,1640 extrinsic as any,1641 schedArgs,1642 expectSuccess,1643 );1644 }1645 };1646}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034import '@unique-nft/opal-testnet-types/augment-api.js';5import '@unique-nft/opal-testnet-types/augment-types.js';6import '@unique-nft/opal-testnet-types/types-lookup.js';78import {stringToU8a} from '@polkadot/util';9import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';10import type {ChainHelperBaseConstructor, UniqueHelperConstructor} from '@unique-nft/playgrounds/unique.js';11import {UniqueHelper, ChainHelperBase, HelperGroup} from '@unique-nft/playgrounds/unique.js';12import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';13import * as defs from '@unique-nft/opal-testnet-types/definitions.js';14import type {IKeyringPair} from '@polkadot/types/types';15import type {EventRecord} from '@polkadot/types/interfaces';16import type {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from '@unique-nft/playgrounds/types.js';17import type {FrameSystemEventRecord, StagingXcmV2TraitsError, StagingXcmV3TraitsOutcome} from '@polkadot/types/lookup';18import type {SignerOptions, VoidFn} from '@polkadot/api/types';19import {spawnSync} from 'child_process';20import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup, HydraDxHelper} from './xcm/index.js';21import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, RankedCollectiveGroup, ReferendaGroup} from './governance.js';22import type {ICollectiveGroup, IFellowshipGroup} from './governance.js';2324export class SilentLogger {25 log(_msg: any, _level: any): void { }26 level = {27 ERROR: 'ERROR' as const,28 WARNING: 'WARNING' as const,29 INFO: 'INFO' as const,30 };31}3233export class SilentConsole {34 // TODO: Remove, this is temporary: Filter unneeded API output35 // (Jaco promised it will be removed in the next version)36 consoleErr: any;37 consoleLog: any;38 consoleWarn: any;3940 constructor() {41 this.consoleErr = console.error;42 this.consoleLog = console.log;43 this.consoleWarn = console.warn;44 }4546 enable() {47 const outFn = (printer: any) => (...args: any[]) => {48 for(const arg of args) {49 if(typeof arg !== 'string')50 continue;51 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'];52 const needToSkip = skippedWarnings.reduce((a, b) => a || arg.includes(b), false);53 if(needToSkip || arg === 'Normal connection closure')54 return;55 }56 printer(...args);57 };5859 console.error = outFn(this.consoleErr.bind(console));60 console.log = outFn(this.consoleLog.bind(console));61 console.warn = outFn(this.consoleWarn.bind(console));62 }6364 disable() {65 console.error = this.consoleErr;66 console.log = this.consoleLog;67 console.warn = this.consoleWarn;68 }69}7071export interface IEventHelper {72 section(): string;7374 method(): string;7576 wrapEvent(data: any[]): any;77}7879// eslint-disable-next-line @typescript-eslint/naming-convention80function EventHelper(section: string, method: string, wrapEvent: (data: any[]) => any) {81 const helperClass = class implements IEventHelper {82 wrapEvent: (data: any[]) => any;83 _section: string;84 _method: string;8586 constructor() {87 this.wrapEvent = wrapEvent;88 this._section = section;89 this._method = method;90 }9192 section(): string {93 return this._section;94 }9596 method(): string {97 return this._method;98 }99100 filter(txres: ITransactionResult) {101 return txres.result.events.filter(e => e.event.section === section && e.event.method === method)102 .map(e => this.wrapEvent(e.event.data));103 }104105 find(txres: ITransactionResult) {106 const e = txres.result.events.find(e => e.event.section === section && e.event.method === method);107 return e ? this.wrapEvent(e.event.data) : null;108 }109110 expect(txres: ITransactionResult) {111 const e = this.find(txres);112 if(e) {113 return e;114 } else {115 throw Error(`Expected event ${section}.${method}`);116 }117 }118 };119120 return helperClass;121}122123function eventJsonData<T = any>(data: any[], index: number) {124 return data[index].toJSON() as T;125}126127function eventHumanData(data: any[], index: number) {128 return data[index].toHuman();129}130131function eventData<T = any>(data: any[], index: number) {132 return data[index] as T;133}134135// eslint-disable-next-line @typescript-eslint/naming-convention136function EventSection(section: string) {137 return class Section {138 static section = section;139140 static Method(name: string, wrapEvent: (data: any[]) => any = () => {}) {141 const helperClass = EventHelper(Section.section, name, wrapEvent);142 return new helperClass();143 }144 };145}146147function schedulerSection(schedulerInstance: string) {148 return class extends EventSection(schedulerInstance) {149 static Dispatched = this.Method('Dispatched', data => ({150 task: eventJsonData(data, 0),151 id: eventHumanData(data, 1),152 result: data[2],153 }));154155 static PriorityChanged = this.Method('PriorityChanged', data => ({156 task: eventJsonData(data, 0),157 priority: eventJsonData(data, 1),158 }));159 };160}161162export class Event {163 static Democracy = class extends EventSection('democracy') {164 static Proposed = this.Method('Proposed', data => ({165 proposalIndex: eventJsonData<number>(data, 0),166 }));167168 static ExternalTabled = this.Method('ExternalTabled');169170 static Started = this.Method('Started', data => ({171 referendumIndex: eventJsonData<number>(data, 0),172 threshold: eventHumanData(data, 1),173 }));174175 static Voted = this.Method('Voted', data => ({176 voter: eventJsonData(data, 0),177 referendumIndex: eventJsonData<number>(data, 1),178 vote: eventJsonData(data, 2),179 }));180181 static Passed = this.Method('Passed', data => ({182 referendumIndex: eventJsonData<number>(data, 0),183 }));184185 static ProposalCanceled = this.Method('ProposalCanceled', data => ({186 propIndex: eventJsonData<number>(data, 0),187 }));188189 static Cancelled = this.Method('Cancelled', data => ({190 propIndex: eventJsonData<number>(data, 0),191 }));192193 static Vetoed = this.Method('Vetoed', data => ({194 who: eventHumanData(data, 0),195 proposalHash: eventHumanData(data, 1),196 until: eventJsonData<number>(data, 1),197 }));198 };199200 static Council = class extends EventSection('council') {201 static Proposed = this.Method('Proposed', data => ({202 account: eventHumanData(data, 0),203 proposalIndex: eventJsonData<number>(data, 1),204 proposalHash: eventHumanData(data, 2),205 threshold: eventJsonData<number>(data, 3),206 }));207 static Closed = this.Method('Closed', data => ({208 proposalHash: eventHumanData(data, 0),209 yes: eventJsonData<number>(data, 1),210 no: eventJsonData<number>(data, 2),211 }));212 static Executed = this.Method('Executed', data => ({213 proposalHash: eventHumanData(data, 0),214 }));215 };216217 static FinCouncil = class extends EventSection('financialCouncil') {218 static Proposed = this.Method('Proposed', data => ({219 account: eventHumanData(data, 0),220 proposalIndex: eventJsonData<number>(data, 1),221 proposalHash: eventHumanData(data, 2),222 threshold: eventJsonData<number>(data, 3),223 }));224 static Closed = this.Method('Closed', data => ({225 proposalHash: eventHumanData(data, 0),226 yes: eventJsonData<number>(data, 1),227 no: eventJsonData<number>(data, 2),228 }));229 static Executed = this.Method('Executed', data => ({230 proposalHash: eventHumanData(data, 0),231 }));232 };233234 static TechnicalCommittee = class extends EventSection('technicalCommittee') {235 static Proposed = this.Method('Proposed', data => ({236 account: eventHumanData(data, 0),237 proposalIndex: eventJsonData<number>(data, 1),238 proposalHash: eventHumanData(data, 2),239 threshold: eventJsonData<number>(data, 3),240 }));241 static Closed = this.Method('Closed', data => ({242 proposalHash: eventHumanData(data, 0),243 yes: eventJsonData<number>(data, 1),244 no: eventJsonData<number>(data, 2),245 }));246 static Approved = this.Method('Approved', data => ({247 proposalHash: eventHumanData(data, 0),248 }));249 static Executed = this.Method('Executed', data => ({250 proposalHash: eventHumanData(data, 0),251 result: eventHumanData(data, 1),252 }));253 };254255 static FellowshipReferenda = class extends EventSection('fellowshipReferenda') {256 static Submitted = this.Method('Submitted', data => ({257 referendumIndex: eventJsonData<number>(data, 0),258 trackId: eventJsonData<number>(data, 1),259 proposal: eventJsonData(data, 2),260 }));261262 static Cancelled = this.Method('Cancelled', data => ({263 index: eventJsonData<number>(data, 0),264 tally: eventJsonData(data, 1),265 }));266 };267268 static UniqueScheduler = schedulerSection('uniqueScheduler');269 static Scheduler = schedulerSection('scheduler');270271 static XcmpQueue = class extends EventSection('xcmpQueue') {272 static XcmpMessageSent = this.Method('XcmpMessageSent', data => ({273 messageHash: eventJsonData(data, 0),274 }));275276 static Success = this.Method('Success', data => ({277 messageHash: eventJsonData(data, 0),278 }));279280 static Fail = this.Method('Fail', data => ({281 messageHash: eventJsonData(data, 0),282 outcome: eventData<StagingXcmV2TraitsError>(data, 2),283 }));284 };285286 static DmpQueue = class extends EventSection('dmpQueue') {287 static ExecutedDownward = this.Method('ExecutedDownward', data => ({288 outcome: eventData<StagingXcmV3TraitsOutcome>(data, 2),289 }));290 };291}292293// eslint-disable-next-line @typescript-eslint/naming-convention294export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {295 return class extends Base {296 constructor(...args: any[]) {297 super(...args);298 }299300 override async executeExtrinsic(301 sender: IKeyringPair,302 extrinsic: string,303 params: any[],304 expectSuccess?: boolean,305 options: Partial<SignerOptions> | null = null,306 ): Promise<ITransactionResult> {307 const call = this.constructApiCall(extrinsic, params);308 const result = await super.executeExtrinsic(309 sender,310 'api.tx.sudo.sudo',311 [call],312 expectSuccess,313 options,314 );315316 if(result.status === 'Fail') return result;317318 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;319 if(data.isErr) {320 if(data.asErr.isModule) {321 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;322 const metaError = super.getApi()?.registry.findMetaError(error);323 throw new Error(`${metaError.section}.${metaError.name}`);324 } else if(data.asErr.isToken) {325 throw new Error(`Token: ${data.asErr.asToken}`);326 }327 // May be [object Object] in case of unhandled non-unit enum328 throw new Error(`Misc: ${data.asErr.toHuman()}`);329 }330 return result;331 }332 override async executeExtrinsicUncheckedWeight(333 sender: IKeyringPair,334 extrinsic: string,335 params: any[],336 expectSuccess?: boolean,337 options: Partial<SignerOptions> | null = null,338 ): Promise<ITransactionResult> {339 const call = this.constructApiCall(extrinsic, params);340 const result = await super.executeExtrinsic(341 sender,342 'api.tx.sudo.sudoUncheckedWeight',343 [call, {refTime: 0, proofSize: 0}],344 expectSuccess,345 options,346 );347348 if(result.status === 'Fail') return result;349350 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;351 if(data.isErr) {352 if(data.asErr.isModule) {353 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;354 const metaError = super.getApi()?.registry.findMetaError(error);355 throw new Error(`${metaError.section}.${metaError.name}`);356 } else if(data.asErr.isToken) {357 throw new Error(`Token: ${data.asErr.asToken}`);358 }359 // May be [object Object] in case of unhandled non-unit enum360 throw new Error(`Misc: ${data.asErr.toHuman()}`);361 }362 return result;363 }364 };365}366367class SchedulerGroup extends HelperGroup<UniqueHelper> {368 constructor(helper: UniqueHelper) {369 super(helper);370 }371372 cancelScheduled(signer: TSigner, scheduledId: string) {373 return this.helper.executeExtrinsic(374 signer,375 'api.tx.scheduler.cancelNamed',376 [scheduledId],377 true,378 );379 }380381 changePriority(signer: TSigner, scheduledId: string, priority: number) {382 return this.helper.executeExtrinsic(383 signer,384 'api.tx.scheduler.changeNamedPriority',385 [scheduledId, priority],386 true,387 );388 }389390 scheduleAt<T extends DevUniqueHelper>(391 executionBlockNumber: number,392 options: ISchedulerOptions = {},393 ) {394 return this.schedule<T>('schedule', executionBlockNumber, options);395 }396397 scheduleAfter<T extends DevUniqueHelper>(398 blocksBeforeExecution: number,399 options: ISchedulerOptions = {},400 ) {401 return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);402 }403404 schedule<T extends UniqueHelper>(405 scheduleFn: 'schedule' | 'scheduleAfter',406 blocksNum: number,407 options: ISchedulerOptions = {},408 ) {409 // eslint-disable-next-line @typescript-eslint/naming-convention410 const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);411 return this.helper.clone(ScheduledHelperType, {412 scheduleFn,413 blocksNum,414 options,415 }) as T;416 }417}418419class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {420 //todo:collator documentation421 addInvulnerable(signer: TSigner, address: string) {422 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);423 }424425 removeInvulnerable(signer: TSigner, address: string) {426 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);427 }428429 async getInvulnerables(): Promise<string[]> {430 return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());431 }432433 /** and also total max invulnerables */434 maxCollators(): number {435 return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);436 }437438 async getDesiredCollators(): Promise<number> {439 return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();440 }441442 setLicenseBond(signer: TSigner, amount: bigint) {443 return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);444 }445446 async getLicenseBond(): Promise<bigint> {447 return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();448 }449450 obtainLicense(signer: TSigner) {451 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);452 }453454 releaseLicense(signer: TSigner) {455 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);456 }457458 forceReleaseLicense(signer: TSigner, released: string) {459 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);460 }461462 async hasLicense(address: string): Promise<bigint> {463 return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();464 }465466 onboard(signer: TSigner) {467 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);468 }469470 offboard(signer: TSigner) {471 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);472 }473474 async getCandidates(): Promise<string[]> {475 return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());476 }477}478479export class DevUniqueHelper extends UniqueHelper {480 /**481 * Arrange methods for tests482 */483 arrange: ArrangeGroup;484 wait: WaitGroup;485 admin: AdminGroup;486 session: SessionGroup;487 testUtils: TestUtilGroup;488 foreignAssets: ForeignAssetsGroup;489 xcm: XcmGroup<DevUniqueHelper>;490 xTokens: XTokensGroup<DevUniqueHelper>;491 tokens: TokensGroup<DevUniqueHelper>;492 scheduler: SchedulerGroup;493 collatorSelection: CollatorSelectionGroup;494 council: ICollectiveGroup;495 finCouncil: ICollectiveGroup;496 technicalCommittee: ICollectiveGroup;497 fellowship: IFellowshipGroup;498 democracy: DemocracyGroup;499500 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {501 options.helperBase = options.helperBase ?? DevUniqueHelper;502503 super(logger, options);504 this.arrange = new ArrangeGroup(this);505 this.wait = new WaitGroup(this);506 this.admin = new AdminGroup(this);507 this.testUtils = new TestUtilGroup(this);508 this.session = new SessionGroup(this);509 this.foreignAssets = new ForeignAssetsGroup(this);510 this.xcm = new XcmGroup(this, 'polkadotXcm');511 this.xTokens = new XTokensGroup(this);512 this.tokens = new TokensGroup(this);513 this.scheduler = new SchedulerGroup(this);514 this.collatorSelection = new CollatorSelectionGroup(this);515 this.council = {516 collective: new CollectiveGroup(this, 'council'),517 membership: new CollectiveMembershipGroup(this, 'councilMembership'),518 };519 this.finCouncil = {520 collective: new CollectiveGroup(this, 'financialCouncil'),521 membership: new CollectiveMembershipGroup(this, 'financialCouncilMembership'),522 };523 this.technicalCommittee = {524 collective: new CollectiveGroup(this, 'technicalCommittee'),525 membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),526 };527 this.fellowship = {528 collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),529 referenda: new ReferendaGroup(this, 'fellowshipReferenda'),530 };531 this.democracy = new DemocracyGroup(this);532 }533534 override async connect(wsEndpoint: string, _listeners?: any): Promise<void> {535 if(!wsEndpoint) throw new Error('wsEndpoint was not set');536 const wsProvider = new WsProvider(wsEndpoint);537 this.api = new ApiPromise({538 provider: wsProvider,539 signedExtensions: {540 ContractHelpers: {541 extrinsic: {},542 payload: {},543 },544 CheckMaintenance: {545 extrinsic: {},546 payload: {},547 },548 DisableIdentityCalls: {549 extrinsic: {},550 payload: {},551 },552 FakeTransactionFinalizer: {553 extrinsic: {},554 payload: {},555 },556 },557 rpc: {558 unique: defs.unique.rpc,559 appPromotion: defs.appPromotion.rpc,560 povinfo: defs.povinfo.rpc,561 eth: {562 feeHistory: {563 description: 'Dummy',564 params: [],565 type: 'u8',566 },567 maxPriorityFeePerGas: {568 description: 'Dummy',569 params: [],570 type: 'u8',571 },572 },573 },574 });575 await this.api.isReadyOrError;576 this.network = await UniqueHelper.detectNetwork(this.api);577 this.wsEndpoint = wsEndpoint;578 }579 getSudo<T extends DevUniqueHelper>() {580 // eslint-disable-next-line @typescript-eslint/naming-convention581 const SudoHelperType = SudoHelper(this.helperBase);582 return this.clone(SudoHelperType) as T;583 }584}585586export class DevRelayHelper extends RelayHelper {587 wait: WaitGroup;588589 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {590 options.helperBase = options.helperBase ?? DevRelayHelper;591592 super(logger, options);593 this.wait = new WaitGroup(this);594 }595596 getSudo() {597 // eslint-disable-next-line @typescript-eslint/naming-convention598 const SudoHelperType = SudoHelper(this.helperBase);599 return this.clone(SudoHelperType) as DevRelayHelper;600 }601}602603export class DevWestmintHelper extends WestmintHelper {604 wait: WaitGroup;605606 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {607 options.helperBase = options.helperBase ?? DevWestmintHelper;608609 super(logger, options);610 this.wait = new WaitGroup(this);611 }612}613614export class DevStatemineHelper extends DevWestmintHelper {}615616export class DevStatemintHelper extends DevWestmintHelper {}617618export class DevMoonbeamHelper extends MoonbeamHelper {619 account: MoonbeamAccountGroup;620 wait: WaitGroup;621 fastDemocracy: MoonbeamFastDemocracyGroup;622623 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {624 options.helperBase = options.helperBase ?? DevMoonbeamHelper;625 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';626627 super(logger, options);628 this.account = new MoonbeamAccountGroup(this);629 this.wait = new WaitGroup(this);630 this.fastDemocracy = new MoonbeamFastDemocracyGroup(this);631 }632}633634export class DevMoonriverHelper extends DevMoonbeamHelper {635 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {636 options.notePreimagePallet = options.notePreimagePallet ?? 'preimage';637 super(logger, options);638 }639}640641export class DevAstarHelper extends AstarHelper {642 wait: WaitGroup;643644 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {645 options.helperBase = options.helperBase ?? DevAstarHelper;646647 super(logger, options);648 this.wait = new WaitGroup(this);649 }650651 getSudo<T extends AstarHelper>() {652 // eslint-disable-next-line @typescript-eslint/naming-convention653 const SudoHelperType = SudoHelper(this.helperBase);654 return this.clone(SudoHelperType) as T;655 }656}657658export class DevShidenHelper extends DevAstarHelper { }659660export class DevAcalaHelper extends AcalaHelper {661 wait: WaitGroup;662663 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {664 options.helperBase = options.helperBase ?? DevAcalaHelper;665666 super(logger, options);667 this.wait = new WaitGroup(this);668 }669 getSudo() {670 // eslint-disable-next-line @typescript-eslint/naming-convention671 const SudoHelperType = SudoHelper(this.helperBase);672 return this.clone(SudoHelperType) as DevAcalaHelper;673 }674}675676export class DevPolkadexHelper extends PolkadexHelper {677 wait: WaitGroup;678 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {679 options.helperBase = options.helperBase ?? PolkadexHelper;680681 super(logger, options);682 this.wait = new WaitGroup(this);683 }684685 getSudo() {686 // eslint-disable-next-line @typescript-eslint/naming-convention687 const SudoHelperType = SudoHelper(this.helperBase);688 return this.clone(SudoHelperType) as DevPolkadexHelper;689 }690}691692export class DevHydraDxHelper extends HydraDxHelper {693 wait: WaitGroup;694 fastDemocracy: HydraFastDemocracyGroup;695696 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {697 options.helperBase = options.helperBase ?? DevHydraDxHelper;698699 super(logger, options);700701 this.wait = new WaitGroup(this);702 this.fastDemocracy = new HydraFastDemocracyGroup(this);703 }704}705706export class DevKaruraHelper extends DevAcalaHelper {}707708export class ArrangeGroup {709 helper: DevUniqueHelper;710711 scheduledIdSlider = 0;712713 constructor(helper: DevUniqueHelper) {714 this.helper = helper;715 }716717 /**718 * Generates accounts with the specified UNQ token balance719 * @param balances balances for generated accounts. Each balance will be multiplied by the token nominal.720 * @param donor donor account for balances721 * @returns array of newly created accounts722 * @example const [acc1, acc2, acc3] = await createAccounts([0n, 10n, 20n], donor);723 */724 createAccounts = async (balances: bigint[], donor: IKeyringPair): Promise<IKeyringPair[]> => {725 let nonce = await this.helper.chain.getNonce(donor.address);726 const wait = new WaitGroup(this.helper);727 const ss58Format = this.helper.chain.getChainProperties().ss58Format;728 const tokenNominal = this.helper.balance.getOneTokenNominal();729 const transactions = [];730 const accounts: IKeyringPair[] = [];731 for(const balance of balances) {732 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);733 accounts.push(recipient);734 if(balance !== 0n) {735 const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, balance * tokenNominal]);736 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));737 nonce++;738 }739 }740741 await Promise.all(transactions).catch(_e => {});742743 //#region TODO remove this region, when nonce problem will be solved744 const checkBalances = async () => {745 let isSuccess = true;746 for(let i = 0; i < balances.length; i++) {747 const balance = await this.helper.balance.getSubstrate(accounts[i].address);748 if(balance !== balances[i] * tokenNominal) {749 isSuccess = false;750 break;751 }752 }753 return isSuccess;754 };755756 let accountsCreated = false;757 const maxBlocksChecked = await this.helper.arrange.isDevNode() ? 50 : 5;758 // checkBalances retry up to 5-50 blocks759 for(let index = 0; index < maxBlocksChecked; index++) {760 accountsCreated = await checkBalances();761 if(accountsCreated) break;762 await wait.newBlocks(1);763 }764765 if(!accountsCreated) throw Error('Accounts generation failed');766 //#endregion767768 return accounts;769 };770771 // TODO combine this method and createAccounts into one772 createCrowd = async (accountsToCreate: number, withBalance: bigint, donor: IKeyringPair): Promise<IKeyringPair[]> => {773 const createAsManyAsCan = async () => {774 let transactions: any = [];775 const accounts: IKeyringPair[] = [];776 let nonce = await this.helper.chain.getNonce(donor.address);777 const tokenNominal = this.helper.balance.getOneTokenNominal();778 const ss58Format = this.helper.chain.getChainProperties().ss58Format;779 for(let i = 0; i < accountsToCreate; i++) {780 if(i === 500) { // if there are too many accounts to create781 await Promise.allSettled(transactions); // wait while first 500 (should be 100 for devnode) tx will be settled782 transactions = []; //783 nonce = await this.helper.chain.getNonce(donor.address); // update nonce784 }785 const recipient = this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);786 accounts.push(recipient);787 if(withBalance !== 0n) {788 const tx = this.helper.constructApiCall('api.tx.balances.transferKeepAlive', [{Id: recipient.address}, withBalance * tokenNominal]);789 transactions.push(this.helper.signTransaction(donor, tx, {nonce}, 'account generation'));790 nonce++;791 }792 }793794 const fullfilledAccounts = [];795 await Promise.allSettled(transactions);796 for(const account of accounts) {797 const accountBalance = await this.helper.balance.getSubstrate(account.address);798 if(accountBalance === withBalance * tokenNominal) {799 fullfilledAccounts.push(account);800 }801 }802 return fullfilledAccounts;803 };804805806 const crowd: IKeyringPair[] = [];807 // do up to 5 retries808 for(let index = 0; index < 5 && accountsToCreate !== 0; index++) {809 const asManyAsCan = await createAsManyAsCan();810 crowd.push(...asManyAsCan);811 accountsToCreate -= asManyAsCan.length;812 }813814 if(accountsToCreate !== 0) throw Error(`Crowd generation failed: ${accountsToCreate} accounts left`);815816 return crowd;817 };818819 /**820 * Generates one account with zero balance821 * @returns the newly generated account822 * @example const account = await helper.arrange.createEmptyAccount();823 */824 createEmptyAccount = (): IKeyringPair => {825 const ss58Format = this.helper.chain.getChainProperties().ss58Format;826 return this.helper.util.fromSeed(mnemonicGenerate(), ss58Format);827 };828829 isDevNode = async () => {830 let blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();831 if(blockNumber == 0) {832 await this.helper.wait.newBlocks(1);833 blockNumber = (await this.helper.callRpc('api.query.system.number')).toJSON();834 }835 const block2 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber])]);836 const block1 = await this.helper.callRpc('api.rpc.chain.getBlock', [await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockNumber - 1])]);837 const findCreationDate = (block: any) => {838 const humanBlock = block.toHuman();839 let date;840 humanBlock.block.extrinsics.forEach((ext: any) => {841 if(ext.method.section === 'timestamp') {842 date = Number(ext.method.args.now.replaceAll(',', ''));843 }844 });845 return date;846 };847 const block1date = await findCreationDate(block1);848 const block2date = await findCreationDate(block2);849 if(block2date! - block1date! < 9000) return true;850 return false;851 };852853 async calculcateFee(payer: ICrossAccountId, promise: () => Promise<any>): Promise<bigint> {854 const address = 'Substrate' in payer ? payer.Substrate : this.helper.address.ethToSubstrate(payer.Ethereum);855 let balance = await this.helper.balance.getSubstrate(address);856857 await promise();858859 balance -= await this.helper.balance.getSubstrate(address);860861 return balance;862 }863864 async calculatePoVInfo(txs: any[]): Promise<IPovInfo> {865 const rawPovInfo = await this.helper.callRpc('api.rpc.povinfo.estimateExtrinsicPoV', [txs]);866867 const kvJson: {[key: string]: string} = {};868869 for(const kv of rawPovInfo.keyValues) {870 kvJson[kv.key.toHex()] = kv.value.toHex();871 }872873 const kvStr = JSON.stringify(kvJson);874875 const chainql = spawnSync(876 'chainql',877 [878 `--tla-code=data=${kvStr}`,879 '-e', `function(data) cql.dump(cql.chain("${this.helper.getEndpoint()}").latest._meta, data, {omit_empty:true})`,880 ],881 );882883 if(!chainql.stdout) {884 throw Error('unable to get an output from the `chainql`');885 }886887 return {888 proofSize: rawPovInfo.proofSize.toNumber(),889 compactProofSize: rawPovInfo.compactProofSize.toNumber(),890 compressedProofSize: rawPovInfo.compressedProofSize.toNumber(),891 results: rawPovInfo.results,892 kv: JSON.parse(chainql.stdout.toString()),893 };894 }895896 calculatePalletAddress(palletId: any) {897 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));898 return encodeAddress(address, this.helper.chain.getChainProperties().ss58Format);899 }900901 makeScheduledIds(num: number): string[] {902 function makeId(slider: number) {903 const scheduledIdSize = 64;904 const hexId = slider.toString(16);905 const prefixSize = scheduledIdSize - hexId.length;906907 const scheduledId = '0x' + '0'.repeat(prefixSize) + hexId;908909 return scheduledId;910 }911912 const ids = [];913 for(let i = 0; i < num; i++) {914 ids.push(makeId(this.scheduledIdSlider));915 this.scheduledIdSlider += 1;916 }917918 return ids;919 }920921 makeScheduledId(): string {922 return (this.makeScheduledIds(1))[0];923 }924925 async captureEvents(eventSection: string, eventMethod: string): Promise<EventCapture> {926 const capture = new EventCapture(this.helper, eventSection, eventMethod);927 await capture.startCapture();928929 return capture;930 }931932 makeXcmProgramWithdrawDeposit(beneficiary: Uint8Array, id: any, amount: bigint) {933 return {934 V2: [935 {936 WithdrawAsset: [937 {938 id,939 fun: {940 Fungible: amount,941 },942 },943 ],944 },945 {946 BuyExecution: {947 fees: {948 id,949 fun: {950 Fungible: amount,951 },952 },953 weightLimit: 'Unlimited',954 },955 },956 {957 DepositAsset: {958 assets: {959 Wild: 'All',960 },961 maxAssets: 1,962 beneficiary: {963 parents: 0,964 interior: {965 X1: {966 AccountId32: {967 network: 'Any',968 id: beneficiary,969 },970 },971 },972 },973 },974 },975 ],976 };977 }978979 makeXcmProgramReserveAssetDeposited(beneficiary: Uint8Array, id: any, amount: bigint) {980 return {981 V2: [982 {983 ReserveAssetDeposited: [984 {985 id,986 fun: {987 Fungible: amount,988 },989 },990 ],991 },992 {993 BuyExecution: {994 fees: {995 id,996 fun: {997 Fungible: amount,998 },999 },1000 weightLimit: 'Unlimited',1001 },1002 },1003 {1004 DepositAsset: {1005 assets: {1006 Wild: 'All',1007 },1008 maxAssets: 1,1009 beneficiary: {1010 parents: 0,1011 interior: {1012 X1: {1013 AccountId32: {1014 network: 'Any',1015 id: beneficiary,1016 },1017 },1018 },1019 },1020 },1021 },1022 ],1023 };1024 }10251026 makeUnpaidSudoTransactProgram(info: {weightMultiplier: number, call: string}) {1027 return {1028 V3: [1029 {1030 UnpaidExecution: {1031 weightLimit: 'Unlimited',1032 checkOrigin: null,1033 },1034 },1035 {1036 Transact: {1037 originKind: 'Superuser',1038 requireWeightAtMost: {1039 refTime: info.weightMultiplier * 200000000,1040 proofSize: info.weightMultiplier * 3000,1041 },1042 call: {1043 encoded: info.call,1044 },1045 },1046 },1047 ],1048 };1049 }1050}10511052class MoonbeamAccountGroup {1053 helper: MoonbeamHelper;10541055 keyring: Keyring;1056 _alithAccount: IKeyringPair;1057 _baltatharAccount: IKeyringPair;1058 _dorothyAccount: IKeyringPair;10591060 constructor(helper: MoonbeamHelper) {1061 this.helper = helper;10621063 this.keyring = new Keyring({type: 'ethereum'});1064 const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133';1065 const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b';1066 const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68';10671068 this._alithAccount = this.keyring.addFromUri(alithPrivateKey, undefined, 'ethereum');1069 this._baltatharAccount = this.keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum');1070 this._dorothyAccount = this.keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum');1071 }10721073 alithAccount() {1074 return this._alithAccount;1075 }10761077 baltatharAccount() {1078 return this._baltatharAccount;1079 }10801081 dorothyAccount() {1082 return this._dorothyAccount;1083 }10841085 create() {1086 return this.keyring.addFromUri(mnemonicGenerate());1087 }1088}10891090class MoonbeamFastDemocracyGroup {1091 helper: DevMoonbeamHelper;10921093 constructor(helper: DevMoonbeamHelper) {1094 this.helper = helper;1095 }10961097 async executeProposal(proposalDesciption: string, encodedProposal: string) {1098 const proposalHash = blake2AsHex(encodedProposal);10991100 const alithAccount = this.helper.account.alithAccount();1101 const baltatharAccount = this.helper.account.baltatharAccount();1102 const dorothyAccount = this.helper.account.dorothyAccount();11031104 const councilVotingThreshold = 2;1105 const technicalCommitteeThreshold = 2;1106 const fastTrackVotingPeriod = 3;1107 const fastTrackDelayPeriod = 0;11081109 console.log(`[democracy] executing '${proposalDesciption}' proposal`);11101111 // >>> Propose external motion through council >>>1112 console.log('\t* Propose external motion through council.......');1113 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1114 const encodedMotion = externalMotion?.method.toHex() || '';1115 const motionHash = blake2AsHex(encodedMotion);1116 console.log('\t* Motion hash is %s', motionHash);11171118 await this.helper.collective.council.propose(1119 baltatharAccount,1120 councilVotingThreshold,1121 externalMotion,1122 externalMotion.encodedLength,1123 );11241125 const councilProposalIdx = await this.helper.collective.council.proposalCount() - 1;1126 await this.helper.collective.council.vote(dorothyAccount, motionHash, councilProposalIdx, true);1127 await this.helper.collective.council.vote(baltatharAccount, motionHash, councilProposalIdx, true);11281129 await this.helper.collective.council.close(1130 dorothyAccount,1131 motionHash,1132 councilProposalIdx,1133 {1134 refTime: 1_000_000_000,1135 proofSize: 1_000_000,1136 },1137 externalMotion.encodedLength,1138 );1139 console.log('\t* Propose external motion through council.......DONE');1140 // <<< Propose external motion through council <<<11411142 // >>> Fast track proposal through technical committee >>>1143 console.log('\t* Fast track proposal through technical committee.......');1144 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1145 const encodedFastTrack = fastTrack?.method.toHex() || '';1146 const fastTrackHash = blake2AsHex(encodedFastTrack);1147 console.log('\t* FastTrack hash is %s', fastTrackHash);11481149 await this.helper.collective.techCommittee.propose(alithAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);11501151 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1152 await this.helper.collective.techCommittee.vote(baltatharAccount, fastTrackHash, techProposalIdx, true);1153 await this.helper.collective.techCommittee.vote(alithAccount, fastTrackHash, techProposalIdx, true);11541155 await this.helper.collective.techCommittee.close(1156 baltatharAccount,1157 fastTrackHash,1158 techProposalIdx,1159 {1160 refTime: 1_000_000_000,1161 proofSize: 1_000_000,1162 },1163 fastTrack.encodedLength,1164 );1165 console.log('\t* Fast track proposal through technical committee.......DONE');1166 // <<< Fast track proposal through technical committee <<<11671168 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1169 const referendumIndex = democracyStarted.referendumIndex;11701171 // >>> Referendum voting >>>1172 console.log(`\t* Referendum #${referendumIndex} voting.......`);1173 await this.helper.democracy.referendumVote(dorothyAccount, referendumIndex, {1174 balance: 10_000_000_000_000_000_000n,1175 vote: {aye: true, conviction: 1},1176 });1177 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1178 // <<< Referendum voting <<<11791180 // Wait the proposal to pass1181 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);11821183 await this.helper.wait.newBlocks(1);11841185 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1186 }1187}11881189class HydraFastDemocracyGroup {1190 helper: DevHydraDxHelper;11911192 constructor(helper: DevHydraDxHelper) {1193 this.helper = helper;1194 }11951196 async executeProposal(proposalDesciption: string, encodedProposal: string) {1197 const proposalHash = blake2AsHex(encodedProposal);1198 const aliceAccount = this.helper.util.fromSeed('//Alice');1199 const bobAccount = this.helper.util.fromSeed('//Bob');1200 const eveAccount = this.helper.util.fromSeed('//Eve');12011202 const councilVotingThreshold = 1;1203 const technicalCommitteeThreshold = 3;1204 const fastTrackVotingPeriod = 3;1205 const fastTrackDelayPeriod = 0;12061207 console.log(`[democracy] executing '${proposalDesciption}' proposal`);12081209 // >>> Propose external motion through council >>>1210 console.log('\t* Propose external motion through council.......');1211 const externalMotion = this.helper.democracy.externalProposeMajority({Inline: encodedProposal});1212 const encodedMotion = externalMotion?.method.toHex() || '';1213 const motionHash = blake2AsHex(encodedMotion);1214 console.log('\t* Motion hash is %s', motionHash);12151216 await this.helper.collective.council.propose(1217 aliceAccount,1218 councilVotingThreshold,1219 externalMotion,1220 externalMotion.encodedLength,1221 );12221223 console.log('\t* Propose external motion through council.......DONE');1224 // <<< Propose external motion through council <<<12251226 // >>> Fast track proposal through technical committee >>>1227 console.log('\t* Fast track proposal through technical committee.......');1228 const fastTrack = this.helper.democracy.fastTrack(proposalHash, fastTrackVotingPeriod, fastTrackDelayPeriod);1229 const encodedFastTrack = fastTrack?.method.toHex() || '';1230 const fastTrackHash = blake2AsHex(encodedFastTrack);1231 console.log('\t* FastTrack hash is %s', fastTrackHash);12321233 await this.helper.collective.techCommittee.propose(aliceAccount, technicalCommitteeThreshold, fastTrack, fastTrack.encodedLength);12341235 const techProposalIdx = await this.helper.collective.techCommittee.proposalCount() - 1;1236 await this.helper.collective.techCommittee.vote(aliceAccount, fastTrackHash, techProposalIdx, true);1237 await this.helper.collective.techCommittee.vote(bobAccount, fastTrackHash, techProposalIdx, true);1238 await this.helper.collective.techCommittee.vote(eveAccount, fastTrackHash, techProposalIdx, true);12391240 await this.helper.collective.techCommittee.close(1241 bobAccount,1242 fastTrackHash,1243 techProposalIdx,1244 {1245 refTime: 1_000_000_000,1246 proofSize: 1_000_000,1247 },1248 fastTrack.encodedLength,1249 );1250 console.log('\t* Fast track proposal through technical committee.......DONE');1251 // <<< Fast track proposal through technical committee <<<12521253 const democracyStarted = await this.helper.wait.expectEvent(3, Event.Democracy.Started);1254 const referendumIndex = democracyStarted.referendumIndex;12551256 // >>> Referendum voting >>>1257 console.log(`\t* Referendum #${referendumIndex} voting.......`);1258 await this.helper.democracy.referendumVote(eveAccount, referendumIndex, {1259 balance: 10_000_000_000_000_000_000n,1260 vote: {aye: true, conviction: 1},1261 });1262 console.log(`\t* Referendum #${referendumIndex} voting.......DONE`);1263 // <<< Referendum voting <<<12641265 // Wait the proposal to pass1266 await this.helper.wait.expectEvent(3, Event.Democracy.Passed, event => event.referendumIndex == referendumIndex);12671268 await this.helper.wait.newBlocks(1);12691270 console.log(`[democracy] executing '${proposalDesciption}' proposal.......DONE`);1271 }1272}12731274class WaitGroup {1275 helper: ChainHelperBase;12761277 constructor(helper: ChainHelperBase) {1278 this.helper = helper;1279 }12801281 sleep(milliseconds: number) {1282 return new Promise((resolve) => setTimeout(resolve, milliseconds));1283 }12841285 private async waitWithTimeout(promise: Promise<any>, timeout: number) {1286 let isBlock = false;1287 promise.then(() => isBlock = true).catch(() => isBlock = true);1288 let totalTime = 0;1289 const step = 100;1290 while(!isBlock) {1291 await this.sleep(step);1292 totalTime += step;1293 if(totalTime >= timeout) throw Error('Blocks production failed');1294 }1295 return promise;1296 }12971298 /**1299 * Launch some async operation, or throw an error after some time. Note that it will still continue executing after the timeout.1300 * @param promise async operation to race against the timeout1301 * @param timeoutMS time after which to time out1302 * @param timeoutError error message to throw1303 * @returns promise of the same type the operation had1304 */1305 withTimeout<T>(1306 promise: Promise<T>,1307 timeoutMS = 30000,1308 timeoutError = 'The operation has timed out!',1309 ): Promise<T> {1310 const timeout = new Promise<never>((_, reject) => {1311 setTimeout(() => {1312 reject(new Error(timeoutError));1313 }, timeoutMS);1314 });13151316 return Promise.race<T>([promise, timeout]).catch(e => {throw new Error(e);});1317 }13181319 /**1320 * Wait for specified number of blocks1321 * @param blocksCount number of blocks to wait1322 * @returns1323 */1324 async newBlocks(blocksCount = 1, timeout?: number): Promise<void> {1325 timeout = timeout ?? blocksCount * 60_000;1326 // eslint-disable-next-line no-async-promise-executor1327 const promise = new Promise<void>(async (resolve) => {1328 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(() => {1329 if(blocksCount > 0) {1330 blocksCount--;1331 } else {1332 unsubscribe();1333 resolve();1334 }1335 });1336 });1337 await this.waitWithTimeout(promise, timeout);1338 return promise;1339 }13401341 /**1342 * Wait for the specified number of sessions to pass.1343 * Only applicable if the Session pallet is turned on.1344 * @param sessionCount number of sessions to wait1345 * @param blockTimeout time in ms until panicking that the chain has stopped producing blocks1346 * @returns1347 */1348 async newSessions(sessionCount = 1, blockTimeout = 60000): Promise<void> {1349 console.log(`Waiting for ${sessionCount} new session${sessionCount>1's'''}.`1350 + ' This might take a while -- check SessionPeriod in pallet_session::Config for session time.');13511352 const expectedSessionIndex = await (this.helper as DevUniqueHelper).session.getIndex() + sessionCount;1353 let currentSessionIndex = -1;13541355 while(currentSessionIndex < expectedSessionIndex) {1356 // eslint-disable-next-line no-async-promise-executor1357 currentSessionIndex = await this.withTimeout(new Promise(async (resolve) => {1358 await this.newBlocks(1);1359 const res = await (this.helper as DevUniqueHelper).session.getIndex();1360 resolve(res);1361 }), blockTimeout, 'The chain has stopped producing blocks!');1362 }1363 }13641365 async forParachainBlockNumber(blockNumber: bigint | number, timeout?: number) {1366 timeout = timeout ?? 30 * 60 * 1000;1367 // eslint-disable-next-line no-async-promise-executor1368 const promise = new Promise<void>(async (resolve) => {1369 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1370 if(data.number.toNumber() >= blockNumber) {1371 unsubscribe();1372 resolve();1373 }1374 });1375 });1376 await this.waitWithTimeout(promise, timeout);1377 return promise;1378 }13791380 async forRelayBlockNumber(blockNumber: bigint | number, timeout?: number) {1381 timeout = timeout ?? 30 * 60 * 1000;1382 // eslint-disable-next-line no-async-promise-executor1383 const promise = new Promise<void>(async (resolve) => {1384 const unsubscribe = await this.helper.getApi().query.parachainSystem.validationData((data: any) => {1385 if(data.value.relayParentNumber.toNumber() >= blockNumber) {1386 // @ts-ignore1387 unsubscribe();1388 resolve();1389 }1390 });1391 });1392 await this.waitWithTimeout(promise, timeout);1393 return promise;1394 }13951396 noScheduledTasks() {1397 const api = this.helper.getApi();13981399 // eslint-disable-next-line no-async-promise-executor1400 const promise = new Promise<void>(async resolve => {1401 const unsubscribe = await api.rpc.chain.subscribeNewHeads(async () => {1402 const areThereScheduledTasks = await api.query.scheduler.lookup.entries();14031404 if(areThereScheduledTasks.length == 0) {1405 unsubscribe();1406 resolve();1407 }1408 });1409 });14101411 return promise;1412 }14131414 parachainBlockMultiplesOf(val: bigint) {1415 // eslint-disable-next-line no-async-promise-executor1416 const promise = new Promise<void>(async resolve => {1417 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads((data: any) => {1418 if(data.number.toBigInt() % val == 0n) {1419 console.log(`from waiter: ${data.number.toBigInt()}`);1420 unsubscribe();1421 resolve();1422 }1423 });1424 });1425 return promise;1426 }14271428 event<T extends IEventHelper>(1429 maxBlocksToWait: number,1430 eventHelper: T,1431 filter: (_: any) => boolean = () => true,1432 ): any {1433 // eslint-disable-next-line no-async-promise-executor1434 const promise = new Promise<T | null>(async (resolve) => {1435 const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => {1436 const blockNumber = header.number.toJSON();1437 const blockHash = header.hash;1438 const eventIdStr = `${eventHelper.section()}.${eventHelper.method()}`;1439 const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`;14401441 this.helper.logger.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`);14421443 const apiAt = await this.helper.getApi().at(blockHash);1444 const eventRecords = (await apiAt.query.system.events()) as any;14451446 const neededEvent = eventRecords.toArray()1447 .filter((r: FrameSystemEventRecord) => r.event.section == eventHelper.section() && r.event.method == eventHelper.method())1448 .map((r: FrameSystemEventRecord) => eventHelper.wrapEvent(r.event.data))1449 .find(filter);14501451 if(neededEvent) {1452 unsubscribe();1453 resolve(neededEvent);1454 } else if(maxBlocksToWait > 0) {1455 maxBlocksToWait--;1456 } else {1457 this.helper.logger.log(`Eligible event \`${eventIdStr}\` is NOT found.1458 The wait lasted until block ${blockNumber} inclusive`);1459 unsubscribe();1460 resolve(null);1461 }1462 });1463 });1464 return promise;1465 }14661467 async expectEvent<T extends IEventHelper>(1468 maxBlocksToWait: number,1469 eventHelper: T,1470 filter: (e: any) => boolean = () => true,1471 ) {1472 const e = await this.event(maxBlocksToWait, eventHelper, filter);1473 if(e == null) {1474 throw Error(`The event '${eventHelper.section()}.${eventHelper.method()}' is expected`);1475 } else {1476 return e;1477 }1478 }1479}14801481class SessionGroup {1482 helper: ChainHelperBase;14831484 constructor(helper: ChainHelperBase) {1485 this.helper = helper;1486 }14871488 //todo:collator documentation1489 async getIndex(): Promise<number> {1490 return (await this.helper.callRpc('api.query.session.currentIndex', [])).toNumber();1491 }14921493 newSessions(sessionCount = 1, blockTimeout = 24000): Promise<void> {1494 return (this.helper as DevUniqueHelper).wait.newSessions(sessionCount, blockTimeout);1495 }14961497 setOwnKeys(signer: TSigner, key: string) {1498 return this.helper.executeExtrinsic(1499 signer,1500 'api.tx.session.setKeys',1501 [key, '0x0'],1502 true,1503 );1504 }15051506 setOwnKeysFromAddress(signer: TSigner) {1507 return this.setOwnKeys(signer, '0x' + Buffer.from(signer.addressRaw).toString('hex'));1508 }1509}15101511class TestUtilGroup {1512 helper: DevUniqueHelper;15131514 constructor(helper: DevUniqueHelper) {1515 this.helper = helper;1516 }15171518 async enable(testUtilsPalletName: string) {1519 if(this.helper.fetchMissingPalletNames([testUtilsPalletName]).length != 0) {1520 return;1521 }15221523 const signer = this.helper.util.fromSeed('//Alice');1524 await this.helper.getSudo<DevUniqueHelper>().executeExtrinsic(signer, 'api.tx.testUtils.enable', [], true);1525 }15261527 async setTestValue(signer: TSigner, testVal: number) {1528 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValue', [testVal], true);1529 }15301531 async incTestValue(signer: TSigner) {1532 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.incTestValue', [], true);1533 }15341535 async setTestValueAndRollback(signer: TSigner, testVal: number) {1536 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.setTestValueAndRollback', [testVal], true);1537 }15381539 async testValue(blockIdx?: number) {1540 const api = blockIdx1541 ? await this.helper.getApi().at(await this.helper.callRpc('api.rpc.chain.getBlockHash', [blockIdx]))1542 : this.helper.getApi();15431544 return (await api.query.testUtils.testValue()).toJSON();1545 }15461547 async justTakeFee(signer: TSigner) {1548 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.justTakeFee', [], true);1549 }15501551 async selfCancelingInc(signer: TSigner, scheduledId: string, maxTestVal: number) {1552 await this.helper.executeExtrinsic(signer, 'api.tx.testUtils.selfCancelingInc', [scheduledId, maxTestVal], true);1553 }1554}15551556class EventCapture {1557 helper: DevUniqueHelper;1558 eventSection: string;1559 eventMethod: string;1560 events: EventRecord[] = [];1561 unsubscribe: VoidFn | null = null;15621563 constructor(1564 helper: DevUniqueHelper,1565 eventSection: string,1566 eventMethod: string,1567 ) {1568 this.helper = helper;1569 this.eventSection = eventSection;1570 this.eventMethod = eventMethod;1571 }15721573 async startCapture() {1574 this.stopCapture();1575 this.unsubscribe = (await this.helper.getApi().query.system.events((eventRecords: FrameSystemEventRecord[]) => {1576 const newEvents = eventRecords.filter(r => r.event.section == this.eventSection && r.event.method == this.eventMethod);15771578 this.events.push(...newEvents);1579 })) as any;1580 }15811582 stopCapture() {1583 if(this.unsubscribe !== null) {1584 this.unsubscribe();1585 }1586 }15871588 extractCapturedEvents() {1589 return this.events;1590 }1591}15921593class AdminGroup {1594 helper: UniqueHelper;15951596 constructor(helper: UniqueHelper) {1597 this.helper = helper;1598 }15991600 async payoutStakers(signer: IKeyringPair, stakersToPayout: number): Promise<{staker: string, stake: bigint, payout: bigint}[]> {1601 const payoutResult = await this.helper.executeExtrinsic(signer, 'api.tx.appPromotion.payoutStakers', [stakersToPayout], true);1602 return payoutResult.result.events.filter(e => e.event.method === 'StakingRecalculation').map(e => ({1603 staker: e.event.data[0].toString(),1604 stake: e.event.data[1].toBigInt(),1605 payout: e.event.data[2].toBigInt(),1606 }));1607 }1608}16091610// eslint-disable-next-line @typescript-eslint/naming-convention1611function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {1612 return class extends Base {1613 scheduleFn: 'schedule' | 'scheduleAfter';1614 blocksNum: number;1615 options: ISchedulerOptions;16161617 constructor(...args: any[]) {1618 const logger = args[0] as ILogger;1619 const options = args[1] as {1620 scheduleFn: 'schedule' | 'scheduleAfter',1621 blocksNum: number,1622 options: ISchedulerOptions1623 };16241625 super(logger);16261627 this.scheduleFn = options.scheduleFn;1628 this.blocksNum = options.blocksNum;1629 this.options = options.options;1630 }16311632 override executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {1633 const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);16341635 const mandatorySchedArgs = [1636 this.blocksNum,1637 this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,1638 this.options.priority ?? null,1639 scheduledTx,1640 ];16411642 let schedArgs;1643 let scheduleFn;16441645 if(this.options.scheduledId) {1646 schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];16471648 if(this.scheduleFn == 'schedule') {1649 scheduleFn = 'scheduleNamed';1650 } else if(this.scheduleFn == 'scheduleAfter') {1651 scheduleFn = 'scheduleNamedAfter';1652 }1653 } else {1654 schedArgs = mandatorySchedArgs;1655 scheduleFn = this.scheduleFn;1656 }16571658 const extrinsic = 'api.tx.scheduler.' + scheduleFn;16591660 return super.executeExtrinsic(1661 sender,1662 extrinsic as any,1663 schedArgs,1664 expectSuccess,1665 );1666 }1667 };1668}js-packages/test-utils/util.tsdiffbeforeafterboth--- a/js-packages/test-utils/util.ts
+++ b/js-packages/test-utils/util.ts
@@ -117,6 +117,7 @@
Identity = 'identity',
Democracy = 'democracy',
Council = 'council',
+ FinancialCouncil = 'financialcouncil',
//CouncilMembership = 'councilmembership',
TechnicalCommittee = 'technicalcommittee',
Fellowship = 'fellowshipcollective',
js-packages/tests/sub/governance/council.test.tsdiffbeforeafterboth--- a/js-packages/tests/sub/governance/council.test.ts
+++ b/js-packages/tests/sub/governance/council.test.ts
@@ -2,7 +2,7 @@
import type {IKeyringPair} from '@polkadot/types/types';
import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
import {Event} from '@unique/test-utils';
-import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin} from './util.js';
+import {initCouncil, democracyLaunchPeriod, democracyVotingPeriod, democracyEnactmentPeriod, councilMotionDuration, democracyFastTrackVotingPeriod, fellowshipRankLimit, clearCouncil, clearTechComm, initTechComm, clearFellowship, dummyProposal, dummyProposalCall, initFellowship, defaultEnactmentMoment, fellowshipPropositionOrigin, initFinCouncil} from './util.js';
import type {ICounselors} from './util.js';
describeGov('Governance: Council tests', () => {
@@ -192,6 +192,25 @@
expect(techCommMembers).to.not.contains(techComm.andy.address);
});
+ itSub('Council can remove FinCouncil member', async ({helper}) => {
+ const finCouncil = await initFinCouncil(donor, sudoer);
+ const removeMemberPrpoposal = helper.finCouncil.membership.removeMemberCall(finCouncil.andy.address);
+ await proposalFromMoreThanHalfCouncil(removeMemberPrpoposal);
+
+ const finCouncilMembers = await helper.finCouncil.membership.getMembers();
+ expect(finCouncilMembers).to.not.contains(finCouncil.andy.address);
+ });
+
+ itSub('Council can add FinCouncil member', async ({helper}) => {
+ await initFinCouncil(donor, sudoer);
+ const newFinCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberPrpoposal = helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address);
+ await proposalFromMoreThanHalfCouncil(addMemberPrpoposal);
+
+ const finCouncilMembers = await helper.finCouncil.membership.getMembers();
+ expect(finCouncilMembers).to.contains(newFinCouncilMember.address);
+ });
+
itSub.skip('Council member can add Fellowship member', async ({helper}) => {
const newFellowshipMember = helper.arrange.createEmptyAccount();
await expect(helper.council.collective.execute(
@@ -328,6 +347,22 @@
)).to.be.rejectedWith('BadOrigin');
});
+ itSub('[Negative] Council member can\'t add FinCouncil member', async ({helper}) => {
+ const newFinCouncilMember = helper.arrange.createEmptyAccount();
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address),
+ )).rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] Council member can\'t remove FinCouncil member', async ({helper}) => {
+ const finCouncil = await initFinCouncil(donor, sudoer);
+ await expect(helper.council.collective.execute(
+ counselors.alex,
+ helper.finCouncil.membership.removeMemberCall(finCouncil.ildar.address),
+ )).rejectedWith('BadOrigin');
+ });
+
itSub('[Negative] Council member cannot promote/demote a Fellowship member', async ({helper}) => {
const fellowship = await initFellowship(donor, sudoer);
const memberWithRankOne = fellowship[1][0];
js-packages/tests/sub/governance/financialCouncil.test.tsdiffbeforeafterboth--- /dev/null
+++ b/js-packages/tests/sub/governance/financialCouncil.test.ts
@@ -0,0 +1,406 @@
+import type {IKeyringPair} from '@polkadot/types/types';
+import {usingPlaygrounds, itSub, expect, Pallets, requirePalletsOrSkip, describeGov} from '@unique/test-utils/util.js';
+import {Event} from '@unique/test-utils';
+import {democracyFastTrackVotingPeriod, IFinCounselors, clearTechComm, dummyProposalCall, initFinCouncil, clearFinCouncil, democracyLaunchPeriod, initFellowship, dummyProposal, fellowshipPropositionOrigin, defaultEnactmentMoment, initCouncil, clearCouncil, clearFellowship} from './util.js';
+
+
+describeGov('Governance: Financial Council tests', () => {
+ let donor: IKeyringPair;
+ let finCounselors: IFinCounselors;
+ let sudoer: IKeyringPair;
+
+ const moreThanHalfCouncilThreshold = 2;
+
+ before(async function() {
+ await usingPlaygrounds(async (helper, privateKey) => {
+ requirePalletsOrSkip(this, helper, [Pallets.FinancialCouncil]);
+ sudoer = await privateKey('//Alice');
+ donor = await privateKey({url: import.meta.url});
+ });
+ });
+
+ beforeEach(async () => {
+ finCounselors = await initFinCouncil(donor, sudoer);
+ });
+
+ afterEach(async () => {
+ await clearFinCouncil(sudoer);
+ await clearTechComm(sudoer);
+ });
+
+ async function proposalFromMoreThanHalfCouncil(proposal: any) {
+ return await usingPlaygrounds(async (helper) => {
+ expect((await helper.finCouncil.membership.getMembers()).length).to.be.equal(3);
+ const proposeResult = await helper.finCouncil.collective.propose(
+ finCounselors.ildar,
+ proposal,
+ moreThanHalfCouncilThreshold,
+ );
+
+ const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult);
+ const proposalIndex = councilProposedEvent.proposalIndex;
+ const proposalHash = councilProposedEvent.proposalHash;
+
+ await helper.finCouncil.collective.vote(finCounselors.greg, proposalHash, proposalIndex, true);
+ await helper.finCouncil.collective.vote(finCounselors.ildar, proposalHash, proposalIndex, true);
+
+ return await helper.finCouncil.collective.close(finCounselors.ildar, proposalHash, proposalIndex);
+ });
+ }
+
+ async function proposalFromAllCouncil(proposal: any) {
+ return await usingPlaygrounds(async (helper) => {
+ expect((await helper.finCouncil.membership.getMembers()).length).to.be.equal(3);
+ const proposeResult = await helper.finCouncil.collective.propose(
+ finCounselors.ildar,
+ proposal,
+ moreThanHalfCouncilThreshold,
+ );
+
+ const councilProposedEvent = Event.FinCouncil.Proposed.expect(proposeResult);
+ const proposalIndex = councilProposedEvent.proposalIndex;
+ const proposalHash = councilProposedEvent.proposalHash;
+
+ await helper.finCouncil.collective.vote(finCounselors.greg, proposalHash, proposalIndex, true);
+ await helper.finCouncil.collective.vote(finCounselors.ildar, proposalHash, proposalIndex, true);
+ await helper.finCouncil.collective.vote(finCounselors.andy, proposalHash, proposalIndex, true);
+
+ return await helper.finCouncil.collective.close(finCounselors.andy, proposalHash, proposalIndex);
+ });
+ }
+
+ itSub('FinCouncil member can register foreign asset', async ({helper}) => {
+ const location = {
+ parents: 1,
+ interior: {X3: [
+ {
+ Parachain: 1000,
+ },
+ {
+ PalletInstance: 50,
+ },
+ {
+ GeneralIndex: 1984,
+ },
+ ]},
+ };
+ const assetId = {Concrete: location};
+
+ const registerForeignAssetCall = helper.constructApiCall(
+ 'api.tx.foreignAssets.forceRegisterForeignAsset',
+ [{V3: assetId}, helper.util.str2vec('New Asset'), 'NEW', {Fungible: 10}],
+ );
+
+ await helper.finCouncil.collective.execute(finCounselors.andy, registerForeignAssetCall);
+
+ const asset = await helper.foreignAssets.foreignCollectionId(location);
+ expect(asset).not.null;
+ });
+
+ itSub('[Negative] FinCouncil can\'t fast-track Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.wait.parachainBlockMultiplesOf(35n);
+
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(proposalFromAllCouncil(helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0)))
+ .rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot fast-track Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.democracy.fastTrackCall(preimageHash, democracyFastTrackVotingPeriod, 0),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil can\'t cancel Democracy proposals', async ({helper}) => {
+ const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+ const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+ await expect(proposalFromAllCouncil(helper.democracy.cancelProposalCall(proposalIndex)))
+ .rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot cancel Democracy proposals', async ({helper}) => {
+ const proposeResult = await helper.getSudo().democracy.propose(sudoer, dummyProposalCall(helper), 0n);
+ const proposalIndex = Event.Democracy.Proposed.expect(proposeResult).proposalIndex;
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.democracy.cancelProposalCall(proposalIndex),
+ ))
+ .to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil can\'t cancel ongoing Democracy referendums', async ({helper}) => {
+ await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ await expect(proposalFromAllCouncil(helper.democracy.emergencyCancelCall(referendumIndex)))
+ .rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot cancel ongoing Democracy referendums', async ({helper}) => {
+ await helper.getSudo().democracy.externalProposeDefault(sudoer, dummyProposalCall(helper));
+ const startedEvent = await helper.wait.expectEvent(democracyLaunchPeriod, Event.Democracy.Started);
+ const referendumIndex = startedEvent.referendumIndex;
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.democracy.emergencyCancelCall(referendumIndex),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member can\'t veto Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.democracy.vetoExternalCall(preimageHash),
+ )).rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot blacklist Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(proposalFromAllCouncil(helper.democracy.blacklistCall(preimageHash))).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot blacklist Democracy proposals', async ({helper}) => {
+ const preimageHash = await helper.preimage.notePreimageFromCall(sudoer, dummyProposalCall(helper), true);
+ await helper.getSudo().democracy.externalProposeDefaultWithPreimage(sudoer, preimageHash);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.democracy.blacklistCall(preimageHash),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil can\' cancel Fellowship referendums', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const fellowshipProposer = fellowship[5][0];
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ fellowshipProposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+ await expect(proposalFromAllCouncil(helper.fellowship.referenda.cancelCall(referendumIndex)))
+ .rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot cancel Fellowship referendums', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+ const fellowshipProposer = fellowship[5][0];
+ const proposal = dummyProposal(helper);
+
+ const submitResult = await helper.fellowship.referenda.submit(
+ fellowshipProposer,
+ fellowshipPropositionOrigin,
+ proposal,
+ defaultEnactmentMoment,
+ );
+
+ const referendumIndex = Event.FellowshipReferenda.Submitted.expect(submitResult).referendumIndex;
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.fellowship.referenda.cancelCall(referendumIndex),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member can\' add a Fellowship member', async ({helper}) => {
+ const newFellowshipMember = helper.arrange.createEmptyAccount();
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.fellowship.collective.addMemberCall(newFellowshipMember.address),
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot submit regular democracy proposal', async ({helper}) => {
+ const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
+
+ await expect(proposalFromAllCouncil(councilProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+ const commiteeProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot submit regular democracy proposal', async ({helper}) => {
+ const memberProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot externally propose SimpleMajority', async ({helper}) => {
+ const commiteeProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot externally propose SuperMajorityApprove', async ({helper}) => {
+ const commiteeProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
+
+ await expect(proposalFromAllCouncil(commiteeProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot externally propose SuperMajorityAgainst', async ({helper}) => {
+ const memberProposal = await helper.democracy.externalProposeDefaultCall(dummyProposalCall(helper));
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot externally propose SimpleMajority', async ({helper}) => {
+ const memberProposal = await helper.democracy.externalProposeMajorityCall(dummyProposalCall(helper));
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot externally propose SuperMajorityApprove', async ({helper}) => {
+ const memberProposal = await helper.democracy.externalProposeCall(dummyProposalCall(helper));
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ memberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot add/remove a Council member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
+
+ await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+ await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot add/remove a Council member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCouncilMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCouncilMember.address);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ addMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ removeMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot add/remove a FinCouncil member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.finCouncil.membership.addMemberCall(newCouncilMember.address);
+ const removeMemberProposal = helper.finCouncil.membership.removeMemberCall(finCounselors.ildar.address);
+
+ await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+ await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot add/remove a FinCouncil member', async ({helper}) => {
+ const newCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.finCouncil.membership.addMemberCall(newCouncilMember.address);
+ const removeMemberProposal = helper.finCouncil.membership.removeMemberCall(finCounselors.ildar.address);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ addMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ removeMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot set/clear Council prime member', async ({helper}) => {
+ const counselors = await initCouncil(donor, sudoer);
+ const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
+ const proposalForClear = await helper.council.membership.clearPrimeCall();
+
+ await expect(proposalFromAllCouncil(proposalForSet)).to.be.rejectedWith('BadOrigin');
+ await expect(proposalFromAllCouncil(proposalForClear)).to.be.rejectedWith('BadOrigin');
+ await clearCouncil(sudoer);
+ });
+
+ itSub('[Negative] FinCouncil member cannot set/clear Council prime member', async ({helper}) => {
+ const counselors = await initCouncil(donor, sudoer);
+ const proposalForSet = await helper.council.membership.setPrimeCall(counselors.charu.address);
+ const proposalForClear = await helper.council.membership.clearPrimeCall();
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ proposalForSet,
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ proposalForClear,
+ )).to.be.rejectedWith('BadOrigin');
+ await clearCouncil(sudoer);
+ });
+
+ itSub('[Negative] FinCouncil cannot add/remove a TechComm member', async ({helper}) => {
+ const newCommMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
+
+ await expect(proposalFromAllCouncil(addMemberProposal)).to.be.rejectedWith('BadOrigin');
+ await expect(proposalFromAllCouncil(removeMemberProposal)).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil member cannot add/remove a TechComm member', async ({helper}) => {
+ const newCommMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.council.membership.addMemberCall(newCommMember.address);
+ const removeMemberProposal = helper.council.membership.removeMemberCall(newCommMember.address);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ addMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ removeMemberProposal,
+ )).to.be.rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] FinCouncil cannot remove a Fellowship member', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+
+ await expect(proposalFromAllCouncil(helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5))).to.be.rejectedWith('BadOrigin');
+ await clearFellowship(sudoer);
+ });
+
+ itSub('[Negative] FinCouncil member cannot remove a Fellowship member', async ({helper}) => {
+ const fellowship = await initFellowship(donor, sudoer);
+
+ await expect(helper.finCouncil.collective.execute(
+ finCounselors.andy,
+ helper.fellowship.collective.removeMemberCall(fellowship[5][0].address, 5),
+ )).to.be.rejectedWith('BadOrigin');
+ await clearFellowship(sudoer);
+ });
+
+
+});
js-packages/tests/sub/governance/technicalCommittee.test.tsdiffbeforeafterboth--- a/js-packages/tests/sub/governance/technicalCommittee.test.ts
+++ b/js-packages/tests/sub/governance/technicalCommittee.test.ts
@@ -130,6 +130,50 @@
await clearFellowship(sudoer);
});
+ itSub('[Negative] TechComm can\'t add FinCouncil member', async ({helper}) => {
+ const newFinCouncilMember = helper.arrange.createEmptyAccount();
+ const addMemberProposal = helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address);
+ await proposalFromAllCommittee(addMemberProposal);
+
+ const finCouncilMembers = await helper.finCouncil.membership.getMembers();
+ expect(finCouncilMembers).to.contains(newFinCouncilMember.address);
+ });
+
+
+ itSub('[Negative] TechComm member can\'t add FinCouncil member', async ({helper}) => {
+ const newFinCouncilMember = helper.arrange.createEmptyAccount();
+ await expect(helper.technicalCommittee.collective.execute(
+ techcomms.greg,
+ helper.finCouncil.membership.addMemberCall(newFinCouncilMember.address),
+ )).rejectedWith('BadOrigin');
+ });
+
+ itSub('[Negative] TechComm member cannot register foreign asset', async ({helper}) => {
+ const location = {
+ parents: 1,
+ interior: {X3: [
+ {
+ Parachain: 1000,
+ },
+ {
+ PalletInstance: 50,
+ },
+ {
+ GeneralIndex: 1985,
+ },
+ ]},
+ };
+ const assetId = {Concrete: location};
+
+ const foreignAssetProposal = helper.constructApiCall(
+ 'api.tx.foreignAssets.forceRegisterForeignAsset',
+ [{V3: assetId}, helper.util.str2vec('New Asset2'), 'NEW', {Fungible: 10}],
+ );
+
+ await expect(helper.technicalCommittee.collective.execute(techcomms.andy, foreignAssetProposal))
+ .to.be.rejectedWith('BadOrigin');
+ });
+
itSub('[Negative] TechComm cannot submit regular democracy proposal', async ({helper}) => {
const councilProposal = await helper.democracy.proposeCall(dummyProposalCall(helper), 0n);
js-packages/tests/sub/governance/util.tsdiffbeforeafterboth--- a/js-packages/tests/sub/governance/util.ts
+++ b/js-packages/tests/sub/governance/util.ts
@@ -29,12 +29,62 @@
filip: IKeyringPair;
irina: IKeyringPair;
}
+
+export interface IFinCounselors {
+ greg: IKeyringPair;
+ ildar: IKeyringPair;
+ andy: IKeyringPair;
+}
+
export interface ITechComms {
greg: IKeyringPair;
andy: IKeyringPair;
constantine: IKeyringPair;
}
+export function initFinCouncil(donor: IKeyringPair, superuser: IKeyringPair): Promise<IFinCounselors> {
+ return usingPlaygrounds(async (helper) => {
+ const [greg, ildar, andy] = await helper.arrange.createAccounts([10_000n, 10_000n, 10_000n], donor);
+ const sudo = helper.getSudo();
+ {
+ const members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON() as [];
+ if(members.length != 0) {
+ await clearFinCouncil(superuser);
+ }
+ }
+ const expectedMembers = [greg, ildar, andy];
+ for(const member of expectedMembers) {
+ await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.addMember', [member.address]);
+ }
+ await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.setPrime', [greg.address]);
+ {
+ const members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON();
+ expect(members).to.containSubset(expectedMembers.map((x: IKeyringPair) => x.address));
+ expect(members.length).to.be.equal(expectedMembers.length);
+ }
+
+ return {
+ greg,
+ ildar,
+ andy,
+ };
+ });
+}
+
+export async function clearFinCouncil(superuser: IKeyringPair) {
+ await usingPlaygrounds(async (helper) => {
+ let members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON();
+ if(members.length) {
+ const sudo = helper.getSudo();
+ for(const address of members) {
+ await sudo.executeExtrinsic(superuser, 'api.tx.financialCouncilMembership.removeMember', [address]);
+ }
+ members = (await helper.callRpc('api.query.financialCouncilMembership.members')).toJSON();
+ }
+ expect(members).to.be.deep.equal([]);
+ });
+}
+
export async function initCouncil(donor: IKeyringPair, superuser: IKeyringPair) {
let counselors: IKeyringPair[] = [];