Browse Source

Merge pull request #943 from Lezek123/cli-linter-fixes

CLI: Linter fixes
Mokhtar Naamani 4 years ago
parent
commit
e6325a66f6
48 changed files with 3154 additions and 3177 deletions
  1. 6 6
      .github/workflows/joystream-cli.yml
  2. 7 0
      cli/.eslintrc.js
  3. 1 1
      cli/package.json
  4. 385 396
      cli/src/Api.ts
  5. 11 11
      cli/src/ExitCodes.ts
  6. 290 267
      cli/src/Types.ts
  7. 213 224
      cli/src/base/AccountsCommandBase.ts
  8. 374 372
      cli/src/base/ApiCommandBase.ts
  9. 84 73
      cli/src/base/DefaultCommandBase.ts
  10. 94 95
      cli/src/base/StateAwareCommandBase.ts
  11. 235 233
      cli/src/base/WorkingGroupsCommandBase.ts
  12. 25 25
      cli/src/commands/account/choose.ts
  13. 35 35
      cli/src/commands/account/create.ts
  14. 34 34
      cli/src/commands/account/current.ts
  15. 62 61
      cli/src/commands/account/export.ts
  16. 23 21
      cli/src/commands/account/forget.ts
  17. 34 36
      cli/src/commands/account/import.ts
  18. 53 54
      cli/src/commands/account/transferTokens.ts
  19. 7 8
      cli/src/commands/api/getUri.ts
  20. 210 203
      cli/src/commands/api/inspect.ts
  21. 22 22
      cli/src/commands/api/setUri.ts
  22. 48 49
      cli/src/commands/council/info.ts
  23. 31 32
      cli/src/commands/working-groups/application.ts
  24. 77 74
      cli/src/commands/working-groups/createOpening.ts
  25. 53 55
      cli/src/commands/working-groups/decreaseWorkerStake.ts
  26. 52 59
      cli/src/commands/working-groups/evictWorker.ts
  27. 49 52
      cli/src/commands/working-groups/fillOpening.ts
  28. 37 37
      cli/src/commands/working-groups/increaseStake.ts
  29. 20 29
      cli/src/commands/working-groups/leaveRole.ts
  30. 67 66
      cli/src/commands/working-groups/opening.ts
  31. 18 18
      cli/src/commands/working-groups/openings.ts
  32. 34 35
      cli/src/commands/working-groups/overview.ts
  33. 52 50
      cli/src/commands/working-groups/slashWorker.ts
  34. 31 33
      cli/src/commands/working-groups/startAcceptingApplications.ts
  35. 29 33
      cli/src/commands/working-groups/startReviewPeriod.ts
  36. 29 34
      cli/src/commands/working-groups/terminateApplication.ts
  37. 44 50
      cli/src/commands/working-groups/updateRewardAccount.ts
  38. 54 60
      cli/src/commands/working-groups/updateRoleAccount.ts
  39. 53 58
      cli/src/commands/working-groups/updateWorkerReward.ts
  40. 49 49
      cli/src/helpers/display.ts
  41. 21 19
      cli/src/helpers/promptOptions.ts
  42. 14 14
      cli/src/helpers/validation.ts
  43. 1 1
      cli/src/index.ts
  44. 38 33
      cli/src/promptOptions/addWorkerOpening.ts
  45. 38 49
      cli/src/validators/common.ts
  46. 7 7
      cli/test/commands/council/info.test.ts
  47. 1 3
      cli/test/tsconfig.json
  48. 2 1
      cli/tsconfig.json

+ 6 - 6
.github/workflows/joystream-cli.yml

@@ -3,7 +3,7 @@ on: [pull_request, push]
 
 jobs:
   cli_build_ubuntu:
-    name: Ubuntu Build
+    name: Ubuntu Checks
     runs-on: ubuntu-latest
     strategy:
       matrix:
@@ -14,14 +14,14 @@ jobs:
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
-    - name: build
+    - name: checks
       run: |
         yarn install --frozen-lockfile
         yarn madge --circular types/
-        yarn workspace joystream-cli build
+        yarn workspace joystream-cli checks
 
   cli_build_osx:
-    name: MacOS Build
+    name: MacOS Checks
     runs-on: macos-latest
     strategy:
       matrix:
@@ -32,8 +32,8 @@ jobs:
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
-    - name: build
+    - name: checks
       run: |
         yarn install --frozen-lockfile --network-timeout 120000
         yarn madge --circular types/
-        yarn workspace joystream-cli build
+        yarn workspace joystream-cli checks

+ 7 - 0
cli/.eslintrc.js

@@ -1,4 +1,7 @@
 module.exports = {
+  env: {
+    mocha: true,
+  },
   extends: [
     // The oclif rules have some code-style/formatting rules which may conflict with
     // our prettier global settings. Disabling for now
@@ -7,4 +10,8 @@ module.exports = {
     // "oclif",
     // "oclif-typescript",
   ],
+  rules: {
+    "no-unused-vars": "off", // Required by the typescript rule below
+    "@typescript-eslint/no-unused-vars": ["error"]
+  }
 }

+ 1 - 1
cli/package.json

@@ -90,7 +90,7 @@
     "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
     "build": "tsc --build tsconfig.json",
     "version": "oclif-dev readme && git add README.md",
-    "lint": "eslint ./src/ --quiet --ext .ts",
+    "lint": "eslint ./ --quiet --ext .ts",
     "checks": "yarn lint && tsc --noEmit --pretty && prettier ./ --check",
     "format": "prettier ./ --write"
   },

+ 385 - 396
cli/src/Api.ts

@@ -1,480 +1,469 @@
-import BN from 'bn.js';
-import { registerJoystreamTypes } from '@joystream/types/';
-import { ApiPromise, WsProvider } from '@polkadot/api';
-import { QueryableStorageMultiArg } from '@polkadot/api/types';
-import { formatBalance } from '@polkadot/util';
-import { Hash, Balance } from '@polkadot/types/interfaces';
-import { KeyringPair } from '@polkadot/keyring/types';
-import { Codec } from '@polkadot/types/types';
-import { Option, Vec } from '@polkadot/types';
-import { u32 } from '@polkadot/types/primitive';
+import BN from 'bn.js'
+import { registerJoystreamTypes } from '@joystream/types/'
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { QueryableStorageMultiArg } from '@polkadot/api/types'
+import { formatBalance } from '@polkadot/util'
+import { Hash, Balance, Moment } from '@polkadot/types/interfaces'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { Codec } from '@polkadot/types/types'
+import { Option, Vec } from '@polkadot/types'
+import { u32 } from '@polkadot/types/primitive'
 import {
-    AccountSummary,
-    CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj,
-    WorkingGroups,
-    Reward,
-    GroupMember,
-    OpeningStatus,
-    GroupOpeningStage,
-    GroupOpening,
-    GroupApplication
-} from './Types';
-import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
-import { CLIError } from '@oclif/errors';
-import ExitCodes from './ExitCodes';
+  AccountSummary,
+  CouncilInfoObj,
+  CouncilInfoTuple,
+  createCouncilInfoObj,
+  WorkingGroups,
+  Reward,
+  GroupMember,
+  OpeningStatus,
+  GroupOpeningStage,
+  GroupOpening,
+  GroupApplication,
+} from './Types'
+import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types'
+import { CLIError } from '@oclif/errors'
+import ExitCodes from './ExitCodes'
 import {
-    Worker, WorkerId,
-    RoleStakeProfile,
-    Opening as WGOpening,
-    Application as WGApplication
-} from '@joystream/types/working-group';
+  Worker,
+  WorkerId,
+  RoleStakeProfile,
+  Opening as WGOpening,
+  Application as WGApplication,
+} from '@joystream/types/working-group'
 import {
-    Opening,
-    Application,
-    OpeningStage,
-    ApplicationStageKeys,
-    ApplicationId,
-    OpeningId
-} from '@joystream/types/hiring';
-import { MemberId, Profile } from '@joystream/types/members';
-import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
-import { Stake, StakeId } from '@joystream/types/stake';
-import { LinkageResult } from '@polkadot/types/codec/Linkage';
-import { Moment } from '@polkadot/types/interfaces';
-import { InputValidationLengthConstraint } from '@joystream/types/common';
-
-export const DEFAULT_API_URI = 'wss://rome-rpc-endpoint.joystream.org:9944/';
-const DEFAULT_DECIMALS = new u32(12);
+  Opening,
+  Application,
+  OpeningStage,
+  ApplicationStageKeys,
+  ApplicationId,
+  OpeningId,
+} from '@joystream/types/hiring'
+import { MemberId, Profile } from '@joystream/types/members'
+import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards'
+import { Stake, StakeId } from '@joystream/types/stake'
+import { LinkageResult } from '@polkadot/types/codec/Linkage'
+
+import { InputValidationLengthConstraint } from '@joystream/types/common'
+
+export const DEFAULT_API_URI = 'wss://rome-rpc-endpoint.joystream.org:9944/'
+const DEFAULT_DECIMALS = new u32(12)
 
 // Mapping of working group to api module
 export const apiModuleByGroup: { [key in WorkingGroups]: string } = {
-    [WorkingGroups.StorageProviders]: 'storageWorkingGroup'
-};
+  [WorkingGroups.StorageProviders]: 'storageWorkingGroup',
+}
 
 // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
 export default class Api {
-    private _api: ApiPromise;
+  private _api: ApiPromise
 
-    private constructor(originalApi: ApiPromise) {
-        this._api = originalApi;
-    }
+  private constructor(originalApi: ApiPromise) {
+    this._api = originalApi
+  }
 
-    public getOriginalApi(): ApiPromise {
-        return this._api;
-    }
+  public getOriginalApi(): ApiPromise {
+    return this._api
+  }
+
+  private static async initApi(apiUri: string = DEFAULT_API_URI): Promise<ApiPromise> {
+    const wsProvider: WsProvider = new WsProvider(apiUri)
+    registerJoystreamTypes()
+    const api = await ApiPromise.create({ provider: wsProvider })
+
+    // Initializing some api params based on pioneer/packages/react-api/Api.tsx
+    const [properties] = await Promise.all([api.rpc.system.properties()])
+
+    const tokenSymbol = properties.tokenSymbol.unwrapOr('DEV').toString()
+    const tokenDecimals = properties.tokenDecimals.unwrapOr(DEFAULT_DECIMALS).toNumber()
+
+    // formatBlanace config
+    formatBalance.setDefaults({
+      decimals: tokenDecimals,
+      unit: tokenSymbol,
+    })
 
-    private static async initApi(apiUri: string = DEFAULT_API_URI): Promise<ApiPromise> {
-        const wsProvider: WsProvider = new WsProvider(apiUri);
-        registerJoystreamTypes();
-        const api = await ApiPromise.create({ provider: wsProvider });
+    return api
+  }
 
-        // Initializing some api params based on pioneer/packages/react-api/Api.tsx
-        const [properties] = await Promise.all([
-            api.rpc.system.properties()
-        ]);
+  static async create(apiUri: string = DEFAULT_API_URI): Promise<Api> {
+    const originalApi: ApiPromise = await Api.initApi(apiUri)
+    return new Api(originalApi)
+  }
 
-        const tokenSymbol = properties.tokenSymbol.unwrapOr('DEV').toString();
-        const tokenDecimals = properties.tokenDecimals.unwrapOr(DEFAULT_DECIMALS).toNumber();
+  private async queryMultiOnce(queries: Parameters<typeof ApiPromise.prototype.queryMulti>[0]): Promise<Codec[]> {
+    let results: Codec[] = []
 
-        // formatBlanace config
-        formatBalance.setDefaults({
-            decimals: tokenDecimals,
-            unit: tokenSymbol
-        });
+    const unsub = await this._api.queryMulti(queries, (res) => {
+      results = res
+    })
+    unsub()
 
-        return api;
+    if (!results.length || results.length !== queries.length) {
+      throw new CLIError('API querying issue', { exit: ExitCodes.ApiError })
     }
 
-    static async create(apiUri: string = DEFAULT_API_URI): Promise<Api> {
-        const originalApi: ApiPromise = await Api.initApi(apiUri);
-        return new Api(originalApi);
-    }
+    return results
+  }
+
+  async getAccountsBalancesInfo(accountAddresses: string[]): Promise<DerivedBalances[]> {
+    const accountsBalances: DerivedBalances[] = await this._api.derive.balances.votingBalances(accountAddresses)
 
-    private async queryMultiOnce(queries: Parameters<typeof ApiPromise.prototype.queryMulti>[0]): Promise<Codec[]> {
-        let results: Codec[] = [];
+    return accountsBalances
+  }
 
-        const unsub = await this._api.queryMulti(
-            queries,
-            (res) => { results = res }
-        );
-        unsub();
+  // Get on-chain data related to given account.
+  // For now it's just account balances
+  async getAccountSummary(accountAddresses: string): Promise<AccountSummary> {
+    const balances: DerivedBalances = (await this.getAccountsBalancesInfo([accountAddresses]))[0]
+    // TODO: Some more information can be fetched here in the future
 
-        if (!results.length || results.length !== queries.length) {
-            throw new CLIError('API querying issue', { exit: ExitCodes.ApiError });
-        }
+    return { balances }
+  }
 
-        return results;
+  async getCouncilInfo(): Promise<CouncilInfoObj> {
+    const queries: { [P in keyof CouncilInfoObj]: QueryableStorageMultiArg<'promise'> } = {
+      activeCouncil: this._api.query.council.activeCouncil,
+      termEndsAt: this._api.query.council.termEndsAt,
+      autoStart: this._api.query.councilElection.autoStart,
+      newTermDuration: this._api.query.councilElection.newTermDuration,
+      candidacyLimit: this._api.query.councilElection.candidacyLimit,
+      councilSize: this._api.query.councilElection.councilSize,
+      minCouncilStake: this._api.query.councilElection.minCouncilStake,
+      minVotingStake: this._api.query.councilElection.minVotingStake,
+      announcingPeriod: this._api.query.councilElection.announcingPeriod,
+      votingPeriod: this._api.query.councilElection.votingPeriod,
+      revealingPeriod: this._api.query.councilElection.revealingPeriod,
+      round: this._api.query.councilElection.round,
+      stage: this._api.query.councilElection.stage,
     }
+    const results: CouncilInfoTuple = (await this.queryMultiOnce(Object.values(queries))) as CouncilInfoTuple
 
-    async getAccountsBalancesInfo(accountAddresses: string[]): Promise<DerivedBalances[]> {
-        let accountsBalances: DerivedBalances[] = await this._api.derive.balances.votingBalances(accountAddresses);
+    return createCouncilInfoObj(...results)
+  }
 
-        return accountsBalances;
-    }
+  // TODO: This formula is probably not too good, so some better implementation will be required in the future
+  async estimateFee(account: KeyringPair, recipientAddr: string, amount: BN): Promise<BN> {
+    const transfer = this._api.tx.balances.transfer(recipientAddr, amount)
+    const signature = account.sign(transfer.toU8a())
+    const transactionByteSize: BN = new BN(transfer.encodedLength + signature.length)
 
-    // Get on-chain data related to given account.
-    // For now it's just account balances
-    async getAccountSummary(accountAddresses: string): Promise<AccountSummary> {
-        const balances: DerivedBalances = (await this.getAccountsBalancesInfo([accountAddresses]))[0];
-        // TODO: Some more information can be fetched here in the future
+    const fees: DerivedFees = await this._api.derive.balances.fees()
 
-        return { balances };
-    }
+    const estimatedFee = fees.transactionBaseFee.add(fees.transactionByteFee.mul(transactionByteSize))
 
-    async getCouncilInfo(): Promise<CouncilInfoObj> {
-        const queries: { [P in keyof CouncilInfoObj]: QueryableStorageMultiArg<"promise"> } = {
-            activeCouncil: this._api.query.council.activeCouncil,
-            termEndsAt: this._api.query.council.termEndsAt,
-            autoStart: this._api.query.councilElection.autoStart,
-            newTermDuration: this._api.query.councilElection.newTermDuration,
-            candidacyLimit: this._api.query.councilElection.candidacyLimit,
-            councilSize: this._api.query.councilElection.councilSize,
-            minCouncilStake: this._api.query.councilElection.minCouncilStake,
-            minVotingStake: this._api.query.councilElection.minVotingStake,
-            announcingPeriod: this._api.query.councilElection.announcingPeriod,
-            votingPeriod: this._api.query.councilElection.votingPeriod,
-            revealingPeriod: this._api.query.councilElection.revealingPeriod,
-            round: this._api.query.councilElection.round,
-            stage: this._api.query.councilElection.stage
-        }
-        const results: CouncilInfoTuple = <CouncilInfoTuple>await this.queryMultiOnce(Object.values(queries));
-
-        return createCouncilInfoObj(...results);
-    }
+    return estimatedFee
+  }
 
-    // TODO: This formula is probably not too good, so some better implementation will be required in the future
-    async estimateFee(account: KeyringPair, recipientAddr: string, amount: BN): Promise<BN> {
-        const transfer = this._api.tx.balances.transfer(recipientAddr, amount);
-        const signature = account.sign(transfer.toU8a());
-        const transactionByteSize: BN = new BN(transfer.encodedLength + signature.length);
+  async transfer(account: KeyringPair, recipientAddr: string, amount: BN): Promise<Hash> {
+    const txHash = await this._api.tx.balances.transfer(recipientAddr, amount).signAndSend(account)
+    return txHash
+  }
 
-        const fees: DerivedFees = await this._api.derive.balances.fees();
+  // Working groups
+  // TODO: This is a lot of repeated logic from "/pioneer/joy-roles/src/transport.substrate.ts"
+  // (although simplified a little bit)
+  // Hopefully this will be refactored to "joystream-js" soon
+  protected singleLinkageResult<T extends Codec>(result: LinkageResult) {
+    return result[0] as T
+  }
 
-        const estimatedFee = fees.transactionBaseFee.add(fees.transactionByteFee.mul(transactionByteSize));
+  protected multiLinkageResult<K extends Codec, V extends Codec>(result: LinkageResult): [Vec<K>, Vec<V>] {
+    return [result[0] as Vec<K>, result[1] as Vec<V>]
+  }
 
-        return estimatedFee;
-    }
+  protected async blockHash(height: number): Promise<string> {
+    const blockHash = await this._api.rpc.chain.getBlockHash(height)
 
-    async transfer(account: KeyringPair, recipientAddr: string, amount: BN): Promise<Hash> {
-        const txHash = await this._api.tx.balances
-            .transfer(recipientAddr, amount)
-            .signAndSend(account);
-        return txHash;
-    }
+    return blockHash.toString()
+  }
 
-    // Working groups
-    // TODO: This is a lot of repeated logic from "/pioneer/joy-roles/src/transport.substrate.ts"
-    // (although simplified a little bit)
-    // Hopefully this will be refactored to "joystream-js" soon
-    protected singleLinkageResult<T extends Codec>(result: LinkageResult) {
-        return result[0] as T;
-    }
+  protected async blockTimestamp(height: number): Promise<Date> {
+    const blockTime = (await this._api.query.timestamp.now.at(await this.blockHash(height))) as Moment
 
-    protected multiLinkageResult<K extends Codec, V extends Codec>(result: LinkageResult): [Vec<K>, Vec<V>] {
-        return [result[0] as Vec<K>, result[1] as Vec<V>];
-    }
+    return new Date(blockTime.toNumber())
+  }
 
-    protected async blockHash(height: number): Promise<string> {
-        const blockHash = await this._api.rpc.chain.getBlockHash(height);
+  protected workingGroupApiQuery(group: WorkingGroups) {
+    const module = apiModuleByGroup[group]
+    return this._api.query[module]
+  }
 
-        return blockHash.toString();
-    }
+  protected async memberProfileById(memberId: MemberId): Promise<Profile | null> {
+    const profile = (await this._api.query.members.memberProfile(memberId)) as Option<Profile>
 
-    protected async blockTimestamp(height: number): Promise<Date> {
-        const blockTime = (await this._api.query.timestamp.now.at(await this.blockHash(height))) as Moment;
+    return profile.unwrapOr(null)
+  }
 
-        return new Date(blockTime.toNumber());
-    }
+  async groupLead(group: WorkingGroups): Promise<GroupMember | null> {
+    const optLeadId = (await this.workingGroupApiQuery(group).currentLead()) as Option<WorkerId>
 
-    protected workingGroupApiQuery(group: WorkingGroups) {
-        const module = apiModuleByGroup[group];
-        return this._api.query[module];
+    if (!optLeadId.isSome) {
+      return null
     }
 
-    protected async memberProfileById(memberId: MemberId): Promise<Profile | null> {
-        const profile = await this._api.query.members.memberProfile(memberId) as Option<Profile>;
+    const leadWorkerId = optLeadId.unwrap()
+    const leadWorker = await this.workerByWorkerId(group, leadWorkerId.toNumber())
 
-        return profile.unwrapOr(null);
-    }
+    return await this.parseGroupMember(leadWorkerId, leadWorker)
+  }
 
-    async groupLead(group: WorkingGroups): Promise<GroupMember | null> {
-        const optLeadId = (await this.workingGroupApiQuery(group).currentLead()) as Option<WorkerId>;
+  protected async stakeValue(stakeId: StakeId): Promise<Balance> {
+    const stake = this.singleLinkageResult<Stake>((await this._api.query.stake.stakes(stakeId)) as LinkageResult)
+    return stake.value
+  }
 
-        if (!optLeadId.isSome) {
-            return null;
-        }
+  protected async workerStake(stakeProfile: RoleStakeProfile): Promise<Balance> {
+    return this.stakeValue(stakeProfile.stake_id)
+  }
 
-        const leadWorkerId = optLeadId.unwrap();
-        const leadWorker = await this.workerByWorkerId(group, leadWorkerId.toNumber());
+  protected async workerReward(relationshipId: RewardRelationshipId): Promise<Reward> {
+    const rewardRelationship = this.singleLinkageResult<RewardRelationship>(
+      (await this._api.query.recurringRewards.rewardRelationships(relationshipId)) as LinkageResult
+    )
 
-        return await this.parseGroupMember(leadWorkerId, leadWorker);
+    return {
+      totalRecieved: rewardRelationship.total_reward_received,
+      value: rewardRelationship.amount_per_payout,
+      interval: rewardRelationship.payout_interval.unwrapOr(undefined)?.toNumber(),
+      nextPaymentBlock: rewardRelationship.next_payment_at_block.unwrapOr(new BN(0)).toNumber(),
     }
+  }
 
-    protected async stakeValue(stakeId: StakeId): Promise<Balance> {
-        const stake = this.singleLinkageResult<Stake>(
-            await this._api.query.stake.stakes(stakeId) as LinkageResult
-        );
-        return stake.value;
-    }
+  protected async parseGroupMember(id: WorkerId, worker: Worker): Promise<GroupMember> {
+    const roleAccount = worker.role_account_id
+    const memberId = worker.member_id
 
-    protected async workerStake (stakeProfile: RoleStakeProfile): Promise<Balance> {
-        return this.stakeValue(stakeProfile.stake_id);
-    }
+    const profile = await this.memberProfileById(memberId)
 
-    protected async workerReward(relationshipId: RewardRelationshipId): Promise<Reward> {
-        const rewardRelationship = this.singleLinkageResult<RewardRelationship>(
-            await this._api.query.recurringRewards.rewardRelationships(relationshipId) as LinkageResult
-        );
-
-        return {
-            totalRecieved: rewardRelationship.total_reward_received,
-            value: rewardRelationship.amount_per_payout,
-            interval: rewardRelationship.payout_interval.unwrapOr(undefined)?.toNumber(),
-            nextPaymentBlock: rewardRelationship.next_payment_at_block.unwrapOr(new BN(0)).toNumber()
-        };
+    if (!profile) {
+      throw new Error(`Group member profile not found! (member id: ${memberId.toNumber()})`)
     }
 
-    protected async parseGroupMember(
-        id: WorkerId,
-        worker: Worker
-    ): Promise<GroupMember> {
-        const roleAccount = worker.role_account_id;
-        const memberId = worker.member_id;
-
-        const profile = await this.memberProfileById(memberId);
-
-        if (!profile) {
-            throw new Error(`Group member profile not found! (member id: ${memberId.toNumber()})`);
-        }
-
-        let stake: Balance | undefined;
-        if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
-            stake = await this.workerStake(worker.role_stake_profile.unwrap());
-        }
-
-        let reward: Reward | undefined;
-        if (worker.reward_relationship && worker.reward_relationship.isSome) {
-            reward = await this.workerReward(worker.reward_relationship.unwrap());
-        }
-
-        return ({
-            workerId: id,
-            roleAccount,
-            memberId,
-            profile,
-            stake,
-            reward
-        });
+    let stake: Balance | undefined
+    if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
+      stake = await this.workerStake(worker.role_stake_profile.unwrap())
     }
 
-    async workerByWorkerId(group: WorkingGroups, workerId: number): Promise<Worker> {
-        const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId;
-
-        // This is chain specfic, but if next id is still 0, it means no workers have been added yet
-        if (workerId < 0 || workerId >= nextId.toNumber()) {
-            throw new CLIError('Invalid worker id!');
-        }
-
-        const worker = this.singleLinkageResult<Worker>(
-            (await this.workingGroupApiQuery(group).workerById(workerId)) as LinkageResult
-        );
-
-        if (!worker.is_active) {
-            throw new CLIError('This worker is not active anymore');
-        }
-
-        return worker;
+    let reward: Reward | undefined
+    if (worker.reward_relationship && worker.reward_relationship.isSome) {
+      reward = await this.workerReward(worker.reward_relationship.unwrap())
     }
 
-    async groupMember(group: WorkingGroups, workerId: number) {
-        const worker = await this.workerByWorkerId(group, workerId);
-        return await this.parseGroupMember(new WorkerId(workerId), worker);
+    return {
+      workerId: id,
+      roleAccount,
+      memberId,
+      profile,
+      stake,
+      reward,
     }
+  }
 
-    async groupMembers(group: WorkingGroups): Promise<GroupMember[]> {
-        const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId;
+  async workerByWorkerId(group: WorkingGroups, workerId: number): Promise<Worker> {
+    const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId
 
-        // This is chain specfic, but if next id is still 0, it means no workers have been added yet
-        if (nextId.eq(0)) {
-            return [];
-        }
-
-        const [workerIds, workers] = this.multiLinkageResult<WorkerId, Worker>(
-            (await this.workingGroupApiQuery(group).workerById()) as LinkageResult
-        );
+    // This is chain specfic, but if next id is still 0, it means no workers have been added yet
+    if (workerId < 0 || workerId >= nextId.toNumber()) {
+      throw new CLIError('Invalid worker id!')
+    }
 
-        let groupMembers: GroupMember[] = [];
-        for (let [index, worker] of Object.entries(workers.toArray())) {
-            const workerId = workerIds[parseInt(index)];
-            if (worker.is_active) {
-                groupMembers.push(await this.parseGroupMember(workerId, worker));
-            }
-        }
+    const worker = this.singleLinkageResult<Worker>(
+      (await this.workingGroupApiQuery(group).workerById(workerId)) as LinkageResult
+    )
 
-        return groupMembers.reverse();
+    if (!worker.is_active) {
+      throw new CLIError('This worker is not active anymore')
     }
 
-    async openingsByGroup(group: WorkingGroups): Promise<GroupOpening[]> {
-        const openings: GroupOpening[] = [];
-        const nextId = (await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId;
+    return worker
+  }
 
-        // This is chain specfic, but if next id is still 0, it means no openings have been added yet
-        if (!nextId.eq(0)) {
-            const highestId = nextId.toNumber() - 1;
-            for (let i = highestId; i >= 0; i--) {
-                openings.push(await this.groupOpening(group, i));
-            }
-        }
-
-        return openings;
-    }
+  async groupMember(group: WorkingGroups, workerId: number) {
+    const worker = await this.workerByWorkerId(group, workerId)
+    return await this.parseGroupMember(new WorkerId(workerId), worker)
+  }
 
-    protected async hiringOpeningById(id: number | OpeningId): Promise<Opening> {
-        const result = await this._api.query.hiring.openingById(id) as LinkageResult;
-        return this.singleLinkageResult<Opening>(result);
-    }
+  async groupMembers(group: WorkingGroups): Promise<GroupMember[]> {
+    const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId
 
-    protected async hiringApplicationById(id: number | ApplicationId): Promise<Application> {
-        const result = await this._api.query.hiring.applicationById(id) as LinkageResult;
-        return this.singleLinkageResult<Application>(result);
+    // This is chain specfic, but if next id is still 0, it means no workers have been added yet
+    if (nextId.eq(0)) {
+      return []
     }
 
-    async wgApplicationById(group: WorkingGroups, wgApplicationId: number): Promise<WGApplication> {
-        const nextAppId = await this.workingGroupApiQuery(group).nextApplicationId() as ApplicationId;
-
-        if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
-            throw new CLIError('Invalid working group application ID!');
-        }
+    const [workerIds, workers] = this.multiLinkageResult<WorkerId, Worker>(
+      (await this.workingGroupApiQuery(group).workerById()) as LinkageResult
+    )
 
-        return this.singleLinkageResult<WGApplication>(
-            await this.workingGroupApiQuery(group).applicationById(wgApplicationId) as LinkageResult
-        );
+    const groupMembers: GroupMember[] = []
+    for (const [index, worker] of Object.entries(workers.toArray())) {
+      const workerId = workerIds[parseInt(index)]
+      if (worker.is_active) {
+        groupMembers.push(await this.parseGroupMember(workerId, worker))
+      }
     }
 
-    protected async parseApplication(wgApplicationId: number, wgApplication: WGApplication): Promise<GroupApplication> {
-        const appId = wgApplication.application_id;
-        const application = await this.hiringApplicationById(appId);
-
-        const { active_role_staking_id: roleStakingId, active_application_staking_id: appStakingId } = application;
-
-        return {
-            wgApplicationId,
-            applicationId: appId.toNumber(),
-            wgOpeningId: wgApplication.opening_id.toNumber(),
-            member: await this.memberProfileById(wgApplication.member_id),
-            roleAccout: wgApplication.role_account_id,
-            stakes: {
-                application: appStakingId.isSome ? (await this.stakeValue(appStakingId.unwrap())).toNumber() : 0,
-                role: roleStakingId.isSome ? (await this.stakeValue(roleStakingId.unwrap())).toNumber() : 0
-            },
-            humanReadableText: application.human_readable_text.toString(),
-            stage: application.stage.type as ApplicationStageKeys
-        };
-    }
+    return groupMembers.reverse()
+  }
 
-    async groupApplication(group: WorkingGroups, wgApplicationId: number): Promise<GroupApplication> {
-        const wgApplication = await this.wgApplicationById(group, wgApplicationId);
-        return await this.parseApplication(wgApplicationId, wgApplication);
-    }
+  async openingsByGroup(group: WorkingGroups): Promise<GroupOpening[]> {
+    const openings: GroupOpening[] = []
+    const nextId = (await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId
 
-    protected async groupOpeningApplications(group: WorkingGroups, wgOpeningId: number): Promise<GroupApplication[]> {
-        const applications: GroupApplication[] = [];
+    // This is chain specfic, but if next id is still 0, it means no openings have been added yet
+    if (!nextId.eq(0)) {
+      const highestId = nextId.toNumber() - 1
+      for (let i = highestId; i >= 0; i--) {
+        openings.push(await this.groupOpening(group, i))
+      }
+    }
 
-        const nextAppId = await this.workingGroupApiQuery(group).nextApplicationId() as ApplicationId;
-        for (let i = 0; i < nextAppId.toNumber(); i++) {
-            const wgApplication = await this.wgApplicationById(group, i);
-            if (wgApplication.opening_id.toNumber() !== wgOpeningId) {
-                continue;
-            }
-            applications.push(await this.parseApplication(i, wgApplication));
-        }
+    return openings
+  }
 
+  protected async hiringOpeningById(id: number | OpeningId): Promise<Opening> {
+    const result = (await this._api.query.hiring.openingById(id)) as LinkageResult
+    return this.singleLinkageResult<Opening>(result)
+  }
 
-        return applications;
-    }
+  protected async hiringApplicationById(id: number | ApplicationId): Promise<Application> {
+    const result = (await this._api.query.hiring.applicationById(id)) as LinkageResult
+    return this.singleLinkageResult<Application>(result)
+  }
 
-    async groupOpening(group: WorkingGroups, wgOpeningId: number): Promise<GroupOpening> {
-        const nextId = ((await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId).toNumber();
-
-        if (wgOpeningId < 0 || wgOpeningId >= nextId) {
-            throw new CLIError('Invalid working group opening ID!');
-        }
-
-        const groupOpening = this.singleLinkageResult<WGOpening>(
-            await this.workingGroupApiQuery(group).openingById(wgOpeningId) as LinkageResult
-        );
-
-        const openingId = groupOpening.hiring_opening_id.toNumber();
-        const opening = await this.hiringOpeningById(openingId);
-        const applications = await this.groupOpeningApplications(group, wgOpeningId);
-        const stage = await this.parseOpeningStage(opening.stage);
-        const type = groupOpening.opening_type;
-        const stakes = {
-            application: opening.application_staking_policy.unwrapOr(undefined),
-            role: opening.role_staking_policy.unwrapOr(undefined)
-        }
-
-        return ({
-            wgOpeningId,
-            openingId,
-            opening,
-            stage,
-            stakes,
-            applications,
-            type
-        });
-    }
+  async wgApplicationById(group: WorkingGroups, wgApplicationId: number): Promise<WGApplication> {
+    const nextAppId = (await this.workingGroupApiQuery(group).nextApplicationId()) as ApplicationId
 
-    async parseOpeningStage(stage: OpeningStage): Promise<GroupOpeningStage> {
-        let
-            status: OpeningStatus | undefined,
-            stageBlock: number | undefined,
-            stageDate: Date | undefined;
-
-        if (stage.isOfType('WaitingToBegin')) {
-            const stageData = stage.asType('WaitingToBegin');
-            const currentBlockNumber = (await this._api.derive.chain.bestNumber()).toNumber();
-            const expectedBlockTime = (this._api.consts.babe.expectedBlockTime as Moment).toNumber();
-            status = OpeningStatus.WaitingToBegin;
-            stageBlock = stageData.begins_at_block.toNumber();
-            stageDate = new Date(Date.now() + (stageBlock - currentBlockNumber) * expectedBlockTime);
-        }
-
-        if (stage.isOfType('Active')) {
-            const stageData = stage.asType('Active');
-            const substage = stageData.stage;
-            if (substage.isOfType('AcceptingApplications')) {
-                status = OpeningStatus.AcceptingApplications;
-                stageBlock = substage.asType('AcceptingApplications').started_accepting_applicants_at_block.toNumber();
-            }
-            if (substage.isOfType('ReviewPeriod')) {
-                status = OpeningStatus.InReview;
-                stageBlock = substage.asType('ReviewPeriod').started_review_period_at_block.toNumber();
-            }
-            if (substage.isOfType('Deactivated')) {
-                status = substage.asType('Deactivated').cause.isOfType('Filled')
-                    ? OpeningStatus.Complete
-                    : OpeningStatus.Cancelled;
-                stageBlock = substage.asType('Deactivated').deactivated_at_block.toNumber();
-            }
-            if (stageBlock) {
-                stageDate = new Date(await this.blockTimestamp(stageBlock));
-            }
-        }
-
-        return {
-            status: status || OpeningStatus.Unknown,
-            block: stageBlock,
-            date: stageDate
-        };
+    if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
+      throw new CLIError('Invalid working group application ID!')
     }
 
-    async getMemberIdsByControllerAccount(address: string): Promise<MemberId[]> {
-        const ids = await this._api.query.members.memberIdsByControllerAccountId(address) as Vec<MemberId>;
-        return ids.toArray();
-    }
+    return this.singleLinkageResult<WGApplication>(
+      (await this.workingGroupApiQuery(group).applicationById(wgApplicationId)) as LinkageResult
+    )
+  }
 
-    async workerExitRationaleConstraint(group: WorkingGroups): Promise<InputValidationLengthConstraint> {
-        return await this.workingGroupApiQuery(group).workerExitRationaleText() as InputValidationLengthConstraint;
+  protected async parseApplication(wgApplicationId: number, wgApplication: WGApplication): Promise<GroupApplication> {
+    const appId = wgApplication.application_id
+    const application = await this.hiringApplicationById(appId)
+
+    const { active_role_staking_id: roleStakingId, active_application_staking_id: appStakingId } = application
+
+    return {
+      wgApplicationId,
+      applicationId: appId.toNumber(),
+      wgOpeningId: wgApplication.opening_id.toNumber(),
+      member: await this.memberProfileById(wgApplication.member_id),
+      roleAccout: wgApplication.role_account_id,
+      stakes: {
+        application: appStakingId.isSome ? (await this.stakeValue(appStakingId.unwrap())).toNumber() : 0,
+        role: roleStakingId.isSome ? (await this.stakeValue(roleStakingId.unwrap())).toNumber() : 0,
+      },
+      humanReadableText: application.human_readable_text.toString(),
+      stage: application.stage.type as ApplicationStageKeys,
     }
+  }
+
+  async groupApplication(group: WorkingGroups, wgApplicationId: number): Promise<GroupApplication> {
+    const wgApplication = await this.wgApplicationById(group, wgApplicationId)
+    return await this.parseApplication(wgApplicationId, wgApplication)
+  }
+
+  protected async groupOpeningApplications(group: WorkingGroups, wgOpeningId: number): Promise<GroupApplication[]> {
+    const applications: GroupApplication[] = []
+
+    const nextAppId = (await this.workingGroupApiQuery(group).nextApplicationId()) as ApplicationId
+    for (let i = 0; i < nextAppId.toNumber(); i++) {
+      const wgApplication = await this.wgApplicationById(group, i)
+      if (wgApplication.opening_id.toNumber() !== wgOpeningId) {
+        continue
+      }
+      applications.push(await this.parseApplication(i, wgApplication))
+    }
+
+    return applications
+  }
+
+  async groupOpening(group: WorkingGroups, wgOpeningId: number): Promise<GroupOpening> {
+    const nextId = ((await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId).toNumber()
+
+    if (wgOpeningId < 0 || wgOpeningId >= nextId) {
+      throw new CLIError('Invalid working group opening ID!')
+    }
+
+    const groupOpening = this.singleLinkageResult<WGOpening>(
+      (await this.workingGroupApiQuery(group).openingById(wgOpeningId)) as LinkageResult
+    )
+
+    const openingId = groupOpening.hiring_opening_id.toNumber()
+    const opening = await this.hiringOpeningById(openingId)
+    const applications = await this.groupOpeningApplications(group, wgOpeningId)
+    const stage = await this.parseOpeningStage(opening.stage)
+    const type = groupOpening.opening_type
+    const stakes = {
+      application: opening.application_staking_policy.unwrapOr(undefined),
+      role: opening.role_staking_policy.unwrapOr(undefined),
+    }
+
+    return {
+      wgOpeningId,
+      openingId,
+      opening,
+      stage,
+      stakes,
+      applications,
+      type,
+    }
+  }
+
+  async parseOpeningStage(stage: OpeningStage): Promise<GroupOpeningStage> {
+    let status: OpeningStatus | undefined, stageBlock: number | undefined, stageDate: Date | undefined
+
+    if (stage.isOfType('WaitingToBegin')) {
+      const stageData = stage.asType('WaitingToBegin')
+      const currentBlockNumber = (await this._api.derive.chain.bestNumber()).toNumber()
+      const expectedBlockTime = (this._api.consts.babe.expectedBlockTime as Moment).toNumber()
+      status = OpeningStatus.WaitingToBegin
+      stageBlock = stageData.begins_at_block.toNumber()
+      stageDate = new Date(Date.now() + (stageBlock - currentBlockNumber) * expectedBlockTime)
+    }
+
+    if (stage.isOfType('Active')) {
+      const stageData = stage.asType('Active')
+      const substage = stageData.stage
+      if (substage.isOfType('AcceptingApplications')) {
+        status = OpeningStatus.AcceptingApplications
+        stageBlock = substage.asType('AcceptingApplications').started_accepting_applicants_at_block.toNumber()
+      }
+      if (substage.isOfType('ReviewPeriod')) {
+        status = OpeningStatus.InReview
+        stageBlock = substage.asType('ReviewPeriod').started_review_period_at_block.toNumber()
+      }
+      if (substage.isOfType('Deactivated')) {
+        status = substage.asType('Deactivated').cause.isOfType('Filled')
+          ? OpeningStatus.Complete
+          : OpeningStatus.Cancelled
+        stageBlock = substage.asType('Deactivated').deactivated_at_block.toNumber()
+      }
+      if (stageBlock) {
+        stageDate = new Date(await this.blockTimestamp(stageBlock))
+      }
+    }
+
+    return {
+      status: status || OpeningStatus.Unknown,
+      block: stageBlock,
+      date: stageDate,
+    }
+  }
+
+  async getMemberIdsByControllerAccount(address: string): Promise<MemberId[]> {
+    const ids = (await this._api.query.members.memberIdsByControllerAccountId(address)) as Vec<MemberId>
+    return ids.toArray()
+  }
+
+  async workerExitRationaleConstraint(group: WorkingGroups): Promise<InputValidationLengthConstraint> {
+    return (await this.workingGroupApiQuery(group).workerExitRationaleText()) as InputValidationLengthConstraint
+  }
 }

+ 11 - 11
cli/src/ExitCodes.ts

@@ -1,15 +1,15 @@
 enum ExitCodes {
-    OK = 0,
+  OK = 0,
 
-    InvalidInput = 400,
-    FileNotFound = 401,
-    InvalidFile = 402,
-    NoAccountFound = 403,
-    NoAccountSelected = 404,
-    AccessDenied = 405,
+  InvalidInput = 400,
+  FileNotFound = 401,
+  InvalidFile = 402,
+  NoAccountFound = 403,
+  NoAccountSelected = 404,
+  AccessDenied = 405,
 
-    UnexpectedException = 500,
-    FsOperationFailed = 501,
-    ApiError = 502,
+  UnexpectedException = 500,
+  FsOperationFailed = 501,
+  ApiError = 502,
 }
-export = ExitCodes;
+export = ExitCodes

+ 290 - 267
cli/src/Types.ts

@@ -1,348 +1,371 @@
-import BN from 'bn.js';
-import { ElectionStage, Seat } from '@joystream/types/council';
-import { Option, Text } from '@polkadot/types';
-import { Constructor, Codec } from '@polkadot/types/types';
-import { Struct, Vec } from '@polkadot/types/codec';
-import { u32 } from '@polkadot/types/primitive';
-import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces';
-import { DerivedBalances } from '@polkadot/api-derive/types';
-import { KeyringPair } from '@polkadot/keyring/types';
-import { WorkerId, OpeningType } from '@joystream/types/working-group';
-import { Profile, MemberId } from '@joystream/types/members';
+import BN from 'bn.js'
+import { ElectionStage, Seat } from '@joystream/types/council'
+import { Option, Text } from '@polkadot/types'
+import { Constructor, Codec } from '@polkadot/types/types'
+import { Struct, Vec } from '@polkadot/types/codec'
+import { u32 } from '@polkadot/types/primitive'
+import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces'
+import { DerivedBalances } from '@polkadot/api-derive/types'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { WorkerId, OpeningType } from '@joystream/types/working-group'
+import { Profile, MemberId } from '@joystream/types/members'
 import {
-    GenericJoyStreamRoleSchema,
-    JobSpecifics,
-    ApplicationDetails,
-    QuestionSections,
-    QuestionSection,
-    QuestionsFields,
-    QuestionField,
-    EntryInMembershipModuke,
-    HiringProcess,
-    AdditionalRolehiringProcessDetails,
-    CreatorDetails
-} from '@joystream/types/hiring/schemas/role.schema.typings';
-import ajv from 'ajv';
-import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring';
-import { Validator } from 'inquirer';
+  GenericJoyStreamRoleSchema,
+  JobSpecifics,
+  ApplicationDetails,
+  QuestionSections,
+  QuestionSection,
+  QuestionsFields,
+  QuestionField,
+  EntryInMembershipModuke,
+  HiringProcess,
+  AdditionalRolehiringProcessDetails,
+  CreatorDetails,
+} from '@joystream/types/hiring/schemas/role.schema.typings'
+import ajv from 'ajv'
+import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring'
+import { Validator } from 'inquirer'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
 // If not provided in the account json file, the meta.name value is set to "Unnamed Account"
 export type NamedKeyringPair = KeyringPair & {
-    meta: {
-        name: string
-    }
+  meta: {
+    name: string
+  }
 }
 
 // Summary of the account information fetched from the api for "account:current" purposes (currently just balances)
 export type AccountSummary = {
-    balances: DerivedBalances
+  balances: DerivedBalances
 }
 
-// Object/Tuple containing council/councilElection information (council:info).
-// The tuple is useful, because that's how api.queryMulti returns the results.
-export type CouncilInfoTuple = Parameters<typeof createCouncilInfoObj>;
-export type CouncilInfoObj = ReturnType<typeof createCouncilInfoObj>;
 // This function allows us to easily transform the tuple into the object
-// and simplifies the creation of consitent Object and Tuple types (seen above).
+// and simplifies the creation of consitent Object and Tuple types (seen below).
 export function createCouncilInfoObj(
-    activeCouncil: Seat[],
-    termEndsAt: BlockNumber,
-    autoStart: Boolean,
-    newTermDuration: BN,
-    candidacyLimit: BN,
-    councilSize: BN,
-    minCouncilStake: Balance,
-    minVotingStake: Balance,
-    announcingPeriod: BlockNumber,
-    votingPeriod: BlockNumber,
-    revealingPeriod: BlockNumber,
-    round: BN,
-    stage: Option<ElectionStage>
+  activeCouncil: Seat[],
+  termEndsAt: BlockNumber,
+  autoStart: boolean,
+  newTermDuration: BN,
+  candidacyLimit: BN,
+  councilSize: BN,
+  minCouncilStake: Balance,
+  minVotingStake: Balance,
+  announcingPeriod: BlockNumber,
+  votingPeriod: BlockNumber,
+  revealingPeriod: BlockNumber,
+  round: BN,
+  stage: Option<ElectionStage>
 ) {
-    return {
-        activeCouncil,
-        termEndsAt,
-        autoStart,
-        newTermDuration,
-        candidacyLimit,
-        councilSize,
-        minCouncilStake,
-        minVotingStake,
-        announcingPeriod,
-        votingPeriod,
-        revealingPeriod,
-        round,
-        stage
-    };
+  return {
+    activeCouncil,
+    termEndsAt,
+    autoStart,
+    newTermDuration,
+    candidacyLimit,
+    councilSize,
+    minCouncilStake,
+    minVotingStake,
+    announcingPeriod,
+    votingPeriod,
+    revealingPeriod,
+    round,
+    stage,
+  }
 }
+// Object/Tuple containing council/councilElection information (council:info).
+// The tuple is useful, because that's how api.queryMulti returns the results.
+export type CouncilInfoTuple = Parameters<typeof createCouncilInfoObj>
+export type CouncilInfoObj = ReturnType<typeof createCouncilInfoObj>
 
 // Object with "name" and "value" properties, used for rendering simple CLI tables like:
 // Total balance:   100 JOY
 // Free calance:     50 JOY
-export type NameValueObj = { name: string, value: string };
+export type NameValueObj = { name: string; value: string }
 
 // Working groups related types
 export enum WorkingGroups {
-    StorageProviders = 'storageProviders'
+  StorageProviders = 'storageProviders',
 }
 
 // In contrast to Pioneer, currently only StorageProviders group is available in CLI
-export const AvailableGroups: readonly WorkingGroups[] = [
-  WorkingGroups.StorageProviders
-] as const;
+export const AvailableGroups: readonly WorkingGroups[] = [WorkingGroups.StorageProviders] as const
 
 export type Reward = {
-    totalRecieved: Balance;
-    value: Balance;
-    interval?: number;
-    nextPaymentBlock: number; // 0 = no incoming payment
+  totalRecieved: Balance
+  value: Balance
+  interval?: number
+  nextPaymentBlock: number // 0 = no incoming payment
 }
 
 // Compound working group types
 export type GroupMember = {
-    workerId: WorkerId;
-    memberId: MemberId;
-    roleAccount: AccountId;
-    profile: Profile;
-    stake?: Balance;
-    reward?: Reward;
+  workerId: WorkerId
+  memberId: MemberId
+  roleAccount: AccountId
+  profile: Profile
+  stake?: Balance
+  reward?: Reward
 }
 
 export type GroupApplication = {
-    wgApplicationId: number;
-    applicationId: number;
-    wgOpeningId: number;
-    member: Profile | null;
-    roleAccout: AccountId;
-    stakes: {
-        application: number;
-        role: number;
-    },
-    humanReadableText: string;
-    stage: ApplicationStageKeys;
+  wgApplicationId: number
+  applicationId: number
+  wgOpeningId: number
+  member: Profile | null
+  roleAccout: AccountId
+  stakes: {
+    application: number
+    role: number
+  }
+  humanReadableText: string
+  stage: ApplicationStageKeys
 }
 
 export enum OpeningStatus {
-    WaitingToBegin = 'WaitingToBegin',
-    AcceptingApplications = 'AcceptingApplications',
-    InReview = 'InReview',
-    Complete = 'Complete',
-    Cancelled = 'Cancelled',
-    Unknown = 'Unknown'
+  WaitingToBegin = 'WaitingToBegin',
+  AcceptingApplications = 'AcceptingApplications',
+  InReview = 'InReview',
+  Complete = 'Complete',
+  Cancelled = 'Cancelled',
+  Unknown = 'Unknown',
 }
 
 export type GroupOpeningStage = {
-    status: OpeningStatus;
-    block?: number;
-    date?: Date;
+  status: OpeningStatus
+  block?: number
+  date?: Date
 }
 
 export type GroupOpeningStakes = {
-    application?: StakingPolicy;
-    role?: StakingPolicy;
+  application?: StakingPolicy
+  role?: StakingPolicy
 }
 
 export type GroupOpening = {
-    wgOpeningId: number;
-    openingId: number;
-    stage: GroupOpeningStage;
-    opening: Opening;
-    stakes: GroupOpeningStakes;
-    applications: GroupApplication[];
-    type: OpeningType;
+  wgOpeningId: number
+  openingId: number
+  stage: GroupOpeningStage
+  opening: Opening
+  stakes: GroupOpeningStakes
+  applications: GroupApplication[]
+  type: OpeningType
 }
 
 // Some helper structs for generating human_readable_text in working group opening extrinsic
 // Note those types are not part of the runtime etc., we just use them to simplify prompting for values
 // (since there exists functionality that handles that for substrate types like: Struct, Vec etc.)
 interface WithJSONable<T> {
-    toJSON: () => T;
+  toJSON: () => T
 }
 export class HRTJobSpecificsStruct extends Struct implements WithJSONable<JobSpecifics> {
-    constructor (value?: JobSpecifics) {
-        super({
-          title: "Text",
-          description: "Text",
-        }, value);
-    }
-    get title(): string {
-        return (this.get('title') as Text).toString();
-    }
-    get description(): string {
-        return (this.get('description') as Text).toString();
-    }
-    toJSON(): JobSpecifics {
-        const { title, description } = this;
-        return { title, description };
-    }
+  constructor(value?: JobSpecifics) {
+    super(
+      {
+        title: 'Text',
+        description: 'Text',
+      },
+      value
+    )
+  }
+  get title(): string {
+    return (this.get('title') as Text).toString()
+  }
+  get description(): string {
+    return (this.get('description') as Text).toString()
+  }
+  toJSON(): JobSpecifics {
+    const { title, description } = this
+    return { title, description }
+  }
 }
 export class HRTEntryInMembershipModukeStruct extends Struct implements WithJSONable<EntryInMembershipModuke> {
-    constructor (value?: EntryInMembershipModuke) {
-        super({
-          handle: "Text",
-        }, value);
-    }
-    get handle(): string {
-        return (this.get('handle') as Text).toString();
-    }
-    toJSON(): EntryInMembershipModuke {
-        const { handle } = this;
-        return { handle };
-    }
+  constructor(value?: EntryInMembershipModuke) {
+    super(
+      {
+        handle: 'Text',
+      },
+      value
+    )
+  }
+  get handle(): string {
+    return (this.get('handle') as Text).toString()
+  }
+  toJSON(): EntryInMembershipModuke {
+    const { handle } = this
+    return { handle }
+  }
 }
 export class HRTCreatorDetailsStruct extends Struct implements WithJSONable<CreatorDetails> {
-    constructor (value?: CreatorDetails) {
-        super({
-          membership: HRTEntryInMembershipModukeStruct,
-        }, value);
-    }
-    get membership(): EntryInMembershipModuke {
-        return (this.get('membership') as HRTEntryInMembershipModukeStruct).toJSON();
-    }
-    toJSON(): CreatorDetails {
-        const { membership } = this;
-        return { membership };
-    }
+  constructor(value?: CreatorDetails) {
+    super(
+      {
+        membership: HRTEntryInMembershipModukeStruct,
+      },
+      value
+    )
+  }
+  get membership(): EntryInMembershipModuke {
+    return (this.get('membership') as HRTEntryInMembershipModukeStruct).toJSON()
+  }
+  toJSON(): CreatorDetails {
+    const { membership } = this
+    return { membership }
+  }
 }
 export class HRTHiringProcessStruct extends Struct implements WithJSONable<HiringProcess> {
-    constructor (value?: HiringProcess) {
-        super({
-          details: "Vec<Text>",
-        }, value);
-    }
-    get details(): AdditionalRolehiringProcessDetails {
-        return (this.get('details') as Vec<Text>).toArray().map(v => v.toString());
-    }
-    toJSON(): HiringProcess {
-        const { details } = this;
-        return { details };
-    }
+  constructor(value?: HiringProcess) {
+    super(
+      {
+        details: 'Vec<Text>',
+      },
+      value
+    )
+  }
+  get details(): AdditionalRolehiringProcessDetails {
+    return (this.get('details') as Vec<Text>).toArray().map((v) => v.toString())
+  }
+  toJSON(): HiringProcess {
+    const { details } = this
+    return { details }
+  }
 }
 export class HRTQuestionFieldStruct extends Struct implements WithJSONable<QuestionField> {
-    constructor (value?: QuestionField) {
-        super({
-            title: "Text",
-            type: "Text"
-        }, value);
-    }
-    get title(): string {
-        return (this.get('title') as Text).toString();
-    }
-    get type(): string {
-        return (this.get('type') as Text).toString();
-    }
-    toJSON(): QuestionField {
-        const { title, type } = this;
-        return { title, type };
-    }
+  constructor(value?: QuestionField) {
+    super(
+      {
+        title: 'Text',
+        type: 'Text',
+      },
+      value
+    )
+  }
+  get title(): string {
+    return (this.get('title') as Text).toString()
+  }
+  get type(): string {
+    return (this.get('type') as Text).toString()
+  }
+  toJSON(): QuestionField {
+    const { title, type } = this
+    return { title, type }
+  }
 }
 class HRTQuestionsFieldsVec extends Vec.with(HRTQuestionFieldStruct) implements WithJSONable<QuestionsFields> {
-    toJSON(): QuestionsFields {
-        return this.toArray().map(v => v.toJSON());
-    }
+  toJSON(): QuestionsFields {
+    return this.toArray().map((v) => v.toJSON())
+  }
 }
 export class HRTQuestionSectionStruct extends Struct implements WithJSONable<QuestionSection> {
-    constructor (value?: QuestionSection) {
-        super({
-            title: "Text",
-            questions: HRTQuestionsFieldsVec
-        }, value);
-    }
-    get title(): string {
-        return (this.get('title') as Text).toString();
-    }
-    get questions(): QuestionsFields {
-        return (this.get('questions') as HRTQuestionsFieldsVec).toJSON();
-    }
-    toJSON(): QuestionSection {
-        const { title, questions } = this;
-        return { title, questions };
-    }
+  constructor(value?: QuestionSection) {
+    super(
+      {
+        title: 'Text',
+        questions: HRTQuestionsFieldsVec,
+      },
+      value
+    )
+  }
+  get title(): string {
+    return (this.get('title') as Text).toString()
+  }
+  get questions(): QuestionsFields {
+    return (this.get('questions') as HRTQuestionsFieldsVec).toJSON()
+  }
+  toJSON(): QuestionSection {
+    const { title, questions } = this
+    return { title, questions }
+  }
+}
+export class HRTQuestionSectionsVec extends Vec.with(HRTQuestionSectionStruct)
+  implements WithJSONable<QuestionSections> {
+  toJSON(): QuestionSections {
+    return this.toArray().map((v) => v.toJSON())
+  }
 }
-export class HRTQuestionSectionsVec extends Vec.with(HRTQuestionSectionStruct) implements WithJSONable<QuestionSections> {
-    toJSON(): QuestionSections {
-        return this.toArray().map(v => v.toJSON());
-    }
-};
 export class HRTApplicationDetailsStruct extends Struct implements WithJSONable<ApplicationDetails> {
-    constructor (value?: ApplicationDetails) {
-        super({
-            sections: HRTQuestionSectionsVec
-        }, value);
-    }
-    get sections(): QuestionSections {
-        return (this.get('sections') as HRTQuestionSectionsVec).toJSON();
-    }
-    toJSON(): ApplicationDetails {
-        const { sections } = this;
-        return { sections };
-    }
+  constructor(value?: ApplicationDetails) {
+    super(
+      {
+        sections: HRTQuestionSectionsVec,
+      },
+      value
+    )
+  }
+  get sections(): QuestionSections {
+    return (this.get('sections') as HRTQuestionSectionsVec).toJSON()
+  }
+  toJSON(): ApplicationDetails {
+    const { sections } = this
+    return { sections }
+  }
 }
 export class HRTStruct extends Struct implements WithJSONable<GenericJoyStreamRoleSchema> {
-    constructor (value?: GenericJoyStreamRoleSchema) {
-        super({
-            version: "u32",
-            headline: "Text",
-            job: HRTJobSpecificsStruct,
-            application: HRTApplicationDetailsStruct,
-            reward: "Text",
-            creator: HRTCreatorDetailsStruct,
-            process: HRTHiringProcessStruct
-        }, value);
-    }
-    get version(): number {
-        return (this.get('version') as u32).toNumber();
-    }
-    get headline(): string {
-        return (this.get('headline') as Text).toString();
-    }
-    get job(): JobSpecifics {
-        return (this.get('job') as HRTJobSpecificsStruct).toJSON();
-    }
-    get application(): ApplicationDetails {
-        return (this.get('application') as HRTApplicationDetailsStruct).toJSON();
-    }
-    get reward(): string {
-        return (this.get('reward') as Text).toString();
-    }
-    get creator(): CreatorDetails {
-        return (this.get('creator') as HRTCreatorDetailsStruct).toJSON();
-    }
-    get process(): HiringProcess {
-        return (this.get('process') as HRTHiringProcessStruct).toJSON();
-    }
-    toJSON(): GenericJoyStreamRoleSchema {
-        const { version, headline, job, application, reward, creator, process } = this;
-        return { version, headline, job, application, reward, creator, process };
-    }
-};
+  constructor(value?: GenericJoyStreamRoleSchema) {
+    super(
+      {
+        version: 'u32',
+        headline: 'Text',
+        job: HRTJobSpecificsStruct,
+        application: HRTApplicationDetailsStruct,
+        reward: 'Text',
+        creator: HRTCreatorDetailsStruct,
+        process: HRTHiringProcessStruct,
+      },
+      value
+    )
+  }
+  get version(): number {
+    return (this.get('version') as u32).toNumber()
+  }
+  get headline(): string {
+    return (this.get('headline') as Text).toString()
+  }
+  get job(): JobSpecifics {
+    return (this.get('job') as HRTJobSpecificsStruct).toJSON()
+  }
+  get application(): ApplicationDetails {
+    return (this.get('application') as HRTApplicationDetailsStruct).toJSON()
+  }
+  get reward(): string {
+    return (this.get('reward') as Text).toString()
+  }
+  get creator(): CreatorDetails {
+    return (this.get('creator') as HRTCreatorDetailsStruct).toJSON()
+  }
+  get process(): HiringProcess {
+    return (this.get('process') as HRTHiringProcessStruct).toJSON()
+  }
+  toJSON(): GenericJoyStreamRoleSchema {
+    const { version, headline, job, application, reward, creator, process } = this
+    return { version, headline, job, application, reward, creator, process }
+  }
+}
 
 // Api-related
 
 // Additional options that can be passed to ApiCommandBase.promptForParam in order to override
 // its default behaviour, change param name, add validation etc.
 export type ApiParamOptions<ParamType = Codec> = {
-    forcedName?: string,
-    value?: {
-        default: ParamType;
-        locked?: boolean;
-    }
-    jsonSchema?: {
-        struct: Constructor<Struct>;
-        schemaValidator: ajv.ValidateFunction;
-    }
-    validator?: Validator,
-    nestedOptions?: ApiParamsOptions // For more complex params, like structs
-};
+  forcedName?: string
+  value?: {
+    default: ParamType
+    locked?: boolean
+  }
+  jsonSchema?: {
+    struct: Constructor<Struct>
+    schemaValidator: ajv.ValidateFunction
+  }
+  validator?: Validator
+  nestedOptions?: ApiParamsOptions // For more complex params, like structs
+}
 export type ApiParamsOptions = {
-    [paramName: string]: ApiParamOptions;
+  [paramName: string]: ApiParamOptions
 }
 
-export type ApiMethodArg = Codec;
+export type ApiMethodArg = Codec
 export type ApiMethodNamedArg = {
-    name: string;
-    value: ApiMethodArg;
-};
-export type ApiMethodNamedArgs = ApiMethodNamedArg[];
+  name: string
+  value: ApiMethodArg
+}
+export type ApiMethodNamedArgs = ApiMethodNamedArg[]

+ 213 - 224
cli/src/base/AccountsCommandBase.ts

@@ -1,18 +1,18 @@
-import fs from 'fs';
-import path from 'path';
-import slug from 'slug';
-import inquirer from 'inquirer';
-import ExitCodes from '../ExitCodes';
-import { CLIError } from '@oclif/errors';
-import ApiCommandBase from './ApiCommandBase';
-import { Keyring } from '@polkadot/api';
-import { formatBalance } from '@polkadot/util';
-import { NamedKeyringPair } from '../Types';
-import { DerivedBalances } from '@polkadot/api-derive/types';
-import { toFixedLength } from '../helpers/display';
-
-const ACCOUNTS_DIRNAME = 'accounts';
-const SPECIAL_ACCOUNT_POSTFIX = '__DEV';
+import fs from 'fs'
+import path from 'path'
+import slug from 'slug'
+import inquirer from 'inquirer'
+import ExitCodes from '../ExitCodes'
+import { CLIError } from '@oclif/errors'
+import ApiCommandBase from './ApiCommandBase'
+import { Keyring } from '@polkadot/api'
+import { formatBalance } from '@polkadot/util'
+import { NamedKeyringPair } from '../Types'
+import { DerivedBalances } from '@polkadot/api-derive/types'
+import { toFixedLength } from '../helpers/display'
+
+const ACCOUNTS_DIRNAME = 'accounts'
+const SPECIAL_ACCOUNT_POSTFIX = '__DEV'
 
 /**
  * Abstract base class for account-related commands.
@@ -22,216 +22,205 @@ const SPECIAL_ACCOUNT_POSTFIX = '__DEV';
  * Where: APP_DATA_PATH is provided by StateAwareCommandBase and ACCOUNTS_DIRNAME is a const (see above).
  */
 export default abstract class AccountsCommandBase extends ApiCommandBase {
-    getAccountsDirPath(): string {
-        return path.join(this.getAppDataPath(), ACCOUNTS_DIRNAME);
+  getAccountsDirPath(): string {
+    return path.join(this.getAppDataPath(), ACCOUNTS_DIRNAME)
+  }
+
+  getAccountFilePath(account: NamedKeyringPair, isSpecial = false): string {
+    return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account, isSpecial))
+  }
+
+  generateAccountFilename(account: NamedKeyringPair, isSpecial = false): string {
+    return `${slug(account.meta.name, '_')}__${account.address}${isSpecial ? SPECIAL_ACCOUNT_POSTFIX : ''}.json`
+  }
+
+  private initAccountsFs(): void {
+    if (!fs.existsSync(this.getAccountsDirPath())) {
+      fs.mkdirSync(this.getAccountsDirPath())
+    }
+  }
+
+  saveAccount(account: NamedKeyringPair, password: string, isSpecial = false): void {
+    try {
+      const destPath = this.getAccountFilePath(account, isSpecial)
+      fs.writeFileSync(destPath, JSON.stringify(account.toJson(password)))
+    } catch (e) {
+      throw this.createDataWriteError()
+    }
+  }
+
+  // Add dev "Alice" and "Bob" accounts
+  initSpecialAccounts() {
+    const keyring = new Keyring({ type: 'sr25519' })
+    keyring.addFromUri('//Alice', { name: 'Alice' })
+    keyring.addFromUri('//Bob', { name: 'Bob' })
+    keyring.getPairs().forEach((pair) => this.saveAccount({ ...pair, meta: { name: pair.meta.name } }, '', true))
+  }
+
+  fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair {
+    if (!fs.existsSync(jsonBackupFilePath)) {
+      throw new CLIError('Input file does not exist!', { exit: ExitCodes.FileNotFound })
     }
-
-    getAccountFilePath(account: NamedKeyringPair, isSpecial: boolean = false): string {
-        return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account, isSpecial));
-    }
-
-    generateAccountFilename(account: NamedKeyringPair, isSpecial: boolean = false): string {
-        return `${ slug(account.meta.name, '_') }__${ account.address }${ isSpecial ? SPECIAL_ACCOUNT_POSTFIX : '' }.json`;
-    }
-
-    private initAccountsFs(): void {
-        if (!fs.existsSync(this.getAccountsDirPath())) {
-            fs.mkdirSync(this.getAccountsDirPath());
-        }
+    if (path.extname(jsonBackupFilePath) !== '.json') {
+      throw new CLIError('Invalid input file: File extension should be .json', { exit: ExitCodes.InvalidFile })
     }
-
-    saveAccount(account: NamedKeyringPair, password: string, isSpecial: boolean = false): void {
-        try {
-            const destPath = this.getAccountFilePath(account, isSpecial);
-            fs.writeFileSync(destPath, JSON.stringify(account.toJson(password)));
-        } catch(e) {
-            throw this.createDataWriteError();
-        }
-    }
-
-    // Add dev "Alice" and "Bob" accounts
-    initSpecialAccounts() {
-        const keyring = new Keyring({ type: 'sr25519' });
-        keyring.addFromUri('//Alice', { name: 'Alice' });
-        keyring.addFromUri('//Bob', { name: 'Bob' });
-        keyring.getPairs().forEach(pair => this.saveAccount(
-            { ...pair, meta: { name: pair.meta.name } },
-            '',
-            true
-        ));
-    }
-
-    fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair {
-        if (!fs.existsSync(jsonBackupFilePath)) {
-            throw new CLIError('Input file does not exist!', { exit: ExitCodes.FileNotFound });
-        }
-        if (path.extname(jsonBackupFilePath) !== '.json') {
-            throw new CLIError('Invalid input file: File extension should be .json', { exit: ExitCodes.InvalidFile });
-        }
-        let accountJsonObj: any;
-        try {
-            accountJsonObj = require(jsonBackupFilePath);
-        } catch (e) {
-            throw new CLIError('Provided backup file is not valid or cannot be accessed', { exit: ExitCodes.InvalidFile });
-        }
-        if (typeof accountJsonObj !== 'object' || accountJsonObj === null) {
-            throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile });
-        }
-
-        // Force some default account name if none is provided in the original backup
-        if (!accountJsonObj.meta) accountJsonObj.meta = {};
-        if (!accountJsonObj.meta.name) accountJsonObj.meta.name = 'Unnamed Account';
-
-        let keyring = new Keyring();
-        let account:NamedKeyringPair;
-        try {
-            // Try adding and retrieving the keys in order to validate that the backup file is correct
-            keyring.addFromJson(accountJsonObj);
-            account = <NamedKeyringPair> keyring.getPair(accountJsonObj.address); // We can be sure it's named, because we forced it before
-        } catch (e) {
-            throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile });
-        }
-
-        return account;
-    }
-
-    private fetchAccountOrNullFromFile(jsonFilePath: string): NamedKeyringPair | null {
-        try {
-            return this.fetchAccountFromJsonFile(jsonFilePath);
-        } catch (e) {
-            // Here in case of a typical CLIError we just return null (otherwise we throw)
-            if (!(e instanceof CLIError)) throw e;
-            return null;
-        }
-    }
-
-    fetchAccounts(includeSpecial: boolean = false): NamedKeyringPair[] {
-        let files: string[] = [];
-        const accountDir = this.getAccountsDirPath();
-        try {
-            files = fs.readdirSync(accountDir);
-        }
-        catch(e) {
-        }
-
-        // We have to assert the type, because TS is not aware that we're filtering out the nulls at the end
-        return <NamedKeyringPair[]> files
-            .map(fileName => {
-                const filePath = path.join(accountDir, fileName);
-                if (!includeSpecial && filePath.includes(SPECIAL_ACCOUNT_POSTFIX+'.')) return null;
-                return this.fetchAccountOrNullFromFile(filePath);
-            })
-            .filter(accObj => accObj !== null);
-    }
-
-    getSelectedAccountFilename(): string {
-        return this.getPreservedState().selectedAccountFilename;
-    }
-
-    getSelectedAccount(): NamedKeyringPair | null {
-        const selectedAccountFilename = this.getSelectedAccountFilename();
-
-        if (!selectedAccountFilename) {
-            return null;
-        }
-
-        const account = this.fetchAccountOrNullFromFile(
-            path.join(this.getAccountsDirPath(), selectedAccountFilename)
-        );
-
-        return account;
-    }
-
-    // Use when account usage is required in given command
-    async getRequiredSelectedAccount(promptIfMissing: boolean = true): Promise<NamedKeyringPair> {
-        let selectedAccount: NamedKeyringPair | null = this.getSelectedAccount();
-        if (!selectedAccount) {
-            this.warn('No default account selected! Use account:choose to set the default account!');
-            if (!promptIfMissing) this.exit(ExitCodes.NoAccountSelected);
-            const accounts: NamedKeyringPair[] = this.fetchAccounts();
-            if (!accounts.length) {
-                this.error('There are no accounts available!', { exit: ExitCodes.NoAccountFound });
-            }
-
-            selectedAccount = await this.promptForAccount(accounts);
-        }
-
-        return selectedAccount;
-    }
-
-    async setSelectedAccount(account: NamedKeyringPair): Promise<void> {
-        const accountFilename = fs.existsSync(this.getAccountFilePath(account, true))
-            ? this.generateAccountFilename(account, true)
-            : this.generateAccountFilename(account);
-
-        await this.setPreservedState({ selectedAccountFilename: accountFilename });
-    }
-
-    async promptForPassword(message:string = 'Your account\'s password') {
-        const { password } = await inquirer.prompt([
-            { name: 'password', type: 'password', message }
-        ]);
-
-        return password;
-    }
-
-    async requireConfirmation(message: string = 'Are you sure you want to execute this action?'): Promise<void> {
-        const { confirmed } = await inquirer.prompt([
-            { type: 'confirm', name: 'confirmed', message, default: false }
-        ]);
-        if (!confirmed) this.exit(ExitCodes.OK);
-    }
-
-    async promptForAccount(
-        accounts: NamedKeyringPair[],
-        defaultAccount: NamedKeyringPair | null = null,
-        message: string = 'Select an account',
-        showBalances: boolean = true
-    ): Promise<NamedKeyringPair> {
-        let balances: DerivedBalances[];
-        if (showBalances) {
-            balances = await this.getApi().getAccountsBalancesInfo(accounts.map(acc => acc.address));
-        }
-        const longestAccNameLength: number = accounts.reduce((prev, curr) => Math.max(curr.meta.name.length, prev), 0);
-        const accNameColLength: number = Math.min(longestAccNameLength + 1, 20);
-        const { chosenAccountFilename } = await inquirer.prompt([{
-            name: 'chosenAccountFilename',
-            message,
-            type: 'list',
-            choices: accounts.map((account: NamedKeyringPair, i) => ({
-                name: (
-                    `${ toFixedLength(account.meta.name, accNameColLength) } | `+
-                    `${ account.address } | ` +
-                    ((showBalances || '') && (
-                        `${ formatBalance(balances[i].availableBalance) } / `+
-                        `${ formatBalance(balances[i].votingBalance) }`
-                    ))
-                ),
-                value: this.generateAccountFilename(account),
-                short: `${ account.meta.name } (${ account.address })`
-            })),
-            default: defaultAccount && this.generateAccountFilename(defaultAccount)
-        }]);
-
-        return <NamedKeyringPair> accounts.find(acc => this.generateAccountFilename(acc) === chosenAccountFilename);
-    }
-
-    async requestAccountDecoding(account: NamedKeyringPair): Promise<void> {
-        const password: string = await this.promptForPassword();
-        try {
-            account.decodePkcs8(password);
-        } catch (e) {
-            this.error('Invalid password!', { exit: ExitCodes.InvalidInput });
-        }
-    }
-
-    async init() {
-        await super.init();
-        try {
-            this.initAccountsFs();
-            this.initSpecialAccounts();
-        } catch (e) {
-            throw this.createDataDirInitError();
-        }
+    let accountJsonObj: any
+    try {
+      accountJsonObj = require(jsonBackupFilePath)
+    } catch (e) {
+      throw new CLIError('Provided backup file is not valid or cannot be accessed', { exit: ExitCodes.InvalidFile })
+    }
+    if (typeof accountJsonObj !== 'object' || accountJsonObj === null) {
+      throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile })
     }
+
+    // Force some default account name if none is provided in the original backup
+    if (!accountJsonObj.meta) accountJsonObj.meta = {}
+    if (!accountJsonObj.meta.name) accountJsonObj.meta.name = 'Unnamed Account'
+
+    const keyring = new Keyring()
+    let account: NamedKeyringPair
+    try {
+      // Try adding and retrieving the keys in order to validate that the backup file is correct
+      keyring.addFromJson(accountJsonObj)
+      account = keyring.getPair(accountJsonObj.address) as NamedKeyringPair // We can be sure it's named, because we forced it before
+    } catch (e) {
+      throw new CLIError('Provided backup file is not valid', { exit: ExitCodes.InvalidFile })
+    }
+
+    return account
+  }
+
+  private fetchAccountOrNullFromFile(jsonFilePath: string): NamedKeyringPair | null {
+    try {
+      return this.fetchAccountFromJsonFile(jsonFilePath)
+    } catch (e) {
+      // Here in case of a typical CLIError we just return null (otherwise we throw)
+      if (!(e instanceof CLIError)) throw e
+      return null
+    }
+  }
+
+  fetchAccounts(includeSpecial = false): NamedKeyringPair[] {
+    let files: string[] = []
+    const accountDir = this.getAccountsDirPath()
+    try {
+      files = fs.readdirSync(accountDir)
+    } catch (e) {
+      // Do nothing
+    }
+
+    // We have to assert the type, because TS is not aware that we're filtering out the nulls at the end
+    return files
+      .map((fileName) => {
+        const filePath = path.join(accountDir, fileName)
+        if (!includeSpecial && filePath.includes(SPECIAL_ACCOUNT_POSTFIX + '.')) return null
+        return this.fetchAccountOrNullFromFile(filePath)
+      })
+      .filter((accObj) => accObj !== null) as NamedKeyringPair[]
+  }
+
+  getSelectedAccountFilename(): string {
+    return this.getPreservedState().selectedAccountFilename
+  }
+
+  getSelectedAccount(): NamedKeyringPair | null {
+    const selectedAccountFilename = this.getSelectedAccountFilename()
+
+    if (!selectedAccountFilename) {
+      return null
+    }
+
+    const account = this.fetchAccountOrNullFromFile(path.join(this.getAccountsDirPath(), selectedAccountFilename))
+
+    return account
+  }
+
+  // Use when account usage is required in given command
+  async getRequiredSelectedAccount(promptIfMissing = true): Promise<NamedKeyringPair> {
+    let selectedAccount: NamedKeyringPair | null = this.getSelectedAccount()
+    if (!selectedAccount) {
+      this.warn('No default account selected! Use account:choose to set the default account!')
+      if (!promptIfMissing) this.exit(ExitCodes.NoAccountSelected)
+      const accounts: NamedKeyringPair[] = this.fetchAccounts()
+      if (!accounts.length) {
+        this.error('There are no accounts available!', { exit: ExitCodes.NoAccountFound })
+      }
+
+      selectedAccount = await this.promptForAccount(accounts)
+    }
+
+    return selectedAccount
+  }
+
+  async setSelectedAccount(account: NamedKeyringPair): Promise<void> {
+    const accountFilename = fs.existsSync(this.getAccountFilePath(account, true))
+      ? this.generateAccountFilename(account, true)
+      : this.generateAccountFilename(account)
+
+    await this.setPreservedState({ selectedAccountFilename: accountFilename })
+  }
+
+  async promptForPassword(message = "Your account's password") {
+    const { password } = await inquirer.prompt([{ name: 'password', type: 'password', message }])
+
+    return password
+  }
+
+  async requireConfirmation(message = 'Are you sure you want to execute this action?'): Promise<void> {
+    const { confirmed } = await inquirer.prompt([{ type: 'confirm', name: 'confirmed', message, default: false }])
+    if (!confirmed) this.exit(ExitCodes.OK)
+  }
+
+  async promptForAccount(
+    accounts: NamedKeyringPair[],
+    defaultAccount: NamedKeyringPair | null = null,
+    message = 'Select an account',
+    showBalances = true
+  ): Promise<NamedKeyringPair> {
+    let balances: DerivedBalances[]
+    if (showBalances) {
+      balances = await this.getApi().getAccountsBalancesInfo(accounts.map((acc) => acc.address))
+    }
+    const longestAccNameLength: number = accounts.reduce((prev, curr) => Math.max(curr.meta.name.length, prev), 0)
+    const accNameColLength: number = Math.min(longestAccNameLength + 1, 20)
+    const { chosenAccountFilename } = await inquirer.prompt([
+      {
+        name: 'chosenAccountFilename',
+        message,
+        type: 'list',
+        choices: accounts.map((account: NamedKeyringPair, i) => ({
+          name:
+            `${toFixedLength(account.meta.name, accNameColLength)} | ` +
+            `${account.address} | ` +
+            ((showBalances || '') &&
+              `${formatBalance(balances[i].availableBalance)} / ` + `${formatBalance(balances[i].votingBalance)}`),
+          value: this.generateAccountFilename(account),
+          short: `${account.meta.name} (${account.address})`,
+        })),
+        default: defaultAccount && this.generateAccountFilename(defaultAccount),
+      },
+    ])
+
+    return accounts.find((acc) => this.generateAccountFilename(acc) === chosenAccountFilename) as NamedKeyringPair
+  }
+
+  async requestAccountDecoding(account: NamedKeyringPair): Promise<void> {
+    const password: string = await this.promptForPassword()
+    try {
+      account.decodePkcs8(password)
+    } catch (e) {
+      this.error('Invalid password!', { exit: ExitCodes.InvalidInput })
+    }
+  }
+
+  async init() {
+    await super.init()
+    try {
+      this.initAccountsFs()
+      this.initSpecialAccounts()
+    } catch (e) {
+      throw this.createDataDirInitError()
+    }
+  }
 }

+ 374 - 372
cli/src/base/ApiCommandBase.ts

@@ -1,405 +1,407 @@
-import ExitCodes from '../ExitCodes';
-import { CLIError } from '@oclif/errors';
-import StateAwareCommandBase from './StateAwareCommandBase';
-import Api from '../Api';
-import { getTypeDef, createType, Option, Tuple, Bytes } from '@polkadot/types';
-import { Codec, TypeDef, TypeDefInfo, Constructor } from '@polkadot/types/types';
-import { Vec, Struct, Enum } from '@polkadot/types/codec';
-import { ApiPromise } from '@polkadot/api';
-import { KeyringPair } from '@polkadot/keyring/types';
-import chalk from 'chalk';
-import { SubmittableResultImpl } from '@polkadot/api/types';
-import ajv from 'ajv';
-import { ApiMethodArg, ApiMethodNamedArgs, ApiParamsOptions, ApiParamOptions } from '../Types';
-import { createParamOptions } from '../helpers/promptOptions';
-
-class ExtrinsicFailedError extends Error { };
+import ExitCodes from '../ExitCodes'
+import { CLIError } from '@oclif/errors'
+import StateAwareCommandBase from './StateAwareCommandBase'
+import Api from '../Api'
+import { getTypeDef, createType, Option, Tuple, Bytes } from '@polkadot/types'
+import { Codec, TypeDef, TypeDefInfo, Constructor } from '@polkadot/types/types'
+import { Vec, Struct, Enum } from '@polkadot/types/codec'
+import { ApiPromise } from '@polkadot/api'
+import { KeyringPair } from '@polkadot/keyring/types'
+import chalk from 'chalk'
+import { SubmittableResultImpl } from '@polkadot/api/types'
+import ajv from 'ajv'
+import { ApiMethodArg, ApiMethodNamedArgs, ApiParamsOptions, ApiParamOptions } from '../Types'
+import { createParamOptions } from '../helpers/promptOptions'
+
+class ExtrinsicFailedError extends Error {}
 
 /**
  * Abstract base class for commands that require access to the API.
  */
 export default abstract class ApiCommandBase extends StateAwareCommandBase {
-    private api: Api | null = null;
-
-    getApi(): Api {
-        if (!this.api) throw new CLIError('Tried to get API before initialization.', { exit: ExitCodes.ApiError });
-        return this.api;
+  private api: Api | null = null
+
+  getApi(): Api {
+    if (!this.api) throw new CLIError('Tried to get API before initialization.', { exit: ExitCodes.ApiError })
+    return this.api
+  }
+
+  // Get original api for lower-level api calls
+  getOriginalApi(): ApiPromise {
+    return this.getApi().getOriginalApi()
+  }
+
+  async init() {
+    await super.init()
+    const apiUri: string = this.getPreservedState().apiUri
+    this.api = await Api.create(apiUri)
+  }
+
+  // This is needed to correctly handle some structs, enums etc.
+  // Where the main typeDef doesn't provide enough information
+  protected getRawTypeDef(type: string) {
+    const instance = createType(type as any)
+    return getTypeDef(instance.toRawType())
+  }
+
+  // Prettifier for type names which are actually JSON strings
+  protected prettifyJsonTypeName(json: string) {
+    const obj = JSON.parse(json) as { [key: string]: string }
+    return (
+      '{\n' +
+      Object.keys(obj)
+        .map((prop) => `  ${prop}${chalk.white(':' + obj[prop])}`)
+        .join('\n') +
+      '\n}'
+    )
+  }
+
+  // Get param name based on TypeDef object
+  protected paramName(typeDef: TypeDef) {
+    return chalk.green(
+      typeDef.displayName ||
+        typeDef.name ||
+        (typeDef.type.startsWith('{') ? this.prettifyJsonTypeName(typeDef.type) : typeDef.type)
+    )
+  }
+
+  // Prompt for simple/plain value (provided as string) of given type
+  async promptForSimple(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Codec> {
+    const providedValue = await this.simplePrompt({
+      message: `Provide value for ${this.paramName(typeDef)}`,
+      type: 'input',
+      // If not default provided - show default value resulting from providing empty string
+      default: paramOptions?.value?.default?.toString() || createType(typeDef.type as any, '').toString(),
+      validate: paramOptions?.validator,
+    })
+    return createType(typeDef.type as any, providedValue)
+  }
+
+  // Prompt for Option<Codec> value
+  async promptForOption(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Option<Codec>> {
+    const subtype = typeDef.sub as TypeDef // We assume that Opion always has a single subtype
+    const defaultValue = paramOptions?.value?.default as Option<Codec> | undefined
+    const confirmed = await this.simplePrompt({
+      message: `Do you want to provide the optional ${this.paramName(typeDef)} parameter?`,
+      type: 'confirm',
+      default: defaultValue ? defaultValue.isSome : false,
+    })
+
+    if (confirmed) {
+      this.openIndentGroup()
+      const value = await this.promptForParam(
+        subtype.type,
+        createParamOptions(subtype.name, defaultValue?.unwrapOr(undefined))
+      )
+      this.closeIndentGroup()
+      return new Option(subtype.type as any, value)
     }
 
-    // Get original api for lower-level api calls
-    getOriginalApi(): ApiPromise {
-        return this.getApi().getOriginalApi();
-    }
+    return new Option(subtype.type as any, null)
+  }
 
-    async init() {
-        await super.init();
-        const apiUri: string = this.getPreservedState().apiUri;
-        this.api = await Api.create(apiUri);
-    }
+  // Prompt for Tuple
+  // TODO: Not well tested yet
+  async promptForTuple(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Tuple> {
+    console.log(chalk.grey(`Providing values for ${this.paramName(typeDef)} tuple:`))
 
-    // This is needed to correctly handle some structs, enums etc.
-    // Where the main typeDef doesn't provide enough information
-    protected getRawTypeDef(type: string) {
-        const instance = createType(type as any);
-        return getTypeDef(instance.toRawType());
-    }
+    this.openIndentGroup()
+    const result: ApiMethodArg[] = []
+    // We assume that for Tuple there is always at least 1 subtype (pethaps it's even always an array?)
+    const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub! : [typeDef.sub!]
+    const defaultValue = paramOptions?.value?.default as Tuple | undefined
 
-    // Prettifier for type names which are actually JSON strings
-    protected prettifyJsonTypeName(json: string) {
-        const obj = JSON.parse(json) as { [key: string]: string };
-        return "{\n"+Object.keys(obj).map(prop => `  ${prop}${chalk.white(':'+obj[prop])}`).join("\n")+"\n}";
+    for (const [index, subtype] of Object.entries(subtypes)) {
+      const entryDefaultVal = defaultValue && defaultValue[parseInt(index)]
+      const inputParam = await this.promptForParam(subtype.type, createParamOptions(subtype.name, entryDefaultVal))
+      result.push(inputParam)
     }
-
-    // Get param name based on TypeDef object
-    protected paramName(typeDef: TypeDef) {
-        return chalk.green(
-            typeDef.displayName ||
-            typeDef.name ||
-            (typeDef.type.startsWith('{') ? this.prettifyJsonTypeName(typeDef.type) : typeDef.type)
-        );
+    this.closeIndentGroup()
+
+    return new Tuple(subtypes.map((subtype) => subtype.type) as any, result)
+  }
+
+  // Prompt for Struct
+  async promptForStruct(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<ApiMethodArg> {
+    console.log(chalk.grey(`Providing values for ${this.paramName(typeDef)} struct:`))
+
+    this.openIndentGroup()
+    const structType = typeDef.type
+    const rawTypeDef = this.getRawTypeDef(structType)
+    // We assume struct typeDef always has array of typeDefs inside ".sub"
+    const structSubtypes = rawTypeDef.sub as TypeDef[]
+    const structDefault = paramOptions?.value?.default as Struct | undefined
+
+    const structValues: { [key: string]: ApiMethodArg } = {}
+    for (const subtype of structSubtypes) {
+      const fieldOptions = paramOptions?.nestedOptions && paramOptions.nestedOptions[subtype.name!]
+      const fieldDefaultValue = fieldOptions?.value?.default || (structDefault && structDefault.get(subtype.name!))
+      const finalFieldOptions: ApiParamOptions = {
+        ...fieldOptions,
+        forcedName: subtype.name,
+        value: fieldDefaultValue && { ...fieldOptions?.value, default: fieldDefaultValue },
+      }
+      structValues[subtype.name!] = await this.promptForParam(subtype.type, finalFieldOptions)
     }
-
-    // Prompt for simple/plain value (provided as string) of given type
-    async promptForSimple(
-        typeDef: TypeDef,
-        paramOptions?: ApiParamOptions
-    ): Promise<Codec> {
-        const providedValue = await this.simplePrompt({
-            message: `Provide value for ${ this.paramName(typeDef) }`,
-            type: 'input',
-            // If not default provided - show default value resulting from providing empty string
-            default: paramOptions?.value?.default?.toString() || createType(typeDef.type as any, '').toString(),
-            validate: paramOptions?.validator
-        });
-        return createType(typeDef.type as any, providedValue);
+    this.closeIndentGroup()
+
+    return createType(structType as any, structValues)
+  }
+
+  // Prompt for Vec
+  async promptForVec(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Vec<Codec>> {
+    console.log(chalk.grey(`Providing values for ${this.paramName(typeDef)} vector:`))
+
+    this.openIndentGroup()
+    // We assume Vec always has one TypeDef as ".sub"
+    const subtype = typeDef.sub as TypeDef
+    const defaultValue = paramOptions?.value?.default as Vec<Codec> | undefined
+    const entries: Codec[] = []
+    let addAnother = false
+    do {
+      addAnother = await this.simplePrompt({
+        message: `Do you want to add another entry to ${this.paramName(typeDef)} vector (currently: ${
+          entries.length
+        })?`,
+        type: 'confirm',
+        default: defaultValue ? entries.length < defaultValue.length : false,
+      })
+      const defaultEntryValue = defaultValue && defaultValue[entries.length]
+      if (addAnother) {
+        entries.push(await this.promptForParam(subtype.type, createParamOptions(subtype.name, defaultEntryValue)))
+      }
+    } while (addAnother)
+    this.closeIndentGroup()
+
+    return new Vec(subtype.type as any, entries)
+  }
+
+  // Prompt for Enum
+  async promptForEnum(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Enum> {
+    const enumType = typeDef.type
+    const rawTypeDef = this.getRawTypeDef(enumType)
+    // We assume enum always has array on TypeDefs inside ".sub"
+    const enumSubtypes = rawTypeDef.sub as TypeDef[]
+    const defaultValue = paramOptions?.value?.default as Enum | undefined
+
+    const enumSubtypeName = await this.simplePrompt({
+      message: `Choose value for ${this.paramName(typeDef)}:`,
+      type: 'list',
+      choices: enumSubtypes.map((subtype) => ({
+        name: subtype.name,
+        value: subtype.name,
+      })),
+      default: defaultValue?.type,
+    })
+
+    const enumSubtype = enumSubtypes.find((st) => st.name === enumSubtypeName)!
+
+    if (enumSubtype.type !== 'Null') {
+      const subtypeOptions = createParamOptions(enumSubtype.name, defaultValue?.value)
+      return createType(enumType as any, {
+        [enumSubtype.name!]: await this.promptForParam(enumSubtype.type, subtypeOptions),
+      })
     }
 
-    // Prompt for Option<Codec> value
-    async promptForOption(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Option<Codec>> {
-        const subtype = <TypeDef> typeDef.sub; // We assume that Opion always has a single subtype
-        const defaultValue = paramOptions?.value?.default as Option<Codec> | undefined;
-        const confirmed = await this.simplePrompt({
-            message: `Do you want to provide the optional ${ this.paramName(typeDef) } parameter?`,
-            type: 'confirm',
-            default: defaultValue ? defaultValue.isSome : false,
-        });
-
-        if (confirmed) {
-            this.openIndentGroup();
-            const value = await this.promptForParam(subtype.type, createParamOptions(subtype.name, defaultValue?.unwrapOr(undefined)));
-            this.closeIndentGroup();
-            return new Option(subtype.type as any, value);
-        }
+    return createType(enumType as any, enumSubtype.name)
+  }
 
-        return new Option(subtype.type as any, null);
-    }
+  // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
+  // TODO: This may not yet work for all possible types
+  async promptForParam(
+    paramType: string,
+    paramOptions?: ApiParamOptions // TODO: This is not fully implemented for all types yet
+  ): Promise<ApiMethodArg> {
+    const typeDef = getTypeDef(paramType)
+    const rawTypeDef = this.getRawTypeDef(paramType)
 
-    // Prompt for Tuple
-    // TODO: Not well tested yet
-    async promptForTuple(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Tuple> {
-        console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } tuple:`));
-
-        this.openIndentGroup();
-        const result: ApiMethodArg[] = [];
-        // We assume that for Tuple there is always at least 1 subtype (pethaps it's even always an array?)
-        const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub! : [ typeDef.sub! ];
-        const defaultValue = paramOptions?.value?.default as Tuple | undefined;
-
-        for (const [index, subtype] of Object.entries(subtypes)) {
-            const entryDefaultVal = defaultValue && defaultValue[parseInt(index)];
-            const inputParam = await this.promptForParam(subtype.type, createParamOptions(subtype.name, entryDefaultVal));
-            result.push(inputParam);
-        }
-        this.closeIndentGroup();
-
-        return new Tuple((subtypes.map(subtype => subtype.type)) as any, result);
+    if (paramOptions?.forcedName) {
+      typeDef.name = paramOptions.forcedName
     }
 
-    // Prompt for Struct
-    async promptForStruct(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<ApiMethodArg> {
-        console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } struct:`));
-
-        this.openIndentGroup();
-        const structType = typeDef.type;
-        const rawTypeDef = this.getRawTypeDef(structType);
-        // We assume struct typeDef always has array of typeDefs inside ".sub"
-        const structSubtypes = rawTypeDef.sub as TypeDef[];
-        const structDefault = paramOptions?.value?.default as Struct | undefined;
-
-        const structValues: { [key: string]: ApiMethodArg } = {};
-        for (const subtype of structSubtypes) {
-            const fieldOptions = paramOptions?.nestedOptions && paramOptions.nestedOptions[subtype.name!];
-            const fieldDefaultValue = fieldOptions?.value?.default || (structDefault && structDefault.get(subtype.name!));
-            const finalFieldOptions: ApiParamOptions = {
-                ...fieldOptions,
-                forcedName: subtype.name,
-                value: fieldDefaultValue && { ...fieldOptions?.value, default: fieldDefaultValue }
-            }
-            structValues[subtype.name!] = await this.promptForParam(subtype.type, finalFieldOptions);
-        }
-        this.closeIndentGroup();
-
-        return createType(structType as any, structValues);
+    if (paramOptions?.value?.locked) {
+      return paramOptions.value.default
     }
 
-    // Prompt for Vec
-    async promptForVec(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Vec<Codec>> {
-        console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } vector:`));
-
-        this.openIndentGroup();
-        // We assume Vec always has one TypeDef as ".sub"
-        const subtype = typeDef.sub as TypeDef;
-        const defaultValue = paramOptions?.value?.default as Vec<Codec> | undefined;
-        let entries: Codec[] = [];
-        let addAnother = false;
-        do {
-            addAnother = await this.simplePrompt({
-                message: `Do you want to add another entry to ${ this.paramName(typeDef) } vector (currently: ${entries.length})?`,
-                type: 'confirm',
-                default: defaultValue ? entries.length < defaultValue.length : false
-            });
-            const defaultEntryValue = defaultValue && defaultValue[entries.length];
-            if (addAnother) {
-                entries.push(await this.promptForParam(subtype.type, createParamOptions(subtype.name, defaultEntryValue)));
-            }
-        } while (addAnother);
-        this.closeIndentGroup();
-
-        return new Vec(subtype.type as any, entries);
+    if (paramOptions?.jsonSchema) {
+      const { struct, schemaValidator } = paramOptions.jsonSchema
+      return await this.promptForJsonBytes(
+        struct,
+        typeDef.name,
+        paramOptions.value?.default as Bytes | undefined,
+        schemaValidator
+      )
     }
 
-    // Prompt for Enum
-    async promptForEnum(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Enum> {
-        const enumType = typeDef.type;
-        const rawTypeDef = this.getRawTypeDef(enumType);
-        // We assume enum always has array on TypeDefs inside ".sub"
-        const enumSubtypes = rawTypeDef.sub as TypeDef[];
-        const defaultValue = paramOptions?.value?.default as Enum | undefined;
-
-        const enumSubtypeName = await this.simplePrompt({
-            message: `Choose value for ${this.paramName(typeDef)}:`,
-            type: 'list',
-            choices: enumSubtypes.map(subtype => ({
-                name: subtype.name,
-                value: subtype.name
-            })),
-            default: defaultValue?.type
-        });
-
-        const enumSubtype = enumSubtypes.find(st => st.name === enumSubtypeName)!;
-
-        if (enumSubtype.type !== 'Null') {
-            const subtypeOptions = createParamOptions(enumSubtype.name, defaultValue?.value);
-            return createType(
-                enumType as any,
-                { [enumSubtype.name!]: await this.promptForParam(enumSubtype.type, subtypeOptions) }
-            );
-        }
-
-        return createType(enumType as any, enumSubtype.name);
+    if (rawTypeDef.info === TypeDefInfo.Option) {
+      return await this.promptForOption(typeDef, paramOptions)
+    } else if (rawTypeDef.info === TypeDefInfo.Tuple) {
+      return await this.promptForTuple(typeDef, paramOptions)
+    } else if (rawTypeDef.info === TypeDefInfo.Struct) {
+      return await this.promptForStruct(typeDef, paramOptions)
+    } else if (rawTypeDef.info === TypeDefInfo.Enum) {
+      return await this.promptForEnum(typeDef, paramOptions)
+    } else if (rawTypeDef.info === TypeDefInfo.Vec) {
+      return await this.promptForVec(typeDef, paramOptions)
+    } else {
+      return await this.promptForSimple(typeDef, paramOptions)
     }
-
-    // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
-    // TODO: This may not yet work for all possible types
-    async promptForParam(
-        paramType: string,
-        paramOptions?: ApiParamOptions // TODO: This is not fully implemented for all types yet
-    ): Promise<ApiMethodArg> {
-        const typeDef = getTypeDef(paramType);
-        const rawTypeDef = this.getRawTypeDef(paramType);
-
-        if (paramOptions?.forcedName) {
-            typeDef.name = paramOptions.forcedName;
-        }
-
-        if (paramOptions?.value?.locked) {
-            return paramOptions.value.default;
-        }
-
-        if (paramOptions?.jsonSchema) {
-            const { struct, schemaValidator } = paramOptions.jsonSchema;
-            return await this.promptForJsonBytes(
-                struct,
-                typeDef.name,
-                paramOptions.value?.default as Bytes | undefined,
-                schemaValidator
-            );
-        }
-
-        if (rawTypeDef.info === TypeDefInfo.Option) {
-            return await this.promptForOption(typeDef, paramOptions);
-        }
-        else if (rawTypeDef.info === TypeDefInfo.Tuple) {
-            return await this.promptForTuple(typeDef, paramOptions);
-        }
-        else if (rawTypeDef.info === TypeDefInfo.Struct) {
-            return await this.promptForStruct(typeDef, paramOptions);
-        }
-        else if (rawTypeDef.info === TypeDefInfo.Enum) {
-            return await this.promptForEnum(typeDef, paramOptions);
-        }
-        else if (rawTypeDef.info === TypeDefInfo.Vec) {
-            return await this.promptForVec(typeDef, paramOptions);
-        }
-        else {
-            return await this.promptForSimple(typeDef, paramOptions);
-        }
+  }
+
+  async promptForJsonBytes(
+    JsonStruct: Constructor<Struct>,
+    argName?: string,
+    defaultValue?: Bytes,
+    schemaValidator?: ajv.ValidateFunction
+  ) {
+    const rawType = new JsonStruct().toRawType()
+    const typeDef = getTypeDef(rawType)
+
+    const defaultStruct =
+      defaultValue && new JsonStruct(JSON.parse(Buffer.from(defaultValue.toHex().replace('0x', ''), 'hex').toString()))
+
+    if (argName) {
+      typeDef.name = argName
     }
 
-    async promptForJsonBytes(
-        JsonStruct: Constructor<Struct>,
-        argName?: string,
-        defaultValue?: Bytes,
-        schemaValidator?: ajv.ValidateFunction
-    ) {
-        const rawType = (new JsonStruct()).toRawType();
-        const typeDef = getTypeDef(rawType);
-
-        const defaultStruct =
-            defaultValue &&
-            new JsonStruct(JSON.parse(Buffer.from(defaultValue.toHex().replace('0x', ''), 'hex').toString()));
-
-        if (argName) {
-            typeDef.name = argName;
+    let isValid = true,
+      jsonText: string
+    do {
+      const structVal = await this.promptForStruct(typeDef, createParamOptions(typeDef.name, defaultStruct))
+      jsonText = JSON.stringify(structVal.toJSON())
+      if (schemaValidator) {
+        isValid = Boolean(schemaValidator(JSON.parse(jsonText)))
+        if (!isValid) {
+          this.log('\n')
+          this.warn(
+            'Schema validation failed with:\n' +
+              schemaValidator.errors?.map((e) => chalk.red(`${chalk.bold(e.dataPath)}: ${e.message}`)).join('\n') +
+              '\nTry again...'
+          )
+          this.log('\n')
         }
-
-        let isValid: boolean = true, jsonText: string;
-        do {
-            const structVal = await this.promptForStruct(typeDef, createParamOptions(typeDef.name, defaultStruct));
-            jsonText = JSON.stringify(structVal.toJSON());
-            if (schemaValidator) {
-                isValid = Boolean(schemaValidator(JSON.parse(jsonText)));
-                if (!isValid) {
-                    this.log("\n");
-                    this.warn(
-                        "Schema validation failed with:\n"+
-                        schemaValidator.errors?.map(e => chalk.red(`${chalk.bold(e.dataPath)}: ${e.message}`)).join("\n")+
-                        "\nTry again..."
-                    )
-                    this.log("\n");
-                }
-            }
-        } while(!isValid);
-
-        return new Bytes('0x'+Buffer.from(jsonText, 'ascii').toString('hex'));
+      }
+    } while (!isValid)
+
+    return new Bytes('0x' + Buffer.from(jsonText, 'ascii').toString('hex'))
+  }
+
+  async promptForExtrinsicParams(
+    module: string,
+    method: string,
+    paramsOptions?: ApiParamsOptions
+  ): Promise<ApiMethodArg[]> {
+    const extrinsicMethod = this.getOriginalApi().tx[module][method]
+    const values: ApiMethodArg[] = []
+
+    this.openIndentGroup()
+    for (const arg of extrinsicMethod.meta.args.toArray()) {
+      const argName = arg.name.toString()
+      const argType = arg.type.toString()
+      let argOptions = paramsOptions && paramsOptions[argName]
+      if (!argOptions?.forcedName) {
+        argOptions = { ...argOptions, forcedName: argName }
+      }
+      values.push(await this.promptForParam(argType, argOptions))
     }
-
-    async promptForExtrinsicParams(
-        module: string,
-        method: string,
-        paramsOptions?: ApiParamsOptions
-    ): Promise<ApiMethodArg[]> {
-        const extrinsicMethod = this.getOriginalApi().tx[module][method];
-        let values: ApiMethodArg[] = [];
-
-        this.openIndentGroup();
-        for (const arg of extrinsicMethod.meta.args.toArray()) {
-            const argName = arg.name.toString();
-            const argType = arg.type.toString();
-            let argOptions = paramsOptions && paramsOptions[argName];
-            if (!argOptions?.forcedName) {
-                argOptions = { ...argOptions, forcedName: argName };
-            }
-            values.push(await this.promptForParam(argType, argOptions));
-        };
-        this.closeIndentGroup();
-
-        return values;
+    this.closeIndentGroup()
+
+    return values
+  }
+
+  sendExtrinsic(account: KeyringPair, module: string, method: string, params: Codec[]) {
+    return new Promise((resolve, reject) => {
+      const extrinsicMethod = this.getOriginalApi().tx[module][method]
+      let unsubscribe: () => void
+      extrinsicMethod(...params)
+        .signAndSend(account, {}, (result: SubmittableResultImpl) => {
+          // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
+          if (!result || !result.status) {
+            return
+          }
+
+          if (result.status.isFinalized) {
+            unsubscribe()
+            result.events
+              .filter(({ event: { section } }): boolean => section === 'system')
+              .forEach(({ event: { method } }): void => {
+                if (method === 'ExtrinsicFailed') {
+                  reject(new ExtrinsicFailedError('Extrinsic execution error!'))
+                } else if (method === 'ExtrinsicSuccess') {
+                  resolve()
+                }
+              })
+          } else if (result.isError) {
+            reject(new ExtrinsicFailedError('Extrinsic execution error!'))
+          }
+        })
+        .then((unsubFunc) => (unsubscribe = unsubFunc))
+        .catch((e) =>
+          reject(new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : JSON.stringify(e)}`))
+        )
+    })
+  }
+
+  async sendAndFollowExtrinsic(
+    account: KeyringPair,
+    module: string,
+    method: string,
+    params: Codec[],
+    warnOnly = false // If specified - only warning will be displayed (instead of error beeing thrown)
+  ) {
+    try {
+      this.log(chalk.white(`\nSending ${module}.${method} extrinsic...`))
+      await this.sendExtrinsic(account, module, method, params)
+      this.log(chalk.green(`Extrinsic successful!`))
+    } catch (e) {
+      if (e instanceof ExtrinsicFailedError && warnOnly) {
+        this.warn(`${module}.${method} extrinsic failed! ${e.message}`)
+      } else if (e instanceof ExtrinsicFailedError) {
+        throw new CLIError(`${module}.${method} extrinsic failed! ${e.message}`, { exit: ExitCodes.ApiError })
+      } else {
+        throw e
+      }
     }
-
-    sendExtrinsic(account: KeyringPair, module: string, method: string, params: Codec[]) {
-        return new Promise((resolve, reject) => {
-            const extrinsicMethod = this.getOriginalApi().tx[module][method];
-            let unsubscribe: () => void;
-            extrinsicMethod(...params)
-                .signAndSend(account, {}, (result: SubmittableResultImpl) => {
-                    // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
-                    if (!result || !result.status) {
-                        return;
-                    }
-
-                    if (result.status.isFinalized) {
-                      unsubscribe();
-                      result.events
-                        .filter(({ event: { section } }): boolean => section === 'system')
-                        .forEach(({ event: { method } }): void => {
-                          if (method === 'ExtrinsicFailed') {
-                            reject(new ExtrinsicFailedError('Extrinsic execution error!'));
-                          } else if (method === 'ExtrinsicSuccess') {
-                            resolve();
-                          }
-                        });
-                    } else if (result.isError) {
-                        reject(new ExtrinsicFailedError('Extrinsic execution error!'));
-                    }
-                })
-                .then(unsubFunc => unsubscribe = unsubFunc)
-                .catch(e => reject(new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : JSON.stringify(e)}`)));
-        });
+  }
+
+  async buildAndSendExtrinsic(
+    account: KeyringPair,
+    module: string,
+    method: string,
+    paramsOptions: ApiParamsOptions,
+    warnOnly = false // If specified - only warning will be displayed (instead of error beeing thrown)
+  ): Promise<ApiMethodArg[]> {
+    const params = await this.promptForExtrinsicParams(module, method, paramsOptions)
+    await this.sendAndFollowExtrinsic(account, module, method, params, warnOnly)
+
+    return params
+  }
+
+  extrinsicArgsFromDraft(module: string, method: string, draftFilePath: string): ApiMethodNamedArgs {
+    let draftJSONObj
+    const parsedArgs: ApiMethodNamedArgs = []
+    const extrinsicMethod = this.getOriginalApi().tx[module][method]
+    try {
+      // eslint-disable-next-line @typescript-eslint/no-var-requires
+      draftJSONObj = require(draftFilePath)
+    } catch (e) {
+      throw new CLIError(`Could not load draft from: ${draftFilePath}`, { exit: ExitCodes.InvalidFile })
     }
-
-    async sendAndFollowExtrinsic(
-        account: KeyringPair,
-        module: string,
-        method: string,
-        params: Codec[],
-        warnOnly: boolean = false // If specified - only warning will be displayed (instead of error beeing thrown)
-    ) {
-        try {
-            this.log(chalk.white(`\nSending ${ module }.${ method } extrinsic...`));
-            await this.sendExtrinsic(account, module, method, params);
-            this.log(chalk.green(`Extrinsic successful!`));
-        } catch (e) {
-            if (e instanceof ExtrinsicFailedError && warnOnly) {
-                this.warn(`${ module }.${ method } extrinsic failed! ${ e.message }`);
-            }
-            else if (e instanceof ExtrinsicFailedError) {
-                throw new CLIError(`${ module }.${ method } extrinsic failed! ${ e.message }`, { exit: ExitCodes.ApiError });
-            }
-            else {
-                throw e;
-            }
-        }
+    if (!draftJSONObj || !Array.isArray(draftJSONObj) || draftJSONObj.length !== extrinsicMethod.meta.args.length) {
+      throw new CLIError(`The draft file at ${draftFilePath} is invalid!`, { exit: ExitCodes.InvalidFile })
     }
-
-    async buildAndSendExtrinsic(
-        account: KeyringPair,
-        module: string,
-        method: string,
-        paramsOptions: ApiParamsOptions,
-        warnOnly: boolean = false // If specified - only warning will be displayed (instead of error beeing thrown)
-    ): Promise<ApiMethodArg[]> {
-        const params = await this.promptForExtrinsicParams(module, method, paramsOptions);
-        await this.sendAndFollowExtrinsic(account, module, method, params, warnOnly);
-
-        return params;
+    for (const [index, arg] of Object.entries(extrinsicMethod.meta.args.toArray())) {
+      const argName = arg.name.toString()
+      const argType = arg.type.toString()
+      try {
+        parsedArgs.push({ name: argName, value: createType(argType as any, draftJSONObj[parseInt(index)]) })
+      } catch (e) {
+        throw new CLIError(`Couldn't parse ${argName} value from draft at ${draftFilePath}!`, {
+          exit: ExitCodes.InvalidFile,
+        })
+      }
     }
 
-    extrinsicArgsFromDraft(module: string, method: string, draftFilePath: string): ApiMethodNamedArgs {
-        let draftJSONObj, parsedArgs: ApiMethodNamedArgs = [];
-        const extrinsicMethod = this.getOriginalApi().tx[module][method];
-        try {
-            draftJSONObj = require(draftFilePath);
-        } catch(e) {
-            throw new CLIError(`Could not load draft from: ${draftFilePath}`, { exit: ExitCodes.InvalidFile });
-        }
-        if (
-            !draftJSONObj
-            || !Array.isArray(draftJSONObj)
-            || draftJSONObj.length !== extrinsicMethod.meta.args.length
-        ) {
-            throw new CLIError(`The draft file at ${draftFilePath} is invalid!`, { exit: ExitCodes.InvalidFile });
-        }
-        for (const [index, arg] of Object.entries(extrinsicMethod.meta.args.toArray())) {
-            const argName = arg.name.toString();
-            const argType = arg.type.toString();
-            try {
-                parsedArgs.push({ name: argName, value: createType(argType as any, draftJSONObj[parseInt(index)]) });
-            } catch (e) {
-                throw new CLIError(`Couldn't parse ${argName} value from draft at ${draftFilePath}!`, { exit: ExitCodes.InvalidFile });
-            }
-        }
-
-        return parsedArgs;
-    }
+    return parsedArgs
+  }
 }

+ 84 - 73
cli/src/base/DefaultCommandBase.ts

@@ -1,95 +1,106 @@
-import ExitCodes from '../ExitCodes';
-import Command from '@oclif/command';
-import inquirer, { DistinctQuestion } from 'inquirer';
-import chalk from 'chalk';
+import ExitCodes from '../ExitCodes'
+import Command from '@oclif/command'
+import inquirer, { DistinctQuestion } from 'inquirer'
+import chalk from 'chalk'
 
 /**
  * Abstract base class for pretty much all commands
  * (prevents console.log from hanging the process and unifies the default exit code)
  */
 export default abstract class DefaultCommandBase extends Command {
-    protected indentGroupsOpened = 0;
-    protected jsonPrettyIdent = '';
+  protected indentGroupsOpened = 0
+  protected jsonPrettyIdent = ''
 
-    openIndentGroup() {
-        console.group();
-        ++this.indentGroupsOpened;
-    }
-
-    closeIndentGroup() {
-        console.groupEnd();
-        --this.indentGroupsOpened;
-    }
+  openIndentGroup() {
+    console.group()
+    ++this.indentGroupsOpened
+  }
 
-    async simplePrompt(question: DistinctQuestion) {
-        const { result } = await inquirer.prompt([{
-            ...question,
-            name: 'result',
-            // prefix = 2 spaces for each group - 1 (because 1 is always added by default)
-            prefix: Array.from(new Array(this.indentGroupsOpened)).map(() => '  ').join('').slice(1)
-        }]);
+  closeIndentGroup() {
+    console.groupEnd()
+    --this.indentGroupsOpened
+  }
 
-        return result;
-    }
+  async simplePrompt(question: DistinctQuestion) {
+    const { result } = await inquirer.prompt([
+      {
+        ...question,
+        name: 'result',
+        // prefix = 2 spaces for each group - 1 (because 1 is always added by default)
+        prefix: Array.from(new Array(this.indentGroupsOpened))
+          .map(() => '  ')
+          .join('')
+          .slice(1),
+      },
+    ])
 
-    private jsonPrettyIndented(line:string) {
-        return `${this.jsonPrettyIdent}${ line }`;
-    }
+    return result
+  }
 
-    private jsonPrettyOpen(char: '{' | '[') {
-        this.jsonPrettyIdent += '    ';
-        return chalk.gray(char)+"\n";
-    }
+  private jsonPrettyIndented(line: string) {
+    return `${this.jsonPrettyIdent}${line}`
+  }
 
-    private jsonPrettyClose(char: '}' | ']') {
-        this.jsonPrettyIdent = this.jsonPrettyIdent.slice(0, -4);
-        return this.jsonPrettyIndented(chalk.gray(char));
-    }
+  private jsonPrettyOpen(char: '{' | '[') {
+    this.jsonPrettyIdent += '    '
+    return chalk.gray(char) + '\n'
+  }
 
-    private jsonPrettyKeyVal(key:string, val:any): string {
-        return this.jsonPrettyIndented(chalk.white(`${key}: ${this.jsonPrettyAny(val)}`));
-    }
+  private jsonPrettyClose(char: '}' | ']') {
+    this.jsonPrettyIdent = this.jsonPrettyIdent.slice(0, -4)
+    return this.jsonPrettyIndented(chalk.gray(char))
+  }
 
-    private jsonPrettyObj(obj: { [key: string]: any }): string {
-        return this.jsonPrettyOpen('{')
-            + Object.keys(obj).map(k => this.jsonPrettyKeyVal(k, obj[k])).join(',\n') + "\n"
-            + this.jsonPrettyClose('}');
-    }
+  private jsonPrettyKeyVal(key: string, val: any): string {
+    return this.jsonPrettyIndented(chalk.white(`${key}: ${this.jsonPrettyAny(val)}`))
+  }
 
-    private jsonPrettyArr(arr: any[]): string {
-        return this.jsonPrettyOpen('[')
-            + arr.map(v => this.jsonPrettyIndented(this.jsonPrettyAny(v))).join(',\n') + "\n"
-            + this.jsonPrettyClose(']');
-    }
+  private jsonPrettyObj(obj: { [key: string]: any }): string {
+    return (
+      this.jsonPrettyOpen('{') +
+      Object.keys(obj)
+        .map((k) => this.jsonPrettyKeyVal(k, obj[k]))
+        .join(',\n') +
+      '\n' +
+      this.jsonPrettyClose('}')
+    )
+  }
 
-    private jsonPrettyAny(val: any): string {
-        if (Array.isArray(val)) {
-            return this.jsonPrettyArr(val);
-        }
-        else if (typeof val === 'object' && val !== null) {
-            return this.jsonPrettyObj(val);
-        }
-        else if (typeof val === 'string') {
-            return chalk.green(`"${val}"`);
-        }
+  private jsonPrettyArr(arr: any[]): string {
+    return (
+      this.jsonPrettyOpen('[') +
+      arr.map((v) => this.jsonPrettyIndented(this.jsonPrettyAny(v))).join(',\n') +
+      '\n' +
+      this.jsonPrettyClose(']')
+    )
+  }
 
-        // Number, boolean etc.
-        return chalk.cyan(val);
+  private jsonPrettyAny(val: any): string {
+    if (Array.isArray(val)) {
+      return this.jsonPrettyArr(val)
+    } else if (typeof val === 'object' && val !== null) {
+      return this.jsonPrettyObj(val)
+    } else if (typeof val === 'string') {
+      return chalk.green(`"${val}"`)
     }
 
-    jsonPrettyPrint(json: string) {
-        try {
-            const parsed = JSON.parse(json);
-            console.log(this.jsonPrettyAny(parsed));
-        } catch(e) {
-            console.log(this.jsonPrettyAny(json));
-        }
-    }
+    // Number, boolean etc.
+    return chalk.cyan(val)
+  }
 
-    async finally(err: any) {
-        // called after run and catch regardless of whether or not the command errored
-        // We'll force exit here, in case there is no error, to prevent console.log from hanging the process
-        if (!err) this.exit(ExitCodes.OK);
-        super.finally(err);
+  jsonPrettyPrint(json: string) {
+    try {
+      const parsed = JSON.parse(json)
+      console.log(this.jsonPrettyAny(parsed))
+    } catch (e) {
+      console.log(this.jsonPrettyAny(json))
     }
+  }
+
+  async finally(err: any) {
+    // called after run and catch regardless of whether or not the command errored
+    // We'll force exit here, in case there is no error, to prevent console.log from hanging the process
+    if (!err) this.exit(ExitCodes.OK)
+    super.finally(err)
+  }
 }

+ 94 - 95
cli/src/base/StateAwareCommandBase.ts

@@ -1,32 +1,32 @@
-import fs from 'fs';
-import path from 'path';
-import ExitCodes from '../ExitCodes';
-import { CLIError } from '@oclif/errors';
-import { DEFAULT_API_URI } from '../Api';
-import lockFile from 'proper-lockfile';
-import DefaultCommandBase from './DefaultCommandBase';
-import os from 'os';
+import fs from 'fs'
+import path from 'path'
+import ExitCodes from '../ExitCodes'
+import { CLIError } from '@oclif/errors'
+import { DEFAULT_API_URI } from '../Api'
+import lockFile from 'proper-lockfile'
+import DefaultCommandBase from './DefaultCommandBase'
+import os from 'os'
 
 // Type for the state object (which is preserved as json in the state file)
 type StateObject = {
-    selectedAccountFilename: string,
-    apiUri: string
-};
+  selectedAccountFilename: string
+  apiUri: string
+}
 
 // State object default values
 const DEFAULT_STATE: StateObject = {
-    selectedAccountFilename: '',
-    apiUri: DEFAULT_API_URI
+  selectedAccountFilename: '',
+  apiUri: DEFAULT_API_URI,
 }
 
 // State file path (relative to getAppDataPath())
-const STATE_FILE = '/state.json';
+const STATE_FILE = '/state.json'
 
 // Possible data directory access errors
 enum DataDirErrorType {
-    Init = 0,
-    Read = 1,
-    Write = 2,
+  Init = 0,
+  Read = 1,
+  Write = 2,
 }
 
 /**
@@ -37,95 +37,94 @@ enum DataDirErrorType {
  * choosen by the user after executing account:choose command etc. (see "StateObject" type above).
  */
 export default abstract class StateAwareCommandBase extends DefaultCommandBase {
-    getAppDataPath(): string {
-        const systemAppDataPath =
-            process.env.APPDATA ||
-            (
-                process.platform === 'darwin'
-                    ? path.join(os.homedir(), '/Library/Application Support')
-                    : path.join(os.homedir(), '/.local/share')
-            );
-        const packageJson: { name?: string } = require('../../package.json');
-        if (!packageJson || !packageJson.name) {
-            throw new CLIError('Cannot get package name from package.json!');
-        }
-        return path.join(systemAppDataPath, packageJson.name);
+  getAppDataPath(): string {
+    const systemAppDataPath =
+      process.env.APPDATA ||
+      (process.platform === 'darwin'
+        ? path.join(os.homedir(), '/Library/Application Support')
+        : path.join(os.homedir(), '/.local/share'))
+    // eslint-disable-next-line @typescript-eslint/no-var-requires
+    const packageJson: { name?: string } = require('../../package.json')
+    if (!packageJson || !packageJson.name) {
+      throw new CLIError('Cannot get package name from package.json!')
     }
-
-    getStateFilePath(): string {
-        return path.join(this.getAppDataPath(), STATE_FILE);
+    return path.join(systemAppDataPath, packageJson.name)
+  }
+
+  getStateFilePath(): string {
+    return path.join(this.getAppDataPath(), STATE_FILE)
+  }
+
+  private createDataDirFsError(errorType: DataDirErrorType, specificPath = '') {
+    const actionStrs: { [x in DataDirErrorType]: string } = {
+      [DataDirErrorType.Init]: 'initialize',
+      [DataDirErrorType.Read]: 'read from',
+      [DataDirErrorType.Write]: 'write into',
     }
 
-    private createDataDirFsError(errorType: DataDirErrorType, specificPath: string = '') {
-        const actionStrs: { [x in DataDirErrorType]: string } = {
-            [DataDirErrorType.Init]: 'initialize',
-            [DataDirErrorType.Read]: 'read from',
-            [DataDirErrorType.Write]: 'write into'
-        };
+    const errorMsg =
+      `Unexpected error while trying to ${actionStrs[errorType]} the data directory.` +
+      `(${path.join(this.getAppDataPath(), specificPath)})! Permissions issue?`
 
-        const errorMsg =
-            `Unexpected error while trying to ${ actionStrs[errorType] } the data directory.`+
-            `(${ path.join(this.getAppDataPath(), specificPath) })! Permissions issue?`;
+    return new CLIError(errorMsg, { exit: ExitCodes.FsOperationFailed })
+  }
 
-        return new CLIError(errorMsg, { exit: ExitCodes.FsOperationFailed });
-    }
+  createDataReadError(specificPath = ''): CLIError {
+    return this.createDataDirFsError(DataDirErrorType.Read, specificPath)
+  }
 
-    createDataReadError(specificPath: string = ''): CLIError {
-        return this.createDataDirFsError(DataDirErrorType.Read, specificPath);
-    }
+  createDataWriteError(specificPath = ''): CLIError {
+    return this.createDataDirFsError(DataDirErrorType.Write, specificPath)
+  }
 
-    createDataWriteError(specificPath: string = ''): CLIError {
-        return this.createDataDirFsError(DataDirErrorType.Write, specificPath);
-    }
+  createDataDirInitError(specificPath = ''): CLIError {
+    return this.createDataDirFsError(DataDirErrorType.Init, specificPath)
+  }
 
-    createDataDirInitError(specificPath: string = ''): CLIError {
-        return this.createDataDirFsError(DataDirErrorType.Init, specificPath);
+  private initStateFs(): void {
+    if (!fs.existsSync(this.getAppDataPath())) {
+      fs.mkdirSync(this.getAppDataPath())
     }
-
-    private initStateFs(): void {
-        if (!fs.existsSync(this.getAppDataPath())) {
-            fs.mkdirSync(this.getAppDataPath());
-        }
-        if (!fs.existsSync(this.getStateFilePath())) {
-            fs.writeFileSync(this.getStateFilePath(), JSON.stringify(DEFAULT_STATE));
-        }
+    if (!fs.existsSync(this.getStateFilePath())) {
+      fs.writeFileSync(this.getStateFilePath(), JSON.stringify(DEFAULT_STATE))
     }
-
-    getPreservedState(): StateObject {
-        let preservedState: StateObject;
-        try {
-            preservedState = <StateObject> require(this.getStateFilePath());
-        } catch(e) {
-            throw this.createDataReadError();
-        }
-        // The state preserved in a file may be missing some required values ie.
-        // if the user previously used the older version of the software.
-        // That's why we combine it with default state before returing.
-        return { ...DEFAULT_STATE, ...preservedState };
+  }
+
+  getPreservedState(): StateObject {
+    let preservedState: StateObject
+    try {
+      preservedState = require(this.getStateFilePath()) as StateObject
+    } catch (e) {
+      throw this.createDataReadError()
     }
-
-    // Modifies preserved state. Uses file lock in order to avoid updating an older state.
-    // (which could potentialy change between read and write operation)
-    async setPreservedState(modifiedState: Partial<StateObject>): Promise<void> {
-        const stateFilePath = this.getStateFilePath();
-        const unlock = await lockFile.lock(stateFilePath);
-        let oldState: StateObject = this.getPreservedState();
-        let newState: StateObject = { ...oldState, ...modifiedState };
-        try {
-            fs.writeFileSync(stateFilePath, JSON.stringify(newState));
-        } catch(e) {
-            await unlock();
-            throw this.createDataWriteError();
-        }
-        await unlock();
+    // The state preserved in a file may be missing some required values ie.
+    // if the user previously used the older version of the software.
+    // That's why we combine it with default state before returing.
+    return { ...DEFAULT_STATE, ...preservedState }
+  }
+
+  // Modifies preserved state. Uses file lock in order to avoid updating an older state.
+  // (which could potentialy change between read and write operation)
+  async setPreservedState(modifiedState: Partial<StateObject>): Promise<void> {
+    const stateFilePath = this.getStateFilePath()
+    const unlock = await lockFile.lock(stateFilePath)
+    const oldState: StateObject = this.getPreservedState()
+    const newState: StateObject = { ...oldState, ...modifiedState }
+    try {
+      fs.writeFileSync(stateFilePath, JSON.stringify(newState))
+    } catch (e) {
+      await unlock()
+      throw this.createDataWriteError()
     }
-
-    async init() {
-        await super.init();
-        try {
-            await this.initStateFs();
-        } catch (e) {
-            throw this.createDataDirInitError();
-        }
+    await unlock()
+  }
+
+  async init() {
+    await super.init()
+    try {
+      await this.initStateFs()
+    } catch (e) {
+      throw this.createDataDirInitError()
     }
+  }
 }

+ 235 - 233
cli/src/base/WorkingGroupsCommandBase.ts

@@ -1,269 +1,271 @@
-import ExitCodes from '../ExitCodes';
-import AccountsCommandBase from './AccountsCommandBase';
-import { flags } from '@oclif/command';
-import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening, ApiMethodArg, ApiMethodNamedArgs, OpeningStatus, GroupApplication } from '../Types';
-import { apiModuleByGroup } from '../Api';
-import { CLIError } from '@oclif/errors';
-import fs from 'fs';
-import path from 'path';
-import _ from 'lodash';
-import { ApplicationStageKeys } from '@joystream/types/hiring';
-
-const DEFAULT_GROUP = WorkingGroups.StorageProviders;
-const DRAFTS_FOLDER = 'opening-drafts';
+import ExitCodes from '../ExitCodes'
+import AccountsCommandBase from './AccountsCommandBase'
+import { flags } from '@oclif/command'
+import {
+  WorkingGroups,
+  AvailableGroups,
+  NamedKeyringPair,
+  GroupMember,
+  GroupOpening,
+  ApiMethodArg,
+  ApiMethodNamedArgs,
+  OpeningStatus,
+  GroupApplication,
+} from '../Types'
+import { apiModuleByGroup } from '../Api'
+import { CLIError } from '@oclif/errors'
+import fs from 'fs'
+import path from 'path'
+import _ from 'lodash'
+import { ApplicationStageKeys } from '@joystream/types/hiring'
+
+const DEFAULT_GROUP = WorkingGroups.StorageProviders
+const DRAFTS_FOLDER = 'opening-drafts'
 
 /**
  * Abstract base class for commands related to working groups
  */
 export default abstract class WorkingGroupsCommandBase extends AccountsCommandBase {
-    group: WorkingGroups = DEFAULT_GROUP;
-
-    static flags = {
-        group: flags.string({
-            char: 'g',
-            description:
-                "The working group context in which the command should be executed\n" +
-                `Available values are: ${AvailableGroups.join(', ')}.`,
-            required: true,
-            default: DEFAULT_GROUP
-        }),
-    };
-
-    // Use when lead access is required in given command
-    async getRequiredLead(): Promise<GroupMember> {
-        let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
-        let lead = await this.getApi().groupLead(this.group);
-
-        if (!lead || lead.roleAccount.toString() !== selectedAccount.address) {
-            this.error('Lead access required for this command!', { exit: ExitCodes.AccessDenied });
-        }
-
-        return lead;
+  group: WorkingGroups = DEFAULT_GROUP
+
+  static flags = {
+    group: flags.string({
+      char: 'g',
+      description:
+        'The working group context in which the command should be executed\n' +
+        `Available values are: ${AvailableGroups.join(', ')}.`,
+      required: true,
+      default: DEFAULT_GROUP,
+    }),
+  }
+
+  // Use when lead access is required in given command
+  async getRequiredLead(): Promise<GroupMember> {
+    const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount()
+    const lead = await this.getApi().groupLead(this.group)
+
+    if (!lead || lead.roleAccount.toString() !== selectedAccount.address) {
+      this.error('Lead access required for this command!', { exit: ExitCodes.AccessDenied })
     }
 
-    // Use when worker access is required in given command
-    async getRequiredWorker(): Promise<GroupMember> {
-        let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
-        let groupMembers = await this.getApi().groupMembers(this.group);
-        let groupMembersByAccount = groupMembers.filter(m => m.roleAccount.toString() === selectedAccount.address);
-
-        if (!groupMembersByAccount.length) {
-            this.error('Worker access required for this command!', { exit: ExitCodes.AccessDenied });
-        }
-        else if (groupMembersByAccount.length === 1) {
-            return groupMembersByAccount[0];
-        }
-        else {
-            return await this.promptForWorker(groupMembersByAccount);
-        }
+    return lead
+  }
+
+  // Use when worker access is required in given command
+  async getRequiredWorker(): Promise<GroupMember> {
+    const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount()
+    const groupMembers = await this.getApi().groupMembers(this.group)
+    const groupMembersByAccount = groupMembers.filter((m) => m.roleAccount.toString() === selectedAccount.address)
+
+    if (!groupMembersByAccount.length) {
+      this.error('Worker access required for this command!', { exit: ExitCodes.AccessDenied })
+    } else if (groupMembersByAccount.length === 1) {
+      return groupMembersByAccount[0]
+    } else {
+      return await this.promptForWorker(groupMembersByAccount)
     }
-
-    // Use when member controller access is required, but one of the associated roles is expected to be selected
-    async getRequiredWorkerByMemberController(): Promise<GroupMember> {
-        const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
-        const memberIds = await this.getApi().getMemberIdsByControllerAccount(selectedAccount.address);
-        const controlledWorkers = (await this.getApi().groupMembers(this.group))
-            .filter(groupMember => memberIds.some(memberId => groupMember.memberId.eq(memberId)));
-
-        if (!controlledWorkers.length) {
-            this.error(
-                `Member controller account with some associated ${this.group} group roles needs to be selected!`,
-                { exit: ExitCodes.AccessDenied }
-            );
-        }
-        else if (controlledWorkers.length === 1) {
-            return controlledWorkers[0];
-        }
-        else {
-            return await this.promptForWorker(controlledWorkers);
-        }
-    }
-
-    async promptForWorker(groupMembers: GroupMember[]): Promise<GroupMember> {
-        const chosenWorkerIndex = await this.simplePrompt({
-            message: 'Choose the intended worker context:',
-            type: 'list',
-            choices: groupMembers.map((groupMember, index) => ({
-                name: `Worker ID ${ groupMember.workerId.toString() }`,
-                value: index
-            }))
-        });
-
-        return groupMembers[chosenWorkerIndex];
-    }
-
-    async promptForApplicationsToAccept(opening: GroupOpening): Promise<number[]> {
-        const acceptableApplications = opening.applications.filter(a => a.stage === ApplicationStageKeys.Active);
-        const acceptedApplications = await this.simplePrompt({
-            message: 'Select succesful applicants',
-            type: 'checkbox',
-            choices: acceptableApplications.map(a => ({
-                name: ` ${a.wgApplicationId}: ${a.member?.handle.toString()}`,
-                value: a.wgApplicationId,
-            }))
-        });
-
-        return acceptedApplications;
+  }
+
+  // Use when member controller access is required, but one of the associated roles is expected to be selected
+  async getRequiredWorkerByMemberController(): Promise<GroupMember> {
+    const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount()
+    const memberIds = await this.getApi().getMemberIdsByControllerAccount(selectedAccount.address)
+    const controlledWorkers = (await this.getApi().groupMembers(this.group)).filter((groupMember) =>
+      memberIds.some((memberId) => groupMember.memberId.eq(memberId))
+    )
+
+    if (!controlledWorkers.length) {
+      this.error(`Member controller account with some associated ${this.group} group roles needs to be selected!`, {
+        exit: ExitCodes.AccessDenied,
+      })
+    } else if (controlledWorkers.length === 1) {
+      return controlledWorkers[0]
+    } else {
+      return await this.promptForWorker(controlledWorkers)
     }
-
-    async promptForNewOpeningDraftName() {
-        let
-            draftName: string = '',
-            fileExists: boolean = false,
-            overrideConfirmed: boolean = false;
-
-        do {
-            draftName = await this.simplePrompt({
-                type: 'input',
-                message: 'Provide the draft name',
-                validate: val => (typeof val === 'string' && val.length >= 1) || 'Draft name is required!'
-            });
-
-            fileExists = fs.existsSync(this.getOpeningDraftPath(draftName));
-            if (fileExists) {
-                overrideConfirmed = await this.simplePrompt({
-                    type: 'confirm',
-                    message: 'Such draft already exists. Do you wish to override it?',
-                    default: false
-                });
-            }
-        } while(fileExists && !overrideConfirmed);
-
-        return draftName;
+  }
+
+  async promptForWorker(groupMembers: GroupMember[]): Promise<GroupMember> {
+    const chosenWorkerIndex = await this.simplePrompt({
+      message: 'Choose the intended worker context:',
+      type: 'list',
+      choices: groupMembers.map((groupMember, index) => ({
+        name: `Worker ID ${groupMember.workerId.toString()}`,
+        value: index,
+      })),
+    })
+
+    return groupMembers[chosenWorkerIndex]
+  }
+
+  async promptForApplicationsToAccept(opening: GroupOpening): Promise<number[]> {
+    const acceptableApplications = opening.applications.filter((a) => a.stage === ApplicationStageKeys.Active)
+    const acceptedApplications = await this.simplePrompt({
+      message: 'Select succesful applicants',
+      type: 'checkbox',
+      choices: acceptableApplications.map((a) => ({
+        name: ` ${a.wgApplicationId}: ${a.member?.handle.toString()}`,
+        value: a.wgApplicationId,
+      })),
+    })
+
+    return acceptedApplications
+  }
+
+  async promptForNewOpeningDraftName() {
+    let draftName = '',
+      fileExists = false,
+      overrideConfirmed = false
+
+    do {
+      draftName = await this.simplePrompt({
+        type: 'input',
+        message: 'Provide the draft name',
+        validate: (val) => (typeof val === 'string' && val.length >= 1) || 'Draft name is required!',
+      })
+
+      fileExists = fs.existsSync(this.getOpeningDraftPath(draftName))
+      if (fileExists) {
+        overrideConfirmed = await this.simplePrompt({
+          type: 'confirm',
+          message: 'Such draft already exists. Do you wish to override it?',
+          default: false,
+        })
+      }
+    } while (fileExists && !overrideConfirmed)
+
+    return draftName
+  }
+
+  async promptForOpeningDraft() {
+    let draftFiles: string[] = []
+    try {
+      draftFiles = fs.readdirSync(this.getOpeingDraftsPath())
+    } catch (e) {
+      throw this.createDataReadError(DRAFTS_FOLDER)
     }
-
-    async promptForOpeningDraft() {
-        let draftFiles: string[] = [];
-        try {
-            draftFiles = fs.readdirSync(this.getOpeingDraftsPath());
-        }
-        catch(e) {
-            throw this.createDataReadError(DRAFTS_FOLDER);
-        }
-        if (!draftFiles.length) {
-            throw new CLIError('No drafts available!', { exit: ExitCodes.FileNotFound });
-        }
-        const draftNames = draftFiles.map(fileName => _.startCase(fileName.replace('.json', '')));
-        const selectedDraftName = await this.simplePrompt({
-            message: 'Select a draft',
-            type: 'list',
-            choices: draftNames
-        });
-
-        return selectedDraftName;
+    if (!draftFiles.length) {
+      throw new CLIError('No drafts available!', { exit: ExitCodes.FileNotFound })
     }
+    const draftNames = draftFiles.map((fileName) => _.startCase(fileName.replace('.json', '')))
+    const selectedDraftName = await this.simplePrompt({
+      message: 'Select a draft',
+      type: 'list',
+      choices: draftNames,
+    })
 
-    async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
-        const opening = await this.getApi().groupOpening(this.group, id);
-
-        if (!opening.type.isOfType('Worker')) {
-            this.error('A lead can only manage Worker openings!',  { exit: ExitCodes.AccessDenied });
-        }
+    return selectedDraftName
+  }
 
-        if (requiredStatus && opening.stage.status !== requiredStatus) {
-            this.error(
-                `The opening needs to be in "${_.startCase(requiredStatus)}" stage! ` +
-                `This one is: "${_.startCase(opening.stage.status)}"`,
-                { exit: ExitCodes.InvalidInput }
-            );
-        }
+  async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
+    const opening = await this.getApi().groupOpening(this.group, id)
 
-        return opening;
+    if (!opening.type.isOfType('Worker')) {
+      this.error('A lead can only manage Worker openings!', { exit: ExitCodes.AccessDenied })
     }
 
-    // An alias for better code readibility in case we don't need the actual return value
-    validateOpeningForLeadAction = this.getOpeningForLeadAction
-
-    async getApplicationForLeadAction(id: number, requiredStatus?: ApplicationStageKeys): Promise<GroupApplication> {
-        const application = await this.getApi().groupApplication(this.group, id);
-        const opening = await this.getApi().groupOpening(this.group, application.wgOpeningId);
-
-        if (!opening.type.isOfType('Worker')) {
-            this.error('A lead can only manage Worker opening applications!',  { exit: ExitCodes.AccessDenied });
-        }
-
-        if (requiredStatus && application.stage !== requiredStatus) {
-            this.error(
-                `The application needs to have "${_.startCase(requiredStatus)}" status! ` +
-                `This one has: "${_.startCase(application.stage)}"`,
-                { exit: ExitCodes.InvalidInput }
-            );
-        }
-
-        return application;
+    if (requiredStatus && opening.stage.status !== requiredStatus) {
+      this.error(
+        `The opening needs to be in "${_.startCase(requiredStatus)}" stage! ` +
+          `This one is: "${_.startCase(opening.stage.status)}"`,
+        { exit: ExitCodes.InvalidInput }
+      )
     }
 
-    async getWorkerForLeadAction(id: number, requireStakeProfile: boolean = false) {
-        const groupMember = await this.getApi().groupMember(this.group, id);
-        const groupLead = await this.getApi().groupLead(this.group);
+    return opening
+  }
 
-        if (groupLead?.workerId.eq(groupMember.workerId)) {
-            this.error('A lead cannot manage his own role this way!', { exit: ExitCodes.AccessDenied });
-        }
+  // An alias for better code readibility in case we don't need the actual return value
+  validateOpeningForLeadAction = this.getOpeningForLeadAction
 
-        if (requireStakeProfile && !groupMember.stake) {
-            this.error('This worker has no associated role stake profile!', { exit: ExitCodes.InvalidInput });
-        }
+  async getApplicationForLeadAction(id: number, requiredStatus?: ApplicationStageKeys): Promise<GroupApplication> {
+    const application = await this.getApi().groupApplication(this.group, id)
+    const opening = await this.getApi().groupOpening(this.group, application.wgOpeningId)
 
-        return groupMember;
+    if (!opening.type.isOfType('Worker')) {
+      this.error('A lead can only manage Worker opening applications!', { exit: ExitCodes.AccessDenied })
     }
 
-    // Helper for better TS handling.
-    // We could also use some magic with conditional types instead, but those don't seem be very well supported yet.
-    async getWorkerWithStakeForLeadAction(id: number) {
-        return (await this.getWorkerForLeadAction(id, true)) as (GroupMember & Required<Pick<GroupMember, 'stake'>>);
+    if (requiredStatus && application.stage !== requiredStatus) {
+      this.error(
+        `The application needs to have "${_.startCase(requiredStatus)}" status! ` +
+          `This one has: "${_.startCase(application.stage)}"`,
+        { exit: ExitCodes.InvalidInput }
+      )
     }
 
-    loadOpeningDraftParams(draftName: string): ApiMethodNamedArgs {
-        const draftFilePath = this.getOpeningDraftPath(draftName);
-        const params = this.extrinsicArgsFromDraft(
-            apiModuleByGroup[this.group],
-            'addOpening',
-            draftFilePath
-        );
+    return application
+  }
 
-        return params;
-    }
+  async getWorkerForLeadAction(id: number, requireStakeProfile = false) {
+    const groupMember = await this.getApi().groupMember(this.group, id)
+    const groupLead = await this.getApi().groupLead(this.group)
 
-    getOpeingDraftsPath() {
-        return path.join(this.getAppDataPath(), DRAFTS_FOLDER);
+    if (groupLead?.workerId.eq(groupMember.workerId)) {
+      this.error('A lead cannot manage his own role this way!', { exit: ExitCodes.AccessDenied })
     }
 
-    getOpeningDraftPath(draftName: string) {
-        return path.join(this.getOpeingDraftsPath(), _.snakeCase(draftName)+'.json');
+    if (requireStakeProfile && !groupMember.stake) {
+      this.error('This worker has no associated role stake profile!', { exit: ExitCodes.InvalidInput })
     }
 
-    saveOpeningDraft(draftName: string, params: ApiMethodArg[]) {
-        const paramsJson = JSON.stringify(
-            params.map(p => p.toJSON()),
-            null,
-            2
-        );
-
-        try {
-            fs.writeFileSync(this.getOpeningDraftPath(draftName), paramsJson);
-        } catch(e) {
-            throw this.createDataWriteError(DRAFTS_FOLDER);
-        }
+    return groupMember
+  }
+
+  // Helper for better TS handling.
+  // We could also use some magic with conditional types instead, but those don't seem be very well supported yet.
+  async getWorkerWithStakeForLeadAction(id: number) {
+    return (await this.getWorkerForLeadAction(id, true)) as GroupMember & Required<Pick<GroupMember, 'stake'>>
+  }
+
+  loadOpeningDraftParams(draftName: string): ApiMethodNamedArgs {
+    const draftFilePath = this.getOpeningDraftPath(draftName)
+    const params = this.extrinsicArgsFromDraft(apiModuleByGroup[this.group], 'addOpening', draftFilePath)
+
+    return params
+  }
+
+  getOpeingDraftsPath() {
+    return path.join(this.getAppDataPath(), DRAFTS_FOLDER)
+  }
+
+  getOpeningDraftPath(draftName: string) {
+    return path.join(this.getOpeingDraftsPath(), _.snakeCase(draftName) + '.json')
+  }
+
+  saveOpeningDraft(draftName: string, params: ApiMethodArg[]) {
+    const paramsJson = JSON.stringify(
+      params.map((p) => p.toJSON()),
+      null,
+      2
+    )
+
+    try {
+      fs.writeFileSync(this.getOpeningDraftPath(draftName), paramsJson)
+    } catch (e) {
+      throw this.createDataWriteError(DRAFTS_FOLDER)
     }
+  }
 
-    private initOpeningDraftsDir(): void {
-        if (!fs.existsSync(this.getOpeingDraftsPath())) {
-            fs.mkdirSync(this.getOpeingDraftsPath());
-        }
+  private initOpeningDraftsDir(): void {
+    if (!fs.existsSync(this.getOpeingDraftsPath())) {
+      fs.mkdirSync(this.getOpeingDraftsPath())
     }
-
-    async init() {
-        await super.init();
-        try {
-            this.initOpeningDraftsDir();
-        } catch (e) {
-            throw this.createDataDirInitError();
-        }
-        const { flags } = this.parse(this.constructor as typeof WorkingGroupsCommandBase);
-        if (!AvailableGroups.includes(flags.group as any)) {
-            throw new CLIError(`Invalid group! Available values are: ${AvailableGroups.join(', ')}`, { exit: ExitCodes.InvalidInput });
-        }
-        this.group = flags.group as WorkingGroups;
+  }
+
+  async init() {
+    await super.init()
+    try {
+      this.initOpeningDraftsDir()
+    } catch (e) {
+      throw this.createDataDirInitError()
+    }
+    const { flags } = this.parse(this.constructor as typeof WorkingGroupsCommandBase)
+    if (!AvailableGroups.includes(flags.group as any)) {
+      throw new CLIError(`Invalid group! Available values are: ${AvailableGroups.join(', ')}`, {
+        exit: ExitCodes.InvalidInput,
+      })
     }
+    this.group = flags.group as WorkingGroups
+  }
 }

+ 25 - 25
cli/src/commands/account/choose.ts

@@ -1,33 +1,33 @@
-import AccountsCommandBase from '../../base/AccountsCommandBase';
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
-import { NamedKeyringPair } from '../../Types';
-import { flags } from '@oclif/command';
+import AccountsCommandBase from '../../base/AccountsCommandBase'
+import chalk from 'chalk'
+import ExitCodes from '../../ExitCodes'
+import { NamedKeyringPair } from '../../Types'
+import { flags } from '@oclif/command'
 
 export default class AccountChoose extends AccountsCommandBase {
-    static description = 'Choose default account to use in the CLI';
-    static flags = {
-        showSpecial: flags.boolean({
-            description: 'Whether to show special (DEV chain) accounts',
-            required: false
-        }),
-    };
+  static description = 'Choose default account to use in the CLI'
+  static flags = {
+    showSpecial: flags.boolean({
+      description: 'Whether to show special (DEV chain) accounts',
+      required: false,
+    }),
+  }
 
-    async run() {
-        const { showSpecial } = this.parse(AccountChoose).flags;
-        const accounts: NamedKeyringPair[] = this.fetchAccounts(showSpecial);
-        const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount();
+  async run() {
+    const { showSpecial } = this.parse(AccountChoose).flags
+    const accounts: NamedKeyringPair[] = this.fetchAccounts(showSpecial)
+    const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount()
 
-        this.log(chalk.white(`Found ${ accounts.length } existing accounts...\n`));
+    this.log(chalk.white(`Found ${accounts.length} existing accounts...\n`))
 
-        if (accounts.length === 0) {
-            this.warn('No account to choose from. Add accont using account:import or account:create.');
-            this.exit(ExitCodes.NoAccountFound);
-        }
+    if (accounts.length === 0) {
+      this.warn('No account to choose from. Add accont using account:import or account:create.')
+      this.exit(ExitCodes.NoAccountFound)
+    }
 
-        const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, selectedAccount);
+    const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, selectedAccount)
 
-        await this.setSelectedAccount(choosenAccount);
-        this.log(chalk.greenBright("\nAccount switched!"));
-    }
+    await this.setSelectedAccount(choosenAccount)
+    this.log(chalk.greenBright('\nAccount switched!'))
   }
+}

+ 35 - 35
cli/src/commands/account/create.ts

@@ -1,47 +1,47 @@
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
-import AccountsCommandBase from '../../base/AccountsCommandBase';
-import { Keyring } from '@polkadot/api';
+import chalk from 'chalk'
+import ExitCodes from '../../ExitCodes'
+import AccountsCommandBase from '../../base/AccountsCommandBase'
+import { Keyring } from '@polkadot/api'
 import { mnemonicGenerate } from '@polkadot/util-crypto'
-import { NamedKeyringPair } from '../../Types';
+import { NamedKeyringPair } from '../../Types'
 
 type AccountCreateArgs = {
-    name: string
-};
+  name: string
+}
 
 export default class AccountCreate extends AccountsCommandBase {
-    static description = 'Create new account';
-
-    static args = [
-        {
-            name: 'name',
-            required: true,
-            description: 'Account name'
-        },
-    ];
-
-    validatePass(password: string, password2: string): void {
-        if (password !== password2) this.error('Passwords are not the same!', { exit: ExitCodes.InvalidInput });
-        if (!password) this.error('You didn\'t provide a password', { exit: ExitCodes.InvalidInput });
-    }
+  static description = 'Create new account'
+
+  static args = [
+    {
+      name: 'name',
+      required: true,
+      description: 'Account name',
+    },
+  ]
+
+  validatePass(password: string, password2: string): void {
+    if (password !== password2) this.error('Passwords are not the same!', { exit: ExitCodes.InvalidInput })
+    if (!password) this.error("You didn't provide a password", { exit: ExitCodes.InvalidInput })
+  }
 
-    async run() {
-        const args: AccountCreateArgs = <AccountCreateArgs> this.parse(AccountCreate).args;
-        const keyring: Keyring = new Keyring();
-        const mnemonic: string = mnemonicGenerate();
+  async run() {
+    const args: AccountCreateArgs = this.parse(AccountCreate).args as AccountCreateArgs
+    const keyring: Keyring = new Keyring()
+    const mnemonic: string = mnemonicGenerate()
 
-        keyring.addFromMnemonic(mnemonic, { name: args.name, whenCreated: Date.now() });
-        const keys: NamedKeyringPair = <NamedKeyringPair> keyring.pairs[0]; // We assigned the name above
+    keyring.addFromMnemonic(mnemonic, { name: args.name, whenCreated: Date.now() })
+    const keys: NamedKeyringPair = keyring.pairs[0] as NamedKeyringPair // We assigned the name above
 
-        const password = await this.promptForPassword('Set your account\'s password');
-        const password2 = await this.promptForPassword('Confirm your password');
+    const password = await this.promptForPassword("Set your account's password")
+    const password2 = await this.promptForPassword('Confirm your password')
 
-        this.validatePass(password, password2);
+    this.validatePass(password, password2)
 
-        this.saveAccount(keys, password);
+    this.saveAccount(keys, password)
 
-        this.log(chalk.greenBright(`\nAccount succesfully created!`));
-        this.log(chalk.white(`${chalk.bold('Name:    ') }${ args.name }`));
-        this.log(chalk.white(`${chalk.bold('Address: ') }${ keys.address }`));
-    }
+    this.log(chalk.greenBright(`\nAccount succesfully created!`))
+    this.log(chalk.white(`${chalk.bold('Name:    ')}${args.name}`))
+    this.log(chalk.white(`${chalk.bold('Address: ')}${keys.address}`))
   }
+}

+ 34 - 34
cli/src/commands/account/current.ts

@@ -1,41 +1,41 @@
-import AccountsCommandBase from '../../base/AccountsCommandBase';
-import { AccountSummary, NameValueObj, NamedKeyringPair } from '../../Types';
-import { DerivedBalances } from '@polkadot/api-derive/types';
-import { displayHeader, displayNameValueTable } from '../../helpers/display';
-import { formatBalance } from '@polkadot/util';
-import moment from 'moment';
+import AccountsCommandBase from '../../base/AccountsCommandBase'
+import { AccountSummary, NameValueObj, NamedKeyringPair } from '../../Types'
+import { DerivedBalances } from '@polkadot/api-derive/types'
+import { displayHeader, displayNameValueTable } from '../../helpers/display'
+import { formatBalance } from '@polkadot/util'
+import moment from 'moment'
 
 export default class AccountCurrent extends AccountsCommandBase {
-    static description = 'Display information about currently choosen default account';
-    static aliases = ['account:info', 'account:default'];
+  static description = 'Display information about currently choosen default account'
+  static aliases = ['account:info', 'account:default']
 
-    async run() {
-        const currentAccount: NamedKeyringPair = await this.getRequiredSelectedAccount(false);
-        const summary: AccountSummary = await this.getApi().getAccountSummary(currentAccount.address);
+  async run() {
+    const currentAccount: NamedKeyringPair = await this.getRequiredSelectedAccount(false)
+    const summary: AccountSummary = await this.getApi().getAccountSummary(currentAccount.address)
 
-        displayHeader('Account information');
-        const creationDate: string = currentAccount.meta.whenCreated ?
-            moment(currentAccount.meta.whenCreated).format('YYYY-MM-DD HH:mm:ss')
-            : '?';
-        const accountRows: NameValueObj[] = [
-            { name: 'Account name:', value: currentAccount.meta.name },
-            { name: 'Address:', value: currentAccount.address },
-            { name: 'Created:', value: creationDate }
-        ];
-        displayNameValueTable(accountRows);
+    displayHeader('Account information')
+    const creationDate: string = currentAccount.meta.whenCreated
+      ? moment(currentAccount.meta.whenCreated).format('YYYY-MM-DD HH:mm:ss')
+      : '?'
+    const accountRows: NameValueObj[] = [
+      { name: 'Account name:', value: currentAccount.meta.name },
+      { name: 'Address:', value: currentAccount.address },
+      { name: 'Created:', value: creationDate },
+    ]
+    displayNameValueTable(accountRows)
 
-        displayHeader('Balances');
-        const balances: DerivedBalances = summary.balances;
-        let balancesRows: NameValueObj[] = [
-            { name: 'Total balance:', value: formatBalance(balances.votingBalance) },
-            { name: 'Transferable balance:', value: formatBalance(balances.availableBalance) }
-        ];
-        if (balances.lockedBalance.gtn(0)) {
-            balancesRows.push({ name: 'Locked balance:', value: formatBalance(balances.lockedBalance) });
-        }
-        if (balances.reservedBalance.gtn(0)) {
-            balancesRows.push({ name: 'Reserved balance:', value: formatBalance(balances.reservedBalance) });
-        }
-        displayNameValueTable(balancesRows);
+    displayHeader('Balances')
+    const balances: DerivedBalances = summary.balances
+    const balancesRows: NameValueObj[] = [
+      { name: 'Total balance:', value: formatBalance(balances.votingBalance) },
+      { name: 'Transferable balance:', value: formatBalance(balances.availableBalance) },
+    ]
+    if (balances.lockedBalance.gtn(0)) {
+      balancesRows.push({ name: 'Locked balance:', value: formatBalance(balances.lockedBalance) })
     }
+    if (balances.reservedBalance.gtn(0)) {
+      balancesRows.push({ name: 'Reserved balance:', value: formatBalance(balances.reservedBalance) })
+    }
+    displayNameValueTable(balancesRows)
   }
+}

+ 62 - 61
cli/src/commands/account/export.ts

@@ -1,73 +1,74 @@
-import fs from 'fs';
-import chalk from 'chalk';
-import path from 'path';
-import ExitCodes from '../../ExitCodes';
-import AccountsCommandBase from '../../base/AccountsCommandBase';
-import { flags } from '@oclif/command';
-import { NamedKeyringPair } from '../../Types';
+import fs from 'fs'
+import chalk from 'chalk'
+import path from 'path'
+import ExitCodes from '../../ExitCodes'
+import AccountsCommandBase from '../../base/AccountsCommandBase'
+import { flags } from '@oclif/command'
+import { NamedKeyringPair } from '../../Types'
 
-type AccountExportFlags = { all: boolean };
-type AccountExportArgs = { path: string };
+type AccountExportFlags = { all: boolean }
+type AccountExportArgs = { path: string }
 
 export default class AccountExport extends AccountsCommandBase {
-    static description = 'Export account(s) to given location';
-    static MULTI_EXPORT_FOLDER_NAME = 'exported_accounts';
+  static description = 'Export account(s) to given location'
+  static MULTI_EXPORT_FOLDER_NAME = 'exported_accounts'
 
-    static args = [
-        {
-            name: 'path',
-            required: true,
-            description: 'Path where the exported files should be placed'
-        }
-    ];
+  static args = [
+    {
+      name: 'path',
+      required: true,
+      description: 'Path where the exported files should be placed',
+    },
+  ]
 
-    static flags = {
-        all: flags.boolean({
-            char: 'a',
-            description: `If provided, exports all existing accounts into "${ AccountExport.MULTI_EXPORT_FOLDER_NAME }" folder inside given path`,
-        }),
-    };
-
-    exportAccount(account: NamedKeyringPair, destPath: string): string {
-        const sourceFilePath: string = this.getAccountFilePath(account);
-        const destFilePath: string = path.join(destPath, this.generateAccountFilename(account));
-        try {
-            fs.copyFileSync(sourceFilePath, destFilePath);
-        }
-        catch (e) {
-            this.error(
-                `Error while trying to copy into the export file: (${ destFilePath }). Permissions issue?`,
-                { exit: ExitCodes.FsOperationFailed }
-            );
-        }
+  static flags = {
+    all: flags.boolean({
+      char: 'a',
+      description: `If provided, exports all existing accounts into "${AccountExport.MULTI_EXPORT_FOLDER_NAME}" folder inside given path`,
+    }),
+  }
 
-        return destFilePath;
+  exportAccount(account: NamedKeyringPair, destPath: string): string {
+    const sourceFilePath: string = this.getAccountFilePath(account)
+    const destFilePath: string = path.join(destPath, this.generateAccountFilename(account))
+    try {
+      fs.copyFileSync(sourceFilePath, destFilePath)
+    } catch (e) {
+      this.error(`Error while trying to copy into the export file: (${destFilePath}). Permissions issue?`, {
+        exit: ExitCodes.FsOperationFailed,
+      })
     }
 
-    async run() {
-        const args: AccountExportArgs = <AccountExportArgs> this.parse(AccountExport).args;
-        const flags: AccountExportFlags = <AccountExportFlags> this.parse(AccountExport).flags;
-        const accounts: NamedKeyringPair[] = this.fetchAccounts();
+    return destFilePath
+  }
 
-        if (!accounts.length) {
-            this.error('No accounts found!', { exit: ExitCodes.NoAccountFound });
-        }
+  async run() {
+    const args: AccountExportArgs = this.parse(AccountExport).args as AccountExportArgs
+    const flags: AccountExportFlags = this.parse(AccountExport).flags as AccountExportFlags
+    const accounts: NamedKeyringPair[] = this.fetchAccounts()
+
+    if (!accounts.length) {
+      this.error('No accounts found!', { exit: ExitCodes.NoAccountFound })
+    }
 
-        if (flags.all) {
-            const destPath: string = path.join(args.path, AccountExport.MULTI_EXPORT_FOLDER_NAME);
-            try {
-                if (!fs.existsSync(destPath)) fs.mkdirSync(destPath);
-            } catch(e) {
-                this.error(`Failed to create the export folder (${ destPath })`, { exit: ExitCodes.FsOperationFailed });
-            }
-            for (let account of accounts) this.exportAccount(account, destPath);
-            this.log(chalk.greenBright(`All accounts succesfully exported succesfully to: ${ chalk.white(destPath) }!`));
-        }
-        else {
-            const destPath: string = args.path;
-            const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, null, 'Select an account to export');
-            const exportedFilePath: string = this.exportAccount(choosenAccount, destPath);
-            this.log(chalk.greenBright(`Account succesfully exported to: ${ chalk.white(exportedFilePath) }`));
-        }
+    if (flags.all) {
+      const destPath: string = path.join(args.path, AccountExport.MULTI_EXPORT_FOLDER_NAME)
+      try {
+        if (!fs.existsSync(destPath)) fs.mkdirSync(destPath)
+      } catch (e) {
+        this.error(`Failed to create the export folder (${destPath})`, { exit: ExitCodes.FsOperationFailed })
+      }
+      for (const account of accounts) this.exportAccount(account, destPath)
+      this.log(chalk.greenBright(`All accounts succesfully exported succesfully to: ${chalk.white(destPath)}!`))
+    } else {
+      const destPath: string = args.path
+      const choosenAccount: NamedKeyringPair = await this.promptForAccount(
+        accounts,
+        null,
+        'Select an account to export'
+      )
+      const exportedFilePath: string = this.exportAccount(choosenAccount, destPath)
+      this.log(chalk.greenBright(`Account succesfully exported to: ${chalk.white(exportedFilePath)}`))
     }
   }
+}

+ 23 - 21
cli/src/commands/account/forget.ts

@@ -1,29 +1,31 @@
-import fs from 'fs';
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
-import AccountsCommandBase from '../../base/AccountsCommandBase';
-import { NamedKeyringPair } from '../../Types';
+import fs from 'fs'
+import chalk from 'chalk'
+import ExitCodes from '../../ExitCodes'
+import AccountsCommandBase from '../../base/AccountsCommandBase'
+import { NamedKeyringPair } from '../../Types'
 
 export default class AccountForget extends AccountsCommandBase {
-    static description = 'Forget (remove) account from the list of available accounts';
+  static description = 'Forget (remove) account from the list of available accounts'
 
-    async run() {
-        const accounts: NamedKeyringPair[] = this.fetchAccounts();
+  async run() {
+    const accounts: NamedKeyringPair[] = this.fetchAccounts()
 
-        if (!accounts.length) {
-            this.error('No accounts found!', { exit: ExitCodes.NoAccountFound });
-        }
-
-        const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, null, 'Select an account to forget');
-        await this.requireConfirmation('Are you sure you want this account to be forgotten?');
+    if (!accounts.length) {
+      this.error('No accounts found!', { exit: ExitCodes.NoAccountFound })
+    }
 
-        const accountFilePath: string = this.getAccountFilePath(choosenAccount);
-        try {
-            fs.unlinkSync(accountFilePath);
-        } catch (e) {
-            this.error(`Could not remove account file (${ accountFilePath }). Permissions issue?`, { exit: ExitCodes.FsOperationFailed });
-        }
+    const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, null, 'Select an account to forget')
+    await this.requireConfirmation('Are you sure you want this account to be forgotten?')
 
-        this.log(chalk.greenBright(`\nAccount has been forgotten!`))
+    const accountFilePath: string = this.getAccountFilePath(choosenAccount)
+    try {
+      fs.unlinkSync(accountFilePath)
+    } catch (e) {
+      this.error(`Could not remove account file (${accountFilePath}). Permissions issue?`, {
+        exit: ExitCodes.FsOperationFailed,
+      })
     }
+
+    this.log(chalk.greenBright(`\nAccount has been forgotten!`))
   }
+}

+ 34 - 36
cli/src/commands/account/import.ts

@@ -1,46 +1,44 @@
-import fs from 'fs';
-import chalk from 'chalk';
-import path from 'path';
-import ExitCodes from '../../ExitCodes';
-import AccountsCommandBase from '../../base/AccountsCommandBase';
-import { NamedKeyringPair } from '../../Types';
+import fs from 'fs'
+import chalk from 'chalk'
+import path from 'path'
+import ExitCodes from '../../ExitCodes'
+import AccountsCommandBase from '../../base/AccountsCommandBase'
+import { NamedKeyringPair } from '../../Types'
 
 type AccountImportArgs = {
-    backupFilePath: string
-};
+  backupFilePath: string
+}
 
 export default class AccountImport extends AccountsCommandBase {
-    static description = 'Import account using JSON backup file';
+  static description = 'Import account using JSON backup file'
 
-    static args = [
-        {
-            name: 'backupFilePath',
-            required: true,
-            description: 'Path to account backup JSON file'
-        },
-    ];
+  static args = [
+    {
+      name: 'backupFilePath',
+      required: true,
+      description: 'Path to account backup JSON file',
+    },
+  ]
 
-    async run() {
-        const args: AccountImportArgs = <AccountImportArgs> this.parse(AccountImport).args;
-        const backupAcc: NamedKeyringPair = this.fetchAccountFromJsonFile(args.backupFilePath);
-        const accountName: string = backupAcc.meta.name;
-        const accountAddress: string = backupAcc.address;
+  async run() {
+    const args: AccountImportArgs = this.parse(AccountImport).args as AccountImportArgs
+    const backupAcc: NamedKeyringPair = this.fetchAccountFromJsonFile(args.backupFilePath)
+    const accountName: string = backupAcc.meta.name
+    const accountAddress: string = backupAcc.address
 
-        const sourcePath: string = args.backupFilePath;
-        const destPath: string = path.join(this.getAccountsDirPath(), this.generateAccountFilename(backupAcc));
+    const sourcePath: string = args.backupFilePath
+    const destPath: string = path.join(this.getAccountsDirPath(), this.generateAccountFilename(backupAcc))
 
-        try {
-            fs.copyFileSync(sourcePath, destPath);
-        }
-        catch (e) {
-            this.error(
-                'Unexpected error while trying to copy input file! Permissions issue?',
-                { exit: ExitCodes.FsOperationFailed }
-            );
-        }
-
-        this.log(chalk.bold.greenBright(`ACCOUNT IMPORTED SUCCESFULLY!`));
-        this.log(chalk.bold.white(`NAME:    `), accountName);
-        this.log(chalk.bold.white(`ADDRESS: `), accountAddress);
+    try {
+      fs.copyFileSync(sourcePath, destPath)
+    } catch (e) {
+      this.error('Unexpected error while trying to copy input file! Permissions issue?', {
+        exit: ExitCodes.FsOperationFailed,
+      })
     }
+
+    this.log(chalk.bold.greenBright(`ACCOUNT IMPORTED SUCCESFULLY!`))
+    this.log(chalk.bold.white(`NAME:    `), accountName)
+    this.log(chalk.bold.white(`ADDRESS: `), accountAddress)
   }
+}

+ 53 - 54
cli/src/commands/account/transferTokens.ts

@@ -1,68 +1,67 @@
-import BN from 'bn.js';
-import AccountsCommandBase from '../../base/AccountsCommandBase';
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
-import { formatBalance } from '@polkadot/util';
-import { Hash } from '@polkadot/types/interfaces';
-import { NamedKeyringPair } from '../../Types';
-import { checkBalance, validateAddress } from '../../helpers/validation';
-import { DerivedBalances } from '@polkadot/api-derive/types';
+import BN from 'bn.js'
+import AccountsCommandBase from '../../base/AccountsCommandBase'
+import chalk from 'chalk'
+import ExitCodes from '../../ExitCodes'
+import { formatBalance } from '@polkadot/util'
+import { Hash } from '@polkadot/types/interfaces'
+import { NamedKeyringPair } from '../../Types'
+import { checkBalance, validateAddress } from '../../helpers/validation'
+import { DerivedBalances } from '@polkadot/api-derive/types'
 
 type AccountTransferArgs = {
-    recipient: string,
-    amount: string
-};
+  recipient: string
+  amount: string
+}
 
 export default class AccountTransferTokens extends AccountsCommandBase {
-    static description = 'Transfer tokens from currently choosen account';
+  static description = 'Transfer tokens from currently choosen account'
 
-    static args = [
-        {
-            name: 'recipient',
-            required: true,
-            description: 'Address of the transfer recipient'
-        },
-        {
-            name: 'amount',
-            required: true,
-            description: 'Amount of tokens to transfer'
-        },
-    ];
+  static args = [
+    {
+      name: 'recipient',
+      required: true,
+      description: 'Address of the transfer recipient',
+    },
+    {
+      name: 'amount',
+      required: true,
+      description: 'Amount of tokens to transfer',
+    },
+  ]
 
-    async run() {
-        const args: AccountTransferArgs = <AccountTransferArgs> this.parse(AccountTransferTokens).args;
-        const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
-        const amountBN: BN = new BN(args.amount);
+  async run() {
+    const args: AccountTransferArgs = this.parse(AccountTransferTokens).args as AccountTransferArgs
+    const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount()
+    const amountBN: BN = new BN(args.amount)
 
-        // Initial validation
-        validateAddress(args.recipient, 'Invalid recipient address');
-        const accBalances: DerivedBalances = (await this.getApi().getAccountsBalancesInfo([ selectedAccount.address ]))[0];
-        checkBalance(accBalances, amountBN);
+    // Initial validation
+    validateAddress(args.recipient, 'Invalid recipient address')
+    const accBalances: DerivedBalances = (await this.getApi().getAccountsBalancesInfo([selectedAccount.address]))[0]
+    checkBalance(accBalances, amountBN)
 
-        await this.requestAccountDecoding(selectedAccount);
+    await this.requestAccountDecoding(selectedAccount)
 
-        this.log(chalk.white('Estimating fee...'));
-        let estimatedFee: BN;
-        try {
-            estimatedFee = await this.getApi().estimateFee(selectedAccount, args.recipient, amountBN);
-        }
-        catch (e) {
-            this.error('Could not estimate the fee.', { exit: ExitCodes.UnexpectedException });
-        }
-        const totalAmount: BN = amountBN.add(estimatedFee);
-        this.log(chalk.white('Estimated fee:', formatBalance(estimatedFee)));
-        this.log(chalk.white('Total transfer amount:', formatBalance(totalAmount)));
+    this.log(chalk.white('Estimating fee...'))
+    let estimatedFee: BN
+    try {
+      estimatedFee = await this.getApi().estimateFee(selectedAccount, args.recipient, amountBN)
+    } catch (e) {
+      this.error('Could not estimate the fee.', { exit: ExitCodes.UnexpectedException })
+    }
+    const totalAmount: BN = amountBN.add(estimatedFee)
+    this.log(chalk.white('Estimated fee:', formatBalance(estimatedFee)))
+    this.log(chalk.white('Total transfer amount:', formatBalance(totalAmount)))
 
-        checkBalance(accBalances, totalAmount);
+    checkBalance(accBalances, totalAmount)
 
-        await this.requireConfirmation('Do you confirm the transfer?');
+    await this.requireConfirmation('Do you confirm the transfer?')
 
-        try {
-            const txHash: Hash = await this.getApi().transfer(selectedAccount, args.recipient, amountBN);
-            this.log(chalk.greenBright('Transaction succesfully sent!'));
-            this.log(chalk.white('Hash:', txHash.toString()));
-        } catch (e) {
-            this.error('Could not send the transaction.', { exit: ExitCodes.UnexpectedException });
-        }
+    try {
+      const txHash: Hash = await this.getApi().transfer(selectedAccount, args.recipient, amountBN)
+      this.log(chalk.greenBright('Transaction succesfully sent!'))
+      this.log(chalk.white('Hash:', txHash.toString()))
+    } catch (e) {
+      this.error('Could not send the transaction.', { exit: ExitCodes.UnexpectedException })
     }
   }
+}

+ 7 - 8
cli/src/commands/api/getUri.ts

@@ -1,12 +1,11 @@
-import StateAwareCommandBase from '../../base/StateAwareCommandBase';
-import chalk from 'chalk';
-
+import StateAwareCommandBase from '../../base/StateAwareCommandBase'
+import chalk from 'chalk'
 
 export default class ApiGetUri extends StateAwareCommandBase {
-    static description = 'Get current api WS provider uri';
+  static description = 'Get current api WS provider uri'
 
-    async run() {
-        const currentUri:string = this.getPreservedState().apiUri;
-        this.log(chalk.green(currentUri));
-    }
+  async run() {
+    const currentUri: string = this.getPreservedState().apiUri
+    this.log(chalk.green(currentUri))
   }
+}

+ 210 - 203
cli/src/commands/api/inspect.ts

@@ -1,227 +1,234 @@
-import { flags } from '@oclif/command';
-import { CLIError } from '@oclif/errors';
-import { displayNameValueTable } from '../../helpers/display';
-import { ApiPromise } from '@polkadot/api';
-import { Option } from '@polkadot/types';
-import { Codec } from '@polkadot/types/types';
-import { ConstantCodec } from '@polkadot/api-metadata/consts/types';
-import ExitCodes from '../../ExitCodes';
-import chalk from 'chalk';
-import { NameValueObj, ApiMethodArg } from '../../Types';
-import ApiCommandBase from '../../base/ApiCommandBase';
+import { flags } from '@oclif/command'
+import { CLIError } from '@oclif/errors'
+import { displayNameValueTable } from '../../helpers/display'
+import { ApiPromise } from '@polkadot/api'
+import { Option } from '@polkadot/types'
+import { Codec } from '@polkadot/types/types'
+import { ConstantCodec } from '@polkadot/api-metadata/consts/types'
+import ExitCodes from '../../ExitCodes'
+import chalk from 'chalk'
+import { NameValueObj, ApiMethodArg } from '../../Types'
+import ApiCommandBase from '../../base/ApiCommandBase'
 
 // Command flags type
 type ApiInspectFlags = {
-    type: string,
-    module: string,
-    method: string,
-    exec: boolean,
-    callArgs: string
-};
+  type: string
+  module: string
+  method: string
+  exec: boolean
+  callArgs: string
+}
 
 // Currently "inspectable" api types
-const TYPES_AVAILABLE = [
-    'query',
-    'consts',
-] as const;
+const TYPES_AVAILABLE = ['query', 'consts'] as const
 
 // String literals type based on TYPES_AVAILABLE const.
 // It works as if we specified: type ApiType = 'query' | 'consts'...;
-type ApiType = typeof TYPES_AVAILABLE[number];
+type ApiType = typeof TYPES_AVAILABLE[number]
 
 export default class ApiInspect extends ApiCommandBase {
-    static description =
-        'Lists available node API modules/methods and/or their description(s), '+
-        'or calls one of the API methods (depending on provided arguments and flags)';
-
-    static examples = [
-        '$ api:inspect',
-        '$ api:inspect -t=query',
-        '$ api:inspect -t=query -M=members',
-        '$ api:inspect -t=query -M=members -m=memberProfile',
-        '$ api:inspect -t=query -M=members -m=memberProfile -e',
-        '$ api:inspect -t=query -M=members -m=memberProfile -e -a=1',
-    ];
-
-    static flags = {
-        type: flags.string({
-            char: 't',
-            description:
-                'Specifies the type/category of the inspected request (ie. "query", "consts" etc.).\n'+
-                'If no "--module" flag is provided then all available modules in that type will be listed.\n'+
-                'If this flag is not provided then all available types will be listed.',
-        }),
-        module: flags.string({
-            char: 'M',
-            description:
-                'Specifies the api module, ie. "system", "staking" etc.\n'+
-                'If no "--method" flag is provided then all methods in that module will be listed along with the descriptions.',
-            dependsOn: ['type'],
-        }),
-        method: flags.string({
-            char: 'm',
-            description: 'Specifies the api method to call/describe.',
-            dependsOn: ['module'],
-        }),
-        exec: flags.boolean({
-            char: 'e',
-            description: 'Provide this flag if you want to execute the actual call, instead of displaying the method description (which is default)',
-            dependsOn: ['method'],
-        }),
-        callArgs: flags.string({
-            char: 'a',
-            description:
-                'Specifies the arguments to use when calling a method. Multiple arguments can be separated with a comma, ie. "-a=arg1,arg2".\n'+
-                'You can omit this flag even if the method requires some aguments.\n'+
-                'In that case you will be promted to provide value for each required argument.\n' +
-                'Ommiting this flag is recommended when input parameters are of more complex types (and it\'s hard to specify them as just simple comma-separated strings)',
-            dependsOn: ['exec'],
-        })
-    };
-
-    getMethodMeta(apiType: ApiType, apiModule: string, apiMethod: string) {
-        if (apiType === 'query') {
-            return this.getOriginalApi().query[apiModule][apiMethod].creator.meta;
-        }
-        else {
-            // Currently the only other optoin is api.consts
-            const method:ConstantCodec = <ConstantCodec> this.getOriginalApi().consts[apiModule][apiMethod];
-            return method.meta;
-        }
+  static description =
+    'Lists available node API modules/methods and/or their description(s), ' +
+    'or calls one of the API methods (depending on provided arguments and flags)'
+
+  static examples = [
+    '$ api:inspect',
+    '$ api:inspect -t=query',
+    '$ api:inspect -t=query -M=members',
+    '$ api:inspect -t=query -M=members -m=memberProfile',
+    '$ api:inspect -t=query -M=members -m=memberProfile -e',
+    '$ api:inspect -t=query -M=members -m=memberProfile -e -a=1',
+  ]
+
+  static flags = {
+    type: flags.string({
+      char: 't',
+      description:
+        'Specifies the type/category of the inspected request (ie. "query", "consts" etc.).\n' +
+        'If no "--module" flag is provided then all available modules in that type will be listed.\n' +
+        'If this flag is not provided then all available types will be listed.',
+    }),
+    module: flags.string({
+      char: 'M',
+      description:
+        'Specifies the api module, ie. "system", "staking" etc.\n' +
+        'If no "--method" flag is provided then all methods in that module will be listed along with the descriptions.',
+      dependsOn: ['type'],
+    }),
+    method: flags.string({
+      char: 'm',
+      description: 'Specifies the api method to call/describe.',
+      dependsOn: ['module'],
+    }),
+    exec: flags.boolean({
+      char: 'e',
+      description:
+        'Provide this flag if you want to execute the actual call, instead of displaying the method description (which is default)',
+      dependsOn: ['method'],
+    }),
+    callArgs: flags.string({
+      char: 'a',
+      description:
+        'Specifies the arguments to use when calling a method. Multiple arguments can be separated with a comma, ie. "-a=arg1,arg2".\n' +
+        'You can omit this flag even if the method requires some aguments.\n' +
+        'In that case you will be promted to provide value for each required argument.\n' +
+        "Ommiting this flag is recommended when input parameters are of more complex types (and it's hard to specify them as just simple comma-separated strings)",
+      dependsOn: ['exec'],
+    }),
+  }
+
+  getMethodMeta(apiType: ApiType, apiModule: string, apiMethod: string) {
+    if (apiType === 'query') {
+      return this.getOriginalApi().query[apiModule][apiMethod].creator.meta
+    } else {
+      // Currently the only other optoin is api.consts
+      const method: ConstantCodec = this.getOriginalApi().consts[apiModule][apiMethod] as ConstantCodec
+      return method.meta
     }
-
-    getMethodDescription(apiType: ApiType, apiModule: string, apiMethod: string): string {
-        let description:string = this.getMethodMeta(apiType, apiModule, apiMethod).documentation.join(' ');
-        return description || 'No description available.';
+  }
+
+  getMethodDescription(apiType: ApiType, apiModule: string, apiMethod: string): string {
+    const description: string = this.getMethodMeta(apiType, apiModule, apiMethod).documentation.join(' ')
+    return description || 'No description available.'
+  }
+
+  getQueryMethodParamsTypes(apiModule: string, apiMethod: string): string[] {
+    const method = this.getOriginalApi().query[apiModule][apiMethod]
+    const { type } = method.creator.meta
+    if (type.isDoubleMap) {
+      return [type.asDoubleMap.key1.toString(), type.asDoubleMap.key2.toString()]
     }
-
-    getQueryMethodParamsTypes(apiModule: string, apiMethod: string): string[] {
-        const method = this.getOriginalApi().query[apiModule][apiMethod];
-        const { type } = method.creator.meta;
-        if (type.isDoubleMap) {
-            return [ type.asDoubleMap.key1.toString(), type.asDoubleMap.key2.toString() ];
+    if (type.isMap) {
+      return type.asMap.linked.isTrue ? [`Option<${type.asMap.key.toString()}>`] : [type.asMap.key.toString()]
+    }
+    return []
+  }
+
+  getMethodReturnType(apiType: ApiType, apiModule: string, apiMethod: string): string {
+    if (apiType === 'query') {
+      const method = this.getOriginalApi().query[apiModule][apiMethod]
+      const {
+        meta: { type, modifier },
+      } = method.creator
+      if (type.isDoubleMap) {
+        return type.asDoubleMap.value.toString()
+      }
+      if (modifier.isOptional) {
+        return `Option<${type.toString()}>`
+      }
+    }
+    // Fallback for "query" and default for "consts"
+    return this.getMethodMeta(apiType, apiModule, apiMethod).type.toString()
+  }
+
+  // Validate the flags - throws an error if flags.type, flags.module or flags.method is invalid / does not exist in the api.
+  // Returns type, module and method which validity we can be sure about (notice they may still be "undefined" if weren't provided).
+  validateFlags(
+    api: ApiPromise,
+    flags: ApiInspectFlags
+  ): { apiType: ApiType | undefined; apiModule: string | undefined; apiMethod: string | undefined } {
+    let apiType: ApiType | undefined = undefined
+    const { module: apiModule, method: apiMethod } = flags
+
+    if (flags.type !== undefined) {
+      const availableTypes: readonly string[] = TYPES_AVAILABLE
+      if (!availableTypes.includes(flags.type)) {
+        throw new CLIError('Such type is not available', { exit: ExitCodes.InvalidInput })
+      }
+      apiType = flags.type as ApiType
+      if (apiModule !== undefined) {
+        if (!api[apiType][apiModule]) {
+          throw new CLIError('Such module was not found', { exit: ExitCodes.InvalidInput })
         }
-        if (type.isMap) {
-            return type.asMap.linked.isTrue ? [ `Option<${type.asMap.key.toString()}>` ] : [ type.asMap.key.toString() ];
+        if (apiMethod !== undefined && !api[apiType][apiModule][apiMethod]) {
+          throw new CLIError('Such method was not found', { exit: ExitCodes.InvalidInput })
         }
-        return [];
+      }
     }
 
-    getMethodReturnType(apiType: ApiType, apiModule: string, apiMethod: string): string {
-        if (apiType === 'query') {
-            const method = this.getOriginalApi().query[apiModule][apiMethod];
-            const { meta: { type, modifier } } = method.creator;
-            if (type.isDoubleMap) {
-                return type.asDoubleMap.value.toString();
-            }
-            if (modifier.isOptional) {
-                return `Option<${type.toString()}>`;
-            }
-        }
-        // Fallback for "query" and default for "consts"
-        return this.getMethodMeta(apiType, apiModule, apiMethod).type.toString();
+    return { apiType, apiModule, apiMethod }
+  }
+
+  // Request values for params using array of param types (strings)
+  async requestParamsValues(paramTypes: string[]): Promise<ApiMethodArg[]> {
+    const result: ApiMethodArg[] = []
+    for (const [key, paramType] of Object.entries(paramTypes)) {
+      this.log(chalk.bold.white(`Parameter no. ${parseInt(key) + 1} (${paramType}):`))
+      const paramValue = await this.promptForParam(paramType)
+      if (paramValue instanceof Option && paramValue.isSome) {
+        result.push(paramValue.unwrap())
+      } else if (!(paramValue instanceof Option)) {
+        result.push(paramValue)
+      }
+      // In case of empty option we MUST NOT add anything to the array (otherwise it causes some error)
     }
 
-    // Validate the flags - throws an error if flags.type, flags.module or flags.method is invalid / does not exist in the api.
-    // Returns type, module and method which validity we can be sure about (notice they may still be "undefined" if weren't provided).
-    validateFlags(api: ApiPromise, flags: ApiInspectFlags): { apiType: ApiType | undefined, apiModule: string | undefined, apiMethod: string | undefined } {
-        let apiType: ApiType | undefined = undefined;
-        const { module: apiModule, method: apiMethod } = flags;
-
-        if (flags.type !== undefined) {
-            const availableTypes: readonly string[] = TYPES_AVAILABLE;
-            if (!availableTypes.includes(flags.type)) {
-                throw new CLIError('Such type is not available', { exit: ExitCodes.InvalidInput });
-            }
-            apiType = <ApiType> flags.type;
-            if (apiModule !== undefined) {
-                if (!api[apiType][apiModule]) {
-                    throw new CLIError('Such module was not found', { exit: ExitCodes.InvalidInput });
-                }
-                if (apiMethod !== undefined && !api[apiType][apiModule][apiMethod]) {
-                    throw new CLIError('Such method was not found', { exit: ExitCodes.InvalidInput });
-                }
-            }
+    return result
+  }
+
+  async run() {
+    const api: ApiPromise = this.getOriginalApi()
+    const flags: ApiInspectFlags = this.parse(ApiInspect).flags as ApiInspectFlags
+    const availableTypes: readonly string[] = TYPES_AVAILABLE
+    const { apiType, apiModule, apiMethod } = this.validateFlags(api, flags)
+
+    // Executing a call
+    if (apiType && apiModule && apiMethod && flags.exec) {
+      let result: Codec
+
+      if (apiType === 'query') {
+        // Api query - call with (or without) arguments
+        let args: (string | ApiMethodArg)[] = flags.callArgs ? flags.callArgs.split(',') : []
+        const paramsTypes: string[] = this.getQueryMethodParamsTypes(apiModule, apiMethod)
+        if (args.length < paramsTypes.length) {
+          this.warn('Some parameters are missing! Please, provide the missing parameters:')
+          const missingParamsValues = await this.requestParamsValues(paramsTypes.slice(args.length))
+          args = args.concat(missingParamsValues)
         }
+        result = await api.query[apiModule][apiMethod](...args)
+      } else {
+        // Api consts - just assign the value
+        result = api.consts[apiModule][apiMethod]
+      }
 
-        return { apiType, apiModule, apiMethod };
+      this.log(chalk.green(result.toString()))
     }
-
-    // Request values for params using array of param types (strings)
-    async requestParamsValues(paramTypes: string[]): Promise<ApiMethodArg[]> {
-        let result: ApiMethodArg[] = [];
-        for (let [key, paramType] of Object.entries(paramTypes)) {
-            this.log(chalk.bold.white(`Parameter no. ${ parseInt(key)+1 } (${ paramType }):`));
-            let paramValue = await this.promptForParam(paramType);
-            if (paramValue instanceof Option && paramValue.isSome) {
-                result.push(paramValue.unwrap());
-            }
-            else if (!(paramValue instanceof Option)) {
-                result.push(paramValue);
-            }
-            // In case of empty option we MUST NOT add anything to the array (otherwise it causes some error)
-        }
-
-        return result;
+    // Describing a method
+    else if (apiType && apiModule && apiMethod) {
+      this.log(chalk.bold.white(`${apiType}.${apiModule}.${apiMethod}`))
+      const description: string = this.getMethodDescription(apiType, apiModule, apiMethod)
+      this.log(`\n${description}\n`)
+      const typesRows: NameValueObj[] = []
+      if (apiType === 'query') {
+        typesRows.push({
+          name: 'Params:',
+          value: this.getQueryMethodParamsTypes(apiModule, apiMethod).join(', ') || '-',
+        })
+      }
+      typesRows.push({ name: 'Returns:', value: this.getMethodReturnType(apiType, apiModule, apiMethod) })
+      displayNameValueTable(typesRows)
     }
-
-    async run() {
-        const api: ApiPromise = this.getOriginalApi();
-        const flags: ApiInspectFlags = <ApiInspectFlags> this.parse(ApiInspect).flags;
-        const availableTypes: readonly string[] = TYPES_AVAILABLE;
-        const { apiType, apiModule, apiMethod } = this.validateFlags(api, flags);
-
-        // Executing a call
-        if (apiType && apiModule && apiMethod && flags.exec) {
-            let result: Codec;
-
-            if (apiType === 'query') {
-                // Api query - call with (or without) arguments
-                let args: (string | ApiMethodArg)[] = flags.callArgs ? flags.callArgs.split(',') : [];
-                const paramsTypes: string[] = this.getQueryMethodParamsTypes(apiModule, apiMethod);
-                if (args.length < paramsTypes.length) {
-                    this.warn('Some parameters are missing! Please, provide the missing parameters:');
-                    let missingParamsValues = await this.requestParamsValues(paramsTypes.slice(args.length));
-                    args = args.concat(missingParamsValues);
-                }
-                result = await api.query[apiModule][apiMethod](...args);
-            }
-            else {
-                // Api consts - just assign the value
-                result = api.consts[apiModule][apiMethod];
-            }
-
-            this.log(chalk.green(result.toString()));
-        }
-        // Describing a method
-        else if (apiType && apiModule && apiMethod) {
-            this.log(chalk.bold.white(`${ apiType }.${ apiModule }.${ apiMethod }`));
-            const description: string = this.getMethodDescription(apiType, apiModule, apiMethod);
-            this.log(`\n${ description }\n`);
-            let typesRows: NameValueObj[] = [];
-            if (apiType === 'query') {
-                typesRows.push({ name: 'Params:', value: this.getQueryMethodParamsTypes(apiModule, apiMethod).join(', ') || '-' });
-            }
-            typesRows.push({ name: 'Returns:', value: this.getMethodReturnType(apiType, apiModule, apiMethod) });
-            displayNameValueTable(typesRows);
-        }
-        // Displaying all available methods
-        else if (apiType && apiModule) {
-            const module = api[apiType][apiModule];
-            const rows: NameValueObj[] = Object.keys(module).map((key: string) => {
-                return { name: key, value: this.getMethodDescription(apiType, apiModule, key) };
-            });
-            displayNameValueTable(rows);
-        }
-        // Displaying all available modules
-        else if (apiType) {
-            this.log(chalk.bold.white('Available modules:'));
-            this.log(Object.keys(api[apiType]).map(key => chalk.white(key)).join('\n'));
-        }
-        // Displaying all available types
-        else {
-            this.log(chalk.bold.white('Available types:'));
-            this.log(availableTypes.map(type => chalk.white(type)).join('\n'));
-        }
+    // Displaying all available methods
+    else if (apiType && apiModule) {
+      const module = api[apiType][apiModule]
+      const rows: NameValueObj[] = Object.keys(module).map((key: string) => {
+        return { name: key, value: this.getMethodDescription(apiType, apiModule, key) }
+      })
+      displayNameValueTable(rows)
+    }
+    // Displaying all available modules
+    else if (apiType) {
+      this.log(chalk.bold.white('Available modules:'))
+      this.log(
+        Object.keys(api[apiType])
+          .map((key) => chalk.white(key))
+          .join('\n')
+      )
+    }
+    // Displaying all available types
+    else {
+      this.log(chalk.bold.white('Available types:'))
+      this.log(availableTypes.map((type) => chalk.white(type)).join('\n'))
     }
+  }
 }

+ 22 - 22
cli/src/commands/api/setUri.ts

@@ -1,28 +1,28 @@
-import StateAwareCommandBase from '../../base/StateAwareCommandBase';
-import chalk from 'chalk';
-import { WsProvider } from '@polkadot/api';
-import ExitCodes from '../../ExitCodes';
+import StateAwareCommandBase from '../../base/StateAwareCommandBase'
+import chalk from 'chalk'
+import { WsProvider } from '@polkadot/api'
+import ExitCodes from '../../ExitCodes'
 
-type ApiSetUriArgs = { uri: string };
+type ApiSetUriArgs = { uri: string }
 
 export default class ApiSetUri extends StateAwareCommandBase {
-    static description = 'Set api WS provider uri';
-    static args = [
-        {
-            name: 'uri',
-            required: true,
-            description: 'Uri of the node api WS provider'
-        }
-    ];
+  static description = 'Set api WS provider uri'
+  static args = [
+    {
+      name: 'uri',
+      required: true,
+      description: 'Uri of the node api WS provider',
+    },
+  ]
 
-    async run() {
-        const args: ApiSetUriArgs = <ApiSetUriArgs> this.parse(ApiSetUri).args;
-        try {
-            new WsProvider(args.uri);
-        } catch(e) {
-            this.error('The WS provider uri seems to be incorrect', { exit: ExitCodes.InvalidInput });
-        }
-        await this.setPreservedState({ apiUri: args.uri });
-        this.log(chalk.greenBright('Api uri successfuly changed! New uri: ') + chalk.white(args.uri))
+  async run() {
+    const args: ApiSetUriArgs = this.parse(ApiSetUri).args as ApiSetUriArgs
+    try {
+      new WsProvider(args.uri)
+    } catch (e) {
+      this.error('The WS provider uri seems to be incorrect', { exit: ExitCodes.InvalidInput })
     }
+    await this.setPreservedState({ apiUri: args.uri })
+    this.log(chalk.greenBright('Api uri successfuly changed! New uri: ') + chalk.white(args.uri))
   }
+}

+ 48 - 49
cli/src/commands/council/info.ts

@@ -1,57 +1,56 @@
-import { ElectionStage } from '@joystream/types/council';
-import { formatNumber, formatBalance } from '@polkadot/util';
-import { BlockNumber } from '@polkadot/types/interfaces';
-import { CouncilInfoObj, NameValueObj } from '../../Types';
-import { displayHeader, displayNameValueTable } from '../../helpers/display';
-import ApiCommandBase from '../../base/ApiCommandBase';
+import { ElectionStage } from '@joystream/types/council'
+import { formatNumber, formatBalance } from '@polkadot/util'
+import { BlockNumber } from '@polkadot/types/interfaces'
+import { CouncilInfoObj, NameValueObj } from '../../Types'
+import { displayHeader, displayNameValueTable } from '../../helpers/display'
+import ApiCommandBase from '../../base/ApiCommandBase'
 
 export default class CouncilInfo extends ApiCommandBase {
-    static description = 'Get current council and council elections information';
+  static description = 'Get current council and council elections information'
 
-    displayInfo(infoObj: CouncilInfoObj) {
-        const { activeCouncil = [], round, stage } = infoObj;
+  displayInfo(infoObj: CouncilInfoObj) {
+    const { activeCouncil = [], round, stage } = infoObj
 
-        displayHeader('Council');
-        const councilRows: NameValueObj[] = [
-            { name: 'Elected:', value: activeCouncil.length ? 'YES' : 'NO' },
-            { name: 'Members:', value: activeCouncil.length.toString() },
-            { name: 'Term ends at block:', value: `#${formatNumber(infoObj.termEndsAt) }` },
-        ];
-        displayNameValueTable(councilRows);
+    displayHeader('Council')
+    const councilRows: NameValueObj[] = [
+      { name: 'Elected:', value: activeCouncil.length ? 'YES' : 'NO' },
+      { name: 'Members:', value: activeCouncil.length.toString() },
+      { name: 'Term ends at block:', value: `#${formatNumber(infoObj.termEndsAt)}` },
+    ]
+    displayNameValueTable(councilRows)
 
-
-        displayHeader('Election');
-        let electionTableRows: NameValueObj[] = [
-            { name: 'Running:', value: stage && stage.isSome ? 'YES' : 'NO' },
-            { name: 'Election round:', value: formatNumber(round) }
-        ];
-        if (stage && stage.isSome) {
-            const stageValue = <ElectionStage> stage.value;
-            const stageName: string = stageValue.type;
-            const stageEndsAt = <BlockNumber> stageValue.value;
-            electionTableRows.push({ name: 'Stage:', value: stageName });
-            electionTableRows.push({ name: 'Stage ends at block:', value: `#${stageEndsAt}` });
-        }
-        displayNameValueTable(electionTableRows);
-
-        displayHeader('Configuration');
-        const isAutoStart = (infoObj.autoStart || false).valueOf();
-        const configTableRows: NameValueObj[] = [
-            { name: 'Auto-start elections:', value: isAutoStart ? 'YES' : 'NO' },
-            { name: 'New term duration:', value: formatNumber(infoObj.newTermDuration) },
-            { name: 'Candidacy limit:', value: formatNumber(infoObj.candidacyLimit) },
-            { name: 'Council size:', value: formatNumber(infoObj.councilSize) },
-            { name: 'Min. council stake:', value: formatBalance(infoObj.minCouncilStake) },
-            { name: 'Min. voting stake:', value: formatBalance(infoObj.minVotingStake) },
-            { name: 'Announcing period:', value: `${ formatNumber(infoObj.announcingPeriod) } blocks` },
-            { name: 'Voting period:', value: `${ formatNumber(infoObj.votingPeriod) } blocks` },
-            { name: 'Revealing period:', value: `${ formatNumber(infoObj.revealingPeriod) } blocks` }
-        ];
-        displayNameValueTable(configTableRows);
+    displayHeader('Election')
+    const electionTableRows: NameValueObj[] = [
+      { name: 'Running:', value: stage && stage.isSome ? 'YES' : 'NO' },
+      { name: 'Election round:', value: formatNumber(round) },
+    ]
+    if (stage && stage.isSome) {
+      const stageValue = stage.value as ElectionStage
+      const stageName: string = stageValue.type
+      const stageEndsAt = stageValue.value as BlockNumber
+      electionTableRows.push({ name: 'Stage:', value: stageName })
+      electionTableRows.push({ name: 'Stage ends at block:', value: `#${stageEndsAt}` })
     }
+    displayNameValueTable(electionTableRows)
 
-    async run() {
-        const infoObj = await this.getApi().getCouncilInfo();
-        this.displayInfo(infoObj);
-    }
+    displayHeader('Configuration')
+    const isAutoStart = (infoObj.autoStart || false).valueOf()
+    const configTableRows: NameValueObj[] = [
+      { name: 'Auto-start elections:', value: isAutoStart ? 'YES' : 'NO' },
+      { name: 'New term duration:', value: formatNumber(infoObj.newTermDuration) },
+      { name: 'Candidacy limit:', value: formatNumber(infoObj.candidacyLimit) },
+      { name: 'Council size:', value: formatNumber(infoObj.councilSize) },
+      { name: 'Min. council stake:', value: formatBalance(infoObj.minCouncilStake) },
+      { name: 'Min. voting stake:', value: formatBalance(infoObj.minVotingStake) },
+      { name: 'Announcing period:', value: `${formatNumber(infoObj.announcingPeriod)} blocks` },
+      { name: 'Voting period:', value: `${formatNumber(infoObj.votingPeriod)} blocks` },
+      { name: 'Revealing period:', value: `${formatNumber(infoObj.revealingPeriod)} blocks` },
+    ]
+    displayNameValueTable(configTableRows)
+  }
+
+  async run() {
+    const infoObj = await this.getApi().getCouncilInfo()
+    this.displayInfo(infoObj)
   }
+}

+ 31 - 32
cli/src/commands/working-groups/application.ts

@@ -1,40 +1,39 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import { displayCollapsedRow, displayHeader } from '../../helpers/display';
-import _ from 'lodash';
-import chalk from 'chalk';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { displayCollapsedRow, displayHeader } from '../../helpers/display'
+import chalk from 'chalk'
 
 export default class WorkingGroupsApplication extends WorkingGroupsCommandBase {
-    static description = 'Shows an overview of given application by Working Group Application ID';
-    static args = [
-        {
-            name: 'wgApplicationId',
-            required: true,
-            description: 'Working Group Application ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Shows an overview of given application by Working Group Application ID'
+  static args = [
+    {
+      name: 'wgApplicationId',
+      required: true,
+      description: 'Working Group Application ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    async run() {
-        const { args } = this.parse(WorkingGroupsApplication);
+  async run() {
+    const { args } = this.parse(WorkingGroupsApplication)
 
-        const application = await this.getApi().groupApplication(this.group, parseInt(args.wgApplicationId));
+    const application = await this.getApi().groupApplication(this.group, parseInt(args.wgApplicationId))
 
-        displayHeader('Human readable text');
-        this.jsonPrettyPrint(application.humanReadableText);
+    displayHeader('Human readable text')
+    this.jsonPrettyPrint(application.humanReadableText)
 
-        displayHeader(`Details`);
-        const applicationRow = {
-            'WG application ID': application.wgApplicationId,
-            'Application ID': application.applicationId,
-            'Member handle': application.member?.handle.toString() || chalk.red('NONE'),
-            'Role account': application.roleAccout.toString(),
-            'Stage': application.stage,
-            'Application stake': application.stakes.application,
-            'Role stake': application.stakes.role,
-            'Total stake': Object.values(application.stakes).reduce((a, b) => a + b)
-        };
-        displayCollapsedRow(applicationRow);
+    displayHeader(`Details`)
+    const applicationRow = {
+      'WG application ID': application.wgApplicationId,
+      'Application ID': application.applicationId,
+      'Member handle': application.member?.handle.toString() || chalk.red('NONE'),
+      'Role account': application.roleAccout.toString(),
+      Stage: application.stage,
+      'Application stake': application.stakes.application,
+      'Role stake': application.stakes.role,
+      'Total stake': Object.values(application.stakes).reduce((a, b) => a + b),
     }
+    displayCollapsedRow(applicationRow)
+  }
 }

+ 77 - 74
cli/src/commands/working-groups/createOpening.ts

@@ -1,86 +1,89 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import { ApiMethodArg, ApiMethodNamedArgs } from '../../Types';
-import chalk from 'chalk';
-import { flags } from '@oclif/command';
-import { apiModuleByGroup } from '../../Api';
-import WorkerOpeningOptions from '../../promptOptions/addWorkerOpening';
-import { setDefaults } from '../../helpers/promptOptions';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { ApiMethodArg, ApiMethodNamedArgs } from '../../Types'
+import chalk from 'chalk'
+import { flags } from '@oclif/command'
+import { apiModuleByGroup } from '../../Api'
+import WorkerOpeningOptions from '../../promptOptions/addWorkerOpening'
+import { setDefaults } from '../../helpers/promptOptions'
 
 export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase {
-    static description = 'Create working group opening (requires lead access)';
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-        useDraft: flags.boolean({
-            char: 'd',
-            description:
-                "Whether to create the opening from existing draft.\n"+
-                "If provided without --draftName - the list of choices will be displayed."
-        }),
-        draftName: flags.string({
-            char: 'n',
-            description:
-                'Name of the draft to create the opening from.',
-            dependsOn: ['useDraft']
-        }),
-        createDraftOnly: flags.boolean({
-            char: 'c',
-            description:
-                'If provided - the extrinsic will not be executed. Use this flag if you only want to create a draft.'
-        }),
-        skipPrompts: flags.boolean({
-            char: 's',
-            description:
-                "Whether to skip all prompts when adding from draft (will use all default values)",
-            dependsOn: ['useDraft'],
-            exclusive: ['createDraftOnly']
-        })
-    };
+  static description = 'Create working group opening (requires lead access)'
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+    useDraft: flags.boolean({
+      char: 'd',
+      description:
+        'Whether to create the opening from existing draft.\n' +
+        'If provided without --draftName - the list of choices will be displayed.',
+    }),
+    draftName: flags.string({
+      char: 'n',
+      description: 'Name of the draft to create the opening from.',
+      dependsOn: ['useDraft'],
+    }),
+    createDraftOnly: flags.boolean({
+      char: 'c',
+      description:
+        'If provided - the extrinsic will not be executed. Use this flag if you only want to create a draft.',
+    }),
+    skipPrompts: flags.boolean({
+      char: 's',
+      description: 'Whether to skip all prompts when adding from draft (will use all default values)',
+      dependsOn: ['useDraft'],
+      exclusive: ['createDraftOnly'],
+    }),
+  }
 
-    async run() {
-        const account = await this.getRequiredSelectedAccount();
-        // lead-only gate
-        await this.getRequiredLead();
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    // lead-only gate
+    await this.getRequiredLead()
 
-        const { flags } = this.parse(WorkingGroupsCreateOpening);
+    const { flags } = this.parse(WorkingGroupsCreateOpening)
 
-        let promptOptions = new WorkerOpeningOptions(), defaultValues: ApiMethodNamedArgs | undefined;
-        if (flags.useDraft) {
-            const draftName = flags.draftName || await this.promptForOpeningDraft();
-            defaultValues = await this.loadOpeningDraftParams(draftName);
-            setDefaults(promptOptions, defaultValues);
-        }
+    const promptOptions = new WorkerOpeningOptions()
+    let defaultValues: ApiMethodNamedArgs | undefined
+    if (flags.useDraft) {
+      const draftName = flags.draftName || (await this.promptForOpeningDraft())
+      defaultValues = await this.loadOpeningDraftParams(draftName)
+      setDefaults(promptOptions, defaultValues)
+    }
 
-        if (!flags.skipPrompts) {
-            const module = apiModuleByGroup[this.group];
-            const method = 'addOpening';
+    if (!flags.skipPrompts) {
+      const module = apiModuleByGroup[this.group]
+      const method = 'addOpening'
 
-            let saveDraft = false, params: ApiMethodArg[];
-            if (flags.createDraftOnly) {
-                params = await this.promptForExtrinsicParams(module, method, promptOptions);
-                saveDraft = true;
-            }
-            else {
-                await this.requestAccountDecoding(account); // Prompt for password
-                params = await this.buildAndSendExtrinsic(account, module, method, promptOptions, true);
+      let saveDraft = false,
+        params: ApiMethodArg[]
+      if (flags.createDraftOnly) {
+        params = await this.promptForExtrinsicParams(module, method, promptOptions)
+        saveDraft = true
+      } else {
+        await this.requestAccountDecoding(account) // Prompt for password
+        params = await this.buildAndSendExtrinsic(account, module, method, promptOptions, true)
 
-                saveDraft = await this.simplePrompt({
-                    message: 'Do you wish to save this opening as draft?',
-                    type: 'confirm'
-                });
-            }
+        saveDraft = await this.simplePrompt({
+          message: 'Do you wish to save this opening as draft?',
+          type: 'confirm',
+        })
+      }
 
-            if (saveDraft) {
-                const draftName = await this.promptForNewOpeningDraftName();
-                this.saveOpeningDraft(draftName, params);
+      if (saveDraft) {
+        const draftName = await this.promptForNewOpeningDraftName()
+        this.saveOpeningDraft(draftName, params)
 
-                this.log(chalk.green(`Opening draft ${ chalk.white(draftName) } succesfully saved!`));
-            }
-        }
-        else {
-            await this.requestAccountDecoding(account); // Prompt for password
-            this.log(chalk.white('Sending the extrinsic...'));
-            await this.sendExtrinsic(account, apiModuleByGroup[this.group], 'addOpening', defaultValues!.map(v => v.value));
-            this.log(chalk.green('Opening succesfully created!'));
-        }
+        this.log(chalk.green(`Opening draft ${chalk.white(draftName)} succesfully saved!`))
+      }
+    } else {
+      await this.requestAccountDecoding(account) // Prompt for password
+      this.log(chalk.white('Sending the extrinsic...'))
+      await this.sendExtrinsic(
+        account,
+        apiModuleByGroup[this.group],
+        'addOpening',
+        defaultValues!.map((v) => v.value)
+      )
+      this.log(chalk.green('Opening succesfully created!'))
     }
+  }
 }

+ 53 - 55
cli/src/commands/working-groups/decreaseWorkerStake.ts

@@ -1,58 +1,56 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { WorkerId } from '@joystream/types/working-group';
-import { Balance } from '@polkadot/types/interfaces';
-import { formatBalance } from '@polkadot/util';
-import { minMaxInt } from '../../validators/common';
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
-import { createParamOptions } from '../../helpers/promptOptions';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { WorkerId } from '@joystream/types/working-group'
+import { Balance } from '@polkadot/types/interfaces'
+import { formatBalance } from '@polkadot/util'
+import { minMaxInt } from '../../validators/common'
+import chalk from 'chalk'
+import { createParamOptions } from '../../helpers/promptOptions'
 
 export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsCommandBase {
-    static description =
-        'Decreases given worker stake by an amount that will be returned to the worker role account. ' +
-        'Requires lead access.';
-    static args = [
-        {
-            name: 'workerId',
-            required: true,
-            description: 'Worker ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
-
-    async run() {
-        const { args } = this.parse(WorkingGroupsDecreaseWorkerStake);
-
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
-
-        const workerId = parseInt(args.workerId);
-        const groupMember = await this.getWorkerWithStakeForLeadAction(workerId);
-
-        this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)));
-        const balanceValidator = minMaxInt(1, groupMember.stake.toNumber());
-        const balance = await this.promptForParam('Balance', createParamOptions('amount', undefined, balanceValidator)) as Balance;
-
-        await this.requestAccountDecoding(account);
-
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'decreaseStake',
-            [
-                new WorkerId(workerId),
-                balance
-            ]
-        );
-
-        this.log(chalk.green(
-            `${chalk.white(formatBalance(balance))} from worker ${chalk.white(workerId)} stake `+
-            `has been returned to worker's role account (${chalk.white(groupMember.roleAccount.toString())})!`
-        ));
-    }
+  static description =
+    'Decreases given worker stake by an amount that will be returned to the worker role account. ' +
+    'Requires lead access.'
+  static args = [
+    {
+      name: 'workerId',
+      required: true,
+      description: 'Worker ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
+
+  async run() {
+    const { args } = this.parse(WorkingGroupsDecreaseWorkerStake)
+
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
+
+    const workerId = parseInt(args.workerId)
+    const groupMember = await this.getWorkerWithStakeForLeadAction(workerId)
+
+    this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)))
+    const balanceValidator = minMaxInt(1, groupMember.stake.toNumber())
+    const balance = (await this.promptForParam(
+      'Balance',
+      createParamOptions('amount', undefined, balanceValidator)
+    )) as Balance
+
+    await this.requestAccountDecoding(account)
+
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'decreaseStake', [
+      new WorkerId(workerId),
+      balance,
+    ])
+
+    this.log(
+      chalk.green(
+        `${chalk.white(formatBalance(balance))} from worker ${chalk.white(workerId)} stake ` +
+          `has been returned to worker's role account (${chalk.white(groupMember.roleAccount.toString())})!`
+      )
+    )
+  }
 }

+ 52 - 59
cli/src/commands/working-groups/evictWorker.ts

@@ -1,63 +1,56 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { WorkerId } from '@joystream/types/working-group';
-import { bool } from '@polkadot/types/primitive';
-import { formatBalance } from '@polkadot/util';
-import chalk from 'chalk';
-import { createParamOptions } from '../../helpers/promptOptions';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { WorkerId } from '@joystream/types/working-group'
+import { bool } from '@polkadot/types/primitive'
+import { formatBalance } from '@polkadot/util'
+import chalk from 'chalk'
+import { createParamOptions } from '../../helpers/promptOptions'
 
 export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
-    static description = 'Evicts given worker. Requires lead access.';
-    static args = [
-        {
-            name: 'workerId',
-            required: true,
-            description: 'Worker ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
-
-    async run() {
-        const { args } = this.parse(WorkingGroupsEvictWorker);
-
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
-
-        const workerId = parseInt(args.workerId);
-        // This will also make sure the worker is valid
-        const groupMember = await this.getWorkerForLeadAction(workerId);
-
-        // TODO: Terminate worker text limits? (minMaxStr)
-        const rationale = await this.promptForParam('Bytes', createParamOptions('rationale'));
-        const shouldSlash = groupMember.stake
-            ?
-                await this.simplePrompt({
-                    message: `Should the worker stake (${formatBalance(groupMember.stake)}) be slashed?`,
-                    type: 'confirm',
-                    default: false
-                })
-            : false;
-
-        await this.requestAccountDecoding(account);
-
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'terminateRole',
-            [
-                new WorkerId(workerId),
-                rationale,
-                new bool(shouldSlash)
-            ]
-        );
-
-        this.log(chalk.green(`Worker ${chalk.white(workerId)} has been evicted!`));
-        if (shouldSlash) {
-            this.log(chalk.green(`Worker stake totalling ${chalk.white(formatBalance(groupMember.stake))} has been slashed!`));
-        }
+  static description = 'Evicts given worker. Requires lead access.'
+  static args = [
+    {
+      name: 'workerId',
+      required: true,
+      description: 'Worker ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
+
+  async run() {
+    const { args } = this.parse(WorkingGroupsEvictWorker)
+
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
+
+    const workerId = parseInt(args.workerId)
+    // This will also make sure the worker is valid
+    const groupMember = await this.getWorkerForLeadAction(workerId)
+
+    // TODO: Terminate worker text limits? (minMaxStr)
+    const rationale = await this.promptForParam('Bytes', createParamOptions('rationale'))
+    const shouldSlash = groupMember.stake
+      ? await this.simplePrompt({
+          message: `Should the worker stake (${formatBalance(groupMember.stake)}) be slashed?`,
+          type: 'confirm',
+          default: false,
+        })
+      : false
+
+    await this.requestAccountDecoding(account)
+
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'terminateRole', [
+      new WorkerId(workerId),
+      rationale,
+      new bool(shouldSlash),
+    ])
+
+    this.log(chalk.green(`Worker ${chalk.white(workerId)} has been evicted!`))
+    if (shouldSlash) {
+      this.log(chalk.green(`Worker stake totalling ${chalk.white(formatBalance(groupMember.stake))} has been slashed!`))
     }
+  }
 }

+ 49 - 52
cli/src/commands/working-groups/fillOpening.ts

@@ -1,55 +1,52 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { OpeningStatus } from '../../Types';
-import { apiModuleByGroup } from '../../Api';
-import { OpeningId } from '@joystream/types/hiring';
-import { ApplicationIdSet, RewardPolicy } from '@joystream/types/working-group';
-import chalk from 'chalk';
-import { createParamOptions } from '../../helpers/promptOptions';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { OpeningStatus } from '../../Types'
+import { apiModuleByGroup } from '../../Api'
+import { OpeningId } from '@joystream/types/hiring'
+import { ApplicationIdSet, RewardPolicy } from '@joystream/types/working-group'
+import chalk from 'chalk'
+import { createParamOptions } from '../../helpers/promptOptions'
 
 export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
-    static description = 'Allows filling working group opening that\'s currently in review. Requires lead access.';
-    static args = [
-        {
-            name: 'wgOpeningId',
-            required: true,
-            description: 'Working Group Opening ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
-
-    async run() {
-        const { args } = this.parse(WorkingGroupsFillOpening);
-
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
-
-        const openingId = parseInt(args.wgOpeningId);
-        const opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview);
-
-        const applicationIds = await this.promptForApplicationsToAccept(opening);
-        const rewardPolicyOpt = await this.promptForParam(`Option<${RewardPolicy.name}>`, createParamOptions('RewardPolicy'));
-
-        await this.requestAccountDecoding(account);
-
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'fillOpening',
-            [
-                new OpeningId(openingId),
-                new ApplicationIdSet(applicationIds),
-                rewardPolicyOpt
-            ]
-        );
-
-        this.log(chalk.green(`Opening ${chalk.white(openingId)} succesfully filled!`));
-        this.log(
-            chalk.green('Accepted working group application IDs: ') +
-            chalk.white(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE')
-        );
-    }
+  static description = "Allows filling working group opening that's currently in review. Requires lead access."
+  static args = [
+    {
+      name: 'wgOpeningId',
+      required: true,
+      description: 'Working Group Opening ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
+
+  async run() {
+    const { args } = this.parse(WorkingGroupsFillOpening)
+
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
+
+    const openingId = parseInt(args.wgOpeningId)
+    const opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview)
+
+    const applicationIds = await this.promptForApplicationsToAccept(opening)
+    const rewardPolicyOpt = await this.promptForParam(
+      `Option<${RewardPolicy.name}>`,
+      createParamOptions('RewardPolicy')
+    )
+
+    await this.requestAccountDecoding(account)
+
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'fillOpening', [
+      new OpeningId(openingId),
+      new ApplicationIdSet(applicationIds),
+      rewardPolicyOpt,
+    ])
+
+    this.log(chalk.green(`Opening ${chalk.white(openingId)} succesfully filled!`))
+    this.log(
+      chalk.green('Accepted working group application IDs: ') +
+        chalk.white(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE')
+    )
+  }
 }

+ 37 - 37
cli/src/commands/working-groups/increaseStake.ts

@@ -1,46 +1,46 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { Balance } from '@polkadot/types/interfaces';
-import { formatBalance } from '@polkadot/util';
-import { positiveInt } from '../../validators/common';
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
-import { createParamOptions } from '../../helpers/promptOptions';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { Balance } from '@polkadot/types/interfaces'
+import { formatBalance } from '@polkadot/util'
+import { positiveInt } from '../../validators/common'
+import chalk from 'chalk'
+import ExitCodes from '../../ExitCodes'
+import { createParamOptions } from '../../helpers/promptOptions'
 
 export default class WorkingGroupsIncreaseStake extends WorkingGroupsCommandBase {
-    static description =
-        'Increases current role (lead/worker) stake. Requires active role account to be selected.';
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Increases current role (lead/worker) stake. Requires active role account to be selected.'
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    async run() {
-        const account = await this.getRequiredSelectedAccount();
-        // Worker-only gate
-        const worker = await this.getRequiredWorker();
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    // Worker-only gate
+    const worker = await this.getRequiredWorker()
 
-        if (!worker.stake) {
-            this.error('Cannot increase stake. No associated role stake profile found!', { exit: ExitCodes.InvalidInput });
-        }
+    if (!worker.stake) {
+      this.error('Cannot increase stake. No associated role stake profile found!', { exit: ExitCodes.InvalidInput })
+    }
 
-        this.log(chalk.white('Current stake: ', formatBalance(worker.stake)));
-        const balance = await this.promptForParam('Balance', createParamOptions('amount', undefined, positiveInt())) as Balance;
+    this.log(chalk.white('Current stake: ', formatBalance(worker.stake)))
+    const balance = (await this.promptForParam(
+      'Balance',
+      createParamOptions('amount', undefined, positiveInt())
+    )) as Balance
 
-        await this.requestAccountDecoding(account);
+    await this.requestAccountDecoding(account)
 
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'increaseStake',
-            [
-                worker.workerId,
-                balance
-            ]
-        );
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'increaseStake', [
+      worker.workerId,
+      balance,
+    ])
 
-        this.log(chalk.green(
-            `Worker ${chalk.white(worker.workerId.toNumber())} stake has been increased by ${chalk.white(formatBalance(balance))}`
-        ));
-    }
+    this.log(
+      chalk.green(
+        `Worker ${chalk.white(worker.workerId.toNumber())} stake has been increased by ${chalk.white(
+          formatBalance(balance)
+        )}`
+      )
+    )
+  }
 }

+ 20 - 29
cli/src/commands/working-groups/leaveRole.ts

@@ -1,37 +1,28 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { minMaxStr } from '../../validators/common';
-import chalk from 'chalk';
-import { createParamOptions } from '../../helpers/promptOptions';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { minMaxStr } from '../../validators/common'
+import chalk from 'chalk'
+import { createParamOptions } from '../../helpers/promptOptions'
 
 export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
-    static description = 'Leave the worker or lead role associated with currently selected account.';
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Leave the worker or lead role associated with currently selected account.'
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    async run() {
-        const account = await this.getRequiredSelectedAccount();
-        // Worker-only gate
-        const worker = await this.getRequiredWorker();
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    // Worker-only gate
+    const worker = await this.getRequiredWorker()
 
-        const constraint = await this.getApi().workerExitRationaleConstraint(this.group);
-        const rationaleValidator = minMaxStr(constraint.min.toNumber(), constraint.max.toNumber());
-        const rationale = await this.promptForParam('Bytes', createParamOptions('rationale', undefined, rationaleValidator));
+    const constraint = await this.getApi().workerExitRationaleConstraint(this.group)
+    const rationaleValidator = minMaxStr(constraint.min.toNumber(), constraint.max.toNumber())
+    const rationale = await this.promptForParam('Bytes', createParamOptions('rationale', undefined, rationaleValidator))
 
-        await this.requestAccountDecoding(account);
+    await this.requestAccountDecoding(account)
 
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'leaveRole',
-            [
-                worker.workerId,
-                rationale
-            ]
-        );
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'leaveRole', [worker.workerId, rationale])
 
-        this.log(chalk.green(`Succesfully left the role! (worker id: ${chalk.white(worker.workerId.toNumber())})`));
-    }
+    this.log(chalk.green(`Succesfully left the role! (worker id: ${chalk.white(worker.workerId.toNumber())})`))
+  }
 }

+ 67 - 66
cli/src/commands/working-groups/opening.ts

@@ -1,79 +1,80 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import { displayTable, displayCollapsedRow, displayHeader } from '../../helpers/display';
-import _ from 'lodash';
-import { OpeningStatus, GroupOpeningStage, GroupOpeningStakes } from '../../Types';
-import { StakingAmountLimitModeKeys, StakingPolicy } from '@joystream/types/hiring';
-import { formatBalance } from '@polkadot/util';
-import chalk from 'chalk';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { displayTable, displayCollapsedRow, displayHeader } from '../../helpers/display'
+import _ from 'lodash'
+import { OpeningStatus, GroupOpeningStage, GroupOpeningStakes } from '../../Types'
+import { StakingAmountLimitModeKeys, StakingPolicy } from '@joystream/types/hiring'
+import { formatBalance } from '@polkadot/util'
+import chalk from 'chalk'
 
 export default class WorkingGroupsOpening extends WorkingGroupsCommandBase {
-    static description = 'Shows an overview of given working group opening by Working Group Opening ID';
-    static args = [
-        {
-            name: 'wgOpeningId',
-            required: true,
-            description: 'Working Group Opening ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Shows an overview of given working group opening by Working Group Opening ID'
+  static args = [
+    {
+      name: 'wgOpeningId',
+      required: true,
+      description: 'Working Group Opening ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    stageColumns(stage: GroupOpeningStage) {
-        const { status, date, block } = stage;
-        const statusTimeHeader = status === OpeningStatus.WaitingToBegin ? 'Starts at' : 'Last status change';
-        return {
-            'Stage': _.startCase(status),
-            [statusTimeHeader]: (date && block)
-                ? `~ ${date.toLocaleTimeString()} ${ date.toLocaleDateString()} (#${block})`
-                : (block && `#${block}` || '?')
-        };
+  stageColumns(stage: GroupOpeningStage) {
+    const { status, date, block } = stage
+    const statusTimeHeader = status === OpeningStatus.WaitingToBegin ? 'Starts at' : 'Last status change'
+    return {
+      Stage: _.startCase(status),
+      [statusTimeHeader]:
+        date && block
+          ? `~ ${date.toLocaleTimeString()} ${date.toLocaleDateString()} (#${block})`
+          : (block && `#${block}`) || '?',
     }
+  }
 
-    formatStake(stake: StakingPolicy | undefined) {
-        if (!stake) return 'NONE';
-        const { amount, amount_mode } = stake;
-        return amount_mode.type === StakingAmountLimitModeKeys.AtLeast
-            ? `>= ${ formatBalance(amount) }`
-            : `== ${ formatBalance(amount) }`;
-    }
+  formatStake(stake: StakingPolicy | undefined) {
+    if (!stake) return 'NONE'
+    const { amount, amount_mode: amountMode } = stake
+    return amountMode.type === StakingAmountLimitModeKeys.AtLeast
+      ? `>= ${formatBalance(amount)}`
+      : `== ${formatBalance(amount)}`
+  }
 
-    stakeColumns(stakes: GroupOpeningStakes) {
-        const { role, application } = stakes;
-        return {
-            'Application stake': this.formatStake(application),
-            'Role stake': this.formatStake(role),
-        }
+  stakeColumns(stakes: GroupOpeningStakes) {
+    const { role, application } = stakes
+    return {
+      'Application stake': this.formatStake(application),
+      'Role stake': this.formatStake(role),
     }
+  }
 
-    async run() {
-        const { args } = this.parse(WorkingGroupsOpening);
-
-        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+  async run() {
+    const { args } = this.parse(WorkingGroupsOpening)
 
-        displayHeader('Human readable text');
-        this.jsonPrettyPrint(opening.opening.human_readable_text.toString());
+    const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId))
 
-        displayHeader('Opening details');
-        const openingRow = {
-            'WG Opening ID': opening.wgOpeningId,
-            'Opening ID': opening.openingId,
-            'Type': opening.type.type,
-            ...this.stageColumns(opening.stage),
-            ...this.stakeColumns(opening.stakes)
-        };
-        displayCollapsedRow(openingRow);
+    displayHeader('Human readable text')
+    this.jsonPrettyPrint(opening.opening.human_readable_text.toString())
 
-        displayHeader(`Applications (${opening.applications.length})`);
-        const applicationsRows = opening.applications.map(a => ({
-            'WG appl. ID': a.wgApplicationId,
-            'Appl. ID': a.applicationId,
-            'Member': a.member?.handle.toString() || chalk.red('NONE'),
-            'Stage': a.stage,
-            'Appl. stake': a.stakes.application,
-            'Role stake': a.stakes.role,
-            'Total stake': Object.values(a.stakes).reduce((a, b) => a + b)
-        }));
-        displayTable(applicationsRows, 5);
+    displayHeader('Opening details')
+    const openingRow = {
+      'WG Opening ID': opening.wgOpeningId,
+      'Opening ID': opening.openingId,
+      Type: opening.type.type,
+      ...this.stageColumns(opening.stage),
+      ...this.stakeColumns(opening.stakes),
     }
+    displayCollapsedRow(openingRow)
+
+    displayHeader(`Applications (${opening.applications.length})`)
+    const applicationsRows = opening.applications.map((a) => ({
+      'WG appl. ID': a.wgApplicationId,
+      'Appl. ID': a.applicationId,
+      Member: a.member?.handle.toString() || chalk.red('NONE'),
+      Stage: a.stage,
+      'Appl. stake': a.stakes.application,
+      'Role stake': a.stakes.role,
+      'Total stake': Object.values(a.stakes).reduce((a, b) => a + b),
+    }))
+    displayTable(applicationsRows, 5)
   }
+}

+ 18 - 18
cli/src/commands/working-groups/openings.ts

@@ -1,23 +1,23 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import { displayTable } from '../../helpers/display';
-import _ from 'lodash';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { displayTable } from '../../helpers/display'
+import _ from 'lodash'
 
 export default class WorkingGroupsOpenings extends WorkingGroupsCommandBase {
-    static description = 'Shows an overview of given working group openings';
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Shows an overview of given working group openings'
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    async run() {
-        const openings = await this.getApi().openingsByGroup(this.group);
+  async run() {
+    const openings = await this.getApi().openingsByGroup(this.group)
 
-        const openingsRows = openings.map(o => ({
-            'WG Opening ID': o.wgOpeningId,
-            'Opening ID': o.openingId,
-            'Type': o.type.type,
-            'Stage': `${_.startCase(o.stage.status)}${o.stage.block ? ` (#${o.stage.block})` : ''}`,
-            'Applications': o.applications.length
-        }));
-        displayTable(openingsRows, 5);
-    }
+    const openingsRows = openings.map((o) => ({
+      'WG Opening ID': o.wgOpeningId,
+      'Opening ID': o.openingId,
+      Type: o.type.type,
+      Stage: `${_.startCase(o.stage.status)}${o.stage.block ? ` (#${o.stage.block})` : ''}`,
+      Applications: o.applications.length,
+    }))
+    displayTable(openingsRows, 5)
+  }
 }

+ 34 - 35
cli/src/commands/working-groups/overview.ts

@@ -1,41 +1,40 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import { displayHeader, displayNameValueTable, displayTable } from '../../helpers/display';
-import { formatBalance } from '@polkadot/util';
-import { shortAddress } from '../../helpers/display';
-import chalk from 'chalk';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { displayHeader, displayNameValueTable, displayTable, shortAddress } from '../../helpers/display'
+import { formatBalance } from '@polkadot/util'
 
-export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
-    static description = 'Shows an overview of given working group (current lead and workers)';
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+import chalk from 'chalk'
 
-    async run() {
-        const lead = await this.getApi().groupLead(this.group);
-        const members = await this.getApi().groupMembers(this.group);
+export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
+  static description = 'Shows an overview of given working group (current lead and workers)'
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-        displayHeader('Group lead');
-        if (lead) {
-            displayNameValueTable([
-                { name: 'Member id:', value: lead.memberId.toString() },
-                { name: 'Member handle:', value: lead.profile.handle.toString() },
-                { name: 'Role account:', value: lead.roleAccount.toString() },
-            ]);
-        }
-        else {
-            this.log(chalk.yellow('No lead assigned!'));
-        }
+  async run() {
+    const lead = await this.getApi().groupLead(this.group)
+    const members = await this.getApi().groupMembers(this.group)
 
-        displayHeader('Members');
-        const membersRows = members.map(m => ({
-            '': lead?.workerId.eq(m.workerId) ? "\u{2B50}" : '', // A nice star for the lead
-            'Worker id': m.workerId.toString(),
-            'Member id': m.memberId.toString(),
-            'Member handle': m.profile.handle.toString(),
-            'Stake': formatBalance(m.stake),
-            'Earned': formatBalance(m.reward?.totalRecieved),
-            'Role account': shortAddress(m.roleAccount)
-        }));
-        displayTable(membersRows, 5);
+    displayHeader('Group lead')
+    if (lead) {
+      displayNameValueTable([
+        { name: 'Member id:', value: lead.memberId.toString() },
+        { name: 'Member handle:', value: lead.profile.handle.toString() },
+        { name: 'Role account:', value: lead.roleAccount.toString() },
+      ])
+    } else {
+      this.log(chalk.yellow('No lead assigned!'))
     }
+
+    displayHeader('Members')
+    const membersRows = members.map((m) => ({
+      '': lead?.workerId.eq(m.workerId) ? '\u{2B50}' : '', // A nice star for the lead
+      'Worker id': m.workerId.toString(),
+      'Member id': m.memberId.toString(),
+      'Member handle': m.profile.handle.toString(),
+      Stake: formatBalance(m.stake),
+      Earned: formatBalance(m.reward?.totalRecieved),
+      'Role account': shortAddress(m.roleAccount),
+    }))
+    displayTable(membersRows, 5)
   }
+}

+ 52 - 50
cli/src/commands/working-groups/slashWorker.ts

@@ -1,53 +1,55 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { WorkerId } from '@joystream/types/working-group';
-import { Balance } from '@polkadot/types/interfaces';
-import { formatBalance } from '@polkadot/util';
-import { minMaxInt } from '../../validators/common';
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
-import { createParamOptions } from '../../helpers/promptOptions';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { WorkerId } from '@joystream/types/working-group'
+import { Balance } from '@polkadot/types/interfaces'
+import { formatBalance } from '@polkadot/util'
+import { minMaxInt } from '../../validators/common'
+import chalk from 'chalk'
+import { createParamOptions } from '../../helpers/promptOptions'
 
 export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
-    static description = 'Slashes given worker stake. Requires lead access.';
-    static args = [
-        {
-            name: 'workerId',
-            required: true,
-            description: 'Worker ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
-
-    async run() {
-        const { args } = this.parse(WorkingGroupsSlashWorker);
-
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
-
-        const workerId = parseInt(args.workerId);
-        const groupMember = await this.getWorkerWithStakeForLeadAction(workerId);
-
-        this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)));
-        const balanceValidator = minMaxInt(1, groupMember.stake.toNumber());
-        const balance = await this.promptForParam('Balance', createParamOptions('amount', undefined, balanceValidator)) as Balance;
-
-        await this.requestAccountDecoding(account);
-
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'slashStake',
-            [
-                new WorkerId(workerId),
-                balance
-            ]
-        );
-
-        this.log(chalk.green(`${chalk.white(formatBalance(balance))} from worker ${chalk.white(workerId)} stake has been succesfully slashed!`));
-    }
+  static description = 'Slashes given worker stake. Requires lead access.'
+  static args = [
+    {
+      name: 'workerId',
+      required: true,
+      description: 'Worker ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
+
+  async run() {
+    const { args } = this.parse(WorkingGroupsSlashWorker)
+
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
+
+    const workerId = parseInt(args.workerId)
+    const groupMember = await this.getWorkerWithStakeForLeadAction(workerId)
+
+    this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)))
+    const balanceValidator = minMaxInt(1, groupMember.stake.toNumber())
+    const balance = (await this.promptForParam(
+      'Balance',
+      createParamOptions('amount', undefined, balanceValidator)
+    )) as Balance
+
+    await this.requestAccountDecoding(account)
+
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'slashStake', [
+      new WorkerId(workerId),
+      balance,
+    ])
+
+    this.log(
+      chalk.green(
+        `${chalk.white(formatBalance(balance))} from worker ${chalk.white(
+          workerId
+        )} stake has been succesfully slashed!`
+      )
+    )
+  }
 }

+ 31 - 33
cli/src/commands/working-groups/startAcceptingApplications.ts

@@ -1,42 +1,40 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { OpeningStatus } from '../../Types';
-import { apiModuleByGroup } from '../../Api';
-import { OpeningId } from '@joystream/types/hiring';
-import chalk from 'chalk';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { OpeningStatus } from '../../Types'
+import { apiModuleByGroup } from '../../Api'
+import { OpeningId } from '@joystream/types/hiring'
+import chalk from 'chalk'
 
 export default class WorkingGroupsStartAcceptingApplications extends WorkingGroupsCommandBase {
-    static description = 'Changes the status of pending opening to "Accepting applications". Requires lead access.';
-    static args = [
-        {
-            name: 'wgOpeningId',
-            required: true,
-            description: 'Working Group Opening ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Changes the status of pending opening to "Accepting applications". Requires lead access.'
+  static args = [
+    {
+      name: 'wgOpeningId',
+      required: true,
+      description: 'Working Group Opening ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    async run() {
-        const { args } = this.parse(WorkingGroupsStartAcceptingApplications);
+  async run() {
+    const { args } = this.parse(WorkingGroupsStartAcceptingApplications)
 
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
 
-        const openingId = parseInt(args.wgOpeningId);
-        await this.validateOpeningForLeadAction(openingId, OpeningStatus.WaitingToBegin);
+    const openingId = parseInt(args.wgOpeningId)
+    await this.validateOpeningForLeadAction(openingId, OpeningStatus.WaitingToBegin)
 
-        await this.requestAccountDecoding(account);
+    await this.requestAccountDecoding(account)
 
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'acceptApplications',
-            [ new OpeningId(openingId) ]
-        );
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'acceptApplications', [
+      new OpeningId(openingId),
+    ])
 
-        this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${ chalk.white('Accepting Applications') }`));
-    }
+    this.log(
+      chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('Accepting Applications')}`)
+    )
+  }
 }

+ 29 - 33
cli/src/commands/working-groups/startReviewPeriod.ts

@@ -1,42 +1,38 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { OpeningStatus } from '../../Types';
-import { apiModuleByGroup } from '../../Api';
-import { OpeningId } from '@joystream/types/hiring';
-import chalk from 'chalk';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { OpeningStatus } from '../../Types'
+import { apiModuleByGroup } from '../../Api'
+import { OpeningId } from '@joystream/types/hiring'
+import chalk from 'chalk'
 
 export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommandBase {
-    static description = 'Changes the status of active opening to "In review". Requires lead access.';
-    static args = [
-        {
-            name: 'wgOpeningId',
-            required: true,
-            description: 'Working Group Opening ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Changes the status of active opening to "In review". Requires lead access.'
+  static args = [
+    {
+      name: 'wgOpeningId',
+      required: true,
+      description: 'Working Group Opening ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    async run() {
-        const { args } = this.parse(WorkingGroupsStartReviewPeriod);
+  async run() {
+    const { args } = this.parse(WorkingGroupsStartReviewPeriod)
 
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
 
-        const openingId = parseInt(args.wgOpeningId);
-        await this.validateOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications);
+    const openingId = parseInt(args.wgOpeningId)
+    await this.validateOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications)
 
-        await this.requestAccountDecoding(account);
+    await this.requestAccountDecoding(account)
 
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'beginApplicantReview',
-            [ new OpeningId(openingId) ]
-        );
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'beginApplicantReview', [
+      new OpeningId(openingId),
+    ])
 
-        this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${ chalk.white('In Review') }`));
-    }
+    this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('In Review')}`))
+  }
 }

+ 29 - 34
cli/src/commands/working-groups/terminateApplication.ts

@@ -1,43 +1,38 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import ExitCodes from '../../ExitCodes';
-import { apiModuleByGroup } from '../../Api';
-import { ApplicationStageKeys, ApplicationId } from '@joystream/types/hiring';
-import chalk from 'chalk';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { ApplicationStageKeys, ApplicationId } from '@joystream/types/hiring'
+import chalk from 'chalk'
 
 export default class WorkingGroupsTerminateApplication extends WorkingGroupsCommandBase {
-    static description = 'Terminates given working group application. Requires lead access.';
-    static args = [
-        {
-            name: 'wgApplicationId',
-            required: true,
-            description: 'Working Group Application ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = 'Terminates given working group application. Requires lead access.'
+  static args = [
+    {
+      name: 'wgApplicationId',
+      required: true,
+      description: 'Working Group Application ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    async run() {
-        const { args } = this.parse(WorkingGroupsTerminateApplication);
+  async run() {
+    const { args } = this.parse(WorkingGroupsTerminateApplication)
 
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
 
-        const applicationId = parseInt(args.wgApplicationId);
-        // We don't really need the application itself here, so this one is just for validation purposes
-        await this.getApplicationForLeadAction(applicationId, ApplicationStageKeys.Active);
+    const applicationId = parseInt(args.wgApplicationId)
+    // We don't really need the application itself here, so this one is just for validation purposes
+    await this.getApplicationForLeadAction(applicationId, ApplicationStageKeys.Active)
 
-        await this.requestAccountDecoding(account);
+    await this.requestAccountDecoding(account)
 
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'terminateApplication',
-            [new ApplicationId(applicationId)]
-        );
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'terminateApplication', [
+      new ApplicationId(applicationId),
+    ])
 
-        this.log(chalk.green(`Application ${chalk.white(applicationId)} has been succesfully terminated!`));
-    }
+    this.log(chalk.green(`Application ${chalk.white(applicationId)} has been succesfully terminated!`))
+  }
 }

+ 44 - 50
cli/src/commands/working-groups/updateRewardAccount.ts

@@ -1,54 +1,48 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { validateAddress } from '../../helpers/validation';
-import { GenericAccountId } from '@polkadot/types';
-import chalk from 'chalk';
-import ExitCodes from '../../ExitCodes';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { validateAddress } from '../../helpers/validation'
+import { GenericAccountId } from '@polkadot/types'
+import chalk from 'chalk'
+import ExitCodes from '../../ExitCodes'
 
 export default class WorkingGroupsUpdateRewardAccount extends WorkingGroupsCommandBase {
-    static description = 'Updates the worker/lead reward account (requires current role account to be selected)';
-    static args = [
-        {
-            name: 'accountAddress',
-            required: false,
-            description: 'New reward account address (if omitted, one of the existing CLI accounts can be selected)'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
-
-    async run() {
-        const { args } = this.parse(WorkingGroupsUpdateRewardAccount);
-
-        const account = await this.getRequiredSelectedAccount();
-        // Worker-only gate
-        const worker = await this.getRequiredWorker();
-
-        if (!worker.reward) {
-            this.error('There is no reward relationship associated with this role!', { exit: ExitCodes.InvalidInput });
-        }
-
-        let newRewardAccount: string = args.accountAddress;
-        if (!newRewardAccount) {
-            const accounts = await this.fetchAccounts();
-            newRewardAccount = (await this.promptForAccount(accounts, undefined, 'Choose the new reward account')).address;
-        }
-        validateAddress(newRewardAccount);
-
-        await this.requestAccountDecoding(account);
-
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'updateRewardAccount',
-            [
-                worker.workerId,
-                new GenericAccountId(newRewardAccount)
-            ]
-        );
-
-        this.log(chalk.green(`Succesfully updated the reward account to: ${chalk.white(newRewardAccount)})`));
+  static description = 'Updates the worker/lead reward account (requires current role account to be selected)'
+  static args = [
+    {
+      name: 'accountAddress',
+      required: false,
+      description: 'New reward account address (if omitted, one of the existing CLI accounts can be selected)',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
+
+  async run() {
+    const { args } = this.parse(WorkingGroupsUpdateRewardAccount)
+
+    const account = await this.getRequiredSelectedAccount()
+    // Worker-only gate
+    const worker = await this.getRequiredWorker()
+
+    if (!worker.reward) {
+      this.error('There is no reward relationship associated with this role!', { exit: ExitCodes.InvalidInput })
     }
+
+    let newRewardAccount: string = args.accountAddress
+    if (!newRewardAccount) {
+      const accounts = await this.fetchAccounts()
+      newRewardAccount = (await this.promptForAccount(accounts, undefined, 'Choose the new reward account')).address
+    }
+    validateAddress(newRewardAccount)
+
+    await this.requestAccountDecoding(account)
+
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRewardAccount', [
+      worker.workerId,
+      new GenericAccountId(newRewardAccount),
+    ])
+
+    this.log(chalk.green(`Succesfully updated the reward account to: ${chalk.white(newRewardAccount)})`))
+  }
 }

+ 54 - 60
cli/src/commands/working-groups/updateRoleAccount.ts

@@ -1,64 +1,58 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { validateAddress } from '../../helpers/validation';
-import { GenericAccountId } from '@polkadot/types';
-import chalk from 'chalk';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { validateAddress } from '../../helpers/validation'
+import { GenericAccountId } from '@polkadot/types'
+import chalk from 'chalk'
 
 export default class WorkingGroupsUpdateRoleAccount extends WorkingGroupsCommandBase {
-    static description = 'Updates the worker/lead role account. Requires member controller account to be selected';
-    static args = [
-        {
-            name: 'accountAddress',
-            required: false,
-            description: 'New role account address (if omitted, one of the existing CLI accounts can be selected)'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
-
-    async run() {
-        const { args } = this.parse(WorkingGroupsUpdateRoleAccount);
-
-        const account = await this.getRequiredSelectedAccount();
-        const worker = await this.getRequiredWorkerByMemberController();
-
-        const cliAccounts = await this.fetchAccounts();
-        let newRoleAccount: string = args.accountAddress;
-        if (!newRoleAccount) {
-            newRoleAccount = (await this.promptForAccount(cliAccounts, undefined, 'Choose the new role account')).address;
-        }
-        validateAddress(newRoleAccount);
-
-        await this.requestAccountDecoding(account);
-
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'updateRoleAccount',
-            [
-                worker.workerId,
-                new GenericAccountId(newRoleAccount)
-            ]
-        );
-
-        this.log(chalk.green(`Succesfully updated the role account to: ${chalk.white(newRoleAccount)})`));
-
-        const matchingAccount = cliAccounts.find(account => account.address === newRoleAccount);
-        if (matchingAccount) {
-            const switchAccount = await this.simplePrompt({
-                type: 'confirm',
-                message: 'Do you want to switch the currenly selected CLI account to the new role account?',
-                default: false
-            });
-            if (switchAccount) {
-                await this.setSelectedAccount(matchingAccount);
-                this.log(
-                    chalk.green('Account switched to: ') +
-                    chalk.white(`${matchingAccount.meta.name} (${matchingAccount.address})`)
-                );
-            }
-        }
+  static description = 'Updates the worker/lead role account. Requires member controller account to be selected'
+  static args = [
+    {
+      name: 'accountAddress',
+      required: false,
+      description: 'New role account address (if omitted, one of the existing CLI accounts can be selected)',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
+
+  async run() {
+    const { args } = this.parse(WorkingGroupsUpdateRoleAccount)
+
+    const account = await this.getRequiredSelectedAccount()
+    const worker = await this.getRequiredWorkerByMemberController()
+
+    const cliAccounts = await this.fetchAccounts()
+    let newRoleAccount: string = args.accountAddress
+    if (!newRoleAccount) {
+      newRoleAccount = (await this.promptForAccount(cliAccounts, undefined, 'Choose the new role account')).address
+    }
+    validateAddress(newRoleAccount)
+
+    await this.requestAccountDecoding(account)
+
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRoleAccount', [
+      worker.workerId,
+      new GenericAccountId(newRoleAccount),
+    ])
+
+    this.log(chalk.green(`Succesfully updated the role account to: ${chalk.white(newRoleAccount)})`))
+
+    const matchingAccount = cliAccounts.find((account) => account.address === newRoleAccount)
+    if (matchingAccount) {
+      const switchAccount = await this.simplePrompt({
+        type: 'confirm',
+        message: 'Do you want to switch the currenly selected CLI account to the new role account?',
+        default: false,
+      })
+      if (switchAccount) {
+        await this.setSelectedAccount(matchingAccount)
+        this.log(
+          chalk.green('Account switched to: ') +
+            chalk.white(`${matchingAccount.meta.name} (${matchingAccount.address})`)
+        )
+      }
     }
+  }
 }

+ 53 - 58
cli/src/commands/working-groups/updateWorkerReward.ts

@@ -1,72 +1,67 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import _ from 'lodash';
-import { apiModuleByGroup } from '../../Api';
-import { WorkerId } from '@joystream/types/working-group';
-import { formatBalance } from '@polkadot/util';
-import chalk from 'chalk';
-import { Reward } from '../../Types';
-import { positiveInt } from '../../validators/common';
-import { createParamOptions } from '../../helpers/promptOptions';
-import ExitCodes from '../../ExitCodes';
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import { apiModuleByGroup } from '../../Api'
+import { WorkerId } from '@joystream/types/working-group'
+import { formatBalance } from '@polkadot/util'
+import chalk from 'chalk'
+import { Reward } from '../../Types'
+import { positiveInt } from '../../validators/common'
+import { createParamOptions } from '../../helpers/promptOptions'
+import ExitCodes from '../../ExitCodes'
 
 export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsCommandBase {
-    static description = 'Change given worker\'s reward (amount only). Requires lead access.';
-    static args = [
-        {
-            name: 'workerId',
-            required: true,
-            description: 'Worker ID'
-        },
-    ]
-    static flags = {
-        ...WorkingGroupsCommandBase.flags,
-    };
+  static description = "Change given worker's reward (amount only). Requires lead access."
+  static args = [
+    {
+      name: 'workerId',
+      required: true,
+      description: 'Worker ID',
+    },
+  ]
+  static flags = {
+    ...WorkingGroupsCommandBase.flags,
+  }
 
-    formatReward(reward?: Reward) {
-        return (
-            reward
-                ?
-                    formatBalance(reward.value) + (reward.interval && ` / ${reward.interval} block(s)`) +
-                    (reward.nextPaymentBlock && ` (next payment: #${ reward.nextPaymentBlock })`)
-                : 'NONE'
-        );
-    }
+  formatReward(reward?: Reward) {
+    return reward
+      ? formatBalance(reward.value) +
+          (reward.interval && ` / ${reward.interval} block(s)`) +
+          (reward.nextPaymentBlock && ` (next payment: #${reward.nextPaymentBlock})`)
+      : 'NONE'
+  }
 
-    async run() {
-        const { args } = this.parse(WorkingGroupsUpdateWorkerReward);
+  async run() {
+    const { args } = this.parse(WorkingGroupsUpdateWorkerReward)
 
-        const account = await this.getRequiredSelectedAccount();
-        // Lead-only gate
-        await this.getRequiredLead();
+    const account = await this.getRequiredSelectedAccount()
+    // Lead-only gate
+    await this.getRequiredLead()
 
-        const workerId = parseInt(args.workerId);
-        // This will also make sure the worker is valid
-        const groupMember = await this.getWorkerForLeadAction(workerId);
+    const workerId = parseInt(args.workerId)
+    // This will also make sure the worker is valid
+    const groupMember = await this.getWorkerForLeadAction(workerId)
 
-        const { reward } = groupMember;
+    const { reward } = groupMember
 
-        if (!reward) {
-            this.error('There is no reward relationship associated with this worker!', { exit: ExitCodes.InvalidInput });
-        }
+    if (!reward) {
+      this.error('There is no reward relationship associated with this worker!', { exit: ExitCodes.InvalidInput })
+    }
 
-        console.log(chalk.white(`Current worker reward: ${this.formatReward(reward)}`));
+    console.log(chalk.white(`Current worker reward: ${this.formatReward(reward)}`))
 
-        const newRewardValue = await this.promptForParam('BalanceOfMint', createParamOptions('new_amount', undefined, positiveInt()));
+    const newRewardValue = await this.promptForParam(
+      'BalanceOfMint',
+      createParamOptions('new_amount', undefined, positiveInt())
+    )
 
-        await this.requestAccountDecoding(account);
+    await this.requestAccountDecoding(account)
 
-        await this.sendAndFollowExtrinsic(
-            account,
-            apiModuleByGroup[this.group],
-            'updateRewardAmount',
-            [
-                new WorkerId(workerId),
-                newRewardValue
-            ]
-        );
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRewardAmount', [
+      new WorkerId(workerId),
+      newRewardValue,
+    ])
 
-        const updatedGroupMember = await this.getApi().groupMember(this.group, workerId);
-        this.log(chalk.green(`Worker ${chalk.white(workerId)} reward has been updated!`));
-        this.log(chalk.green(`New worker reward: ${chalk.white(this.formatReward(updatedGroupMember.reward))}`));
-    }
+    const updatedGroupMember = await this.getApi().groupMember(this.group, workerId)
+    this.log(chalk.green(`Worker ${chalk.white(workerId)} reward has been updated!`))
+    this.log(chalk.green(`New worker reward: ${chalk.white(this.formatReward(updatedGroupMember.reward))}`))
+  }
 }

+ 49 - 49
cli/src/helpers/display.ts

@@ -1,72 +1,72 @@
-import { cli, Table } from 'cli-ux';
-import chalk from 'chalk';
-import { NameValueObj } from '../Types';
-import { AccountId } from '@polkadot/types/interfaces';
+import { cli, Table } from 'cli-ux'
+import chalk from 'chalk'
+import { NameValueObj } from '../Types'
+import { AccountId } from '@polkadot/types/interfaces'
 
-export function displayHeader(caption: string, placeholderSign: string = '_', size: number = 50) {
-    let singsPerSide: number = Math.floor((size - (caption.length + 2)) / 2);
-    let finalStr: string = '';
-    for (let i = 0; i < singsPerSide; ++i) finalStr += placeholderSign;
-    finalStr += ` ${ caption} `;
-    while (finalStr.length < size) finalStr += placeholderSign;
+export function displayHeader(caption: string, placeholderSign = '_', size = 50) {
+  const singsPerSide: number = Math.floor((size - (caption.length + 2)) / 2)
+  let finalStr = ''
+  for (let i = 0; i < singsPerSide; ++i) finalStr += placeholderSign
+  finalStr += ` ${caption} `
+  while (finalStr.length < size) finalStr += placeholderSign
 
-    process.stdout.write("\n" + chalk.bold.blueBright(finalStr) + "\n\n");
+  process.stdout.write('\n' + chalk.bold.blueBright(finalStr) + '\n\n')
 }
 
 export function displayNameValueTable(rows: NameValueObj[]) {
-    cli.table(
-        rows,
-        {
-            name: { minWidth: 30, get: row => chalk.bold.white(row.name) },
-            value: { get: row => chalk.white(row.value) }
-        },
-        { 'no-header': true }
-    );
+  cli.table(
+    rows,
+    {
+      name: { minWidth: 30, get: (row) => chalk.bold.white(row.name) },
+      value: { get: (row) => chalk.white(row.value) },
+    },
+    { 'no-header': true }
+  )
 }
 
 export function displayCollapsedRow(row: { [k: string]: string | number }) {
-    const collapsedRow: NameValueObj[] = Object.keys(row).map(name => ({
-        name,
-        value: typeof row[name] === 'string' ? row[name] as string : row[name].toString()
-    }));
+  const collapsedRow: NameValueObj[] = Object.keys(row).map((name) => ({
+    name,
+    value: typeof row[name] === 'string' ? (row[name] as string) : row[name].toString(),
+  }))
 
-    displayNameValueTable(collapsedRow);
+  displayNameValueTable(collapsedRow)
 }
 
 export function displayCollapsedTable(rows: { [k: string]: string | number }[]) {
-    for (const row of rows) displayCollapsedRow(row);
+  for (const row of rows) displayCollapsedRow(row)
 }
 
 export function displayTable(rows: { [k: string]: string | number }[], cellHorizontalPadding = 0) {
-    if (!rows.length) {
-        return;
-    }
-    const maxLength = (columnName: string) => rows.reduce(
-        (maxLength, row) => {
-            const val = row[columnName];
-            const valLength = typeof val === 'string' ? val.length : val.toString().length;
-            return Math.max(maxLength, valLength);
-        },
-        columnName.length
-    )
-    const columnDef = (columnName: string) => ({
-        get: (row: typeof rows[number])  => chalk.white(`${row[columnName]}`),
-        minWidth: maxLength(columnName) + cellHorizontalPadding
-    });
-    let columns: Table.table.Columns<{ [k: string]: string }> = {};
-    Object.keys(rows[0]).forEach(columnName => columns[columnName] = columnDef(columnName))
-    cli.table(rows, columns);
+  if (!rows.length) {
+    return
+  }
+  const maxLength = (columnName: string) =>
+    rows.reduce((maxLength, row) => {
+      const val = row[columnName]
+      const valLength = typeof val === 'string' ? val.length : val.toString().length
+      return Math.max(maxLength, valLength)
+    }, columnName.length)
+  const columnDef = (columnName: string) => ({
+    get: (row: typeof rows[number]) => chalk.white(`${row[columnName]}`),
+    minWidth: maxLength(columnName) + cellHorizontalPadding,
+  })
+  const columns: Table.table.Columns<{ [k: string]: string }> = {}
+  Object.keys(rows[0]).forEach((columnName) => (columns[columnName] = columnDef(columnName)))
+  cli.table(rows, columns)
 }
 
 export function toFixedLength(text: string, length: number, spacesOnLeft = false): string {
-    if (text.length > length && length > 3) {
-        return text.slice(0, length-3) + '...';
-    }
-    while(text.length < length) { spacesOnLeft ? text = ' '+text : text += ' ' };
+  if (text.length > length && length > 3) {
+    return text.slice(0, length - 3) + '...'
+  }
+  while (text.length < length) {
+    spacesOnLeft ? (text = ' ' + text) : (text += ' ')
+  }
 
-    return text;
+  return text
 }
 
 export function shortAddress(address: AccountId | string): string {
-    return address.toString().substr(0, 6) + '...' + address.toString().substr(-6);
+  return address.toString().substr(0, 6) + '...' + address.toString().substr(-6)
 }

+ 21 - 19
cli/src/helpers/promptOptions.ts

@@ -1,28 +1,30 @@
 import { ApiParamsOptions, ApiMethodNamedArgs, ApiParamOptions, ApiMethodArg } from '../Types'
-import { Validator } from 'inquirer';
+import { Validator } from 'inquirer'
 
 export function setDefaults(promptOptions: ApiParamsOptions, defaultValues: ApiMethodNamedArgs) {
-    for (const defaultValue of defaultValues) {
-        const { name: paramName, value: paramValue } = defaultValue;
-        const paramOptions = promptOptions[paramName];
-        if (paramOptions && paramOptions.value) {
-            paramOptions.value.default = paramValue;
-        }
-        else if (paramOptions) {
-            promptOptions[paramName].value = { default: paramValue };
-        }
-        else {
-            promptOptions[paramName] = { value: { default: paramValue } };
-        }
+  for (const defaultValue of defaultValues) {
+    const { name: paramName, value: paramValue } = defaultValue
+    const paramOptions = promptOptions[paramName]
+    if (paramOptions && paramOptions.value) {
+      paramOptions.value.default = paramValue
+    } else if (paramOptions) {
+      promptOptions[paramName].value = { default: paramValue }
+    } else {
+      promptOptions[paramName] = { value: { default: paramValue } }
     }
+  }
 }
 
 // Temporary(?) helper for easier creation of common ApiParamOptions
-export function createParamOptions(forcedName?: string, defaultValue?: ApiMethodArg | undefined, validator?: Validator): ApiParamOptions {
-    const paramOptions: ApiParamOptions = { forcedName, validator };
-    if (defaultValue) {
-        paramOptions.value = { default: defaultValue };
-    }
+export function createParamOptions(
+  forcedName?: string,
+  defaultValue?: ApiMethodArg | undefined,
+  validator?: Validator
+): ApiParamOptions {
+  const paramOptions: ApiParamOptions = { forcedName, validator }
+  if (defaultValue) {
+    paramOptions.value = { default: defaultValue }
+  }
 
-    return paramOptions;
+  return paramOptions
 }

+ 14 - 14
cli/src/helpers/validation.ts

@@ -1,19 +1,19 @@
-import BN from 'bn.js';
-import ExitCodes from '../ExitCodes';
-import { decodeAddress } from '@polkadot/util-crypto';
-import { DerivedBalances } from '@polkadot/api-derive/types';
-import { CLIError } from '@oclif/errors';
+import BN from 'bn.js'
+import ExitCodes from '../ExitCodes'
+import { decodeAddress } from '@polkadot/util-crypto'
+import { DerivedBalances } from '@polkadot/api-derive/types'
+import { CLIError } from '@oclif/errors'
 
-export function validateAddress(address: string, errorMessage: string = 'Invalid address'): void {
-    try {
-        decodeAddress(address);
-    } catch (e) {
-        throw new CLIError(errorMessage, { exit: ExitCodes.InvalidInput });
-    }
+export function validateAddress(address: string, errorMessage = 'Invalid address'): void {
+  try {
+    decodeAddress(address)
+  } catch (e) {
+    throw new CLIError(errorMessage, { exit: ExitCodes.InvalidInput })
+  }
 }
 
 export function checkBalance(accBalances: DerivedBalances, requiredBalance: BN): void {
-    if (requiredBalance.gt(accBalances.availableBalance)) {
-        throw new CLIError('Not enough balance available', { exit: ExitCodes.InvalidInput });
-    }
+  if (requiredBalance.gt(accBalances.availableBalance)) {
+    throw new CLIError('Not enough balance available', { exit: ExitCodes.InvalidInput })
+  }
 }

+ 1 - 1
cli/src/index.ts

@@ -1 +1 @@
-export {run} from '@oclif/command'
+export { run } from '@oclif/command'

+ 38 - 33
cli/src/promptOptions/addWorkerOpening.ts

@@ -1,39 +1,44 @@
-import { ApiParamsOptions, ApiParamOptions, HRTStruct } from '../Types';
-import { OpeningType, SlashingTerms, UnslashableTerms, OpeningType_Worker } from '@joystream/types/working-group';
-import { Bytes } from '@polkadot/types';
-import { schemaValidator } from '@joystream/types/hiring';
-import { WorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group';
+import { ApiParamsOptions, ApiParamOptions, HRTStruct } from '../Types'
+import {
+  OpeningType,
+  SlashingTerms,
+  UnslashableTerms,
+  OpeningType_Worker as OpeningTypeWorker,
+  WorkingGroupOpeningPolicyCommitment,
+} from '@joystream/types/working-group'
+import { Bytes } from '@polkadot/types'
+import { schemaValidator } from '@joystream/types/hiring'
 
 class OpeningPolicyCommitmentOptions implements ApiParamsOptions {
-    [paramName: string]: ApiParamOptions;
-    public role_slashing_terms: ApiParamOptions<SlashingTerms> = {
-        value: {
-            default: SlashingTerms.create('Unslashable', new UnslashableTerms()),
-            locked: true
-        }
-    }
+  [paramName: string]: ApiParamOptions
+  public role_slashing_terms: ApiParamOptions<SlashingTerms> = {
+    value: {
+      default: SlashingTerms.create('Unslashable', new UnslashableTerms()),
+      locked: true,
+    },
+  }
 }
 
 class AddWrokerOpeningOptions implements ApiParamsOptions {
-    [paramName: string]: ApiParamOptions;
-    // Lock value for opening_type
-    public opening_type: ApiParamOptions<OpeningType> = {
-        value: {
-            default: OpeningType.create('Worker', new OpeningType_Worker()),
-            locked: true
-        }
-    };
-    // Json schema for human_readable_text
-    public human_readable_text: ApiParamOptions<Bytes> = {
-        jsonSchema: {
-            schemaValidator,
-            struct: HRTStruct
-        }
-    }
-    // Lock value for role_slashing_terms
-    public commitment: ApiParamOptions<WorkingGroupOpeningPolicyCommitment> = {
-        nestedOptions: new OpeningPolicyCommitmentOptions()
-    }
-};
+  [paramName: string]: ApiParamOptions
+  // Lock value for opening_type
+  public opening_type: ApiParamOptions<OpeningType> = {
+    value: {
+      default: OpeningType.create('Worker', new OpeningTypeWorker()),
+      locked: true,
+    },
+  }
+  // Json schema for human_readable_text
+  public human_readable_text: ApiParamOptions<Bytes> = {
+    jsonSchema: {
+      schemaValidator,
+      struct: HRTStruct,
+    },
+  }
+  // Lock value for role_slashing_terms
+  public commitment: ApiParamOptions<WorkingGroupOpeningPolicyCommitment> = {
+    nestedOptions: new OpeningPolicyCommitmentOptions(),
+  }
+}
 
-export default AddWrokerOpeningOptions;
+export default AddWrokerOpeningOptions

+ 38 - 49
cli/src/validators/common.ts

@@ -3,60 +3,49 @@
 // (usable with inquirer package)
 //
 
-type Validator = (value: any) => boolean | string;
-
-export const isInt = (message?: string) => (value: any) => (
-    (
-        ((typeof value === 'number') && Math.floor(value) === value) ||
-        ((typeof value === 'string') && parseInt(value).toString() === value)
-    )
-        ? true
-        : message || 'The value must be an integer!'
-);
-
-export const gte = (min: number, message?: string) => (value: any) => (
-    parseFloat(value) >= min
-        ? true
-        : message?.replace('{min}', min.toString()) || `The value must be a number greater than or equal ${min}`
-)
-
-export const lte = (max: number, message?: string) => (value: any) => (
-    parseFloat(value) <= max
-        ? true
-        : message?.replace('{max}', max.toString()) || `The value must be less than or equal ${max}`
-);
-
-export const minLen = (min: number, message?: string) => (value: any) => (
-    typeof value === 'string' && value.length >= min
-        ? true
-        : message?.replace('{min}', min.toString()) || `The value should be at least ${min} character(s) long`
-)
-
-export const maxLen = (max: number, message?: string) => (value: any) => (
-    typeof value === 'string' && value.length <= max
-        ? true
-        : message?.replace('{max}', max.toString()) || `The value cannot be more than ${max} character(s) long`
-);
+type Validator = (value: any) => boolean | string
+
+export const isInt = (message?: string) => (value: any) =>
+  (typeof value === 'number' && Math.floor(value) === value) ||
+  (typeof value === 'string' && parseInt(value).toString() === value)
+    ? true
+    : message || 'The value must be an integer!'
+
+export const gte = (min: number, message?: string) => (value: any) =>
+  parseFloat(value) >= min
+    ? true
+    : message?.replace('{min}', min.toString()) || `The value must be a number greater than or equal ${min}`
+
+export const lte = (max: number, message?: string) => (value: any) =>
+  parseFloat(value) <= max
+    ? true
+    : message?.replace('{max}', max.toString()) || `The value must be less than or equal ${max}`
+
+export const minLen = (min: number, message?: string) => (value: any) =>
+  typeof value === 'string' && value.length >= min
+    ? true
+    : message?.replace('{min}', min.toString()) || `The value should be at least ${min} character(s) long`
+
+export const maxLen = (max: number, message?: string) => (value: any) =>
+  typeof value === 'string' && value.length <= max
+    ? true
+    : message?.replace('{max}', max.toString()) || `The value cannot be more than ${max} character(s) long`
 
 export const combined = (validators: Validator[], message?: string) => (value: any) => {
-    for (let validator of validators) {
-        const result = validator(value);
-        if (result !== true) {
-            return message || result;
-        }
+  for (const validator of validators) {
+    const result = validator(value)
+    if (result !== true) {
+      return message || result
     }
+  }
 
-    return true;
+  return true
 }
 
-export const positiveInt = (message?: string) => combined([ isInt(), gte(0) ], message);
+export const positiveInt = (message?: string) => combined([isInt(), gte(0)], message)
 
-export const minMaxInt = (min: number, max: number, message?: string) => combined(
-    [ isInt(), gte(min), lte(max) ],
-    message?.replace('{min}', min.toString()).replace('{max}', max.toString())
-);
+export const minMaxInt = (min: number, max: number, message?: string) =>
+  combined([isInt(), gte(min), lte(max)], message?.replace('{min}', min.toString()).replace('{max}', max.toString()))
 
-export const minMaxStr = (min: number, max: number, message?: string) => combined(
-    [ minLen(min), maxLen(max) ],
-    message?.replace('{min}', min.toString()).replace('{max}', max.toString())
-);
+export const minMaxStr = (min: number, max: number, message?: string) =>
+  combined([minLen(min), maxLen(max)], message?.replace('{min}', min.toString()).replace('{max}', max.toString()))

+ 7 - 7
cli/test/commands/council/info.test.ts

@@ -1,11 +1,11 @@
-import {expect, test} from '@oclif/test'
+import { expect, test } from '@oclif/test'
 
 describe('info', () => {
   test
-  .stdout()
-  .command(['council:info'])
-  .exit(0)
-  .it('displays "Council" string', ctx => {
-    expect(ctx.stdout).to.contain('Council')
-  })
+    .stdout()
+    .command(['council:info'])
+    .exit(0)
+    .it('displays "Council" string', (ctx) => {
+      expect(ctx.stdout).to.contain('Council')
+    })
 })

+ 1 - 3
cli/test/tsconfig.json

@@ -3,7 +3,5 @@
   "compilerOptions": {
     "noEmit": true
   },
-  "references": [
-    {"path": ".."}
-  ]
+  "references": [{ "path": ".." }]
 }

+ 2 - 1
cli/tsconfig.json

@@ -8,7 +8,8 @@
     "strict": true,
     "target": "es2017",
     "esModuleInterop": true,
-	"types" : [ "node" ]
+    "types" : [ "node" ],
+    "noUnusedLocals": true
   },
   "include": [
     "src/**/*"