Эх сурвалжийг харах

Merge branch 'nicaea' into restore_storage_node_cli_commands

# Conflicts:
#	package.json
#	storage-node/package.json
Shamil Gadelshin 4 жил өмнө
parent
commit
cb79347f4d
94 өөрчлөгдсөн 3134 нэмэгдсэн , 1313 устгасан
  1. 7 9
      .editorconfig
  2. 5 0
      .eslintrc.js
  3. 1 1
      .gitignore
  4. 3 0
      .prettierrc.js
  5. 5 5
      Cargo.lock
  6. 0 11
      cli/.editorconfig
  7. 0 6
      cli/.eslintrc
  8. 10 0
      cli/.eslintrc.js
  9. 2 0
      cli/.prettierignore
  10. 8 4
      cli/package.json
  11. 220 36
      cli/src/Api.ts
  12. 234 1
      cli/src/Types.ts
  13. 1 1
      cli/src/base/AccountsCommandBase.ts
  14. 349 1
      cli/src/base/ApiCommandBase.ts
  15. 80 0
      cli/src/base/DefaultCommandBase.ts
  16. 115 3
      cli/src/base/WorkingGroupsCommandBase.ts
  17. 11 61
      cli/src/commands/api/inspect.ts
  18. 40 0
      cli/src/commands/working-groups/application.ts
  19. 96 0
      cli/src/commands/working-groups/createOpening.ts
  20. 58 0
      cli/src/commands/working-groups/fillOpening.ts
  21. 78 0
      cli/src/commands/working-groups/opening.ts
  22. 22 0
      cli/src/commands/working-groups/openings.ts
  23. 1 1
      cli/src/commands/working-groups/overview.ts
  24. 46 0
      cli/src/commands/working-groups/startAcceptingApplications.ts
  25. 46 0
      cli/src/commands/working-groups/startReviewPeriod.ts
  26. 45 0
      cli/src/commands/working-groups/terminateApplication.ts
  27. 24 3
      cli/src/helpers/display.ts
  28. 5 0
      devops/.eslintrc.js
  29. 54 0
      devops/eslint-config/index.js
  30. 34 0
      devops/eslint-config/package.json
  31. 8 0
      devops/prettier-config/index.js
  32. 22 0
      devops/prettier-config/package.json
  33. 1 1
      node/Cargo.toml
  34. 44 40
      package.json
  35. 9 2
      pioneer/.eslintrc.js
  36. 1 0
      pioneer/.prettierignore
  37. 5 1
      pioneer/packages/joy-roles/src/OpeningMetadata.ts
  38. 2 0
      pioneer/packages/joy-roles/src/elements.tsx
  39. 42 18
      pioneer/packages/joy-roles/src/flows/apply.controller.tsx
  40. 2 0
      pioneer/packages/joy-roles/src/flows/apply.elements.stories.tsx
  41. 2 0
      pioneer/packages/joy-roles/src/flows/apply.stories.tsx
  42. 55 101
      pioneer/packages/joy-roles/src/flows/apply.tsx
  43. 3 3
      pioneer/packages/joy-roles/src/index.tsx
  44. 22 0
      pioneer/packages/joy-roles/src/mocks.ts
  45. 130 273
      pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx
  46. 10 10
      pioneer/packages/joy-roles/src/tabs/MyRoles.controller.tsx
  47. 12 9
      pioneer/packages/joy-roles/src/tabs/MyRoles.elements.stories.tsx
  48. 16 4
      pioneer/packages/joy-roles/src/tabs/MyRoles.tsx
  49. 1 0
      pioneer/packages/joy-roles/src/tabs/Opportunities.controller.tsx
  50. 2 1
      pioneer/packages/joy-roles/src/tabs/Opportunities.elements.stories.tsx
  51. 4 8
      pioneer/packages/joy-roles/src/tabs/Opportunities.stories.tsx
  52. 44 19
      pioneer/packages/joy-roles/src/tabs/Opportunities.tsx
  53. 2 22
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.controller.tsx
  54. 1 1
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.stories.tsx
  55. 24 13
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx
  56. 30 45
      pioneer/packages/joy-roles/src/transport.mock.ts
  57. 171 117
      pioneer/packages/joy-roles/src/transport.substrate.ts
  58. 10 9
      pioneer/packages/joy-roles/src/transport.ts
  59. 5 0
      pioneer/packages/joy-roles/src/working_groups.ts
  60. 2 1
      pioneer/packages/joy-utils/src/View.tsx
  61. 1 1
      pioneer/packages/react-components/src/AddressCard.tsx
  62. 1 1
      runtime-modules/content-working-group/Cargo.toml
  63. 11 1
      runtime-modules/content-working-group/src/lib.rs
  64. 1 1
      runtime-modules/hiring/Cargo.toml
  65. 4 1
      runtime-modules/hiring/src/hiring/mod.rs
  66. 0 43
      runtime-modules/hiring/src/hiring/opening.rs
  67. 0 15
      runtime-modules/hiring/src/hiring/staking_policy.rs
  68. 64 1
      runtime-modules/hiring/src/lib.rs
  69. 33 4
      runtime-modules/hiring/src/test/public_api/add_opening.rs
  70. 48 11
      runtime-modules/storage/src/data_directory.rs
  71. 129 0
      runtime-modules/storage/src/tests/data_directory.rs
  72. 3 1
      runtime-modules/storage/src/tests/mock.rs
  73. 1 1
      runtime-modules/working-group/Cargo.toml
  74. 13 1
      runtime-modules/working-group/src/errors.rs
  75. 191 174
      runtime-modules/working-group/src/tests/mod.rs
  76. 1 1
      runtime/Cargo.toml
  77. 6 1
      runtime/src/lib.rs
  78. 29 27
      runtime/src/migration.rs
  79. 33 40
      storage-node/.eslintrc.js
  80. 0 8
      storage-node/.prettierrc
  81. 3 1
      storage-node/package.json
  82. 5 0
      tests/network-tests/.eslintrc.js
  83. 0 6
      tests/network-tests/.prettierrc
  84. 3 3
      tests/network-tests/package.json
  85. 0 8
      tests/network-tests/tslint.json
  86. 3 0
      tsconfig.json
  87. 32 0
      types/src/JoyEnum.ts
  88. 11 2
      types/src/common.ts
  89. 3 19
      types/src/content-working-group/index.ts
  90. 56 38
      types/src/hiring/index.ts
  91. 4 3
      types/src/media.ts
  92. 129 3
      types/src/proposals.ts
  93. 29 13
      types/src/working-group/index.ts
  94. 5 43
      yarn.lock

+ 7 - 9
.editorconfig

@@ -1,16 +1,14 @@
+# In case prettier plugin or eslint with autofix is not enabled in IDE
+# The fallback settings here should match with our prettierrc config
+# so we get consistency!
 root = true
+
 [*]
-indent_style=tab
-indent_size=tab
-tab_width=4
+indent_style=space
+indent_size=2
+tab_width=2
 end_of_line=lf
 charset=utf-8
 trim_trailing_whitespace=true
 max_line_length=120
 insert_final_newline=true
-
-[*.yml]
-indent_style=space
-indent_size=2
-tab_width=8
-end_of_line=lf

+ 5 - 0
.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+    extends: [
+        '@joystream/eslint-config'
+    ]
+}

+ 1 - 1
.gitignore

@@ -22,7 +22,7 @@ yarn*
 .*.sw*
 
 # Visual Studio Code
-.vscode
+.vscode/
 
 # Compiled WASM code
 *.wasm

+ 3 - 0
.prettierrc.js

@@ -0,0 +1,3 @@
+module.exports = {
+  ...require('@joystream/prettier-config'),
+}

+ 5 - 5
Cargo.lock

@@ -1569,7 +1569,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node"
-version = "2.4.1"
+version = "2.5.0"
 dependencies = [
  "ctrlc",
  "derive_more 0.14.1",
@@ -1614,7 +1614,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node-runtime"
-version = "6.18.0"
+version = "6.19.0"
 dependencies = [
  "parity-scale-codec",
  "safe-mix",
@@ -4694,7 +4694,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-content-working-group-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -4857,7 +4857,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-hiring-module"
-version = "1.0.1"
+version = "1.0.2"
 dependencies = [
  "hex-literal 0.1.4",
  "mockall",
@@ -5568,7 +5568,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-working-group-module"
-version = "1.0.0"
+version = "1.0.1"
 dependencies = [
  "parity-scale-codec",
  "serde",

+ 0 - 11
cli/.editorconfig

@@ -1,11 +0,0 @@
-root = true
-
-[*]
-indent_style = space
-indent_size = 4
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
-
-[*.md]
-trim_trailing_whitespace = false

+ 0 - 6
cli/.eslintrc

@@ -1,6 +0,0 @@
-{
-  "extends": [
-    "oclif",
-    "oclif-typescript"
-  ]
-}

+ 10 - 0
cli/.eslintrc.js

@@ -0,0 +1,10 @@
+module.exports = {
+  extends: [
+    // The oclif rules have some code-style/formatting rules which may conflict with
+    // our prettier global settings. Disabling for now
+    // I suggest to only add essential rules absolutely required to make the cli work with oclif
+    // at the end of this file.
+    // "oclif",
+    // "oclif-typescript",
+  ],
+}

+ 2 - 0
cli/.prettierignore

@@ -0,0 +1,2 @@
+/lib/
+.nyc_output

+ 8 - 4
cli/package.json

@@ -8,7 +8,7 @@
   },
   "bugs": "https://github.com/Joystream/substrate-runtime-joystream/issues",
   "dependencies": {
-    "@joystream/types": "./types",
+    "@joystream/types": "^0.11.0",
     "@oclif/command": "^1.5.19",
     "@oclif/config": "^1.14.0",
     "@oclif/plugin-help": "^2.2.3",
@@ -21,7 +21,8 @@
     "moment": "^2.24.0",
     "proper-lockfile": "^4.1.1",
     "slug": "^2.1.1",
-    "tslib": "^1.11.1"
+    "tslib": "^1.11.1",
+    "ajv": "^6.11.0"
   },
   "devDependencies": {
     "@oclif/dev-cli": "^1.22.2",
@@ -84,11 +85,14 @@
   },
   "scripts": {
     "postpack": "rm -f oclif.manifest.json",
-    "posttest": "eslint . --ext .ts --config .eslintrc",
+    "posttest": "yarn lint",
     "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
     "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
     "build": "tsc --build tsconfig.json",
-    "version": "oclif-dev readme && git add README.md"
+    "version": "oclif-dev readme && git add README.md",
+    "lint": "eslint ./src/ --quiet --ext .ts",
+    "checks": "yarn lint && tsc --noEmit --pretty && prettier ./ --check",
+    "format": "prettier ./ --write"
   },
   "types": "lib/index.d.ts"
 }

+ 220 - 36
cli/src/Api.ts

@@ -13,21 +13,39 @@ import {
     CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj,
     WorkingGroups,
     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 } from '@joystream/types/working-group';
+import {
+    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';
 
 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
-const apiModuleByGroup: { [key in WorkingGroups]: string } = {
+export const apiModuleByGroup: { [key in WorkingGroups]: string } = {
     [WorkingGroups.StorageProviders]: 'storageWorkingGroup'
 };
 
@@ -35,7 +53,7 @@ const apiModuleByGroup: { [key in WorkingGroups]: string } = {
 export default class Api {
     private _api: ApiPromise;
 
-    private constructor(originalApi:ApiPromise) {
+    private constructor(originalApi: ApiPromise) {
         this._api = originalApi;
     }
 
@@ -44,12 +62,12 @@ export default class Api {
     }
 
     private static async initApi(apiUri: string = DEFAULT_API_URI): Promise<ApiPromise> {
-        const wsProvider:WsProvider = new WsProvider(apiUri);
+        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([
+        const [properties] = await Promise.all([
             api.rpc.system.properties()
         ]);
 
@@ -58,8 +76,8 @@ export default class Api {
 
         // formatBlanace config
         formatBalance.setDefaults({
-          decimals: tokenDecimals,
-          unit: tokenSymbol
+            decimals: tokenDecimals,
+            unit: tokenSymbol
         });
 
         return api;
@@ -86,7 +104,7 @@ export default class Api {
         return results;
     }
 
-    async getAccountsBalancesInfo(accountAddresses:string[]): Promise<DerivedBalances[]> {
+    async getAccountsBalancesInfo(accountAddresses: string[]): Promise<DerivedBalances[]> {
         let accountsBalances: DerivedBalances[] = await this._api.derive.balances.votingBalances(accountAddresses);
 
         return accountsBalances;
@@ -94,7 +112,7 @@ export default class Api {
 
     // Get on-chain data related to given account.
     // For now it's just account balances
-    async getAccountSummary(accountAddresses:string): Promise<AccountSummary> {
+    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
 
@@ -103,21 +121,21 @@ export default class Api {
 
     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,
+            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
+            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));
+        const results: CouncilInfoTuple = <CouncilInfoTuple>await this.queryMultiOnce(Object.values(queries));
 
         return createCouncilInfoObj(...results);
     }
@@ -126,7 +144,7 @@ export default class Api {
     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);
+        const transactionByteSize: BN = new BN(transfer.encodedLength + signature.length);
 
         const fees: DerivedFees = await this._api.derive.balances.fees();
 
@@ -151,7 +169,19 @@ export default class Api {
     }
 
     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 [result[0] as Vec<K>, result[1] as Vec<V>];
+    }
+
+    protected async blockHash(height: number): Promise<string> {
+        const blockHash = await this._api.rpc.chain.getBlockHash(height);
+
+        return blockHash.toString();
+    }
+
+    protected async blockTimestamp(height: number): Promise<Date> {
+        const blockTime = (await this._api.query.timestamp.now.at(await this.blockHash(height))) as Moment;
+
+        return new Date(blockTime.toNumber());
     }
 
     protected workingGroupApiQuery(group: WorkingGroups) {
@@ -184,8 +214,10 @@ export default class Api {
         return await this.groupMember(leadWorkerId, leadWorker);
     }
 
-    protected async stakeValue (stakeId: StakeId): Promise<Balance> {
-        const stake = (await this._api.query.stake.stakes(stakeId)) as Stake;
+    protected async stakeValue(stakeId: StakeId): Promise<Balance> {
+        const stake = this.singleLinkageResult<Stake>(
+            await this._api.query.stake.stakes(stakeId) as LinkageResult
+        );
         return stake.value;
     }
 
@@ -193,17 +225,17 @@ export default class Api {
         return this.stakeValue(stakeProfile.stake_id);
     }
 
-    protected async workerTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
+    protected async workerTotalReward(relationshipId: RewardRelationshipId): Promise<Balance> {
         const relationship = this.singleLinkageResult<RewardRelationship>(
             await this._api.query.recurringRewards.rewardRelationships(relationshipId) as LinkageResult
         );
         return relationship.total_reward_received;
     }
 
-    protected async groupMember (
+    protected async groupMember(
         id: WorkerId,
         worker: Worker
-      ): Promise<GroupMember> {
+    ): Promise<GroupMember> {
         const roleAccount = worker.role_account_id;
         const memberId = worker.member_id;
 
@@ -215,12 +247,12 @@ export default class Api {
 
         let stakeValue: Balance = this._api.createType("Balance", 0);
         if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
-          stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
+            stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
         }
 
         let earnedValue: Balance = this._api.createType("Balance", 0);
         if (worker.reward_relationship && worker.reward_relationship.isSome) {
-          earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
+            earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
         }
 
         return ({
@@ -233,24 +265,176 @@ export default class Api {
         });
     }
 
-    async groupMembers (group: WorkingGroups): Promise<GroupMember[]> {
+    async groupMembers(group: WorkingGroups): Promise<GroupMember[]> {
         const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId;
 
         // This is chain specfic, but if next id is still 0, it means no curators have been added yet
         if (nextId.eq(0)) {
-          return [];
+            return [];
         }
 
-        const [ workerIds, workers ] = this.multiLinkageResult<WorkerId, Worker>(
+        const [workerIds, workers] = this.multiLinkageResult<WorkerId, Worker>(
             (await this.workingGroupApiQuery(group).workerById()) as LinkageResult
         );
 
         let groupMembers: GroupMember[] = [];
-        for (let [ index, worker ] of Object.entries(workers.toArray())) {
+        for (let [index, worker] of Object.entries(workers.toArray())) {
             const workerId = workerIds[parseInt(index)];
             groupMembers.push(await this.groupMember(workerId, worker));
         }
 
         return groupMembers.reverse();
-      }
+    }
+
+    async openingsByGroup(group: WorkingGroups): Promise<GroupOpening[]> {
+        const openings: GroupOpening[] = [];
+        const nextId = (await this.workingGroupApiQuery(group).nextOpeningId()) as 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 highestId = nextId.toNumber() - 1;
+            for (let i = highestId; i >= 0; i--) {
+                openings.push(await this.groupOpening(group, i));
+            }
+        }
+
+        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);
+    }
+
+    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 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!');
+        }
+
+        return this.singleLinkageResult<WGApplication>(
+            await this.workingGroupApiQuery(group).applicationById(wgApplicationId) as LinkageResult
+        );
+    }
+
+    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(),
+            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 stakes = {
+            application: opening.application_staking_policy.unwrapOr(undefined),
+            role: opening.role_staking_policy.unwrapOr(undefined)
+        }
+
+        return ({
+            wgOpeningId,
+            openingId,
+            opening,
+            stage,
+            stakes,
+            applications
+        });
+    }
+
+    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
+        };
+    }
 }

+ 234 - 1
cli/src/Types.ts

@@ -1,11 +1,29 @@
 import BN from 'bn.js';
 import { ElectionStage, Seat } from '@joystream/types/council';
-import { Option } from '@polkadot/types';
+import { Option, Text } from '@polkadot/types';
+import { Constructor } 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 } 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';
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -83,3 +101,218 @@ export type GroupMember = {
     stake: Balance;
     earned: Balance;
 }
+
+export type GroupApplication = {
+    wgApplicationId: number;
+    applicationId: 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'
+}
+
+export type GroupOpeningStage = {
+    status: OpeningStatus;
+    block?: number;
+    date?: Date;
+}
+
+export type GroupOpeningStakes = {
+    application?: StakingPolicy;
+    role?: StakingPolicy;
+}
+
+export type GroupOpening = {
+    wgOpeningId: number;
+    openingId: number;
+    stage: GroupOpeningStage;
+    opening: Opening;
+    stakes: GroupOpeningStakes;
+    applications: GroupApplication[];
+}
+
+// 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;
+}
+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 };
+    }
+}
+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 };
+    }
+}
+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 };
+    }
+}
+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 };
+    }
+}
+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 };
+    }
+}
+class HRTQuestionsFieldsVec extends Vec.with(HRTQuestionFieldStruct) implements WithJSONable<QuestionsFields> {
+    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 };
+    }
+}
+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 };
+    }
+}
+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 };
+    }
+};
+
+// A mapping of argName to json struct and schemaValidator
+// It is used to map arguments of type "Bytes" that are in fact a json string
+// (and can be validated against a schema)
+export type JSONArgsMapping = { [argName: string]: {
+    struct: Constructor<Struct>,
+    schemaValidator: ajv.ValidateFunction
+} };

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

@@ -11,7 +11,7 @@ import { NamedKeyringPair } from '../Types';
 import { DerivedBalances } from '@polkadot/api-derive/types';
 import { toFixedLength } from '../helpers/display';
 
-const ACCOUNTS_DIRNAME = '/accounts';
+const ACCOUNTS_DIRNAME = 'accounts';
 const SPECIAL_ACCOUNT_POSTFIX = '__DEV';
 
 /**

+ 349 - 1
cli/src/base/ApiCommandBase.ts

@@ -2,7 +2,19 @@ import ExitCodes from '../ExitCodes';
 import { CLIError } from '@oclif/errors';
 import StateAwareCommandBase from './StateAwareCommandBase';
 import Api from '../Api';
-import { ApiPromise } from '@polkadot/api'
+import { JSONArgsMapping } from '../Types';
+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';
+
+export type ApiMethodInputArg = Codec;
+
+class ExtrinsicFailedError extends Error { };
 
 /**
  * Abstract base class for commands that require access to the API.
@@ -25,4 +37,340 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         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, defaultValue?: Codec): Promise<Codec> {
+        const providedValue = await this.simplePrompt({
+            message: `Provide value for ${ this.paramName(typeDef) }`,
+            type: 'input',
+            default: defaultValue?.toString()
+        });
+        return createType(typeDef.type as any, providedValue);
+    }
+
+    // Prompt for Option<Codec> value
+    async promptForOption(typeDef: TypeDef, defaultValue?: Option<Codec>): Promise<Option<Codec>> {
+        const subtype = <TypeDef> typeDef.sub; // We assume that Opion always has a single subtype
+        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, subtype.name, defaultValue?.unwrapOr(undefined));
+            this.closeIndentGroup();
+            return new Option(subtype.type as any, value);
+        }
+
+        return new Option(subtype.type as any, null);
+    }
+
+    // Prompt for Tuple
+    // TODO: Not well tested yet
+    async promptForTuple(typeDef: TypeDef, defaultValue: Tuple): Promise<Tuple> {
+        console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } tuple:`));
+
+        this.openIndentGroup();
+        const result: ApiMethodInputArg[] = [];
+        // 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! ];
+
+        for (const [index, subtype] of Object.entries(subtypes)) {
+            const inputParam = await this.promptForParam(subtype.type, subtype.name, defaultValue[parseInt(index)]);
+            result.push(inputParam);
+        }
+        this.closeIndentGroup();
+
+        return new Tuple((subtypes.map(subtype => subtype.type)) as any, result);
+    }
+
+    // Prompt for Struct
+    async promptForStruct(typeDef: TypeDef, defaultValue?: Struct): Promise<ApiMethodInputArg> {
+        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 structValues: { [key: string]: ApiMethodInputArg } = {};
+        for (const subtype of structSubtypes) {
+            structValues[subtype.name!] =
+                await this.promptForParam(subtype.type, subtype.name, defaultValue && defaultValue.get(subtype.name!));
+        }
+        this.closeIndentGroup();
+
+        return createType(structType as any, structValues);
+    }
+
+    // Prompt for Vec
+    async promptForVec(typeDef: TypeDef, defaultValue?: Vec<Codec>): 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;
+        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, subtype.name, defaultEntryValue));
+            }
+        } while (addAnother);
+        this.closeIndentGroup();
+
+        return new Vec(subtype.type as any, entries);
+    }
+
+    // Prompt for Enum
+    async promptForEnum(typeDef: TypeDef, defaultValue?: Enum): 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 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') {
+            return createType(
+                enumType as any,
+                { [enumSubtype.name!]: await this.promptForParam(enumSubtype.type, enumSubtype.name, defaultValue?.value) }
+            );
+        }
+
+        return createType(enumType as any, enumSubtype.name);
+    }
+
+    // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
+    // TODO: This may not yet work for all possible types
+    async promptForParam(paramType: string, forcedName?: string, defaultValue?: ApiMethodInputArg): Promise<ApiMethodInputArg> {
+        const typeDef = getTypeDef(paramType);
+        const rawTypeDef = this.getRawTypeDef(paramType);
+
+        if (forcedName) {
+            typeDef.name = forcedName;
+        }
+
+        if (rawTypeDef.info === TypeDefInfo.Option) {
+            return await this.promptForOption(typeDef, defaultValue as Option<Codec>);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Tuple) {
+            return await this.promptForTuple(typeDef, defaultValue as Tuple);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Struct) {
+            return await this.promptForStruct(typeDef, defaultValue as Struct);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Enum) {
+            return await this.promptForEnum(typeDef, defaultValue as Enum);
+        }
+        else if (rawTypeDef.info === TypeDefInfo.Vec) {
+            return await this.promptForVec(typeDef, defaultValue as Vec<Codec>);
+        }
+        else {
+            return await this.promptForSimple(typeDef, defaultValue);
+        }
+    }
+
+    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: boolean = true, jsonText: string;
+        do {
+            const structVal = await this.promptForStruct(typeDef, 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'));
+    }
+
+    async promptForExtrinsicParams(
+        module: string,
+        method: string,
+        jsonArgs?: JSONArgsMapping,
+        defaultValues?: ApiMethodInputArg[]
+    ): Promise<ApiMethodInputArg[]> {
+        const extrinsicMethod = this.getOriginalApi().tx[module][method];
+        let values: ApiMethodInputArg[] = [];
+
+        this.openIndentGroup();
+        for (const [index, arg] of Object.entries(extrinsicMethod.meta.args.toArray())) {
+            const argName = arg.name.toString();
+            const argType = arg.type.toString();
+            const defaultValue = defaultValues && defaultValues[parseInt(index)];
+            if (jsonArgs && jsonArgs[argName]) {
+                const { struct, schemaValidator } = jsonArgs[argName];
+                values.push(await this.promptForJsonBytes(struct, argName, defaultValue as Bytes, schemaValidator));
+            }
+            else {
+                values.push(await this.promptForParam(argType, argName, defaultValue));
+            }
+        };
+        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: 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;
+            }
+        }
+    }
+
+    async buildAndSendExtrinsic(
+        account: KeyringPair,
+        module: string,
+        method: string,
+        jsonArgs?: JSONArgsMapping, // Special JSON arguments (ie. human_readable_text of working group opening)
+        defaultValues?: ApiMethodInputArg[],
+        warnOnly: boolean = false // If specified - only warning will be displayed (instead of error beeing thrown)
+    ): Promise<ApiMethodInputArg[]> {
+        const params = await this.promptForExtrinsicParams(module, method, jsonArgs, defaultValues);
+        await this.sendAndFollowExtrinsic(account, module, method, params, warnOnly);
+
+        return params;
+    }
+
+    extrinsicArgsFromDraft(module: string, method: string, draftFilePath: string): ApiMethodInputArg[] {
+        let draftJSONObj, parsedArgs: ApiMethodInputArg[] = [];
+        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(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;
+    }
 }

+ 80 - 0
cli/src/base/DefaultCommandBase.ts

@@ -1,11 +1,91 @@
 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 = '';
+
+    openIndentGroup() {
+        console.group();
+        ++this.indentGroupsOpened;
+    }
+
+    closeIndentGroup() {
+        console.groupEnd();
+        --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)
+        }]);
+
+        return result;
+    }
+
+    private jsonPrettyIndented(line:string) {
+        return `${this.jsonPrettyIdent}${ line }`;
+    }
+
+    private jsonPrettyOpen(char: '{' | '[') {
+        this.jsonPrettyIdent += '    ';
+        return chalk.gray(char)+"\n";
+    }
+
+    private jsonPrettyClose(char: '}' | ']') {
+        this.jsonPrettyIdent = this.jsonPrettyIdent.slice(0, -4);
+        return this.jsonPrettyIndented(chalk.gray(char));
+    }
+
+    private jsonPrettyKeyVal(key:string, val:any): string {
+        return this.jsonPrettyIndented(chalk.white(`${key}: ${this.jsonPrettyAny(val)}`));
+    }
+
+    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 jsonPrettyArr(arr: any[]): string {
+        return this.jsonPrettyOpen('[')
+            + arr.map(v => this.jsonPrettyIndented(this.jsonPrettyAny(v))).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}"`);
+        }
+
+        // Number, boolean etc.
+        return chalk.cyan(val);
+    }
+
+    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

+ 115 - 3
cli/src/base/WorkingGroupsCommandBase.ts

@@ -1,11 +1,18 @@
 import ExitCodes from '../ExitCodes';
 import AccountsCommandBase from './AccountsCommandBase';
 import { flags } from '@oclif/command';
-import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember } from '../Types';
+import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening } from '../Types';
+import { apiModuleByGroup } from '../Api';
 import { CLIError } from '@oclif/errors';
 import inquirer from 'inquirer';
+import { ApiMethodInputArg } from './ApiCommandBase';
+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
@@ -67,11 +74,116 @@ export default abstract class WorkingGroupsCommandBase extends AccountsCommandBa
         return groupMembers[choosenWorkerIndex];
     }
 
+    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: 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 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;
+    }
+
+    loadOpeningDraftParams(draftName: string) {
+        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: ApiMethodInputArg[]) {
+        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());
+        }
+    }
+
     async init() {
         await super.init();
-        const { flags } = this.parse(WorkingGroupsCommandBase);
+        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!', { exit: ExitCodes.InvalidInput });
+            throw new CLIError(`Invalid group! Available values are: ${AvailableGroups.join(', ')}`, { exit: ExitCodes.InvalidInput });
         }
         this.group = flags.group as WorkingGroups;
     }

+ 11 - 61
cli/src/commands/api/inspect.ts

@@ -2,14 +2,13 @@ import { flags } from '@oclif/command';
 import { CLIError } from '@oclif/errors';
 import { displayNameValueTable } from '../../helpers/display';
 import { ApiPromise } from '@polkadot/api';
-import { getTypeDef } from '@polkadot/types';
-import { Codec, TypeDef, TypeDefInfo } from '@polkadot/types/types';
+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 } from '../../Types';
-import inquirer from 'inquirer';
-import ApiCommandBase from '../../base/ApiCommandBase';
+import ApiCommandBase, { ApiMethodInputArg } from '../../base/ApiCommandBase';
 
 // Command flags type
 type ApiInspectFlags = {
@@ -30,12 +29,6 @@ const TYPES_AVAILABLE = [
 // It works as if we specified: type ApiType = 'query' | 'consts'...;
 type ApiType = typeof TYPES_AVAILABLE[number];
 
-// Format of the api input args (as they are specified in the CLI)
-type ApiMethodInputSimpleArg = string;
-// This recurring type allows the correct handling of nested types like:
-// ((Type1, Type2), Option<Type3>) etc.
-type ApiMethodInputArg = ApiMethodInputSimpleArg | ApiMethodInputArg[];
-
 export default class ApiInspect extends ApiCommandBase {
     static description =
         'Lists available node API modules/methods and/or their description(s), '+
@@ -154,62 +147,19 @@ export default class ApiInspect extends ApiCommandBase {
         return { apiType, apiModule, apiMethod };
     }
 
-    // Prompt for simple value (string)
-    async promptForSimple(typeName: string): Promise<string> {
-        const userInput = await inquirer.prompt([{
-            name: 'providedValue',
-            message: `Provide value for ${ typeName }`,
-            type: 'input'
-        } ])
-        return <string> userInput.providedValue;
-    }
-
-    // Prompt for optional value (returns undefined if user refused to provide)
-    async promptForOption(typeDef: TypeDef): Promise<ApiMethodInputArg | undefined> {
-        const userInput = await inquirer.prompt([{
-            name: 'confirmed',
-            message: `Do you want to provide the optional ${ typeDef.type } parameter?`,
-            type: 'confirm'
-        } ]);
-
-        if (userInput.confirmed) {
-            const subtype = <TypeDef> typeDef.sub; // We assume that Opion always has a single subtype
-            let value = await this.promptForParam(subtype.type);
-            return value;
-        }
-    }
-
-    // Prompt for tuple - returns array of values
-    async promptForTuple(typeDef: TypeDef): Promise<(ApiMethodInputArg)[]> {
-        let result: ApiMethodInputArg[] = [];
-
-        if (!typeDef.sub) return [ await this.promptForSimple(typeDef.type) ];
-
-        const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub : [ typeDef.sub ];
-
-        for (let subtype of subtypes) {
-            let inputParam = await this.promptForParam(subtype.type);
-            if (inputParam !== undefined) result.push(inputParam);
-        }
-
-        return result;
-    }
-
-    // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
-    async promptForParam(paramType: string): Promise<ApiMethodInputArg | undefined> {
-        const typeDef: TypeDef = getTypeDef(paramType);
-        if (typeDef.info === TypeDefInfo.Option) return await this.promptForOption(typeDef);
-        else if (typeDef.info === TypeDefInfo.Tuple) return await this.promptForTuple(typeDef);
-        else return await this.promptForSimple(typeDef.type);
-    }
-
     // Request values for params using array of param types (strings)
     async requestParamsValues(paramTypes: string[]): Promise<ApiMethodInputArg[]> {
         let result: ApiMethodInputArg[] = [];
         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 !== undefined) result.push(paramValue);
+            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;
@@ -227,7 +177,7 @@ export default class ApiInspect extends ApiCommandBase {
 
             if (apiType === 'query') {
                 // Api query - call with (or without) arguments
-                let args: ApiMethodInputArg[] = flags.callArgs ? flags.callArgs.split(',') : [];
+                let args: (string | ApiMethodInputArg)[] = 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:');

+ 40 - 0
cli/src/commands/working-groups/application.ts

@@ -0,0 +1,40 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { displayCollapsedRow, displayHeader } from '../../helpers/display';
+import _ from 'lodash';
+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,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsApplication);
+
+        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'),
+            '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);
+    }
+}

+ 96 - 0
cli/src/commands/working-groups/createOpening.ts

@@ -0,0 +1,96 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { HRTStruct } from '../../Types';
+import chalk from 'chalk';
+import { flags } from '@oclif/command';
+import { ApiMethodInputArg } from '../../base/ApiCommandBase';
+import { schemaValidator } from '@joystream/types/hiring';
+import { apiModuleByGroup } from '../../Api';
+
+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']
+        })
+    };
+
+    async run() {
+        const account = await this.getRequiredSelectedAccount();
+        // lead-only gate
+        await this.getRequiredLead();
+
+        const { flags } = this.parse(WorkingGroupsCreateOpening);
+
+        let defaultValues: ApiMethodInputArg[] | undefined = undefined;
+        if (flags.useDraft) {
+            const draftName = flags.draftName || await this.promptForOpeningDraft();
+            defaultValues =  await this.loadOpeningDraftParams(draftName);
+        }
+
+        if (!flags.skipPrompts) {
+            const module = apiModuleByGroup[this.group];
+            const method = 'addOpening';
+            const jsonArgsMapping = { 'human_readable_text': { struct: HRTStruct, schemaValidator } };
+
+            let saveDraft = false, params: ApiMethodInputArg[];
+            if (flags.createDraftOnly) {
+                params = await this.promptForExtrinsicParams(module, method, jsonArgsMapping, defaultValues);
+                saveDraft = true;
+            }
+            else {
+                await this.requestAccountDecoding(account); // Prompt for password
+
+                params = await this.buildAndSendExtrinsic(
+                    account,
+                    module,
+                    method,
+                    jsonArgsMapping,
+                    defaultValues,
+                    true
+                );
+
+                this.log(chalk.green('Opening succesfully created!'));
+
+                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);
+
+                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!);
+            this.log(chalk.green('Opening succesfully created!'));
+        }
+    }
+}

+ 58 - 0
cli/src/commands/working-groups/fillOpening.ts

@@ -0,0 +1,58 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import _ from 'lodash';
+import { OpeningStatus } from '../../Types';
+import ExitCodes from '../../ExitCodes';
+import { apiModuleByGroup } from '../../Api';
+import { OpeningId } from '@joystream/types/hiring';
+import { ApplicationIdSet, RewardPolicy } from '@joystream/types/working-group';
+import chalk from 'chalk';
+
+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 opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+
+        if (opening.stage.status !== OpeningStatus.InReview) {
+            this.error('This opening is not in the Review stage!', { exit: ExitCodes.InvalidInput });
+        }
+
+        const applicationIds = await this.promptForApplicationsToAccept(opening);
+        const rewardPolicyOpt = await this.promptForParam(`Option<${RewardPolicy.name}>`, 'RewardPolicy');
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'fillOpening',
+            [
+                new OpeningId(opening.wgOpeningId),
+                new ApplicationIdSet(applicationIds),
+                rewardPolicyOpt
+            ]
+        );
+
+        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} succesfully filled!`));
+        this.log(
+            chalk.green('Accepted working group application IDs: ') +
+            chalk.white(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE')
+        );
+    }
+}

+ 78 - 0
cli/src/commands/working-groups/opening.ts

@@ -0,0 +1,78 @@
+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,
+    };
+
+    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) }`;
+    }
+
+    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());
+
+        displayHeader('Opening details');
+        const openingRow = {
+            'WG Opening ID': opening.wgOpeningId,
+            'Opening ID': opening.openingId,
+            ...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);
+    }
+  }

+ 22 - 0
cli/src/commands/working-groups/openings.ts

@@ -0,0 +1,22 @@
+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,
+    };
+
+    async run() {
+        const openings = await this.getApi().openingsByGroup(this.group);
+
+        const openingsRows = openings.map(o => ({
+            'WG Opening ID': o.wgOpeningId,
+            'Opening ID': o.openingId,
+            'Stage': `${_.startCase(o.stage.status)}${o.stage.block ? ` (#${o.stage.block})` : ''}`,
+            'Applications': o.applications.length
+        }));
+        displayTable(openingsRows, 5);
+    }
+}

+ 1 - 1
cli/src/commands/working-groups/overview.ts

@@ -33,6 +33,6 @@ export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
             'Stake': formatBalance(m.stake),
             'Earned': formatBalance(m.earned)
         }));
-        displayTable(membersRows, 20);
+        displayTable(membersRows, 5);
     }
   }

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

@@ -0,0 +1,46 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import _ from 'lodash';
+import { OpeningStatus } from '../../Types';
+import ExitCodes from '../../ExitCodes';
+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,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsStartAcceptingApplications);
+
+        const account = await this.getRequiredSelectedAccount();
+        // Lead-only gate
+        await this.getRequiredLead();
+
+        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+
+        if (opening.stage.status !== OpeningStatus.WaitingToBegin) {
+            this.error('This opening is not in "Waiting To Begin" stage!', { exit: ExitCodes.InvalidInput });
+        }
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'acceptApplications',
+            [ new OpeningId(opening.wgOpeningId) ]
+        );
+
+        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} status changed to: ${ chalk.white('Accepting Applications') }`));
+    }
+}

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

@@ -0,0 +1,46 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import _ from 'lodash';
+import { OpeningStatus } from '../../Types';
+import ExitCodes from '../../ExitCodes';
+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,
+    };
+
+    async run() {
+        const { args } = this.parse(WorkingGroupsStartReviewPeriod);
+
+        const account = await this.getRequiredSelectedAccount();
+        // Lead-only gate
+        await this.getRequiredLead();
+
+        const opening = await this.getApi().groupOpening(this.group, parseInt(args.wgOpeningId));
+
+        if (opening.stage.status !== OpeningStatus.AcceptingApplications) {
+            this.error('This opening is not in "Accepting Applications" stage!', { exit: ExitCodes.InvalidInput });
+        }
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'beginApplicantReview',
+            [ new OpeningId(opening.wgOpeningId) ]
+        );
+
+        this.log(chalk.green(`Opening ${chalk.white(opening.wgOpeningId)} status changed to: ${ chalk.white('In Review') }`));
+    }
+}

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

@@ -0,0 +1,45 @@
+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';
+
+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 application = await this.getApi().groupApplication(this.group, parseInt(args.wgApplicationId));
+
+        if (application.stage !== ApplicationStageKeys.Active) {
+            this.error('This application is not active!', { exit: ExitCodes.InvalidInput });
+        }
+
+        await this.requestAccountDecoding(account);
+
+        await this.sendAndFollowExtrinsic(
+            account,
+            apiModuleByGroup[this.group],
+            'terminateApplication',
+            [new ApplicationId(application.wgApplicationId)]
+        );
+
+        this.log(chalk.green(`Application ${chalk.white(application.wgApplicationId)} has been succesfully terminated!`));
+    }
+}

+ 24 - 3
cli/src/helpers/display.ts

@@ -23,13 +23,34 @@ export function displayNameValueTable(rows: NameValueObj[]) {
     );
 }
 
-export function displayTable(rows: { [k: string]: string }[], minColumnWidth = 0) {
+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()
+    }));
+
+    displayNameValueTable(collapsedRow);
+}
+
+export function displayCollapsedTable(rows: { [k: string]: string | number }[]) {
+    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: minColumnWidth
+        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))

+ 5 - 0
devops/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  env: {
+    node: true,
+  },
+}

+ 54 - 0
devops/eslint-config/index.js

@@ -0,0 +1,54 @@
+// This config is used globally at the root of the repo, so it should be as thin
+// as possible with rules that we absolutely require across all projects.
+module.exports = {
+  env: {
+    es6: true,
+  },
+  globals: {
+    Atomics: 'readonly',
+    SharedArrayBuffer: 'readonly',
+  },
+  // We are relying on version that comes with @polkadot/dev
+  // Newest version is breaking pioneer!
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaFeatures: {
+      jsx: true,
+    },
+    ecmaVersion: 2019,
+    sourceType: 'module',
+  },
+  extends: [
+    'standard',
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react/recommended',
+    // this is only in newer versions of eslint-plugin-react-hooks
+    // 'plugin:react-hooks/recommended',
+    'plugin:prettier/recommended',
+    'prettier/@typescript-eslint',
+    'prettier/react',
+    'prettier/standard',
+  ],
+  settings: {
+    react: {
+      version: 'detect',
+    },
+  },
+  rules: {
+    // drop these when using newer versions of eslint-plugin-react-hooks
+    'react-hooks/rules-of-hooks': 'error',
+    'react-hooks/exhaustive-deps': 'warn',
+    // only cli projects should really have this rule, web apps
+    // should prefer using 'debug' package at least to allow control of
+    // output verbosity if logging to console.
+    'no-console': 'off',
+  },
+  plugins: [
+    'standard',
+    '@typescript-eslint',
+    'react',
+    'react-hooks',
+    'prettier',
+  ],
+}

+ 34 - 0
devops/eslint-config/package.json

@@ -0,0 +1,34 @@
+{
+  "name": "@joystream/eslint-config",
+  "version": "1.0.0",
+  "description": "joystream eslint shared config",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/joystream/joystream.git"
+  },
+  "author": "Joystream contributors",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/joystream/joystream/issues"
+  },
+  "homepage": "https://github.com/joystream/joystream#readme",
+  "peerDependencies": {
+    "eslint": ">= 5"
+  },
+  "dependencies": {
+    "@typescript-eslint/parser": "^2.34.0",
+    "eslint-config-prettier": "^6.11.0",
+    "eslint-plugin-prettier": "^3.1.3",
+    "eslint-plugin-react": "^7.16.0",
+    "eslint-plugin-react-hooks": "^2.3.0",
+    "eslint-config-standard": "^14.1.1",
+    "eslint-plugin-standard": "^4.0.1",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-import": "^2.22.0",
+    "eslint-plugin-node": "^11.1.0"
+  }
+}

+ 8 - 0
devops/prettier-config/index.js

@@ -0,0 +1,8 @@
+module.exports = {
+  singleQuote: true,
+  arrowParens: 'always',
+  useTabs: false,
+  tabWidth: 2,
+  semi: false,
+  trailingComma: 'es5',
+}

+ 22 - 0
devops/prettier-config/package.json

@@ -0,0 +1,22 @@
+{
+  "name": "@joystream/prettier-config",
+  "version": "1.0.0",
+  "description": "joystream prettier shared config",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/joystream/joystream.git"
+  },
+  "author": "Joystream contributors",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/joystream/joystream/issues"
+  },
+  "homepage": "https://github.com/joystream/joystream#readme",
+  "peerDependencies": {
+    "prettier": ">= 2"
+  }
+}

+ 1 - 1
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream']
 build = 'build.rs'
 edition = '2018'
 name = 'joystream-node'
-version = '2.4.1'
+version = '2.5.0'
 default-run = "joystream-node"
 
 [[bin]]

+ 44 - 40
package.json

@@ -1,42 +1,46 @@
 {
-	"private": true,
-	"name": "joystream",
-	"version": "1.0.0",
-	"license": "GPL-3.0-only",
-	"scripts": {
-		"test": "yarn && yarn workspaces run test",
-		"test-migration": "yarn && yarn workspaces run test-migration",
-		"postinstall": "yarn workspace @joystream/types build && yarn workspace storage-node run build",
-		"cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
-		"cargo-build": "scripts/cargo-build.sh"
-	},
-	"workspaces": [
-		"tests/network-tests",
-		"cli",
-		"types",
-		"pioneer",
-		"pioneer/packages/*",
-		"storage-node",
-		"storage-node/packages/*"
-	],
-	"resolutions": {
-		"@polkadot/api": "^0.96.1",
-		"@polkadot/api-contract": "^0.96.1",
-		"@polkadot/keyring": "^1.7.0-beta.5",
-		"@polkadot/types": "^0.96.1",
-		"@polkadot/util": "^1.7.0-beta.5",
-		"@polkadot/util-crypto": "^1.7.0-beta.5",
-		"babel-core": "^7.0.0-bridge.0",
-		"typescript": "^3.7.2"
-	},
-	"devDependencies": {
-		"husky": "^4.2.5",
-		"eslint-plugin-prettier": "^3.1.4"
-	},
-	"husky": {
-	  "hooks": {
-		"pre-commit": "devops/git-hooks/pre-commit",
-		"pre-push": "devops/git-hooks/pre-push"
-	  }
-	}
+  "private": true,
+  "name": "joystream",
+  "version": "1.0.0",
+  "license": "GPL-3.0-only",
+  "scripts": {
+    "test": "yarn && yarn workspaces run test",
+    "test-migration": "yarn && yarn workspaces run test-migration",
+    "postinstall": "yarn workspace @joystream/types build && yarn workspace storage-node run build",
+	"cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
+    "cargo-build": "scripts/cargo-build.sh",
+    "lint": "yarn workspaces run lint"
+  },
+  "workspaces": [
+    "tests/network-tests",
+    "cli",
+    "types",
+    "pioneer",
+    "pioneer/packages/*",
+    "storage-node",
+    "storage-node/packages/*",
+    "devops/eslint-config",
+    "devops/prettier-config"
+  ],
+  "resolutions": {
+    "@polkadot/api": "^0.96.1",
+    "@polkadot/api-contract": "^0.96.1",
+    "@polkadot/keyring": "^1.7.0-beta.5",
+    "@polkadot/types": "^0.96.1",
+    "@polkadot/util": "^1.7.0-beta.5",
+    "@polkadot/util-crypto": "^1.7.0-beta.5",
+    "babel-core": "^7.0.0-bridge.0",
+    "typescript": "^3.7.2"
+  },
+  "devDependencies": {
+    "husky": "^4.2.5",
+    "prettier": "2.0.2",
+    "eslint": "^5.16.0"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "devops/git-hooks/pre-commit",
+      "pre-push": "devops/git-hooks/pre-push"
+    }
+  }
 }

+ 9 - 2
pioneer/.eslintrc.js

@@ -1,3 +1,4 @@
+// At some point don't depend on @polkadot rules and use @joystream/eslint-config
 const base = require('@polkadot/dev-react/config/eslint');
 
 // add override for any (a metric ton of them, initial conversion)
@@ -15,6 +16,12 @@ module.exports = {
     '@typescript-eslint/camelcase': 'off',
     'react/prop-types': 'off',
     'new-cap': 'off',
-    '@typescript-eslint/interface-name-prefix': 'off'
-  }
+    '@typescript-eslint/interface-name-prefix': 'off',
+    '@typescript-eslint/ban-ts-comment': 'error',
+    // why only required in VSCode!?!? is eslint plugin not working like eslint commandline?
+    // Or are we having to add this because of new versions of eslint-config-* ?
+    'no-console': 'off',
+  },
+  // isolate pioneer from monorepo eslint rules
+  root: true
 };

+ 1 - 0
pioneer/.prettierignore

@@ -0,0 +1 @@
+**

+ 5 - 1
pioneer/packages/joy-roles/src/OpeningMetadata.ts

@@ -1,6 +1,10 @@
+import { WorkingGroups } from './working_groups';
+import { OpeningType } from '@joystream/types/working-group';
+
 export type OpeningMetadata = {
   id: string;
-  group: string;
+  group: WorkingGroups;
+  type?: OpeningType;
 }
 
 export type OpeningMetadataProps = {

+ 2 - 0
pioneer/packages/joy-roles/src/elements.tsx

@@ -9,6 +9,7 @@ import Identicon from '@polkadot/react-identicon';
 import { IProfile, MemberId } from '@joystream/types/members';
 import { GenericAccountId } from '@polkadot/types';
 import { LeadRoleState } from '@joystream/types/content-working-group';
+import { WorkerId } from '@joystream/types/working-group';
 
 type BalanceProps = {
   balance?: Balance;
@@ -47,6 +48,7 @@ export type GroupMember = {
 
 export type GroupLead = {
   memberId: MemberId;
+  workerId?: WorkerId; // In case of "working-group" module
   roleAccount: GenericAccountId;
   profile: IProfile;
   title: string;

+ 42 - 18
pioneer/packages/joy-roles/src/flows/apply.controller.tsx

@@ -17,7 +17,7 @@ import { keyPairDetails, FlowModal, ProgressSteps } from './apply';
 
 import { OpeningStakeAndApplicationStatus } from '../tabs/Opportunities';
 import { Min, Step, Sum } from '../balances';
-import { WorkingGroups } from '../working_groups';
+import { WorkingGroups, AvailableGroups } from '../working_groups';
 
 type State = {
   // Input data from state
@@ -39,6 +39,7 @@ type State = {
 
   // Data generated for transaction
   transactionDetails: Map<string, string>;
+  roleKeyNameBase: string;
   roleKeyName: string;
 
   // Error capture and display
@@ -52,6 +53,7 @@ const newEmptyState = (): State => {
     appDetails: {},
     hasError: false,
     transactionDetails: new Map<string, string>(),
+    roleKeyNameBase: '',
     roleKeyName: '',
     txKeyAddress: new AccountId(),
     activeStep: 0,
@@ -61,41 +63,51 @@ const newEmptyState = (): State => {
 };
 
 export class ApplyController extends Controller<State, ITransport> {
-  protected currentOpeningId = -1
+  protected currentOpeningId = -1;
+  protected currentGroup: WorkingGroups | null = null;
 
-  constructor (transport: ITransport, initialState: State = newEmptyState()) {
+  constructor (
+    transport: ITransport,
+    initialState: State = newEmptyState()
+  ) {
     super(transport, initialState);
 
     this.transport.accounts().subscribe((keys) => this.updateAccounts(keys));
   }
 
+  protected parseGroup (group: string | undefined): WorkingGroups | undefined {
+    return AvailableGroups.find(availableGroup => availableGroup === group);
+  }
+
   protected updateAccounts (keys: keyPairDetails[]) {
     this.state.keypairs = keys;
     this.dispatch();
   }
 
-  findOpening (rawId: string | undefined) {
+  findOpening (rawId: string | undefined, rawGroup: string | undefined) {
     if (!rawId) {
       return this.onError('ApplyController: no ID provided in params');
     }
     const id = parseInt(rawId);
+    const group = this.parseGroup(rawGroup);
+
+    if (!group) {
+      return this.onError('ApplyController: invalid group');
+    }
 
-    if (this.currentOpeningId === id) {
+    if (this.currentOpeningId === id && this.currentGroup === group) {
       return;
     }
 
     Promise.all(
       [
-        this.transport.curationGroupOpening(id),
-        this.transport.openingApplicationRanks(id)
+        this.transport.groupOpening(group, id),
+        this.transport.openingApplicationRanks(group, id)
       ]
     )
       .then(
         ([opening, ranks]) => {
-          const hrt = opening.opening.parse_human_readable_text();
-          if (typeof hrt !== 'object') {
-            return this.onError('human_readable_text is not an object');
-          }
+          const hrt = opening.opening.parse_human_readable_text_with_fallback();
 
           this.state.role = hrt;
           this.state.applications = opening.applications;
@@ -112,7 +124,7 @@ export class ApplyController extends Controller<State, ITransport> {
             ? ProgressSteps.ConfirmStakes
             : ProgressSteps.ApplicationDetails;
 
-          this.state.roleKeyName = hrt.job.title + ' role key';
+          this.state.roleKeyNameBase = hrt.job.title + ' role key';
 
           // When everything is collected, update the view
           this.dispatch();
@@ -121,11 +133,13 @@ export class ApplyController extends Controller<State, ITransport> {
       .catch(
         (err: any) => {
           this.currentOpeningId = -1;
+          this.currentGroup = null;
           this.onError(err);
         }
       );
 
     this.currentOpeningId = id;
+    this.currentGroup = group;
   }
 
   setApplicationStake (b: Balance): void {
@@ -183,8 +197,22 @@ export class ApplyController extends Controller<State, ITransport> {
     return true;
   }
 
+  private updateRoleKeyName () {
+    let roleKeyNamePrefix = 0;
+    do {
+      this.state.roleKeyName = `${this.state.roleKeyNameBase}${(++roleKeyNamePrefix > 1 ? ` ${roleKeyNamePrefix}` : '')}`;
+    } while (this.state.keypairs?.some(k => (
+      k.shortName.toLowerCase() === this.state.roleKeyName.toLowerCase()
+    )));
+  }
+
   async makeApplicationTransaction (): Promise<number> {
-    return this.transport.applyToCuratorOpening(
+    if (!this.currentGroup || this.currentOpeningId < 0) {
+      throw new Error('Trying to apply to unfetched opening');
+    }
+    this.updateRoleKeyName();
+    return this.transport.applyToOpening(
+      this.currentGroup,
       this.currentOpeningId,
       this.state.roleKeyName,
       this.state.txKeyAddress.toString(),
@@ -197,14 +225,10 @@ export class ApplyController extends Controller<State, ITransport> {
 
 export const ApplyView = View<ApplyController, State>(
   (state, controller, params) => {
-    if (params.get('group') !== WorkingGroups.ContentCurators) {
-      return <h1>Applying not yet implemented for this group!</h1>;
-    }
-    controller.findOpening(params.get('id'));
+    controller.findOpening(params.get('id'), params.get('group'));
     return (
       <Container className="apply-flow">
         <div className="dimmer"></div>
-        // @ts-ignore
         <FlowModal
           role={state.role!}
           applications={state.applications!}

+ 2 - 0
pioneer/packages/joy-roles/src/flows/apply.elements.stories.tsx

@@ -1,3 +1,5 @@
+// TODO: FIXME: Remove the ts-nocheck and fix errors!
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-nocheck
 import React, { useState } from 'react';
 import { number, object, withKnobs } from '@storybook/addon-knobs';

+ 2 - 0
pioneer/packages/joy-roles/src/flows/apply.stories.tsx

@@ -1,3 +1,5 @@
+// TODO: FIXME: Remove the ts-nocheck and fix errors!
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-nocheck
 import React from 'react';
 import { number, object, select, text, withKnobs } from '@storybook/addon-knobs';

+ 55 - 101
pioneer/packages/joy-roles/src/flows/apply.tsx

@@ -172,17 +172,17 @@ export function FundSourceSelector (props: FundSourceSelectorProps & FundSourceC
   );
 }
 
-function rankIcon (place: number, slots: number): SemanticICONS {
-  if (place <= 1) {
-    return 'thermometer empty';
-  } else if (place <= (slots / 4)) {
-    return 'thermometer quarter';
-  } else if (place <= (slots / 2)) {
-    return 'thermometer half';
-  } else if (place > (slots / 2) && place < slots) {
+function rankIcon (estimatedSlot: number, slots: number): SemanticICONS {
+  if (estimatedSlot === 1) { // 1st place
+    return 'thermometer';
+  } else if (estimatedSlot <= (slots / 3)) { // Places 2-33 if slotsCount == 100
     return 'thermometer three quarters';
+  } else if (estimatedSlot <= (slots / 1.5)) { // Places 34-66 if slotsCount == 100
+    return 'thermometer half';
+  } else if (estimatedSlot <= slots) { // Places 67-100 if slotsCount == 100
+    return 'thermometer quarter';
   }
-  return 'thermometer';
+  return 'thermometer empty'; // Places >100 for slotsCount == 100
 }
 
 export type StakeRankSelectorProps = {
@@ -192,40 +192,28 @@ export type StakeRankSelectorProps = {
   step: Balance;
   otherStake: Balance;
   requirement: IStakeRequirement;
+  maxNumberOfApplications: number;
 }
 
 export function StakeRankSelector (props: StakeRankSelectorProps) {
   const slotCount = props.slots.length;
-  const [rank, setRank] = useState(1);
-  const minStake = props.requirement.value;
+  const minStake = props.maxNumberOfApplications && props.slots.length === props.maxNumberOfApplications
+    ? props.slots[0].sub(props.otherStake).addn(1) // Slots are ordered by stake ASC
+    : props.requirement.value;
+  const stakeSufficient = props.stake.gte(minStake);
 
   const ticks = [];
   for (let i = 0; i < slotCount; i++) {
     ticks.push(<div key={i} className="tick" style={{ width: (100 / slotCount) + '%' }}>{slotCount - i}</div>);
   }
 
-  const findRankValue = (newStake: Balance): number => {
-    if (newStake.add(props.otherStake).gt(props.slots[slotCount - 1])) {
-      return slotCount;
-    }
-
-    for (let i = slotCount; i--; i >= 0) {
-      if (newStake.add(props.otherStake).gt(props.slots[i])) {
-        return i + 1;
-      }
-    }
-
-    return 0;
-  };
+  let estimatedSlot = slotCount + 1;
+  props.slots.forEach(slotStake => props.stake.gt(slotStake.sub(props.otherStake)) && --estimatedSlot);
 
   const changeValue = (e: any, { value }: any) => {
     const newStake = new u128(value);
     props.setStake(newStake);
-    setRank(findRankValue(newStake));
   };
-  useEffect(() => {
-    props.setStake(props.slots[0]);
-  }, []);
 
   const slider = null;
   return (
@@ -238,21 +226,26 @@ export function StakeRankSelector (props: StakeRankSelectorProps) {
           type="number"
           step={slotCount > 1 ? props.step.toNumber() : 1}
           value={props.stake.toNumber() > 0 ? props.stake.toNumber() : 0}
-          min={props.slots.length > 0 ? props.slots[0].sub(props.otherStake).toNumber() : 0}
-          error={props.stake.lt(minStake)}
+          min={minStake}
+          error={!stakeSufficient}
         />
-        <Label size='large'>
-          <Icon name={rankIcon(rank, slotCount)} />
-          Estimated rank
-          <Label.Detail>{(slotCount + 1) - rank} / {slotCount}</Label.Detail>
-        </Label>
-        <Label size='large'>
+        { props.maxNumberOfApplications > 0 && (
+          <Label size='large'>
+            <Icon name={rankIcon(estimatedSlot, slotCount)} />
+            Estimated rank
+            <Label.Detail>{estimatedSlot} / {props.maxNumberOfApplications}</Label.Detail>
+          </Label>
+        ) }
+        <Label size='large' color={stakeSufficient ? 'green' : 'red'}>
           <Icon name="shield" />
           Your stake
           <Label.Detail>{formatBalance(props.stake)}</Label.Detail>
         </Label>
       </Container>
       {slider}
+      { !stakeSufficient && (
+        <Label color="red">Currently you need to stake at least {formatBalance(minStake)} to be considered for this position!</Label>
+      ) }
     </Container>
   );
 }
@@ -377,24 +370,17 @@ export type StageTransitionProps = {
   prevTransition: () => void;
 }
 
-export type ApplicationStatusProps = {
-  numberOfApplications: number;
-}
-
 type CaptureKeyAndPassphraseProps = {
   keyAddress: AccountId;
   setKeyAddress: (a: AccountId) => void;
-  keyPassphrase: string;
-  setKeyPassphrase: (p: string) => void;
-  minStake: Balance;
+  // keyPassphrase: string;
+  // setKeyPassphrase: (p: string) => void;
+  // minStake: Balance;
 }
 
 export type ConfirmStakesStageProps =
-  StakeRequirementProps &
   FundSourceSelectorProps &
-  ApplicationStatusProps &
-  StakeRankSelectorProps &
-  CaptureKeyAndPassphraseProps & {
+  Pick<StakeRankSelectorProps, 'slots' | 'step'> & {
     applications: OpeningStakeAndApplicationStatus;
     selectedApplicationStake: Balance;
     setSelectedApplicationStake: (b: Balance) => void;
@@ -426,7 +412,7 @@ export function ConfirmStakesStage (props: ConfirmStakesStageProps & StageTransi
   );
 }
 
-type StakeSelectorProps = ConfirmStakesStageProps & ApplicationStatusProps
+type StakeSelectorProps = ConfirmStakesStageProps;
 
 function ConfirmStakes (props: StakeSelectorProps) {
   if (bothStakesVariable(props.applications)) {
@@ -488,55 +474,25 @@ export type ConfirmStakes2UpProps = {
 }
 
 export function ConfirmStakes2Up (props: ConfirmStakes2UpProps) {
-  const [valid, setValid] = useState(true);
   const slotCount = props.slots.length;
-  const [rank, setRank] = useState(1);
-  const minStake = props.slots[0];
-  const [combined, setCombined] = useState(new u128(0));
-
-  const findRankValue = (newStake: Balance): number => {
-    if (slotCount === 0) {
-      return 0;
-    }
+  const { maxNumberOfApplications, requiredApplicationStake, requiredRoleStake } = props.applications;
+  const minStake = maxNumberOfApplications && props.slots.length === maxNumberOfApplications
+    ? props.slots[0].addn(1) // Slots are sorted by combined stake ASC
+    : requiredApplicationStake.value.add(requiredRoleStake.value);
+  const combined = Add(props.selectedApplicationStake, props.selectedRoleStake);
+  const valid = combined.gte(minStake);
 
-    if (newStake.gt(props.slots[slotCount - 1])) {
-      return slotCount;
-    }
-
-    for (let i = slotCount; i--; i >= 0) {
-      if (newStake.gt(props.slots[i])) {
-        return i + 1;
-      }
-    }
-
-    return 0;
-  };
-
-  // Watch stake values
-  useEffect(() => {
-    const newCombined = Add(props.selectedApplicationStake, props.selectedRoleStake);
-    setCombined(newCombined);
-  },
-  [props.selectedApplicationStake, props.selectedRoleStake]
-  );
-
-  useEffect(() => {
-    setRank(findRankValue(combined));
-    if (slotCount > 0) {
-      setValid(combined.gte(minStake));
-    }
-  },
-  [combined]
-  );
+  let estimatedSlot = slotCount + 1;
+  props.slots.forEach(slotStake => combined.gt(slotStake) && --estimatedSlot);
 
   const ticks = [];
   for (let i = 0; i < slotCount; i++) {
-    ticks.push(<div key={i} className="tick" style={{ width: (100 / slotCount) + '%' }}>{slotCount - i}</div>);
+    ticks.push(<div key={i} className="tick" style={{ width: (100 / slotCount) + '%' }}>{i + 1}</div>);
   }
 
-  const tickLabel = <div className="ui pointing below label" style={{ left: ((100 / slotCount) * rank) + '%' }}>
+  const tickLabel = <div className="ui pointing below label" style={{ left: ((100 / slotCount) * (estimatedSlot - 1)) + '%' }}>
     Your rank
-    <div className="detail">{(slotCount - rank) + 1}/{props.applications.maxNumberOfApplications}</div>
+    <div className="detail">{estimatedSlot}/{props.applications.maxNumberOfApplications}</div>
   </div>;
 
   let tickContainer = null;
@@ -630,11 +586,13 @@ export function ConfirmStakes2Up (props: ConfirmStakes2UpProps) {
                   Your current combined stake
                   <Label.Detail>{formatBalance(new u128(props.selectedApplicationStake.add(props.selectedRoleStake)))}</Label.Detail>
                 </Label>
-                <Label color='grey'>
-                  <Icon name={rankIcon(rank, slotCount)} />
-                  Estimated rank
-                  <Label.Detail>{(slotCount - rank) + 1}/{props.applications.maxNumberOfApplications}</Label.Detail>
-                </Label>
+                { maxNumberOfApplications > 0 && (
+                  <Label color='grey'>
+                    <Icon name={rankIcon(estimatedSlot, slotCount)} />
+                    Estimated rank
+                    <Label.Detail>{estimatedSlot}/{props.applications.maxNumberOfApplications}</Label.Detail>
+                  </Label>
+                ) }
               </Grid.Column>
             </Grid.Row>
           </Grid>
@@ -677,7 +635,8 @@ function StakeRankMiniSelector (props: StakeRankMiniSelectorProps) {
   );
 }
 
-type CaptureStake1UpProps = ApplicationStatusProps & {
+type CaptureStake1UpProps = {
+  numberOfApplications: number;
   name: string;
   stakeReturnPolicy: string;
   colour: string;
@@ -710,11 +669,6 @@ function CaptureStake1Up (props: CaptureStake1UpProps) {
     );
   }
 
-  // Set default value
-  useEffect(() => {
-    props.setValue(props.requirement.value);
-  }, []);
-
   let slider = null;
   let atLeast = null;
   if (props.requirement.atLeast()) {
@@ -1030,7 +984,7 @@ export function DoneStage (props: DoneStageProps) {
   );
 }
 
-export type FlowModalProps = ConfirmStakesStageProps & FundSourceSelectorProps & {
+export type FlowModalProps = Pick<StakeRankSelectorProps, 'slots' | 'step'> & FundSourceSelectorProps & {
   role: GenericJoyStreamRoleSchema;
   applications: OpeningStakeAndApplicationStatus;
   hasConfirmStep: boolean;

+ 3 - 3
pioneer/packages/joy-roles/src/index.tsx

@@ -86,9 +86,9 @@ export const App: React.FC<Props> = (props: Props) => {
         />
       </header>
       <Switch>
-        <Route path={`${basePath}/opportunities/:group/:id/apply`} render={(props) => renderViewComponent(ApplyView(applyCtrl), props)} />
-        <Route path={`${basePath}/opportunities/:group/:id`} render={(props) => renderViewComponent(OpportunityView(oppCtrl), props)} />
-        <Route path={`${basePath}/opportunities/:group`} render={(props) => renderViewComponent(OpportunitiesView(oppsCtrl), props)} />
+        <Route path={`${basePath}/opportunities/:group/:id([0-9]+)/apply`} render={(props) => renderViewComponent(ApplyView(applyCtrl), props)} />
+        <Route path={`${basePath}/opportunities/:group/:id([0-9]+)`} render={(props) => renderViewComponent(OpportunityView(oppCtrl), props)} />
+        <Route path={`${basePath}/opportunities/:group/:lead(lead)?`} render={(props) => renderViewComponent(OpportunitiesView(oppsCtrl), props)} />
         <Route path={`${basePath}/opportunities`} render={() => renderViewComponent(OpportunitiesView(oppsCtrl))} />
         <Route path={`${basePath}/my-roles`} render={() => renderViewComponent(MyRolesView(myRolesCtrl))} />
         <Route path={`${basePath}/admin`} render={() => renderViewComponent(AdminView(adminCtrl))} />

+ 22 - 0
pioneer/packages/joy-roles/src/mocks.ts

@@ -3,6 +3,14 @@ import AccountId from '@polkadot/types/primitive/Generic/AccountId';
 
 import { ActorInRole, IProfile, EntryMethod } from '@joystream/types/members';
 
+import {
+  AcceptingApplications,
+  ActiveOpeningStage,
+  OpeningStage,
+  ActiveOpeningStageVariant,
+  ApplicationId
+} from '@joystream/types/hiring';
+
 export function mockProfile (name: string, avatar_uri = ''): IProfile {
   return {
     handle: new Text(name),
@@ -18,3 +26,17 @@ export function mockProfile (name: string, avatar_uri = ''): IProfile {
     roles: new Vec<ActorInRole>(ActorInRole)
   };
 }
+
+export const mockStage = new OpeningStage({
+  Active: new ActiveOpeningStageVariant({
+    applications_added: new (Vec.with(ApplicationId))([]),
+    active_application_count: new u32(0),
+    unstaking_application_count: new u32(0),
+    deactivated_application_count: new u32(0),
+    stage: new ActiveOpeningStage({
+      AcceptingApplications: new AcceptingApplications({
+        started_accepting_applicants_at_block: new u32(100)
+      })
+    })
+  })
+});

+ 130 - 273
pioneer/packages/joy-roles/src/tabs/Admin.controller.tsx

@@ -1,14 +1,14 @@
-// @ts-nocheck
 import React, { useState } from 'react';
 import { Link } from 'react-router-dom';
 import { formatBalance } from '@polkadot/util';
 
 import { ApiPromise } from '@polkadot/api';
+import { GenericAccountId, Option, Text, Vec, u32, u128 } from '@polkadot/types';
 import { Balance } from '@polkadot/types/interfaces';
-import { GenericAccountId, Option, u32, u64, u128, Set, Text, Vec } from '@polkadot/types';
 
 import { SingleLinkedMapEntry, Controller, View } from '@polkadot/joy-utils/index';
 import { MyAccountProvider, useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
 
 import {
   Accordion,
@@ -35,11 +35,12 @@ import {
   ApplicationStage,
   ActivateOpeningAt,
   ApplicationRationingPolicy,
-  CurrentBlock, ExactBlock,
+  CurrentBlock,
   Opening,
   OpeningStage,
   StakingPolicy,
-  StakingAmountLimitModeKeys
+  StakingAmountLimitModeKeys,
+  StakingAmountLimitMode
 } from '@joystream/types/hiring';
 
 import {
@@ -49,13 +50,10 @@ import {
 
 import { Stake, StakeId } from '@joystream/types/stake';
 
-import {
-  GenericJoyStreamRoleSchema
-} from '@joystream/types/hiring/schemas/role.schema.typings';
 import {
   CuratorApplication, CuratorApplicationId,
   CuratorOpening,
-  OpeningPolicyCommitment, IOpeningPolicyCommitment
+  IOpeningPolicyCommitment, CuratorOpeningId
 } from '@joystream/types/content-working-group';
 
 import {
@@ -68,7 +66,7 @@ import {
   openingDescription
 } from '../openingStateMarkup';
 
-import { Add, Sort, Sum, Zero } from '../balances';
+import { Add, Zero } from '../balances';
 
 type ids = {
   curatorId: number;
@@ -92,6 +90,18 @@ type opening = ids & {
   classification: OpeningStageClassification;
 }
 
+// Only max_review_period_length is not optional, so other fields can be "undefined"
+type policyDescriptor = Pick<IOpeningPolicyCommitment, 'max_review_period_length'> & Partial<IOpeningPolicyCommitment>;
+
+type stakingFieldName = 'application_staking_policy' | 'role_staking_policy';
+
+type openingDescriptor = {
+  title: string;
+  start: ActivateOpeningAt;
+  policy: policyDescriptor;
+  text: Text;
+}
+
 type State = {
   openings: Map<number, opening>;
   currentDescriptor: openingDescriptor;
@@ -143,6 +153,27 @@ function newHRT (title: string): Text {
   );
 }
 
+const createRationingPolicyOpt = (maxApplicants: number) =>
+  new Option<ApplicationRationingPolicy>(
+    ApplicationRationingPolicy,
+    new ApplicationRationingPolicy({
+      max_active_applicants: new u32(maxApplicants)
+    })
+  );
+const createStakingPolicyOpt = (amount: number, amount_mode: StakingAmountLimitMode): Option<StakingPolicy> =>
+  new Option(
+    StakingPolicy,
+    new StakingPolicy({
+      amount: new u128(amount),
+      amount_mode,
+      crowded_out_unstaking_period_length: new Option('BlockNumber', null),
+      review_period_expired_unstaking_period_length: new Option('BlockNumber', null)
+    })
+  );
+
+const STAKING_MODE_EXACT = new StakingAmountLimitMode(StakingAmountLimitModeKeys.Exact);
+const STAKING_MODE_AT_LEAST = new StakingAmountLimitMode(StakingAmountLimitModeKeys.AtLeast);
+
 const stockOpenings: openingDescriptor[] = [
   {
     title: 'Test config A: no application stake, no role stake, no applicant limit',
@@ -165,13 +196,7 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration C')
   },
@@ -180,19 +205,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_rationing_policy: new Option<ApplicationRationingPolicy>(
-        ApplicationRationingPolicy,
-        new ApplicationRationingPolicy({
-          max_active_applicants: new u32(10)
-        })
-      ),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      application_rationing_policy: createRationingPolicyOpt(10),
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration D')
   },
@@ -201,13 +215,7 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration E')
   },
@@ -216,19 +224,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_rationing_policy: new Option<ApplicationRationingPolicy>(
-        ApplicationRationingPolicy,
-        new ApplicationRationingPolicy({
-          max_active_applicants: new u32(10)
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      application_rationing_policy: createRationingPolicyOpt(10),
+      role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration F')
   },
@@ -237,13 +234,7 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration G')
   },
@@ -252,19 +243,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_rationing_policy: new Option<ApplicationRationingPolicy>(
-        ApplicationRationingPolicy,
-        new ApplicationRationingPolicy({
-          max_active_applicants: new u32(10)
-        })
-      ),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      application_rationing_policy: createRationingPolicyOpt(10),
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration H')
   },
@@ -273,13 +253,7 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration I')
   },
@@ -288,19 +262,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_rationing_policy: new Option<ApplicationRationingPolicy>(
-        ApplicationRationingPolicy,
-        new ApplicationRationingPolicy({
-          max_active_applicants: new u32(10)
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      application_rationing_policy: createRationingPolicyOpt(10),
+      role_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration J')
   },
@@ -309,20 +272,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration K')
   },
@@ -331,26 +282,9 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_rationing_policy: new Option<ApplicationRationingPolicy>(
-        ApplicationRationingPolicy,
-        new ApplicationRationingPolicy({
-          max_active_applicants: new u32(10)
-        })
-      ),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      application_rationing_policy: createRationingPolicyOpt(10),
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration L')
   },
@@ -359,20 +293,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration M')
   },
@@ -381,26 +303,9 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_rationing_policy: new Option<ApplicationRationingPolicy>(
-        ApplicationRationingPolicy,
-        new ApplicationRationingPolicy({
-          max_active_applicants: new u32(10)
-        })
-      ),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      application_rationing_policy: createRationingPolicyOpt(10),
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration N')
   },
@@ -409,20 +314,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration O')
   },
@@ -437,20 +330,8 @@ const stockOpenings: openingDescriptor[] = [
           max_active_applicants: new u32(10)
         })
       ),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      )
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_EXACT),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_AT_LEAST)
     },
     text: newHRT('Test configuration P')
   },
@@ -459,20 +340,8 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration Q')
   },
@@ -481,26 +350,9 @@ const stockOpenings: openingDescriptor[] = [
     start: new ActivateOpeningAt(CurrentBlock),
     policy: {
       max_review_period_length: new u32(99999),
-      application_rationing_policy: new Option<ApplicationRationingPolicy>(
-        ApplicationRationingPolicy,
-        new ApplicationRationingPolicy({
-          max_active_applicants: new u32(10)
-        })
-      ),
-      application_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(100),
-          amount_mode: StakingAmountLimitModeKeys.AtLeast
-        })
-      ),
-      role_staking_policy: new Option<StakingPolicy>(
-        StakingPolicy,
-        new StakingPolicy({
-          amount: new u128(200),
-          amount_mode: StakingAmountLimitModeKeys.Exact
-        })
-      )
+      application_rationing_policy: createRationingPolicyOpt(10),
+      application_staking_policy: createStakingPolicyOpt(100, STAKING_MODE_AT_LEAST),
+      role_staking_policy: createStakingPolicyOpt(200, STAKING_MODE_EXACT)
     },
     text: newHRT('Test configuration R')
   }
@@ -509,35 +361,29 @@ const stockOpenings: openingDescriptor[] = [
 const newEmptyState = (): State => {
   return {
     openings: new Map<number, opening>(),
-    openingDescriptor: stockOpenings[0],
+    currentDescriptor: stockOpenings[0],
     modalOpen: false
   };
 };
 
-// TODO: Make a list of stock openings
-type openingDescriptor = {
-  title: string;
-  start: ActivateOpeningAt;
-  policy: IOpeningPolicyCommitment;
-  text: Text;
-}
-
 export class AdminController extends Controller<State, ITransport> {
   api: ApiPromise
   constructor (transport: ITransport, api: ApiPromise, initialState: State = newEmptyState()) {
     super(transport, initialState);
     this.api = api;
-    this.state.openingDescriptor = stockOpenings[0];
+    this.state.currentDescriptor = stockOpenings[0];
     this.updateState();
   }
 
   newOpening (creatorAddress: string, desc: openingDescriptor) {
     const tx = this.api.tx.contentWorkingGroup.addCuratorOpening(
       desc.start,
-      new OpeningPolicyCommitment(desc.policy),
+      desc.policy,
       desc.text
-    );
+    ) as unknown as SubmittableExtrinsic;
 
+    // FIXME: That's a bad way to send extrinsic in Pioneer (without "queueExtrinsic" etc.)
+    // and probably the reason why it always appears as succesful
     tx.signAndSend(creatorAddress, ({ events = [], status }) => {
       if (status.isFinalized) {
         this.updateState();
@@ -553,7 +399,9 @@ export class AdminController extends Controller<State, ITransport> {
   }
 
   startAcceptingApplications (creatorAddress: string, id = 0) {
-    const tx = this.api.tx.contentWorkingGroup.acceptCuratorApplications(new u32(id));
+    const tx = this.api.tx.contentWorkingGroup.acceptCuratorApplications(id);
+    // FIXME: That's a bad way to send extrinsic in Pioneer (without "queueExtrinsic" etc.)
+    // and probably the reason why it always appears as succesful
     tx.signAndSend(creatorAddress, ({ events = [], status }) => {
       if (status.isFinalized) {
         this.updateState();
@@ -576,12 +424,14 @@ export class AdminController extends Controller<State, ITransport> {
     }
     const tx = this.api.tx.contentWorkingGroup.applyOnCuratorOpening(
       membershipIds[0],
-      new u32(openingId),
+      openingId,
       new GenericAccountId(creatorAddress),
       new Option(u128, 400),
       new Option(u128, 400),
       new Text('This is my application')
-    );
+    ) as unknown as SubmittableExtrinsic;
+    // FIXME: That's a bad way to send extrinsic in Pioneer (without "queueExtrinsic" etc.)
+    // and probably the reason why it always appears as succesful
     tx.signAndSend(creatorAddress, ({ events = [], status }) => {
       if (status.isFinalized) {
         this.updateState();
@@ -597,9 +447,9 @@ export class AdminController extends Controller<State, ITransport> {
   }
 
   beginApplicantReview (creatorAddress: string, openingId: number) {
-    const tx = this.api.tx.contentWorkingGroup.beginCuratorApplicantReview(
-      new u32(openingId)
-    );
+    const tx = this.api.tx.contentWorkingGroup.beginCuratorApplicantReview(openingId);
+    // FIXME: That's a bad way to send extrinsic in Pioneer (without "queueExtrinsic" etc.)
+    // and probably the reason why it always appears as succesful
     tx.signAndSend(creatorAddress, ({ events = [], status }) => {
       if (status.isFinalized) {
         this.updateState();
@@ -616,10 +466,12 @@ export class AdminController extends Controller<State, ITransport> {
 
   acceptCuratorApplications (creatorAddress: string, openingId: number, applications: Array<number>) {
     const tx = this.api.tx.contentWorkingGroup.fillCuratorOpening(
-      new u32(openingId),
+      openingId,
       applications,
       null
-    );
+    ) as unknown as SubmittableExtrinsic;
+    // FIXME: That's a bad way to send extrinsic in Pioneer (without "queueExtrinsic" etc.)
+    // and probably the reason why it always appears as succesful
     tx.signAndSend(creatorAddress, ({ events = [], status }) => {
       if (status.isFinalized) {
         this.updateState();
@@ -634,8 +486,8 @@ export class AdminController extends Controller<State, ITransport> {
     });
   }
 
-  protected async profile (id: MemberId): Promise<Profile> {
-    return (await this.api.query.members.memberProfile(id)) as Profile;
+  protected async profile (id: MemberId): Promise<Option<Profile>> {
+    return (await this.api.query.members.memberProfile(id)) as Option<Profile>;
   }
 
   protected async stakeValue (stakeId: StakeId): Promise<Balance> {
@@ -667,14 +519,14 @@ export class AdminController extends Controller<State, ITransport> {
   async updateState () {
     this.state.openings = new Map<number, opening>();
 
-    const nextOpeningId = await this.api.query.contentWorkingGroup.nextCuratorOpeningId() as u64;
+    const nextOpeningId = await this.api.query.contentWorkingGroup.nextCuratorOpeningId() as CuratorOpeningId;
     for (let i = nextOpeningId.toNumber() - 1; i >= 0; i--) {
       const curatorOpening = new SingleLinkedMapEntry<CuratorOpening>(
         CuratorOpening,
         await this.api.query.contentWorkingGroup.curatorOpeningById(i)
       );
 
-      const openingId = curatorOpening.value.getField<u32>('opening_id');
+      const openingId = curatorOpening.value.opening_id;
 
       const baseOpening = new SingleLinkedMapEntry<Opening>(
         Opening,
@@ -683,11 +535,8 @@ export class AdminController extends Controller<State, ITransport> {
         )
       );
 
-      let title = 'unknown (JSON schema invalid)';
-      const hrt = baseOpening.value.parse_human_readable_text();
-      if (typeof hrt === 'object') {
-        title = (hrt).job.title;
-      }
+      const hrt = baseOpening.value.parse_human_readable_text_with_fallback();
+      const title = hrt.job.title;
 
       this.state.openings.set(i, {
         openingId: openingId.toNumber(),
@@ -699,14 +548,14 @@ export class AdminController extends Controller<State, ITransport> {
       });
     }
 
-    const nextAppid = await this.api.query.contentWorkingGroup.nextCuratorApplicationId() as u64;
+    const nextAppid = await this.api.query.contentWorkingGroup.nextCuratorApplicationId() as CuratorApplicationId;
     for (let i = 0; i < nextAppid.toNumber(); i++) {
       const cApplication = new SingleLinkedMapEntry<CuratorApplication>(
         CuratorApplication,
         await this.api.query.contentWorkingGroup.curatorApplicationById(i)
       );
 
-      const appId = cApplication.value.getField<u32>('application_id');
+      const appId = cApplication.value.application_id;
       const baseApplications = new SingleLinkedMapEntry<Application>(
         Application,
         await this.api.query.hiring.applicationById(
@@ -715,16 +564,16 @@ export class AdminController extends Controller<State, ITransport> {
       );
 
       const curatorOpening = this.state.openings.get(
-        cApplication.value.getField<u32>('curator_opening_id').toNumber()
+        cApplication.value.curator_opening_id.toNumber()
       ) as opening;
 
       curatorOpening.applications.push({
         openingId: appId.toNumber(),
         curatorId: i,
-        stage: baseApplications.value.getField<ApplicationStage>('stage'),
-        account: cApplication.value.getField('role_account').toString(),
-        memberId: cApplication.value.getField<u32>('member_id').toNumber(),
-        profile: (await this.profile(cApplication.value.getField<u32>('member_id'))).unwrap(),
+        stage: baseApplications.value.stage,
+        account: cApplication.value.role_account_id.toString(),
+        memberId: cApplication.value.member_id.toNumber(),
+        profile: (await this.profile(cApplication.value.member_id)).unwrap(),
         applicationStake: await this.applicationStake(baseApplications.value),
         roleStake: await this.roleStake(baseApplications.value),
         application: baseApplications.value
@@ -736,7 +585,7 @@ export class AdminController extends Controller<State, ITransport> {
 
   showNewOpeningModal (desc: openingDescriptor) {
     this.state.modalOpen = true;
-    this.state.openingDescriptor = desc;
+    this.state.currentDescriptor = desc;
     this.dispatch();
   }
 
@@ -772,7 +621,7 @@ export const AdminView = View<AdminController, State>(
               <Modal open={state.modalOpen} onClose={() => controller.closeModal()}>
                 <Modal.Content image>
                   <Modal.Description>
-                    <NewOpening desc={state.openingDescriptor} fn={(desc) => controller.newOpening(address, desc)} />
+                    <NewOpening desc={state.currentDescriptor} fn={(desc) => controller.newOpening(address, desc)} />
                   </Modal.Description>
                 </Modal.Content>
               </Modal>
@@ -832,8 +681,8 @@ const NewOpening = (props: NewOpeningProps) => {
 
   const [policy, setPolicy] = useState(props.desc.policy);
 
-  const onChangePolicyField = (fieldName, value) => {
-    const newState = Object.assign({}, policy);
+  const onChangePolicyField = <PolicyKey extends keyof policyDescriptor>(fieldName: PolicyKey, value: policyDescriptor[PolicyKey]) => {
+    const newState = { ...policy };
     newState[fieldName] = value;
     setPolicy(newState);
   };
@@ -863,24 +712,31 @@ const NewOpening = (props: NewOpeningProps) => {
     }
   ];
 
-  const changeStakingMode = (fieldName: string, mode: string, stakeValue: number) => {
-    const value = new Option<StakingPolic>(
-      StakingPolicy,
-      new StakingPolicy({
-        amount: new u128(stakeValue),
-        amount_mode: mode === '' && policy[fieldName].isSome ? policy[fieldName].type : mode
-      })
+  const changeStakingMode = (
+    fieldName: stakingFieldName,
+    mode: StakingAmountLimitModeKeys | '',
+    stakeValue: number
+  ) => {
+    if (mode === '') {
+      const policyField = policy[fieldName];
+      mode = policyField && policyField.isSome
+        ? (policyField.unwrap().amount_mode.type as StakingAmountLimitModeKeys)
+        : StakingAmountLimitModeKeys.Exact; // Default
+    }
+    const value = createStakingPolicyOpt(
+      stakeValue,
+      mode === StakingAmountLimitModeKeys.Exact ? STAKING_MODE_EXACT : STAKING_MODE_AT_LEAST
     );
     onChangePolicyField(fieldName, value);
   };
 
-  const onStakeModeCheckboxChange = (fn: (v: boolean) => void, fieldName: string, checked: boolean, stakeValue: number) => {
+  const onStakeModeCheckboxChange = (fn: (v: boolean) => void, fieldName: stakingFieldName, checked: boolean, stakeValue: number) => {
     fn(checked);
 
     if (checked) {
       changeStakingMode(fieldName, StakingAmountLimitModeKeys.AtLeast, stakeValue);
     } else {
-      onChangePolicyField(fieldName, null);
+      onChangePolicyField(fieldName, undefined);
     }
   };
 
@@ -890,7 +746,8 @@ const NewOpening = (props: NewOpeningProps) => {
     props.fn({
       start: start,
       policy: policy,
-      text: new Text(text)
+      text: new Text(text),
+      title: ''
     });
   };
 
@@ -932,13 +789,13 @@ const NewOpening = (props: NewOpeningProps) => {
               selection
               onChange={(e, { value }: any) => changeStakingMode('application_staking_policy', value, 0)}
               options={stakeLimitOptions}
-              value={policy.application_staking_policy.unwrap().amount_mode.type}
+              value={policy.application_staking_policy?.unwrap().amount_mode.type}
             />
 
             <label>Stake value</label>
             <Input
               type="number"
-              value={policy.application_staking_policy.unwrap().amount.toNumber()}
+              value={policy.application_staking_policy?.unwrap().amount.toNumber()}
               onChange={(e: any, { value }: any) => changeStakingMode('application_staking_policy', '', value)}
             />
           </Message>
@@ -955,13 +812,13 @@ const NewOpening = (props: NewOpeningProps) => {
               selection
               onChange={(e, { value }: any) => changeStakingMode('role_staking_policy', value, 0)}
               options={stakeLimitOptions}
-              value={policy.role_staking_policy.unwrap().amount_mode.type}
+              value={policy.role_staking_policy?.unwrap().amount_mode.type}
             />
 
             <label>Stake value</label>
             <Input
               type="number"
-              value={policy.role_staking_policy.unwrap().amount.toNumber()}
+              value={policy.role_staking_policy?.unwrap().amount.toNumber()}
               onChange={(e: any, { value }: any) => changeStakingMode('role_staking_policy', '', value)}
             />
           </Message>

+ 10 - 10
pioneer/packages/joy-roles/src/tabs/MyRoles.controller.tsx

@@ -10,14 +10,14 @@ import {
 
 type State = {
   applications: OpeningApplication[];
-  currentCurationRoles: ActiveRoleWithCTAs[];
+  currentRoles: ActiveRoleWithCTAs[];
   myAddress: string;
 }
 
 const newEmptyState = (): State => {
   return {
     applications: [],
-    currentCurationRoles: [],
+    currentRoles: [],
     myAddress: ''
   };
 };
@@ -34,18 +34,18 @@ export class MyRolesController extends Controller<State, ITransport> {
   }
 
   protected async updateApplications (myAddress: string) {
-    this.state.applications = await this.transport.openingApplications(myAddress);
+    this.state.applications = await this.transport.openingApplicationsByAddress(myAddress);
     this.dispatch();
   }
 
   protected async updateCurationGroupRoles (myAddress: string) {
-    const roles = await this.transport.myCurationGroupRoles(myAddress);
-    this.state.currentCurationRoles = roles.map(role => ({
+    const roles = await this.transport.myRoles(myAddress);
+    this.state.currentRoles = roles.map(role => ({
       ...role,
       CTAs: [
         {
           title: 'Leave role',
-          callback: (rationale: string) => { this.leaveCurationRole(role, rationale); }
+          callback: (rationale: string) => { this.leaveRole(role, rationale); }
         }
       ]
     })
@@ -53,19 +53,19 @@ export class MyRolesController extends Controller<State, ITransport> {
     this.dispatch();
   }
 
-  leaveCurationRole (role: ActiveRole, rationale: string) {
-    this.transport.leaveCurationRole(this.state.myAddress, role.curatorId.toNumber(), rationale);
+  leaveRole (role: ActiveRole, rationale: string) {
+    this.transport.leaveRole(role.group, this.state.myAddress, role.workerId.toNumber(), rationale);
   }
 
   cancelApplication (application: OpeningApplication) {
-    this.transport.withdrawCuratorApplication(this.state.myAddress, application.id);
+    this.transport.withdrawApplication(application.meta.group, this.state.myAddress, application.id);
   }
 }
 
 export const MyRolesView = View<MyRolesController, State>(
   (state, controller) => (
     <Container className="my-roles">
-      <CurrentRoles currentRoles={state.currentCurationRoles} />
+      <CurrentRoles currentRoles={state.currentRoles} />
       <Applications applications={state.applications} cancelCallback={(a) => controller.cancelApplication(a)} />
     </Container>
   )

+ 12 - 9
pioneer/packages/joy-roles/src/tabs/MyRoles.elements.stories.tsx

@@ -31,6 +31,7 @@ import {
 } from './Opportunities.stories';
 
 import { CuratorId } from '@joystream/types/content-working-group';
+import { WorkingGroups, workerRoleNameByGroup } from '../working_groups';
 
 export default {
   title: 'Roles / Components / My roles tab / Elements',
@@ -45,10 +46,11 @@ export function CurrentRolesFragment () {
   const props: CurrentRolesProps = {
     currentRoles: [
       {
-        curatorId: new CuratorId(1),
-        name: 'Storage provider',
+        workerId: new CuratorId(1),
+        name: workerRoleNameByGroup[WorkingGroups.StorageProviders],
         reward: new u128(321),
         stake: new u128(100),
+        group: WorkingGroups.StorageProviders,
         CTAs: [
           {
             title: 'Unstake',
@@ -57,11 +59,12 @@ export function CurrentRolesFragment () {
         ]
       },
       {
-        curatorId: new CuratorId(1),
+        workerId: new CuratorId(1),
         name: 'Some other role',
         url: 'some URL',
         reward: new u128(321),
         stake: new u128(12343200),
+        group: WorkingGroups.ContentCurators,
         CTAs: [
           {
             title: 'Leave role',
@@ -164,7 +167,7 @@ const permutations: (ApplicationProps & TestProps)[] = [
     id: 1,
     meta: {
       id: '1',
-      group: 'group-name'
+      group: WorkingGroups.ContentCurators
     },
     stage: {
       state: OpeningState.AcceptingApplications,
@@ -184,7 +187,7 @@ const permutations: (ApplicationProps & TestProps)[] = [
     id: 1,
     meta: {
       id: '1',
-      group: 'group-name'
+      group: WorkingGroups.ContentCurators
     },
     stage: {
       state: OpeningState.AcceptingApplications,
@@ -204,7 +207,7 @@ const permutations: (ApplicationProps & TestProps)[] = [
     id: 1,
     meta: {
       id: '1',
-      group: 'group-name'
+      group: WorkingGroups.ContentCurators
     },
     stage: {
       state: OpeningState.InReview,
@@ -226,7 +229,7 @@ const permutations: (ApplicationProps & TestProps)[] = [
     id: 1,
     meta: {
       id: '1',
-      group: 'group-name'
+      group: WorkingGroups.ContentCurators
     },
     stage: {
       state: OpeningState.InReview,
@@ -248,7 +251,7 @@ const permutations: (ApplicationProps & TestProps)[] = [
     id: 1,
     meta: {
       id: '1',
-      group: 'group-name'
+      group: WorkingGroups.ContentCurators
     },
     stage: {
       state: OpeningState.Complete,
@@ -268,7 +271,7 @@ const permutations: (ApplicationProps & TestProps)[] = [
     id: 1,
     meta: {
       id: '1',
-      group: 'group-name'
+      group: WorkingGroups.ContentCurators
     },
     stage: {
       state: OpeningState.Cancelled,

+ 16 - 4
pioneer/packages/joy-roles/src/tabs/MyRoles.tsx

@@ -21,8 +21,6 @@ import { u128 } from '@polkadot/types';
 import { Balance } from '@polkadot/types/interfaces';
 
 import { Loadable } from '@polkadot/joy-utils/index';
-
-import { GenericJoyStreamRoleSchema } from '@joystream/types/hiring/schemas/role.schema.typings';
 import { Opening } from '@joystream/types/hiring';
 
 import {
@@ -35,6 +33,10 @@ import {
 import { CancelledReason, OpeningStageClassification, OpeningState } from '../classifiers';
 import { OpeningMetadata } from '../OpeningMetadata';
 import { CuratorId } from '@joystream/types/content-working-group';
+import { WorkerId } from '@joystream/types/working-group';
+import _ from 'lodash';
+import styled from 'styled-components';
+import { WorkingGroups } from '../working_groups';
 
 type CTACallback = (rationale: string) => void
 
@@ -102,9 +104,10 @@ function RoleName (props: NameAndURL) {
 }
 
 export interface ActiveRole extends NameAndURL {
-  curatorId: CuratorId;
+  workerId: CuratorId | WorkerId;
   reward: Balance;
   stake: Balance;
+  group: WorkingGroups;
 }
 
 export interface ActiveRoleWithCTAs extends ActiveRole {
@@ -379,14 +382,20 @@ function CancelButton (props: ApplicationProps) {
   );
 }
 
+const ApplicationLabel = styled(Label)`
+  margin-left: 1em !important;
+  border: 1px solid #999 !important;
+`;
+
 export function Application (props: ApplicationProps) {
   let countdown = null;
   if (props.stage.state === OpeningState.InReview) {
     countdown = <OpeningBodyReviewInProgress {...props.stage} />;
   }
 
-  const application = props.opening.parse_human_readable_text() as GenericJoyStreamRoleSchema;
+  const application = props.opening.parse_human_readable_text_with_fallback();
   const appState = applicationState(props);
+  const isLeadApplication = props.meta.type?.isOfType('Leader');
 
   let CTA = null;
   if (appState === ApplicationState.Positive && props.stage.state !== OpeningState.Complete) {
@@ -400,6 +409,9 @@ export function Application (props: ApplicationProps) {
         <Label.Detail className="right">
           {openingIcon(props.stage.state)}
           {openingDescription(props.stage.state)}
+          <ApplicationLabel>
+            {_.startCase(props.meta.group) + (isLeadApplication ? ' Lead' : '')}
+          </ApplicationLabel>
         </Label.Detail>
       </Label>
       <Grid columns="equal">

+ 1 - 0
pioneer/packages/joy-roles/src/tabs/Opportunities.controller.tsx

@@ -42,6 +42,7 @@ export const OpportunitiesView = View<OpportunitiesController, State>(
   (state, controller, params) => (
     <OpeningsView
       group={AvailableGroups.includes(params.get('group') as any) ? params.get('group') as WorkingGroups : undefined}
+      lead={!!params.get('lead')}
       openings={state.opportunities}
       block_time_in_seconds={state.blockTime}
       member_id={state.memberId}

+ 2 - 1
pioneer/packages/joy-roles/src/tabs/Opportunities.elements.stories.tsx

@@ -22,6 +22,7 @@ import { OpeningMetadata } from '../OpeningMetadata';
 
 import 'semantic-ui-css/semantic.min.css';
 import '@polkadot/joy-roles/index.sass';
+import { WorkingGroups } from '../working_groups';
 
 export default {
   title: 'Roles / Components / Opportunities groups tab / Elements',
@@ -34,7 +35,7 @@ type TestProps = {
 
 const meta: OpeningMetadata = {
   id: '1',
-  group: 'group-name'
+  group: WorkingGroups.ContentCurators
 };
 
 export function OpeningHeaderByState () {

+ 4 - 8
pioneer/packages/joy-roles/src/tabs/Opportunities.stories.tsx

@@ -7,11 +7,10 @@ import { Balance } from '@polkadot/types/interfaces';
 
 import {
   Opening,
-  AcceptingApplications,
-  ActiveOpeningStage,
   ApplicationRationingPolicy,
   StakingPolicy
 } from '@joystream/types/hiring';
+import { mockStage } from '../mocks';
 import {
   OpeningView,
   OpeningStakeAndApplicationStatus
@@ -27,6 +26,7 @@ import { OpeningMetadata } from '../OpeningMetadata';
 
 import 'semantic-ui-css/semantic.min.css';
 import '@polkadot/joy-roles/index.sass';
+import { WorkingGroups } from '../working_groups';
 
 export default {
   title: 'Roles / Components / Opportunities groups tab',
@@ -58,11 +58,7 @@ export function newMockHumanReadableText (obj: any) {
 
 export const opening = new Opening({
   created: new u32(100),
-  stage: new ActiveOpeningStage({
-    acceptingApplications: new AcceptingApplications({
-      started_accepting_applicants_at_block: new u32(100)
-    })
-  }),
+  stage: mockStage,
   max_review_period_length: new u32(100),
   application_rationing_policy: new Option(ApplicationRationingPolicy),
   application_staking_policy: new Option(StakingPolicy),
@@ -153,7 +149,7 @@ export function OpportunitySandbox () {
 
   const meta: OpeningMetadata = {
     id: '1',
-    group: 'group-name'
+    group: WorkingGroups.ContentCurators
   };
 
   return (

+ 44 - 19
pioneer/packages/joy-roles/src/tabs/Opportunities.tsx

@@ -4,7 +4,7 @@ import NumberFormat from 'react-number-format';
 import marked from 'marked';
 import CopyToClipboard from 'react-copy-to-clipboard';
 
-import { Link, useHistory } from 'react-router-dom';
+import { Link, useHistory, useLocation } from 'react-router-dom';
 import {
   Button,
   Card,
@@ -41,7 +41,7 @@ import {
 import { Loadable } from '@polkadot/joy-utils/index';
 import styled from 'styled-components';
 import _ from 'lodash';
-import { WorkingGroups, AvailableGroups } from '../working_groups';
+import { WorkingGroups, AvailableGroups, workerRoleNameByGroup } from '../working_groups';
 
 type OpeningStage = OpeningMetadataProps & {
   stage: OpeningStageClassification;
@@ -476,19 +476,16 @@ type OpeningViewProps = WorkingGroupOpening & BlockTimeProps & MemberIdProps
 export const OpeningView = Loadable<OpeningViewProps>(
   ['opening', 'block_time_in_seconds'],
   props => {
-    const hrt = props.opening.parse_human_readable_text();
-
-    if (typeof hrt === 'undefined' || typeof hrt === 'string') {
-      return null;
-    }
-
-    const text = hrt;
+    const text = props.opening.parse_human_readable_text_with_fallback();
+    const isLeadOpening = props.meta.type?.isOfType('Leader');
 
     return (
       <Container className={'opening ' + openingClass(props.stage.state)}>
         <OpeningTitle>
           {text.job.title}
-          <OpeningLabel>{ _.startCase(props.meta.group) }</OpeningLabel>
+          <OpeningLabel>
+            { _.startCase(props.meta.group) }{ isLeadOpening ? ' Lead' : '' }
+          </OpeningLabel>
         </OpeningTitle>
         <Card fluid className="container">
           <Card.Content className="header">
@@ -527,15 +524,28 @@ export type OpeningsViewProps = MemberIdProps & {
   openings?: Array<WorkingGroupOpening>;
   block_time_in_seconds?: number;
   group?: WorkingGroups;
+  lead?: boolean;
 }
 
 export const OpeningsView = Loadable<OpeningsViewProps>(
   ['openings', 'block_time_in_seconds'],
   props => {
     const history = useHistory();
-    const { group = '' } = props;
-    const onFilterChange: DropdownProps['onChange'] = (e, data) => (
-      data.value !== group && history.push(`/working-groups/opportunities/${data.value}`)
+    const location = useLocation();
+    const basePath = '/working-groups/opportunities';
+    const { group = null, lead = false } = props;
+    const onFilterChange: DropdownProps['onChange'] = (e, data) => {
+      const newPath = data.value || basePath;
+      if (newPath !== location.pathname) { history.push(newPath as string); }
+    };
+    const groupOption = (group: WorkingGroups | null, lead = false) => ({
+      value: `${basePath}${group ? `/${group}` : ''}${lead ? '/lead' : ''}`,
+      text: _.startCase(`${group || 'All opportuniries'}`) + (lead ? ' (Lead)' : '')
+    });
+    // Can assert "props.openings!" because we're using "Loadable" which prevents them from beeing undefined
+    const filteredOpenings = props.openings!.filter(o =>
+      (!group || o.meta.group === group) &&
+      (!group || !o.meta.type || (lead === o.meta.type.isOfType('Leader')))
     );
 
     return (
@@ -544,17 +554,32 @@ export const OpeningsView = Loadable<OpeningsViewProps>(
           <FilterOpportunitiesDropdown
             placeholder="All opportunities"
             options={
-              [{ value: '', text: 'All opportunities' }]
-                .concat(AvailableGroups.map(g => ({ value: g, text: _.startCase(g) })))
+              [groupOption(null, false)]
+                .concat(AvailableGroups.map(g => groupOption(g)))
+                // Currently we filter-out content curators, because they don't use the new working-group module yet
+                .concat(AvailableGroups.filter(g => g !== WorkingGroups.ContentCurators).map(g => groupOption(g, true)))
             }
-            value={group}
+            value={groupOption(group, lead).value}
             onChange={onFilterChange}
             selection
           />
         </FilterOpportunities>
-        {props.openings && props.openings.filter(o => !group || o.meta.group === group).map((opening, key) => (
-          <OpeningView key={key} {...opening} block_time_in_seconds={props.block_time_in_seconds as number} member_id={props.member_id} />
-        ))}
+        { (
+          filteredOpenings.length
+            ? filteredOpenings.map((opening, key) => (
+              <OpeningView
+                key={key}
+                {...opening}
+                block_time_in_seconds={props.block_time_in_seconds as number}
+                member_id={props.member_id} />
+            ))
+            : (
+              <h2>
+                No openings{group ? ` for ${workerRoleNameByGroup[group]}${lead ? ' Lead' : ''} role ` : ' '}
+                are currently available!
+              </h2>
+            )
+        ) }
       </Container>
     );
   }

+ 2 - 22
pioneer/packages/joy-roles/src/tabs/WorkingGroup.controller.tsx

@@ -7,18 +7,14 @@ import { ITransport } from '../transport';
 import {
   ContentCurators,
   WorkingGroupMembership,
-  GroupLeadStatus,
   StorageProviders
 } from './WorkingGroup';
 
-import { WorkingGroups } from '../working_groups';
 import styled from 'styled-components';
 
 type State = {
   contentCurators?: WorkingGroupMembership;
   storageProviders?: WorkingGroupMembership;
-  contentLeadStatus?: GroupLeadStatus;
-  storageLeadStatus?: GroupLeadStatus;
 }
 
 export class WorkingGroupsController extends Controller<State, ITransport> {
@@ -26,8 +22,6 @@ export class WorkingGroupsController extends Controller<State, ITransport> {
     super(transport, {});
     this.getCurationGroup();
     this.getStorageGroup();
-    this.getCuratorLeadStatus();
-    this.getStorageLeadStatus();
   }
 
   getCurationGroup () {
@@ -43,20 +37,6 @@ export class WorkingGroupsController extends Controller<State, ITransport> {
       this.dispatch();
     });
   }
-
-  getCuratorLeadStatus () {
-    this.transport.groupLeadStatus(WorkingGroups.ContentCurators).then((value: GroupLeadStatus) => {
-      this.setState({ contentLeadStatus: value });
-      this.dispatch();
-    });
-  }
-
-  getStorageLeadStatus () {
-    this.transport.groupLeadStatus(WorkingGroups.StorageProviders).then((value: GroupLeadStatus) => {
-      this.setState({ storageLeadStatus: value });
-      this.dispatch();
-    });
-  }
 }
 
 const WorkingGroupsOverview = styled.div`
@@ -71,8 +51,8 @@ const WorkingGroupsOverview = styled.div`
 export const WorkingGroupsView = View<WorkingGroupsController, State>(
   (state) => (
     <WorkingGroupsOverview>
-      <ContentCurators {...state.contentCurators} leadStatus={state.contentLeadStatus}/>
-      <StorageProviders {...state.storageProviders} leadStatus={state.storageLeadStatus}/>
+      <ContentCurators {...state.contentCurators}/>
+      <StorageProviders {...state.storageProviders}/>
     </WorkingGroupsOverview>
   )
 );

+ 1 - 1
pioneer/packages/joy-roles/src/tabs/WorkingGroup.stories.tsx

@@ -75,6 +75,6 @@ export function ContentCuratorsSection () {
   ];
 
   return (
-    <ContentCurators members={members} rolesAvailable={boolean('Roles available', true)} />
+    <ContentCurators workers={members} workerRolesAvailable={boolean('Roles available', true)} />
   );
 }

+ 24 - 13
pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx

@@ -10,12 +10,14 @@ import styled from 'styled-components';
 import _ from 'lodash';
 
 export type WorkingGroupMembership = {
-  members: GroupMember[];
-  rolesAvailable: boolean;
+  leadStatus: GroupLeadStatus;
+  workers: GroupMember[];
+  workerRolesAvailable: boolean;
+  leadRolesAvailable: boolean;
 }
 
 const NoRolesAvailable = () => (
-  <Message>
+  <Message info>
     <Message.Header>No open roles at the moment</Message.Header>
     <p>The team is full at the moment, but we intend to expand. Check back for open roles soon!</p>
   </Message>
@@ -25,13 +27,14 @@ type JoinRoleProps = {
   group: WorkingGroups;
   title: string;
   description: string;
+  lead?: boolean;
 };
 
-const JoinRole = ({ group, title, description }: JoinRoleProps) => (
+const JoinRole = ({ group, lead = false, title, description }: JoinRoleProps) => (
   <Message positive>
     <Message.Header>{title}</Message.Header>
     <p>{description}</p>
-    <Link to={`/working-groups/opportunities/${group}`}>
+    <Link to={`/working-groups/opportunities/${group}${lead ? '/lead' : ''}`}>
       <Button icon labelPosition="right" color="green" positive>
         Find out more
         <Icon name={'right arrow' as SemanticICONS} />
@@ -66,36 +69,44 @@ type GroupOverviewProps = GroupOverviewOuterProps & {
   customGroupName?: string;
   customJoinTitle?: string;
   customJoinDesc?: string;
+  customBecomeLeadTitle?: string;
+  customBecomeLeadDesc?: string;
 }
 
 const GroupOverview = Loadable<GroupOverviewProps>(
-  ['members', 'leadStatus'],
+  ['workers', 'leadStatus'],
   ({
     group,
     description,
-    members,
+    workers,
     leadStatus,
-    rolesAvailable,
+    workerRolesAvailable,
+    leadRolesAvailable,
     customGroupName,
     customJoinTitle,
-    customJoinDesc
+    customJoinDesc,
+    customBecomeLeadTitle,
+    customBecomeLeadDesc
   }: GroupOverviewProps) => {
     const groupName = customGroupName || _.startCase(group);
     const joinTitle = customJoinTitle || `Join the ${groupName} group!`;
     const joinDesc = customJoinDesc || `There are openings for new ${groupName}. This is a great way to support Joystream!`;
+    const becomeLeadTitle = customBecomeLeadTitle || `Become ${groupName} Lead!`;
+    const becomeLeadDesc = customBecomeLeadDesc || `An opportunity to become ${groupName} Leader is currently available! This is a great way to support Joystream!`;
     return (
       <GroupOverviewSection>
         <h2>{ groupName }</h2>
         <p>{ description }</p>
         <Card.Group>
-          { members!.map((member, key) => (
-            <GroupMemberView key={key} {...member} />
+          { workers!.map((worker, key) => (
+            <GroupMemberView key={key} {...worker} />
           )) }
         </Card.Group>
-        { rolesAvailable
+        { workerRolesAvailable
           ? <JoinRole group={group} title={joinTitle} description={joinDesc} />
           : <NoRolesAvailable /> }
         { leadStatus && <CurrentLead groupName={groupName} {...leadStatus}/> }
+        { leadRolesAvailable && <JoinRole group={group} lead title={becomeLeadTitle} description={becomeLeadDesc} /> }
       </GroupOverviewSection>
     );
   }
@@ -142,7 +153,7 @@ export const CurrentLead = Loadable<CurrentLeadProps>(
     const leadDesc = customLeadDesc || `This role is responsible for hiring ${groupName}.`;
     return (
       <LeadSection>
-        <Message positive>
+        <Message>
           <Message.Header>{ groupName } Lead</Message.Header>
           <p>{ leadDesc }</p>
           {lead

+ 30 - 45
pioneer/packages/joy-roles/src/transport.mock.ts

@@ -9,8 +9,6 @@ import { ITransport } from './transport';
 import { Role, MemberId } from '@joystream/types/members';
 import {
   Opening,
-  AcceptingApplications,
-  ActiveOpeningStage,
   ApplicationRationingPolicy,
   StakingPolicy
 } from '@joystream/types/hiring';
@@ -26,8 +24,8 @@ import { tomorrow, yesterday, newMockHumanReadableText } from './tabs/Opportunit
 import { OpeningState } from './classifiers';
 
 import * as faker from 'faker';
-import { mockProfile } from './mocks';
-import { WorkingGroups } from './working_groups';
+import { mockProfile, mockStage } from './mocks';
+import { WorkingGroups, workerRoleNameByGroup } from './working_groups';
 
 export class Transport extends TransportBase implements ITransport {
   protected simulateApiResponse<T> (value: T): Promise<T> {
@@ -52,10 +50,12 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
-  curationGroup (): Promise<WorkingGroupMembership> {
+  async curationGroup (): Promise<WorkingGroupMembership> {
     return this.simulateApiResponse<WorkingGroupMembership>({
-      rolesAvailable: true,
-      members: [
+      leadStatus: await this.groupLeadStatus(),
+      workerRolesAvailable: true,
+      leadRolesAvailable: false,
+      workers: [
         {
           memberId: new MemberId(1),
           roleAccount: new GenericAccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp'),
@@ -112,10 +112,12 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
-  storageGroup (): Promise<WorkingGroupMembership> {
+  async storageGroup (): Promise<WorkingGroupMembership> {
     return this.simulateApiResponse<WorkingGroupMembership>({
-      rolesAvailable: true,
-      members: [
+      leadStatus: await this.groupLeadStatus(),
+      workerRolesAvailable: true,
+      leadRolesAvailable: true,
+      workers: [
         {
           memberId: new MemberId(1),
           roleAccount: new GenericAccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp'),
@@ -137,11 +139,7 @@ export class Transport extends TransportBase implements ITransport {
         {
           opening: new Opening({
             created: new u32(50000),
-            stage: new ActiveOpeningStage({
-              acceptingApplications: new AcceptingApplications({
-                started_accepting_applicants_at_block: new u32(100)
-              })
-            }),
+            stage: mockStage,
             max_review_period_length: new u32(100),
             application_rationing_policy: new Option(ApplicationRationingPolicy),
             application_staking_policy: new Option(StakingPolicy),
@@ -185,7 +183,7 @@ export class Transport extends TransportBase implements ITransport {
           }),
           meta: {
             id: '1',
-            group: 'somegroup'
+            group: WorkingGroups.ContentCurators
           },
           stage: {
             state: OpeningState.AcceptingApplications,
@@ -212,16 +210,13 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
-  curationGroupOpening (id: number): Promise<WorkingGroupOpening> {
+  // eslint-disable-next-line @typescript-eslint/require-await
+  async groupOpening (group: WorkingGroups, id: number): Promise<WorkingGroupOpening> {
     return this.simulateApiResponse<WorkingGroupOpening>(
       {
         opening: new Opening({
           created: new u32(50000),
-          stage: new ActiveOpeningStage({
-            acceptingApplications: new AcceptingApplications({
-              started_accepting_applicants_at_block: new u32(100)
-            })
-          }),
+          stage: mockStage,
           max_review_period_length: new u32(100),
           application_rationing_policy: new Option(ApplicationRationingPolicy),
           application_staking_policy: new Option(StakingPolicy),
@@ -269,7 +264,7 @@ export class Transport extends TransportBase implements ITransport {
         }),
         meta: {
           id: '1',
-          group: 'group-name'
+          group: WorkingGroups.ContentCurators
         },
         stage: {
           state: OpeningState.AcceptingApplications,
@@ -296,11 +291,7 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
-  async groupOpening (group: WorkingGroups, id: number): Promise<WorkingGroupOpening> {
-    return await this.curationGroupOpening(id);
-  }
-
-  openingApplicationRanks (openingId: number): Promise<Balance[]> {
+  openingApplicationRanks (group: WorkingGroups, openingId: number): Promise<Balance[]> {
     const slots: Balance[] = [];
     for (let i = 0; i < 20; i++) {
       slots.push(new u128((i * 100) + 10 + i + 1));
@@ -350,12 +341,12 @@ export class Transport extends TransportBase implements ITransport {
   }
 
   // eslint-disable-next-line @typescript-eslint/require-await
-  async openingApplications (): Promise<OpeningApplication[]> {
+  async openingApplicationsByAddress (address: string): Promise<OpeningApplication[]> {
     return [{
       id: 1,
       meta: {
         id: '1',
-        group: 'group-name'
+        group: WorkingGroups.ContentCurators
       },
       stage: {
         state: OpeningState.AcceptingApplications,
@@ -365,11 +356,7 @@ export class Transport extends TransportBase implements ITransport {
       },
       opening: new Opening({
         created: new u32(50000),
-        stage: new ActiveOpeningStage({
-          acceptingApplications: new AcceptingApplications({
-            started_accepting_applicants_at_block: new u32(100)
-          })
-        }),
+        stage: mockStage,
         max_review_period_length: new u32(100),
         application_rationing_policy: new Option(ApplicationRationingPolicy),
         application_staking_policy: new Option(StakingPolicy),
@@ -423,11 +410,12 @@ export class Transport extends TransportBase implements ITransport {
   }
 
   // eslint-disable-next-line @typescript-eslint/require-await
-  async myCurationGroupRoles (): Promise<ActiveRole[]> {
+  async myRoles (address: string): Promise<ActiveRole[]> {
     return [
       {
-        curatorId: new CuratorId(1),
-        name: 'My curation group role',
+        workerId: new CuratorId(1),
+        name: workerRoleNameByGroup[WorkingGroups.ContentCurators],
+        group: WorkingGroups.ContentCurators,
         url: 'some URL',
         reward: new u128(321),
         stake: new u128(12343200)
@@ -435,12 +423,9 @@ export class Transport extends TransportBase implements ITransport {
     ];
   }
 
-  myStorageGroupRoles (): Subscribable<ActiveRole[]> {
-    return new Observable<ActiveRole[]>(observer => { /* do nothing */ });
-  }
-
   // eslint-disable-next-line @typescript-eslint/require-await
-  async applyToCuratorOpening (
+  async applyToOpening (
+    group: WorkingGroups,
     id: number,
     roleAccountName: string,
     sourceAccount: string,
@@ -450,11 +435,11 @@ export class Transport extends TransportBase implements ITransport {
     return 0;
   }
 
-  leaveCurationRole (sourceAccount: string, id: number, rationale: string) {
+  leaveRole (group: WorkingGroups, sourceAccount: string, id: number, rationale: string) {
     /* do nothing */
   }
 
-  withdrawCuratorApplication (sourceAccount: string, id: number) {
+  withdrawApplication (group: WorkingGroups, sourceAccount: string, id: number) {
     /* do nothing */
   }
 }

+ 171 - 117
pioneer/packages/joy-roles/src/transport.substrate.ts

@@ -1,9 +1,8 @@
-import { Observable } from 'rxjs';
 import { map, switchMap } from 'rxjs/operators';
 
 import ApiPromise from '@polkadot/api/promise';
 import { Balance } from '@polkadot/types/interfaces';
-import { GenericAccountId, Option, u32, u128, Vec } from '@polkadot/types';
+import { GenericAccountId, Option, u128, Vec } from '@polkadot/types';
 import { Constructor } from '@polkadot/types/types';
 import { Moment } from '@polkadot/types/interfaces/runtime';
 import { QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
@@ -30,7 +29,7 @@ import {
   RoleStakeProfile
 } from '@joystream/types/working-group';
 
-import { Application, Opening, OpeningId, ApplicationId } from '@joystream/types/hiring';
+import { Application, Opening, OpeningId, ApplicationId, ActiveApplicationStage } from '@joystream/types/hiring';
 import { Stake, StakeId } from '@joystream/types/stake';
 import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
 import { ActorInRole, Profile, MemberId, Role, RoleKeys, ActorId } from '@joystream/types/members';
@@ -48,7 +47,7 @@ import {
   classifyOpeningStakes,
   isApplicationHired
 } from './classifiers';
-import { WorkingGroups, AvailableGroups } from './working_groups';
+import { WorkingGroups, AvailableGroups, workerRoleNameByGroup } from './working_groups';
 import { Sort, Sum, Zero } from './balances';
 import _ from 'lodash';
 
@@ -69,7 +68,14 @@ type WGApiMethodType =
   | 'applicationById'
   | 'nextWorkerId'
   | 'workerById';
-type WGApiMethodsMapping = { [key in WGApiMethodType]: string };
+type WGApiTxMethodType =
+  'applyOnOpening'
+  | 'withdrawApplication'
+  | 'leaveRole';
+type WGApiMethodsMapping = {
+  query: { [key in WGApiMethodType]: string };
+  tx: { [key in WGApiTxMethodType]: string };
+};
 
 type GroupApplication = CuratorApplication | WGApplication;
 type GroupApplicationId = CuratorApplicationId | ApplicationId;
@@ -82,6 +88,7 @@ type GroupLead = Lead | Worker;
 type GroupLeadWithMemberId = {
   lead: GroupLead;
   memberId: MemberId;
+  workerId?: WorkerId; // Only when it's `working-groups` module lead
 }
 
 type WGApiMapping = {
@@ -98,12 +105,19 @@ const workingGroupsApiMapping: WGApiMapping = {
   [WorkingGroups.StorageProviders]: {
     module: 'storageWorkingGroup',
     methods: {
-      nextOpeningId: 'nextOpeningId',
-      openingById: 'openingById',
-      nextApplicationId: 'nextApplicationId',
-      applicationById: 'applicationById',
-      nextWorkerId: 'nextWorkerId',
-      workerById: 'workerById'
+      query: {
+        nextOpeningId: 'nextOpeningId',
+        openingById: 'openingById',
+        nextApplicationId: 'nextApplicationId',
+        applicationById: 'applicationById',
+        nextWorkerId: 'nextWorkerId',
+        workerById: 'workerById'
+      },
+      tx: {
+        applyOnOpening: 'applyOnOpening',
+        withdrawApplication: 'withdrawApplication',
+        leaveRole: 'leaveRole'
+      }
     },
     openingType: WGOpening,
     applicationType: WGApplication,
@@ -112,12 +126,19 @@ const workingGroupsApiMapping: WGApiMapping = {
   [WorkingGroups.ContentCurators]: {
     module: 'contentWorkingGroup',
     methods: {
-      nextOpeningId: 'nextCuratorOpeningId',
-      openingById: 'curatorOpeningById',
-      nextApplicationId: 'nextCuratorApplicationId',
-      applicationById: 'curatorApplicationById',
-      nextWorkerId: 'nextCuratorId',
-      workerById: 'curatorById'
+      query: {
+        nextOpeningId: 'nextCuratorOpeningId',
+        openingById: 'curatorOpeningById',
+        nextApplicationId: 'nextCuratorApplicationId',
+        applicationById: 'curatorApplicationById',
+        nextWorkerId: 'nextCuratorId',
+        workerById: 'curatorById'
+      },
+      tx: {
+        applyOnOpening: 'applyOnCuratorOpening',
+        withdrawApplication: 'withdrawCuratorApplication',
+        leaveRole: 'leaveCuratorRole'
+      }
     },
     openingType: CuratorOpening,
     applicationType: CuratorApplication,
@@ -139,11 +160,18 @@ export class Transport extends TransportBase implements ITransport {
 
   cachedApiMethodByGroup (group: WorkingGroups, method: WGApiMethodType) {
     const apiModule = workingGroupsApiMapping[group].module;
-    const apiMethod = workingGroupsApiMapping[group].methods[method];
+    const apiMethod = workingGroupsApiMapping[group].methods.query[method];
 
     return this.cachedApi.query[apiModule][apiMethod];
   }
 
+  apiExtrinsicByGroup (group: WorkingGroups, method: WGApiTxMethodType) {
+    const apiModule = workingGroupsApiMapping[group].module;
+    const apiMethod = workingGroupsApiMapping[group].methods.tx[method];
+
+    return this.api.tx[apiModule][apiMethod];
+  }
+
   unsubscribe () {
     this.cachedApi.unsubscribe();
   }
@@ -233,13 +261,13 @@ export class Transport extends TransportBase implements ITransport {
       roleAccount,
       memberId,
       profile: profile.unwrap(),
-      title: _.startCase(group).slice(0, -1), // FIXME: Temporary solution (just removes "s" at the end)
+      title: workerRoleNameByGroup[group],
       stake: stakeValue,
       earned: earnedValue
     });
   }
 
-  protected async areAnyGroupRolesOpen (group: WorkingGroups): Promise<boolean> {
+  protected async areGroupRolesOpen (group: WorkingGroups, lead = false): Promise<boolean> {
     const nextId = await this.cachedApiMethodByGroup(group, 'nextOpeningId')() as GroupOpeningId;
 
     // This is chain specfic, but if next id is still 0, it means no openings have been added yet
@@ -253,9 +281,16 @@ export class Transport extends TransportBase implements ITransport {
       await this.cachedApiMethodByGroup(group, 'openingById')()
     );
 
-    for (let i = 0; i < groupOpenings.linked_values.length; i++) {
-      const opening = await this.opening(groupOpenings.linked_values[i].hiring_opening_id.toNumber());
-      if (opening.is_active) {
+    for (const groupOpening of groupOpenings.linked_values) {
+      const opening = await this.opening(groupOpening.hiring_opening_id.toNumber());
+      if (
+        opening.is_active &&
+        (
+          groupOpening instanceof WGOpening
+            ? (lead === groupOpening.opening_type.isOfType('Leader'))
+            : !lead // Lead opening are never available for content working group currently
+        )
+      ) {
         return true;
       }
     }
@@ -263,11 +298,6 @@ export class Transport extends TransportBase implements ITransport {
     return false;
   }
 
-  protected async areAnyCuratorRolesOpen (): Promise<boolean> {
-    // Backward compatibility
-    return this.areAnyGroupRolesOpen(WorkingGroups.ContentCurators);
-  }
-
   protected async currentCuratorLead (): Promise<GroupLeadWithMemberId | null> {
     const optLeadId = (await this.cachedApi.query.contentWorkingGroup.currentLeadId()) as Option<LeadId>;
 
@@ -309,7 +339,8 @@ export class Transport extends TransportBase implements ITransport {
 
     return {
       lead: leadWorker,
-      memberId: leadWorker.member_id
+      memberId: leadWorker.member_id,
+      workerId: leadWorkerId
     };
   }
 
@@ -328,6 +359,7 @@ export class Transport extends TransportBase implements ITransport {
       return {
         lead: {
           memberId: currentLead.memberId,
+          workerId: currentLead.workerId,
           roleAccount: currentLead.lead.role_account_id,
           profile: profile.unwrap(),
           title: _.startCase(group) + ' Lead',
@@ -343,32 +375,35 @@ export class Transport extends TransportBase implements ITransport {
   }
 
   async groupOverview (group: WorkingGroups): Promise<WorkingGroupMembership> {
-    const rolesAvailable = await this.areAnyGroupRolesOpen(group);
+    const workerRolesAvailable = await this.areGroupRolesOpen(group);
+    const leadRolesAvailable = await this.areGroupRolesOpen(group, true);
+    const leadStatus = await this.groupLeadStatus(group);
 
     const nextId = await this.cachedApiMethodByGroup(group, 'nextWorkerId')() as GroupWorkerId;
 
-    // This is chain specfic, but if next id is still 0, it means no curators have been added yet
-    if (nextId.eq(0)) {
-      return {
-        members: [],
-        rolesAvailable
-      };
-    }
-
-    const values = new MultipleLinkedMapEntry<GroupWorkerId, GroupWorker>(
-      ActorId,
-      workingGroupsApiMapping[group].workerType,
-      await this.cachedApiMethodByGroup(group, 'workerById')() as GroupWorker
-    );
+    let workersWithIds: { worker: GroupWorker; id: GroupWorkerId }[] = [];
+    // This is chain specfic, but if next id is still 0, it means no workers have been added yet
+    if (!nextId.eq(0)) {
+      const values = new MultipleLinkedMapEntry<GroupWorkerId, GroupWorker>(
+        ActorId,
+        workingGroupsApiMapping[group].workerType,
+        await this.cachedApiMethodByGroup(group, 'workerById')() as GroupWorker
+      );
 
-    const workers = values.linked_values.filter(value => value.is_active).reverse();
-    const workerIds = values.linked_keys.filter((v, k) => values.linked_values[k].is_active).reverse();
+      workersWithIds = values.linked_values
+        // First bind workers with ids
+        .map((worker, i) => ({ worker, id: values.linked_keys[i] }))
+        // Filter by: active and "not lead"
+        .filter(({ worker, id }) => worker.is_active && (!leadStatus.lead?.workerId || !id.eq(leadStatus.lead.workerId)));
+    }
 
     return {
-      members: await Promise.all(
-        workers.map((worker, k) => this.groupMember(group, workerIds[k], worker))
+      leadStatus,
+      workers: await Promise.all(
+        workersWithIds.map(({ worker, id }) => this.groupMember(group, id, worker))
       ),
-      rolesAvailable
+      workerRolesAvailable,
+      leadRolesAvailable
     };
   }
 
@@ -446,14 +481,8 @@ export class Transport extends TransportBase implements ITransport {
     return output;
   }
 
-  protected async curatorOpeningApplications (curatorOpeningId: number): Promise<WorkingGroupPair<Application, CuratorApplication>[]> {
-    // Backwards compatibility
-    const applications = await this.groupOpeningApplications(WorkingGroups.ContentCurators, curatorOpeningId);
-    return applications as WorkingGroupPair<Application, CuratorApplication>[];
-  }
-
   async groupOpening (group: WorkingGroups, id: number): Promise<WorkingGroupOpening> {
-    const nextId = (await this.cachedApiMethodByGroup(group, 'nextOpeningId')() as u32).toNumber();
+    const nextId = (await this.cachedApiMethodByGroup(group, 'nextOpeningId')() as GroupOpeningId).toNumber();
     if (id < 0 || id >= nextId) {
       throw new Error('invalid id');
     }
@@ -461,10 +490,10 @@ export class Transport extends TransportBase implements ITransport {
     const groupOpening = new SingleLinkedMapEntry<GroupOpening>(
       workingGroupsApiMapping[group].openingType,
       await this.cachedApiMethodByGroup(group, 'openingById')(id)
-    );
+    ).value;
 
     const opening = await this.opening(
-      groupOpening.value.hiring_opening_id.toNumber()
+      groupOpening.hiring_opening_id.toNumber()
     );
 
     const applications = await this.groupOpeningApplications(group, id);
@@ -474,7 +503,8 @@ export class Transport extends TransportBase implements ITransport {
       opening: opening,
       meta: {
         id: id.toString(),
-        group
+        group,
+        type: groupOpening instanceof WGOpening ? groupOpening.opening_type : undefined
       },
       stage: await classifyOpeningStage(this, opening),
       applications: {
@@ -488,11 +518,6 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
-  async curationGroupOpening (id: number): Promise<WorkingGroupOpening> {
-    // Backwards compatibility
-    return this.groupOpening(WorkingGroups.ContentCurators, id);
-  }
-
   protected async openingApplicationTotalStake (application: Application): Promise<Balance> {
     const promises = new Array<Promise<Balance>>();
 
@@ -507,13 +532,14 @@ export class Transport extends TransportBase implements ITransport {
     return Sum(await Promise.all(promises));
   }
 
-  async openingApplicationRanks (openingId: number): Promise<Balance[]> {
-    const applications = await this.curatorOpeningApplications(openingId);
+  async openingApplicationRanks (group: WorkingGroups, openingId: number): Promise<Balance[]> {
+    const applications = await this.groupOpeningApplications(group, openingId);
     return Sort(
       (await Promise.all(
-        applications.map(application => this.openingApplicationTotalStake(application.hiringModule))
+        applications
+          .filter(a => a.hiringModule.stage.value instanceof ActiveApplicationStage)
+          .map(application => this.openingApplicationTotalStake(application.hiringModule))
       ))
-        .filter((b) => !b.eq(Zero))
     );
   }
 
@@ -577,11 +603,12 @@ export class Transport extends TransportBase implements ITransport {
   }
 
   protected async myApplicationRank (myApp: Application, applications: Array<Application>): Promise<number> {
+    const activeApplications = applications.filter(app => app.stage.value instanceof ActiveApplicationStage);
     const stakes = await Promise.all(
-      applications.map(app => this.openingApplicationTotalStake(app))
+      activeApplications.map(app => this.openingApplicationTotalStake(app))
     );
 
-    const appvalues = applications.map((app, key) => {
+    const appvalues = activeApplications.map((app, key) => {
       return {
         app: app,
         value: stakes[key]
@@ -601,15 +628,15 @@ export class Transport extends TransportBase implements ITransport {
     return appvalues.findIndex(v => v.app.eq(myApp)) + 1;
   }
 
-  async openingApplications (roleKeyId: string): Promise<OpeningApplication[]> {
-    const curatorApps = new MultipleLinkedMapEntry<CuratorApplicationId, CuratorApplication>(
-      CuratorApplicationId,
-      CuratorApplication,
-      await this.cachedApi.query.contentWorkingGroup.curatorApplicationById()
+  async openingApplicationsByAddressAndGroup (group: WorkingGroups, roleKey: string): Promise<OpeningApplication[]> {
+    const applications = new MultipleLinkedMapEntry<GroupApplicationId, GroupApplication>(
+      ApplicationId,
+      workingGroupsApiMapping[group].applicationType,
+      await this.cachedApiMethodByGroup(group, 'applicationById')()
     );
 
-    const myApps = curatorApps.linked_values.filter(app => app.role_account.eq(roleKeyId));
-    const myAppIds = curatorApps.linked_keys.filter((id, key) => curatorApps.linked_values[key].role_account.eq(roleKeyId));
+    const myApps = applications.linked_values.filter(app => app.role_account_id.eq(roleKey));
+    const myAppIds = applications.linked_keys.filter((id, key) => applications.linked_values[key].role_account_id.eq(roleKey));
 
     const hiringAppPairs = await Promise.all(
       myApps.map(
@@ -628,73 +655,98 @@ export class Transport extends TransportBase implements ITransport {
       hiringApps.map(app => this.applicationStakes(app))
     );
 
-    const wgs = await Promise.all(
-      myApps.map(curatorOpening => {
-        return this.curationGroupOpening(curatorOpening.curator_opening_id.toNumber());
+    const openings = await Promise.all(
+      myApps.map(application => {
+        return this.groupOpening(group, application.opening_id.toNumber());
       })
     );
 
     const allAppsByOpening = (await Promise.all(
-      myApps.map(curatorOpening => {
-        return this.curatorOpeningApplications(curatorOpening.curator_opening_id.toNumber());
+      myApps.map(application => {
+        return this.groupOpeningApplications(group, application.opening_id.toNumber());
       })
     ));
 
     return await Promise.all(
-      wgs.map(async (wg, key) => {
+      openings.map(async (o, key) => {
         return {
           id: myAppIds[key].toNumber(),
           hired: isApplicationHired(hiringApps[key]),
           cancelledReason: classifyApplicationCancellation(hiringApps[key]),
           rank: await this.myApplicationRank(hiringApps[key], allAppsByOpening[key].map(a => a.hiringModule)),
-          capacity: wg.applications.maxNumberOfApplications,
-          stage: wg.stage,
-          opening: wg.opening,
-          meta: wg.meta,
+          capacity: o.applications.maxNumberOfApplications,
+          stage: o.stage,
+          opening: o.opening,
+          meta: o.meta,
           applicationStake: stakes[key].application,
           roleStake: stakes[key].role,
-          review_end_time: wg.stage.review_end_time,
-          review_end_block: wg.stage.review_end_block
+          review_end_time: o.stage.review_end_time,
+          review_end_block: o.stage.review_end_block
         };
       })
     );
   }
 
-  async myCurationGroupRoles (roleKeyId: string): Promise<ActiveRole[]> {
-    const curators = new MultipleLinkedMapEntry<CuratorId, Curator>(
-      CuratorId,
-      Curator,
-      await this.cachedApi.query.contentWorkingGroup.curatorById()
+  // Get opening applications for all groups by address
+  async openingApplicationsByAddress (roleKey: string): Promise<OpeningApplication[]> {
+    let applications: OpeningApplication[] = [];
+    for (const group of AvailableGroups) {
+      applications = applications.concat(await this.openingApplicationsByAddressAndGroup(group, roleKey));
+    }
+
+    return applications;
+  }
+
+  async myRolesByGroup (group: WorkingGroups, roleKeyId: string): Promise<ActiveRole[]> {
+    const workers = new MultipleLinkedMapEntry<GroupWorkerId, GroupWorker>(
+      ActorId,
+      workingGroupsApiMapping[group].workerType,
+      await this.cachedApiMethodByGroup(group, 'workerById')()
     );
 
+    const groupLead = (await this.groupLeadStatus(group)).lead;
+
     return Promise.all(
-      curators
+      workers
         .linked_values
         .toArray()
-        .filter(curator => curator.role_account.eq(roleKeyId) && curator.is_active)
-        .map(async (curator, key) => {
+        // We need to associate worker ids with workers BEFORE filtering the array
+        .map((worker, index) => ({ worker, id: workers.linked_keys[index] }))
+        .filter(({ worker }) => worker.role_account_id.eq(roleKeyId) && worker.is_active)
+        .map(async workerWithId => {
+          const { worker, id } = workerWithId;
+
           let stakeValue: Balance = new u128(0);
-          if (curator.role_stake_profile && curator.role_stake_profile.isSome) {
-            stakeValue = await this.workerStake(curator.role_stake_profile.unwrap());
+          if (worker.role_stake_profile && worker.role_stake_profile.isSome) {
+            stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
           }
 
           let earnedValue: Balance = new u128(0);
-          if (curator.reward_relationship && curator.reward_relationship.isSome) {
-            earnedValue = await this.workerTotalReward(curator.reward_relationship.unwrap());
+          if (worker.reward_relationship && worker.reward_relationship.isSome) {
+            earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
           }
 
           return {
-            curatorId: curators.linked_keys[key],
-            name: 'Content curator',
+            workerId: id,
+            name: (groupLead?.workerId && groupLead.workerId.eq(id))
+              ? _.startCase(group) + ' Lead'
+              : workerRoleNameByGroup[group],
             reward: earnedValue,
-            stake: stakeValue
+            stake: stakeValue,
+            group
           };
         })
     );
   }
 
-  myStorageGroupRoles (): Subscribable<ActiveRole[]> {
-    return new Observable<ActiveRole[]>(observer => { /* do nothing */ });
+  // All groups roles by key
+  async myRoles (roleKey: string): Promise<ActiveRole[]> {
+    let roles: ActiveRole[] = [];
+    for (const group of AvailableGroups) {
+      roles = roles.concat(await this.myRolesByGroup(group, roleKey));
+    }
+
+    return roles;
   }
 
   protected generateRoleAccount (name: string, password = ''): string | null {
@@ -709,7 +761,8 @@ export class Transport extends TransportBase implements ITransport {
     return status.account as string;
   }
 
-  applyToCuratorOpening (
+  applyToOpening (
+    group: WorkingGroups,
     id: number,
     roleAccountName: string,
     sourceAccount: string,
@@ -727,13 +780,14 @@ export class Transport extends TransportBase implements ITransport {
           if (!roleAccount) {
             reject(new Error('failed to create role account'));
           }
-          const tx = this.api.tx.contentWorkingGroup.applyOnCuratorOpening(
-            membershipIds[0],
-            new u32(id),
-            new GenericAccountId(roleAccount as string),
-            roleStake.eq(Zero) ? null : roleStake,
-            appStake.eq(Zero) ? null : appStake,
-            applicationText
+          const tx = this.apiExtrinsicByGroup(group, 'applyOnOpening')(
+            membershipIds[0], // Member id
+            id, // Worker/Curator opening id
+            roleAccount, // Role account
+            // TODO: Will need to be adjusted if AtLeast Zero stakes become possible
+            roleStake.eq(Zero) ? null : roleStake, // Role stake
+            appStake.eq(Zero) ? null : appStake, // Application stake
+            applicationText // Human readable text
           ) as unknown as SubmittableExtrinsic;
 
           const txFailedCb = () => {
@@ -754,8 +808,8 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
-  leaveCurationRole (sourceAccount: string, id: number, rationale: string) {
-    const tx = this.api.tx.contentWorkingGroup.leaveCuratorRole(
+  leaveRole (group: WorkingGroups, sourceAccount: string, id: number, rationale: string) {
+    const tx = this.apiExtrinsicByGroup(group, 'leaveRole')(
       id,
       rationale
     ) as unknown as SubmittableExtrinsic;
@@ -766,8 +820,8 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
-  withdrawCuratorApplication (sourceAccount: string, id: number) {
-    const tx = this.api.tx.contentWorkingGroup.withdrawCuratorApplication(
+  withdrawApplication (group: WorkingGroups, sourceAccount: string, id: number) {
+    const tx = this.apiExtrinsicByGroup(group, 'withdrawApplication')(
       id
     ) as unknown as SubmittableExtrinsic;
 

+ 10 - 9
pioneer/packages/joy-roles/src/transport.ts

@@ -16,22 +16,23 @@ export interface ITransport {
   storageGroup: () => Promise<WorkingGroupMembership>;
   currentOpportunities: () => Promise<Array<WorkingGroupOpening>>;
   groupOpening: (group: WorkingGroups, id: number) => Promise<WorkingGroupOpening>;
-  curationGroupOpening: (id: number) => Promise<WorkingGroupOpening>;
-  openingApplicationRanks: (openingId: number) => Promise<Balance[]>;
+  openingApplicationRanks: (group: WorkingGroups, openingId: number) => Promise<Balance[]>;
   expectedBlockTime: () => Promise<number>;
   blockHash: (height: number) => Promise<string>;
   blockTimestamp: (height: number) => Promise<Date>;
   transactionFee: () => Promise<Balance>;
   accounts: () => Subscribable<keyPairDetails[]>;
-  openingApplications: (address: string) => Promise<OpeningApplication[]>;
-  myCurationGroupRoles: (address: string) => Promise<ActiveRole[]>;
-  myStorageGroupRoles: () => Subscribable<ActiveRole[]>;
-  applyToCuratorOpening: (id: number,
+  openingApplicationsByAddress: (address: string) => Promise<OpeningApplication[]>;
+  myRoles: (address: string) => Promise<ActiveRole[]>;
+  applyToOpening: (
+    group: WorkingGroups,
+    id: number,
     roleAccountName: string,
     sourceAccount: string,
     appStake: Balance,
     roleStake: Balance,
-    applicationText: string) => Promise<number>;
-  leaveCurationRole: (sourceAccount: string, id: number, rationale: string) => void;
-  withdrawCuratorApplication: (sourceAccount: string, id: number) => void;
+    applicationText: string
+  ) => Promise<number>;
+  leaveRole: (group: WorkingGroups, sourceAccount: string, id: number, rationale: string) => void;
+  withdrawApplication: (group: WorkingGroups, sourceAccount: string, id: number) => void;
 }

+ 5 - 0
pioneer/packages/joy-roles/src/working_groups.ts

@@ -7,3 +7,8 @@ export const AvailableGroups: readonly WorkingGroups[] = [
   WorkingGroups.ContentCurators,
   WorkingGroups.StorageProviders
 ] as const;
+
+export const workerRoleNameByGroup: { [key in WorkingGroups]: string } = {
+  [WorkingGroups.ContentCurators]: 'Content Curator',
+  [WorkingGroups.StorageProviders]: 'Storage Provider'
+};

+ 2 - 1
pioneer/packages/joy-utils/src/View.tsx

@@ -39,11 +39,12 @@ export function View<C extends Controller<S, any>, S> (args: ViewProps<C, S>): V
 
       useEffect(() => {
         controller.subscribe(onUpdate);
+        controller.dispatch(); // Dispatch on first subscription (in case there's was a re-render of the View)
 
         return () => {
           controller.unsubscribe(onUpdate);
         };
-      });
+      }, []);
 
       let context: Params;
       if (typeof props.params !== 'undefined') {

+ 1 - 1
pioneer/packages/react-components/src/AddressCard.tsx

@@ -3,7 +3,7 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 // import { I18nProps } from '@polkadot/react-components/types';
 
-// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
+// eslint-disable-next-line @typescript-eslint/ban-ts-ignore, @typescript-eslint/ban-ts-comment
 // @ts-ignore This line needed for the styled export... don't ask why
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 import BN from 'bn.js';

+ 1 - 1
runtime-modules/content-working-group/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-content-working-group-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 11 - 1
runtime-modules/content-working-group/src/lib.rs

@@ -239,6 +239,10 @@ pub static MSG_ORIGIN_IS_NIETHER_MEMBER_CONTROLLER_OR_ROOT: &str =
     "Origin must be controller or root account of member";
 pub static MSG_MEMBER_HAS_ACTIVE_APPLICATION_ON_OPENING: &str =
     "Member already has an active application on the opening";
+pub static MSG_ADD_CURATOR_OPENING_ROLE_STAKE_CANNOT_BE_ZERO: &str =
+    "Add curator opening role stake cannot be zero";
+pub static MSG_ADD_CURATOR_OPENING_APPLICATION_STAKE_CANNOT_BE_ZERO: &str =
+    "Add curator opening application stake cannot be zero";
 
 /// The exit stage of a lead involvement in the working group.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
@@ -836,7 +840,7 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for &str {
             hiring::AddOpeningError::OpeningMustActivateInTheFuture => {
                 MSG_ADD_CURATOR_OPENING_ACTIVATES_IN_THE_PAST
             }
-            hiring::AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(purpose) => {
+            hiring::AddOpeningError::StakeAmountLessThanMinimumStakeBalance(purpose) => {
                 match purpose {
                     hiring::StakePurpose::Role => {
                         MSG_ADD_CURATOR_OPENING_ROLE_STAKE_LESS_THAN_MINIMUM
@@ -849,6 +853,12 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for &str {
             hiring::AddOpeningError::ApplicationRationingZeroMaxApplicants => {
                 MSG_ADD_CURATOR_OPENING_ZERO_MAX_APPLICANT_COUNT
             }
+            hiring::AddOpeningError::StakeAmountCannotBeZero(purpose) => match purpose {
+                hiring::StakePurpose::Role => MSG_ADD_CURATOR_OPENING_ROLE_STAKE_CANNOT_BE_ZERO,
+                hiring::StakePurpose::Application => {
+                    MSG_ADD_CURATOR_OPENING_APPLICATION_STAKE_CANNOT_BE_ZERO
+                }
+            },
         }
     }
 }

+ 1 - 1
runtime-modules/hiring/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-hiring-module'
-version = '1.0.1'
+version = '1.0.2'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 4 - 1
runtime-modules/hiring/src/hiring/mod.rs

@@ -149,9 +149,12 @@ pub enum AddOpeningError {
 
     /// It is not possible to stake less than the minimum balance defined in the
     /// `Currency` module.
-    StakeAmountLessThanMinimumCurrencyBalance(StakePurpose),
+    StakeAmountLessThanMinimumStakeBalance(StakePurpose),
 
     /// It is not possible to provide application rationing policy with zero
     /// 'max_active_applicants' parameter.
     ApplicationRationingZeroMaxApplicants,
+
+    /// It is not possible to stake zero.
+    StakeAmountCannotBeZero(StakePurpose),
 }

+ 0 - 43
runtime-modules/hiring/src/hiring/opening.rs

@@ -6,7 +6,6 @@ use rstd::vec::Vec;
 use codec::{Decode, Encode};
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
-use srml_support::ensure;
 
 use crate::hiring;
 use crate::hiring::*;
@@ -148,48 +147,6 @@ where
             panic!("stage MUST be active")
         }
     }
-
-    /// Performs all necessary check before adding an opening
-    pub(crate) fn ensure_can_add_opening(
-        current_block_height: BlockNumber,
-        activate_at: ActivateOpeningAt<BlockNumber>,
-        runtime_minimum_balance: Balance,
-        application_rationing_policy: Option<ApplicationRationingPolicy>,
-        application_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
-        role_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
-    ) -> Result<(), AddOpeningError> {
-        // Check that exact activation is actually in the future
-        ensure!(
-            match activate_at {
-                ActivateOpeningAt::ExactBlock(block_number) => block_number > current_block_height,
-                _ => true,
-            },
-            AddOpeningError::OpeningMustActivateInTheFuture
-        );
-
-        if let Some(app_rationing_policy) = application_rationing_policy {
-            ensure!(
-                app_rationing_policy.max_active_applicants > 0,
-                AddOpeningError::ApplicationRationingZeroMaxApplicants
-            );
-        }
-
-        // Check that staking amounts clear minimum balance required.
-        StakingPolicy::ensure_amount_valid_in_opt_staking_policy(
-            application_staking_policy,
-            runtime_minimum_balance.clone(),
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Application),
-        )?;
-
-        // Check that staking amounts clear minimum balance required.
-        StakingPolicy::ensure_amount_valid_in_opt_staking_policy(
-            role_staking_policy,
-            runtime_minimum_balance,
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Role),
-        )?;
-
-        Ok(())
-    }
 }
 
 /// The stage at which an `Opening` may be at.

+ 0 - 15
runtime-modules/hiring/src/hiring/staking_policy.rs

@@ -50,21 +50,6 @@ impl<Balance: PartialOrd + Clone, BlockNumber: Clone> StakingPolicy<Balance, Blo
             None
         }
     }
-
-    /// Ensures that optional staking policy prescribes value that clears minimum balance requirement
-    pub(crate) fn ensure_amount_valid_in_opt_staking_policy<Err>(
-        opt_staking_policy: Option<StakingPolicy<Balance, BlockNumber>>,
-        runtime_minimum_balance: Balance,
-        error: Err,
-    ) -> Result<(), Err> {
-        if let Some(ref staking_policy) = opt_staking_policy {
-            if staking_policy.amount < runtime_minimum_balance {
-                return Err(error);
-            }
-        }
-
-        Ok(())
-    }
 }
 
 /// Constraints around staking amount

+ 64 - 1
runtime-modules/hiring/src/lib.rs

@@ -184,7 +184,7 @@ impl<T: Trait> Module<T> {
     ) -> Result<T::OpeningId, AddOpeningError> {
         let current_block_height = <system::Module<T>>::block_number();
 
-        Opening::<BalanceOf<T>, T::BlockNumber, T::ApplicationId>::ensure_can_add_opening(
+        Self::ensure_can_add_opening(
             current_block_height,
             activate_at.clone(),
             T::Currency::minimum_balance(),
@@ -1406,6 +1406,69 @@ impl<T: Trait> Module<T> {
             None
         }
     }
+
+    /// Performs all necessary check before adding an opening
+    pub(crate) fn ensure_can_add_opening(
+        current_block_height: T::BlockNumber,
+        activate_at: ActivateOpeningAt<T::BlockNumber>,
+        minimum_stake_balance: BalanceOf<T>,
+        application_rationing_policy: Option<ApplicationRationingPolicy>,
+        application_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+        role_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+    ) -> Result<(), AddOpeningError> {
+        // Check that exact activation is actually in the future
+        ensure!(
+            match activate_at {
+                ActivateOpeningAt::ExactBlock(block_number) => block_number > current_block_height,
+                _ => true,
+            },
+            AddOpeningError::OpeningMustActivateInTheFuture
+        );
+
+        if let Some(app_rationing_policy) = application_rationing_policy {
+            ensure!(
+                app_rationing_policy.max_active_applicants > 0,
+                AddOpeningError::ApplicationRationingZeroMaxApplicants
+            );
+        }
+
+        // Check that staking amounts clear minimum balance required.
+        Self::ensure_amount_valid_in_opt_staking_policy(
+            application_staking_policy,
+            minimum_stake_balance,
+            StakePurpose::Application,
+        )?;
+
+        // Check that staking amounts clear minimum balance required.
+        Self::ensure_amount_valid_in_opt_staking_policy(
+            role_staking_policy,
+            minimum_stake_balance,
+            StakePurpose::Role,
+        )?;
+
+        Ok(())
+    }
+
+    /// Ensures that optional staking policy prescribes value that clears minimum balance requirement
+    pub(crate) fn ensure_amount_valid_in_opt_staking_policy(
+        opt_staking_policy: Option<StakingPolicy<BalanceOf<T>, T::BlockNumber>>,
+        minimum_stake_balance: BalanceOf<T>,
+        stake_purpose: StakePurpose,
+    ) -> Result<(), AddOpeningError> {
+        if let Some(ref staking_policy) = opt_staking_policy {
+            ensure!(
+                staking_policy.amount > Zero::zero(),
+                AddOpeningError::StakeAmountCannotBeZero(stake_purpose)
+            );
+
+            ensure!(
+                staking_policy.amount >= minimum_stake_balance,
+                AddOpeningError::StakeAmountLessThanMinimumStakeBalance(stake_purpose)
+            );
+        }
+
+        Ok(())
+    }
 }
 
 /*

+ 33 - 4
runtime-modules/hiring/src/test/public_api/add_opening.rs

@@ -1,6 +1,11 @@
-use crate::mock::*;
-use crate::test::*;
+use crate::mock::{build_test_externalities, Hiring, Test};
+use crate::test::{BlockNumber, OpeningId};
 use crate::StakingAmountLimitMode::Exact;
+use crate::*;
+use crate::{
+    ActivateOpeningAt, ActiveOpeningStage, AddOpeningError, ApplicationRationingPolicy, Opening,
+    OpeningStage, StakePurpose, StakingPolicy,
+};
 use rstd::collections::btree_set::BTreeSet;
 
 static FIRST_BLOCK_HEIGHT: <Test as system::Trait>::BlockNumber = 1;
@@ -143,6 +148,18 @@ fn add_opening_succeeds_or_fails_due_to_application_staking_policy() {
 
         opening_data.call_and_assert(Ok(0));
 
+        //Zero stake amount
+        opening_data.application_staking_policy = Some(StakingPolicy {
+            amount: 0,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Err(AddOpeningError::StakeAmountCannotBeZero(
+            StakePurpose::Application,
+        )));
+
         //Invalid stake amount
         opening_data.application_staking_policy = Some(StakingPolicy {
             amount: 1,
@@ -152,7 +169,7 @@ fn add_opening_succeeds_or_fails_due_to_application_staking_policy() {
         });
 
         opening_data.call_and_assert(Err(
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Application),
+            AddOpeningError::StakeAmountLessThanMinimumStakeBalance(StakePurpose::Application),
         ));
     });
 }
@@ -171,6 +188,18 @@ fn add_opening_succeeds_or_fails_due_to_role_staking_policy() {
 
         opening_data.call_and_assert(Ok(0));
 
+        //Zero stake amount
+        opening_data.role_staking_policy = Some(StakingPolicy {
+            amount: 0,
+            amount_mode: Exact,
+            crowded_out_unstaking_period_length: None,
+            review_period_expired_unstaking_period_length: None,
+        });
+
+        opening_data.call_and_assert(Err(AddOpeningError::StakeAmountCannotBeZero(
+            StakePurpose::Role,
+        )));
+
         //Invalid stake amount
         opening_data.role_staking_policy = Some(StakingPolicy {
             amount: 1,
@@ -180,7 +209,7 @@ fn add_opening_succeeds_or_fails_due_to_role_staking_policy() {
         });
 
         opening_data.call_and_assert(Err(
-            AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(StakePurpose::Role),
+            AddOpeningError::StakeAmountLessThanMinimumStakeBalance(StakePurpose::Role),
         ));
     });
 }

+ 48 - 11
runtime-modules/storage/src/data_directory.rs

@@ -22,8 +22,10 @@
 //#![warn(missing_docs)]
 
 use codec::{Decode, Encode};
+use rstd::collections::btree_map::BTreeMap;
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member};
+use srml_support::traits::Get;
 use srml_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
 use system::{self, ensure_root};
 
@@ -56,6 +58,8 @@ pub trait Trait:
 
     /// Validates member id and origin combination.
     type MemberOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
+
+    type MaxObjectsPerInjection: Get<u32>;
 }
 
 decl_error! {
@@ -75,6 +79,9 @@ decl_error! {
 
         /// Require root origin in extrinsics.
         RequireRootOrigin,
+
+        /// DataObject Injection Failed. Too Many DataObjects.
+        DataObjectsInjectionExceededLimit
     }
 }
 
@@ -116,23 +123,32 @@ impl Default for LiaisonJudgement {
     }
 }
 
+/// Alias for DataObjectInternal
+pub type DataObject<T> = DataObjectInternal<
+    MemberId<T>,
+    <T as system::Trait>::BlockNumber,
+    <T as timestamp::Trait>::Moment,
+    <T as data_object_type_registry::Trait>::DataObjectTypeId,
+    StorageProviderId<T>,
+>;
+
 /// Manages content ids, type and storage provider decision about it.
 #[derive(Clone, Encode, Decode, PartialEq, Debug)]
-pub struct DataObject<T: Trait> {
+pub struct DataObjectInternal<MemberId, BlockNumber, Moment, DataObjectTypeId, StorageProviderId> {
     /// Content owner.
-    pub owner: MemberId<T>,
+    pub owner: MemberId,
 
     /// Content added at.
-    pub added_at: BlockAndTime<T::BlockNumber, T::Moment>,
+    pub added_at: BlockAndTime<BlockNumber, Moment>,
 
     /// Content type id.
-    pub type_id: <T as data_object_type_registry::Trait>::DataObjectTypeId,
+    pub type_id: DataObjectTypeId,
 
     /// Content size in bytes.
     pub size: u64,
 
     /// Storage provider id of the liaison.
-    pub liaison: StorageProviderId<T>,
+    pub liaison: StorageProviderId,
 
     /// Storage provider as liaison judgment.
     pub liaison_judgement: LiaisonJudgement,
@@ -141,6 +157,9 @@ pub struct DataObject<T: Trait> {
     pub ipfs_content_id: Vec<u8>,
 }
 
+/// A map collection of unique DataObjects keyed by the ContentId
+pub type DataObjectsMap<T> = BTreeMap<<T as Trait>::ContentId, DataObject<T>>;
+
 decl_storage! {
     trait Store for Module<T: Trait> as DataDirectory {
         /// List of ids known to the system.
@@ -188,6 +207,8 @@ decl_module! {
         /// Predefined errors.
         type Error = Error;
 
+        /// Maximum objects allowed per inject_data_objects() transaction
+        const MaxObjectsPerInjection: u32 = T::MaxObjectsPerInjection::get();
 
         /// Adds the content to the system. Member id should match its origin. The created DataObject
         /// awaits liaison to accept or reject it.
@@ -213,7 +234,7 @@ decl_module! {
             let liaison = T::StorageProviderHelper::get_random_storage_provider()?;
 
             // Let's create the entry then
-            let data: DataObject<T> = DataObject {
+            let data: DataObject<T> = DataObjectInternal {
                 type_id,
                 size,
                 added_at: common::current_block_time::<T>(),
@@ -279,13 +300,29 @@ decl_module! {
             <KnownContentIds<T>>::put(upd_content_ids);
         }
 
-        /// Sets the content id from the list of known content ids. Requires root privileges.
-        fn set_known_content_id(origin, content_ids: Vec<T::ContentId>) {
+        /// Injects a set of data objects and their corresponding content id into the directory.
+        /// The operation is "silent" - no events will be emitted as objects are added.
+        /// The number of objects that can be added per call is limited to prevent the dispatch
+        /// from causing the block production to fail if it takes too much time to process.
+        /// Existing data objects will be overwritten.
+        pub(crate) fn inject_data_objects(origin, objects: DataObjectsMap<T>) {
             ensure_root(origin)?;
 
-            // == MUTATION SAFE ==
-
-            <KnownContentIds<T>>::put(content_ids);
+            // Must provide something to inject
+            ensure!(objects.len() <= T::MaxObjectsPerInjection::get() as usize, Error::DataObjectsInjectionExceededLimit);
+
+            for (id, object) in objects.into_iter() {
+                // append to known content ids
+                // duplicates will be removed at the end
+                <KnownContentIds<T>>::mutate(|ids| ids.push(id));
+                <DataObjectByContentId<T>>::insert(id, object);
+            }
+
+            // remove duplicate ids
+            <KnownContentIds<T>>::mutate(|ids| {
+                ids.sort();
+                ids.dedup();
+            });
         }
     }
 }

+ 129 - 0
runtime-modules/storage/src/tests/data_directory.rs

@@ -2,6 +2,7 @@
 
 use super::mock::*;
 use crate::data_directory::Error;
+use rstd::collections::btree_map::BTreeMap;
 use system::RawOrigin;
 
 #[test]
@@ -169,3 +170,131 @@ fn reject_content_as_liaison() {
         assert_eq!(res, Ok(()));
     });
 }
+
+#[test]
+fn data_object_injection_works() {
+    with_default_mock_builder(|| {
+        // No objects in directory before injection
+        assert_eq!(TestDataDirectory::known_content_ids(), vec![]);
+
+        // new objects to inject into the directory
+        let mut objects = BTreeMap::new();
+
+        let object = data_directory::DataObjectInternal {
+            type_id: 1,
+            size: 1234,
+            added_at: data_directory::BlockAndTime {
+                block: 10,
+                time: 1024,
+            },
+            owner: 1,
+            liaison: TEST_MOCK_LIAISON_STORAGE_PROVIDER_ID,
+            liaison_judgement: data_directory::LiaisonJudgement::Pending,
+            ipfs_content_id: vec![],
+        };
+
+        let content_id_1 = 1;
+        objects.insert(content_id_1, object.clone());
+
+        let content_id_2 = 2;
+        objects.insert(content_id_2, object.clone());
+
+        let res = TestDataDirectory::inject_data_objects(Origin::ROOT, objects);
+        assert!(res.is_ok());
+
+        assert_eq!(
+            TestDataDirectory::known_content_ids(),
+            vec![content_id_1, content_id_2]
+        );
+
+        assert_eq!(
+            TestDataDirectory::data_object_by_content_id(content_id_1),
+            Some(object.clone())
+        );
+
+        assert_eq!(
+            TestDataDirectory::data_object_by_content_id(content_id_2),
+            Some(object)
+        );
+    });
+}
+
+#[test]
+fn data_object_injection_overwrites_and_removes_duplicate_ids() {
+    with_default_mock_builder(|| {
+        let sender = 1u64;
+        let member_id = 1u64;
+        let content_id_1 = 1;
+        let content_id_2 = 2;
+
+        // Start with some existing objects in directory which will be
+        // overwritten
+        let res = TestDataDirectory::add_content(
+            Origin::signed(sender),
+            member_id,
+            content_id_1,
+            1,
+            10,
+            vec![8, 8, 8, 8],
+        );
+        assert!(res.is_ok());
+        let res = TestDataDirectory::add_content(
+            Origin::signed(sender),
+            member_id,
+            content_id_2,
+            2,
+            20,
+            vec![9, 9, 9, 9],
+        );
+        assert!(res.is_ok());
+
+        let mut objects = BTreeMap::new();
+
+        let object1 = data_directory::DataObjectInternal {
+            type_id: 1,
+            size: 6666,
+            added_at: data_directory::BlockAndTime {
+                block: 10,
+                time: 1000,
+            },
+            owner: 5,
+            liaison: TEST_MOCK_LIAISON_STORAGE_PROVIDER_ID,
+            liaison_judgement: data_directory::LiaisonJudgement::Pending,
+            ipfs_content_id: vec![5, 6, 7],
+        };
+
+        let object2 = data_directory::DataObjectInternal {
+            type_id: 1,
+            size: 7777,
+            added_at: data_directory::BlockAndTime {
+                block: 20,
+                time: 2000,
+            },
+            owner: 6,
+            liaison: TEST_MOCK_LIAISON_STORAGE_PROVIDER_ID,
+            liaison_judgement: data_directory::LiaisonJudgement::Pending,
+            ipfs_content_id: vec![5, 6, 7],
+        };
+
+        objects.insert(content_id_1, object1.clone());
+        objects.insert(content_id_2, object2.clone());
+
+        let res = TestDataDirectory::inject_data_objects(Origin::ROOT, objects);
+        assert!(res.is_ok());
+
+        assert_eq!(
+            TestDataDirectory::known_content_ids(),
+            vec![content_id_1, content_id_2]
+        );
+
+        assert_eq!(
+            TestDataDirectory::data_object_by_content_id(content_id_1),
+            Some(object1.clone())
+        );
+
+        assert_eq!(
+            TestDataDirectory::data_object_by_content_id(content_id_2),
+            Some(object2)
+        );
+    });
+}

+ 3 - 1
runtime-modules/storage/src/tests/mock.rs

@@ -63,7 +63,7 @@ impl ContentIdExists<Test> for MockContent {
         which: &<Test as data_directory::Trait>::ContentId,
     ) -> Result<data_directory::DataObject<Test>, &'static str> {
         match *which {
-            TEST_MOCK_EXISTING_CID => Ok(data_directory::DataObject {
+            TEST_MOCK_EXISTING_CID => Ok(data_directory::DataObjectInternal {
                 type_id: 1,
                 size: 1234,
                 added_at: data_directory::BlockAndTime {
@@ -89,6 +89,7 @@ parameter_types! {
     pub const MaximumBlockLength: u32 = 2 * 1024;
     pub const AvailableBlockRatio: Perbill = Perbill::one();
     pub const MinimumPeriod: u64 = 5;
+    pub const MaxObjectsPerInjection: u32 = 5;
 }
 
 impl system::Trait for Test {
@@ -166,6 +167,7 @@ impl data_directory::Trait for Test {
     type StorageProviderHelper = ();
     type IsActiveDataObjectType = AnyDataObjectTypeIsActive;
     type MemberOriginValidator = ();
+    type MaxObjectsPerInjection = MaxObjectsPerInjection;
 }
 
 impl crate::data_directory::StorageProviderHelper<Test> for () {

+ 1 - 1
runtime-modules/working-group/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-working-group-module'
-version = '1.0.0'
+version = '1.0.1'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 13 - 1
runtime-modules/working-group/src/errors.rs

@@ -248,6 +248,12 @@ decl_error! {
         /// Working group size limit exceeded.
         MaxActiveWorkerNumberExceeded,
 
+        /// Add worker opening role stake cannot be zero.
+        AddWorkerOpeningRoleStakeCannotBeZero,
+
+        /// Add worker opening application stake cannot be zero.
+        AddWorkerOpeningApplicationStakeCannotBeZero,
+
         /// Invalid OpeningPolicyCommitment parameter:
         /// fill_opening_failed_applicant_application_stake_unstaking_period should be non-zero.
         FillOpeningFailedApplicantApplicationStakeUnstakingPeriodIsZero,
@@ -306,7 +312,7 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for Error {
             hiring::AddOpeningError::OpeningMustActivateInTheFuture => {
                 Error::AddWorkerOpeningActivatesInThePast
             }
-            hiring::AddOpeningError::StakeAmountLessThanMinimumCurrencyBalance(purpose) => {
+            hiring::AddOpeningError::StakeAmountLessThanMinimumStakeBalance(purpose) => {
                 match purpose {
                     hiring::StakePurpose::Role => Error::AddWorkerOpeningRoleStakeLessThanMinimum,
                     hiring::StakePurpose::Application => {
@@ -317,6 +323,12 @@ impl rstd::convert::From<WrappedError<hiring::AddOpeningError>> for Error {
             hiring::AddOpeningError::ApplicationRationingZeroMaxApplicants => {
                 Error::AddWorkerOpeningZeroMaxApplicantCount
             }
+            hiring::AddOpeningError::StakeAmountCannotBeZero(purpose) => match purpose {
+                hiring::StakePurpose::Role => Error::AddWorkerOpeningRoleStakeCannotBeZero,
+                hiring::StakePurpose::Application => {
+                    Error::AddWorkerOpeningApplicationStakeCannotBeZero
+                }
+            },
         }
     }
 }

+ 191 - 174
runtime-modules/working-group/src/tests/mod.rs

@@ -91,13 +91,13 @@ fn add_opening_fails_with_incorrect_unstaking_periods() {
 }
 
 #[test]
-fn add_worker_opening_succeeds() {
+fn add_opening_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
 
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         EventFixture::assert_last_crate_event(RawEvent::OpeningAdded(opening_id));
     });
@@ -108,10 +108,10 @@ fn add_leader_opening_succeeds_fails_with_incorrect_origin_for_opening_type() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_opening_type(OpeningType::Leader);
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
+        add_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
     });
 }
 
@@ -120,25 +120,25 @@ fn add_leader_opening_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_opening_type(OpeningType::Leader)
             .with_origin(RawOrigin::Root);
 
-        add_worker_opening_fixture.call_and_assert(Ok(()));
+        add_opening_fixture.call_and_assert(Ok(()));
     });
 }
 
 #[test]
-fn add_worker_opening_fails_with_lead_is_not_set() {
+fn add_opening_fails_with_lead_is_not_set() {
     build_test_externalities().execute_with(|| {
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::CurrentLeadNotSet));
+        add_opening_fixture.call_and_assert(Err(Error::CurrentLeadNotSet));
     });
 }
 
 #[test]
-fn add_worker_opening_fails_with_invalid_human_readable_text() {
+fn add_opening_fails_with_invalid_human_readable_text() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
@@ -149,119 +149,119 @@ fn add_worker_opening_fails_with_invalid_human_readable_text() {
             },
         );
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default().with_text(Vec::new());
+        let add_opening_fixture = AddWorkerOpeningFixture::default().with_text(Vec::new());
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooShort")));
+        add_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooShort")));
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_text(b"Long text".to_vec());
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooLong")));
+        add_opening_fixture.call_and_assert(Err(Error::Other("OpeningTextTooLong")));
     });
 }
 
 #[test]
-fn add_worker_opening_fails_with_hiring_error() {
+fn add_opening_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(0));
 
-        add_worker_opening_fixture.call_and_assert(Err(Error::AddWorkerOpeningActivatesInThePast));
+        add_opening_fixture.call_and_assert(Err(Error::AddWorkerOpeningActivatesInThePast));
     });
 }
 
 #[test]
-fn accept_worker_applications_succeeds() {
+fn accept_applications_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(5));
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Ok(()));
+        accept_applications_fixture.call_and_assert(Ok(()));
 
         EventFixture::assert_last_crate_event(RawEvent::AcceptedApplications(opening_id));
     });
 }
 
 #[test]
-fn accept_worker_applications_fails_for_invalid_opening_type() {
+fn accept_applications_fails_for_invalid_opening_type() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_origin(RawOrigin::Root)
             .with_opening_type(OpeningType::Leader)
             .with_activate_at(hiring::ActivateOpeningAt::ExactBlock(5));
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(Error::RequireRootOrigin));
+        accept_applications_fixture.call_and_assert(Err(Error::RequireRootOrigin));
     });
 }
 
 #[test]
-fn accept_worker_applications_fails_with_hiring_error() {
+fn accept_applications_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(
+        accept_applications_fixture.call_and_assert(Err(
             Error::AcceptWorkerApplicationsOpeningIsNotWaitingToBegin,
         ));
     });
 }
 
 #[test]
-fn accept_worker_applications_fails_with_not_lead() {
+fn accept_applications_fails_with_not_lead() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
+        accept_applications_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
     });
 }
 
 #[test]
-fn accept_worker_applications_fails_with_no_opening() {
+fn accept_applications_fails_with_no_opening() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
         let opening_id = 55; // random opening id
 
-        let accept_worker_applications_fixture =
+        let accept_applications_fixture =
             AcceptWorkerApplicationsFixture::default_for_opening_id(opening_id);
-        accept_worker_applications_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
+        accept_applications_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
     });
 }
 
 #[test]
-fn apply_on_worker_opening_succeeds() {
+fn apply_on_opening_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         EventFixture::assert_last_crate_event(RawEvent::AppliedOnOpening(
             opening_id,
@@ -271,59 +271,58 @@ fn apply_on_worker_opening_succeeds() {
 }
 
 #[test]
-fn apply_on_worker_opening_fails_with_no_opening() {
+fn apply_on_opening_fails_with_no_opening() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
         let opening_id = 123; // random opening id
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        appy_on_worker_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
+        apply_on_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
     });
 }
 
 #[test]
-fn apply_on_worker_opening_fails_with_not_set_members() {
+fn apply_on_opening_fails_with_not_set_members() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_origin(RawOrigin::Signed(55), 55);
-        appy_on_worker_opening_fixture
-            .call_and_assert(Err(Error::OriginIsNeitherMemberControllerOrRoot));
+        apply_on_opening_fixture.call_and_assert(Err(Error::OriginIsNeitherMemberControllerOrRoot));
     });
 }
 
 #[test]
-fn apply_on_worker_opening_fails_with_hiring_error() {
+fn apply_on_opening_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
         increase_total_balance_issuance_using_account_id(1, 500000);
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_application_stake(100);
-        appy_on_worker_opening_fixture
+        apply_on_opening_fixture
             .call_and_assert(Err(Error::AddWorkerOpeningStakeProvidedWhenRedundant));
     });
 }
 
 #[test]
-fn apply_on_worker_opening_fails_with_invalid_application_stake() {
+fn apply_on_opening_fails_with_invalid_application_stake() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
         let stake = 100;
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
                 application_staking_policy: Some(hiring::StakingPolicy {
                     amount: stake,
@@ -331,24 +330,45 @@ fn apply_on_worker_opening_fails_with_invalid_application_stake() {
                 }),
                 ..OpeningPolicyCommitment::default()
             });
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_origin(RawOrigin::Signed(2), 2)
                 .with_application_stake(stake);
-        appy_on_worker_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
+        apply_on_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
     });
 }
 
 #[test]
-fn apply_on_worker_opening_fails_with_invalid_role_stake() {
+fn add_opening_fails_with_invalid_zero_application_stake() {
+    build_test_externalities().execute_with(|| {
+        HireLeadFixture::default().hire_lead();
+
+        let zero_stake = 0;
+
+        let add_opening_fixture =
+            AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
+                application_staking_policy: Some(hiring::StakingPolicy {
+                    amount: zero_stake,
+                    amount_mode: hiring::StakingAmountLimitMode::AtLeast,
+                    ..hiring::StakingPolicy::default()
+                }),
+                ..OpeningPolicyCommitment::default()
+            });
+        add_opening_fixture
+            .call_and_assert(Err(Error::AddWorkerOpeningApplicationStakeCannotBeZero));
+    });
+}
+
+#[test]
+fn apply_on_opening_fails_with_invalid_role_stake() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
         let stake = 100;
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
                 role_staking_policy: Some(hiring::StakingPolicy {
                     amount: stake,
@@ -356,23 +376,23 @@ fn apply_on_worker_opening_fails_with_invalid_role_stake() {
                 }),
                 ..OpeningPolicyCommitment::default()
             });
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_role_stake(Some(stake))
                 .with_origin(RawOrigin::Signed(2), 2);
-        appy_on_worker_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
+        apply_on_opening_fixture.call_and_assert(Err(Error::InsufficientBalanceToApply));
     });
 }
 
 #[test]
-fn apply_on_worker_opening_fails_with_invalid_text() {
+fn apply_on_opening_fails_with_invalid_text() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         <crate::WorkerApplicationHumanReadableText<TestWorkingGroupInstance>>::put(
             InputValidationLengthConstraint {
@@ -381,33 +401,31 @@ fn apply_on_worker_opening_fails_with_invalid_text() {
             },
         );
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id).with_text(Vec::new());
-        appy_on_worker_opening_fixture
+        apply_on_opening_fixture
             .call_and_assert(Err(Error::Other("WorkerApplicationTextTooShort")));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_text(b"Long text".to_vec());
-        appy_on_worker_opening_fixture
-            .call_and_assert(Err(Error::Other("WorkerApplicationTextTooLong")));
+        apply_on_opening_fixture.call_and_assert(Err(Error::Other("WorkerApplicationTextTooLong")));
     });
 }
 
 #[test]
-fn apply_on_worker_opening_fails_with_already_active_application() {
+fn apply_on_opening_fails_with_already_active_application() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        apply_on_opening_fixture.call_and_assert(Ok(()));
 
-        appy_on_worker_opening_fixture
-            .call_and_assert(Err(Error::MemberHasActiveApplicationOnOpening));
+        apply_on_opening_fixture.call_and_assert(Err(Error::MemberHasActiveApplicationOnOpening));
     });
 }
 
@@ -416,12 +434,12 @@ fn withdraw_worker_application_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let withdraw_application_fixture =
             WithdrawApplicationFixture::default_for_application_id(application_id);
@@ -447,12 +465,12 @@ fn withdraw_worker_application_fails_invalid_origin() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let withdraw_application_fixture =
             WithdrawApplicationFixture::default_for_application_id(application_id)
@@ -466,12 +484,12 @@ fn withdraw_worker_application_fails_with_invalid_application_author() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let invalid_author_account_id = 55;
         let withdraw_application_fixture =
@@ -486,12 +504,12 @@ fn withdraw_worker_application_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let withdraw_application_fixture =
             WithdrawApplicationFixture::default_for_application_id(application_id);
@@ -506,12 +524,12 @@ fn terminate_worker_application_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let terminate_application_fixture =
             TerminateApplicationFixture::default_for_application_id(application_id);
@@ -526,12 +544,12 @@ fn terminate_worker_application_fails_with_invalid_application_author() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let invalid_author_account_id = 55;
         let terminate_application_fixture =
@@ -546,12 +564,12 @@ fn terminate_worker_application_fails_invalid_origin() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let terminate_application_fixture =
             TerminateApplicationFixture::default_for_application_id(application_id)
@@ -578,12 +596,12 @@ fn terminate_worker_application_fails_with_hiring_error() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let terminate_application_fixture =
             TerminateApplicationFixture::default_for_application_id(application_id);
@@ -598,8 +616,8 @@ fn begin_review_worker_applications_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -614,10 +632,10 @@ fn begin_review_worker_applications_fails_with_invalid_origin_for_opening_type()
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_origin(RawOrigin::Root)
             .with_opening_type(OpeningType::Leader);
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -630,8 +648,8 @@ fn begin_review_worker_applications_fails_with_not_a_lead() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
 
@@ -659,8 +677,8 @@ fn begin_review_worker_applications_with_hiring_error() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -676,8 +694,8 @@ fn begin_review_worker_applications_fails_with_invalid_origin() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id)
@@ -687,12 +705,12 @@ fn begin_review_worker_applications_fails_with_invalid_origin() {
 }
 
 #[test]
-fn fill_worker_opening_succeeds() {
+fn fill_opening_succeeds() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         increase_total_balance_issuance_using_account_id(1, 10000);
 
-        let add_worker_opening_fixture =
+        let add_opening_fixture =
             AddWorkerOpeningFixture::default().with_policy_commitment(OpeningPolicyCommitment {
                 role_staking_policy: Some(hiring::StakingPolicy {
                     amount: 10,
@@ -702,12 +720,12 @@ fn fill_worker_opening_succeeds() {
                 }),
                 ..OpeningPolicyCommitment::default()
             });
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_role_stake(Some(10));
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -716,14 +734,14 @@ fn fill_worker_opening_succeeds() {
         let mint_id = create_mint();
         set_mint_id(mint_id);
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
                 .with_reward_policy(RewardPolicy {
                     amount_per_payout: 1000,
                     next_payment_at_block: 20,
                     payout_interval: None,
                 });
-        let worker_id = fill_worker_opening_fixture.call_and_assert(Ok(()));
+        let worker_id = fill_opening_fixture.call_and_assert(Ok(()));
 
         let mut worker_application_dictionary = BTreeMap::new();
         worker_application_dictionary.insert(application_id, worker_id);
@@ -736,12 +754,12 @@ fn fill_worker_opening_succeeds() {
 }
 
 #[test]
-fn fill_worker_opening_fails_with_invalid_origin_for_opening_type() {
+fn fill_opening_fails_with_invalid_origin_for_opening_type() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
         increase_total_balance_issuance_using_account_id(1, 10000);
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default()
+        let add_opening_fixture = AddWorkerOpeningFixture::default()
             .with_policy_commitment(OpeningPolicyCommitment {
                 role_staking_policy: Some(hiring::StakingPolicy {
                     amount: 10,
@@ -753,12 +771,12 @@ fn fill_worker_opening_fails_with_invalid_origin_for_opening_type() {
             })
             .with_opening_type(OpeningType::Leader)
             .with_origin(RawOrigin::Root);
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id)
                 .with_role_stake(Some(10));
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id)
@@ -767,119 +785,118 @@ fn fill_worker_opening_fails_with_invalid_origin_for_opening_type() {
 
         set_mint_id(create_mint());
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
                 .with_reward_policy(RewardPolicy {
                     amount_per_payout: 1000,
                     next_payment_at_block: 20,
                     payout_interval: None,
                 });
-        fill_worker_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
+        fill_opening_fixture.call_and_assert(Err(Error::RequireRootOrigin));
     });
 }
 
 #[test]
-fn fill_worker_opening_fails_with_invalid_origin() {
+fn fill_opening_fails_with_invalid_origin() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new())
                 .with_origin(RawOrigin::None);
-        fill_worker_opening_fixture.call_and_assert(Err(Error::RequireSignedOrigin));
+        fill_opening_fixture.call_and_assert(Err(Error::RequireSignedOrigin));
     });
 }
 
 #[test]
-fn fill_worker_opening_fails_with_not_a_lead() {
+fn fill_opening_fails_with_not_a_lead() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
         SetLeadFixture::set_lead_with_ids(2, 2, 2);
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
-        fill_worker_opening_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
+        fill_opening_fixture.call_and_assert(Err(Error::IsNotLeadAccount));
     });
 }
 
 #[test]
-fn fill_worker_opening_fails_with_invalid_opening() {
+fn fill_opening_fails_with_invalid_opening() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
         let invalid_opening_id = 6;
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(invalid_opening_id, Vec::new());
-        fill_worker_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
+        fill_opening_fixture.call_and_assert(Err(Error::OpeningDoesNotExist));
     });
 }
 
 #[test]
-fn fill_worker_opening_fails_with_invalid_application_list() {
+fn fill_opening_fails_with_invalid_application_list() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
         begin_review_worker_applications_fixture.call_and_assert(Ok(()));
 
         let invalid_application_id = 66;
-        let fill_worker_opening_fixture = FillWorkerOpeningFixture::default_for_ids(
+        let fill_opening_fixture = FillWorkerOpeningFixture::default_for_ids(
             opening_id,
             vec![application_id, invalid_application_id],
         );
-        fill_worker_opening_fixture
-            .call_and_assert(Err(Error::SuccessfulWorkerApplicationDoesNotExist));
+        fill_opening_fixture.call_and_assert(Err(Error::SuccessfulWorkerApplicationDoesNotExist));
     });
 }
 
 #[test]
-fn fill_worker_opening_fails_with_invalid_application_with_hiring_error() {
+fn fill_opening_fails_with_invalid_application_with_hiring_error() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, Vec::new());
-        fill_worker_opening_fixture
+        fill_opening_fixture
             .call_and_assert(Err(Error::FullWorkerOpeningOpeningNotInReviewPeriodStage));
     });
 }
 
 #[test]
-fn fill_worker_opening_fails_with_invalid_reward_policy() {
+fn fill_opening_fails_with_invalid_reward_policy() {
     build_test_externalities().execute_with(|| {
         HireLeadFixture::default().hire_lead();
 
-        let add_worker_opening_fixture = AddWorkerOpeningFixture::default();
-        let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
+        let add_opening_fixture = AddWorkerOpeningFixture::default();
+        let opening_id = add_opening_fixture.call_and_assert(Ok(()));
 
-        let appy_on_worker_opening_fixture =
+        let apply_on_opening_fixture =
             ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
-        let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
+        let application_id = apply_on_opening_fixture.call_and_assert(Ok(()));
 
         let begin_review_worker_applications_fixture =
             BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
         begin_review_worker_applications_fixture.call_and_assert(Ok(()));
 
-        let fill_worker_opening_fixture =
+        let fill_opening_fixture =
             FillWorkerOpeningFixture::default_for_ids(opening_id, vec![application_id])
                 .with_reward_policy(RewardPolicy {
                     amount_per_payout: 10000,
@@ -887,7 +904,7 @@ fn fill_worker_opening_fails_with_invalid_reward_policy() {
                     next_payment_at_block: 0,
                     payout_interval: None,
                 });
-        fill_worker_opening_fixture
+        fill_opening_fixture
     });
 }
 

+ 1 - 1
runtime/Cargo.toml

@@ -4,7 +4,7 @@ edition = '2018'
 name = 'joystream-node-runtime'
 # Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
 # {Authoring}.{Spec}.{Impl} of the RuntimeVersion
-version = '6.18.0'
+version = '6.19.0'
 
 [features]
 default = ['std']

+ 6 - 1
runtime/src/lib.rs

@@ -161,7 +161,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
     spec_name: create_runtime_str!("joystream-node"),
     impl_name: create_runtime_str!("joystream-node"),
     authoring_version: 6,
-    spec_version: 18,
+    spec_version: 19,
     impl_version: 0,
     apis: RUNTIME_API_VERSIONS,
 };
@@ -560,6 +560,10 @@ impl memo::Trait for Runtime {
     type Event = Event;
 }
 
+parameter_types! {
+    pub const MaxObjectsPerInjection: u32 = 100;
+}
+
 impl storage::data_object_type_registry::Trait for Runtime {
     type Event = Event;
     type DataObjectTypeId = u64;
@@ -571,6 +575,7 @@ impl storage::data_directory::Trait for Runtime {
     type StorageProviderHelper = integration::storage::StorageProviderHelper;
     type IsActiveDataObjectType = DataObjectTypeRegistry;
     type MemberOriginValidator = MembershipOriginValidator<Self>;
+    type MaxObjectsPerInjection = MaxObjectsPerInjection;
 }
 
 impl storage::data_object_storage_registry::Trait for Runtime {

+ 29 - 27
runtime/src/migration.rs

@@ -3,7 +3,10 @@
 
 use crate::VERSION;
 use rstd::prelude::*;
-use sr_primitives::{print, traits::Zero};
+use sr_primitives::{
+    print,
+    traits::{One, Zero},
+};
 use srml_support::{debug, decl_event, decl_module, decl_storage};
 
 impl<T: Trait> Module<T> {
@@ -21,8 +24,7 @@ impl<T: Trait> Module<T> {
 
         Self::initialize_storage_working_group_mint();
         Self::initialize_storage_working_group_text_constraints();
-        // temporary comment storage migration
-        //        Self::clear_storage_data();
+        Self::clear_storage_data();
     }
 }
 
@@ -98,28 +100,28 @@ impl<T: Trait> Module<T> {
         );
     }
 
-    // fn clear_storage_data() {
-    //     // Clear storage data object registry data.
-    //     for id in <storage::data_directory::Module<T>>::known_content_ids() {
-    //         <storage::data_object_storage_registry::RelationshipsByContentId<T>>::remove(id);
-    //     }
-    //
-    //     let mut potential_id = <T as storage::data_object_storage_registry::Trait>::DataObjectStorageRelationshipId::zero();
-    //     while potential_id
-    //         < storage::data_object_storage_registry::Module::<T>::next_relationship_id()
-    //     {
-    //         <storage::data_object_storage_registry::Relationships<T>>::remove(&potential_id);
-    //
-    //         potential_id += <T as storage::data_object_storage_registry::Trait>::DataObjectStorageRelationshipId::one();
-    //     }
-    //
-    //     storage::data_object_storage_registry::NextRelationshipId::<T>::kill();
-    //
-    //     // Clear storage data directory data.
-    //     for id in <storage::data_directory::Module<T>>::known_content_ids() {
-    //         <storage::data_directory::DataObjectByContentId<T>>::remove(id);
-    //     }
-    //
-    //     <storage::data_directory::KnownContentIds<T>>::kill();
-    // }
+    fn clear_storage_data() {
+        // Clear storage data object registry data.
+        for id in <storage::data_directory::Module<T>>::known_content_ids() {
+            <storage::data_object_storage_registry::RelationshipsByContentId<T>>::remove(id);
+        }
+
+        let mut potential_id = <T as storage::data_object_storage_registry::Trait>::DataObjectStorageRelationshipId::zero();
+        while potential_id
+            < storage::data_object_storage_registry::Module::<T>::next_relationship_id()
+        {
+            <storage::data_object_storage_registry::Relationships<T>>::remove(&potential_id);
+
+            potential_id += <T as storage::data_object_storage_registry::Trait>::DataObjectStorageRelationshipId::one();
+        }
+
+        storage::data_object_storage_registry::NextRelationshipId::<T>::kill();
+
+        // Clear storage data directory data.
+        for id in <storage::data_directory::Module<T>>::known_content_ids() {
+            <storage::data_directory::DataObjectByContentId<T>>::remove(id);
+        }
+
+        <storage::data_directory::KnownContentIds<T>>::kill();
+    }
 }

+ 33 - 40
storage-node/.eslintrc.js

@@ -1,42 +1,35 @@
 module.exports = {
-    env: {
-        node: true,
-        es6: true,
-		mocha: true,
+  env: {
+    node: true,
+    es6: true,
+    mocha: true,
+  },
+  rules: {
+    'import/no-commonjs': 'off', // remove after converting to TS.
+    // Disabling Rules because of monorepo environment:
+    // https://github.com/benmosher/eslint-plugin-import/issues/1174
+    'import/no-extraneous-dependencies': 'off',
+    'import/no-nodejs-modules': 'off', // nodejs project
+    'no-console': 'off', // we use console in the project
+    '@typescript-eslint/no-var-requires': 'warn',
+    '@typescript-eslint/camelcase': 'warn',
+  },
+  overrides: [
+    {
+      files: [
+        '**/test/ranges.js',
+        '**/test/lru.js',
+        '**/test/fs/walk.js',
+        '**/test/storage.js',
+        '**/test/identities.js',
+        '**/test/balances.js',
+        '**/test/assets.js',
+      ],
+      rules: {
+        // Disabling Rules because of used chai lib:
+        // https://stackoverflow.com/questions/45079454/no-unused-expressions-in-mocha-chai-unit-test-using-standardjs
+        'no-unused-expressions': 'off',
+      },
     },
-    globals: {
-        Atomics: "readonly",
-        SharedArrayBuffer: "readonly",
-    },
-    extends: [
-        "esnext",
-        "esnext/style-guide",
-        "plugin:prettier/recommended"
-    ],
-	"rules": {
-		"import/no-commonjs": "off", // remove after converting to TS.
-		// Disabling Rules because of monorepo environment:
-		// https://github.com/benmosher/eslint-plugin-import/issues/1174
-		"import/no-extraneous-dependencies": "off",
-		"import/no-nodejs-modules": "off", // nodejs project
-		"no-console": "off" // we use console in the project
-	},
-	"overrides": [
-		{
-			"files": [
-				"**/test/ranges.js",
-				"**/test/lru.js",
-				"**/test/fs/walk.js",
-				"**/test/storage.js",
-				"**/test/identities.js",
-				"**/test/balances.js",
-				"**/test/assets.js",
-			],
-			"rules": {
-				// Disabling Rules because of used chai lib:
-				// https://stackoverflow.com/questions/45079454/no-unused-expressions-in-mocha-chai-unit-test-using-standardjs
-				"no-unused-expressions": "off",
-			}
-		}
-	]
-};
+  ],
+}

+ 0 - 8
storage-node/.prettierrc

@@ -1,8 +0,0 @@
-{
-    "semi": false,
-    "trailingComma": "es5",
-    "singleQuote": true,
-	"arrowParens": "avoid",
-	"useTabs": false,
-	"tabWidth": 2
-}

+ 3 - 1
storage-node/package.json

@@ -33,7 +33,9 @@
   "scripts": {
     "test": "wsrun --serial test",
     "lint": "eslint --ignore-path .gitignore .",
-    "build": "yarn workspace @joystream/storage-cli run build"
+    "build": "yarn workspace @joystream/storage-cli run build",
+    "checks": "yarn lint && prettier . --check",
+    "format": "prettier ./ --write"
   },
   "devDependencies": {
     "@types/chai": "^4.2.11",

+ 5 - 0
tests/network-tests/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+    env: {
+        node: true,
+    }
+}

+ 0 - 6
tests/network-tests/.prettierrc

@@ -1,6 +0,0 @@
-{
-  "singleQuote": true,
-  "arrowParens": "avoid",
-  "useTabs": false,
-  "tabWidth": 2
-}

+ 3 - 3
tests/network-tests/package.json

@@ -6,8 +6,9 @@
     "build": "tsc --build tsconfig.json",
     "test": "tap --files ts-node/register src/constantinople/tests/proposals/*Test.ts",
     "test-migration": "tap --files src/rome/tests/romeRuntimeUpgradeTest.ts --files src/constantinople/tests/electingCouncilTest.ts",
-    "lint": "tslint --project tsconfig.json",
-    "prettier": "prettier --write ./src"
+    "lint": "eslint . --quiet --ext .ts",
+    "checks": "yarn lint && tsc --noEmit --pretty && prettier . --check",
+    "format": "prettier ./ --write "
   },
   "dependencies": {
     "@constantinople/types": "./types",
@@ -29,7 +30,6 @@
     "prettier": "2.0.2",
     "tap": "^14.10.7",
     "ts-node": "^8.8.1",
-    "tslint": "^6.1.0",
     "typescript": "^3.8.3"
   }
 }

+ 0 - 8
tests/network-tests/tslint.json

@@ -1,8 +0,0 @@
-{
-  "extends": ["tslint:recommended"],
-  "rules": {
-    "interface-name": [true, "never-prefix"],
-    "max-line-length": [true, 140],
-    "no-console": false
-  }
-}

+ 3 - 0
tsconfig.json

@@ -0,0 +1,3 @@
+{
+
+}

+ 32 - 0
types/src/JoyEnum.ts

@@ -0,0 +1,32 @@
+import { Constructor } from '@polkadot/types/types';
+import { Enum } from '@polkadot/types/codec';
+import { EnumConstructor } from '@polkadot/types/codec/Enum';
+
+export interface ExtendedEnum<Types extends Record<string, Constructor>> extends Enum {
+  isOfType: (type: keyof Types) => boolean;
+  asType<TypeKey extends keyof Types>(type: TypeKey): InstanceType<Types[TypeKey]>;
+};
+
+// Helper for creating extended Enum type with TS-compatible isOfType and asType helpers
+export function JoyEnum<Types extends Record<string, Constructor>>(types: Types): EnumConstructor<ExtendedEnum<Types>>
+{
+  // Unique values check
+  if (Object.values(types).some((val, i) => Object.values(types).indexOf(val, i + 1) !== -1)) {
+    throw new Error('Values passed to JoyEnum are not unique. Create an individual class for each value.');
+  }
+
+  return class extends Enum {
+    constructor(value?: any, index?: number) {
+      super(types, value, index);
+    }
+    public isOfType(typeKey: keyof Types) {
+      return this.value instanceof types[typeKey];
+    }
+    public asType<TypeKey extends keyof Types>(typeKey: TypeKey) {
+      if (!(this.value instanceof types[typeKey])) {
+        throw new Error(`Enum.asType(${typeKey}) - value is not of type ${typeKey}`);
+      }
+      return this.value as InstanceType<Types[TypeKey]>;
+    }
+  }
+}

+ 11 - 2
types/src/common.ts

@@ -1,10 +1,12 @@
-import { Struct, Option, Text, bool, Vec, u16, u32, u64, getTypeRegistry } from "@polkadot/types";
+import { Struct, Option, Text, bool, Vec, u16, u32, u64, getTypeRegistry, Null } from "@polkadot/types";
 import { BlockNumber, Moment } from '@polkadot/types/interfaces';
 import { Codec } from "@polkadot/types/types";
 // we get 'moment' because it is a dependency of @polkadot/util, via @polkadot/keyring
 import moment from 'moment';
 import { JoyStruct } from './JoyStruct';
 export { JoyStruct } from './JoyStruct';
+import { JoyEnum } from './JoyEnum';
+export { JoyEnum } from './JoyEnum';
 
 // Treat a BTreeSet as a Vec since it is encoded in the same way
 export class BTreeSet<T extends Codec> extends Vec<T> {}
@@ -107,6 +109,12 @@ export class InputValidationLengthConstraint extends JoyStruct<InputValidationLe
     }
 }
 
+export const WorkingGroupDef = {
+  Storage: Null
+} as const;
+export type WorkingGroupKeys = keyof typeof WorkingGroupDef;
+export class WorkingGroup extends JoyEnum(WorkingGroupDef) { };
+
 export function registerCommonTypes() {
     const typeRegistry = getTypeRegistry();
 
@@ -117,6 +125,7 @@ export function registerCommonTypes() {
       ThreadId,
       PostId,
       InputValidationLengthConstraint,
-      BTreeSet // Is this even necessary?
+      BTreeSet, // Is this even necessary?
+      WorkingGroup
     });
 }

+ 3 - 19
types/src/content-working-group/index.ts

@@ -1,5 +1,5 @@
-import { getTypeRegistry, BTreeMap, Enum, bool, u8, u32, u128, Text, GenericAccountId, Null , Option, Vec, u16 } from '@polkadot/types';
-import { BlockNumber, AccountId, Balance } from '@polkadot/types/interfaces';
+import { getTypeRegistry, BTreeMap, Enum, bool, u8, u32, Text, GenericAccountId, Null , Option, Vec, u16 } from '@polkadot/types';
+import { BlockNumber, AccountId } from '@polkadot/types/interfaces';
 import { BTreeSet, JoyStruct, OptionText, Credential } from '../common';
 import { ActorId, MemberId } from '../members';
 import { StakeId } from '../stake';
@@ -526,21 +526,6 @@ export class CuratorApplicationIdToCuratorIdMap extends BTreeMap<ApplicationId,
   }
 }
 
-export type IRewardPolicy = {
-  amount_per_payout: Balance,
-  next_payment_at_block: BlockNumber,
-  payout_interval: Option<BlockNumber>,
-};
-export class RewardPolicy extends JoyStruct<IRewardPolicy> {
-  constructor (value?: IRewardPolicy) {
-    super({
-      amount_per_payout: u128,
-      next_payment_at_block: u32,
-      payout_interval: Option.with(u32),
-    }, value);
-  }
-};
-
 export function registerContentWorkingGroupTypes () {
   try {
     getTypeRegistry().register({
@@ -564,8 +549,7 @@ export function registerContentWorkingGroupTypes () {
       Principal,
       WorkingGroupUnstaker,
       CuratorApplicationIdToCuratorIdMap,
-      CuratorApplicationIdSet: Vec.with(CuratorApplicationId),
-      RewardPolicy,
+      CuratorApplicationIdSet: Vec.with(CuratorApplicationId)
     });
   } catch (err) {
     console.error('Failed to register custom types of content working group module', err);

+ 56 - 38
types/src/hiring/index.ts

@@ -1,7 +1,7 @@
 import { getTypeRegistry, Null, u128, u64, u32, Vec, Option, Text } from '@polkadot/types';
 import { Enum } from '@polkadot/types/codec';
 import { BlockNumber, Balance } from '@polkadot/types/interfaces';
-import { JoyStruct } from '../common';
+import { JoyStruct, JoyEnum } from '../common';
 import { StakeId } from '../stake';
 
 import { GenericJoyStreamRoleSchema } from './schemas/role.schema.typings'
@@ -99,7 +99,7 @@ export class ApplicationStage extends Enum {
   constructor(value?: any, index?: number) {
     super(
       {
-        [ApplicationStageKeys.Active]: Null,
+        [ApplicationStageKeys.Active]: ActiveApplicationStage,
         [ApplicationStageKeys.Unstaking]: UnstakingApplicationStage,
         [ApplicationStageKeys.Inactive]: InactiveApplicationStage,
       },
@@ -137,6 +137,7 @@ export class WaitingToBeingOpeningStageVariant extends JoyStruct<WaitingToBeingO
   }
 };
 
+// TODO: Find usages and replace them with JoyEnum helpers
 export enum OpeningDeactivationCauseKeys {
   CancelledBeforeActivation = 'CancelledBeforeActivation',
   CancelledAcceptingApplications = 'CancelledAcceptingApplications',
@@ -145,19 +146,19 @@ export enum OpeningDeactivationCauseKeys {
   Filled = 'Filled',
 }
 
-export class OpeningDeactivationCause extends Enum {
-  constructor(value?: any, index?: number) {
-    super(
-      [
-        OpeningDeactivationCauseKeys.CancelledBeforeActivation,
-        OpeningDeactivationCauseKeys.CancelledAcceptingApplications,
-        OpeningDeactivationCauseKeys.CancelledInReviewPeriod,
-        OpeningDeactivationCauseKeys.ReviewPeriodExpired,
-        OpeningDeactivationCauseKeys.Filled,
-      ],
-      value, index);
-  }
-};
+class OpeningDeactivationCause_CancelledBeforeActivation extends Null { };
+class OpeningDeactivationCause_CancelledAcceptingApplications extends Null { };
+class OpeningDeactivationCause_CancelledInReviewPeriod extends Null { };
+class OpeningDeactivationCause_ReviewPeriodExpired extends Null { };
+class OpeningDeactivationCause_Filled extends Null { };
+
+export class OpeningDeactivationCause extends JoyEnum({
+  'CancelledBeforeActivation': OpeningDeactivationCause_CancelledBeforeActivation,
+  'CancelledAcceptingApplications': OpeningDeactivationCause_CancelledAcceptingApplications,
+  'CancelledInReviewPeriod': OpeningDeactivationCause_CancelledInReviewPeriod,
+  'ReviewPeriodExpired': OpeningDeactivationCause_ReviewPeriodExpired,
+  'Filled': OpeningDeactivationCause_Filled,
+} as const) { };
 
 export type IAcceptingApplications = {
   started_accepting_applicants_at_block: BlockNumber,
@@ -228,23 +229,14 @@ export class Deactivated extends JoyStruct<IDeactivated> {
   }
 };
 
+// TODO: Find usages and replace them with JoyEnum helpers
 export enum ActiveOpeningStageKeys {
   AcceptingApplications = 'AcceptingApplications',
   ReviewPeriod = 'ReviewPeriod',
   Deactivated = 'Deactivated',
 }
 
-export class ActiveOpeningStage extends Enum {
-  constructor(value?: any, index?: number) {
-    super(
-      {
-        [ActiveOpeningStageKeys.AcceptingApplications]: AcceptingApplications,
-        [ActiveOpeningStageKeys.ReviewPeriod]: ReviewPeriod,
-        [ActiveOpeningStageKeys.Deactivated]: Deactivated,
-      },
-      value, index);
-  }
-}
+export class ActiveOpeningStage extends JoyEnum({AcceptingApplications, ReviewPeriod, Deactivated} as const) { }
 
 export type ActiveOpeningStageVariantType = {
   stage: ActiveOpeningStage,
@@ -278,21 +270,16 @@ export class ActiveOpeningStageVariant extends JoyStruct<ActiveOpeningStageVaria
   }
 }
 
+// TODO: Find usages and replace them with JoyEnum helpers
 export enum OpeningStageKeys {
   WaitingToBegin = 'WaitingToBegin',
   Active = 'Active',
 }
 
-export class OpeningStage extends Enum {
-  constructor(value?: any, index?: number) {
-    super(
-      {
-        [OpeningStageKeys.WaitingToBegin]: WaitingToBeingOpeningStageVariant,
-        [OpeningStageKeys.Active]: ActiveOpeningStageVariant,
-      },
-      value, index);
-  }
-};
+export class OpeningStage extends JoyEnum({
+  'WaitingToBegin': WaitingToBeingOpeningStageVariant,
+  'Active': ActiveOpeningStageVariant
+} as const) { };
 
 export enum StakingAmountLimitModeKeys {
   AtLeast = 'AtLeast',
@@ -345,7 +332,23 @@ export class StakingPolicy extends JoyStruct<IStakingPolicy> {
 };
 
 import * as role_schema_json from './schemas/role.schema.json'
-const schemaValidator = new ajv({ allErrors: true }).compile(role_schema_json)
+export const schemaValidator: ajv.ValidateFunction = new ajv({ allErrors: true }).compile(role_schema_json);
+
+const OpeningHRTFallback: GenericJoyStreamRoleSchema = {
+  version: 1,
+  headline: "Unknown",
+  job: {
+    title: "Unknown",
+    description: "Unknown"
+  },
+  application: {},
+  reward: "Unknown",
+  creator: {
+    membership: {
+      handle: "Unknown"
+    }
+  }
+};
 
 export type IOpening = {
   created: BlockNumber,
@@ -384,13 +387,24 @@ export class Opening extends JoyStruct<IOpening> {
       if (schemaValidator(obj) === true) {
         return obj as unknown as GenericJoyStreamRoleSchema
       }
+      console.log("parse_human_readable_text JSON schema validation failed:", schemaValidator.errors);
     } catch (e) {
-      console.log("JSON schema validation failed:", e.toString())
+      console.log("parse_human_readable_text JSON schema validation failed:", e.toString())
     }
 
     return str
   }
 
+  parse_human_readable_text_with_fallback(): GenericJoyStreamRoleSchema {
+    const hrt = this.parse_human_readable_text();
+
+    if (typeof hrt !== 'object') {
+      return OpeningHRTFallback;
+    }
+
+    return hrt;
+  }
+
   get created(): BlockNumber {
     return this.getField<BlockNumber>('created')
   }
@@ -415,6 +429,10 @@ export class Opening extends JoyStruct<IOpening> {
     return this.getField<Option<StakingPolicy>>('role_staking_policy')
   }
 
+  get human_readable_text(): Text {
+    return this.getField<Text>('human_readable_text');
+  }
+
   get max_applicants(): number {
     const appPolicy = this.application_rationing_policy
     if (appPolicy.isNone) {

+ 4 - 3
types/src/media.ts

@@ -1,4 +1,4 @@
-import { Enum, Struct, Option, Vec as Vector, H256 } from '@polkadot/types';
+import { Enum, Struct, Option, Vec as Vector, H256, BTreeMap } from '@polkadot/types';
 import { getTypeRegistry, u64, bool, Text } from '@polkadot/types';
 import { BlockAndTime } from './common';
 import { MemberId } from './members';
@@ -127,11 +127,11 @@ export class DataObjectType extends Struct {
   }
 }
 
+export class DataObjectsMap extends BTreeMap.with(ContentId, DataObject) {}
+
 export function registerMediaTypes () {
   try {
     getTypeRegistry().register({
-      '::ContentId': ContentId,
-      '::DataObjectTypeId': DataObjectTypeId,
       ContentId,
       LiaisonJudgement,
       DataObject,
@@ -139,6 +139,7 @@ export function registerMediaTypes () {
       DataObjectStorageRelationship,
       DataObjectTypeId,
       DataObjectType,
+      DataObjectsMap
     });
   } catch (err) {
     console.error('Failed to register custom types of media module', err);

+ 129 - 3
types/src/proposals.ts

@@ -1,11 +1,14 @@
 import { Text, u32, Enum, getTypeRegistry, Tuple, GenericAccountId, u8, Vec, Option, Struct, Null, Bytes } from "@polkadot/types";
+import { bool } from "@polkadot/types/primitive";
 import { BlockNumber, Balance } from "@polkadot/types/interfaces";
 import AccountId from "@polkadot/types/primitive/Generic/AccountId";
-import { ThreadId, JoyStruct } from "./common";
+import { ThreadId, JoyStruct, WorkingGroup } from "./common";
 import { MemberId } from "./members";
 import { RoleParameters } from "./roles";
 import { StakeId } from "./stake";
 import { ElectionParameters } from "./council";
+import { ActivateOpeningAt, OpeningId, ApplicationId } from "./hiring";
+import { WorkingGroupOpeningPolicyCommitment, WorkerId, RewardPolicy } from "./working-group";
 
 export type IVotingResults = {
   abstensions: u32;
@@ -284,7 +287,15 @@ export class ProposalDetails extends Enum {
         SetContentWorkingGroupMintCapacity: "Balance",
         EvictStorageProvider: "AccountId",
         SetValidatorCount: "u32",
-        SetStorageRoleParameters: RoleParameters
+        SetStorageRoleParameters: RoleParameters,
+        AddWorkingGroupLeaderOpening: AddOpeningParameters,
+        BeginReviewWorkingGroupLeaderApplication: Tuple.with([OpeningId, WorkingGroup]),
+        FillWorkingGroupLeaderOpening: FillOpeningParameters,
+        SetWorkingGroupMintCapacity: Tuple.with(["Balance", WorkingGroup]),
+        DecreaseWorkingGroupLeaderStake: Tuple.with([WorkerId, "Balance", WorkingGroup]),
+        SlashWorkingGroupLeaderStake: Tuple.with([WorkerId, "Balance", WorkingGroup]),
+        SetWorkingGroupLeaderReward: Tuple.with([WorkerId, "Balance", WorkingGroup]),
+        TerminateWorkingGroupLeaderRole: TerminateRoleParameters,
       },
       value,
       index
@@ -440,6 +451,117 @@ export class DiscussionPost extends Struct {
   }
 }
 
+export type IAddOpeningParameters = {
+  activate_at: ActivateOpeningAt;
+  commitment: WorkingGroupOpeningPolicyCommitment;
+  human_readable_text: Bytes;
+  working_group: WorkingGroup;
+};
+
+export class AddOpeningParameters extends JoyStruct<IAddOpeningParameters> {
+  constructor(value?: IAddOpeningParameters) {
+    super(
+      {
+        activate_at: ActivateOpeningAt,
+        commitment: WorkingGroupOpeningPolicyCommitment,
+        human_readable_text: Bytes,
+        working_group: WorkingGroup
+      },
+      value
+    );
+  }
+
+  get activate_at(): ActivateOpeningAt {
+    return this.getField<ActivateOpeningAt>('activate_at');
+  }
+
+  get commitment(): WorkingGroupOpeningPolicyCommitment {
+    return this.getField<WorkingGroupOpeningPolicyCommitment>('commitment');
+  }
+
+  get human_readable_text(): Bytes {
+    return this.getField<Bytes>('human_readable_text');
+  }
+
+  get working_group(): WorkingGroup {
+    return this.getField<WorkingGroup>('working_group');
+  }
+}
+
+export type IFillOpeningParameters = {
+  opening_id: OpeningId;
+  successful_application_id: ApplicationId;
+  reward_policy: Option<RewardPolicy>;
+  working_group: WorkingGroup;
+}
+
+export class FillOpeningParameters extends JoyStruct<IFillOpeningParameters> {
+  constructor(value?: IFillOpeningParameters) {
+    super(
+      {
+        opening_id: OpeningId,
+        successful_application_id: ApplicationId,
+        reward_policy: Option.with(RewardPolicy),
+        working_group: WorkingGroup,
+      },
+      value
+    );
+  }
+
+  get opening_id(): OpeningId {
+    return this.getField<OpeningId>('opening_id');
+  }
+
+  get successful_application_id(): ApplicationId {
+    return this.getField<ApplicationId>('successful_application_id');
+  }
+
+  get reward_policy(): Option<RewardPolicy> {
+    return this.getField<Option<RewardPolicy>>('reward_policy');
+  }
+
+  get working_group(): WorkingGroup {
+    return this.getField<WorkingGroup>('working_group');
+  }
+}
+
+export type ITerminateRoleParameters = {
+  worker_id: WorkerId;
+  rationale: Bytes;
+  slash: bool;
+  working_group: WorkingGroup;
+}
+
+export class TerminateRoleParameters extends JoyStruct<ITerminateRoleParameters> {
+  constructor(value?: ITerminateRoleParameters) {
+    super(
+      {
+        worker_id: WorkerId,
+        rationale: Bytes,
+        slash: bool,
+        working_group: WorkingGroup,
+      },
+      value
+    );
+  }
+
+  get worker_id(): WorkerId {
+    return this.getField<WorkerId>('worker_id');
+  }
+
+  get rationale(): Bytes {
+    return this.getField<Bytes>('rationale');
+  }
+
+  get slash(): bool {
+    return this.getField<bool>('slash');
+  }
+
+  get working_group(): WorkingGroup {
+    return this.getField<WorkingGroup>('working_group');
+  }
+}
+
 // export default proposalTypes;
 export function registerProposalTypes() {
   try {
@@ -448,12 +570,16 @@ export function registerProposalTypes() {
       ProposalStatus,
       ProposalOf: Proposal,
       ProposalDetails,
+      ProposalDetailsOf: ProposalDetails, // Runtime alias
       VotingResults,
       ProposalParameters,
       VoteKind,
       ThreadCounter,
       DiscussionThread,
-      DiscussionPost
+      DiscussionPost,
+      AddOpeningParameters,
+      FillOpeningParameters,
+      TerminateRoleParameters
     });
   } catch (err) {
     console.error("Failed to register custom types of proposals module", err);

+ 29 - 13
types/src/working-group/index.ts

@@ -1,11 +1,12 @@
 import { getTypeRegistry, Bytes, BTreeMap, Option, Enum } from '@polkadot/types';
 import { u16, Null } from '@polkadot/types/primitive';
-import { AccountId, BlockNumber } from '@polkadot/types/interfaces';
+import { AccountId, BlockNumber, Balance } from '@polkadot/types/interfaces';
 import { BTreeSet, JoyStruct } from '../common';
 import { MemberId, ActorId } from '../members';
 import { RewardRelationshipId } from '../recurring-rewards';
 import { StakeId } from '../stake';
 import { ApplicationId, OpeningId, ApplicationRationingPolicy, StakingPolicy } from '../hiring';
+import { JoyEnum } from '../JoyEnum';
 
 export class RationaleText extends Bytes { };
 
@@ -248,17 +249,12 @@ export enum OpeningTypeKeys {
   Worker = 'Worker'
 };
 
-export class OpeningType extends Enum {
-  constructor (value?: any, index?: number) {
-    super(
-      {
-        Leader: Null,
-        Worker: Null
-      },
-      value, index
-    );
-  }
-};
+export class OpeningType_Leader extends Null { };
+export class OpeningType_Worker extends Null { };
+export class OpeningType extends JoyEnum({
+  Leader: OpeningType_Leader,
+  Worker: OpeningType_Worker
+} as const) { };
 
 export type IOpening = {
   hiring_opening_id: OpeningId,
@@ -297,6 +293,23 @@ export class Opening extends JoyStruct<IOpening> {
   }
 }
 
+// Also defined in "content-working-group" runtime module, but those definitions are the consistent
+export type IRewardPolicy = {
+  amount_per_payout: Balance,
+  next_payment_at_block: BlockNumber,
+  payout_interval: Option<BlockNumber>,
+};
+
+export class RewardPolicy extends JoyStruct<IRewardPolicy> {
+  constructor (value?: IRewardPolicy) {
+    super({
+      amount_per_payout: 'Balance',
+      next_payment_at_block: 'BlockNumber',
+      payout_interval: 'Option<BlockNumber>',
+    }, value);
+  }
+};
+
 export function registerWorkingGroupTypes() {
   try {
     getTypeRegistry().register({
@@ -310,7 +323,10 @@ export function registerWorkingGroupTypes() {
       StorageProviderId,
       OpeningType,
       /// Alias used by the runtime working-group module
-      HiringApplicationId: ApplicationId
+      HiringApplicationId: ApplicationId,
+      RewardPolicy,
+      'working_group::OpeningId': OpeningId,
+      'working_group::WorkerId': WorkerId
     });
   } catch (err) {
     console.error('Failed to register custom types of working-group module', err);

+ 5 - 43
yarn.lock

@@ -4329,7 +4329,7 @@
     "@typescript-eslint/typescript-estree" "2.9.0"
     eslint-visitor-keys "^1.1.0"
 
-"@typescript-eslint/parser@^2.6.1":
+"@typescript-eslint/parser@^2.34.0", "@typescript-eslint/parser@^2.6.1":
   version "2.34.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8"
   integrity sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==
@@ -6534,11 +6534,6 @@ buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
     base64-js "^1.0.2"
     ieee754 "^1.1.4"
 
-builtin-modules@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
-  integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
-
 builtin-status-codes@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -7396,7 +7391,7 @@ commander@2.17.x:
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
   integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
 
-commander@^2.12.1, commander@^2.13.0, commander@^2.15.0, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.5.0, commander@^2.8.1, commander@~2.20.3:
+commander@^2.13.0, commander@^2.15.0, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.5.0, commander@^2.8.1, commander@~2.20.3:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -9661,7 +9656,7 @@ eslint-plugin-eslint-plugin@^2.1.0:
   resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz#a7a00f15a886957d855feacaafee264f039e62d5"
   integrity sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg==
 
-eslint-plugin-import@^2.14.0:
+eslint-plugin-import@^2.14.0, eslint-plugin-import@^2.22.0:
   version "2.22.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e"
   integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==
@@ -9758,7 +9753,7 @@ eslint-plugin-node@^7.0.1:
     resolve "^1.8.1"
     semver "^5.5.0"
 
-eslint-plugin-prettier@^3.1.4:
+eslint-plugin-prettier@^3.1.3, eslint-plugin-prettier@^3.1.4:
   version "3.1.4"
   resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz#168ab43154e2ea57db992a2cd097c828171f75c2"
   integrity sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==
@@ -15773,13 +15768,6 @@ mkdirp@0.3.0:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
   integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=
 
-mkdirp@^0.5.3:
-  version "0.5.5"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
-  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
-  dependencies:
-    minimist "^1.2.5"
-
 mocha@^5.2.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6"
@@ -22601,7 +22589,7 @@ tsconfig-paths@^3.4.0, tsconfig-paths@^3.9.0:
     minimist "^1.2.0"
     strip-bom "^3.0.0"
 
-tslib@^1.10.0, tslib@^1.11.1:
+tslib@^1.11.1:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
   integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
@@ -22611,37 +22599,11 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
   integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
 
-tslint@^6.1.0:
-  version "6.1.2"
-  resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.2.tgz#2433c248512cc5a7b2ab88ad44a6b1b34c6911cf"
-  integrity sha512-UyNrLdK3E0fQG/xWNqAFAC5ugtFyPO4JJR1KyyfQAyzR8W0fTRrC91A8Wej4BntFzcvETdCSDa/4PnNYJQLYiA==
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    builtin-modules "^1.1.1"
-    chalk "^2.3.0"
-    commander "^2.12.1"
-    diff "^4.0.1"
-    glob "^7.1.1"
-    js-yaml "^3.13.1"
-    minimatch "^3.0.4"
-    mkdirp "^0.5.3"
-    resolve "^1.3.2"
-    semver "^5.3.0"
-    tslib "^1.10.0"
-    tsutils "^2.29.0"
-
 tsscmp@1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
   integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
 
-tsutils@^2.29.0:
-  version "2.29.0"
-  resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
-  integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
-  dependencies:
-    tslib "^1.8.1"
-
 tsutils@^3.17.1:
   version "3.17.1"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"