git.delta.rocks / unique-network / refs/commits / f32d3e56bf10

difftreelog

Merge pull request #996 from UniqueNetwork/tests/playgrounds-refactor

Yaroslav Bolyukin2023-09-15parents: #1054e65 #477a07d.patch.diff
in: master
refactor(playgorunds): rearranging the code structure

10 files changed

modified.envrcdiffbeforeafterboth
--- a/.envrc
+++ b/.envrc
@@ -30,7 +30,7 @@
     fi
 
     echo -e "${GREEN}Baedeker env updated${RESET}"
-    nginx_id=$(docker compose -f .baedeker/.bdk-env/docker-compose.yml ps --format=json | jq -r '.[] | select(.Service == "nginx") | .ID' -e)
+    nginx_id=$(docker compose -f .baedeker/.bdk-env/docker-compose.yml ps --format=json | jq -s 'flatten' | jq -r '.[] | select(.Service == "nginx") | .ID' -e)
     if ! [ $? -eq 0 ]; then
         echo -e "${RED}Nginx container not found${RESET}"
         exit 0
modifiedtests/src/governance/util.tsdiffbeforeafterboth
--- a/tests/src/governance/util.ts
+++ b/tests/src/governance/util.ts
@@ -2,6 +2,7 @@
 import {xxhashAsHex} from '@polkadot/util-crypto';
 import {usingPlaygrounds, expect} from '../util';
 import {UniqueHelper} from '../util/playgrounds/unique';
+import {DevUniqueHelper} from '../util/playgrounds/unique.dev';
 
 export const democracyLaunchPeriod = 35;
 export const democracyVotingPeriod = 35;
@@ -203,7 +204,7 @@
   });
 }
 
-export async function voteUnanimouslyInFellowship(helper: UniqueHelper, fellows: IKeyringPair[][], minRank: number, referendumIndex: number) {
+export async function voteUnanimouslyInFellowship(helper: DevUniqueHelper, fellows: IKeyringPair[][], minRank: number, referendumIndex: number) {
   for(let rank = minRank; rank < fellowshipRankLimit; rank++) {
     for(const member of fellows[rank]) {
       await helper.fellowship.collective.vote(member, referendumIndex, true);
modifiedtests/src/maintenance.seqtest.tsdiffbeforeafterboth
--- a/tests/src/maintenance.seqtest.ts
+++ b/tests/src/maintenance.seqtest.ts
@@ -192,19 +192,22 @@
       const blocksToWait = 6;
 
       // Scheduling works before the maintenance
-      await nftBeforeMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdBeforeMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdBeforeMM})
+        .nft.transferToken(bob, collection.collectionId, nftBeforeMM.tokenId, {Substrate: superuser.address});
+
 
       await helper.wait.newBlocks(blocksToWait + 1);
       expect(await nftBeforeMM.getOwner()).to.be.deep.equal({Substrate: superuser.address});
 
       // Schedule a transaction that should occur *during* the maintenance
-      await nftDuringMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdDuringMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdDuringMM})
+        .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address});
+
 
       // Schedule a transaction that should occur *after* the maintenance
-      await nftDuringMM.scheduleAfter(blocksToWait * 2, {scheduledId: scheduledIdBunkerThroughMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait * 2, {scheduledId: scheduledIdBunkerThroughMM})
+        .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address});
+
 
       await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.enable', []);
       expect(await maintenanceEnabled(helper.getApi()), 'MM is OFF when it should be ON').to.be.true;
@@ -214,16 +217,16 @@
       expect(await nftDuringMM.getOwner()).to.be.deep.equal({Substrate: bob.address});
 
       // Any attempts to schedule a tx during the MM should be rejected
-      await expect(nftDuringMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAttemptDuringMM})
-        .transfer(bob, {Substrate: superuser.address}))
+      await expect(helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAttemptDuringMM})
+        .nft.transferToken(bob, collection.collectionId, nftDuringMM.tokenId, {Substrate: superuser.address}))
         .to.be.rejectedWith(/Invalid Transaction: Transaction call is not expected/);
 
       await helper.getSudo().executeExtrinsic(superuser, 'api.tx.maintenance.disable', []);
       expect(await maintenanceEnabled(helper.getApi()), 'MM is ON when it should be OFF').to.be.false;
 
       // Scheduling works after the maintenance
-      await nftAfterMM.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAfterMM})
-        .transfer(bob, {Substrate: superuser.address});
+      await helper.scheduler.scheduleAfter(blocksToWait, {scheduledId: scheduledIdAfterMM})
+        .nft.transferToken(bob, collection.collectionId, nftAfterMM.tokenId, {Substrate: superuser.address});
 
       await helper.wait.newBlocks(blocksToWait + 1);
 
modifiedtests/src/scheduler.seqtest.tsdiffbeforeafterboth
--- a/tests/src/scheduler.seqtest.ts
+++ b/tests/src/scheduler.seqtest.ts
@@ -47,9 +47,8 @@
     const token = await collection.mintToken(alice);
     const scheduledId = scheduleKind == 'named' ? helper.arrange.makeScheduledId() : undefined;
     const blocksBeforeExecution = 4;
-
-    await token.scheduleAfter(blocksBeforeExecution, {scheduledId})
-      .transfer(alice, {Substrate: bob.address});
+    await helper.scheduler.scheduleAfter(blocksBeforeExecution, {scheduledId})
+      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});
     const executionBlock = await helper.chain.getLatestBlockNumber() + blocksBeforeExecution + 1;
 
     expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});
@@ -103,8 +102,8 @@
 
     expect(await token.getOwner()).to.be.deep.equal({Substrate: alice.address});
 
-    await token.scheduleAfter(waitForBlocks, {scheduledId})
-      .transfer(alice, {Substrate: bob.address});
+    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})
+      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});
     const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
 
     await helper.scheduler.cancelScheduled(alice, scheduledId);
@@ -363,8 +362,8 @@
     const scheduledId = helper.arrange.makeScheduledId();
     const waitForBlocks = 4;
 
-    await token.scheduleAfter(waitForBlocks, {scheduledId})
-      .transfer(bob, {Substrate: alice.address});
+    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})
+      .nft.transferToken(bob, collection.collectionId, token.tokenId, {Substrate: alice.address});
     const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
 
     await helper.getSudo().scheduler.cancelScheduled(superuser, scheduledId);
@@ -404,8 +403,8 @@
     const scheduledId = helper.arrange.makeScheduledId();
     const waitForBlocks = 6;
 
-    await token.scheduleAfter(waitForBlocks, {scheduledId})
-      .transfer(bob, {Substrate: alice.address});
+    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})
+      .nft.transferToken(bob, collection.collectionId, token.tokenId, {Substrate: alice.address});
     const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
 
     const priority = 112;
@@ -583,8 +582,8 @@
     const scheduledId = helper.arrange.makeScheduledId();
     const waitForBlocks = 4;
 
-    await token.scheduleAfter(waitForBlocks, {scheduledId})
-      .transfer(alice, {Substrate: bob.address});
+    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})
+      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});
     const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
 
     const scheduled = helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId});
@@ -614,8 +613,8 @@
     const scheduledId = helper.arrange.makeScheduledId();
     const waitForBlocks = 4;
 
-    await token.scheduleAfter(waitForBlocks, {scheduledId})
-      .transfer(alice, {Substrate: bob.address});
+    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})
+      .nft.transferToken(alice, collection.collectionId, token.tokenId, {Substrate: bob.address});
     const executionBlock = await helper.chain.getLatestBlockNumber() + waitForBlocks + 1;
 
     await expect(helper.scheduler.cancelScheduled(bob, scheduledId))
@@ -655,8 +654,8 @@
     const scheduledId = helper.arrange.makeScheduledId();
     const waitForBlocks = 4;
 
-    await token.scheduleAfter(waitForBlocks, {scheduledId})
-      .transfer(bob, {Substrate: alice.address});
+    await helper.scheduler.scheduleAfter(waitForBlocks, {scheduledId})
+      .nft.transferToken(bob, collection.collectionId, token.tokenId, {Substrate: alice.address});
 
     const priority = 112;
     await expect(helper.scheduler.changePriority(alice, scheduledId, priority))
modifiedtests/src/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/types.ts
+++ b/tests/src/util/playgrounds/types.ts
@@ -216,43 +216,6 @@
   },
 }
 
-export interface IForeignAssetMetadata {
-  name?: number | Uint8Array,
-  symbol?: string,
-  decimals?: number,
-  minimalBalance?: bigint,
-}
-
-export interface MoonbeamAssetInfo {
-  location: any,
-  metadata: {
-    name: string,
-    symbol: string,
-    decimals: number,
-    isFrozen: boolean,
-    minimalBalance: bigint,
-  },
-  existentialDeposit: bigint,
-  isSufficient: boolean,
-  unitsPerSecond: bigint,
-  numAssetsWeightHint: number,
-}
-
-export interface AcalaAssetMetadata {
-  name: string,
-  symbol: string,
-  decimals: number,
-  minimalBalance: bigint,
-}
-
-export interface DemocracyStandardAccountVote {
-  balance: bigint,
-  vote: {
-    aye: boolean,
-    conviction: number,
-  },
-}
-
 export interface DemocracySplitAccount {
   aye: bigint,
   nay: bigint,
addedtests/src/util/playgrounds/types.xcm.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/types.xcm.ts
@@ -0,0 +1,36 @@
+export interface AcalaAssetMetadata {
+  name: string,
+  symbol: string,
+  decimals: number,
+  minimalBalance: bigint,
+}
+
+export interface MoonbeamAssetInfo {
+  location: any,
+  metadata: {
+    name: string,
+    symbol: string,
+    decimals: number,
+    isFrozen: boolean,
+    minimalBalance: bigint,
+  },
+  existentialDeposit: bigint,
+  isSufficient: boolean,
+  unitsPerSecond: bigint,
+  numAssetsWeightHint: number,
+}
+
+export interface DemocracyStandardAccountVote {
+  balance: bigint,
+  vote: {
+    aye: boolean,
+    conviction: number,
+  },
+}
+
+export interface IForeignAssetMetadata {
+  name?: number | Uint8Array,
+  symbol?: string,
+  decimals?: number,
+  minimalBalance?: bigint,
+}
\ No newline at end of file
modifiedtests/src/util/playgrounds/unique.dev.tsdiffbeforeafterboth
--- a/tests/src/util/playgrounds/unique.dev.ts
+++ b/tests/src/util/playgrounds/unique.dev.ts
@@ -3,16 +3,18 @@
 
 import {stringToU8a} from '@polkadot/util';
 import {blake2AsHex, encodeAddress, mnemonicGenerate} from '@polkadot/util-crypto';
-import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper, WestmintHelper, AstarHelper, PolkadexHelper} from './unique';
+import {UniqueHelper, ChainHelperBase, ChainHelperBaseConstructor, HelperGroup, UniqueHelperConstructor} from './unique';
 import {ApiPromise, Keyring, WsProvider} from '@polkadot/api';
 import * as defs from '../../interfaces/definitions';
 import {IKeyringPair} from '@polkadot/types/types';
 import {EventRecord} from '@polkadot/types/interfaces';
-import {ICrossAccountId, IPovInfo, ITransactionResult, TSigner} from './types';
+import {ICrossAccountId, ILogger, IPovInfo, ISchedulerOptions, ITransactionResult, TSigner} from './types';
 import {FrameSystemEventRecord, XcmV2TraitsError} from '@polkadot/types/lookup';
-import {VoidFn} from '@polkadot/api/types';
+import {SignerOptions, VoidFn} from '@polkadot/api/types';
 import {Pallets} from '..';
 import {spawnSync} from 'child_process';
+import {AcalaHelper, AstarHelper, MoonbeamHelper, PolkadexHelper, RelayHelper, WestmintHelper, ForeignAssetsGroup, XcmGroup, XTokensGroup, TokensGroup} from './unique.xcm';
+import {CollectiveGroup, CollectiveMembershipGroup, DemocracyGroup, ICollectiveGroup, IFellowshipGroup, RankedCollectiveGroup, ReferendaGroup} from './unique.governance';
 
 export class SilentLogger {
   log(_msg: any, _level: any): void { }
@@ -260,6 +262,192 @@
   };
 }
 
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {
+  return class extends Base {
+    constructor(...args: any[]) {
+      super(...args);
+    }
+
+    async executeExtrinsic(
+      sender: IKeyringPair,
+      extrinsic: string,
+      params: any[],
+      expectSuccess?: boolean,
+      options: Partial<SignerOptions> | null = null,
+    ): Promise<ITransactionResult> {
+      const call = this.constructApiCall(extrinsic, params);
+      const result = await super.executeExtrinsic(
+        sender,
+        'api.tx.sudo.sudo',
+        [call],
+        expectSuccess,
+        options,
+      );
+
+      if(result.status === 'Fail') return result;
+
+      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
+      if(data.isErr) {
+        if(data.asErr.isModule) {
+          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
+          const metaError = super.getApi()?.registry.findMetaError(error);
+          throw new Error(`${metaError.section}.${metaError.name}`);
+        } else if(data.asErr.isToken) {
+          throw new Error(`Token: ${data.asErr.asToken}`);
+        }
+        // May be [object Object] in case of unhandled non-unit enum
+        throw new Error(`Misc: ${data.asErr.toHuman()}`);
+      }
+      return result;
+    }
+    async executeExtrinsicUncheckedWeight(
+      sender: IKeyringPair,
+      extrinsic: string,
+      params: any[],
+      expectSuccess?: boolean,
+      options: Partial<SignerOptions> | null = null,
+    ): Promise<ITransactionResult> {
+      const call = this.constructApiCall(extrinsic, params);
+      const result = await super.executeExtrinsic(
+        sender,
+        'api.tx.sudo.sudoUncheckedWeight',
+        [call, {refTime: 0, proofSize: 0}],
+        expectSuccess,
+        options,
+      );
+
+      if(result.status === 'Fail') return result;
+
+      const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
+      if(data.isErr) {
+        if(data.asErr.isModule) {
+          const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
+          const metaError = super.getApi()?.registry.findMetaError(error);
+          throw new Error(`${metaError.section}.${metaError.name}`);
+        } else if(data.asErr.isToken) {
+          throw new Error(`Token: ${data.asErr.asToken}`);
+        }
+        // May be [object Object] in case of unhandled non-unit enum
+        throw new Error(`Misc: ${data.asErr.toHuman()}`);
+      }
+      return result;
+    }
+  };
+}
+
+class SchedulerGroup extends HelperGroup<UniqueHelper> {
+  constructor(helper: UniqueHelper) {
+    super(helper);
+  }
+
+  cancelScheduled(signer: TSigner, scheduledId: string) {
+    return this.helper.executeExtrinsic(
+      signer,
+      'api.tx.scheduler.cancelNamed',
+      [scheduledId],
+      true,
+    );
+  }
+
+  changePriority(signer: TSigner, scheduledId: string, priority: number) {
+    return this.helper.executeExtrinsic(
+      signer,
+      'api.tx.scheduler.changeNamedPriority',
+      [scheduledId, priority],
+      true,
+    );
+  }
+
+  scheduleAt<T extends DevUniqueHelper>(
+    executionBlockNumber: number,
+    options: ISchedulerOptions = {},
+  ) {
+    return this.schedule<T>('schedule', executionBlockNumber, options);
+  }
+
+  scheduleAfter<T extends DevUniqueHelper>(
+    blocksBeforeExecution: number,
+    options: ISchedulerOptions = {},
+  ) {
+    return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);
+  }
+
+  schedule<T extends UniqueHelper>(
+    scheduleFn: 'schedule' | 'scheduleAfter',
+    blocksNum: number,
+    options: ISchedulerOptions = {},
+  ) {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);
+    return this.helper.clone(ScheduledHelperType, {
+      scheduleFn,
+      blocksNum,
+      options,
+    }) as T;
+  }
+}
+
+class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {
+  //todo:collator documentation
+  addInvulnerable(signer: TSigner, address: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);
+  }
+
+  removeInvulnerable(signer: TSigner, address: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);
+  }
+
+  async getInvulnerables(): Promise<string[]> {
+    return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());
+  }
+
+  /** and also total max invulnerables */
+  maxCollators(): number {
+    return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);
+  }
+
+  async getDesiredCollators(): Promise<number> {
+    return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();
+  }
+
+  setLicenseBond(signer: TSigner, amount: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);
+  }
+
+  async getLicenseBond(): Promise<bigint> {
+    return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();
+  }
+
+  obtainLicense(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);
+  }
+
+  releaseLicense(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);
+  }
+
+  forceReleaseLicense(signer: TSigner, released: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);
+  }
+
+  async hasLicense(address: string): Promise<bigint> {
+    return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();
+  }
+
+  onboard(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);
+  }
+
+  offboard(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);
+  }
+
+  async getCandidates(): Promise<string[]> {
+    return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());
+  }
+}
+
 export class DevUniqueHelper extends UniqueHelper {
   /**
    * Arrange methods for tests
@@ -269,6 +457,16 @@
   admin: AdminGroup;
   session: SessionGroup;
   testUtils: TestUtilGroup;
+  foreignAssets: ForeignAssetsGroup;
+  xcm: XcmGroup<UniqueHelper>;
+  xTokens: XTokensGroup<UniqueHelper>;
+  tokens: TokensGroup<UniqueHelper>;
+  scheduler: SchedulerGroup;
+  collatorSelection: CollatorSelectionGroup;
+  council: ICollectiveGroup;
+  technicalCommittee: ICollectiveGroup;
+  fellowship: IFellowshipGroup;
+  democracy: DemocracyGroup;
 
   constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
     options.helperBase = options.helperBase ?? DevUniqueHelper;
@@ -279,6 +477,25 @@
     this.admin = new AdminGroup(this);
     this.testUtils = new TestUtilGroup(this);
     this.session = new SessionGroup(this);
+    this.foreignAssets = new ForeignAssetsGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+    this.xTokens = new XTokensGroup(this);
+    this.tokens = new TokensGroup(this);
+    this.scheduler = new SchedulerGroup(this);
+    this.collatorSelection = new CollatorSelectionGroup(this);
+    this.council = {
+      collective: new CollectiveGroup(this, 'council'),
+      membership: new CollectiveMembershipGroup(this, 'councilMembership'),
+    };
+    this.technicalCommittee = {
+      collective: new CollectiveGroup(this, 'technicalCommittee'),
+      membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),
+    };
+    this.fellowship = {
+      collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),
+      referenda: new ReferendaGroup(this, 'fellowshipReferenda'),
+    };
+    this.democracy = new DemocracyGroup(this);
   }
 
   async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
@@ -326,6 +543,11 @@
     this.network = await UniqueHelper.detectNetwork(this.api);
     this.wsEndpoint = wsEndpoint;
   }
+  getSudo<T extends DevUniqueHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
+  }
 }
 
 export class DevRelayHelper extends RelayHelper {
@@ -386,19 +608,16 @@
     super(logger, options);
     this.wait = new WaitGroup(this);
   }
-}
-
-export class DevShidenHelper extends AstarHelper {
-  wait: WaitGroup;
 
-  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {
-    options.helperBase = options.helperBase ?? DevShidenHelper;
-
-    super(logger, options);
-    this.wait = new WaitGroup(this);
+  getSudo<T extends AstarHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
   }
 }
 
+export class DevShidenHelper extends DevAstarHelper { }
+
 export class DevAcalaHelper extends AcalaHelper {
   wait: WaitGroup;
 
@@ -408,6 +627,11 @@
     super(logger, options);
     this.wait = new WaitGroup(this);
   }
+  getSudo<T extends AcalaHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
+  }
 }
 
 export class DevPolkadexHelper extends PolkadexHelper {
@@ -418,6 +642,12 @@
     super(logger, options);
     this.wait = new WaitGroup(this);
   }
+
+  getSudo<T extends PolkadexHelper>() {
+    // eslint-disable-next-line @typescript-eslint/naming-convention
+    const SudoHelperType = SudoHelper(this.helperBase);
+    return this.clone(SudoHelperType) as T;
+  }
 }
 
 export class DevKaruraHelper extends DevAcalaHelper {}
@@ -1211,3 +1441,63 @@
     }));
   }
 }
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {
+  return class extends Base {
+    scheduleFn: 'schedule' | 'scheduleAfter';
+    blocksNum: number;
+    options: ISchedulerOptions;
+
+    constructor(...args: any[]) {
+      const logger = args[0] as ILogger;
+      const options = args[1] as {
+        scheduleFn: 'schedule' | 'scheduleAfter',
+        blocksNum: number,
+        options: ISchedulerOptions
+      };
+
+      super(logger);
+
+      this.scheduleFn = options.scheduleFn;
+      this.blocksNum = options.blocksNum;
+      this.options = options.options;
+    }
+
+    executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {
+      const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);
+
+      const mandatorySchedArgs = [
+        this.blocksNum,
+        this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,
+        this.options.priority ?? null,
+        scheduledTx,
+      ];
+
+      let schedArgs;
+      let scheduleFn;
+
+      if(this.options.scheduledId) {
+        schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];
+
+        if(this.scheduleFn == 'schedule') {
+          scheduleFn = 'scheduleNamed';
+        } else if(this.scheduleFn == 'scheduleAfter') {
+          scheduleFn = 'scheduleNamedAfter';
+        }
+      } else {
+        schedArgs = mandatorySchedArgs;
+        scheduleFn = this.scheduleFn;
+      }
+
+      const extrinsic = 'api.tx.scheduler.' + scheduleFn;
+
+      return super.executeExtrinsic(
+        sender,
+        extrinsic as any,
+        schedArgs,
+        expectSuccess,
+      );
+    }
+  };
+}
\ No newline at end of file
addedtests/src/util/playgrounds/unique.governance.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/unique.governance.ts
@@ -0,0 +1,531 @@
+import {blake2AsHex} from '@polkadot/util-crypto';
+import {PalletDemocracyConviction} from '@polkadot/types/lookup';
+import {IPhasicEvent, TSigner} from './types';
+import {HelperGroup, UniqueHelper} from './unique';
+
+export class CollectiveGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'council', 'technicalCommittee'
+   */
+  private collective: string;
+
+  constructor(helper: UniqueHelper, collective: string) {
+    super(helper);
+    this.collective = collective;
+  }
+
+  /**
+   * Check the result of a proposal execution for the success of the underlying proposed extrinsic.
+   * @param events events of the proposal execution
+   * @returns proposal hash
+   */
+  private checkExecutedEvent(events: IPhasicEvent[]): string {
+    const executionEvents = events.filter(x =>
+      x.event.section === this.collective && (x.event.method === 'Executed' || x.event.method === 'MemberExecuted'));
+
+    if(executionEvents.length != 1) {
+      if(events.filter(x => x.event.section === this.collective && x.event.method === 'Disapproved').length > 0)
+        throw new Error(`Disapproved by ${this.collective}`);
+      else
+        throw new Error(`Expected one 'Executed' or 'MemberExecuted' event for ${this.collective}`);
+    }
+
+    const result = (executionEvents[0].event.data as any).result;
+
+    if(result.isErr) {
+      if(result.asErr.isModule) {
+        const error = result.asErr.asModule;
+        const metaError = this.helper.getApi()?.registry.findMetaError(error);
+        throw new Error(`Proposal execution failed with ${metaError.section}.${metaError.name}`);
+      } else {
+        throw new Error('Proposal execution failed with ' + result.asErr.toHuman());
+      }
+    }
+
+    return (executionEvents[0].event.data as any).proposalHash;
+  }
+
+  /**
+   * Returns an array of members' addresses.
+   */
+  async getMembers() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.members`, [])).toHuman();
+  }
+
+  /**
+   * Returns the optional address of the prime member of the collective.
+   */
+  async getPrimeMember() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.prime`, [])).toHuman();
+  }
+
+  /**
+   * Returns an array of proposal hashes that are currently active for this collective.
+   */
+  async getProposals() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.proposals`, [])).toHuman();
+  }
+
+  /**
+   * Returns the call originally encoded under the specified hash.
+   * @param hash h256-encoded proposal
+   * @returns the optional call that the proposal hash stands for.
+   */
+  async getProposalCallOf(hash: string) {
+    return (await this.helper.callRpc(`api.query.${this.collective}.proposalOf`, [hash])).toHuman();
+  }
+
+  /**
+   * Returns the total number of proposals so far.
+   */
+  async getTotalProposalsCount() {
+    return (await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, [])).toNumber();
+  }
+
+  /**
+   * Creates a new proposal up for voting. If the threshold is set to 1, the proposal will be executed immediately.
+   * @param signer keyring of the proposer
+   * @param proposal constructed call to be executed if the proposal is successful
+   * @param voteThreshold minimal number of votes for the proposal to be verified and executed
+   * @param lengthBound byte length of the encoded call
+   * @returns promise of extrinsic execution and its result
+   */
+  async propose(signer: TSigner, proposal: any, voteThreshold: number, lengthBound = 10000) {
+    return await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [voteThreshold, proposal, lengthBound]);
+  }
+
+  /**
+   * Casts a vote to either approve or reject a proposal.
+   * @param signer keyring of the voter
+   * @param proposalHash hash of the proposal to be voted for
+   * @param proposalIndex absolute index of the proposal used for absolutely nothing but throwing pointless errors
+   * @param approve aye or nay
+   * @returns promise of extrinsic execution and its result
+   */
+  vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve]);
+  }
+
+  /**
+   * Executes a call immediately as a member of the collective. Needed for the Member origin.
+   * @param signer keyring of the executor member
+   * @param proposal constructed call to be executed by the member
+   * @param lengthBound byte length of the encoded call
+   * @returns promise of extrinsic execution
+   */
+  async execute(signer: TSigner, proposal: any, lengthBound = 10000) {
+    const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.execute`, [proposal, lengthBound]);
+    this.checkExecutedEvent(result.result.events);
+    return result;
+  }
+
+  /**
+   * Attempt to close and execute a proposal. Note that there must already be enough votes to meet the threshold set when proposing.
+   * @param signer keyring of the executor. Can be absolutely anyone.
+   * @param proposalHash hash of the proposal to close
+   * @param proposalIndex index of the proposal generated on its creation
+   * @param weightBound weight of the proposed call. Can be obtained by calling `paymentInfo()` on the call.
+   * @param lengthBound byte length of the encoded call
+   * @returns promise of extrinsic execution and its result
+   */
+  async close(
+    signer: TSigner,
+    proposalHash: string,
+    proposalIndex: number,
+    weightBound: [number, number] | any = [20_000_000_000, 1000_000],
+    lengthBound = 10_000,
+  ) {
+    const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [
+      proposalHash,
+      proposalIndex,
+      weightBound,
+      lengthBound,
+    ]);
+    this.checkExecutedEvent(result.result.events);
+    return result;
+  }
+
+  /**
+   * Shut down a proposal, regardless of its current state.
+   * @param signer keyring of the disapprover. Must be root
+   * @param proposalHash hash of the proposal to close
+   * @returns promise of extrinsic execution and its result
+   */
+  disapproveProposal(signer: TSigner, proposalHash: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.disapproveProposal`, [proposalHash]);
+  }
+}
+
+export class CollectiveMembershipGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'councilMembership', 'technicalCommitteeMembership'
+   */
+  private membership: string;
+
+  constructor(helper: UniqueHelper, membership: string) {
+    super(helper);
+    this.membership = membership;
+  }
+
+  /**
+   * Returns an array of members' addresses according to the membership pallet's perception.
+   * Note that it does not recognize the original pallet's members set with `setMembers()`.
+   */
+  async getMembers() {
+    return (await this.helper.callRpc(`api.query.${this.membership}.members`, [])).toHuman();
+  }
+
+  /**
+   * Returns the optional address of the prime member of the collective.
+   */
+  async getPrimeMember() {
+    return (await this.helper.callRpc(`api.query.${this.membership}.prime`, [])).toHuman();
+  }
+
+  /**
+   * Add a member to the collective.
+   * @param signer keyring of the setter. Must be root
+   * @param member address of the member to add
+   * @returns promise of extrinsic execution and its result
+   */
+  addMember(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.addMember`, [member]);
+  }
+
+  addMemberCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.addMember`, [member]);
+  }
+
+  /**
+   * Remove a member from the collective.
+   * @param signer keyring of the setter. Must be root
+   * @param member address of the member to remove
+   * @returns promise of extrinsic execution and its result
+   */
+  removeMember(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.removeMember`, [member]);
+  }
+
+  removeMemberCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.removeMember`, [member]);
+  }
+
+  /**
+   * Set members of the collective to the given list of addresses.
+   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
+   * @param members addresses of the members to set
+   * @returns promise of extrinsic execution and its result
+   */
+  resetMembers(signer: TSigner, members: string[]) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.resetMembers`, [members]);
+  }
+
+  /**
+   * Set the collective's prime member to the given address.
+   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
+   * @param prime address of the prime member of the collective
+   * @returns promise of extrinsic execution and its result
+   */
+  setPrime(signer: TSigner, prime: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.setPrime`, [prime]);
+  }
+
+  setPrimeCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.setPrime`, [member]);
+  }
+
+  /**
+   * Remove the collective's prime member.
+   * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
+   * @returns promise of extrinsic execution and its result
+   */
+  clearPrime(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.clearPrime`, []);
+  }
+
+  clearPrimeCall() {
+    return this.helper.constructApiCall(`api.tx.${this.membership}.clearPrime`, []);
+  }
+}
+
+export class RankedCollectiveGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'FellowshipCollective'
+   */
+  private collective: string;
+
+  constructor(helper: UniqueHelper, collective: string) {
+    super(helper);
+    this.collective = collective;
+  }
+
+  addMember(signer: TSigner, newMember: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.addMember`, [newMember]);
+  }
+
+  addMemberCall(newMember: string) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.addMember`, [newMember]);
+  }
+
+  removeMember(signer: TSigner, member: string, minRank: number) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.removeMember`, [member, minRank]);
+  }
+
+  removeMemberCall(newMember: string, minRank: number) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.removeMember`, [newMember, minRank]);
+  }
+
+  promote(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.promoteMember`, [member]);
+  }
+
+  promoteCall(member: string) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.promoteMember`, [member]);
+  }
+
+  demote(signer: TSigner, member: string) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.demoteMember`, [member]);
+  }
+
+  demoteCall(newMember: string) {
+    return this.helper.constructApiCall(`api.tx.${this.collective}.demoteMember`, [newMember]);
+  }
+
+  vote(signer: TSigner, pollIndex: number, aye: boolean) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [pollIndex, aye]);
+  }
+
+  async getMembers() {
+    return (await this.helper.getApi().query.fellowshipCollective.members.keys())
+      .map((key) => key.args[0].toString());
+  }
+
+  async getMemberRank(member: string) {
+    return (await this.helper.callRpc('api.query.fellowshipCollective.members', [member])).toJSON().rank;
+  }
+}
+
+export class ReferendaGroup extends HelperGroup<UniqueHelper> {
+  /**
+   * Pallet name to make an API call to. Examples: 'FellowshipReferenda'
+   */
+  private referenda: string;
+
+  constructor(helper: UniqueHelper, referenda: string) {
+    super(helper);
+    this.referenda = referenda;
+  }
+
+  submit(
+    signer: TSigner,
+    proposalOrigin: string,
+    proposal: any,
+    enactmentMoment: any,
+  ) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.submit`, [
+      {Origins: proposalOrigin},
+      proposal,
+      enactmentMoment,
+    ]);
+  }
+
+  placeDecisionDeposit(signer: TSigner, referendumIndex: number) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.placeDecisionDeposit`, [referendumIndex]);
+  }
+
+  cancel(signer: TSigner, referendumIndex: number) {
+    return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.cancel`, [referendumIndex]);
+  }
+
+  cancelCall(referendumIndex: number) {
+    return this.helper.constructApiCall(`api.tx.${this.referenda}.cancel`, [referendumIndex]);
+  }
+
+  async referendumInfo(referendumIndex: number) {
+    return (await this.helper.callRpc(`api.query.${this.referenda}.referendumInfoFor`, [referendumIndex])).toJSON();
+  }
+
+  async enactmentEventId(referendumIndex: number) {
+    const api = await this.helper.getApi();
+
+    const bytes = api.createType('([u8;8], Text, u32)', ['assembly', 'enactment', referendumIndex]).toU8a();
+    return blake2AsHex(bytes, 256);
+  }
+}
+
+export interface IFellowshipGroup {
+  collective: RankedCollectiveGroup;
+  referenda: ReferendaGroup;
+}
+
+export interface ICollectiveGroup {
+  collective: CollectiveGroup;
+  membership: CollectiveMembershipGroup;
+}
+
+export class DemocracyGroup extends HelperGroup<UniqueHelper> {
+  // todo displace proposal into types?
+  propose(signer: TSigner, call: any, deposit: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
+  }
+
+  proposeWithPreimage(signer: TSigner, preimage: string, deposit: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Legacy: preimage}, deposit]);
+  }
+
+  proposeCall(call: any, deposit: bigint) {
+    return this.helper.constructApiCall('api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
+  }
+
+  second(signer: TSigner, proposalIndex: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.second', [proposalIndex]);
+  }
+
+  externalPropose(signer: TSigner, proposalCall: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeMajority(signer: TSigner, proposalCall: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefault(signer: TSigner, proposalCall: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefaultWithPreimage(signer: TSigner, preimage: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
+  }
+
+  externalProposeCall(proposalCall: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeMajorityCall(proposalCall: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefaultCall(proposalCall: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
+  }
+
+  externalProposeDefaultWithPreimageCall(preimage: string) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
+  }
+
+  // ... and blacklist external proposal hash.
+  vetoExternal(signer: TSigner, proposalHash: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vetoExternal', [proposalHash]);
+  }
+
+  vetoExternalCall(proposalHash: string) {
+    return this.helper.constructApiCall('api.tx.democracy.vetoExternal', [proposalHash]);
+  }
+
+  blacklist(signer: TSigner, proposalHash: string, referendumIndex: number | null = null) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
+  }
+
+  blacklistCall(proposalHash: string, referendumIndex: number | null = null) {
+    return this.helper.constructApiCall('api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
+  }
+
+  // proposal. CancelProposalOrigin (root or all techcom)
+  cancelProposal(signer: TSigner, proposalIndex: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.cancelProposal', [proposalIndex]);
+  }
+
+  cancelProposalCall(proposalIndex: number) {
+    return this.helper.constructApiCall('api.tx.democracy.cancelProposal', [proposalIndex]);
+  }
+
+  clearPublicProposals(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.clearPublicProposals', []);
+  }
+
+  fastTrack(signer: TSigner, proposalHash: string, votingPeriod: number, delayPeriod: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
+  }
+
+  fastTrackCall(proposalHash: string, votingPeriod: number, delayPeriod: number) {
+    return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
+  }
+
+  // referendum. CancellationOrigin (TechCom member)
+  emergencyCancel(signer: TSigner, referendumIndex: number) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.emergencyCancel', [referendumIndex]);
+  }
+
+  emergencyCancelCall(referendumIndex: number) {
+    return this.helper.constructApiCall('api.tx.democracy.emergencyCancel', [referendumIndex]);
+  }
+
+  vote(signer: TSigner, referendumIndex: number, vote: any) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, vote]);
+  }
+
+  removeVote(signer: TSigner, referendumIndex: number, targetAccount?: string) {
+    if(targetAccount) {
+      return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeOtherVote', [targetAccount, referendumIndex]);
+    } else {
+      return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeVote', [referendumIndex]);
+    }
+  }
+
+  unlock(signer: TSigner, targetAccount: string) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.unlock', [targetAccount]);
+  }
+
+  delegate(signer: TSigner, toAccount: string, conviction: PalletDemocracyConviction, balance: bigint) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.delegate', [toAccount, conviction, balance]);
+  }
+
+  undelegate(signer: TSigner) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.undelegate', []);
+  }
+
+  async referendumInfo(referendumIndex: number) {
+    return (await this.helper.callRpc('api.query.democracy.referendumInfoOf', [referendumIndex])).toJSON();
+  }
+
+  async publicProposals() {
+    return (await this.helper.callRpc('api.query.democracy.publicProps', [])).toJSON();
+  }
+
+  async findPublicProposal(proposalIndex: number) {
+    const proposalInfo = (await this.publicProposals()).find((proposalInfo: any[]) => proposalInfo[0] == proposalIndex);
+
+    return proposalInfo ? proposalInfo[1] : null;
+  }
+
+  async expectPublicProposal(proposalIndex: number) {
+    const proposal = await this.findPublicProposal(proposalIndex);
+
+    if(proposal) {
+      return proposal;
+    } else {
+      throw Error(`Proposal #${proposalIndex} is expected to exist`);
+    }
+  }
+
+  async getExternalProposal() {
+    return (await this.helper.callRpc('api.query.democracy.nextExternal', []));
+  }
+
+  async expectExternalProposal() {
+    const proposal = await this.getExternalProposal();
+
+    if(proposal) {
+      return proposal;
+    } else {
+      throw Error('An external proposal is expected to exist');
+    }
+  }
+
+  /* setMetadata? */
+
+  /* todo?
+  referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
+    return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
+  }*/
+}
modifiedtests/src/util/playgrounds/unique.tsdiffbeforeafterboth
39 TSigner,39 TSigner,
40 TSubstrateAccount,40 TSubstrateAccount,
41 TNetworks,41 TNetworks,
42 IForeignAssetMetadata,
43 AcalaAssetMetadata,
44 MoonbeamAssetInfo,
45 DemocracyStandardAccountVote,
46 IEthCrossAccountId,42 IEthCrossAccountId,
47 IPhasicEvent,
48} from './types';43} from './types';
49import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';44import {RuntimeDispatchInfo} from '@polkadot/types/interfaces';
50import type {Vec} from '@polkadot/types-codec';45import type {Vec} from '@polkadot/types-codec';
51import {FrameSystemEventRecord, PalletDemocracyConviction} from '@polkadot/types/lookup';46import {FrameSystemEventRecord} from '@polkadot/types/lookup';
5247
53export class CrossAccountId {48export class CrossAccountId {
54 Substrate!: TSubstrateAccount;49 Substrate!: TSubstrateAccount;
818}813}
819814
820815
821class HelperGroup<T extends ChainHelperBase> {816export class HelperGroup<T extends ChainHelperBase> {
822 helper: T;817 helper: T;
823818
824 constructor(uniqueHelper: T) {819 constructor(uniqueHelper: T) {
2373 }2368 }
2374}2369}
23752370
2376class SubstrateBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {2371export class SubstrateBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {
2377 /**2372 /**
2378 * Get substrate address balance2373 * Get substrate address balance
2379 * @param address substrate address2374 * @param address substrate address
2440 }2435 }
2441}2436}
24422437
2443class EthereumBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {2438export class EthereumBalanceGroup<T extends ChainHelperBase> extends HelperGroup<T> {
2444 /**2439 /**
2445 * Get ethereum address balance2440 * Get ethereum address balance
2446 * @param address ethereum address2441 * @param address ethereum address
2867 }2863 }
2868}2864}
28692865
2870class SchedulerGroup extends HelperGroup<UniqueHelper> {
2871 constructor(helper: UniqueHelper) {
2872 super(helper);
2873 }
2874
2875 cancelScheduled(signer: TSigner, scheduledId: string) {
2876 return this.helper.executeExtrinsic(
2877 signer,
2878 'api.tx.scheduler.cancelNamed',
2879 [scheduledId],
2880 true,
2881 );
2882 }
2883
2884 changePriority(signer: TSigner, scheduledId: string, priority: number) {
2885 return this.helper.executeExtrinsic(
2886 signer,
2887 'api.tx.scheduler.changeNamedPriority',
2888 [scheduledId, priority],
2889 true,
2890 );
2891 }
2892
2893 scheduleAt<T extends UniqueHelper>(
2894 executionBlockNumber: number,
2895 options: ISchedulerOptions = {},
2896 ) {
2897 return this.schedule<T>('schedule', executionBlockNumber, options);
2898 }
2899
2900 scheduleAfter<T extends UniqueHelper>(
2901 blocksBeforeExecution: number,
2902 options: ISchedulerOptions = {},
2903 ) {
2904 return this.schedule<T>('scheduleAfter', blocksBeforeExecution, options);
2905 }
2906
2907 schedule<T extends UniqueHelper>(
2908 scheduleFn: 'schedule' | 'scheduleAfter',
2909 blocksNum: number,
2910 options: ISchedulerOptions = {},
2911 ) {
2912 // eslint-disable-next-line @typescript-eslint/naming-convention
2913 const ScheduledHelperType = ScheduledUniqueHelper(this.helper.helperBase);
2914 return this.helper.clone(ScheduledHelperType, {
2915 scheduleFn,
2916 blocksNum,
2917 options,
2918 }) as T;
2919 }
2920}
2921
2922class CollatorSelectionGroup extends HelperGroup<UniqueHelper> {
2923 //todo:collator documentation
2924 addInvulnerable(signer: TSigner, address: string) {
2925 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.addInvulnerable', [address]);
2926 }
2927
2928 removeInvulnerable(signer: TSigner, address: string) {
2929 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.removeInvulnerable', [address]);
2930 }
2931
2932 async getInvulnerables(): Promise<string[]> {
2933 return (await this.helper.callRpc('api.query.collatorSelection.invulnerables')).map((x: any) => x.toHuman());
2934 }
2935
2936 /** and also total max invulnerables */
2937 maxCollators(): number {
2938 return (this.helper.getApi().consts.configuration.defaultCollatorSelectionMaxCollators.toJSON() as number);
2939 }
2940
2941 async getDesiredCollators(): Promise<number> {
2942 return (await this.helper.callRpc('api.query.configuration.collatorSelectionDesiredCollatorsOverride')).toNumber();
2943 }
2944
2945 setLicenseBond(signer: TSigner, amount: bigint) {
2946 return this.helper.executeExtrinsic(signer, 'api.tx.configuration.setCollatorSelectionLicenseBond', [amount]);
2947 }
2948
2949 async getLicenseBond(): Promise<bigint> {
2950 return (await this.helper.callRpc('api.query.configuration.collatorSelectionLicenseBondOverride')).toBigInt();
2951 }
2952
2953 obtainLicense(signer: TSigner) {
2954 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.getLicense', []);
2955 }
2956
2957 releaseLicense(signer: TSigner) {
2958 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.releaseLicense', []);
2959 }
2960
2961 forceReleaseLicense(signer: TSigner, released: string) {
2962 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.forceReleaseLicense', [released]);
2963 }
2964
2965 async hasLicense(address: string): Promise<bigint> {
2966 return (await this.helper.callRpc('api.query.collatorSelection.licenseDepositOf', [address])).toBigInt();
2967 }
2968
2969 onboard(signer: TSigner) {
2970 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.onboard', []);
2971 }
2972
2973 offboard(signer: TSigner) {
2974 return this.helper.executeExtrinsic(signer, 'api.tx.collatorSelection.offboard', []);
2975 }
2976
2977 async getCandidates(): Promise<string[]> {
2978 return (await this.helper.callRpc('api.query.collatorSelection.candidates')).map((x: any) => x.toHuman());
2979 }
2980}
2981
2982class CollectiveGroup extends HelperGroup<UniqueHelper> {
2983 /**
2984 * Pallet name to make an API call to. Examples: 'council', 'technicalCommittee'
2985 */
2986 private collective: string;
2987
2988 constructor(helper: UniqueHelper, collective: string) {
2989 super(helper);
2990 this.collective = collective;
2991 }
2992
2993 /**
2994 * Check the result of a proposal execution for the success of the underlying proposed extrinsic.
2995 * @param events events of the proposal execution
2996 * @returns proposal hash
2997 */
2998 private checkExecutedEvent(events: IPhasicEvent[]): string {
2999 const executionEvents = events.filter(x =>
3000 x.event.section === this.collective && (x.event.method === 'Executed' || x.event.method === 'MemberExecuted'));
3001
3002 if(executionEvents.length != 1) {
3003 if(events.filter(x => x.event.section === this.collective && x.event.method === 'Disapproved').length > 0)
3004 throw new Error(`Disapproved by ${this.collective}`);
3005 else
3006 throw new Error(`Expected one 'Executed' or 'MemberExecuted' event for ${this.collective}`);
3007 }
3008
3009 const result = (executionEvents[0].event.data as any).result;
3010
3011 if(result.isErr) {
3012 if(result.asErr.isModule) {
3013 const error = result.asErr.asModule;
3014 const metaError = this.helper.getApi()?.registry.findMetaError(error);
3015 throw new Error(`Proposal execution failed with ${metaError.section}.${metaError.name}`);
3016 } else {
3017 throw new Error('Proposal execution failed with ' + result.asErr.toHuman());
3018 }
3019 }
3020
3021 return (executionEvents[0].event.data as any).proposalHash;
3022 }
3023
3024 /**
3025 * Returns an array of members' addresses.
3026 */
3027 async getMembers() {
3028 return (await this.helper.callRpc(`api.query.${this.collective}.members`, [])).toHuman();
3029 }
3030
3031 /**
3032 * Returns the optional address of the prime member of the collective.
3033 */
3034 async getPrimeMember() {
3035 return (await this.helper.callRpc(`api.query.${this.collective}.prime`, [])).toHuman();
3036 }
3037
3038 /**
3039 * Returns an array of proposal hashes that are currently active for this collective.
3040 */
3041 async getProposals() {
3042 return (await this.helper.callRpc(`api.query.${this.collective}.proposals`, [])).toHuman();
3043 }
3044
3045 /**
3046 * Returns the call originally encoded under the specified hash.
3047 * @param hash h256-encoded proposal
3048 * @returns the optional call that the proposal hash stands for.
3049 */
3050 async getProposalCallOf(hash: string) {
3051 return (await this.helper.callRpc(`api.query.${this.collective}.proposalOf`, [hash])).toHuman();
3052 }
3053
3054 /**
3055 * Returns the total number of proposals so far.
3056 */
3057 async getTotalProposalsCount() {
3058 return (await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, [])).toNumber();
3059 }
3060
3061 /**
3062 * Creates a new proposal up for voting. If the threshold is set to 1, the proposal will be executed immediately.
3063 * @param signer keyring of the proposer
3064 * @param proposal constructed call to be executed if the proposal is successful
3065 * @param voteThreshold minimal number of votes for the proposal to be verified and executed
3066 * @param lengthBound byte length of the encoded call
3067 * @returns promise of extrinsic execution and its result
3068 */
3069 async propose(signer: TSigner, proposal: any, voteThreshold: number, lengthBound = 10000) {
3070 return await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [voteThreshold, proposal, lengthBound]);
3071 }
3072
3073 /**
3074 * Casts a vote to either approve or reject a proposal.
3075 * @param signer keyring of the voter
3076 * @param proposalHash hash of the proposal to be voted for
3077 * @param proposalIndex absolute index of the proposal used for absolutely nothing but throwing pointless errors
3078 * @param approve aye or nay
3079 * @returns promise of extrinsic execution and its result
3080 */
3081 vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
3082 return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve]);
3083 }
3084
3085 /**
3086 * Executes a call immediately as a member of the collective. Needed for the Member origin.
3087 * @param signer keyring of the executor member
3088 * @param proposal constructed call to be executed by the member
3089 * @param lengthBound byte length of the encoded call
3090 * @returns promise of extrinsic execution
3091 */
3092 async execute(signer: TSigner, proposal: any, lengthBound = 10000) {
3093 const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.execute`, [proposal, lengthBound]);
3094 this.checkExecutedEvent(result.result.events);
3095 return result;
3096 }
3097
3098 /**
3099 * Attempt to close and execute a proposal. Note that there must already be enough votes to meet the threshold set when proposing.
3100 * @param signer keyring of the executor. Can be absolutely anyone.
3101 * @param proposalHash hash of the proposal to close
3102 * @param proposalIndex index of the proposal generated on its creation
3103 * @param weightBound weight of the proposed call. Can be obtained by calling `paymentInfo()` on the call.
3104 * @param lengthBound byte length of the encoded call
3105 * @returns promise of extrinsic execution and its result
3106 */
3107 async close(
3108 signer: TSigner,
3109 proposalHash: string,
3110 proposalIndex: number,
3111 weightBound: [number, number] | any = [20_000_000_000, 1000_000],
3112 lengthBound = 10_000,
3113 ) {
3114 const result = await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [
3115 proposalHash,
3116 proposalIndex,
3117 weightBound,
3118 lengthBound,
3119 ]);
3120 this.checkExecutedEvent(result.result.events);
3121 return result;
3122 }
3123
3124 /**
3125 * Shut down a proposal, regardless of its current state.
3126 * @param signer keyring of the disapprover. Must be root
3127 * @param proposalHash hash of the proposal to close
3128 * @returns promise of extrinsic execution and its result
3129 */
3130 disapproveProposal(signer: TSigner, proposalHash: string) {
3131 return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.disapproveProposal`, [proposalHash]);
3132 }
3133}
3134
3135class CollectiveMembershipGroup extends HelperGroup<UniqueHelper> {
3136 /**
3137 * Pallet name to make an API call to. Examples: 'councilMembership', 'technicalCommitteeMembership'
3138 */
3139 private membership: string;
3140
3141 constructor(helper: UniqueHelper, membership: string) {
3142 super(helper);
3143 this.membership = membership;
3144 }
3145
3146 /**
3147 * Returns an array of members' addresses according to the membership pallet's perception.
3148 * Note that it does not recognize the original pallet's members set with `setMembers()`.
3149 */
3150 async getMembers() {
3151 return (await this.helper.callRpc(`api.query.${this.membership}.members`, [])).toHuman();
3152 }
3153
3154 /**
3155 * Returns the optional address of the prime member of the collective.
3156 */
3157 async getPrimeMember() {
3158 return (await this.helper.callRpc(`api.query.${this.membership}.prime`, [])).toHuman();
3159 }
3160
3161 /**
3162 * Add a member to the collective.
3163 * @param signer keyring of the setter. Must be root
3164 * @param member address of the member to add
3165 * @returns promise of extrinsic execution and its result
3166 */
3167 addMember(signer: TSigner, member: string) {
3168 return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.addMember`, [member]);
3169 }
3170
3171 addMemberCall(member: string) {
3172 return this.helper.constructApiCall(`api.tx.${this.membership}.addMember`, [member]);
3173 }
3174
3175 /**
3176 * Remove a member from the collective.
3177 * @param signer keyring of the setter. Must be root
3178 * @param member address of the member to remove
3179 * @returns promise of extrinsic execution and its result
3180 */
3181 removeMember(signer: TSigner, member: string) {
3182 return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.removeMember`, [member]);
3183 }
3184
3185 removeMemberCall(member: string) {
3186 return this.helper.constructApiCall(`api.tx.${this.membership}.removeMember`, [member]);
3187 }
3188
3189 /**
3190 * Set members of the collective to the given list of addresses.
3191 * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
3192 * @param members addresses of the members to set
3193 * @returns promise of extrinsic execution and its result
3194 */
3195 resetMembers(signer: TSigner, members: string[]) {
3196 return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.resetMembers`, [members]);
3197 }
3198
3199 /**
3200 * Set the collective's prime member to the given address.
3201 * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
3202 * @param prime address of the prime member of the collective
3203 * @returns promise of extrinsic execution and its result
3204 */
3205 setPrime(signer: TSigner, prime: string) {
3206 return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.setPrime`, [prime]);
3207 }
3208
3209 setPrimeCall(member: string) {
3210 return this.helper.constructApiCall(`api.tx.${this.membership}.setPrime`, [member]);
3211 }
3212
3213 /**
3214 * Remove the collective's prime member.
3215 * @param signer keyring of the setter. Must be root (for the direct call, bypassing a public motion)
3216 * @returns promise of extrinsic execution and its result
3217 */
3218 clearPrime(signer: TSigner) {
3219 return this.helper.executeExtrinsic(signer, `api.tx.${this.membership}.clearPrime`, []);
3220 }
3221
3222 clearPrimeCall() {
3223 return this.helper.constructApiCall(`api.tx.${this.membership}.clearPrime`, []);
3224 }
3225}
3226
3227class RankedCollectiveGroup extends HelperGroup<UniqueHelper> {
3228 /**
3229 * Pallet name to make an API call to. Examples: 'FellowshipCollective'
3230 */
3231 private collective: string;
3232
3233 constructor(helper: UniqueHelper, collective: string) {
3234 super(helper);
3235 this.collective = collective;
3236 }
3237
3238 addMember(signer: TSigner, newMember: string) {
3239 return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.addMember`, [newMember]);
3240 }
3241
3242 addMemberCall(newMember: string) {
3243 return this.helper.constructApiCall(`api.tx.${this.collective}.addMember`, [newMember]);
3244 }
3245
3246 removeMember(signer: TSigner, member: string, minRank: number) {
3247 return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.removeMember`, [member, minRank]);
3248 }
3249
3250 removeMemberCall(newMember: string, minRank: number) {
3251 return this.helper.constructApiCall(`api.tx.${this.collective}.removeMember`, [newMember, minRank]);
3252 }
3253
3254 promote(signer: TSigner, member: string) {
3255 return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.promoteMember`, [member]);
3256 }
3257
3258 promoteCall(member: string) {
3259 return this.helper.constructApiCall(`api.tx.${this.collective}.promoteMember`, [member]);
3260 }
3261
3262 demote(signer: TSigner, member: string) {
3263 return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.demoteMember`, [member]);
3264 }
3265
3266 demoteCall(newMember: string) {
3267 return this.helper.constructApiCall(`api.tx.${this.collective}.demoteMember`, [newMember]);
3268 }
3269
3270 vote(signer: TSigner, pollIndex: number, aye: boolean) {
3271 return this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [pollIndex, aye]);
3272 }
3273
3274 async getMembers() {
3275 return (await this.helper.getApi().query.fellowshipCollective.members.keys())
3276 .map((key) => key.args[0].toString());
3277 }
3278
3279 async getMemberRank(member: string) {
3280 return (await this.helper.callRpc('api.query.fellowshipCollective.members', [member])).toJSON().rank;
3281 }
3282}
3283
3284class ReferendaGroup extends HelperGroup<UniqueHelper> {
3285 /**
3286 * Pallet name to make an API call to. Examples: 'FellowshipReferenda'
3287 */
3288 private referenda: string;
3289
3290 constructor(helper: UniqueHelper, referenda: string) {
3291 super(helper);
3292 this.referenda = referenda;
3293 }
3294
3295 submit(
3296 signer: TSigner,
3297 proposalOrigin: string,
3298 proposal: any,
3299 enactmentMoment: any,
3300 ) {
3301 return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.submit`, [
3302 {Origins: proposalOrigin},
3303 proposal,
3304 enactmentMoment,
3305 ]);
3306 }
3307
3308 placeDecisionDeposit(signer: TSigner, referendumIndex: number) {
3309 return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.placeDecisionDeposit`, [referendumIndex]);
3310 }
3311
3312 cancel(signer: TSigner, referendumIndex: number) {
3313 return this.helper.executeExtrinsic(signer, `api.tx.${this.referenda}.cancel`, [referendumIndex]);
3314 }
3315
3316 cancelCall(referendumIndex: number) {
3317 return this.helper.constructApiCall(`api.tx.${this.referenda}.cancel`, [referendumIndex]);
3318 }
3319
3320 async referendumInfo(referendumIndex: number) {
3321 return (await this.helper.callRpc(`api.query.${this.referenda}.referendumInfoFor`, [referendumIndex])).toJSON();
3322 }
3323
3324 async enactmentEventId(referendumIndex: number) {
3325 const api = await this.helper.getApi();
3326
3327 const bytes = api.createType('([u8;8], Text, u32)', ['assembly', 'enactment', referendumIndex]).toU8a();
3328 return blake2AsHex(bytes, 256);
3329 }
3330}
3331
3332export interface IFellowshipGroup {
3333 collective: RankedCollectiveGroup;
3334 referenda: ReferendaGroup;
3335}
3336
3337export interface ICollectiveGroup {
3338 collective: CollectiveGroup;
3339 membership: CollectiveMembershipGroup;
3340}
3341
3342class DemocracyGroup extends HelperGroup<UniqueHelper> {
3343 // todo displace proposal into types?
3344 propose(signer: TSigner, call: any, deposit: bigint) {
3345 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
3346 }
3347
3348 proposeWithPreimage(signer: TSigner, preimage: string, deposit: bigint) {
3349 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.propose', [{Legacy: preimage}, deposit]);
3350 }
3351
3352 proposeCall(call: any, deposit: bigint) {
3353 return this.helper.constructApiCall('api.tx.democracy.propose', [{Inline: call.method.toHex()}, deposit]);
3354 }
3355
3356 second(signer: TSigner, proposalIndex: number) {
3357 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.second', [proposalIndex]);
3358 }
3359
3360 externalPropose(signer: TSigner, proposalCall: any) {
3361 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
3362 }
3363
3364 externalProposeMajority(signer: TSigner, proposalCall: any) {
3365 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
3366 }
3367
3368 externalProposeDefault(signer: TSigner, proposalCall: any) {
3369 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
3370 }
3371
3372 externalProposeDefaultWithPreimage(signer: TSigner, preimage: string) {
3373 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
3374 }
3375
3376 externalProposeCall(proposalCall: any) {
3377 return this.helper.constructApiCall('api.tx.democracy.externalPropose', [{Inline: proposalCall.method.toHex()}]);
3378 }
3379
3380 externalProposeMajorityCall(proposalCall: any) {
3381 return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [{Inline: proposalCall.method.toHex()}]);
3382 }
3383
3384 externalProposeDefaultCall(proposalCall: any) {
3385 return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Inline: proposalCall.method.toHex()}]);
3386 }
3387
3388 externalProposeDefaultWithPreimageCall(preimage: string) {
3389 return this.helper.constructApiCall('api.tx.democracy.externalProposeDefault', [{Legacy: preimage}]);
3390 }
3391
3392 // ... and blacklist external proposal hash.
3393 vetoExternal(signer: TSigner, proposalHash: string) {
3394 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vetoExternal', [proposalHash]);
3395 }
3396
3397 vetoExternalCall(proposalHash: string) {
3398 return this.helper.constructApiCall('api.tx.democracy.vetoExternal', [proposalHash]);
3399 }
3400
3401 blacklist(signer: TSigner, proposalHash: string, referendumIndex: number | null = null) {
3402 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
3403 }
3404
3405 blacklistCall(proposalHash: string, referendumIndex: number | null = null) {
3406 return this.helper.constructApiCall('api.tx.democracy.blacklist', [proposalHash, referendumIndex]);
3407 }
3408
3409 // proposal. CancelProposalOrigin (root or all techcom)
3410 cancelProposal(signer: TSigner, proposalIndex: number) {
3411 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.cancelProposal', [proposalIndex]);
3412 }
3413
3414 cancelProposalCall(proposalIndex: number) {
3415 return this.helper.constructApiCall('api.tx.democracy.cancelProposal', [proposalIndex]);
3416 }
3417
3418 clearPublicProposals(signer: TSigner) {
3419 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.clearPublicProposals', []);
3420 }
3421
3422 fastTrack(signer: TSigner, proposalHash: string, votingPeriod: number, delayPeriod: number) {
3423 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
3424 }
3425
3426 fastTrackCall(proposalHash: string, votingPeriod: number, delayPeriod: number) {
3427 return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
3428 }
3429
3430 // referendum. CancellationOrigin (TechCom member)
3431 emergencyCancel(signer: TSigner, referendumIndex: number) {
3432 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.emergencyCancel', [referendumIndex]);
3433 }
3434
3435 emergencyCancelCall(referendumIndex: number) {
3436 return this.helper.constructApiCall('api.tx.democracy.emergencyCancel', [referendumIndex]);
3437 }
3438
3439 vote(signer: TSigner, referendumIndex: number, vote: any) {
3440 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, vote]);
3441 }
3442
3443 removeVote(signer: TSigner, referendumIndex: number, targetAccount?: string) {
3444 if(targetAccount) {
3445 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeOtherVote', [targetAccount, referendumIndex]);
3446 } else {
3447 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.removeVote', [referendumIndex]);
3448 }
3449 }
3450
3451 unlock(signer: TSigner, targetAccount: string) {
3452 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.unlock', [targetAccount]);
3453 }
3454
3455 delegate(signer: TSigner, toAccount: string, conviction: PalletDemocracyConviction, balance: bigint) {
3456 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.delegate', [toAccount, conviction, balance]);
3457 }
3458
3459 undelegate(signer: TSigner) {
3460 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.undelegate', []);
3461 }
3462
3463 async referendumInfo(referendumIndex: number) {
3464 return (await this.helper.callRpc('api.query.democracy.referendumInfoOf', [referendumIndex])).toJSON();
3465 }
3466
3467 async publicProposals() {
3468 return (await this.helper.callRpc('api.query.democracy.publicProps', [])).toJSON();
3469 }
3470
3471 async findPublicProposal(proposalIndex: number) {
3472 const proposalInfo = (await this.publicProposals()).find((proposalInfo: any[]) => proposalInfo[0] == proposalIndex);
3473
3474 return proposalInfo ? proposalInfo[1] : null;
3475 }
3476
3477 async expectPublicProposal(proposalIndex: number) {
3478 const proposal = await this.findPublicProposal(proposalIndex);
3479
3480 if(proposal) {
3481 return proposal;
3482 } else {
3483 throw Error(`Proposal #${proposalIndex} is expected to exist`);
3484 }
3485 }
3486
3487 async getExternalProposal() {
3488 return (await this.helper.callRpc('api.query.democracy.nextExternal', []));
3489 }
3490
3491 async expectExternalProposal() {
3492 const proposal = await this.getExternalProposal();
3493
3494 if(proposal) {
3495 return proposal;
3496 } else {
3497 throw Error('An external proposal is expected to exist');
3498 }
3499 }
3500
3501 /* setMetadata? */
3502
3503 /* todo?
3504 referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
3505 return this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
3506 }*/
3507}
35082866
3509class PreimageGroup extends HelperGroup<UniqueHelper> {2867class PreimageGroup extends HelperGroup<UniqueHelper> {
3510 async getPreimageInfo(h256: string) {2868 async getPreimageInfo(h256: string) {
3575 }2933 }
3576}2934}
3577
3578class ForeignAssetsGroup extends HelperGroup<UniqueHelper> {
3579 async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) {
3580 await this.helper.executeExtrinsic(
3581 signer,
3582 'api.tx.foreignAssets.registerForeignAsset',
3583 [ownerAddress, location, metadata],
3584 true,
3585 );
3586 }
3587
3588 async update(signer: TSigner, foreignAssetId: number, location: any, metadata: IForeignAssetMetadata) {
3589 await this.helper.executeExtrinsic(
3590 signer,
3591 'api.tx.foreignAssets.updateForeignAsset',
3592 [foreignAssetId, location, metadata],
3593 true,
3594 );
3595 }
3596}
3597
3598class XcmGroup<T extends ChainHelperBase> extends HelperGroup<T> {
3599 palletName: string;
3600
3601 constructor(helper: T, palletName: string) {
3602 super(helper);
3603
3604 this.palletName = palletName;
3605 }
3606
3607 async limitedReserveTransferAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number, weightLimit: any) {
3608 await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.limitedReserveTransferAssets`, [destination, beneficiary, assets, feeAssetItem, weightLimit], true);
3609 }
3610
3611 async setSafeXcmVersion(signer: TSigner, version: number) {
3612 await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.forceDefaultXcmVersion`, [version], true);
3613 }
3614
3615 async teleportAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number) {
3616 await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.teleportAssets`, [destination, beneficiary, assets, feeAssetItem], true);
3617 }
3618
3619 async teleportNativeAsset(signer: TSigner, destinationParaId: number, targetAccount: Uint8Array, amount: bigint, xcmVersion = 3) {
3620 const destinationContent = {
3621 parents: 0,
3622 interior: {
3623 X1: {
3624 Parachain: destinationParaId,
3625 },
3626 },
3627 };
3628
3629 const beneficiaryContent = {
3630 parents: 0,
3631 interior: {
3632 X1: {
3633 AccountId32: {
3634 network: 'Any',
3635 id: targetAccount,
3636 },
3637 },
3638 },
3639 };
3640
3641 const assetsContent = [
3642 {
3643 id: {
3644 Concrete: {
3645 parents: 0,
3646 interior: 'Here',
3647 },
3648 },
3649 fun: {
3650 Fungible: amount,
3651 },
3652 },
3653 ];
3654
3655 let destination;
3656 let beneficiary;
3657 let assets;
3658
3659 if(xcmVersion == 2) {
3660 destination = {V1: destinationContent};
3661 beneficiary = {V1: beneficiaryContent};
3662 assets = {V1: assetsContent};
3663
3664 } else if(xcmVersion == 3) {
3665 destination = {V2: destinationContent};
3666 beneficiary = {V2: beneficiaryContent};
3667 assets = {V2: assetsContent};
3668
3669 } else {
3670 throw Error('Unknown XCM version: ' + xcmVersion);
3671 }
3672
3673 const feeAssetItem = 0;
3674
3675 await this.teleportAssets(signer, destination, beneficiary, assets, feeAssetItem);
3676 }
3677
3678 async send(signer: IKeyringPair, destination: any, message: any) {
3679 await this.helper.executeExtrinsic(
3680 signer,
3681 `api.tx.${this.palletName}.send`,
3682 [
3683 destination,
3684 message,
3685 ],
3686 true,
3687 );
3688 }
3689}
3690
3691class XTokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
3692 async transfer(signer: TSigner, currencyId: any, amount: bigint, destination: any, destWeight: any) {
3693 await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transfer', [currencyId, amount, destination, destWeight], true);
3694 }
3695
3696 async transferMultiasset(signer: TSigner, asset: any, destination: any, destWeight: any) {
3697 await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMultiasset', [asset, destination, destWeight], true);
3698 }
3699
3700 async transferMulticurrencies(signer: TSigner, currencies: any[], feeItem: number, destLocation: any, destWeight: any) {
3701 await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMulticurrencies', [currencies, feeItem, destLocation, destWeight], true);
3702 }
3703}
3704
3705class PolkadexXcmHelperGroup<T extends ChainHelperBase> extends HelperGroup<T> {
3706 async whitelistToken(signer: TSigner, assetId: any) {
3707 await this.helper.executeExtrinsic(signer, 'api.tx.xcmHelper.whitelistToken', [assetId], true);
3708 }
3709}
3710
3711class TokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
3712 async accounts(address: string, currencyId: any) {
3713 const {free} = (await this.helper.callRpc('api.query.tokens.accounts', [address, currencyId])).toJSON() as any;
3714 return BigInt(free);
3715 }
3716}
3717
3718class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {
3719 async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) {
3720 await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
3721 }
3722
3723 async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) {
3724 await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true);
3725 }
3726
3727 async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) {
3728 await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
3729 }
3730
3731 async account(assetId: string | number, address: string) {
3732 const accountAsset = (
3733 await this.helper.callRpc('api.query.assets.account', [assetId, address])
3734 ).toJSON()! as any;
3735
3736 if(accountAsset !== null) {
3737 return BigInt(accountAsset['balance']);
3738 } else {
3739 return null;
3740 }
3741 }
3742}
37432935
3744class UtilityGroup<T extends ChainHelperBase> extends HelperGroup<T> {2936class UtilityGroup<T extends ChainHelperBase> extends HelperGroup<T> {
3745 async batch(signer: TSigner, txs: any[]) {2937 async batch(signer: TSigner, txs: any[]) {
3755 }2947 }
3756}2948}
3757
3758class AcalaAssetRegistryGroup extends HelperGroup<AcalaHelper> {
3759 async registerForeignAsset(signer: TSigner, destination: any, metadata: AcalaAssetMetadata) {
3760 await this.helper.executeExtrinsic(signer, 'api.tx.assetRegistry.registerForeignAsset', [destination, metadata], true);
3761 }
3762}
3763
3764class MoonbeamAssetManagerGroup extends HelperGroup<MoonbeamHelper> {
3765 makeRegisterForeignAssetProposal(assetInfo: MoonbeamAssetInfo) {
3766 const apiPrefix = 'api.tx.assetManager.';
3767
3768 const registerTx = this.helper.constructApiCall(
3769 apiPrefix + 'registerForeignAsset',
3770 [assetInfo.location, assetInfo.metadata, assetInfo.existentialDeposit, assetInfo.isSufficient],
3771 );
3772
3773 const setUnitsTx = this.helper.constructApiCall(
3774 apiPrefix + 'setAssetUnitsPerSecond',
3775 [assetInfo.location, assetInfo.unitsPerSecond, assetInfo.numAssetsWeightHint],
3776 );
3777
3778 const batchCall = this.helper.getApi().tx.utility.batchAll([registerTx, setUnitsTx]);
3779 const encodedProposal = batchCall?.method.toHex() || '';
3780 return encodedProposal;
3781 }
3782
3783 async assetTypeId(location: any) {
3784 return await this.helper.callRpc('api.query.assetManager.assetTypeId', [location]);
3785 }
3786}
3787
3788class MoonbeamDemocracyGroup extends HelperGroup<MoonbeamHelper> {
3789 notePreimagePallet: string;
3790
3791 constructor(helper: MoonbeamHelper, options: { [key: string]: any } = {}) {
3792 super(helper);
3793 this.notePreimagePallet = options.notePreimagePallet;
3794 }
3795
3796 async notePreimage(signer: TSigner, encodedProposal: string) {
3797 await this.helper.executeExtrinsic(signer, `api.tx.${this.notePreimagePallet}.notePreimage`, [encodedProposal], true);
3798 }
3799
3800 externalProposeMajority(proposal: any) {
3801 return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [proposal]);
3802 }
3803
3804 fastTrack(proposalHash: string, votingPeriod: number, delayPeriod: number) {
3805 return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
3806 }
3807
3808 async referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
3809 await this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
3810 }
3811}
3812
3813class MoonbeamCollectiveGroup extends HelperGroup<MoonbeamHelper> {
3814 collective: string;
3815
3816 constructor(helper: MoonbeamHelper, collective: string) {
3817 super(helper);
3818
3819 this.collective = collective;
3820 }
3821
3822 async propose(signer: TSigner, threshold: number, proposalHash: string, lengthBound: number) {
3823 await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [threshold, proposalHash, lengthBound], true);
3824 }
3825
3826 async vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
3827 await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve], true);
3828 }
3829
3830 async close(signer: TSigner, proposalHash: string, proposalIndex: number, weightBound: any, lengthBound: number) {
3831 await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [proposalHash, proposalIndex, weightBound, lengthBound], true);
3832 }
3833
3834 async proposalCount() {
3835 return Number(await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, []));
3836 }
3837}
38382949
3839export type ChainHelperBaseConstructor = new (...args: any[]) => ChainHelperBase;2950export type ChainHelperBaseConstructor = new (...args: any[]) => ChainHelperBase;
3840export type UniqueHelperConstructor = new (...args: any[]) => UniqueHelper;2951export type UniqueHelperConstructor = new (...args: any[]) => UniqueHelper;
3846 rft: RFTGroup;2957 rft: RFTGroup;
3847 ft: FTGroup;2958 ft: FTGroup;
3848 staking: StakingGroup;2959 staking: StakingGroup;
3849 scheduler: SchedulerGroup;
3850 collatorSelection: CollatorSelectionGroup;
3851 council: ICollectiveGroup;
3852 technicalCommittee: ICollectiveGroup;
3853 fellowship: IFellowshipGroup;
3854 democracy: DemocracyGroup;
3855 preimage: PreimageGroup;2960 preimage: PreimageGroup;
3856 foreignAssets: ForeignAssetsGroup;
3857 xcm: XcmGroup<UniqueHelper>;
3858 xTokens: XTokensGroup<UniqueHelper>;
3859 tokens: TokensGroup<UniqueHelper>;
3860 utility: UtilityGroup<UniqueHelper>;2961 utility: UtilityGroup<UniqueHelper>;
38612962
3862 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {2963 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
3868 this.rft = new RFTGroup(this);2969 this.rft = new RFTGroup(this);
3869 this.ft = new FTGroup(this);2970 this.ft = new FTGroup(this);
3870 this.staking = new StakingGroup(this);2971 this.staking = new StakingGroup(this);
3871 this.scheduler = new SchedulerGroup(this);
3872 this.collatorSelection = new CollatorSelectionGroup(this);
3873 this.council = {
3874 collective: new CollectiveGroup(this, 'council'),
3875 membership: new CollectiveMembershipGroup(this, 'councilMembership'),
3876 };
3877 this.technicalCommittee = {
3878 collective: new CollectiveGroup(this, 'technicalCommittee'),
3879 membership: new CollectiveMembershipGroup(this, 'technicalCommitteeMembership'),
3880 };
3881 this.fellowship = {
3882 collective: new RankedCollectiveGroup(this, 'fellowshipCollective'),
3883 referenda: new ReferendaGroup(this, 'fellowshipReferenda'),
3884 };
3885 this.democracy = new DemocracyGroup(this);
3886 this.preimage = new PreimageGroup(this);2972 this.preimage = new PreimageGroup(this);
3887 this.foreignAssets = new ForeignAssetsGroup(this);
3888 this.xcm = new XcmGroup(this, 'polkadotXcm');
3889 this.xTokens = new XTokensGroup(this);
3890 this.tokens = new TokensGroup(this);
3891 this.utility = new UtilityGroup(this);2973 this.utility = new UtilityGroup(this);
3892 }2974 }
3893
3894 getSudo<T extends UniqueHelper>() {
3895 // eslint-disable-next-line @typescript-eslint/naming-convention
3896 const SudoHelperType = SudoHelper(this.helperBase);
3897 return this.clone(SudoHelperType) as T;
3898 }
3899}2975}
3900
3901export class XcmChainHelper extends ChainHelperBase {
3902 async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
3903 const wsProvider = new WsProvider(wsEndpoint);
3904 this.api = new ApiPromise({
3905 provider: wsProvider,
3906 });
3907 await this.api.isReadyOrError;
3908 this.network = await UniqueHelper.detectNetwork(this.api);
3909 }
3910}
3911
3912export class RelayHelper extends XcmChainHelper {
3913 balance: SubstrateBalanceGroup<RelayHelper>;
3914 xcm: XcmGroup<RelayHelper>;
3915
3916 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
3917 super(logger, options.helperBase ?? RelayHelper);
3918
3919 this.balance = new SubstrateBalanceGroup(this);
3920 this.xcm = new XcmGroup(this, 'xcmPallet');
3921 }
3922}
3923
3924export class WestmintHelper extends XcmChainHelper {
3925 balance: SubstrateBalanceGroup<WestmintHelper>;
3926 xcm: XcmGroup<WestmintHelper>;
3927 assets: AssetsGroup<WestmintHelper>;
3928 xTokens: XTokensGroup<WestmintHelper>;
3929
3930 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
3931 super(logger, options.helperBase ?? WestmintHelper);
3932
3933 this.balance = new SubstrateBalanceGroup(this);
3934 this.xcm = new XcmGroup(this, 'polkadotXcm');
3935 this.assets = new AssetsGroup(this);
3936 this.xTokens = new XTokensGroup(this);
3937 }
3938}
3939
3940export class MoonbeamHelper extends XcmChainHelper {
3941 balance: EthereumBalanceGroup<MoonbeamHelper>;
3942 assetManager: MoonbeamAssetManagerGroup;
3943 assets: AssetsGroup<MoonbeamHelper>;
3944 xTokens: XTokensGroup<MoonbeamHelper>;
3945 democracy: MoonbeamDemocracyGroup;
3946 collective: {
3947 council: MoonbeamCollectiveGroup,
3948 techCommittee: MoonbeamCollectiveGroup,
3949 };
3950
3951 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
3952 super(logger, options.helperBase ?? MoonbeamHelper);
3953
3954 this.balance = new EthereumBalanceGroup(this);
3955 this.assetManager = new MoonbeamAssetManagerGroup(this);
3956 this.assets = new AssetsGroup(this);
3957 this.xTokens = new XTokensGroup(this);
3958 this.democracy = new MoonbeamDemocracyGroup(this, options);
3959 this.collective = {
3960 council: new MoonbeamCollectiveGroup(this, 'councilCollective'),
3961 techCommittee: new MoonbeamCollectiveGroup(this, 'techCommitteeCollective'),
3962 };
3963 }
3964}
3965
3966export class AstarHelper extends XcmChainHelper {
3967 balance: SubstrateBalanceGroup<AstarHelper>;
3968 assets: AssetsGroup<AstarHelper>;
3969 xcm: XcmGroup<AstarHelper>;
3970
3971 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
3972 super(logger, options.helperBase ?? AstarHelper);
3973
3974 this.balance = new SubstrateBalanceGroup(this);
3975 this.assets = new AssetsGroup(this);
3976 this.xcm = new XcmGroup(this, 'polkadotXcm');
3977 }
3978
3979 getSudo<T extends UniqueHelper>() {
3980 // eslint-disable-next-line @typescript-eslint/naming-convention
3981 const SudoHelperType = SudoHelper(this.helperBase);
3982 return this.clone(SudoHelperType) as T;
3983 }
3984}
3985
3986export class AcalaHelper extends XcmChainHelper {
3987 balance: SubstrateBalanceGroup<AcalaHelper>;
3988 assetRegistry: AcalaAssetRegistryGroup;
3989 xTokens: XTokensGroup<AcalaHelper>;
3990 tokens: TokensGroup<AcalaHelper>;
3991 xcm: XcmGroup<AcalaHelper>;
3992
3993 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
3994 super(logger, options.helperBase ?? AcalaHelper);
3995
3996 this.balance = new SubstrateBalanceGroup(this);
3997 this.assetRegistry = new AcalaAssetRegistryGroup(this);
3998 this.xTokens = new XTokensGroup(this);
3999 this.tokens = new TokensGroup(this);
4000 this.xcm = new XcmGroup(this, 'polkadotXcm');
4001 }
4002
4003 getSudo<T extends AcalaHelper>() {
4004 // eslint-disable-next-line @typescript-eslint/naming-convention
4005 const SudoHelperType = SudoHelper(this.helperBase);
4006 return this.clone(SudoHelperType) as T;
4007 }
4008}
4009
4010export class PolkadexHelper extends XcmChainHelper {
4011 assets: AssetsGroup<PolkadexHelper>;
4012 balance: SubstrateBalanceGroup<PolkadexHelper>;
4013 xTokens: XTokensGroup<PolkadexHelper>;
4014 xcm: XcmGroup<PolkadexHelper>;
4015 xcmHelper: PolkadexXcmHelperGroup<PolkadexHelper>;
4016
4017 constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
4018 super(logger, options.helperBase ?? PolkadexHelper);
4019
4020 this.assets = new AssetsGroup(this);
4021 this.balance = new SubstrateBalanceGroup(this);
4022 this.xTokens = new XTokensGroup(this);
4023 this.xcm = new XcmGroup(this, 'polkadotXcm');
4024 this.xcmHelper = new PolkadexXcmHelperGroup(this);
4025 }
4026
4027 getSudo<T extends PolkadexHelper>() {
4028 // eslint-disable-next-line @typescript-eslint/naming-convention
4029 const SudoHelperType = SudoHelper(this.helperBase);
4030 return this.clone(SudoHelperType) as T;
4031 }
4032}
4033
4034// eslint-disable-next-line @typescript-eslint/naming-convention
4035function ScheduledUniqueHelper<T extends UniqueHelperConstructor>(Base: T) {
4036 return class extends Base {
4037 scheduleFn: 'schedule' | 'scheduleAfter';
4038 blocksNum: number;
4039 options: ISchedulerOptions;
4040
4041 constructor(...args: any[]) {
4042 const logger = args[0] as ILogger;
4043 const options = args[1] as {
4044 scheduleFn: 'schedule' | 'scheduleAfter',
4045 blocksNum: number,
4046 options: ISchedulerOptions
4047 };
4048
4049 super(logger);
4050
4051 this.scheduleFn = options.scheduleFn;
4052 this.blocksNum = options.blocksNum;
4053 this.options = options.options;
4054 }
4055
4056 executeExtrinsic(sender: IKeyringPair, scheduledExtrinsic: string, scheduledParams: any[], expectSuccess?: boolean): Promise<ITransactionResult> {
4057 const scheduledTx = this.constructApiCall(scheduledExtrinsic, scheduledParams);
4058
4059 const mandatorySchedArgs = [
4060 this.blocksNum,
4061 this.options.periodic ? [this.options.periodic.period, this.options.periodic.repetitions] : null,
4062 this.options.priority ?? null,
4063 scheduledTx,
4064 ];
4065
4066 let schedArgs;
4067 let scheduleFn;
4068
4069 if(this.options.scheduledId) {
4070 schedArgs = [this.options.scheduledId!, ...mandatorySchedArgs];
4071
4072 if(this.scheduleFn == 'schedule') {
4073 scheduleFn = 'scheduleNamed';
4074 } else if(this.scheduleFn == 'scheduleAfter') {
4075 scheduleFn = 'scheduleNamedAfter';
4076 }
4077 } else {
4078 schedArgs = mandatorySchedArgs;
4079 scheduleFn = this.scheduleFn;
4080 }
4081
4082 const extrinsic = 'api.tx.scheduler.' + scheduleFn;
4083
4084 return super.executeExtrinsic(
4085 sender,
4086 extrinsic as any,
4087 schedArgs,
4088 expectSuccess,
4089 );
4090 }
4091 };
4092}
4093
4094// eslint-disable-next-line @typescript-eslint/naming-convention
4095function SudoHelper<T extends ChainHelperBaseConstructor>(Base: T) {
4096 return class extends Base {
4097 constructor(...args: any[]) {
4098 super(...args);
4099 }
4100
4101 async executeExtrinsic(
4102 sender: IKeyringPair,
4103 extrinsic: string,
4104 params: any[],
4105 expectSuccess?: boolean,
4106 options: Partial<SignerOptions> | null = null,
4107 ): Promise<ITransactionResult> {
4108 const call = this.constructApiCall(extrinsic, params);
4109 const result = await super.executeExtrinsic(
4110 sender,
4111 'api.tx.sudo.sudo',
4112 [call],
4113 expectSuccess,
4114 options,
4115 );
4116
4117 if(result.status === 'Fail') return result;
4118
4119 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
4120 if(data.isErr) {
4121 if(data.asErr.isModule) {
4122 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
4123 const metaError = super.getApi()?.registry.findMetaError(error);
4124 throw new Error(`${metaError.section}.${metaError.name}`);
4125 } else if(data.asErr.isToken) {
4126 throw new Error(`Token: ${data.asErr.asToken}`);
4127 }
4128 // May be [object Object] in case of unhandled non-unit enum
4129 throw new Error(`Misc: ${data.asErr.toHuman()}`);
4130 }
4131 return result;
4132 }
4133 async executeExtrinsicUncheckedWeight(
4134 sender: IKeyringPair,
4135 extrinsic: string,
4136 params: any[],
4137 expectSuccess?: boolean,
4138 options: Partial<SignerOptions> | null = null,
4139 ): Promise<ITransactionResult> {
4140 const call = this.constructApiCall(extrinsic, params);
4141 const result = await super.executeExtrinsic(
4142 sender,
4143 'api.tx.sudo.sudoUncheckedWeight',
4144 [call, {refTime: 0, proofSize: 0}],
4145 expectSuccess,
4146 options,
4147 );
4148
4149 if(result.status === 'Fail') return result;
4150
4151 const data = (result.result.events.find(x => x.event.section == 'sudo' && x.event.method == 'Sudid')?.event.data as any).sudoResult;
4152 if(data.isErr) {
4153 if(data.asErr.isModule) {
4154 const error = (result.result.events[1].event.data as any).sudoResult.asErr.asModule;
4155 const metaError = super.getApi()?.registry.findMetaError(error);
4156 throw new Error(`${metaError.section}.${metaError.name}`);
4157 } else if(data.asErr.isToken) {
4158 throw new Error(`Token: ${data.asErr.asToken}`);
4159 }
4160 // May be [object Object] in case of unhandled non-unit enum
4161 throw new Error(`Misc: ${data.asErr.toHuman()}`);
4162 }
4163 return result;
4164 }
4165 };
4166}
41672976
4168export class UniqueBaseCollection {2977export class UniqueBaseCollection {
4169 helper: UniqueHelper;2978 helper: UniqueHelper;
4274 return await this.helper.collection.burn(signer, this.collectionId);3083 return await this.helper.collection.burn(signer, this.collectionId);
4275 }3084 }
4276
4277 scheduleAt<T extends UniqueHelper>(
4278 executionBlockNumber: number,
4279 options: ISchedulerOptions = {},
4280 ) {
4281 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
4282 return new UniqueBaseCollection(this.collectionId, scheduledHelper);
4283 }
4284
4285 scheduleAfter<T extends UniqueHelper>(
4286 blocksBeforeExecution: number,
4287 options: ISchedulerOptions = {},
4288 ) {
4289 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
4290 return new UniqueBaseCollection(this.collectionId, scheduledHelper);
4291 }
4292
4293 getSudo<T extends UniqueHelper>() {
4294 return new UniqueBaseCollection(this.collectionId, this.helper.getSudo<T>());
4295 }
4296}3085}
4297
42983086
4388 return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);3176 return await this.helper.nft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);
4389 }3177 }
4390
4391 scheduleAt<T extends UniqueHelper>(
4392 executionBlockNumber: number,
4393 options: ISchedulerOptions = {},
4394 ) {
4395 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
4396 return new UniqueNFTCollection(this.collectionId, scheduledHelper);
4397 }
4398
4399 scheduleAfter<T extends UniqueHelper>(
4400 blocksBeforeExecution: number,
4401 options: ISchedulerOptions = {},
4402 ) {
4403 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
4404 return new UniqueNFTCollection(this.collectionId, scheduledHelper);
4405 }
4406
4407 getSudo<T extends UniqueHelper>() {
4408 return new UniqueNFTCollection(this.collectionId, this.helper.getSudo<T>());
4409 }
4410}3178}
4411
44123179
4514 return await this.helper.rft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);3281 return await this.helper.rft.unnestToken(signer, {collectionId: this.collectionId, tokenId}, fromTokenObj, toAddressObj);
4515 }3282 }
4516
4517 scheduleAt<T extends UniqueHelper>(
4518 executionBlockNumber: number,
4519 options: ISchedulerOptions = {},
4520 ) {
4521 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
4522 return new UniqueRFTCollection(this.collectionId, scheduledHelper);
4523 }
4524
4525 scheduleAfter<T extends UniqueHelper>(
4526 blocksBeforeExecution: number,
4527 options: ISchedulerOptions = {},
4528 ) {
4529 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
4530 return new UniqueRFTCollection(this.collectionId, scheduledHelper);
4531 }
4532
4533 getSudo<T extends UniqueHelper>() {
4534 return new UniqueRFTCollection(this.collectionId, this.helper.getSudo<T>());
4535 }
4536}3283}
4537
45383284
4581 return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount);3327 return await this.helper.ft.approveTokens(signer, this.collectionId, toAddressObj, amount);
4582 }3328 }
4583
4584 scheduleAt<T extends UniqueHelper>(
4585 executionBlockNumber: number,
4586 options: ISchedulerOptions = {},
4587 ) {
4588 const scheduledHelper = this.helper.scheduler.scheduleAt<T>(executionBlockNumber, options);
4589 return new UniqueFTCollection(this.collectionId, scheduledHelper);
4590 }
4591
4592 scheduleAfter<T extends UniqueHelper>(
4593 blocksBeforeExecution: number,
4594 options: ISchedulerOptions = {},
4595 ) {
4596 const scheduledHelper = this.helper.scheduler.scheduleAfter<T>(blocksBeforeExecution, options);
4597 return new UniqueFTCollection(this.collectionId, scheduledHelper);
4598 }
4599
4600 getSudo<T extends UniqueHelper>() {
4601 return new UniqueFTCollection(this.collectionId, this.helper.getSudo<T>());
4602 }
4603}3329}
4604
46053330
4642 return this.collection.helper.util.getTokenAccount(this);3367 return this.collection.helper.util.getTokenAccount(this);
4643 }3368 }
4644
4645 scheduleAt<T extends UniqueHelper>(
4646 executionBlockNumber: number,
4647 options: ISchedulerOptions = {},
4648 ) {
4649 const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);
4650 return new UniqueBaseToken(this.tokenId, scheduledCollection);
4651 }
4652
4653 scheduleAfter<T extends UniqueHelper>(
4654 blocksBeforeExecution: number,
4655 options: ISchedulerOptions = {},
4656 ) {
4657 const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);
4658 return new UniqueBaseToken(this.tokenId, scheduledCollection);
4659 }
4660
4661 getSudo<T extends UniqueHelper>() {
4662 return new UniqueBaseToken(this.tokenId, this.collection.getSudo<T>());
4663 }
4664}3369}
4665
46663370
4720 return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj);3424 return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj);
4721 }3425 }
4722
4723 scheduleAt<T extends UniqueHelper>(
4724 executionBlockNumber: number,
4725 options: ISchedulerOptions = {},
4726 ) {
4727 const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);
4728 return new UniqueNFToken(this.tokenId, scheduledCollection);
4729 }
4730
4731 scheduleAfter<T extends UniqueHelper>(
4732 blocksBeforeExecution: number,
4733 options: ISchedulerOptions = {},
4734 ) {
4735 const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);
4736 return new UniqueNFToken(this.tokenId, scheduledCollection);
4737 }
4738
4739 getSudo<T extends UniqueHelper>() {
4740 return new UniqueNFToken(this.tokenId, this.collection.getSudo<T>());
4741 }
4742}3426}
47433427
4744export class UniqueRFToken extends UniqueBaseToken {3428export class UniqueRFToken extends UniqueBaseToken {
4809 return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount);3493 return await this.collection.burnTokenFrom(signer, this.tokenId, fromAddressObj, amount);
4810 }3494 }
4811
4812 scheduleAt<T extends UniqueHelper>(
4813 executionBlockNumber: number,
4814 options: ISchedulerOptions = {},
4815 ) {
4816 const scheduledCollection = this.collection.scheduleAt<T>(executionBlockNumber, options);
4817 return new UniqueRFToken(this.tokenId, scheduledCollection);
4818 }
4819
4820 scheduleAfter<T extends UniqueHelper>(
4821 blocksBeforeExecution: number,
4822 options: ISchedulerOptions = {},
4823 ) {
4824 const scheduledCollection = this.collection.scheduleAfter<T>(blocksBeforeExecution, options);
4825 return new UniqueRFToken(this.tokenId, scheduledCollection);
4826 }
4827
4828 getSudo<T extends UniqueHelper>() {
4829 return new UniqueRFToken(this.tokenId, this.collection.getSudo<T>());
4830 }
4831}3495}
48323496
addedtests/src/util/playgrounds/unique.xcm.tsdiffbeforeafterboth
--- /dev/null
+++ b/tests/src/util/playgrounds/unique.xcm.ts
@@ -0,0 +1,371 @@
+import {ApiPromise, WsProvider} from '@polkadot/api';
+import {IKeyringPair} from '@polkadot/types/types';
+import {ChainHelperBase, EthereumBalanceGroup, HelperGroup, SubstrateBalanceGroup, UniqueHelper} from './unique';
+import {ILogger, TSigner, TSubstrateAccount} from './types';
+import {AcalaAssetMetadata, DemocracyStandardAccountVote, IForeignAssetMetadata, MoonbeamAssetInfo} from './types.xcm';
+
+
+export class XcmChainHelper extends ChainHelperBase {
+  async connect(wsEndpoint: string, _listeners?: any): Promise<void> {
+    const wsProvider = new WsProvider(wsEndpoint);
+    this.api = new ApiPromise({
+      provider: wsProvider,
+    });
+    await this.api.isReadyOrError;
+    this.network = await UniqueHelper.detectNetwork(this.api);
+  }
+}
+
+class AcalaAssetRegistryGroup extends HelperGroup<AcalaHelper> {
+  async registerForeignAsset(signer: TSigner, destination: any, metadata: AcalaAssetMetadata) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assetRegistry.registerForeignAsset', [destination, metadata], true);
+  }
+}
+
+class MoonbeamAssetManagerGroup extends HelperGroup<MoonbeamHelper> {
+  makeRegisterForeignAssetProposal(assetInfo: MoonbeamAssetInfo) {
+    const apiPrefix = 'api.tx.assetManager.';
+
+    const registerTx = this.helper.constructApiCall(
+      apiPrefix + 'registerForeignAsset',
+      [assetInfo.location, assetInfo.metadata, assetInfo.existentialDeposit, assetInfo.isSufficient],
+    );
+
+    const setUnitsTx = this.helper.constructApiCall(
+      apiPrefix + 'setAssetUnitsPerSecond',
+      [assetInfo.location, assetInfo.unitsPerSecond, assetInfo.numAssetsWeightHint],
+    );
+
+    const batchCall = this.helper.getApi().tx.utility.batchAll([registerTx, setUnitsTx]);
+    const encodedProposal = batchCall?.method.toHex() || '';
+    return encodedProposal;
+  }
+
+  async assetTypeId(location: any) {
+    return await this.helper.callRpc('api.query.assetManager.assetTypeId', [location]);
+  }
+}
+
+class MoonbeamDemocracyGroup extends HelperGroup<MoonbeamHelper> {
+  notePreimagePallet: string;
+
+  constructor(helper: MoonbeamHelper, options: { [key: string]: any } = {}) {
+    super(helper);
+    this.notePreimagePallet = options.notePreimagePallet;
+  }
+
+  async notePreimage(signer: TSigner, encodedProposal: string) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.notePreimagePallet}.notePreimage`, [encodedProposal], true);
+  }
+
+  externalProposeMajority(proposal: any) {
+    return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [proposal]);
+  }
+
+  fastTrack(proposalHash: string, votingPeriod: number, delayPeriod: number) {
+    return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]);
+  }
+
+  async referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true);
+  }
+}
+
+class MoonbeamCollectiveGroup extends HelperGroup<MoonbeamHelper> {
+  collective: string;
+
+  constructor(helper: MoonbeamHelper, collective: string) {
+    super(helper);
+
+    this.collective = collective;
+  }
+
+  async propose(signer: TSigner, threshold: number, proposalHash: string, lengthBound: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [threshold, proposalHash, lengthBound], true);
+  }
+
+  async vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve], true);
+  }
+
+  async close(signer: TSigner, proposalHash: string, proposalIndex: number, weightBound: any, lengthBound: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [proposalHash, proposalIndex, weightBound, lengthBound], true);
+  }
+
+  async proposalCount() {
+    return Number(await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, []));
+  }
+}
+
+class PolkadexXcmHelperGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async whitelistToken(signer: TSigner, assetId: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xcmHelper.whitelistToken', [assetId], true);
+  }
+}
+
+export class ForeignAssetsGroup extends HelperGroup<UniqueHelper> {
+  async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) {
+    await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.foreignAssets.registerForeignAsset',
+      [ownerAddress, location, metadata],
+      true,
+    );
+  }
+
+  async update(signer: TSigner, foreignAssetId: number, location: any, metadata: IForeignAssetMetadata) {
+    await this.helper.executeExtrinsic(
+      signer,
+      'api.tx.foreignAssets.updateForeignAsset',
+      [foreignAssetId, location, metadata],
+      true,
+    );
+  }
+}
+
+export class XcmGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  palletName: string;
+
+  constructor(helper: T, palletName: string) {
+    super(helper);
+
+    this.palletName = palletName;
+  }
+
+  async limitedReserveTransferAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number, weightLimit: any) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.limitedReserveTransferAssets`, [destination, beneficiary, assets, feeAssetItem, weightLimit], true);
+  }
+
+  async setSafeXcmVersion(signer: TSigner, version: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.forceDefaultXcmVersion`, [version], true);
+  }
+
+  async teleportAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number) {
+    await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.teleportAssets`, [destination, beneficiary, assets, feeAssetItem], true);
+  }
+
+  async teleportNativeAsset(signer: TSigner, destinationParaId: number, targetAccount: Uint8Array, amount: bigint, xcmVersion = 3) {
+    const destinationContent = {
+      parents: 0,
+      interior: {
+        X1: {
+          Parachain: destinationParaId,
+        },
+      },
+    };
+
+    const beneficiaryContent = {
+      parents: 0,
+      interior: {
+        X1: {
+          AccountId32: {
+            network: 'Any',
+            id: targetAccount,
+          },
+        },
+      },
+    };
+
+    const assetsContent = [
+      {
+        id: {
+          Concrete: {
+            parents: 0,
+            interior: 'Here',
+          },
+        },
+        fun: {
+          Fungible: amount,
+        },
+      },
+    ];
+
+    let destination;
+    let beneficiary;
+    let assets;
+
+    if(xcmVersion == 2) {
+      destination = {V1: destinationContent};
+      beneficiary = {V1: beneficiaryContent};
+      assets = {V1: assetsContent};
+
+    } else if(xcmVersion == 3) {
+      destination = {V2: destinationContent};
+      beneficiary = {V2: beneficiaryContent};
+      assets = {V2: assetsContent};
+
+    } else {
+      throw Error('Unknown XCM version: ' + xcmVersion);
+    }
+
+    const feeAssetItem = 0;
+
+    await this.teleportAssets(signer, destination, beneficiary, assets, feeAssetItem);
+  }
+
+  async send(signer: IKeyringPair, destination: any, message: any) {
+    await this.helper.executeExtrinsic(
+      signer,
+      `api.tx.${this.palletName}.send`,
+      [
+        destination,
+        message,
+      ],
+      true,
+    );
+  }
+}
+
+export class XTokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async transfer(signer: TSigner, currencyId: any, amount: bigint, destination: any, destWeight: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transfer', [currencyId, amount, destination, destWeight], true);
+  }
+
+  async transferMultiasset(signer: TSigner, asset: any, destination: any, destWeight: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMultiasset', [asset, destination, destWeight], true);
+  }
+
+  async transferMulticurrencies(signer: TSigner, currencies: any[], feeItem: number, destLocation: any, destWeight: any) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMulticurrencies', [currencies, feeItem, destLocation, destWeight], true);
+  }
+}
+
+
+
+export class TokensGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async accounts(address: string, currencyId: any) {
+    const {free} = (await this.helper.callRpc('api.query.tokens.accounts', [address, currencyId])).toJSON() as any;
+    return BigInt(free);
+  }
+}
+
+export class AssetsGroup<T extends ChainHelperBase> extends HelperGroup<T> {
+  async create(signer: TSigner, assetId: number, admin: string, minimalBalance: bigint) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.create', [assetId, admin, minimalBalance], true);
+  }
+
+  async setMetadata(signer: TSigner, assetId: number, name: string, symbol: string, decimals: number) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.setMetadata', [assetId, name, symbol, decimals], true);
+  }
+
+  async mint(signer: TSigner, assetId: number, beneficiary: string, amount: bigint) {
+    await this.helper.executeExtrinsic(signer, 'api.tx.assets.mint', [assetId, beneficiary, amount], true);
+  }
+
+  async account(assetId: string | number, address: string) {
+    const accountAsset = (
+      await this.helper.callRpc('api.query.assets.account', [assetId, address])
+    ).toJSON()! as any;
+
+    if(accountAsset !== null) {
+      return BigInt(accountAsset['balance']);
+    } else {
+      return null;
+    }
+  }
+}
+
+export class RelayHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<RelayHelper>;
+  xcm: XcmGroup<RelayHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? RelayHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.xcm = new XcmGroup(this, 'xcmPallet');
+  }
+}
+
+export class WestmintHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<WestmintHelper>;
+  xcm: XcmGroup<WestmintHelper>;
+  assets: AssetsGroup<WestmintHelper>;
+  xTokens: XTokensGroup<WestmintHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? WestmintHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+    this.assets = new AssetsGroup(this);
+    this.xTokens = new XTokensGroup(this);
+  }
+}
+
+export class MoonbeamHelper extends XcmChainHelper {
+  balance: EthereumBalanceGroup<MoonbeamHelper>;
+  assetManager: MoonbeamAssetManagerGroup;
+  assets: AssetsGroup<MoonbeamHelper>;
+  xTokens: XTokensGroup<MoonbeamHelper>;
+  democracy: MoonbeamDemocracyGroup;
+  collective: {
+    council: MoonbeamCollectiveGroup,
+    techCommittee: MoonbeamCollectiveGroup,
+  };
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? MoonbeamHelper);
+
+    this.balance = new EthereumBalanceGroup(this);
+    this.assetManager = new MoonbeamAssetManagerGroup(this);
+    this.assets = new AssetsGroup(this);
+    this.xTokens = new XTokensGroup(this);
+    this.democracy = new MoonbeamDemocracyGroup(this, options);
+    this.collective = {
+      council: new MoonbeamCollectiveGroup(this, 'councilCollective'),
+      techCommittee: new MoonbeamCollectiveGroup(this, 'techCommitteeCollective'),
+    };
+  }
+}
+
+export class AstarHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<AstarHelper>;
+  assets: AssetsGroup<AstarHelper>;
+  xcm: XcmGroup<AstarHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? AstarHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.assets = new AssetsGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+  }
+}
+
+export class AcalaHelper extends XcmChainHelper {
+  balance: SubstrateBalanceGroup<AcalaHelper>;
+  assetRegistry: AcalaAssetRegistryGroup;
+  xTokens: XTokensGroup<AcalaHelper>;
+  tokens: TokensGroup<AcalaHelper>;
+  xcm: XcmGroup<AcalaHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? AcalaHelper);
+
+    this.balance = new SubstrateBalanceGroup(this);
+    this.assetRegistry = new AcalaAssetRegistryGroup(this);
+    this.xTokens = new XTokensGroup(this);
+    this.tokens = new TokensGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+  }
+}
+
+export class PolkadexHelper extends XcmChainHelper {
+  assets: AssetsGroup<PolkadexHelper>;
+  balance: SubstrateBalanceGroup<PolkadexHelper>;
+  xTokens: XTokensGroup<PolkadexHelper>;
+  xcm: XcmGroup<PolkadexHelper>;
+  xcmHelper: PolkadexXcmHelperGroup<PolkadexHelper>;
+
+  constructor(logger?: ILogger, options: { [key: string]: any } = {}) {
+    super(logger, options.helperBase ?? PolkadexHelper);
+
+    this.assets = new AssetsGroup(this);
+    this.balance = new SubstrateBalanceGroup(this);
+    this.xTokens = new XTokensGroup(this);
+    this.xcm = new XcmGroup(this, 'polkadotXcm');
+    this.xcmHelper = new PolkadexXcmHelperGroup(this);
+  }
+}
+