Browse Source

CLI initial compatibility

Leszek Wiesner 4 years ago
parent
commit
000db57baf
28 changed files with 389 additions and 859 deletions
  1. 106 236
      cli/src/Api.ts
  2. 30 113
      cli/src/Types.ts
  3. 11 7
      cli/src/base/ApiCommandBase.ts
  4. 1 1
      cli/src/base/ContentDirectoryCommandBase.ts
  5. 15 33
      cli/src/base/WorkingGroupsCommandBase.ts
  6. 8 9
      cli/src/commands/api/inspect.ts
  7. 2 2
      cli/src/commands/content-directory/createCuratorGroup.ts
  8. 3 1
      cli/src/commands/content-directory/curatorGroup.ts
  9. 0 56
      cli/src/commands/council/info.ts
  10. 4 9
      cli/src/commands/working-groups/application.ts
  11. 27 37
      cli/src/commands/working-groups/createOpening.ts
  12. 18 13
      cli/src/commands/working-groups/decreaseWorkerStake.ts
  13. 28 16
      cli/src/commands/working-groups/evictWorker.ts
  14. 3 10
      cli/src/commands/working-groups/fillOpening.ts
  15. 20 12
      cli/src/commands/working-groups/increaseStake.ts
  16. 13 7
      cli/src/commands/working-groups/leaveRole.ts
  17. 19 51
      cli/src/commands/working-groups/opening.ts
  18. 1 3
      cli/src/commands/working-groups/openings.ts
  19. 4 3
      cli/src/commands/working-groups/overview.ts
  20. 29 14
      cli/src/commands/working-groups/slashWorker.ts
  21. 0 38
      cli/src/commands/working-groups/startAcceptingApplications.ts
  22. 0 36
      cli/src/commands/working-groups/startReviewPeriod.ts
  23. 0 37
      cli/src/commands/working-groups/terminateApplication.ts
  24. 18 25
      cli/src/commands/working-groups/updateWorkerReward.ts
  25. 5 0
      cli/src/helpers/validation.ts
  26. 14 50
      cli/src/json-schemas/WorkingGroupOpening.schema.json
  27. 9 40
      cli/src/json-schemas/typings/WorkingGroupOpening.schema.d.ts
  28. 1 0
      cli/tsconfig.json

+ 106 - 236
cli/src/Api.ts

@@ -1,66 +1,50 @@
 import BN from 'bn.js'
 import { types } from '@joystream/types/'
 import { ApiPromise, WsProvider } from '@polkadot/api'
-import { QueryableStorageMultiArg, SubmittableExtrinsic, QueryableStorageEntry } from '@polkadot/api/types'
+import { SubmittableExtrinsic, AugmentedQuery } from '@polkadot/api/types'
 import { formatBalance } from '@polkadot/util'
-import { Balance, Moment, BlockNumber } from '@polkadot/types/interfaces'
+import { Balance } from '@polkadot/types/interfaces'
 import { KeyringPair } from '@polkadot/keyring/types'
-import { Codec, CodecArg } from '@polkadot/types/types'
-import { Option, Vec, UInt } from '@polkadot/types'
+import { Codec, Observable } from '@polkadot/types/types'
+import { UInt } from '@polkadot/types'
 import {
   AccountSummary,
-  CouncilInfoObj,
-  CouncilInfoTuple,
-  createCouncilInfoObj,
   WorkingGroups,
   Reward,
   GroupMember,
-  OpeningStatus,
-  GroupOpeningStage,
-  GroupOpening,
-  GroupApplication,
-  openingPolicyUnstakingPeriodsKeys,
-  UnstakingPeriods,
-  StakingPolicyUnstakingPeriodKey,
+  ApplicationDetails,
+  OpeningDetails,
+  UnaugmentedApiPromise,
 } from './Types'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { CLIError } from '@oclif/errors'
-import ExitCodes from './ExitCodes'
 import {
   Worker,
   WorkerId,
-  RoleStakeProfile,
-  Opening as WGOpening,
-  Application as WGApplication,
-  StorageProviderId,
-} from '@joystream/types/working-group'
-import {
-  Opening,
+  OpeningId,
   Application,
-  OpeningStage,
-  ApplicationStageKeys,
   ApplicationId,
-  OpeningId,
-  StakingPolicy,
-} from '@joystream/types/hiring'
-import { MemberId, Membership } from '@joystream/types/members'
-import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards'
-import { Stake, StakeId } from '@joystream/types/stake'
-
-import { InputValidationLengthConstraint } from '@joystream/types/common'
+  StorageProviderId,
+  Opening,
+} from '@joystream/types/working-group'
+import { Membership } from '@joystream/types/members'
+import { AccountId, MemberId } from '@joystream/types/common'
 import { Class, ClassId, CuratorGroup, CuratorGroupId, Entity, EntityId } from '@joystream/types/content-directory'
 import { ContentId, DataObject } from '@joystream/types/media'
-import { ServiceProviderRecord, Url } from '@joystream/types/discovery'
+import { ServiceProviderRecord } from '@joystream/types/discovery'
 import _ from 'lodash'
+import ExitCodes from './ExitCodes'
 
 export const DEFAULT_API_URI = 'ws://localhost:9944/'
 const DEFAULT_DECIMALS = new BN(12)
 
 // Mapping of working group to api module
-export const apiModuleByGroup: { [key in WorkingGroups]: string } = {
+export const apiModuleByGroup = {
   [WorkingGroups.StorageProviders]: 'storageWorkingGroup',
   [WorkingGroups.Curators]: 'contentDirectoryWorkingGroup',
-}
+  [WorkingGroups.Forum]: 'forumWorkingGroup',
+  [WorkingGroups.Membership]: 'membershipWorkingGroup',
+} as const
 
 // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
 export default class Api {
@@ -75,6 +59,11 @@ export default class Api {
     return this._api
   }
 
+  // Get api for use-cases where no type augmentations are desirable
+  public getUnaugmentedApi(): UnaugmentedApiPromise {
+    return (this._api as unknown) as UnaugmentedApiPromise
+  }
+
   private static async initApi(
     apiUri: string = DEFAULT_API_URI,
     metadataCache: Record<string, any>
@@ -140,27 +129,6 @@ export default class Api {
     return { balances }
   }
 
-  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
-
-    return createCouncilInfoObj(...results)
-  }
-
   async estimateFee(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<Balance> {
     const paymentInfo = await tx.paymentInfo(account)
     return paymentInfo.partialFee
@@ -174,12 +142,10 @@ export default class Api {
   // TODO: This is a lot of repeated logic from "/pioneer/joy-utils/transport"
   // It will be refactored to "joystream-js" soon
   async entriesByIds<IDType extends UInt, ValueType extends Codec>(
-    apiMethod: QueryableStorageEntry<'promise'>,
-    firstKey?: CodecArg // First key in case of double maps
+    apiMethod: AugmentedQuery<'promise', (key: IDType) => Observable<ValueType>>
   ): Promise<[IDType, ValueType][]> {
-    const entries: [IDType, ValueType][] = (await apiMethod.entries<ValueType>(firstKey)).map(([storageKey, value]) => [
-      // If double-map (first key is provided), we map entries by second key
-      storageKey.args[firstKey !== undefined ? 1 : 0] as IDType,
+    const entries: [IDType, ValueType][] = (await apiMethod.entries()).map(([storageKey, value]) => [
+      storageKey.args[0] as IDType,
       value,
     ])
 
@@ -193,7 +159,7 @@ export default class Api {
   }
 
   protected async blockTimestamp(height: number): Promise<Date> {
-    const blockTime = (await this._api.query.timestamp.now.at(await this.blockHash(height))) as Moment
+    const blockTime = await this._api.query.timestamp.now.at(await this.blockHash(height))
 
     return new Date(blockTime.toNumber())
   }
@@ -204,14 +170,13 @@ export default class Api {
   }
 
   protected async membershipById(memberId: MemberId): Promise<Membership | null> {
-    const profile = (await this._api.query.members.membershipById(memberId)) as Membership
+    const profile = await this._api.query.members.membershipById(memberId)
 
-    // Can't just use profile.isEmpty because profile.suspended is Bool (which isEmpty method always returns false)
-    return profile.handle.isEmpty ? null : profile
+    return profile.isEmpty ? null : profile
   }
 
   async groupLead(group: WorkingGroups): Promise<GroupMember | null> {
-    const optLeadId = (await this.workingGroupApiQuery(group).currentLead()) as Option<WorkerId>
+    const optLeadId = await this.workingGroupApiQuery(group).currentLead()
 
     if (!optLeadId.isSome) {
       return null
@@ -223,26 +188,11 @@ export default class Api {
     return await this.parseGroupMember(leadWorkerId, leadWorker)
   }
 
-  protected async stakeValue(stakeId: StakeId): Promise<Balance> {
-    const stake = await this._api.query.stake.stakes<Stake>(stakeId)
-    return stake.value
-  }
-
-  protected async workerStake(stakeProfile: RoleStakeProfile): Promise<Balance> {
-    return this.stakeValue(stakeProfile.stake_id)
-  }
-
-  protected async workerReward(relationshipId: RewardRelationshipId): Promise<Reward> {
-    const rewardRelationship = await this._api.query.recurringRewards.rewardRelationships<RewardRelationship>(
-      relationshipId
+  protected async fetchStake(account: AccountId | string): Promise<Balance> {
+    return this._api.createType(
+      'Balance',
+      (await this._api.query.balances.locks(account)).reduce((sum, lock) => sum.add(lock.amount), new BN(0))
     )
-
-    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 parseGroupMember(id: WorkerId, worker: Worker): Promise<GroupMember> {
@@ -256,13 +206,13 @@ export default class Api {
     }
 
     let stake: Balance | undefined
-    if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
-      stake = await this.workerStake(worker.role_stake_profile.unwrap())
+    if (worker.staking_account_id.isSome) {
+      stake = await this.fetchStake(worker.staking_account_id.unwrap())
     }
 
-    let reward: Reward | undefined
-    if (worker.reward_relationship && worker.reward_relationship.isSome) {
-      reward = await this.workerReward(worker.reward_relationship.unwrap())
+    const reward: Reward = {
+      valuePerBlock: worker.reward_per_block.unwrapOrDefault(),
+      totalMissed: worker.missed_reward.unwrapOrDefault(),
     }
 
     return {
@@ -311,185 +261,107 @@ export default class Api {
     return this.entriesByIds<WorkerId, Worker>(this.workingGroupApiQuery(group).workerById)
   }
 
-  async openingsByGroup(group: WorkingGroups): Promise<GroupOpening[]> {
-    let openings: GroupOpening[] = []
-    const nextId = await this.workingGroupApiQuery(group).nextOpeningId<OpeningId>()
-
-    // This is chain specfic, but if next id is still 0, it means no openings have been added yet
-    if (!nextId.eq(0)) {
-      const ids = Array.from(Array(nextId.toNumber()).keys()).reverse() // Sort by newest
-      openings = await Promise.all(ids.map((id) => this.groupOpening(group, id)))
-    }
-
-    return openings
-  }
-
-  protected async hiringOpeningById(id: number | OpeningId): Promise<Opening> {
-    const result = await this._api.query.hiring.openingById<Opening>(id)
-    return result
-  }
+  async openingsByGroup(group: WorkingGroups): Promise<OpeningDetails[]> {
+    const openings = await this.entriesByIds<OpeningId, Opening>(this.workingGroupApiQuery(group).openingById)
 
-  protected async hiringApplicationById(id: number | ApplicationId): Promise<Application> {
-    const result = await this._api.query.hiring.applicationById<Application>(id)
-    return result
+    return Promise.all(openings.map(([id, opening]) => this.fetchOpeningDetails(group, opening, id.toNumber())))
   }
 
-  async wgApplicationById(group: WorkingGroups, wgApplicationId: number): Promise<WGApplication> {
+  async applicationById(group: WorkingGroups, applicationId: number): Promise<Application> {
     const nextAppId = await this.workingGroupApiQuery(group).nextApplicationId<ApplicationId>()
 
-    if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
+    if (applicationId < 0 || applicationId >= nextAppId.toNumber()) {
       throw new CLIError('Invalid working group application ID!')
     }
 
-    const result = await this.workingGroupApiQuery(group).applicationById<WGApplication>(wgApplicationId)
-    return result
-  }
+    const result = await this.workingGroupApiQuery(group).applicationById(applicationId)
 
-  protected async parseApplication(wgApplicationId: number, wgApplication: WGApplication): Promise<GroupApplication> {
-    const appId = wgApplication.application_id
-    const application = await this.hiringApplicationById(appId)
+    if (result.isEmpty) {
+      throw new CLIError(`Application of ID=${applicationId} no longer exists!`)
+    }
 
-    const { active_role_staking_id: roleStakingId, active_application_staking_id: appStakingId } = application
+    return result
+  }
 
+  protected async fetchApplicationDetails(
+    applicationId: number,
+    application: Application
+  ): Promise<ApplicationDetails> {
     return {
-      wgApplicationId,
-      applicationId: appId.toNumber(),
-      wgOpeningId: wgApplication.opening_id.toNumber(),
-      member: await this.membershipById(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,
+      applicationId,
+      member: await this.membershipById(application.member_id),
+      roleAccout: application.role_account_id,
+      rewardAccount: application.reward_account_id,
+      stakingAccount: application.staking_account_id.unwrapOr(undefined),
+      descriptionHash: application.description_hash.toString(),
     }
   }
 
-  async groupApplication(group: WorkingGroups, wgApplicationId: number): Promise<GroupApplication> {
-    const wgApplication = await this.wgApplicationById(group, wgApplicationId)
-    return await this.parseApplication(wgApplicationId, wgApplication)
+  async groupApplication(group: WorkingGroups, applicationId: number): Promise<ApplicationDetails> {
+    const application = await this.applicationById(group, applicationId)
+    return await this.fetchApplicationDetails(applicationId, application)
   }
 
-  protected async groupOpeningApplications(group: WorkingGroups, wgOpeningId: number): Promise<GroupApplication[]> {
-    const wgApplicationEntries = await this.entriesByIds<ApplicationId, WGApplication>(
+  protected async groupOpeningApplications(
+    group: WorkingGroups /*, openingId: number */
+  ): Promise<ApplicationDetails[]> {
+    const applicationEntries = await this.entriesByIds<ApplicationId, Application>(
       this.workingGroupApiQuery(group).applicationById
     )
 
     return Promise.all(
-      wgApplicationEntries
-        .filter(([, /* id */ wgApplication]) => wgApplication.opening_id.eqn(wgOpeningId))
-        .map(([id, wgApplication]) => this.parseApplication(id.toNumber(), wgApplication))
+      applicationEntries
+        // TODO: No relation between application and opening yet!
+        // .filter(([, /* id */ wgApplication]) => wgApplication.opening_id.eqn(wgOpeningId))
+        .map(([id, wgApplication]) => this.fetchApplicationDetails(id.toNumber(), wgApplication))
     )
   }
 
-  async groupOpening(group: WorkingGroups, wgOpeningId: number): Promise<GroupOpening> {
-    const nextId = ((await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId).toNumber()
+  async fetchOpening(group: WorkingGroups, openingId: number): Promise<Opening> {
+    const nextId = (await this.workingGroupApiQuery(group).nextOpeningId()).toNumber()
 
-    if (wgOpeningId < 0 || wgOpeningId >= nextId) {
+    if (openingId < 0 || openingId >= nextId) {
       throw new CLIError('Invalid working group opening ID!')
     }
 
-    const groupOpening = await this.workingGroupApiQuery(group).openingById<WGOpening>(wgOpeningId)
-
-    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 { application_staking_policy: applSP, role_staking_policy: roleSP } = opening
-    const stakes = {
-      application: applSP.unwrapOr(undefined),
-      role: roleSP.unwrapOr(undefined),
-    }
+    const opening = await this.workingGroupApiQuery(group).openingById(openingId)
 
-    const unstakingPeriod = (period: Option<BlockNumber>) => period.unwrapOr(new BN(0)).toNumber()
-    const spUnstakingPeriod = (sp: Option<StakingPolicy>, key: StakingPolicyUnstakingPeriodKey) =>
-      sp.isSome ? unstakingPeriod(sp.unwrap()[key]) : 0
-
-    const unstakingPeriods: Partial<UnstakingPeriods> = {
-      'review_period_expired_application_stake_unstaking_period_length': spUnstakingPeriod(
-        applSP,
-        'review_period_expired_unstaking_period_length'
-      ),
-      'crowded_out_application_stake_unstaking_period_length': spUnstakingPeriod(
-        applSP,
-        'crowded_out_unstaking_period_length'
-      ),
-      'review_period_expired_role_stake_unstaking_period_length': spUnstakingPeriod(
-        roleSP,
-        'review_period_expired_unstaking_period_length'
-      ),
-      'crowded_out_role_stake_unstaking_period_length': spUnstakingPeriod(
-        roleSP,
-        'crowded_out_unstaking_period_length'
-      ),
+    if (opening.isEmpty) {
+      throw new CLIError(`Opening of ID=${openingId} no longer exists!`)
     }
 
-    openingPolicyUnstakingPeriodsKeys.forEach((key) => {
-      unstakingPeriods[key] = unstakingPeriod(groupOpening.policy_commitment[key])
-    })
+    return opening
+  }
+
+  async fetchOpeningDetails(group: WorkingGroups, opening: Opening, openingId: number): Promise<OpeningDetails> {
+    const applications = await this.groupOpeningApplications(group)
+    const type = opening.opening_type
+    const stake = opening.stake_policy.isSome
+      ? {
+          unstakingPeriod: opening.stake_policy.unwrap().leaving_unstaking_period.toNumber(),
+          value: opening.stake_policy.unwrap().stake_amount,
+        }
+      : undefined
 
     return {
-      wgOpeningId,
       openingId,
-      opening,
-      stage,
-      stakes,
       applications,
       type,
-      unstakingPeriods: unstakingPeriods as UnstakingPeriods,
+      stake,
+      createdAtBlock: opening.created.toNumber(),
+      rewardPerBlock: opening.reward_per_block.unwrapOr(undefined),
     }
   }
 
-  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 groupOpening(group: WorkingGroups, openingId: number): Promise<OpeningDetails> {
+    const opening = await this.fetchOpening(group, openingId)
+    return this.fetchOpeningDetails(group, opening, openingId)
   }
 
   async getMemberIdsByControllerAccount(address: string): Promise<MemberId[]> {
-    const ids = await this._api.query.members.memberIdsByControllerAccountId<Vec<MemberId>>(address)
-    return ids.toArray()
-  }
-
-  async workerExitRationaleConstraint(group: WorkingGroups): Promise<InputValidationLengthConstraint> {
-    return await this.workingGroupApiQuery(group).workerExitRationaleText<InputValidationLengthConstraint>()
+    // TODO: FIXME: Temporary ugly solution, the account management in CLI needs to be changed
+    const membersEntries = await this.entriesByIds(this._api.query.members.membershipById)
+    return membersEntries.filter(([, m]) => m.controller_account.eq(address)).map(([id]) => id)
   }
 
   // Content directory
@@ -505,15 +377,15 @@ export default class Api {
 
   async curatorGroupById(id: number): Promise<CuratorGroup | null> {
     const exists = !!(await this._api.query.contentDirectory.curatorGroupById.size(id)).toNumber()
-    return exists ? await this._api.query.contentDirectory.curatorGroupById<CuratorGroup>(id) : null
+    return exists ? await this._api.query.contentDirectory.curatorGroupById(id) : null
   }
 
   async nextCuratorGroupId(): Promise<number> {
-    return (await this._api.query.contentDirectory.nextCuratorGroupId<CuratorGroupId>()).toNumber()
+    return (await this._api.query.contentDirectory.nextCuratorGroupId()).toNumber()
   }
 
   async classById(id: number): Promise<Class | null> {
-    const c = await this._api.query.contentDirectory.classById<Class>(id)
+    const c = await this._api.query.contentDirectory.classById(id)
     return c.isEmpty ? null : c
   }
 
@@ -524,25 +396,23 @@ export default class Api {
 
   async entityById(id: number): Promise<Entity | null> {
     const exists = !!(await this._api.query.contentDirectory.entityById.size(id)).toNumber()
-    return exists ? await this._api.query.contentDirectory.entityById<Entity>(id) : null
+    return exists ? await this._api.query.contentDirectory.entityById(id) : null
   }
 
   async dataObjectByContentId(contentId: ContentId): Promise<DataObject | null> {
-    const dataObject = await this._api.query.dataDirectory.dataObjectByContentId<Option<DataObject>>(contentId)
+    const dataObject = await this._api.query.dataDirectory.dataObjectByContentId(contentId)
     return dataObject.unwrapOr(null)
   }
 
   async ipnsIdentity(storageProviderId: number): Promise<string | null> {
-    const accountInfo = await this._api.query.discovery.accountInfoByStorageProviderId<ServiceProviderRecord>(
-      storageProviderId
-    )
+    const accountInfo = await this._api.query.discovery.accountInfoByStorageProviderId(storageProviderId)
     return accountInfo.isEmpty || accountInfo.expires_at.toNumber() <= (await this.bestNumber())
       ? null
       : accountInfo.identity.toString()
   }
 
   async getRandomBootstrapEndpoint(): Promise<string | null> {
-    const endpoints = await this._api.query.discovery.bootstrapEndpoints<Vec<Url>>()
+    const endpoints = await this._api.query.discovery.bootstrapEndpoints()
     const randomEndpoint = _.sample(endpoints.toArray())
     return randomEndpoint ? randomEndpoint.toString() : null
   }

+ 30 - 113
cli/src/Types.ts

@@ -1,14 +1,13 @@
-import BN from 'bn.js'
-import { ElectionStage, Seat } from '@joystream/types/council'
-import { Option } from '@polkadot/types'
 import { Codec } from '@polkadot/types/types'
-import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces'
+import { Balance, AccountId } from '@polkadot/types/interfaces'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { KeyringPair } from '@polkadot/keyring/types'
 import { WorkerId, OpeningType } from '@joystream/types/working-group'
-import { Membership, MemberId } from '@joystream/types/members'
-import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring'
+import { Membership } from '@joystream/types/members'
+import { MemberId } from '@joystream/types/common'
 import { Validator } from 'inquirer'
+import { ApiPromise } from '@polkadot/api'
+import { SubmittableModuleExtrinsics, QueryableModuleStorage, QueryableModuleConsts } from '@polkadot/api/types'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -24,44 +23,6 @@ export type AccountSummary = {
   balances: DeriveBalancesAll
 }
 
-// This function allows us to easily transform the tuple into the object
-// 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>
-) {
-  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
@@ -71,19 +32,21 @@ export type NameValueObj = { name: string; value: string }
 export enum WorkingGroups {
   StorageProviders = 'storageProviders',
   Curators = 'curators',
+  Forum = 'forum',
+  Membership = 'membership',
 }
 
 // In contrast to Pioneer, currently only StorageProviders group is available in CLI
 export const AvailableGroups: readonly WorkingGroups[] = [
   WorkingGroups.StorageProviders,
   WorkingGroups.Curators,
+  WorkingGroups.Forum,
+  WorkingGroups.Membership,
 ] as const
 
 export type Reward = {
-  totalRecieved: Balance
-  value: Balance
-  interval?: number
-  nextPaymentBlock: number // 0 = no incoming payment
+  totalMissed: Balance
+  valuePerBlock: Balance
 }
 
 // Compound working group types
@@ -96,78 +59,25 @@ export type GroupMember = {
   reward?: Reward
 }
 
-export type GroupApplication = {
-  wgApplicationId: number
+export type ApplicationDetails = {
   applicationId: number
-  wgOpeningId: number
   member: Membership | 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',
+  stakingAccount?: AccountId
+  rewardAccount: AccountId
+  descriptionHash: string
 }
 
-export type GroupOpeningStage = {
-  status: OpeningStatus
-  block?: number
-  date?: Date
-}
-
-export type GroupOpeningStakes = {
-  application?: StakingPolicy
-  role?: StakingPolicy
-}
-
-export const stakingPolicyUnstakingPeriodKeys = [
-  'crowded_out_unstaking_period_length',
-  'review_period_expired_unstaking_period_length',
-] as const
-
-export type StakingPolicyUnstakingPeriodKey = typeof stakingPolicyUnstakingPeriodKeys[number]
-
-export const openingPolicyUnstakingPeriodsKeys = [
-  'fill_opening_failed_applicant_application_stake_unstaking_period',
-  'fill_opening_failed_applicant_role_stake_unstaking_period',
-  'fill_opening_successful_applicant_application_stake_unstaking_period',
-  'terminate_application_stake_unstaking_period',
-  'terminate_role_stake_unstaking_period',
-  'exit_role_application_stake_unstaking_period',
-  'exit_role_stake_unstaking_period',
-] as const
-
-export type OpeningPolicyUnstakingPeriodsKey = typeof openingPolicyUnstakingPeriodsKeys[number]
-export type UnstakingPeriodsKey =
-  | OpeningPolicyUnstakingPeriodsKey
-  | 'crowded_out_application_stake_unstaking_period_length'
-  | 'crowded_out_role_stake_unstaking_period_length'
-  | 'review_period_expired_application_stake_unstaking_period_length'
-  | 'review_period_expired_role_stake_unstaking_period_length'
-
-export type UnstakingPeriods = {
-  [k in UnstakingPeriodsKey]: number
-}
-
-export type GroupOpening = {
-  wgOpeningId: number
+export type OpeningDetails = {
   openingId: number
-  stage: GroupOpeningStage
-  opening: Opening
-  stakes: GroupOpeningStakes
-  applications: GroupApplication[]
+  stake?: {
+    value: Balance
+    unstakingPeriod: number
+  }
+  applications: ApplicationDetails[]
   type: OpeningType
-  unstakingPeriods: UnstakingPeriods
+  createdAtBlock: number
+  rewardPerBlock?: Balance
 }
 
 // Api-related
@@ -193,3 +103,10 @@ export type ApiMethodNamedArg = {
   value: ApiMethodArg
 }
 export type ApiMethodNamedArgs = ApiMethodNamedArg[]
+
+// Api without TypeScript augmentations for "query", "tx" and "consts" (useful when more type flexibility is needed)
+export type UnaugmentedApiPromise = Omit<ApiPromise, 'query' | 'tx' | 'consts'> & {
+  query: { [key: string]: QueryableModuleStorage<'promise'> }
+  tx: { [key: string]: SubmittableModuleExtrinsics<'promise'> }
+  consts: { [key: string]: QueryableModuleConsts }
+}

+ 11 - 7
cli/src/base/ApiCommandBase.ts

@@ -6,7 +6,7 @@ import { getTypeDef, Option, Tuple, TypeRegistry } from '@polkadot/types'
 import { Registry, Codec, CodecArg, TypeDef, TypeDefInfo } from '@polkadot/types/types'
 
 import { Vec, Struct, Enum } from '@polkadot/types/codec'
-import { ApiPromise, WsProvider } from '@polkadot/api'
+import { WsProvider } from '@polkadot/api'
 import { KeyringPair } from '@polkadot/keyring/types'
 import chalk from 'chalk'
 import { InterfaceTypes } from '@polkadot/types/types/registry'
@@ -31,11 +31,15 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return this.api
   }
 
-  // Get original api for lower-level api calls
-  getOriginalApi(): ApiPromise {
+  // Shortcuts
+  getOriginalApi() {
     return this.getApi().getOriginalApi()
   }
 
+  getUnaugmentedApi() {
+    return this.getApi().getUnaugmentedApi()
+  }
+
   getTypesRegistry(): Registry {
     return this.getOriginalApi().registry
   }
@@ -332,7 +336,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     method: string,
     paramsOptions?: ApiParamsOptions
   ): Promise<ApiMethodArg[]> {
-    const extrinsicMethod = this.getOriginalApi().tx[module][method]
+    const extrinsicMethod = (await this.getUnaugmentedApi().tx)[module][method]
     const values: ApiMethodArg[] = []
 
     this.openIndentGroup()
@@ -350,7 +354,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return values
   }
 
-  sendExtrinsic(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>) {
+  sendExtrinsic(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<void> {
     return new Promise((resolve, reject) => {
       let unsubscribe: () => void
       tx.signAndSend(account, {}, (result) => {
@@ -425,7 +429,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     warnOnly = false
   ): Promise<boolean> {
     this.log(chalk.white(`\nSending ${module}.${method} extrinsic...`))
-    const tx = await this.getOriginalApi().tx[module][method](...params)
+    const tx = await this.getUnaugmentedApi().tx[module][method](...params)
     return await this.sendAndFollowTx(account, tx, warnOnly)
   }
 
@@ -445,7 +449,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
   extrinsicArgsFromDraft(module: string, method: string, draftFilePath: string): ApiMethodNamedArgs {
     let draftJSONObj
     const parsedArgs: ApiMethodNamedArgs = []
-    const extrinsicMethod = this.getOriginalApi().tx[module][method]
+    const extrinsicMethod = this.getUnaugmentedApi().tx[module][method]
     try {
       // eslint-disable-next-line @typescript-eslint/no-var-requires
       draftJSONObj = require(draftFilePath)

+ 1 - 1
cli/src/base/ContentDirectoryCommandBase.ts

@@ -157,7 +157,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     const choices = curators
       .filter((c) => (ids ? ids.includes(c.workerId.toNumber()) : true))
       .map((c) => ({
-        name: `${c.profile.handle.toString()} (Worker ID: ${c.workerId})`,
+        name: `${c.profile.handle_hash.toString()} (Worker ID: ${c.workerId})`,
         value: c.workerId.toNumber(),
       }))
 

+ 15 - 33
cli/src/base/WorkingGroupsCommandBase.ts

@@ -6,12 +6,10 @@ import {
   AvailableGroups,
   NamedKeyringPair,
   GroupMember,
-  GroupOpening,
-  OpeningStatus,
-  GroupApplication,
+  OpeningDetails,
+  ApplicationDetails,
 } from '../Types'
 import _ from 'lodash'
-import { ApplicationStageKeys } from '@joystream/types/hiring'
 import chalk from 'chalk'
 import { IConfig } from '@oclif/config'
 
@@ -113,33 +111,24 @@ export default abstract class WorkingGroupsCommandBase extends RolesCommandBase
     }),
   }
 
-  async promptForApplicationsToAccept(opening: GroupOpening): Promise<number[]> {
-    const acceptableApplications = opening.applications.filter((a) => a.stage === ApplicationStageKeys.Active)
+  async promptForApplicationsToAccept(opening: OpeningDetails): Promise<number[]> {
     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,
+      choices: opening.applications.map((a) => ({
+        name: ` ${a.applicationId}: ${a.member?.handle_hash.toString()}`,
+        value: a.applicationId,
       })),
     })
 
     return acceptedApplications
   }
 
-  async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
+  async getOpeningForLeadAction(id: number): Promise<OpeningDetails> {
     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 })
-    }
-
-    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 }
-      )
+    if (!opening.type.isOfType('Regular')) {
+      this.error('A lead can only manage Regular openings!', { exit: ExitCodes.AccessDenied })
     }
 
     return opening
@@ -148,21 +137,14 @@ export default abstract class WorkingGroupsCommandBase extends RolesCommandBase
   // 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> {
+  async getApplicationForLeadAction(id: number): Promise<ApplicationDetails> {
     const application = await this.getApi().groupApplication(this.group, id)
-    const opening = await this.getApi().groupOpening(this.group, application.wgOpeningId)
+    // TODO: Add once opening-application connection is established
+    // 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 }
-      )
-    }
+    // if (!opening.type.isOfType('Worker')) {
+    //   this.error('A lead can only manage Worker opening applications!', { exit: ExitCodes.AccessDenied })
+    // }
 
     return application
   }

+ 8 - 9
cli/src/commands/api/inspect.ts

@@ -1,13 +1,12 @@
 import { flags } from '@oclif/command'
 import { CLIError } from '@oclif/errors'
 import { displayNameValueTable } from '../../helpers/display'
-import { ApiPromise } from '@polkadot/api'
 import { Codec } from '@polkadot/types/types'
-import { ConstantCodec } from '@polkadot/metadata/Decorated/consts/types'
 import ExitCodes from '../../ExitCodes'
 import chalk from 'chalk'
-import { NameValueObj, ApiMethodArg } from '../../Types'
+import { NameValueObj, ApiMethodArg, UnaugmentedApiPromise } from '../../Types'
 import ApiCommandBase from '../../base/ApiCommandBase'
+import { AugmentedConst } from '@polkadot/api/types'
 
 // Command flags type
 type ApiInspectFlags = {
@@ -78,10 +77,10 @@ export default class ApiInspect extends ApiCommandBase {
 
   getMethodMeta(apiType: ApiType, apiModule: string, apiMethod: string) {
     if (apiType === 'query') {
-      return this.getOriginalApi().query[apiModule][apiMethod].creator.meta
+      return this.getUnaugmentedApi().query[apiModule][apiMethod].creator.meta
     } else {
       // Currently the only other optoin is api.consts
-      const method: ConstantCodec = this.getOriginalApi().consts[apiModule][apiMethod] as ConstantCodec
+      const method = (this.getUnaugmentedApi().consts[apiModule][apiMethod] as unknown) as AugmentedConst<'promise'>
       return method.meta
     }
   }
@@ -92,7 +91,7 @@ export default class ApiInspect extends ApiCommandBase {
   }
 
   getQueryMethodParamsTypes(apiModule: string, apiMethod: string): string[] {
-    const method = this.getOriginalApi().query[apiModule][apiMethod]
+    const method = this.getUnaugmentedApi().query[apiModule][apiMethod]
     const { type } = method.creator.meta
     if (type.isDoubleMap) {
       return [type.asDoubleMap.key1.toString(), type.asDoubleMap.key2.toString()]
@@ -105,7 +104,7 @@ export default class ApiInspect extends ApiCommandBase {
 
   getMethodReturnType(apiType: ApiType, apiModule: string, apiMethod: string): string {
     if (apiType === 'query') {
-      const method = this.getOriginalApi().query[apiModule][apiMethod]
+      const method = this.getUnaugmentedApi().query[apiModule][apiMethod]
       const {
         meta: { type, modifier },
       } = method.creator
@@ -126,7 +125,7 @@ export default class ApiInspect extends ApiCommandBase {
   // 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,
+    api: UnaugmentedApiPromise,
     flags: ApiInspectFlags
   ): { apiType: ApiType | undefined; apiModule: string | undefined; apiMethod: string | undefined } {
     let apiType: ApiType | undefined
@@ -164,7 +163,7 @@ export default class ApiInspect extends ApiCommandBase {
   }
 
   async run() {
-    const api: ApiPromise = this.getOriginalApi()
+    const api: UnaugmentedApiPromise = this.getUnaugmentedApi()
     const flags: ApiInspectFlags = this.parse(ApiInspect).flags as ApiInspectFlags
     const availableTypes: readonly string[] = TYPES_AVAILABLE
     const { apiType, apiModule, apiMethod } = this.validateFlags(api, flags)

+ 2 - 2
cli/src/commands/content-directory/createCuratorGroup.ts

@@ -12,7 +12,7 @@ export default class AddCuratorGroupCommand extends ContentDirectoryCommandBase
     await this.requestAccountDecoding(account)
     await this.buildAndSendExtrinsic(account, 'contentDirectory', 'addCuratorGroup')
 
-    const newGroupId = (await this.getApi().nextCuratorGroupId()) - 1
-    console.log(chalk.green(`New group succesfully created! (ID: ${chalk.white(newGroupId)})`))
+    // TODO: Get id from event?
+    console.log(chalk.green(`New group succesfully created!`))
   }
 }

+ 3 - 1
cli/src/commands/content-directory/curatorGroup.ts

@@ -32,7 +32,9 @@ export default class CuratorGroupCommand extends ContentDirectoryCommandBase {
     displayHeader(`Group Members (${members.length})`)
     this.log(
       members
-        .map((curator) => chalk.white(`${curator.profile.handle} (WorkerID: ${curator.workerId.toString()})`))
+        .map((curator) =>
+          chalk.white(`${curator.profile.handle_hash.toString()} (WorkerID: ${curator.workerId.toString()})`)
+        )
         .join(', ')
     )
   }

+ 0 - 56
cli/src/commands/council/info.ts

@@ -1,56 +0,0 @@
-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'
-
-  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('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)
-
-    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)
-  }
-}

+ 4 - 9
cli/src/commands/working-groups/application.ts

@@ -21,19 +21,14 @@ export default class WorkingGroupsApplication extends WorkingGroupsCommandBase {
 
     const application = await this.getApi().groupApplication(this.group, parseInt(args.wgApplicationId))
 
-    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'),
+      'Member handle': application.member?.handle_hash.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),
+      'Reward account': application.rewardAccount.toString(),
+      'Staking account': application.stakingAccount?.toString() || 'NONE',
+      'Description': application.descriptionHash.toString(),
     }
     displayCollapsedRow(applicationRow)
   }

+ 27 - 37
cli/src/commands/working-groups/createOpening.ts

@@ -6,14 +6,14 @@ import HRTSchema from '@joystream/types/hiring/schemas/role.schema.json'
 import { GenericJoyStreamRoleSchema as HRTJson } from '@joystream/types/hiring/schemas/role.schema.typings'
 import { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
-import WGOpeningSchema from '../../json-schemas/WorkingGroupOpening.schema.json'
-import { WorkingGroupOpening as WGOpeningJson } from '../../json-schemas/typings/WorkingGroupOpening.schema'
+import OpeningParamsSchema from '../../json-schemas/WorkingGroupOpening.schema.json'
+import { WorkingGroupOpening as OpeningParamsJson } from '../../json-schemas/typings/WorkingGroupOpening.schema'
 import _ from 'lodash'
 import { IOFlags, getInputJson, ensureOutputFileIsWriteable, saveOutputJsonToFile } from '../../helpers/InputOutput'
 import Ajv from 'ajv'
 import ExitCodes from '../../ExitCodes'
 import { flags } from '@oclif/command'
-import { createType } from '@joystream/types'
+import { AugmentedSubmittables } from '@polkadot/api/types'
 
 export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase {
   static description = 'Create working group opening (requires lead access)'
@@ -76,46 +76,36 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
     }
   }
 
-  createTxParams(wgOpeningJson: WGOpeningJson, hrtJson: HRTJson) {
+  createTxParams(
+    openingParamsJson: OpeningParamsJson,
+    hrtJson: HRTJson
+  ): Parameters<AugmentedSubmittables<'promise'>['membershipWorkingGroup']['addOpening']> {
     return [
-      wgOpeningJson.activateAt,
-      createType('WorkingGroupOpeningPolicyCommitment', {
-        max_review_period_length: wgOpeningJson.maxReviewPeriodLength,
-        application_rationing_policy: wgOpeningJson.maxActiveApplicants
-          ? { max_active_applicants: wgOpeningJson.maxActiveApplicants }
-          : null,
-        application_staking_policy: wgOpeningJson.applicationStake
-          ? {
-              amount: wgOpeningJson.applicationStake.value,
-              amount_mode: wgOpeningJson.applicationStake.mode,
-            }
-          : null,
-        role_staking_policy: wgOpeningJson.roleStake
-          ? {
-              amount: wgOpeningJson.roleStake.value,
-              amount_mode: wgOpeningJson.roleStake.mode,
-            }
-          : null,
-        terminate_role_stake_unstaking_period: wgOpeningJson.terminateRoleUnstakingPeriod,
-        exit_role_stake_unstaking_period: wgOpeningJson.leaveRoleUnstakingPeriod,
-      }),
       JSON.stringify(hrtJson),
-      createType('OpeningType', 'Worker'),
+      'Regular',
+      openingParamsJson.stakingPolicy
+        ? {
+            stake_amount: openingParamsJson.stakingPolicy.amount,
+            leaving_unstaking_period: openingParamsJson.stakingPolicy.unstakingPeriod,
+          }
+        : null,
+      // TODO: Proper bigint handling?
+      openingParamsJson.rewardPerBlock?.toString() || null,
     ]
   }
 
   async promptForData(
     lead: GroupMember,
-    rememberedInput?: [WGOpeningJson, HRTJson]
-  ): Promise<[WGOpeningJson, HRTJson]> {
+    rememberedInput?: [OpeningParamsJson, HRTJson]
+  ): Promise<[OpeningParamsJson, HRTJson]> {
     const openingDefaults = rememberedInput?.[0]
-    const openingPrompt = new JsonSchemaPrompter<WGOpeningJson>(
-      (WGOpeningSchema as unknown) as JSONSchema,
+    const openingPrompt = new JsonSchemaPrompter<OpeningParamsJson>(
+      (OpeningParamsSchema as unknown) as JSONSchema,
       openingDefaults
     )
-    const wgOpeningJson = await openingPrompt.promptAll()
+    const openingParamsJson = await openingPrompt.promptAll()
 
-    const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults(lead.profile.handle.toString())
+    const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults(lead.profile.handle_hash.toString())
     this.log(`Values for ${chalk.greenBright('human_readable_text')} json:`)
     const hrtPropmpt = new JsonSchemaPrompter<HRTJson>((HRTSchema as unknown) as JSONSchema, hrtDefaults)
     // Prompt only for 'headline', 'job', 'application', 'reward' and 'process', leave the rest default
@@ -131,17 +121,17 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
 
     const hrtJson = { ...hrtDefaults, job, headline, application, reward, process }
 
-    return [wgOpeningJson, hrtJson]
+    return [openingParamsJson, hrtJson]
   }
 
-  async getInputFromFile(filePath: string): Promise<[WGOpeningJson, HRTJson]> {
+  async getInputFromFile(filePath: string): Promise<[OpeningParamsJson, HRTJson]> {
     const ajv = new Ajv({ allErrors: true })
-    const inputParams = await getInputJson<[WGOpeningJson, HRTJson]>(filePath)
+    const inputParams = await getInputJson<[OpeningParamsJson, HRTJson]>(filePath)
     if (!Array.isArray(inputParams) || inputParams.length !== 2) {
       this.error('Invalid input file', { exit: ExitCodes.InvalidInput })
     }
     const [openingJson, hrtJson] = inputParams
-    if (!ajv.validate(WGOpeningSchema, openingJson)) {
+    if (!ajv.validate(OpeningParamsSchema, openingJson)) {
       this.error(`Invalid input file:\n${ajv.errorsText(undefined, { dataVar: 'openingJson', separator: '\n' })}`, {
         exit: ExitCodes.InvalidInput,
       })
@@ -168,7 +158,7 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
     ensureOutputFileIsWriteable(output)
 
     let tryAgain = false
-    let rememberedInput: [WGOpeningJson, HRTJson] | undefined
+    let rememberedInput: [OpeningParamsJson, HRTJson] | undefined
     do {
       if (edit) {
         rememberedInput = await this.getInputFromFile(input as string)

+ 18 - 13
cli/src/commands/working-groups/decreaseWorkerStake.ts

@@ -1,10 +1,10 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-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'
+import { isValidBalance } from '../../helpers/validation'
+import ExitCodes from '../../ExitCodes'
+import BN from 'bn.js'
 
 export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsCommandBase {
   static description =
@@ -17,6 +17,11 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
       required: true,
       description: 'Worker ID',
     },
+    {
+      name: 'amount',
+      required: true,
+      description: 'Amount of JOY to decrease the current worker stake by',
+    },
   ]
 
   static flags = {
@@ -24,29 +29,29 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
   }
 
   async run() {
-    const { args } = this.parse(WorkingGroupsDecreaseWorkerStake)
+    const {
+      args: { workerId, amount },
+    } = 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)
+    const groupMember = await this.getWorkerWithStakeForLeadAction(parseInt(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
+
+    if (!isValidBalance(amount) || groupMember.stake.lt(new BN(amount))) {
+      this.error('Invalid amount', { exit: ExitCodes.InvalidInput })
+    }
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'decreaseStake', [workerId, balance])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'decreaseStake', [workerId, amount])
 
     this.log(
       chalk.green(
-        `${chalk.white(formatBalance(balance))} from worker ${chalk.white(workerId)} stake ` +
+        `${chalk.white(formatBalance(amount))} from worker ${chalk.white(workerId)} stake ` +
           `has been returned to worker's role account (${chalk.white(groupMember.roleAccount.toString())})!`
       )
     )

+ 28 - 16
cli/src/commands/working-groups/evictWorker.ts

@@ -2,7 +2,10 @@ import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
 import { formatBalance } from '@polkadot/util'
 import chalk from 'chalk'
-import { createParamOptions } from '../../helpers/promptOptions'
+import { flags } from '@oclif/command'
+import { isValidBalance } from '../../helpers/validation'
+import ExitCodes from '../../ExitCodes'
+import BN from 'bn.js'
 
 export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
   static description = 'Evicts given worker. Requires lead access.'
@@ -16,10 +19,21 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
 
   static flags = {
     ...WorkingGroupsCommandBase.flags,
+    penalty: flags.string({
+      description: 'Optional penalty in JOY',
+      required: false,
+    }),
+    rationale: flags.string({
+      description: 'Optional rationale',
+      required: false,
+    }),
   }
 
   async run() {
-    const { args } = this.parse(WorkingGroupsEvictWorker)
+    const {
+      args,
+      flags: { penalty, rationale },
+    } = this.parse(WorkingGroupsEvictWorker)
 
     const account = await this.getRequiredSelectedAccount()
     // Lead-only gate
@@ -29,27 +43,25 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
     // 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
+    if (penalty && !isValidBalance(penalty)) {
+      this.error('Invalid penalty amount', { exit: ExitCodes.InvalidInput })
+    }
+
+    if (penalty && (!groupMember.stake || groupMember.stake.lt(new BN(penalty)))) {
+      this.error('Penalty cannot exceed worker stake', { exit: ExitCodes.InvalidInput })
+    }
 
     await this.requestAccountDecoding(account)
 
     await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'terminateRole', [
       workerId,
-      rationale,
-      shouldSlash,
+      penalty || null,
+      rationale || null,
     ])
 
-    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!`))
+    this.log(chalk.green(`Worker ${chalk.white(workerId.toString())} has been evicted!`))
+    if (penalty) {
+      this.log(chalk.green(`${chalk.white(formatBalance(penalty))} of worker's stake has been slashed!`))
     }
   }
 }

+ 3 - 10
cli/src/commands/working-groups/fillOpening.ts

@@ -1,8 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { OpeningStatus } from '../../Types'
 import { apiModuleByGroup } from '../../Api'
 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."
@@ -26,20 +24,15 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
     await this.getRequiredLead()
 
     const openingId = parseInt(args.wgOpeningId)
-    const opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview)
+    const opening = await this.getOpeningForLeadAction(openingId)
 
     const applicationIds = await this.promptForApplicationsToAccept(opening)
-    const rewardPolicyOpt = await this.promptForParam(`Option<RewardPolicy>`, createParamOptions('RewardPolicy'))
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'fillOpening', [
-      openingId,
-      applicationIds,
-      rewardPolicyOpt,
-    ])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'fillOpening', [openingId, applicationIds])
 
-    this.log(chalk.green(`Opening ${chalk.white(openingId)} succesfully filled!`))
+    this.log(chalk.green(`Opening ${chalk.white(openingId.toString())} succesfully filled!`))
     this.log(
       chalk.green('Accepted working group application IDs: ') +
         chalk.white(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE')

+ 20 - 12
cli/src/commands/working-groups/increaseStake.ts

@@ -1,14 +1,20 @@
 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'
+import { isValidBalance } from '../../helpers/validation'
 
 export default class WorkingGroupsIncreaseStake extends WorkingGroupsCommandBase {
   static description = 'Increases current role (lead/worker) stake. Requires active role account to be selected.'
+  static args = [
+    {
+      name: 'amount',
+      required: true,
+      description: 'Amount of JOY to increase the current stake by',
+    },
+  ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -18,24 +24,26 @@ export default class WorkingGroupsIncreaseStake extends WorkingGroupsCommandBase
     // Worker-only gate
     const worker = await this.getRequiredWorker()
 
+    const {
+      args: { amount },
+    } = this.parse(WorkingGroupsIncreaseStake)
+
+    if (!isValidBalance(amount)) {
+      this.error('Invalid stake amount!', { 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
-
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'increaseStake', [worker.workerId, balance])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'increaseStake', [worker.workerId, amount])
 
     this.log(
       chalk.green(
-        `Worker ${chalk.white(worker.workerId.toNumber())} stake has been increased by ${chalk.white(
-          formatBalance(balance)
+        `Worker ${chalk.white(worker.workerId.toString())} stake has been increased by ${chalk.white(
+          formatBalance(amount)
         )}`
       )
     )

+ 13 - 7
cli/src/commands/working-groups/leaveRole.ts

@@ -1,13 +1,16 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-import { minMaxStr } from '../../validators/common'
 import chalk from 'chalk'
-import { createParamOptions } from '../../helpers/promptOptions'
+import { flags } from '@oclif/command'
 
 export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
   static description = 'Leave the worker or lead role associated with currently selected account.'
   static flags = {
     ...WorkingGroupsCommandBase.flags,
+    rationale: flags.string({
+      name: 'Optional rationale',
+      required: false,
+    }),
   }
 
   async run() {
@@ -15,14 +18,17 @@ export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
     // 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 {
+      flags: { rationale },
+    } = this.parse(WorkingGroupsLeaveRole)
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'leaveRole', [worker.workerId, rationale])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'leaveRole', [
+      worker.workerId,
+      rationale || null,
+    ])
 
-    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.toString())})`))
   }
 }

+ 19 - 51
cli/src/commands/working-groups/opening.ts

@@ -1,8 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { displayTable, displayCollapsedRow, displayHeader } from '../../helpers/display'
-import _ from 'lodash'
-import { OpeningStatus, GroupOpeningStage, GroupOpeningStakes, UnstakingPeriodsKey } from '../../Types'
-import { StakingAmountLimitModeKeys, StakingPolicy } from '@joystream/types/hiring'
+import { displayTable, displayCollapsedRow, displayHeader, shortAddress } from '../../helpers/display'
 import { formatBalance } from '@polkadot/util'
 import chalk from 'chalk'
 
@@ -20,69 +17,40 @@ export default class WorkingGroupsOpening extends WorkingGroupsCommandBase {
     ...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}`) || '?',
-    }
-  }
-
-  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),
-    }
-  }
-
   async run() {
     const { args } = this.parse(WorkingGroupsOpening)
 
     const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId))
 
-    displayHeader('Human readable text')
-    this.jsonPrettyPrint(opening.opening.human_readable_text.toString())
+    // TODO: Opening desc?
 
     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),
+      'Opening type': opening.type.type,
+      'Created': `#${opening.createdAtBlock}`,
+      'Reward per block': formatBalance(opening.rewardPerBlock),
     }
     displayCollapsedRow(openingRow)
 
-    displayHeader('Unstaking periods')
-    const periodsRow: { [k: string]: string } = {}
-    for (const key of Object.keys(opening.unstakingPeriods).sort()) {
-      const displayKey = _.startCase(key) + ':  '
-      periodsRow[displayKey] = opening.unstakingPeriods[key as UnstakingPeriodsKey].toLocaleString() + ' blocks'
+    displayHeader('Staking policy')
+    if (opening.stake) {
+      const stakingRow = {
+        'Stake amount': formatBalance(opening.stake.value),
+        'Unstaking period': formatBalance(opening.stake.unstakingPeriod),
+      }
+      displayCollapsedRow(stakingRow)
+    } else {
+      this.log('NONE')
     }
-    displayCollapsedRow(periodsRow)
 
     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),
+      'ID': a.applicationId,
+      Member: a.member?.handle_hash.toString() || chalk.red('NONE'),
+      'Role Acc': shortAddress(a.roleAccout),
+      'Reward Acc': shortAddress(a.rewardAccount),
+      'Staking Acc': a.stakingAccount ? shortAddress(a.stakingAccount) : 'NONE',
     }))
     displayTable(applicationsRows, 5)
   }

+ 1 - 3
cli/src/commands/working-groups/openings.ts

@@ -1,6 +1,5 @@
 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'
@@ -12,9 +11,8 @@ export default class WorkingGroupsOpenings extends WorkingGroupsCommandBase {
     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)

+ 4 - 3
cli/src/commands/working-groups/overview.ts

@@ -18,7 +18,7 @@ export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
     if (lead) {
       displayNameValueTable([
         { name: 'Member id:', value: lead.memberId.toString() },
-        { name: 'Member handle:', value: lead.profile.handle.toString() },
+        { name: 'Member handle:', value: lead.profile.handle_hash.toString() },
         { name: 'Role account:', value: lead.roleAccount.toString() },
       ])
     } else {
@@ -31,9 +31,10 @@ export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
     const membersRows = members.map((m) => ({
       'Worker id': m.workerId.toString(),
       'Member id': m.memberId.toString(),
-      'Member handle': m.profile.handle.toString(),
+      'Member handle': m.profile.handle_hash.toString(),
       Stake: formatBalance(m.stake),
-      Earned: formatBalance(m.reward?.totalRecieved),
+      'Reward': formatBalance(m.reward?.valuePerBlock),
+      'Missed reward': formatBalance(m.reward?.totalMissed),
       'Role account': shortAddress(m.roleAccount),
       '':
         (lead?.workerId.eq(m.workerId) ? '\u{2B50}' : '  ') +

+ 29 - 14
cli/src/commands/working-groups/slashWorker.ts

@@ -1,10 +1,11 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-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'
+import { flags } from '@oclif/command'
+import { isValidBalance } from '../../helpers/validation'
+import ExitCodes from '../../ExitCodes'
+import BN from 'bn.js'
 
 export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
   static description = 'Slashes given worker stake. Requires lead access.'
@@ -14,37 +15,51 @@ export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
       required: true,
       description: 'Worker ID',
     },
+    {
+      name: 'amount',
+      required: true,
+      description: 'Slash amount',
+    },
   ]
 
   static flags = {
     ...WorkingGroupsCommandBase.flags,
+    rationale: flags.string({
+      name: 'Optional rationale',
+      required: false,
+    }),
   }
 
   async run() {
-    const { args } = this.parse(WorkingGroupsSlashWorker)
+    const {
+      args: { amount, workerId },
+      flags: { rationale },
+    } = 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)
+    const groupMember = await this.getWorkerWithStakeForLeadAction(parseInt(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
+
+    if (!isValidBalance(amount) || groupMember.stake.lt(new BN(amount))) {
+      this.error('Invalid slash amount', { exit: ExitCodes.InvalidInput })
+    }
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'slashStake', [workerId, balance])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'slashStake', [
+      workerId,
+      amount,
+      rationale || null,
+    ])
 
     this.log(
       chalk.green(
-        `${chalk.white(formatBalance(balance))} from worker ${chalk.white(
-          workerId
+        `${chalk.white(formatBalance(amount))} from worker ${chalk.white(
+          workerId.toString()
         )} stake has been succesfully slashed!`
       )
     )

+ 0 - 38
cli/src/commands/working-groups/startAcceptingApplications.ts

@@ -1,38 +0,0 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { OpeningStatus } from '../../Types'
-import { apiModuleByGroup } from '../../Api'
-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,
-  }
-
-  async run() {
-    const { args } = this.parse(WorkingGroupsStartAcceptingApplications)
-
-    const account = await this.getRequiredSelectedAccount()
-    // Lead-only gate
-    await this.getRequiredLead()
-
-    const openingId = parseInt(args.wgOpeningId)
-    await this.validateOpeningForLeadAction(openingId, OpeningStatus.WaitingToBegin)
-
-    await this.requestAccountDecoding(account)
-
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'acceptApplications', [openingId])
-
-    this.log(
-      chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('Accepting Applications')}`)
-    )
-  }
-}

+ 0 - 36
cli/src/commands/working-groups/startReviewPeriod.ts

@@ -1,36 +0,0 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { OpeningStatus } from '../../Types'
-import { apiModuleByGroup } from '../../Api'
-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,
-  }
-
-  async run() {
-    const { args } = this.parse(WorkingGroupsStartReviewPeriod)
-
-    const account = await this.getRequiredSelectedAccount()
-    // Lead-only gate
-    await this.getRequiredLead()
-
-    const openingId = parseInt(args.wgOpeningId)
-    await this.validateOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications)
-
-    await this.requestAccountDecoding(account)
-
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId])
-
-    this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('In Review')}`))
-  }
-}

+ 0 - 37
cli/src/commands/working-groups/terminateApplication.ts

@@ -1,37 +0,0 @@
-import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { apiModuleByGroup } from '../../Api'
-import { ApplicationStageKeys } 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,
-  }
-
-  async run() {
-    const { args } = this.parse(WorkingGroupsTerminateApplication)
-
-    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)
-
-    await this.requestAccountDecoding(account)
-
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'terminateApplication', [applicationId])
-
-    this.log(chalk.green(`Application ${chalk.white(applicationId)} has been succesfully terminated!`))
-  }
-}

+ 18 - 25
cli/src/commands/working-groups/updateWorkerReward.ts

@@ -3,9 +3,8 @@ import { apiModuleByGroup } from '../../Api'
 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 { isValidBalance } from '../../helpers/validation'
 
 export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsCommandBase {
   static description = "Change given worker's reward (amount only). Requires lead access."
@@ -15,53 +14,47 @@ export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsComman
       required: true,
       description: 'Worker ID',
     },
+    {
+      name: 'newReward',
+      required: true,
+      description: 'New reward',
+    },
   ]
 
   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): string {
+    return reward ? formatBalance(reward.valuePerBlock) + ' / block' : 'NONE'
   }
 
   async run() {
-    const { args } = this.parse(WorkingGroupsUpdateWorkerReward)
+    const {
+      args: { workerId, newReward },
+    } = this.parse(WorkingGroupsUpdateWorkerReward)
+
+    if (!isValidBalance(newReward)) {
+      this.error('Invalid reward', { exit: ExitCodes.InvalidInput })
+    }
 
     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 { reward } = groupMember
 
-    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)}`))
-
-    const newRewardValue = await this.promptForParam(
-      'BalanceOfMint',
-      createParamOptions('new_amount', undefined, positiveInt())
-    )
+    this.log(chalk.white(`Current worker reward: ${this.formatReward(reward)}`))
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'updateRewardAmount', [
-      workerId,
-      newRewardValue,
-    ])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'updateRewardAmount', [workerId, newReward])
 
     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(`Worker ${chalk.white(workerId.toString())} reward has been updated!`))
     this.log(chalk.green(`New worker reward: ${chalk.white(this.formatReward(updatedGroupMember.reward))}`))
   }
 }

+ 5 - 0
cli/src/helpers/validation.ts

@@ -17,3 +17,8 @@ export function checkBalance(accBalances: DeriveBalancesAll, requiredBalance: BN
     throw new CLIError('Not enough balance available', { exit: ExitCodes.InvalidInput })
   }
 }
+
+// We assume balance can be bigger than JavaScript integer
+export function isValidBalance(balance: string): boolean {
+  return /^[1-9][0-9]{0,38}$/.test(balance)
+}

+ 14 - 50
cli/src/json-schemas/WorkingGroupOpening.schema.json

@@ -7,65 +7,29 @@
   "additionalProperties": false,
   "required": ["activateAt", "maxReviewPeriodLength"],
   "properties": {
-    "activateAt": {
-      "oneOf": [
-        {
-          "type": "object",
-          "additionalProperties": false,
-          "required": ["ExactBlock"],
-          "properties": {
-            "ExactBlock": {
-              "type": "integer",
-              "minimum": 1,
-              "description": "Exact block number"
-            }
-          }
-        },
-        {
-          "type": "object",
-          "additionalProperties": false,
-          "required": ["CurrentBlock"],
-          "properties": { "CurrentBlock": { "type": "null" } }
-        }
-      ]
-    },
-    "maxActiveApplicants": {
+    "stakingPolicy": { "$ref": "#/definitions/StakingPolicy", "description": "Staking policy" },
+    "rewardPerBlock": {
       "type": "integer",
-      "description": "Max. number of active applicants",
-      "minimum": 1,
-      "default": 10
-    },
-    "maxReviewPeriodLength": {
-      "type": "integer",
-      "description": "Max. review period length in blocks",
-      "minimum": 1,
-      "default": 432000
-    },
-    "applicationStake": { "$ref": "#/definitions/StakingPolicy", "description": "Application stake properties" },
-    "roleStake": { "$ref": "#/definitions/StakingPolicy", "description": "Role stake properties" },
-    "terminateRoleUnstakingPeriod": { "$ref": "#/definitions/UnstakingPeriod" },
-    "leaveRoleUnstakingPeriod": { "$ref": "#/definitions/UnstakingPeriod" }
+      "description": "Reward per block",
+      "minimum": 1
+    }
   },
   "definitions": {
-    "UnstakingPeriod": {
-      "type": "integer",
-      "minimum": 1,
-      "default": 100800
-    },
     "StakingPolicy": {
       "type": "object",
       "additionalProperties": false,
-      "required": ["value", "mode"],
+      "required": ["amount", "unstakingPeriod"],
       "properties": {
-        "mode": {
-          "type": "string",
-          "description": "Application stake mode (Exact/AtLeast)",
-          "enum": ["Exact", "AtLeast"]
-        },
-        "value": {
+        "amount": {
           "type": "integer",
-          "description": "Required stake value in JOY",
+          "description": "Stake amount",
           "minimum": 1
+        },
+        "unstakingPeriod": {
+          "type": "integer",
+          "description": "Unstaking period in blocks",
+          "exclusiveMinimum": 43200,
+          "maximum": 4294967295
         }
       }
     }

+ 9 - 40
cli/src/json-schemas/typings/WorkingGroupOpening.schema.d.ts

@@ -5,56 +5,25 @@
  * and run json-schema-to-typescript to regenerate this file.
  */
 
-export type UnstakingPeriod = number
-
 /**
  * JSON schema to describe Joystream working group opening
  */
 export interface WorkingGroupOpening {
-  activateAt:
-    | {
-        /**
-         * Exact block number
-         */
-        ExactBlock: number
-      }
-    | {
-        CurrentBlock: null
-      }
-  /**
-   * Max. number of active applicants
-   */
-  maxActiveApplicants?: number
-  /**
-   * Max. review period length in blocks
-   */
-  maxReviewPeriodLength: number
   /**
-   * Application stake properties
+   * Staking policy
    */
-  applicationStake?: {
+  stakingPolicy?: {
     /**
-     * Application stake mode (Exact/AtLeast)
+     * Stake amount
      */
-    mode: 'Exact' | 'AtLeast'
+    amount: number;
     /**
-     * Required stake value in JOY
+     * Unstaking period in blocks
      */
-    value: number
-  }
+    unstakingPeriod: number;
+  };
   /**
-   * Role stake properties
+   * Reward per block
    */
-  roleStake?: {
-    /**
-     * Application stake mode (Exact/AtLeast)
-     */
-    mode: 'Exact' | 'AtLeast'
-    /**
-     * Required stake value in JOY
-     */
-    value: number
-  }
-  terminateRoleUnstakingPeriod?: UnstakingPeriod
-  leaveRoleUnstakingPeriod?: UnstakingPeriod
+  rewardPerBlock?: number;
 }

+ 1 - 0
cli/tsconfig.json

@@ -13,6 +13,7 @@
     "baseUrl": ".",
     "paths": {
       "@polkadot/types/augment": ["../types/augment-codec/augment-types.ts"],
+      "@polkadot/api/augment": ["../types/augment-codec/augment-api.ts"]
     },
     "resolveJsonModule": true,
     "skipLibCheck": true