Browse Source

Merge branch 'nicaea' into working_group_hirable_lead

# Conflicts:
#	runtime-modules/storage/src/tests/data_object_storage_registry.rs
#	runtime-modules/working-group/src/lib.rs
#	runtime-modules/working-group/src/tests/fixtures.rs
#	runtime-modules/working-group/src/tests/mod.rs
Shamil Gadelshin 4 years ago
parent
commit
eea97dfe53
100 changed files with 1191 additions and 1025 deletions
  1. 1 1
      Cargo.lock
  2. 7 3
      cli/package.json
  3. 147 6
      cli/src/Api.ts
  4. 1 0
      cli/src/ExitCodes.ts
  5. 29 2
      cli/src/Types.ts
  6. 31 11
      cli/src/base/AccountsCommandBase.ts
  7. 22 6
      cli/src/base/StateAwareCommandBase.ts
  8. 78 0
      cli/src/base/WorkingGroupsCommandBase.ts
  9. 10 2
      cli/src/commands/account/choose.ts
  10. 1 1
      cli/src/commands/council/info.ts
  11. 38 0
      cli/src/commands/working-groups/overview.ts
  12. 14 1
      cli/src/helpers/display.ts
  13. 2 1
      cli/tsconfig.json
  14. 5 2
      node/src/chain_spec.rs
  15. 1 1
      node/src/forum_config/mod.rs
  16. 1 1
      node/src/service.rs
  17. 1 1
      pioneer/package.json
  18. 12 22
      pioneer/packages/apps-routing/src/index.ts
  19. 0 17
      pioneer/packages/apps-routing/src/joy-storage.ts
  20. 1 1
      pioneer/packages/apps/src/SideBar/Item.tsx
  21. 1 1
      pioneer/packages/joy-election/src/Applicant.tsx
  22. 1 1
      pioneer/packages/joy-election/src/ApplyForm.tsx
  23. 1 1
      pioneer/packages/joy-election/src/Council.tsx
  24. 1 1
      pioneer/packages/joy-election/src/Dashboard.tsx
  25. 1 1
      pioneer/packages/joy-election/src/SealedVote.tsx
  26. 1 1
      pioneer/packages/joy-election/src/index.tsx
  27. 2 1
      pioneer/packages/joy-forum/src/CategoryList.tsx
  28. 1 1
      pioneer/packages/joy-forum/src/Context.tsx
  29. 2 1
      pioneer/packages/joy-forum/src/EditReply.tsx
  30. 2 1
      pioneer/packages/joy-forum/src/EditThread.tsx
  31. 2 1
      pioneer/packages/joy-forum/src/Moderate.tsx
  32. 2 1
      pioneer/packages/joy-forum/src/ViewThread.tsx
  33. 2 1
      pioneer/packages/joy-forum/src/utils.tsx
  34. 1 1
      pioneer/packages/joy-forum/src/validation.tsx
  35. 5 5
      pioneer/packages/joy-media/src/DiscoveryProvider.tsx
  36. 4 4
      pioneer/packages/joy-media/src/Upload.tsx
  37. 24 4
      pioneer/packages/joy-media/src/common/MediaPlayerWithResolver.tsx
  38. 1 1
      pioneer/packages/joy-media/src/transport.substrate.ts
  39. 1 1
      pioneer/packages/joy-media/src/upload/UploadVideo.tsx
  40. 1 1
      pioneer/packages/joy-members/src/Details.tsx
  41. 1 1
      pioneer/packages/joy-members/src/EditForm.tsx
  42. 1 1
      pioneer/packages/joy-members/src/MemberPreview.tsx
  43. 7 3
      pioneer/packages/joy-proposals/src/Proposal/Body.tsx
  44. 4 4
      pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx
  45. 1 1
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx
  46. 5 7
      pioneer/packages/joy-proposals/src/Proposal/Votes.tsx
  47. 1 1
      pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx
  48. 1 1
      pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx
  49. 0 2
      pioneer/packages/joy-proposals/src/validationSchema.ts
  50. 2 2
      pioneer/packages/joy-roles/src/elements.tsx
  51. 4 0
      pioneer/packages/joy-roles/src/flows/apply.controller.tsx
  52. 0 1
      pioneer/packages/joy-roles/src/index.sass
  53. 1 0
      pioneer/packages/joy-roles/src/index.tsx
  54. 1 4
      pioneer/packages/joy-roles/src/tabs.stories.tsx
  55. 4 1
      pioneer/packages/joy-roles/src/tabs/Opportunities.controller.tsx
  56. 49 4
      pioneer/packages/joy-roles/src/tabs/Opportunities.tsx
  57. 9 3
      pioneer/packages/joy-roles/src/tabs/Opportunity.controller.tsx
  58. 33 13
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.controller.tsx
  59. 0 21
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.sass
  60. 3 39
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.stories.tsx
  61. 120 100
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx
  62. 24 25
      pioneer/packages/joy-roles/src/transport.mock.ts
  63. 216 88
      pioneer/packages/joy-roles/src/transport.substrate.ts
  64. 6 4
      pioneer/packages/joy-roles/src/transport.ts
  65. 6 0
      pioneer/packages/joy-roles/src/working_groups.ts
  66. 0 1
      pioneer/packages/joy-storage/README.md
  67. 0 15
      pioneer/packages/joy-storage/package.json
  68. 0 76
      pioneer/packages/joy-storage/src/ActorsList/index.tsx
  69. 0 95
      pioneer/packages/joy-storage/src/AvailableRoles/index.tsx
  70. 0 98
      pioneer/packages/joy-storage/src/MyRequests/index.tsx
  71. 0 0
      pioneer/packages/joy-storage/src/index.css
  72. 0 112
      pioneer/packages/joy-storage/src/index.tsx
  73. 0 7
      pioneer/packages/joy-storage/src/props.ts
  74. 0 3
      pioneer/packages/joy-storage/src/translate.ts
  75. 1 1
      pioneer/packages/joy-utils/src/index.ts
  76. 3 3
      pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx
  77. 14 2
      pioneer/packages/joy-utils/src/transport/council.ts
  78. 1 1
      pioneer/packages/joy-utils/src/transport/index.ts
  79. 4 0
      pioneer/packages/joy-utils/src/transport/members.ts
  80. 31 11
      pioneer/packages/joy-utils/src/transport/proposals.ts
  81. 6 1
      pioneer/packages/joy-utils/src/types/proposals.ts
  82. 0 2
      pioneer/tsconfig.json
  83. 1 1
      runtime-modules/content-working-group/src/genesis.rs
  84. 30 26
      runtime-modules/content-working-group/src/lib.rs
  85. 1 3
      runtime-modules/content-working-group/src/tests.rs
  86. 1 1
      runtime-modules/forum/Cargo.toml
  87. 1 1
      runtime-modules/forum/src/lib.rs
  88. 1 1
      runtime-modules/hiring/src/lib.rs
  89. 49 0
      runtime-modules/proposals/codex/src/proposal_types/mod.rs
  90. 6 3
      runtime-modules/service-discovery/src/lib.rs
  91. 7 4
      runtime-modules/service-discovery/src/mock.rs
  92. 5 6
      runtime-modules/storage/Cargo.toml
  93. 2 2
      runtime-modules/storage/src/data_directory.rs
  94. 3 35
      runtime-modules/storage/src/data_object_storage_registry.rs
  95. 2 2
      runtime-modules/storage/src/data_object_type_registry.rs
  96. 4 1
      runtime-modules/storage/src/lib.rs
  97. 0 31
      runtime-modules/storage/src/tests/data_object_storage_registry.rs
  98. 5 24
      runtime-modules/storage/src/tests/mock.rs
  99. 0 3
      runtime-modules/working-group/src/errors.rs
  100. 56 18
      runtime-modules/working-group/src/lib.rs

+ 1 - 1
Cargo.lock

@@ -4806,7 +4806,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-forum-module"
-version = "1.2.1"
+version = "1.2.2"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",

+ 7 - 3
cli/package.json

@@ -8,7 +8,7 @@
   },
   "bugs": "https://github.com/Joystream/substrate-runtime-joystream/issues",
   "dependencies": {
-    "@joystream/types": "^0.10.0",
+    "@joystream/types": "./types",
     "@oclif/command": "^1.5.19",
     "@oclif/config": "^1.14.0",
     "@oclif/plugin-help": "^2.2.3",
@@ -26,6 +26,7 @@
   "devDependencies": {
     "@oclif/dev-cli": "^1.22.2",
     "@oclif/test": "^1.2.5",
+    "@polkadot/ts": "^0.1.56",
     "@types/chai": "^4.2.11",
     "@types/mocha": "^5.2.7",
     "@types/node": "^10.17.18",
@@ -37,8 +38,7 @@
     "mocha": "^5.2.0",
     "nyc": "^14.1.1",
     "ts-node": "^8.8.2",
-    "typescript": "^3.8.3",
-    "@polkadot/ts": "^0.1.56"
+    "typescript": "^3.8.3"
   },
   "engines": {
     "node": ">=8.0.0"
@@ -71,6 +71,9 @@
       },
       "api": {
         "description": "Inspect the substrate node api, perform lower-level api calls or change the current api provider uri"
+      },
+      "working-groups": {
+        "description": "Working group lead and worker actions"
       }
     }
   },
@@ -84,6 +87,7 @@
     "posttest": "eslint . --ext .ts --config .eslintrc",
     "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"
   },
   "types": "lib/index.d.ts"

+ 147 - 6
cli/src/Api.ts

@@ -3,19 +3,36 @@ import { registerJoystreamTypes } from '@joystream/types/';
 import { ApiPromise, WsProvider } from '@polkadot/api';
 import { QueryableStorageMultiArg } from '@polkadot/api/types';
 import { formatBalance } from '@polkadot/util';
-import { Hash } from '@polkadot/types/interfaces';
+import { Hash, Balance } from '@polkadot/types/interfaces';
 import { KeyringPair } from '@polkadot/keyring/types';
 import { Codec } from '@polkadot/types/types';
-import { AccountSummary, CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj } from './Types';
+import { Option, Vec } from '@polkadot/types';
+import { u32 } from '@polkadot/types/primitive';
+import {
+    AccountSummary,
+    CouncilInfoObj, CouncilInfoTuple, createCouncilInfoObj,
+    WorkingGroups,
+    GroupLeadWithProfile,
+    GroupMember,
+} from './Types';
 import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
 import { CLIError } from '@oclif/errors';
 import ExitCodes from './ExitCodes';
+import { Worker, Lead as WorkerLead, WorkerId, WorkerRoleStakeProfile } from '@joystream/types/lib/bureaucracy';
+import { MemberId, Profile } from '@joystream/types/lib/members';
+import { RewardRelationship, RewardRelationshipId } from '@joystream/types/lib/recurring-rewards';
+import { Stake, StakeId } from '@joystream/types/lib/stake';
+import { LinkageResult } from '@polkadot/types/codec/Linkage';
 
 export const DEFAULT_API_URI = 'wss://rome-rpc-endpoint.joystream.org:9944/';
-export const TOKEN_SYMBOL = 'JOY';
+const DEFAULT_DECIMALS = new u32(12);
 
-// Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
+// Mapping of working group to api module
+const apiModuleByGroup: { [key in WorkingGroups]: string } = {
+    [WorkingGroups.StorageProviders]: 'storageBureaucracy'
+};
 
+// Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
 export default class Api {
     private _api: ApiPromise;
 
@@ -28,11 +45,25 @@ export default class Api {
     }
 
     private static async initApi(apiUri: string = DEFAULT_API_URI): Promise<ApiPromise> {
-        formatBalance.setDefaults({ unit: TOKEN_SYMBOL });
         const wsProvider:WsProvider = new WsProvider(apiUri);
         registerJoystreamTypes();
+        const api = await ApiPromise.create({ provider: wsProvider });
+
+        // Initializing some api params based on pioneer/packages/react-api/Api.tsx
+        const [ properties ] = await Promise.all([
+            api.rpc.system.properties()
+        ]);
 
-        return await ApiPromise.create({ provider: wsProvider });
+        const tokenSymbol = properties.tokenSymbol.unwrapOr('DEV').toString();
+        const tokenDecimals = properties.tokenDecimals.unwrapOr(DEFAULT_DECIMALS).toNumber();
+
+        // formatBlanace config
+        formatBalance.setDefaults({
+          decimals: tokenDecimals,
+          unit: tokenSymbol
+        });
+
+        return api;
     }
 
     static async create(apiUri: string = DEFAULT_API_URI): Promise<Api> {
@@ -111,4 +142,114 @@ export default class Api {
             .signAndSend(account);
         return txHash;
     }
+
+    // Working groups
+    // TODO: This is a lot of repeated logic from "/pioneer/joy-roles/src/transport.substrate.ts"
+    // (although simplified a little bit)
+    // Hopefully this will be refactored to "joystream-js" soon
+    protected singleLinkageResult<T extends Codec>(result: LinkageResult) {
+        return result[0] as T;
+    }
+
+    protected multiLinkageResult<K extends Codec, V extends Codec>(result: LinkageResult): [Vec<K>, Vec<V>] {
+        return [ result[0] as Vec<K>, result[1] as Vec<V> ];
+    }
+
+    protected workingGroupApiQuery(group: WorkingGroups) {
+        const module = apiModuleByGroup[group];
+        return this._api.query[module];
+    }
+
+    protected async memberProfileById(memberId: MemberId): Promise<Profile | null> {
+        const profile = await this._api.query.members.memberProfile(memberId) as Option<Profile>;
+
+        return profile.unwrapOr(null);
+    }
+
+    async groupLead (group: WorkingGroups): Promise <GroupLeadWithProfile | null> {
+        const optLead = (await this.workingGroupApiQuery(group).currentLead()) as Option<WorkerLead>;
+
+        if (!optLead.isSome) {
+          return null;
+        }
+
+        const lead = optLead.unwrap();
+        const profile = await this.memberProfileById(lead.member_id);
+
+        if (!profile) {
+            throw new Error(`Group lead profile not found! (member id: ${lead.member_id.toNumber()})`);
+        }
+
+        return { lead, profile };
+    }
+
+    protected async stakeValue (stakeId: StakeId): Promise<Balance> {
+        const stake = (await this._api.query.stake.stakes(stakeId)) as Stake;
+        return stake.value;
+    }
+
+    protected async workerStake (stakeProfile: WorkerRoleStakeProfile): Promise<Balance> {
+        return this.stakeValue(stakeProfile.stake_id);
+    }
+
+    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 (
+        id: WorkerId,
+        worker: Worker
+      ): Promise<GroupMember> {
+        const roleAccount = worker.role_account;
+        const memberId = worker.member_id;
+
+        const profile = await this.memberProfileById(memberId);
+
+        if (!profile) {
+            throw new Error(`Group member profile not found! (member id: ${memberId.toNumber()})`);
+        }
+
+        let 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());
+        }
+
+        let earnedValue: Balance = this._api.createType("Balance", 0);
+        if (worker.reward_relationship && worker.reward_relationship.isSome) {
+          earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
+        }
+
+        return ({
+            workerId: id,
+            roleAccount,
+            memberId,
+            profile,
+            stake: stakeValue,
+            earned: earnedValue
+        });
+    }
+
+    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 [];
+        }
+
+        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())) {
+            const workerId = workerIds[parseInt(index)];
+            groupMembers.push(await this.groupMember(workerId, worker));
+        }
+
+        return groupMembers.reverse();
+      }
 }

+ 1 - 0
cli/src/ExitCodes.ts

@@ -6,6 +6,7 @@ enum ExitCodes {
     InvalidFile = 402,
     NoAccountFound = 403,
     NoAccountSelected = 404,
+    AccessDenied = 405,
 
     UnexpectedException = 500,
     FsOperationFailed = 501,

+ 29 - 2
cli/src/Types.ts

@@ -1,9 +1,11 @@
 import BN from 'bn.js';
-import { ElectionStage, Seat } from '@joystream/types/';
+import { ElectionStage, Seat } from '@joystream/types/lib/council';
 import { Option } from '@polkadot/types';
-import { BlockNumber, Balance } from '@polkadot/types/interfaces';
+import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces';
 import { DerivedBalances } from '@polkadot/api-derive/types';
 import { KeyringPair } from '@polkadot/keyring/types';
+import { WorkerId, Lead } from '@joystream/types/lib/bureaucracy';
+import { Profile, MemberId } from '@joystream/types/lib/members';
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -61,3 +63,28 @@ export function createCouncilInfoObj(
 // Total balance:   100 JOY
 // Free calance:     50 JOY
 export type NameValueObj = { name: string, value: string };
+
+// Working groups related types
+export enum WorkingGroups {
+    StorageProviders = 'storageProviders'
+}
+
+// In contrast to Pioneer, currently only StorageProviders group is available in CLI
+export const AvailableGroups: readonly WorkingGroups[] = [
+  WorkingGroups.StorageProviders
+] as const;
+
+// Compound working group types
+export type GroupLeadWithProfile = {
+    lead: Lead;
+    profile: Profile;
+}
+
+export type GroupMember = {
+    workerId: WorkerId;
+    memberId: MemberId;
+    roleAccount: AccountId;
+    profile: Profile;
+    stake: Balance;
+    earned: Balance;
+}

+ 31 - 11
cli/src/base/AccountsCommandBase.ts

@@ -12,25 +12,26 @@ import { DerivedBalances } from '@polkadot/api-derive/types';
 import { toFixedLength } from '../helpers/display';
 
 const ACCOUNTS_DIRNAME = '/accounts';
+const SPECIAL_ACCOUNT_POSTFIX = '__DEV';
 
 /**
  * Abstract base class for account-related commands.
  *
  * All the accounts available in the CLI are stored in the form of json backup files inside:
- * { this.config.dataDir }/{ ACCOUNTS_DIRNAME } (ie. ~/.local/share/joystream-cli/accounts on Ubuntu)
- * Where: this.config.dataDir is provided by oclif and ACCOUNTS_DIRNAME is a const (see above).
+ * { APP_DATA_PATH }/{ ACCOUNTS_DIRNAME } (ie. ~/.local/share/joystream-cli/accounts on Ubuntu)
+ * Where: APP_DATA_PATH is provided by StateAwareCommandBase and ACCOUNTS_DIRNAME is a const (see above).
  */
 export default abstract class AccountsCommandBase extends ApiCommandBase {
     getAccountsDirPath(): string {
-        return path.join(this.config.dataDir, ACCOUNTS_DIRNAME);
+        return path.join(this.getAppDataPath(), ACCOUNTS_DIRNAME);
     }
 
-    getAccountFilePath(account: NamedKeyringPair): string {
-        return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account));
+    getAccountFilePath(account: NamedKeyringPair, isSpecial: boolean = false): string {
+        return path.join(this.getAccountsDirPath(), this.generateAccountFilename(account, isSpecial));
     }
 
-    generateAccountFilename(account: NamedKeyringPair): string {
-        return `${ slug(account.meta.name, '_') }__${ account.address }.json`;
+    generateAccountFilename(account: NamedKeyringPair, isSpecial: boolean = false): string {
+        return `${ slug(account.meta.name, '_') }__${ account.address }${ isSpecial ? SPECIAL_ACCOUNT_POSTFIX : '' }.json`;
     }
 
     private initAccountsFs(): void {
@@ -39,14 +40,27 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         }
     }
 
-    saveAccount(account: NamedKeyringPair, password: string): void {
+    saveAccount(account: NamedKeyringPair, password: string, isSpecial: boolean = false): void {
         try {
-            fs.writeFileSync(this.getAccountFilePath(account), JSON.stringify(account.toJson(password)));
+            const destPath = this.getAccountFilePath(account, isSpecial);
+            fs.writeFileSync(destPath, JSON.stringify(account.toJson(password)));
         } catch(e) {
             throw this.createDataWriteError();
         }
     }
 
+    // Add dev "Alice" and "Bob" accounts
+    initSpecialAccounts() {
+        const keyring = new Keyring({ type: 'sr25519' });
+        keyring.addFromUri('//Alice', { name: 'Alice' });
+        keyring.addFromUri('//Bob', { name: 'Bob' });
+        keyring.getPairs().forEach(pair => this.saveAccount(
+            { ...pair, meta: { name: pair.meta.name } },
+            '',
+            true
+        ));
+    }
+
     fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair {
         if (!fs.existsSync(jsonBackupFilePath)) {
             throw new CLIError('Input file does not exist!', { exit: ExitCodes.FileNotFound });
@@ -91,7 +105,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         }
     }
 
-    fetchAccounts(): NamedKeyringPair[] {
+    fetchAccounts(includeSpecial: boolean = false): NamedKeyringPair[] {
         let files: string[] = [];
         const accountDir = this.getAccountsDirPath();
         try {
@@ -104,6 +118,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         return <NamedKeyringPair[]> files
             .map(fileName => {
                 const filePath = path.join(accountDir, fileName);
+                if (!includeSpecial && filePath.includes(SPECIAL_ACCOUNT_POSTFIX+'.')) return null;
                 return this.fetchAccountOrNullFromFile(filePath);
             })
             .filter(accObj => accObj !== null);
@@ -145,7 +160,11 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     }
 
     async setSelectedAccount(account: NamedKeyringPair): Promise<void> {
-        await this.setPreservedState({ selectedAccountFilename: this.generateAccountFilename(account) });
+        const accountFilename = fs.existsSync(this.getAccountFilePath(account, true))
+            ? this.generateAccountFilename(account, true)
+            : this.generateAccountFilename(account);
+
+        await this.setPreservedState({ selectedAccountFilename: accountFilename });
     }
 
     async promptForPassword(message:string = 'Your account\'s password') {
@@ -210,6 +229,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         await super.init();
         try {
             this.initAccountsFs();
+            this.initSpecialAccounts();
         } catch (e) {
             throw this.createDataDirInitError();
         }

+ 22 - 6
cli/src/base/StateAwareCommandBase.ts

@@ -5,6 +5,7 @@ import { CLIError } from '@oclif/errors';
 import { DEFAULT_API_URI } from '../Api';
 import lockFile from 'proper-lockfile';
 import DefaultCommandBase from './DefaultCommandBase';
+import os from 'os';
 
 // Type for the state object (which is preserved as json in the state file)
 type StateObject = {
@@ -18,7 +19,7 @@ const DEFAULT_STATE: StateObject = {
     apiUri: DEFAULT_API_URI
 }
 
-// State file path (relative to this.config.dataDir)
+// State file path (relative to getAppDataPath())
 const STATE_FILE = '/state.json';
 
 // Possible data directory access errors
@@ -31,13 +32,28 @@ enum DataDirErrorType {
 /**
  * Abstract base class for commands that need to work with the preserved state.
  *
- * The preserved state is kept in a json file inside the data directory (this.config.dataDir, supplied by oclif).
+ * The preserved state is kept in a json file inside the data directory.
  * The state object contains all the information that needs to be preserved across sessions, ie. the default account
  * choosen by the user after executing account:choose command etc. (see "StateObject" type above).
  */
 export default abstract class StateAwareCommandBase extends DefaultCommandBase {
+    getAppDataPath(): string {
+        const systemAppDataPath =
+            process.env.APPDATA ||
+            (
+                process.platform === 'darwin'
+                    ? path.join(os.homedir(), '/Library/Application Support')
+                    : path.join(os.homedir(), '/.local/share')
+            );
+        const packageJson: { name?: string } = require('../../package.json');
+        if (!packageJson || !packageJson.name) {
+            throw new CLIError('Cannot get package name from package.json!');
+        }
+        return path.join(systemAppDataPath, packageJson.name);
+    }
+
     getStateFilePath(): string {
-        return path.join(this.config.dataDir, STATE_FILE);
+        return path.join(this.getAppDataPath(), STATE_FILE);
     }
 
     private createDataDirFsError(errorType: DataDirErrorType, specificPath: string = '') {
@@ -49,7 +65,7 @@ export default abstract class StateAwareCommandBase extends DefaultCommandBase {
 
         const errorMsg =
             `Unexpected error while trying to ${ actionStrs[errorType] } the data directory.`+
-            `(${ path.join(this.config.dataDir, specificPath) })! Permissions issue?`;
+            `(${ path.join(this.getAppDataPath(), specificPath) })! Permissions issue?`;
 
         return new CLIError(errorMsg, { exit: ExitCodes.FsOperationFailed });
     }
@@ -67,8 +83,8 @@ export default abstract class StateAwareCommandBase extends DefaultCommandBase {
     }
 
     private initStateFs(): void {
-        if (!fs.existsSync(this.config.dataDir)) {
-            fs.mkdirSync(this.config.dataDir);
+        if (!fs.existsSync(this.getAppDataPath())) {
+            fs.mkdirSync(this.getAppDataPath());
         }
         if (!fs.existsSync(this.getStateFilePath())) {
             fs.writeFileSync(this.getStateFilePath(), JSON.stringify(DEFAULT_STATE));

+ 78 - 0
cli/src/base/WorkingGroupsCommandBase.ts

@@ -0,0 +1,78 @@
+import ExitCodes from '../ExitCodes';
+import AccountsCommandBase from './AccountsCommandBase';
+import { flags } from '@oclif/command';
+import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupLeadWithProfile, GroupMember } from '../Types';
+import { CLIError } from '@oclif/errors';
+import inquirer from 'inquirer';
+
+const DEFAULT_GROUP = WorkingGroups.StorageProviders;
+
+/**
+ * Abstract base class for commands related to working groups
+ */
+export default abstract class WorkingGroupsCommandBase extends AccountsCommandBase {
+    group: WorkingGroups = DEFAULT_GROUP;
+
+    static flags = {
+        group: flags.string({
+            char: 'g',
+            description:
+                "The working group context in which the command should be executed\n" +
+                `Available values are: ${AvailableGroups.join(', ')}.`,
+            required: true,
+            default: DEFAULT_GROUP
+        }),
+    };
+
+    // Use when lead access is required in given command
+    async getRequiredLead(): Promise<GroupLeadWithProfile> {
+        let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
+        let lead = await this.getApi().groupLead(this.group);
+
+        if (!lead || lead.lead.role_account_id.toString() !== selectedAccount.address) {
+            this.error('Lead access required for this command!', { exit: ExitCodes.AccessDenied });
+        }
+
+        return lead;
+    }
+
+    // Use when worker access is required in given command
+    async getRequiredWorker(): Promise<GroupMember> {
+        let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
+        let groupMembers = await this.getApi().groupMembers(this.group);
+        let groupMembersByAccount = groupMembers.filter(m => m.roleAccount.toString() === selectedAccount.address);
+
+        if (!groupMembersByAccount.length) {
+            this.error('Worker access required for this command!', { exit: ExitCodes.AccessDenied });
+        }
+        else if (groupMembersByAccount.length === 1) {
+            return groupMembersByAccount[0];
+        }
+        else {
+            return await this.promptForWorker(groupMembersByAccount);
+        }
+    }
+
+    async promptForWorker(groupMembers: GroupMember[]): Promise<GroupMember> {
+        const { choosenWorkerIndex } = await inquirer.prompt([{
+            name: 'chosenWorkerIndex',
+            message: 'Choose the worker to execute the command as',
+            type: 'list',
+            choices: groupMembers.map((groupMember, index) => ({
+                name: `Worker ID ${ groupMember.workerId.toString() }`,
+                value: index
+            }))
+        }]);
+
+        return groupMembers[choosenWorkerIndex];
+    }
+
+    async init() {
+        await super.init();
+        const { flags } = this.parse(WorkingGroupsCommandBase);
+        if (!AvailableGroups.includes(flags.group as any)) {
+            throw new CLIError('Invalid group!', { exit: ExitCodes.InvalidInput });
+        }
+        this.group = flags.group as WorkingGroups;
+    }
+}

+ 10 - 2
cli/src/commands/account/choose.ts

@@ -1,13 +1,21 @@
 import AccountsCommandBase from '../../base/AccountsCommandBase';
 import chalk from 'chalk';
 import ExitCodes from '../../ExitCodes';
-import { NamedKeyringPair } from '../../Types'
+import { NamedKeyringPair } from '../../Types';
+import { flags } from '@oclif/command';
 
 export default class AccountChoose extends AccountsCommandBase {
     static description = 'Choose default account to use in the CLI';
+    static flags = {
+        showSpecial: flags.boolean({
+            description: 'Whether to show special (DEV chain) accounts',
+            required: false
+        }),
+    };
 
     async run() {
-        const accounts: NamedKeyringPair[] = this.fetchAccounts();
+        const { showSpecial } = this.parse(AccountChoose).flags;
+        const accounts: NamedKeyringPair[] = this.fetchAccounts(showSpecial);
         const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount();
 
         this.log(chalk.white(`Found ${ accounts.length } existing accounts...\n`));

+ 1 - 1
cli/src/commands/council/info.ts

@@ -1,4 +1,4 @@
-import { ElectionStage } from '@joystream/types/';
+import { ElectionStage } from '@joystream/types/lib/council';
 import { formatNumber, formatBalance } from '@polkadot/util';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { CouncilInfoObj, NameValueObj } from '../../Types';

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

@@ -0,0 +1,38 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
+import { displayHeader, displayNameValueTable, displayTable } from '../../helpers/display';
+import { formatBalance } from '@polkadot/util';
+import chalk from 'chalk';
+
+export default class WorkingGroupsOverview extends WorkingGroupsCommandBase {
+    static description = 'Shows an overview of given working group (current lead and workers)';
+    static flags = {
+        ...WorkingGroupsCommandBase.flags,
+    };
+
+    async run() {
+        const lead = await this.getApi().groupLead(this.group);
+        const members = await this.getApi().groupMembers(this.group);
+
+        displayHeader('Group lead');
+        if (lead) {
+            displayNameValueTable([
+                { name: 'Member id:', value: lead.lead.member_id.toString() },
+                { name: 'Member handle:', value: lead.profile.handle.toString() },
+                { name: 'Role account:', value: lead.lead.role_account_id.toString() },
+            ]);
+        }
+        else {
+            this.log(chalk.yellow('No lead assigned!'));
+        }
+
+        displayHeader('Members');
+        const membersRows = members.map(m => ({
+            'Worker id': m.workerId.toString(),
+            'Member id': m.memberId.toString(),
+            'Member handle': m.profile.handle.toString(),
+            'Stake': formatBalance(m.stake),
+            'Earned': formatBalance(m.earned)
+        }));
+        displayTable(membersRows, 20);
+    }
+  }

+ 14 - 1
cli/src/helpers/display.ts

@@ -1,4 +1,4 @@
-import { cli } from 'cli-ux';
+import { cli, Table } from 'cli-ux';
 import chalk from 'chalk';
 import { NameValueObj } from '../Types';
 
@@ -23,6 +23,19 @@ export function displayNameValueTable(rows: NameValueObj[]) {
     );
 }
 
+export function displayTable(rows: { [k: string]: string }[], minColumnWidth = 0) {
+    if (!rows.length) {
+        return;
+    }
+    const columnDef = (columnName: string) => ({
+        get: (row: typeof rows[number])  => chalk.white(row[columnName]),
+        minWidth: minColumnWidth
+    });
+    let columns: Table.table.Columns<{ [k: string]: string }> = {};
+    Object.keys(rows[0]).forEach(columnName => columns[columnName] = columnDef(columnName))
+    cli.table(rows, columns);
+}
+
 export function toFixedLength(text: string, length: number, spacesOnLeft = false): string {
     if (text.length > length && length > 3) {
         return text.slice(0, length-3) + '...';

+ 2 - 1
cli/tsconfig.json

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

+ 5 - 2
node/src/chain_spec.rs

@@ -25,8 +25,8 @@ use node_runtime::{
     CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig,
     DataObjectTypeRegistryConfig, ElectionParameters, GrandpaConfig, ImOnlineConfig, IndicesConfig,
     MembersConfig, MigrationConfig, Perbill, ProposalsCodexConfig, SessionConfig, SessionKeys,
-    Signature, StakerStatus, StakingConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS,
-    WASM_BINARY,
+    Signature, StakerStatus, StakingConfig, StorageWorkingGroupConfig, SudoConfig, SystemConfig,
+    VersionedStoreConfig, DAYS, WASM_BINARY,
 };
 pub use node_runtime::{AccountId, GenesisConfig};
 use primitives::{sr25519, Pair, Public};
@@ -267,6 +267,9 @@ pub fn testnet_genesis(
         }),
         data_object_storage_registry: Some(DataObjectStorageRegistryConfig {
             first_relationship_id: 1,
+        }),
+        working_group_Instance2: Some(StorageWorkingGroupConfig {
+            phantom: Default::default(),
             storage_working_group_mint_capacity: 0,
             opening_human_readable_text_constraint: default_text_constraint,
             worker_application_human_readable_text_constraint: default_text_constraint,

+ 1 - 1
node/src/forum_config/mod.rs

@@ -3,7 +3,7 @@ pub mod from_serialized;
 // Not exported - only here as sample code
 // mod from_encoded;
 
-use node_runtime::forum::InputValidationLengthConstraint;
+use node_runtime::common::constraints::InputValidationLengthConstraint;
 
 pub fn new_validation(min: u16, max_min_diff: u16) -> InputValidationLengthConstraint {
     InputValidationLengthConstraint { min, max_min_diff }

+ 1 - 1
node/src/service.rs

@@ -224,7 +224,7 @@ macro_rules! new_full {
 			(true, false) => {
 				// start the full GRANDPA voter
 				let grandpa_config = grandpa::GrandpaParams {
-					config: config,
+					config,
 					link: grandpa_link,
 					network: service.network(),
 					inherent_data_providers: inherent_data_providers.clone(),

+ 1 - 1
pioneer/package.json

@@ -82,6 +82,6 @@
     "node-sass": "^4.13.0",
     "sass-loader": "^8.0.0",
     "style-loader": "^1.0.0",
-    "@joystream/types": "^0.10.0"
+    "@joystream/types": "./types"
   }
 }

+ 12 - 22
pioneer/packages/apps-routing/src/index.ts

@@ -13,7 +13,6 @@ import media from './joy-media';
 import members from './joy-members';
 import proposals from './joy-proposals';
 import roles from './joy-roles';
-import storageRoles from './joy-storage';
 import pages from './joy-pages';
 
 // import template from './123code';
@@ -39,46 +38,37 @@ import transfer from './transfer';
 
 let routes: Routes = ([] as Routes);
 
-if (appSettings.isFullMode) {
-  routes = routes.concat(explorer);
-}
-
 // Basic routes
 routes = routes.concat(
-  staking,
-  roles,
-  storageRoles,
-  transfer,
-  null,
   media,
+  roles,
+  proposals,
+  election,
   forum,
   members,
+  staking,
+  null,
+  transfer,
   accounts,
   addressbook,
-  null,
-  election,
-  proposals,
-  null
+  settings,
+  pages
 );
 
 if (appSettings.isFullMode) {
   routes = routes.concat(
+    null,
+    explorer,
     storage,
     extrinsics,
     sudo,
     js,
-    toolbox,
-    null
+    toolbox
   );
 }
 
-routes = routes.concat(
-  settings,
-  pages
-);
-
 const setup: Routing = {
-  default: 'staking',
+  default: 'media',
   routes
 };
 

+ 0 - 17
pioneer/packages/apps-routing/src/joy-storage.ts

@@ -1,17 +0,0 @@
-import { Routes } from './types';
-
-import Storage from '@polkadot/joy-storage/index';
-
-export default [
-  {
-    Component: Storage,
-    display: {
-      needsApi: ['query.actors.actorAccountIds']
-    },
-    i18n: {
-      defaultValue: 'Storage'
-    },
-    icon: 'database',
-    name: 'storage'
-  }
-] as Routes;

+ 1 - 1
pioneer/packages/apps/src/SideBar/Item.tsx

@@ -20,7 +20,7 @@ import { Option } from '@polkadot/types';
 import translate from '../translate';
 
 import { queryToProp } from '@polkadot/joy-utils/index';
-import { ElectionStage } from '@joystream/types/';
+import { ElectionStage } from '@joystream/types/council';
 import { councilSidebarName } from '@polkadot/apps-routing/joy-election';
 
 interface Props extends I18nProps {

+ 1 - 1
pioneer/packages/joy-election/src/Applicant.tsx

@@ -11,7 +11,7 @@ import CandidatePreview from './CandidatePreview';
 
 import translate from './translate';
 import { calcTotalStake } from '@polkadot/joy-utils/index';
-import { Stake } from '@joystream/types/';
+import { Stake } from '@joystream/types/council';
 
 type Props = ApiProps & I18nProps & {
   index: number;

+ 1 - 1
pioneer/packages/joy-election/src/ApplyForm.tsx

@@ -10,7 +10,7 @@ import { Balance } from '@polkadot/types/interfaces';
 import translate from './translate';
 import TxButton from '@polkadot/joy-utils/TxButton';
 import InputStake from '@polkadot/joy-utils/InputStake';
-import { Stake } from '@joystream/types/';
+import { Stake } from '@joystream/types/council';
 import { calcTotalStake, ZERO } from '@polkadot/joy-utils/index';
 import { MyAddressProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
 

+ 1 - 1
pioneer/packages/joy-election/src/Council.tsx

@@ -8,7 +8,7 @@ import { formatBalance } from '@polkadot/util';
 import CouncilCandidate from './CandidatePreview';
 
 import { calcBackersStake } from '@polkadot/joy-utils/index';
-import { Seat } from '@joystream/types/';
+import { Seat } from '@joystream/types/council';
 import translate from './translate';
 import Section from '@polkadot/joy-utils/Section';
 

+ 1 - 1
pioneer/packages/joy-election/src/Dashboard.tsx

@@ -11,7 +11,7 @@ import { formatNumber, formatBalance } from '@polkadot/util';
 
 import Section from '@polkadot/joy-utils/Section';
 import { queryToProp } from '@polkadot/joy-utils/index';
-import { ElectionStage, Seat } from '@joystream/types/';
+import { ElectionStage, Seat } from '@joystream/types/council';
 import translate from './translate';
 
 type Props = ApiProps & I18nProps & {

+ 1 - 1
pioneer/packages/joy-election/src/SealedVote.tsx

@@ -10,7 +10,7 @@ import { formatBalance } from '@polkadot/util';
 
 import translate from './translate';
 import { calcTotalStake } from '@polkadot/joy-utils/index';
-import { SealedVote } from '@joystream/types/';
+import { SealedVote } from '@joystream/types/council';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';
 import CandidatePreview from './CandidatePreview';
 import { findVoteByHash } from './myVotesStore';

+ 1 - 1
pioneer/packages/joy-election/src/index.tsx

@@ -18,7 +18,7 @@ import Applicants from './Applicants';
 import Votes from './Votes';
 import Reveals from './Reveals';
 import { queryToProp } from '@polkadot/joy-utils/index';
-import { Seat } from '@joystream/types/';
+import { Seat } from '@joystream/types/council';
 
 // define out internal types
 type Props = AppProps & ApiProps & I18nProps & {

+ 2 - 1
pioneer/packages/joy-forum/src/CategoryList.tsx

@@ -7,7 +7,8 @@ import orderBy from 'lodash/orderBy';
 import BN from 'bn.js';
 
 import { Option, bool } from '@polkadot/types';
-import { CategoryId, Category, ThreadId, Thread } from '@joystream/types/forum';
+import { ThreadId } from '@joystream/types/common';
+import { CategoryId, Category, Thread } from '@joystream/types/forum';
 import { ViewThread } from './ViewThread';
 import { MutedSpan } from '@polkadot/joy-utils/MutedText';
 import { UrlHasIdProps, CategoryCrumbs, Pagination, ThreadsPerPage } from './utils';

+ 1 - 1
pioneer/packages/joy-forum/src/Context.tsx

@@ -3,7 +3,7 @@
 
 import React, { useReducer, createContext, useContext } from 'react';
 import { Category, Thread, Reply, ModerationAction } from '@joystream/types/forum';
-import { BlockAndTime } from '@joystream/types/media';
+import { BlockAndTime } from '@joystream/types/common';
 import { Option, Text, GenericAccountId } from '@polkadot/types';
 
 type CategoryId = number;

+ 2 - 1
pioneer/packages/joy-forum/src/EditReply.tsx

@@ -10,7 +10,8 @@ import { withMulti } from '@polkadot/react-api/with';
 
 import * as JoyForms from '@polkadot/joy-utils/forms';
 import { Text } from '@polkadot/types';
-import { PostId, Post, ThreadId } from '@joystream/types/forum';
+import { PostId, ThreadId } from '@joystream/types/common';
+import { Post } from '@joystream/types/forum';
 import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
 import Section from '@polkadot/joy-utils/Section';
 import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';

+ 2 - 1
pioneer/packages/joy-forum/src/EditThread.tsx

@@ -12,7 +12,8 @@ import { withMulti } from '@polkadot/react-api/with';
 
 import * as JoyForms from '@polkadot/joy-utils/forms';
 import { Text } from '@polkadot/types';
-import { ThreadId, Thread, CategoryId } from '@joystream/types/forum';
+import { ThreadId } from '@joystream/types/common';
+import { Thread, CategoryId } from '@joystream/types/forum';
 import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
 import Section from '@polkadot/joy-utils/Section';
 import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';

+ 2 - 1
pioneer/packages/joy-forum/src/Moderate.tsx

@@ -9,7 +9,8 @@ import { withMulti } from '@polkadot/react-api/with';
 
 import * as JoyForms from '@polkadot/joy-utils/forms';
 import { Text } from '@polkadot/types';
-import { ReplyId, ThreadId } from '@joystream/types/forum';
+import { ThreadId } from '@joystream/types/common';
+import { ReplyId } from '@joystream/types/forum';
 import Section from '@polkadot/joy-utils/Section';
 import { withOnlyForumSudo } from './ForumSudo';
 import { ValidationProps, withPostModerationValidation } from './validation';

+ 2 - 1
pioneer/packages/joy-forum/src/ViewThread.tsx

@@ -5,7 +5,8 @@ import { Table, Button, Label } from 'semantic-ui-react';
 import { History } from 'history';
 import BN from 'bn.js';
 
-import { Category, Thread, ThreadId, Post, PostId } from '@joystream/types/forum';
+import { ThreadId, PostId } from '@joystream/types/common';
+import { Category, Thread, Post } from '@joystream/types/forum';
 import { Pagination, RepliesPerPage, CategoryCrumbs } from './utils';
 import { ViewReply } from './ViewReply';
 import { Moderate } from './Moderate';

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

@@ -2,7 +2,8 @@ import React from 'react';
 import { Link } from 'react-router-dom';
 import { Pagination as SuiPagination } from 'semantic-ui-react';
 
-import { Category, CategoryId, Thread, ThreadId } from '@joystream/types/forum';
+import { ThreadId } from '@joystream/types/common';
+import { Category, CategoryId, Thread } from '@joystream/types/forum';
 import { withForumCalls } from './calls';
 import { withMulti } from '@polkadot/react-api';
 

+ 1 - 1
pioneer/packages/joy-forum/src/validation.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { withMulti } from '@polkadot/react-api/with';
-import { InputValidationLengthConstraint } from '@joystream/types/forum';
+import { InputValidationLengthConstraint } from '@joystream/types/common';
 import { withForumCalls } from './calls';
 import { componentName } from '@polkadot/joy-utils/react/helpers';
 

+ 5 - 5
pioneer/packages/joy-media/src/DiscoveryProvider.tsx

@@ -2,7 +2,7 @@ import React, { useState, useEffect, useContext, createContext } from 'react';
 import { Message } from 'semantic-ui-react';
 import axios, { CancelToken } from 'axios';
 
-import { AccountId } from '@polkadot/types/interfaces';
+import { StorageProviderId } from '@joystream/types/bureaucracy';
 import { Vec } from '@polkadot/types';
 import { Url } from '@joystream/types/discovery';
 import ApiContext from '@polkadot/react-api/ApiContext';
@@ -15,8 +15,8 @@ export type BootstrapNodes = {
 };
 
 export type DiscoveryProvider = {
-  resolveAssetEndpoint: (provider: AccountId, contentId?: string, cancelToken?: CancelToken) => Promise<string>;
-  reportUnreachable: (provider: AccountId) => void;
+  resolveAssetEndpoint: (provider: StorageProviderId, contentId?: string, cancelToken?: CancelToken) => Promise<string>;
+  reportUnreachable: (provider: StorageProviderId) => void;
 };
 
 export type DiscoveryProviderProps = {
@@ -41,7 +41,7 @@ type ProviderStats = {
 function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryProvider {
   const stats: Map<string, ProviderStats> = new Map();
 
-  const resolveAssetEndpoint = async (storageProvider: AccountId, contentId?: string, cancelToken?: CancelToken) => {
+  const resolveAssetEndpoint = async (storageProvider: StorageProviderId, contentId?: string, cancelToken?: CancelToken) => {
     const providerKey = storageProvider.toString();
 
     let stat = stats.get(providerKey);
@@ -98,7 +98,7 @@ function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryPro
     throw new Error('Resolving failed.');
   };
 
-  const reportUnreachable = (provider: AccountId) => {
+  const reportUnreachable = (provider: StorageProviderId) => {
     const key = provider.toString();
     const stat = stats.get(key);
     if (stat) {

+ 4 - 4
pioneer/packages/joy-media/src/Upload.tsx

@@ -11,7 +11,6 @@ import { SubmittableResult } from '@polkadot/api';
 import { Option } from '@polkadot/types/codec';
 import { withMulti, withApi } from '@polkadot/react-api';
 import { formatNumber } from '@polkadot/util';
-import { AccountId } from '@polkadot/types/interfaces';
 
 import translate from './translate';
 import { fileNameWoExt } from './utils';
@@ -24,6 +23,7 @@ import { ChannelId } from '@joystream/types/content-working-group';
 import { EditVideoView } from './upload/EditVideo.view';
 import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
 import { IterableFile } from './IterableFile';
+import { StorageProviderId } from '@joystream/types/bureaucracy';
 
 const MAX_FILE_SIZE_MB = 500;
 const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
@@ -107,8 +107,8 @@ class Component extends React.PureComponent<Props, State> {
   private resetForm = () => {
     const { cancelSource } = this.state;
     this.setState({
-      cancelSource,
-      ...defaultState()
+      ...defaultState(),
+      cancelSource
     });
   }
 
@@ -285,7 +285,7 @@ class Component extends React.PureComponent<Props, State> {
     }
   }
 
-  private uploadFileTo = async (storageProvider: AccountId) => {
+  private uploadFileTo = async (storageProvider: StorageProviderId) => {
     const { file, newContentId, cancelSource } = this.state;
     if (!file || !file.size) {
       this.setState({

+ 24 - 4
pioneer/packages/joy-media/src/common/MediaPlayerWithResolver.tsx

@@ -6,7 +6,7 @@ import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
 import { withMulti } from '@polkadot/react-api/with';
 import { Option } from '@polkadot/types/codec';
-import { AccountId } from '@polkadot/types/interfaces';
+import { StorageProviderId, Worker } from '@joystream/types/bureaucracy';
 
 import translate from '../translate';
 import { DiscoveryProviderProps, withDiscoveryProvider } from '../DiscoveryProvider';
@@ -14,6 +14,7 @@ import { DataObjectStorageRelationshipId, DataObjectStorageRelationship } from '
 import { Message } from 'semantic-ui-react';
 import { MediaPlayerView, RequiredMediaPlayerProps } from './MediaPlayerView';
 import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
+import { MultipleLinkedMapEntry } from '@polkadot/joy-utils/index';
 
 type Props = ApiProps & I18nProps & DiscoveryProviderProps & RequiredMediaPlayerProps;
 
@@ -29,6 +30,23 @@ function InnerComponent (props: Props) {
   const [contentType, setContentType] = useState<string>();
   const [cancelSource, setCancelSource] = useState<CancelTokenSource>(newCancelSource());
 
+  const getActiveStorageProviderIds = async (): Promise<StorageProviderId[]> => {
+    const nextId = await api.query.storageBureaucracy.nextWorkerId() as StorageProviderId;
+    // This is chain specfic, but if next id is still 0, it means no workers have been added,
+    // so the workerById is empty
+    if (nextId.eq(0)) {
+      return [];
+    }
+
+    const workers = new MultipleLinkedMapEntry<StorageProviderId, Worker>(
+      StorageProviderId,
+      Worker,
+      await api.query.storageBureaucracy.workerById()
+    );
+
+    return workers.linked_keys;
+  };
+
   const resolveAsset = async () => {
     setError(undefined);
     setCancelSource(newCancelSource());
@@ -37,6 +55,7 @@ function InnerComponent (props: Props) {
 
     const allRelationships: Option<DataObjectStorageRelationship>[] = await Promise.all(rids.map((id) => api.query.dataObjectStorageRegistry.relationships(id))) as any;
 
+    // Providers that have signalled onchain that they have the asset
     let readyProviders = allRelationships.filter(r => r.isSome).map(r => r.unwrap())
       .filter(r => r.ready)
       .map(r => r.storage_provider);
@@ -49,10 +68,11 @@ function InnerComponent (props: Props) {
       return;
     }
 
-    // filter out providers no longer in actors list
-    const stakedActors = await api.query.actors.actorAccountIds() as unknown as AccountId[];
+    // filter out providers no longer active - relationships of providers that have left
+    // are not pruned onchain.
+    const activeProviders = await getActiveStorageProviderIds();
+    readyProviders = _.intersectionBy(activeProviders, readyProviders, provider => provider.toString());
 
-    readyProviders = _.intersectionBy(stakedActors, readyProviders, provider => provider.toString());
     console.log(`Found ${readyProviders.length} providers ready to serve content: ${readyProviders}`);
 
     // shuffle to spread the load

+ 1 - 1
pioneer/packages/joy-media/src/transport.substrate.ts

@@ -1,7 +1,7 @@
 import BN from 'bn.js';
 import { MediaTransport, ChannelValidationConstraints } from './transport';
 import { ClassId, Class, EntityId, Entity, ClassName } from '@joystream/types/versioned-store';
-import { InputValidationLengthConstraint } from '@joystream/types/forum';
+import { InputValidationLengthConstraint } from '@joystream/types/common';
 import { PlainEntity, EntityCodecResolver } from '@joystream/types/versioned-store/EntityCodec';
 import { MusicTrackType } from './schemas/music/MusicTrack';
 import { MusicAlbumType } from './schemas/music/MusicAlbum';

+ 1 - 1
pioneer/packages/joy-media/src/upload/UploadVideo.tsx

@@ -14,7 +14,7 @@ import { MediaDropdownOptions } from '../common/MediaDropdownOptions';
 import { FormTabs } from '../common/FormTabs';
 import { ChannelId } from '@joystream/types/content-working-group';
 import { ChannelEntity } from '../entities/ChannelEntity';
-import { Credential } from '@joystream/types/versioned-store/permissions/credentials';
+import { Credential } from '@joystream/types/common';
 import { Class, VecClassPropertyValue } from '@joystream/types/versioned-store';
 import { TxCallback } from '@polkadot/react-components/Status/types';
 import { SubmittableResult } from '@polkadot/api';

+ 1 - 1
pioneer/packages/joy-members/src/Details.tsx

@@ -14,7 +14,7 @@ import { formatNumber } from '@polkadot/util';
 import translate from './translate';
 import { MemberId, Profile, EntryMethod, Paid, Screening, Genesis, SubscriptionId } from '@joystream/types/members';
 import { queryMembershipToProp } from './utils';
-import { Seat } from '@joystream/types/';
+import { Seat } from '@joystream/types/council';
 import { nonEmptyStr, queryToProp } from '@polkadot/joy-utils/index';
 import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
 

+ 1 - 1
pioneer/packages/joy-members/src/EditForm.tsx

@@ -10,7 +10,7 @@ import TxButton from '@polkadot/joy-utils/TxButton';
 import * as JoyForms from '@polkadot/joy-utils/forms';
 import { SubmittableResult } from '@polkadot/api';
 import { MemberId, UserInfo, Profile, PaidTermId, PaidMembershipTerms } from '@joystream/types/members';
-import { OptionText } from '@joystream/types/';
+import { OptionText } from '@joystream/types/common';
 import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
 import { queryMembershipToProp } from './utils';
 import { withCalls } from '@polkadot/react-api/index';

+ 1 - 1
pioneer/packages/joy-members/src/MemberPreview.tsx

@@ -11,7 +11,7 @@ import IdentityIcon from '@polkadot/react-components/IdentityIcon';
 import translate from './translate';
 import { MemberId, Profile } from '@joystream/types/members';
 import { queryMembershipToProp } from './utils';
-import { Seat } from '@joystream/types/';
+import { Seat } from '@joystream/types/council';
 import { nonEmptyStr, queryToProp } from '@polkadot/joy-utils/index';
 import { FlexCenter } from '@polkadot/joy-utils/FlexCenter';
 import { MutedSpan } from '@polkadot/joy-utils/MutedText';

+ 7 - 3
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -119,6 +119,9 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => { [key: string]: s
   })
 };
 
+const StyledProposalDescription = styled(Card.Description)`
+  font-size: 1.15rem;
+`;
 const ProposalParams = styled.div`
   display: grid;
   font-weight: bold;
@@ -127,7 +130,7 @@ const ProposalParams = styled.div`
   border: 1px solid rgba(0,0,0,.2);
   padding: 1.5rem 1.5rem 1rem 1.25rem;
   position: relative;
-  margin-top: 1.5rem;
+  margin-top: 1.7rem;
   @media screen and (max-width: 767px) {
     grid-template-columns: 1fr;
   }
@@ -149,6 +152,7 @@ const ProposalParamValue = styled.div`
   color: black;
   word-wrap: break-word;
   word-break: break-word;
+  font-size: 1.15rem;
   & .TextProposalContent {
     font-weight: normal;
   }
@@ -176,9 +180,9 @@ export default function Body ({
         <Card.Header>
           <Header as="h1">{title}</Header>
         </Card.Header>
-        <Card.Description>
+        <StyledProposalDescription>
           <ReactMarkdown source={description} linkTarget='_blank' />
-        </Card.Description>
+        </StyledProposalDescription>
         <ProposalParams>
           <ParamsHeader>Parameters:</ParamsHeader>
           { Object.entries(parsedParams).map(([paramName, paramValue]) => (

+ 4 - 4
pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx

@@ -5,7 +5,7 @@ import Body from './Body';
 import VotingSection from './VotingSection';
 import Votes from './Votes';
 import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
-import { ParsedProposal, ProposalVote } from '@polkadot/joy-utils/types/proposals';
+import { ParsedProposal, ProposalVotes } from '@polkadot/joy-utils/types/proposals';
 import { withCalls } from '@polkadot/react-api';
 import { withMulti } from '@polkadot/react-api/with';
 
@@ -13,7 +13,7 @@ import './Proposal.css';
 import { ProposalId, ProposalDecisionStatuses, ApprovedProposalStatuses, ExecutionFailedStatus } from '@joystream/types/proposals';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { MemberId } from '@joystream/types/members';
-import { Seat } from '@joystream/types/';
+import { Seat } from '@joystream/types/council';
 import { PromiseComponent } from '@polkadot/joy-utils/react/components';
 import ProposalDiscussion from './discussion/ProposalDiscussion';
 
@@ -115,7 +115,7 @@ export function getExtendedStatus (proposal: ParsedProposal, bestNumber: BlockNu
 type ProposalDetailsProps = MyAccountProps & {
   proposal: ParsedProposal;
   proposalId: ProposalId;
-  votesListState: { data: ProposalVote[]; error: any; loading: boolean };
+  votesListState: { data: ProposalVotes | null; error: any; loading: boolean };
   bestNumber?: BlockNumber;
   council?: Seat[];
 };
@@ -160,7 +160,7 @@ function ProposalDetails ({
             error={votesListState.error}
             loading={votesListState.loading}
             message="Fetching the votes...">
-            <Votes votes={votesListState.data} />
+            <Votes votes={votesListState.data as ProposalVotes} />
           </PromiseComponent>
         </ProposalDetailsVoting>
       </ProposalDetailsMain>

+ 1 - 1
pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx

@@ -59,7 +59,7 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
   const filteredProposals = proposalsMap.get(activeFilter) as ParsedProposal[];
 
   return (
-    <Container className="Proposal">
+    <Container className="Proposal" fluid>
       <PromiseComponent error={ error } loading={ loading } message="Fetching proposals...">
         <Menu tabular className="list-menu">
           {filters.map((filter, idx) => (

+ 5 - 7
pioneer/packages/joy-proposals/src/Proposal/Votes.tsx

@@ -1,31 +1,29 @@
 import React from 'react';
 import { Header, Divider, Table, Icon } from 'semantic-ui-react';
 import useVoteStyles from './useVoteStyles';
-import { ProposalVote } from '@polkadot/joy-utils/types/proposals';
+import { ProposalVotes } from '@polkadot/joy-utils/types/proposals';
 import { VoteKind } from '@joystream/types/proposals';
 import { VoteKindStr } from './VotingSection';
 import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview';
 
 type VotesProps = {
-  votes: ProposalVote[];
+  votes: ProposalVotes;
 };
 
 export default function Votes ({ votes }: VotesProps) {
-  const nonEmptyVotes = votes.filter(proposalVote => proposalVote.vote !== null);
-
-  if (!nonEmptyVotes.length) {
+  if (!votes.votes.length) {
     return <Header as="h4">No votes has been submitted!</Header>;
   }
 
   return (
     <>
       <Header as="h3">
-        All Votes: ({nonEmptyVotes.length} / {votes.length})
+        All Votes: ({votes.votes.length}/{votes.councilMembersLength})
       </Header>
       <Divider />
       <Table basic="very">
         <Table.Body>
-          {nonEmptyVotes.map((proposalVote, idx) => {
+          {votes.votes.map((proposalVote, idx) => {
             const { vote, member } = proposalVote;
             const voteStr = (vote as VoteKind).type.toString() as VoteKindStr;
             const { icon, textColor } = useVoteStyles(voteStr);

+ 1 - 1
pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx

@@ -8,7 +8,7 @@ import { SubmittableResult } from '@polkadot/api';
 import { Button } from 'semantic-ui-react';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
 import { ParsedPost, DiscussionContraints } from '@polkadot/joy-utils/types/proposals';
-import { ThreadId } from '@joystream/types/forum';
+import { ThreadId } from '@joystream/types/common';
 import { MemberId } from '@joystream/types/members';
 
 type OuterProps = {

+ 1 - 1
pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx

@@ -19,7 +19,7 @@ import { createType } from '@polkadot/types';
 import './forms.css';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import _ from 'lodash';
-import { ElectionParameters } from '@joystream/types/proposals';
+import { ElectionParameters } from '@joystream/types/council';
 import { PromiseComponent } from '@polkadot/joy-utils/react/components';
 
 type FormValues = GenericFormValues & {

+ 0 - 2
pioneer/packages/joy-proposals/src/validationSchema.ts

@@ -1,5 +1,4 @@
 import * as Yup from 'yup';
-import { checkAddress } from '@polkadot/util-crypto';
 
 // TODO: If we really need this (currency unit) we can we make "Validation" a functiction that returns an object.
 // We could then "instantialize" it in "withFormContainer" where instead of passing
@@ -242,7 +241,6 @@ const Validation: ValidationType = {
       .required('You need to specify an amount of tokens.'),
     destinationAccount: Yup.string()
       .required('Select a destination account!')
-      .test('address-test', 'Invalid account address.', account => checkAddress(account, 5)[0])
   },
   SetLead: {
     workingGroupLead: Yup.string().required('Select a proposed lead!')

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

@@ -102,7 +102,7 @@ export type GroupLead = {
   roleAccount: GenericAccountId;
   profile: IProfile;
   title: string;
-  stage: LeadRoleState;
+  stage?: LeadRoleState;
 }
 
 type inset = {
@@ -131,7 +131,7 @@ export function GroupLeadView (props: GroupLead & inset) {
         <Card.Description>
           <Label color='teal' ribbon={fluid}>
             <Icon name="shield" />
-          Content Lead
+            { props.title }
             <Label.Detail>{/* ... */}</Label.Detail>
           </Label>
         </Card.Description>

+ 4 - 0
pioneer/packages/joy-roles/src/flows/apply.controller.tsx

@@ -17,6 +17,7 @@ import { keyPairDetails, FlowModal, ProgressSteps } from './apply';
 
 import { OpeningStakeAndApplicationStatus } from '../tabs/Opportunities';
 import { Min, Step, Sum } from '../balances';
+import { WorkingGroups } from '../working_groups';
 
 type State = {
   // Input data from state
@@ -196,6 +197,9 @@ 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'));
     return (
       <Container className="apply-flow">

+ 0 - 1
pioneer/packages/joy-roles/src/index.sass

@@ -4,7 +4,6 @@
 @import 'styles/icons'
 @import 'styles/countdown'
 
-@import 'tabs/WorkingGroup'
 @import 'tabs/Opportunities'
 @import 'tabs/MyRoles'
 @import 'flows/apply'

+ 1 - 0
pioneer/packages/joy-roles/src/index.tsx

@@ -88,6 +88,7 @@ export const App: React.FC<Props> = (props: Props) => {
       <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`} render={() => renderViewComponent(OpportunitiesView(oppsCtrl))} />
         <Route path={`${basePath}/my-roles`} render={() => renderViewComponent(MyRolesView(myRolesCtrl))} />
         <Route path={`${basePath}/admin`} render={() => renderViewComponent(AdminView(adminCtrl))} />

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

@@ -1,7 +1,7 @@
 import React from 'react';
 import { withKnobs } from '@storybook/addon-knobs';
 import { Container, Tab } from 'semantic-ui-react';
-import { ContentCuratorsSection, StorageProvidersSection } from './tabs/WorkingGroup.stories';
+import { ContentCuratorsSection } from './tabs/WorkingGroup.stories';
 import { OpportunitySandbox } from './tabs/Opportunities.stories';
 import { ApplicationSandbox } from './flows/apply.stories';
 import { MyRolesSandbox } from './tabs/MyRoles.stories';
@@ -17,9 +17,6 @@ export const RolesPage = () => {
       <Container className="outer">
         <ContentCuratorsSection />
       </Container>
-      <Container>
-        <StorageProvidersSection />
-      </Container>
     </Container>
   );
 

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

@@ -11,6 +11,8 @@ import {
   OpeningsView
 } from './Opportunities';
 
+import { AvailableGroups, WorkingGroups } from '../working_groups';
+
 type State = {
   blockTime?: number;
   opportunities?: Array<WorkingGroupOpening>;
@@ -37,8 +39,9 @@ export class OpportunitiesController extends Controller<State, ITransport> {
 }
 
 export const OpportunitiesView = View<OpportunitiesController, State>(
-  (state) => (
+  (state, controller, params) => (
     <OpeningsView
+      group={AvailableGroups.includes(params.get('group') as any) ? params.get('group') as WorkingGroups : undefined}
       openings={state.opportunities}
       block_time_in_seconds={state.blockTime}
       member_id={state.memberId}

+ 49 - 4
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 } from 'react-router-dom';
+import { Link, useHistory } from 'react-router-dom';
 import {
   Button,
   Card,
@@ -14,7 +14,9 @@ import {
   Label,
   List,
   Message,
-  Statistic
+  Statistic,
+  Dropdown,
+  DropdownProps
 } from 'semantic-ui-react';
 
 import { formatBalance } from '@polkadot/util';
@@ -37,6 +39,9 @@ import {
 } from '../openingStateMarkup';
 
 import { Loadable } from '@polkadot/joy-utils/index';
+import styled from 'styled-components';
+import _ from 'lodash';
+import { WorkingGroups, AvailableGroups } from '../working_groups';
 
 type OpeningStage = OpeningMetadataProps & {
   stage: OpeningStageClassification;
@@ -458,6 +463,14 @@ export type WorkingGroupOpening = OpeningStage & DefactoMinimumStake & OpeningMe
   applications: OpeningStakeAndApplicationStatus;
 }
 
+const OpeningTitle = styled.h2`
+  display: flex;
+  align-items: flex-end;
+`;
+const OpeningLabel = styled(Label)`
+  margin-left: auto !important;
+`;
+
 type OpeningViewProps = WorkingGroupOpening & BlockTimeProps & MemberIdProps
 
 export const OpeningView = Loadable<OpeningViewProps>(
@@ -473,7 +486,10 @@ export const OpeningView = Loadable<OpeningViewProps>(
 
     return (
       <Container className={'opening ' + openingClass(props.stage.state)}>
-        <h2>{text.job.title}</h2>
+        <OpeningTitle>
+          {text.job.title}
+          <OpeningLabel>{ _.startCase(props.meta.group) }</OpeningLabel>
+        </OpeningTitle>
         <Card fluid className="container">
           <Card.Content className="header">
             <OpeningHeader stage={props.stage} meta={props.meta} />
@@ -497,17 +513,46 @@ export const OpeningView = Loadable<OpeningViewProps>(
   }
 );
 
+const FilterOpportunities = styled.div`
+  display: flex;
+  width: 100%;
+  margin-bottom: 1rem;
+`;
+const FilterOpportunitiesDropdown = styled(Dropdown)`
+  margin-left: auto !important;
+  width: 250px !important;
+`;
+
 export type OpeningsViewProps = MemberIdProps & {
   openings?: Array<WorkingGroupOpening>;
   block_time_in_seconds?: number;
+  group?: WorkingGroups;
 }
 
 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}`)
+    );
+
     return (
       <Container>
-        {props.openings && props.openings.map((opening, key) => (
+        <FilterOpportunities>
+          <FilterOpportunitiesDropdown
+            placeholder="All opportunities"
+            options={
+              [{ value: '', text: 'All opportunities' }]
+                .concat(AvailableGroups.map(g => ({ value: g, text: _.startCase(g) })))
+            }
+            value={group}
+            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} />
         ))}
       </Container>

+ 9 - 3
pioneer/packages/joy-roles/src/tabs/Opportunity.controller.tsx

@@ -12,6 +12,8 @@ import {
   OpeningView
 } from './Opportunities';
 
+import { WorkingGroups, AvailableGroups } from '../working_groups';
+
 type State = {
   blockTime?: number;
   opportunity?: WorkingGroupOpening;
@@ -26,12 +28,16 @@ export class OpportunityController extends Controller<State, ITransport> {
   }
 
   @memoize()
-  async getOpportunity (id: string | undefined) {
+  async getOpportunity (group: string | undefined, id: string | undefined) {
     if (!id) {
       return this.onError('OpportunityController: no ID provided in params');
     }
 
-    this.state.opportunity = await this.transport.curationGroupOpening(parseInt(id));
+    if (!group || !AvailableGroups.includes(group as any)) {
+      return this.onError('OppportunityController: invalid group provided in params');
+    }
+
+    this.state.opportunity = await this.transport.groupOpening(group as WorkingGroups, parseInt(id));
     this.dispatch();
   }
 
@@ -42,7 +48,7 @@ export class OpportunityController extends Controller<State, ITransport> {
 }
 
 const renderOpeningView = (state: State, controller: OpportunityController, params: Params) => {
-  controller.getOpportunity(params.get('id'));
+  controller.getOpportunity(params.get('group'), params.get('id'));
   return (
     <OpeningView {...state.opportunity!} block_time_in_seconds={state.blockTime!} member_id={state.memberId} />
   );

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

@@ -7,15 +7,18 @@ import { ITransport } from '../transport';
 import {
   ContentCurators,
   WorkingGroupMembership,
-  StorageAndDistributionMembership,
   GroupLeadStatus,
-  ContentLead
+  StorageProviders
 } from './WorkingGroup';
 
+import { WorkingGroups } from '../working_groups';
+import styled from 'styled-components';
+
 type State = {
   contentCurators?: WorkingGroupMembership;
-  storageProviders?: StorageAndDistributionMembership;
-  groupLeadStatus?: GroupLeadStatus;
+  storageProviders?: WorkingGroupMembership;
+  contentLeadStatus?: GroupLeadStatus;
+  storageLeadStatus?: GroupLeadStatus;
 }
 
 export class WorkingGroupsController extends Controller<State, ITransport> {
@@ -23,7 +26,8 @@ export class WorkingGroupsController extends Controller<State, ITransport> {
     super(transport, {});
     this.getCurationGroup();
     this.getStorageGroup();
-    this.getGroupLeadStatus();
+    this.getCuratorLeadStatus();
+    this.getStorageLeadStatus();
   }
 
   getCurationGroup () {
@@ -34,25 +38,41 @@ export class WorkingGroupsController extends Controller<State, ITransport> {
   }
 
   getStorageGroup () {
-    this.transport.storageGroup().then((value: StorageAndDistributionMembership) => {
+    this.transport.storageGroup().then((value: WorkingGroupMembership) => {
       this.setState({ storageProviders: value });
       this.dispatch();
     });
   }
 
-  getGroupLeadStatus () {
-    this.transport.groupLeadStatus().then((value: GroupLeadStatus) => {
-      this.setState({ groupLeadStatus: value });
+  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`
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-gap: 2rem;
+  @media screen and (max-width: 1199px) {
+    grid-template-columns: 1fr;
+  }
+`;
+
 export const WorkingGroupsView = View<WorkingGroupsController, State>(
   (state) => (
-    <div>
-      <ContentCurators {...state.contentCurators!} />
-      <ContentLead {...state.groupLeadStatus!} />
-    </div>
+    <WorkingGroupsOverview>
+      <ContentCurators {...state.contentCurators} leadStatus={state.contentLeadStatus}/>
+      <StorageProviders {...state.storageProviders} leadStatus={state.storageLeadStatus}/>
+    </WorkingGroupsOverview>
   )
 );

+ 0 - 21
pioneer/packages/joy-roles/src/tabs/WorkingGroup.sass

@@ -1,21 +0,0 @@
-#storage-providers 
-  margin-top: 3em
-  .container
-    margin-right: 1em
-
-  .balance span, .memo span
-    font-weight: bold
-
-  .balance
-    font-size: 0.8em
-
-  .button
-    color: green !important
-
-#content-curators 
-  .staked-card
-    margin-right: 1.2em !important
-
-  .cards
-    margin-top: 1em
-    margin-bottom: 1em

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

@@ -1,13 +1,11 @@
 import React from 'react';
 import { boolean, number, text, withKnobs } from '@storybook/addon-knobs';
 
-import { Balance } from '@polkadot/types/interfaces';
-import { Text, u128, GenericAccountId } from '@polkadot/types';
+import { u128, GenericAccountId } from '@polkadot/types';
 
-import { Actor } from '@joystream/types/roles';
-import { IProfile, MemberId } from '@joystream/types/members';
+import { MemberId } from '@joystream/types/members';
 
-import { ContentCurators, StorageAndDistribution } from '@polkadot/joy-roles/tabs/WorkingGroup';
+import { ContentCurators } from '@polkadot/joy-roles/tabs/WorkingGroup';
 import { GroupMember } from '../elements';
 
 import { mockProfile } from '../mocks';
@@ -80,37 +78,3 @@ export function ContentCuratorsSection () {
     <ContentCurators members={members} rolesAvailable={boolean('Roles available', true)} />
   );
 }
-
-export const StorageProvidersSection = () => {
-  const balances = new Map<string, Balance>([
-    ['5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp', new u128(101)]
-  ]);
-
-  const memos = new Map<string, Text>([
-    ['5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp', new Text('This is a memo')]
-  ]);
-
-  const profiles = new Map<number, IProfile>([
-    [1, mockProfile('bwhm0')],
-    [2, mockProfile(
-      'benholdencrowther',
-      'https://www.benholdencrowther.com/wp-content/uploads/2019/03/Hanging_Gardens_of_Babylon.jpg'
-    )]
-  ]);
-
-  const storageProviders: Actor[] = [
-    new Actor({ member_id: 1, account: '5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp' }),
-    new Actor({ member_id: 2, account: '5DQqNWRFPruFs9YKheVMqxUbqoXeMzAWfVfcJgzuia7NA3D3' })
-  ];
-
-  return (
-    <div>
-      <StorageAndDistribution
-        actors={storageProviders}
-        balances={balances}
-        memos={memos}
-        profiles={profiles}
-      />
-    </div>
-  );
-};

+ 120 - 100
pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx

@@ -1,135 +1,155 @@
 import React from 'react';
-import { Button, Card, Icon, Message, SemanticICONS, Table } from 'semantic-ui-react';
+import { Button, Card, Icon, Message, SemanticICONS } from 'semantic-ui-react';
 import { Link } from 'react-router-dom';
 
-import { Balance } from '@polkadot/types/interfaces';
-import { Actor } from '@joystream/types/roles';
-import { IProfile } from '@joystream/types/members';
-import { Text } from '@polkadot/types';
-
-import { ActorDetailsView, MemberView, GroupMemberView, GroupLeadView, GroupMember, GroupLead } from '../elements';
-
+import { GroupLeadView, GroupMember, GroupMemberView, GroupLead } from '../elements';
 import { Loadable } from '@polkadot/joy-utils/index';
 
+import { WorkingGroups } from '../working_groups';
+import styled from 'styled-components';
+import _ from 'lodash';
+
 export type WorkingGroupMembership = {
   members: GroupMember[];
   rolesAvailable: boolean;
 }
 
-export const ContentCurators = Loadable<WorkingGroupMembership>(
-  ['members'],
-  props => {
-    let message = (
-      <Message>
-        <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>
-    );
+const NoRolesAvailable = () => (
+  <Message>
+    <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>
+);
 
-    if (props.rolesAvailable) {
-      message = (
-        <Message positive>
-          <Message.Header>Join us and get paid to curate!</Message.Header>
-          <p>
-            There are openings for new content curators. This is a great way to support Joystream!
-          </p>
-          <Link to="/working-groups/opportunities">
-            <Button icon labelPosition="right" color="green" positive>
-              Find out more
-              <Icon name={'right arrow' as SemanticICONS} />
-            </Button>
-          </Link>
-        </Message>
-      );
-    }
+type JoinRoleProps = {
+  group: WorkingGroups;
+  title: string;
+  description: string;
+};
+
+const JoinRole = ({ group, title, description }: JoinRoleProps) => (
+  <Message positive>
+    <Message.Header>{title}</Message.Header>
+    <p>{description}</p>
+    <Link to={`/working-groups/opportunities/${group}`}>
+      <Button icon labelPosition="right" color="green" positive>
+        Find out more
+        <Icon name={'right arrow' as SemanticICONS} />
+      </Button>
+    </Link>
+  </Message>
+);
+
+const GroupOverviewSection = styled.section`
+  padding: 2rem;
+  background: #fff;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+
+  & .staked-card {
+    margin-right: 1.2em !important;
+  }
+
+  & .cards {
+    margin-top: 1em;
+    margin-bottom: 1em;
+  }
+`;
 
+type GroupOverviewOuterProps = Partial<WorkingGroupMembership> & {
+  leadStatus?: GroupLeadStatus;
+}
+
+type GroupOverviewProps = GroupOverviewOuterProps & {
+  group: WorkingGroups;
+  description: string;
+  customGroupName?: string;
+  customJoinTitle?: string;
+  customJoinDesc?: string;
+}
+
+const GroupOverview = Loadable<GroupOverviewProps>(
+  ['members', 'leadStatus'],
+  ({
+    group,
+    description,
+    members,
+    leadStatus,
+    rolesAvailable,
+    customGroupName,
+    customJoinTitle,
+    customJoinDesc
+  }: 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!`;
     return (
-      <section id="content-curators">
-        <h2>Content curators</h2>
-        <p>
-          Content Curators are responsible for ensuring that all content is uploaded correctly and in line with the terms of service.
-        </p>
+      <GroupOverviewSection>
+        <h2>{ groupName }</h2>
+        <p>{ description }</p>
         <Card.Group>
-          {props.members.map((member, key) => (
+          { members!.map((member, key) => (
             <GroupMemberView key={key} {...member} />
-          ))}
+          )) }
         </Card.Group>
-        {message}
-      </section>
+        { rolesAvailable
+          ? <JoinRole group={group} title={joinTitle} description={joinDesc} />
+          : <NoRolesAvailable /> }
+        { leadStatus && <CurrentLead groupName={groupName} {...leadStatus}/> }
+      </GroupOverviewSection>
     );
   }
 );
 
-export type StorageAndDistributionMembership = {
-  actors: Actor[];
-  balances: Map<string, Balance>;
-  memos: Map<string, Text>;
-  profiles: Map<number, IProfile>;
-}
+export const ContentCurators = (props: GroupOverviewOuterProps) => (
+  <GroupOverview
+    group={WorkingGroups.ContentCurators}
+    description={
+      'Content Curators are responsible for ensuring that all content is uploaded correctly ' +
+      'and in line with the terms of service.'
+    }
+    {...props}
+  />
+);
 
-export const StorageAndDistribution = Loadable<StorageAndDistributionMembership>(
-  ['actors'],
-  props => {
-    return (
-      <section id="storage-providers">
-        <h2>Storage and distribution</h2>
-        <Table basic='very'>
-          <Table.Header>
-            <Table.Row>
-              <Table.HeaderCell>Member</Table.HeaderCell>
-              <Table.HeaderCell>Details</Table.HeaderCell>
-            </Table.Row>
-          </Table.Header>
-          <Table.Body>
-            {props.actors.map((actor, key) => (
-              <Table.Row key={key}>
-                <Table.Cell>
-                  <MemberView
-                    actor={actor}
-                    balance={props.balances.get(actor.account.toString())}
-                    profile={props.profiles.get(actor.member_id.toNumber()) as IProfile}
-                  />
-                </Table.Cell>
-                <Table.Cell>
-                  <ActorDetailsView
-                    actor={actor}
-                    balance={props.balances.get(actor.account.toString())}
-                    memo={props.memos.get(actor.account.toString())}
-                  />
-                </Table.Cell>
-              </Table.Row>
-            ))}
-          </Table.Body>
-        </Table>
-      </section>
-    );
-  }
+export const StorageProviders = (props: GroupOverviewOuterProps) => (
+  <GroupOverview
+    group={WorkingGroups.StorageProviders}
+    description={
+      'Storage Providers are responsible for storing and providing platform content!'
+    }
+    {...props}
+  />
 );
 
+const LeadSection = styled.div`
+  margin-top: 1rem;
+`;
+
 export type GroupLeadStatus = {
   lead?: GroupLead;
   loaded: boolean;
 }
 
-export const ContentLead = Loadable<GroupLeadStatus>(
+type CurrentLeadProps = GroupLeadStatus & {
+  groupName: string;
+  customLeadDesc?: string;
+};
+
+export const CurrentLead = Loadable<CurrentLeadProps>(
   ['loaded'],
-  props => {
+  ({ customLeadDesc, groupName, lead }: CurrentLeadProps) => {
+    const leadDesc = customLeadDesc || `This role is responsible for hiring ${groupName}.`;
     return (
-      <section id='lead'>
-        <br/>
+      <LeadSection>
         <Message positive>
-          <Message.Header>Content Lead</Message.Header>
-          <p>
-          This role is responsible for hiring curators, and is assigned by the platform.
-          </p>
-          {props.lead
-            ? <Card.Group>
-              <GroupLeadView {...props.lead} />
-            </Card.Group>
-            : 'There is no active Content Lead assigned.'}
+          <Message.Header>{ groupName } Lead</Message.Header>
+          <p>{ leadDesc }</p>
+          {lead
+            ? <Card.Group><GroupLeadView {...lead} /></Card.Group>
+            : `There is no active ${groupName} Lead assigned.` }
         </Message>
-
-      </section>
+      </LeadSection>
     );
   }
 );

+ 24 - 25
pioneer/packages/joy-roles/src/transport.mock.ts

@@ -1,12 +1,12 @@
 import { Observable } from 'rxjs';
 import { Balance } from '@polkadot/types/interfaces';
-import { Option, Text, u32, u128, GenericAccountId } from '@polkadot/types';
+import { Option, u32, u128, GenericAccountId } from '@polkadot/types';
 
 import { Subscribable, Transport as TransportBase } from '@polkadot/joy-utils/index';
 
 import { ITransport } from './transport';
 
-import { Actor, Role } from '@joystream/types/roles';
+import { Role, MemberId } from '@joystream/types/members';
 import {
   Opening,
   AcceptingApplications,
@@ -14,9 +14,8 @@ import {
   ApplicationRationingPolicy,
   StakingPolicy
 } from '@joystream/types/hiring';
-import { IProfile, MemberId } from '@joystream/types/members';
 
-import { WorkingGroupMembership, StorageAndDistributionMembership, GroupLeadStatus } from './tabs/WorkingGroup';
+import { WorkingGroupMembership, GroupLeadStatus } from './tabs/WorkingGroup';
 import { CuratorId } from '@joystream/types/content-working-group';
 import { WorkingGroupOpening } from './tabs/Opportunities';
 import { ActiveRole, OpeningApplication } from './tabs/MyRoles';
@@ -28,6 +27,7 @@ import { OpeningState } from './classifiers';
 
 import * as faker from 'faker';
 import { mockProfile } from './mocks';
+import { WorkingGroups } from './working_groups';
 
 export class Transport extends TransportBase implements ITransport {
   protected simulateApiResponse<T> (value: T): Promise<T> {
@@ -46,7 +46,7 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
-  groupLeadStatus (): Promise<GroupLeadStatus> {
+  groupLeadStatus (group: WorkingGroups = WorkingGroups.ContentCurators): Promise<GroupLeadStatus> {
     return this.simulateApiResponse<GroupLeadStatus>({
       loaded: true
     });
@@ -112,28 +112,23 @@ export class Transport extends TransportBase implements ITransport {
     });
   }
 
-  storageGroup (): Promise<StorageAndDistributionMembership> {
-    return this.simulateApiResponse<StorageAndDistributionMembership>(
-      {
-        balances: new Map<string, Balance>([
-          ['5DfJWGbBAH8hLAg8rcRYZW5BEZbE4BJeCQKoxUeqoyewLSew', new u128(101)]
-        ]),
-        memos: new Map<string, Text>([
-          ['5DfJWGbBAH8hLAg8rcRYZW5BEZbE4BJeCQKoxUeqoyewLSew', new Text('This is a memo')]
-        ]),
-        profiles: new Map<number, IProfile>([
-          [1, mockProfile('bwhm0')],
-          [2, mockProfile(
+  storageGroup (): Promise<WorkingGroupMembership> {
+    return this.simulateApiResponse<WorkingGroupMembership>({
+      rolesAvailable: true,
+      members: [
+        {
+          memberId: new MemberId(1),
+          roleAccount: new GenericAccountId('5HZ6GtaeyxagLynPryM7ZnmLzoWFePKuDrkb4AT8rT4pU1fp'),
+          profile: mockProfile(
             'benholdencrowther',
             'https://www.benholdencrowther.com/wp-content/uploads/2019/03/Hanging_Gardens_of_Babylon.jpg'
-          )]
-        ]),
-        actors: [
-          new Actor({ member_id: 1, account: '5DfJWGbBAH8hLAg8rcRYZW5BEZbE4BJeCQKoxUeqoyewLSew' }),
-          new Actor({ member_id: 2, account: '5DQqNWRFPruFs9YKheVMqxUbqoXeMzAWfVfcJgzuia7NA3D3' })
-        ]
-      }
-    );
+          ),
+          title: 'Storage provider',
+          stake: new u128(10101),
+          earned: new u128(347829)
+        }
+      ]
+    });
   }
 
   currentOpportunities (): Promise<Array<WorkingGroupOpening>> {
@@ -301,6 +296,10 @@ 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[]> {
     const slots: Balance[] = [];
     for (let i = 0; i < 20; i++) {

+ 216 - 88
pioneer/packages/joy-roles/src/transport.substrate.ts

@@ -3,7 +3,8 @@ import { map, switchMap } from 'rxjs/operators';
 
 import ApiPromise from '@polkadot/api/promise';
 import { Balance } from '@polkadot/types/interfaces';
-import { GenericAccountId, Option, u32, u64, u128, Vec } from '@polkadot/types';
+import { GenericAccountId, Option, u32, 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';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
@@ -14,23 +15,29 @@ import { APIQueryCache, MultipleLinkedMapEntry, SingleLinkedMapEntry, Subscribab
 import { ITransport } from './transport';
 import { GroupMember } from './elements';
 
-import { Role } from '@joystream/types/roles';
 import {
   Curator, CuratorId,
   CuratorApplication, CuratorApplicationId,
-  CuratorInduction,
   CuratorRoleStakeProfile,
   CuratorOpening, CuratorOpeningId,
   Lead, LeadId
 } from '@joystream/types/content-working-group';
 
+import {
+  WorkerApplication, WorkerApplicationId,
+  WorkerOpening, WorkerOpeningId,
+  Worker, WorkerId,
+  WorkerRoleStakeProfile,
+  Lead as LeadOf
+} from '@joystream/types/bureaucracy';
+
 import { Application, Opening, OpeningId } from '@joystream/types/hiring';
 import { Stake, StakeId } from '@joystream/types/stake';
-import { Recipient, RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
-import { ActorInRole, Profile, MemberId, Role as MemberRole, RoleKeys, ActorId } from '@joystream/types/members';
+import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards';
+import { ActorInRole, Profile, MemberId, Role, RoleKeys, ActorId } from '@joystream/types/members';
 import { createAccount, generateSeed } from '@polkadot/joy-utils/accounts';
 
-import { WorkingGroupMembership, StorageAndDistributionMembership, GroupLeadStatus } from './tabs/WorkingGroup';
+import { WorkingGroupMembership, GroupLeadStatus } from './tabs/WorkingGroup';
 import { WorkingGroupOpening } from './tabs/Opportunities';
 import { ActiveRole, OpeningApplication } from './tabs/MyRoles';
 
@@ -42,8 +49,9 @@ import {
   classifyOpeningStakes,
   isApplicationHired
 } from './classifiers';
-import { WorkingGroups } from './working_groups';
+import { WorkingGroups, AvailableGroups } from './working_groups';
 import { Sort, Sum, Zero } from './balances';
+import _ from 'lodash';
 
 type WorkingGroupPair<HiringModuleType, WorkingGroupType> = {
   hiringModule: HiringModuleType;
@@ -55,13 +63,69 @@ type StakePair<T = Balance> = {
   role: T;
 }
 
-interface IRoleAccounter {
-  role_account: GenericAccountId;
-  induction?: CuratorInduction;
-  role_stake_profile?: Option<CuratorRoleStakeProfile>;
-  reward_relationship: Option<RewardRelationshipId>;
+type WGApiMethodType =
+  'nextOpeningId'
+  | 'openingById'
+  | 'nextApplicationId'
+  | 'applicationById'
+  | 'nextWorkerId'
+  | 'workerById';
+type WGApiMethodsMapping = { [key in WGApiMethodType]: string };
+
+type GroupApplication = CuratorApplication | WorkerApplication;
+type GroupApplicationId = CuratorApplicationId | WorkerApplicationId;
+type GroupOpening = CuratorOpening | WorkerOpening;
+type GroupOpeningId = CuratorOpeningId | WorkerOpeningId;
+type GroupWorker = Worker | Curator;
+type GroupWorkerId = CuratorId | WorkerId;
+type GroupWorkerStakeProfile = WorkerRoleStakeProfile | CuratorRoleStakeProfile;
+type GroupLead = Lead | LeadOf;
+type GroupLeadWithMemberId = {
+  lead: GroupLead;
+  memberId: MemberId;
 }
 
+type WGApiMapping = {
+  [key in WorkingGroups]: {
+    module: string;
+    methods: WGApiMethodsMapping;
+    openingType: Constructor<GroupOpening>;
+    applicationType: Constructor<GroupApplication>;
+    workerType: Constructor<GroupWorker>;
+  }
+};
+
+const workingGroupsApiMapping: WGApiMapping = {
+  [WorkingGroups.StorageProviders]: {
+    module: 'storageBureaucracy',
+    methods: {
+      nextOpeningId: 'nextWorkerOpeningId',
+      openingById: 'workerOpeningById',
+      nextApplicationId: 'nextWorkerApplicationId',
+      applicationById: 'workerApplicationById',
+      nextWorkerId: 'nextWorkerId',
+      workerById: 'workerById'
+    },
+    openingType: WorkerOpening,
+    applicationType: WorkerApplication,
+    workerType: Worker
+  },
+  [WorkingGroups.ContentCurators]: {
+    module: 'contentWorkingGroup',
+    methods: {
+      nextOpeningId: 'nextCuratorOpeningId',
+      openingById: 'curatorOpeningById',
+      nextApplicationId: 'nextCuratorApplicationId',
+      applicationById: 'curatorApplicationById',
+      nextWorkerId: 'nextCuratorId',
+      workerById: 'curatorById'
+    },
+    openingType: CuratorOpening,
+    applicationType: CuratorApplication,
+    workerType: Curator
+  }
+};
+
 export class Transport extends TransportBase implements ITransport {
   protected api: ApiPromise
   protected cachedApi: APIQueryCache
@@ -74,6 +138,13 @@ export class Transport extends TransportBase implements ITransport {
     this.queueExtrinsic = queueExtrinsic;
   }
 
+  cachedApiMethodByGroup (group: WorkingGroups, method: WGApiMethodType) {
+    const apiModule = workingGroupsApiMapping[group].module;
+    const apiMethod = workingGroupsApiMapping[group].methods[method];
+
+    return this.cachedApi.query[apiModule][apiMethod];
+  }
+
   unsubscribe () {
     this.cachedApi.unsubscribe();
   }
@@ -93,27 +164,21 @@ export class Transport extends TransportBase implements ITransport {
     return stake.value.value;
   }
 
-  protected async curatorStake (stakeProfile: CuratorRoleStakeProfile): Promise<Balance> {
+  protected async workerStake (stakeProfile: GroupWorkerStakeProfile): Promise<Balance> {
     return this.stakeValue(stakeProfile.stake_id);
   }
 
-  protected async curatorTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
+  protected async workerTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
     const relationship = new SingleLinkedMapEntry<RewardRelationship>(
       RewardRelationship,
       await this.cachedApi.query.recurringRewards.rewardRelationships(
         relationshipId
       )
     );
-    const recipient = new SingleLinkedMapEntry<Recipient>(
-      Recipient,
-      await this.cachedApi.query.recurringRewards.rewardRelationships(
-        relationship.value.recipient
-      )
-    );
-    return recipient.value.total_reward_received;
+    return relationship.value.total_reward_received;
   }
 
-  protected async memberIdFromRoleAndActorId (role: MemberRole, id: ActorId): Promise<MemberId> {
+  protected async memberIdFromRoleAndActorId (role: Role, id: ActorId): Promise<MemberId> {
     const memberId = (
       await this.cachedApi.query.members.membershipIdByActorInRole(
         new ActorInRole({
@@ -128,21 +193,27 @@ export class Transport extends TransportBase implements ITransport {
 
   protected memberIdFromCuratorId (curatorId: CuratorId): Promise<MemberId> {
     return this.memberIdFromRoleAndActorId(
-      new MemberRole(RoleKeys.Curator),
+      new Role(RoleKeys.Curator),
       curatorId
     );
   }
 
   protected memberIdFromLeadId (leadId: LeadId): Promise<MemberId> {
     return this.memberIdFromRoleAndActorId(
-      new MemberRole(RoleKeys.CuratorLead),
+      new Role(RoleKeys.CuratorLead),
       leadId
     );
   }
 
-  protected async groupMember (id: CuratorId, curator: IRoleAccounter): Promise<GroupMember> {
-    const roleAccount = curator.role_account;
-    const memberId = await this.memberIdFromCuratorId(id);
+  protected async groupMember (
+    group: WorkingGroups,
+    id: GroupWorkerId,
+    worker: GroupWorker
+  ): Promise<GroupMember> {
+    const roleAccount = worker.role_account;
+    const memberId = group === WorkingGroups.ContentCurators
+      ? await this.memberIdFromCuratorId(id)
+      : (worker as Worker).member_id;
 
     const profile = await this.cachedApi.query.members.memberProfile(memberId) as Option<Profile>;
     if (profile.isNone) {
@@ -150,41 +221,41 @@ export class Transport extends TransportBase implements ITransport {
     }
 
     let stakeValue: Balance = new u128(0);
-    if (curator.role_stake_profile && curator.role_stake_profile.isSome) {
-      stakeValue = await this.curatorStake(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.curatorTotalReward(curator.reward_relationship.unwrap());
+    if (worker.reward_relationship && worker.reward_relationship.isSome) {
+      earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
     }
 
     return ({
       roleAccount,
       memberId,
       profile: profile.unwrap(),
-      title: 'Content curator',
+      title: _.startCase(group).slice(0, -1), // FIXME: Temporary solution (just removes "s" at the end)
       stake: stakeValue,
       earned: earnedValue
     });
   }
 
-  protected async areAnyCuratorRolesOpen (): Promise<boolean> {
-    const nextId = await this.cachedApi.query.contentWorkingGroup.nextCuratorOpeningId() as CuratorId;
+  protected async areAnyGroupRolesOpen (group: WorkingGroups): 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
     if (nextId.eq(0)) {
       return false;
     }
 
-    const curatorOpenings = new MultipleLinkedMapEntry<CuratorOpeningId, CuratorOpening>(
-      CuratorOpeningId,
-      CuratorOpening,
-      await this.cachedApi.query.contentWorkingGroup.curatorOpeningById()
+    const groupOpenings = new MultipleLinkedMapEntry<GroupOpeningId, GroupOpening>(
+      OpeningId,
+      workingGroupsApiMapping[group].openingType,
+      await this.cachedApiMethodByGroup(group, 'openingById')()
     );
 
-    for (let i = 0; i < curatorOpenings.linked_values.length; i++) {
-      const opening = await this.opening(curatorOpenings.linked_values[i].opening_id.toNumber());
+    for (let i = 0; i < groupOpenings.linked_values.length; i++) {
+      const opening = await this.opening(groupOpenings.linked_values[i].opening_id.toNumber());
       if (opening.is_active) {
         return true;
       }
@@ -193,30 +264,64 @@ export class Transport extends TransportBase implements ITransport {
     return false;
   }
 
-  async groupLeadStatus (): Promise<GroupLeadStatus> {
+  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>;
 
-    if (optLeadId.isSome) {
-      const leadId = optLeadId.unwrap();
-      const lead = new SingleLinkedMapEntry<Lead>(
-        Lead,
-        await this.cachedApi.query.contentWorkingGroup.leadById(leadId)
-      );
+    if (!optLeadId.isSome) {
+      return null;
+    }
+
+    const leadId = optLeadId.unwrap();
+    const lead = new SingleLinkedMapEntry<Lead>(
+      Lead,
+      await this.cachedApi.query.contentWorkingGroup.leadById(leadId)
+    );
+
+    const memberId = await this.memberIdFromLeadId(leadId);
 
-      const memberId = await this.memberIdFromLeadId(leadId);
+    return {
+      lead: lead.value,
+      memberId
+    };
+  }
+
+  protected async currentStorageLead (): Promise <GroupLeadWithMemberId | null> {
+    const optLead = (await this.cachedApi.query.storageBureaucracy.currentLead()) as Option<LeadOf>;
+
+    if (!optLead.isSome) {
+      return null;
+    }
+
+    return {
+      lead: optLead.unwrap(),
+      memberId: optLead.unwrap().member_id
+    };
+  }
+
+  async groupLeadStatus (group: WorkingGroups = WorkingGroups.ContentCurators): Promise<GroupLeadStatus> {
+    const currentLead = group === WorkingGroups.ContentCurators
+      ? await this.currentCuratorLead()
+      : await this.currentStorageLead();
+
+    if (currentLead !== null) {
+      const profile = await this.cachedApi.query.members.memberProfile(currentLead.memberId) as Option<Profile>;
 
-      const profile = await this.cachedApi.query.members.memberProfile(memberId) as Option<Profile>;
       if (profile.isNone) {
-        throw new Error('no profile found');
+        throw new Error(`${group} lead profile not found!`);
       }
 
       return {
         lead: {
-          memberId,
-          roleAccount: lead.value.role_account,
+          memberId: currentLead.memberId,
+          roleAccount: currentLead.lead.role_account_id,
           profile: profile.unwrap(),
-          title: 'Content Lead',
-          stage: lead.value.stage
+          title: _.startCase(group) + ' Lead',
+          stage: group === WorkingGroups.ContentCurators ? (currentLead.lead as Lead).stage : undefined
         },
         loaded: true
       };
@@ -227,10 +332,10 @@ export class Transport extends TransportBase implements ITransport {
     }
   }
 
-  async curationGroup (): Promise<WorkingGroupMembership> {
-    const rolesAvailable = await this.areAnyCuratorRolesOpen();
+  async groupOverview (group: WorkingGroups): Promise<WorkingGroupMembership> {
+    const rolesAvailable = await this.areAnyGroupRolesOpen(group);
 
-    const nextId = await this.cachedApi.query.contentWorkingGroup.nextCuratorId() as CuratorId;
+    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)) {
@@ -240,45 +345,57 @@ export class Transport extends TransportBase implements ITransport {
       };
     }
 
-    const values = new MultipleLinkedMapEntry<CuratorId, Curator>(
-      CuratorId,
-      Curator,
-      await this.cachedApi.query.contentWorkingGroup.curatorById()
+    const values = new MultipleLinkedMapEntry<GroupWorkerId, GroupWorker>(
+      ActorId,
+      workingGroupsApiMapping[group].workerType,
+      await this.cachedApiMethodByGroup(group, 'workerById')() as GroupWorker
     );
 
-    const members = values.linked_values.filter(value => value.is_active).reverse();
-    const memberIds = values.linked_keys.filter((v, k) => values.linked_values[k].is_active).reverse();
+    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();
 
     return {
       members: await Promise.all(
-        members.map((member, k) => this.groupMember(memberIds[k], member))
+        workers.map((worker, k) => this.groupMember(group, workerIds[k], worker))
       ),
       rolesAvailable
     };
   }
 
-  storageGroup (): Promise<StorageAndDistributionMembership> {
-    return this.promise<StorageAndDistributionMembership>(
-      {} as StorageAndDistributionMembership
-    );
+  curationGroup (): Promise<WorkingGroupMembership> {
+    return this.groupOverview(WorkingGroups.ContentCurators);
+  }
+
+  storageGroup (): Promise<WorkingGroupMembership> {
+    return this.groupOverview(WorkingGroups.StorageProviders);
   }
 
-  async currentOpportunities (): Promise<Array<WorkingGroupOpening>> {
+  async opportunitiesByGroup (group: WorkingGroups): Promise<WorkingGroupOpening[]> {
     const output = new Array<WorkingGroupOpening>();
-    const nextId = await this.cachedApi.query.contentWorkingGroup.nextCuratorOpeningId() as CuratorOpeningId;
+    const nextId = (await this.cachedApiMethodByGroup(group, 'nextOpeningId')()) as GroupOpeningId;
 
     // This is chain specfic, but if next id is still 0, it means no curator openings have been added yet
     if (!nextId.eq(0)) {
       const highestId = nextId.toNumber() - 1;
 
       for (let i = highestId; i >= 0; i--) {
-        output.push(await this.curationGroupOpening(i));
+        output.push(await this.groupOpening(group, i));
       }
     }
 
     return output;
   }
 
+  async currentOpportunities (): Promise<WorkingGroupOpening[]> {
+    let opportunities: WorkingGroupOpening[] = [];
+
+    for (const group of AvailableGroups) {
+      opportunities = opportunities.concat(await this.opportunitiesByGroup(group));
+    }
+
+    return opportunities.sort((a, b) => b.stage.starting_block - a.stage.starting_block);
+  }
+
   protected async opening (id: number): Promise<Opening> {
     const opening = new SingleLinkedMapEntry<Opening>(
       Opening,
@@ -288,17 +405,17 @@ export class Transport extends TransportBase implements ITransport {
     return opening.value;
   }
 
-  protected async curatorOpeningApplications (curatorOpeningId: number): Promise<Array<WorkingGroupPair<Application, CuratorApplication>>> {
-    const output = new Array<WorkingGroupPair<Application, CuratorApplication>>();
+  protected async groupOpeningApplications (group: WorkingGroups, groupOpeningId: number): Promise<WorkingGroupPair<Application, GroupApplication>[]> {
+    const output = new Array<WorkingGroupPair<Application, GroupApplication>>();
 
-    const nextAppid = await this.cachedApi.query.contentWorkingGroup.nextCuratorApplicationId() as u64;
+    const nextAppid = (await this.cachedApiMethodByGroup(group, 'nextApplicationId')()) as GroupApplicationId;
     for (let i = 0; i < nextAppid.toNumber(); i++) {
-      const cApplication = new SingleLinkedMapEntry<CuratorApplication>(
-        CuratorApplication,
-        await this.cachedApi.query.contentWorkingGroup.curatorApplicationById(i)
+      const cApplication = new SingleLinkedMapEntry<GroupApplication>(
+        workingGroupsApiMapping[group].applicationType,
+        await this.cachedApiMethodByGroup(group, 'applicationById')(i)
       );
 
-      if (cApplication.value.curator_opening_id.toNumber() !== curatorOpeningId) {
+      if (cApplication.value.worker_opening_id.toNumber() !== groupOpeningId) {
         continue;
       }
 
@@ -319,29 +436,35 @@ export class Transport extends TransportBase implements ITransport {
     return output;
   }
 
-  async curationGroupOpening (id: number): Promise<WorkingGroupOpening> {
-    const nextId = (await this.cachedApi.query.contentWorkingGroup.nextCuratorOpeningId() as u32).toNumber();
+  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();
     if (id < 0 || id >= nextId) {
       throw new Error('invalid id');
     }
 
-    const curatorOpening = new SingleLinkedMapEntry<CuratorOpening>(
-      CuratorOpening,
-      await this.cachedApi.query.contentWorkingGroup.curatorOpeningById(id)
+    const groupOpening = new SingleLinkedMapEntry<GroupOpening>(
+      workingGroupsApiMapping[group].openingType,
+      await this.cachedApiMethodByGroup(group, 'openingById')(id)
     );
 
     const opening = await this.opening(
-      curatorOpening.value.getField<OpeningId>('opening_id').toNumber()
+      groupOpening.value.opening_id.toNumber()
     );
 
-    const applications = await this.curatorOpeningApplications(id);
+    const applications = await this.groupOpeningApplications(group, id);
     const stakes = classifyOpeningStakes(opening);
 
     return ({
       opening: opening,
       meta: {
         id: id.toString(),
-        group: WorkingGroups.ContentCurators
+        group
       },
       stage: await classifyOpeningStage(this, opening),
       applications: {
@@ -355,6 +478,11 @@ 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>>();
 
@@ -537,12 +665,12 @@ export class Transport extends TransportBase implements ITransport {
         .map(async (curator, key) => {
           let stakeValue: Balance = new u128(0);
           if (curator.role_stake_profile && curator.role_stake_profile.isSome) {
-            stakeValue = await this.curatorStake(curator.role_stake_profile.unwrap());
+            stakeValue = await this.workerStake(curator.role_stake_profile.unwrap());
           }
 
           let earnedValue: Balance = new u128(0);
           if (curator.reward_relationship && curator.reward_relationship.isSome) {
-            earnedValue = await this.curatorTotalReward(curator.reward_relationship.unwrap());
+            earnedValue = await this.workerTotalReward(curator.reward_relationship.unwrap());
           }
 
           return {

+ 6 - 4
pioneer/packages/joy-roles/src/transport.ts

@@ -1,19 +1,21 @@
 import { Subscribable } from '@polkadot/joy-utils/index';
 import { Balance } from '@polkadot/types/interfaces';
 
-import { Role } from '@joystream/types/roles';
+import { Role } from '@joystream/types/members';
 
-import { WorkingGroupMembership, StorageAndDistributionMembership, GroupLeadStatus } from './tabs/WorkingGroup';
+import { WorkingGroupMembership, GroupLeadStatus } from './tabs/WorkingGroup';
 import { WorkingGroupOpening } from './tabs/Opportunities';
 import { keyPairDetails } from './flows/apply';
 import { ActiveRole, OpeningApplication } from './tabs/MyRoles';
+import { WorkingGroups } from './working_groups';
 
 export interface ITransport {
   roles: () => Promise<Array<Role>>;
-  groupLeadStatus: () => Promise<GroupLeadStatus>;
+  groupLeadStatus: (group: WorkingGroups) => Promise<GroupLeadStatus>;
   curationGroup: () => Promise<WorkingGroupMembership>;
-  storageGroup: () => Promise<StorageAndDistributionMembership>;
+  storageGroup: () => Promise<WorkingGroupMembership>;
   currentOpportunities: () => Promise<Array<WorkingGroupOpening>>;
+  groupOpening: (group: WorkingGroups, id: number) => Promise<WorkingGroupOpening>;
   curationGroupOpening: (id: number) => Promise<WorkingGroupOpening>;
   openingApplicationRanks: (openingId: number) => Promise<Balance[]>;
   expectedBlockTime: () => Promise<number>;

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

@@ -1,3 +1,9 @@
 export enum WorkingGroups {
   ContentCurators = 'curators',
+  StorageProviders = 'storageProviders'
 }
+
+export const AvailableGroups: readonly WorkingGroups[] = [
+  WorkingGroups.ContentCurators,
+  WorkingGroups.StorageProviders
+] as const;

+ 0 - 1
pioneer/packages/joy-storage/README.md

@@ -1 +0,0 @@
-# @polkadot/joy-actors

+ 0 - 15
pioneer/packages/joy-storage/package.json

@@ -1,15 +0,0 @@
-{
-  "name": "@polkadot/joy-storage",
-  "version": "0.0.1",
-  "description": "Staked roles module for Joystream node",
-  "main": "index.js",
-  "scripts": {},
-  "author": "Joystream contributors",
-  "maintainers": [],
-  "dependencies": {
-    "@babel/runtime": "^7.7.1",
-    "@polkadot/react-components": "^0.37.0-beta.63",
-    "@polkadot/react-query": "^0.37.0-beta.63",
-    "@polkadot/joy-utils": "^0.1.1"
-  }
-}

+ 0 - 76
pioneer/packages/joy-storage/src/ActorsList/index.tsx

@@ -1,76 +0,0 @@
-import React from 'react';
-import { BareProps } from '@polkadot/react-components/types';
-import { ComponentProps } from '../props';
-import { withCalls } from '@polkadot/react-api/index';
-import { Table } from 'semantic-ui-react';
-import { Option } from '@polkadot/types';
-import { AccountId } from '@polkadot/types/interfaces';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
-import { Actor } from '@joystream/types/roles';
-import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
-import TxButton from '@polkadot/joy-utils/TxButton';
-
-type Props = BareProps & ComponentProps & MyAccountProps;
-
-class ActorsList extends React.PureComponent<Props> {
-  render () {
-    const { actorAccountIds, myMemberId, iAmMember } = this.props;
-
-    return (
-      <Table>
-        <Table.Header>
-          <Table.Row>
-            <Table.HeaderCell>Member Id</Table.HeaderCell>
-            <Table.HeaderCell>Role</Table.HeaderCell>
-            <Table.HeaderCell>Actor Account</Table.HeaderCell>
-            <Table.HeaderCell></Table.HeaderCell>
-          </Table.Row>
-        </Table.Header>
-        <Table.Body>{actorAccountIds.map((actor_account: string) =>
-          <ActorDisplay key={actor_account} actor_account={actor_account} myMemberId={myMemberId} iAmMember={iAmMember} />
-        )}
-        </Table.Body>
-      </Table>
-    );
-  }
-}
-
-type ActorProps = MyAccountProps & {
-  actor_account: string;
-  actor?: Option<Actor>;
-}
-
-class ActorInner extends React.PureComponent<ActorProps> {
-  render () {
-    const { actor: actorOpt, iAmMember, myMemberId } = this.props;
-
-    if (!actorOpt || actorOpt.isNone) return null;
-
-    const actor = actorOpt.unwrap();
-    const memberIsActor = iAmMember && myMemberId &&
-                            (myMemberId.toString() === actor.member_id.toString());
-
-    return (
-      <Table.Row>
-        <Table.Cell>{actor.member_id.toString()}</Table.Cell>
-        <Table.Cell>{actor.role.toString()}</Table.Cell>
-        <Table.Cell>
-          <AddressMini value={actor.account} isShort={false} isPadded={false} withBalance={true} /></Table.Cell>
-        {memberIsActor ? <Table.Cell>{this.renderUnstakeTxButton(actor.account)}</Table.Cell> : null}
-      </Table.Row>
-    );
-  }
-
-  private renderUnstakeTxButton (account: AccountId) {
-    return <TxButton tx={'actors.unstake'} params={[account]} label={'Unstake'}
-      type='submit' size='large' isDisabled={false} />;
-  }
-}
-
-const ActorDisplay = withCalls<ActorProps>(
-  ['query.actors.actorByAccountId', { propName: 'actor', paramName: 'actor_account' }]
-)(ActorInner);
-
-const ActionableActorsList = withMyAccount(ActorsList);
-
-export default ActionableActorsList;

+ 0 - 95
pioneer/packages/joy-storage/src/AvailableRoles/index.tsx

@@ -1,95 +0,0 @@
-import React from 'react';
-import { BareProps } from '@polkadot/react-components/types';
-import { ComponentProps } from '../props';
-import { Role, RoleParameters } from '@joystream/types/roles';
-import { Option } from '@polkadot/types';
-import { AccountId } from '@polkadot/types/interfaces';
-import { withCalls } from '@polkadot/react-api/index';
-import { Table } from 'semantic-ui-react';
-import Section from '@polkadot/joy-utils/Section';
-import { formatBalance } from '@polkadot/util';
-
-import BN from 'bn.js';
-
-type Props = BareProps & ComponentProps;
-
-export default class AvailableRoles extends React.PureComponent<Props> {
-  render () {
-    return (
-      <div>{this.props.roles.map((role) =>
-        <div key={role.toString()}><RoleDisplay role={role} /></div>)
-      }</div>
-    );
-  }
-}
-
-type RoleProps = BareProps & {
-  role: Role;
-  roleParams?: Option<RoleParameters>;
-  actorAccountIds?: Array<AccountId>;
-}
-
-class RoleDisplayInner extends React.PureComponent<RoleProps> {
-  render () {
-    const { role, roleParams, actorAccountIds } = this.props;
-    if (!roleParams || roleParams.isNone || !actorAccountIds) return <em>Loading...</em>;
-
-    const params = roleParams.unwrap();
-
-    return (
-      <Section title={role.toString()}>
-        <Parameters role={role} params={params} active={actorAccountIds.length}></Parameters>
-      </Section>
-    );
-  }
-}
-
-const RoleDisplay = withCalls<RoleProps>(
-  ['query.actors.parameters', { propName: 'roleParams', paramName: 'role' }],
-  ['query.actors.accountIdsByRole', { propName: 'actorAccountIds', paramName: 'role' }]
-)(RoleDisplayInner);
-
-type ParamProps = BareProps & {
-  role: Role;
-  params: RoleParameters;
-  active: number;
-}
-
-const Parameters = function Parameters (props: ParamProps) {
-  const { params, role, active } = props;
-
-  const minStake = formatBalance(new BN(params.min_stake));
-  const maxActors = (new BN(params.max_actors)).toString();
-  const reward = formatBalance(new BN(params.reward));
-  const rewardPeriod = (new BN(params.reward_period)).toString();
-  const unbondingPeriod = (new BN(params.unbonding_period)).toString();
-
-  return (
-    <Table>
-      <Table.Header>
-        <Table.Row>
-          <Table.HeaderCell>Role Id</Table.HeaderCell>
-          <Table.HeaderCell>{role.toNumber()}</Table.HeaderCell>
-        </Table.Row>
-      </Table.Header>
-      <Table.Body>
-        <Table.Row>
-          <Table.Cell>Minimum Stake</Table.Cell>
-          <Table.Cell>{minStake}</Table.Cell>
-        </Table.Row>
-        <Table.Row>
-          <Table.Cell>Actors</Table.Cell>
-          <Table.Cell>{active}/{maxActors}</Table.Cell>
-        </Table.Row>
-        <Table.Row>
-          <Table.Cell>Reward</Table.Cell>
-          <Table.Cell>{reward}, every {rewardPeriod} blocks</Table.Cell>
-        </Table.Row>
-        <Table.Row>
-          <Table.Cell>Unbonding Period</Table.Cell>
-          <Table.Cell>{unbondingPeriod} blocks</Table.Cell>
-        </Table.Row>
-      </Table.Body>
-    </Table>
-  );
-};

+ 0 - 98
pioneer/packages/joy-storage/src/MyRequests/index.tsx

@@ -1,98 +0,0 @@
-import React from 'react';
-import { Table } from 'semantic-ui-react';
-import { BareProps, CallProps } from '@polkadot/react-api/types';
-import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import { withCalls, withMulti } from '@polkadot/react-api/index';
-import { Request, Role, RoleParameters } from '@joystream/types/roles';
-import { Option } from '@polkadot/types';
-import { AccountId, Balance } from '@polkadot/types/interfaces';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import BN from 'bn.js';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
-import { ComponentProps } from '../props';
-
-type Props = BareProps & ComponentProps & MyAccountProps & {
-  requests: Array<Request>;
-};
-
-class ActionList extends React.PureComponent<Props> {
-  render () {
-    const { myMemberId, requests } = this.props;
-    if (!myMemberId) {
-      return <em>Loading...</em>;
-    }
-
-    // filter requests for member
-    const filteredRequests = requests.filter((request) => request[1].toString() === myMemberId.toString());
-
-    if (filteredRequests.length) {
-      return this.renderActions(filteredRequests);
-    } else {
-      return <div>No requests for member id: {myMemberId.toString()}</div>;
-    }
-  }
-
-  private renderActions (requests: Array<Request>) {
-    return (
-      <Table>
-        <Table.Body>
-          {
-            requests.map(([account, _, role]: Request) => {
-              return <ActionDisplay account={account} role={role} key={account.toString()}></ActionDisplay>;
-            })
-          }
-        </Table.Body>
-      </Table>
-    );
-  }
-}
-
-type ActionProps = BareProps & CallProps & {
-  account: AccountId;
-  role: Role;
-  balance?: Balance;
-  roleParams?: Option<RoleParameters>;
-};
-
-class Action extends React.PureComponent<ActionProps> {
-  render () {
-    const { account, role, balance, roleParams } = this.props;
-
-    if (!balance || !roleParams || roleParams.isNone) return null;
-
-    const params = roleParams.unwrap();
-
-    const minStake = new BN(params.min_stake);
-    const canStake = balance.gte(minStake);
-
-    return (
-      <Table.Row>
-        <Table.Cell>
-          <AddressMini value={account} isShort={false} isPadded={false} withBalance={true} />
-        </Table.Cell>
-        <Table.Cell>{role.toString()}</Table.Cell>
-        <Table.Cell>{canStake ? null : `(minimum required balance: ${minStake.toString()})`}</Table.Cell>
-        <Table.Cell>
-          <TxButton
-            type='submit'
-            size='large'
-            label={'Stake'}
-            isDisabled={!canStake}
-            params={[role, account]}
-            tx={'actors.stake'}
-          />
-        </Table.Cell>
-      </Table.Row>
-    );
-  }
-}
-
-const ActionDisplay = withCalls<ActionProps>(
-  ['query.balances.freeBalance', { propName: 'balance', paramName: 'account' }],
-  ['query.actors.parameters', { propName: 'roleParams', paramName: 'role' }]
-)(Action);
-
-export default withMulti(
-  ActionList,
-  withOnlyMembers
-);

+ 0 - 0
pioneer/packages/joy-storage/src/index.css


+ 0 - 112
pioneer/packages/joy-storage/src/index.tsx

@@ -1,112 +0,0 @@
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
-import { ApiProps } from '@polkadot/react-api/types';
-import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
-import { ComponentProps } from './props';
-import { Request, Role } from '@joystream/types/roles';
-
-import React from 'react';
-import { Route, Switch } from 'react-router';
-import { AccountId } from '@polkadot/types/interfaces';
-import Tabs, { TabItem } from '@polkadot/react-components/Tabs';
-import accountObservable from '@polkadot/ui-keyring/observable/accounts';
-import { withCalls, withMulti, withObservable } from '@polkadot/react-api/index';
-
-import ActorsList from './ActorsList';
-import MyRequests from './MyRequests';
-import AvailableRoles from './AvailableRoles';
-
-import './index.css';
-
-import translate from './translate';
-
-type Props = AppProps &
-ApiProps &
-I18nProps & {
-  requests?: Array<Request>;
-  actorAccountIds?: Array<AccountId>;
-  roles?: Array<Role>;
-  allAccounts?: SubjectInfo;
-};
-
-type State = {
-  tabs: Array<TabItem>;
-  actorAccountIds: Array<string>;
-  requests: Array<Request>;
-  roles: Array<Role>;
-};
-
-class App extends React.PureComponent<Props, State> {
-  state: State;
-
-  constructor (props: Props) {
-    super(props);
-
-    const { t } = props;
-
-    this.state = {
-      actorAccountIds: [],
-      requests: [],
-      roles: [],
-      tabs: [
-        {
-          isRoot: true,
-          name: 'actors',
-          text: t('Staked Providers')
-        },
-        {
-          name: 'roles',
-          text: t('Role Details')
-        },
-        {
-          name: 'requests',
-          text: t('My Staking Requests')
-        }
-      ]
-    };
-  }
-
-  static getDerivedStateFromProps ({ actorAccountIds, requests, roles }: Props): State {
-    return {
-      actorAccountIds: (actorAccountIds || []).map(accountId => accountId.toString()),
-      requests: (requests || []).map(request => request),
-      roles: (roles || []).map(role => role)
-    } as State;
-  }
-
-  render () {
-    const { tabs } = this.state;
-    const { basePath } = this.props;
-
-    return (
-      <main className="actors--App">
-        <header>
-          <Tabs basePath={basePath} items={tabs} />
-        </header>
-        <Switch>
-          <Route path={`${basePath}/requests`} render={this.renderComponent(MyRequests)} />
-          <Route path={`${basePath}/roles`} render={this.renderComponent(AvailableRoles)} />
-          <Route render={this.renderComponent(ActorsList)} />
-        </Switch>
-      </main>
-    );
-  }
-
-  private renderComponent (Component: React.ComponentType<ComponentProps>) {
-    return (): React.ReactNode => {
-      const { actorAccountIds, requests, roles } = this.state;
-
-      return <Component actorAccountIds={actorAccountIds} requests={requests} roles={roles} />;
-    };
-  }
-}
-
-export default withMulti(
-  App,
-  translate,
-  withObservable(accountObservable.subject, { propName: 'allAccounts' }),
-  withCalls<Props>(
-    ['query.actors.actorAccountIds', { propName: 'actorAccountIds' }],
-    ['query.actors.roleEntryRequests', { propName: 'requests' }],
-    ['query.actors.availableRoles', { propName: 'roles' }]
-  )
-);

+ 0 - 7
pioneer/packages/joy-storage/src/props.ts

@@ -1,7 +0,0 @@
-import { Request, Role } from '@joystream/types/roles';
-
-export type ComponentProps = {
-  actorAccountIds: Array<string>;
-  requests: Array<Request>;
-  roles: Array<Role>;
-};

+ 0 - 3
pioneer/packages/joy-storage/src/translate.ts

@@ -1,3 +0,0 @@
-import { withTranslation } from 'react-i18next';
-
-export default withTranslation(['actors', 'ui']);

+ 1 - 1
pioneer/packages/joy-utils/src/index.ts

@@ -7,7 +7,7 @@ import keyring from '@polkadot/ui-keyring';
 // Joystream Stake utils
 // --------------------------------------
 
-import { Stake, Backer } from '@joystream/types/';
+import { Stake, Backer } from '@joystream/types/council';
 
 // Substrate/Polkadot API utils
 // --------------------------------------

+ 3 - 3
pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx

@@ -1,5 +1,5 @@
 import { useState, useEffect } from 'react';
-import { ParsedProposal, ProposalVote } from '../../../types/proposals';
+import { ParsedProposal, ProposalVotes } from '../../../types/proposals';
 import { useTransport, usePromise } from '../';
 import { ProposalId } from '@joystream/types/proposals';
 
@@ -15,9 +15,9 @@ const useProposalSubscription = (id: ProposalId) => {
     {} as ParsedProposal
   );
 
-  const [votes, votesError, votesLoading, refreshVotes] = usePromise<ProposalVote[]>(
+  const [votes, votesError, votesLoading, refreshVotes] = usePromise<ProposalVotes | null>(
     () => transport.proposals.votes(id),
-    []
+    null
   );
 
   // Function to re-fetch the data using transport

+ 14 - 2
pioneer/packages/joy-utils/src/transport/council.ts

@@ -1,19 +1,31 @@
 import { ParsedMember } from '../types/members';
 import BaseTransport from './base';
-import { Seats, ElectionParameters } from '@joystream/types/proposals';
+import { Seats, ElectionParameters } from '@joystream/types/council';
 import { MemberId, Profile } from '@joystream/types/members';
 import { u32, Vec } from '@polkadot/types/';
 import { Balance, BlockNumber } from '@polkadot/types/interfaces';
 import { FIRST_MEMBER_ID } from '../consts/members';
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
+import ChainTransport from './chain';
 
 export default class CouncilTransport extends BaseTransport {
   private membersT: MembersTransport;
+  private chainT: ChainTransport;
 
-  constructor (api: ApiPromise, membersTransport: MembersTransport) {
+  constructor (api: ApiPromise, membersTransport: MembersTransport, chainTransport: ChainTransport) {
     super(api);
     this.membersT = membersTransport;
+    this.chainT = chainTransport;
+  }
+
+  async councilMembersLength (atBlock?: number): Promise<number> {
+    if (atBlock) {
+      const blockHash = await this.chainT.blockHash(atBlock);
+      return ((await this.council.activeCouncil.at(blockHash)) as Seats).length;
+    }
+
+    return ((await this.council.activeCouncil()) as Seats).length;
   }
 
   async councilMembers (): Promise<(ParsedMember & { memberId: MemberId })[]> {

+ 1 - 1
pioneer/packages/joy-utils/src/transport/index.ts

@@ -24,7 +24,7 @@ export default class Transport {
     this.members = new MembersTransport(api);
     this.storageProviders = new StorageProvidersTransport(api);
     this.validators = new ValidatorsTransport(api);
-    this.council = new CouncilTransport(api, this.members);
+    this.council = new CouncilTransport(api, this.members, this.chain);
     this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.members);
     this.proposals = new ProposalsTransport(api, this.members, this.chain, this.council);
   }

+ 4 - 0
pioneer/packages/joy-utils/src/transport/members.ts

@@ -6,4 +6,8 @@ export default class MembersTransport extends BaseTransport {
   memberProfile (id: MemberId | number): Promise<Option<Profile>> {
     return this.members.memberProfile(id) as Promise<Option<Profile>>;
   }
+
+  async membersCreated (): Promise<number> {
+    return (await this.members.membersCreated() as MemberId).toNumber();
+  }
 }

+ 31 - 11
pioneer/packages/joy-utils/src/transport/proposals.ts

@@ -3,6 +3,7 @@ import {
   ProposalType,
   ProposalTypes,
   ProposalVote,
+  ProposalVotes,
   ParsedPost,
   ParsedDiscussion,
   DiscussionContraints
@@ -11,7 +12,7 @@ import { ParsedMember } from '../types/members';
 
 import BaseTransport from './base';
 
-import { ThreadId, PostId } from '@joystream/types/forum';
+import { ThreadId, PostId } from '@joystream/types/common';
 import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost } from '@joystream/types/proposals';
 import { MemberId } from '@joystream/types/members';
 import { u32, u64 } from '@polkadot/types/';
@@ -20,6 +21,7 @@ import { BalanceOf } from '@polkadot/types/interfaces';
 import { includeKeys, bytesToString } from '../functions/misc';
 import _ from 'lodash';
 import proposalsConsts from '../consts/proposals';
+import { FIRST_MEMBER_ID } from '../consts/members';
 
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
@@ -120,17 +122,35 @@ export default class ProposalsTransport extends BaseTransport {
     return hasVoted ? vote : null;
   }
 
-  async votes (proposalId: ProposalId): Promise<ProposalVote[]> {
-    const councilMembers = await this.councilT.councilMembers();
-    return Promise.all(
-      councilMembers.map(async member => {
-        const vote = await this.voteByProposalAndMember(proposalId, member.memberId);
-        return {
-          vote,
-          member
-        };
-      })
+  async votes (proposalId: ProposalId): Promise<ProposalVotes> {
+    const voteEntries = await this.doubleMapEntries(
+      'proposalsEngine.voteExistsByProposalByVoter', // Double map of intrest
+      proposalId, // First double-map key value
+      (v) => new VoteKind(v), // Converter from hex
+      async () => (await this.membersT.membersCreated()), // A function that returns the number of iterations to go through when chekcing possible values for the second double-map key (memberId)
+      FIRST_MEMBER_ID.toNumber() // Min. possible value for second double-map key (memberId)
     );
+
+    const votesWithMembers: ProposalVote[] = [];
+    for (const voteEntry of voteEntries) {
+      const memberId = voteEntry.secondKey;
+      const vote = voteEntry.value;
+      const parsedMember = (await this.membersT.memberProfile(memberId)).toJSON() as ParsedMember;
+      votesWithMembers.push({
+        vote,
+        member: {
+          memberId: new MemberId(memberId),
+          ...parsedMember
+        }
+      });
+    }
+
+    const proposal = await this.rawProposalById(proposalId);
+
+    return {
+      councilMembersLength: await this.councilT.councilMembersLength(proposal.createdAt.toNumber()),
+      votes: votesWithMembers
+    };
   }
 
   async fetchProposalMethodsFromCodex (includeKey: string) {

+ 6 - 1
pioneer/packages/joy-utils/src/types/proposals.ts

@@ -1,6 +1,6 @@
 import { ProposalId, VoteKind } from '@joystream/types/proposals';
 import { MemberId, Profile } from '@joystream/types/members';
-import { ThreadId, PostId } from '@joystream/types/forum';
+import { ThreadId, PostId } from '@joystream/types/common';
 import { ParsedMember } from './members';
 
 export const ProposalTypes = [
@@ -46,6 +46,11 @@ export type ProposalVote = {
   member: ParsedMember & { memberId: MemberId };
 };
 
+export type ProposalVotes = {
+  councilMembersLength: number;
+  votes: ProposalVote[];
+};
+
 export const Categories = {
   storage: 'Storage',
   council: 'Council',

+ 0 - 2
pioneer/tsconfig.json

@@ -28,8 +28,6 @@
       "@polkadot/joy-proposals/*": [ "packages/joy-proposals/src/*" ],
       "@polkadot/joy-roles/": [ "packages/joy-roles/src/" ],
       "@polkadot/joy-roles/*": [ "packages/joy-roles/src/*" ],
-      "@polkadot/joy-storage/": [ "packages/joy-storage/src/" ],
-      "@polkadot/joy-storage/*": [ "packages/joy-storage/src/*" ],
       "@polkadot/joy-settings/": [ "packages/joy-settings/src/" ],
       "@polkadot/joy-settings/*": [ "packages/joy-settings/src/*" ],
       "@polkadot/joy-utils/": [ "packages/joy-utils/src/" ],

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

@@ -6,7 +6,7 @@ use rstd::prelude::*;
 
 /// DIRTY IMPORT BECAUSE
 /// InputValidationLengthConstraint has not been factored out yet!!!
-use forum::InputValidationLengthConstraint;
+use common::constraints::InputValidationLengthConstraint;
 
 /// The way a map (linked_map) is represented in the GenesisConfig produced by decl_storage
 //pub type GenesisConfigMap<K, V> = std::vec::Vec<(K, V)>;

+ 30 - 26
runtime-modules/content-working-group/src/lib.rs

@@ -1179,17 +1179,17 @@ decl_module! {
             let new_channel = Channel {
                 verified: false,
                 handle: handle.clone(),
-                title: title,
-                description: description,
-                avatar: avatar,
-                banner: banner,
-                content: content,
-                owner: owner,
-                role_account: role_account,
-                publication_status: publication_status,
+                title,
+                description,
+                avatar,
+                banner,
+                content,
+                owner,
+                role_account,
+                publication_status,
                 curation_status: ChannelCurationStatus::Normal,
                 created: <system::Module<T>>::block_number(),
-                principal_id: principal_id
+                principal_id,
             };
 
             // Add channel to ChannelById under id
@@ -1387,9 +1387,9 @@ decl_module! {
 
             // Create and add curator opening.
             let new_opening_by_id = CuratorOpening {
-                opening_id : opening_id,
+                opening_id,
                 curator_applications: BTreeSet::new(),
-                policy_commitment: policy_commitment
+                policy_commitment,
             };
 
             CuratorOpeningById::<T>::insert(new_curator_opening_id, new_opening_by_id);
@@ -1466,6 +1466,23 @@ decl_module! {
             // Ensure curator opening exists
             let (curator_opening, _) = Self::ensure_curator_opening_exists(&curator_opening_id)?;
 
+            // Ensure a mint exists if lead is providing a reward for positions being filled
+            let create_reward_settings = if let Some(policy) = reward_policy {
+                // A reward will need to be created so ensure our configured mint exists
+                let mint_id = Self::mint();
+
+                // Technically this is a bug-check and should not be here.
+                ensure!(<minting::Mints<T>>::exists(mint_id), MSG_FILL_CURATOR_OPENING_MINT_DOES_NOT_EXIST);
+
+                // Make sure valid parameters are selected for next payment at block number
+                ensure!(policy.next_payment_at_block > <system::Module<T>>::block_number(), MSG_FILL_CURATOR_OPENING_INVALID_NEXT_PAYMENT_BLOCK);
+
+                // The verified reward settings to use
+                Some((mint_id, policy))
+            } else {
+                None
+            };
+
             // Make iterator over successful curator application
             let successful_iter = successful_curator_application_ids
                                     .iter()
@@ -1516,21 +1533,6 @@ decl_module! {
                 )
             )?;
 
-            let create_reward_settings = if let Some(policy) = reward_policy {
-                // A reward will need to be created so ensure our configured mint exists
-                let mint_id = Self::mint();
-
-                ensure!(<minting::Mints<T>>::exists(mint_id), MSG_FILL_CURATOR_OPENING_MINT_DOES_NOT_EXIST);
-
-                // Make sure valid parameters are selected for next payment at block number
-                ensure!(policy.next_payment_at_block > <system::Module<T>>::block_number(), MSG_FILL_CURATOR_OPENING_INVALID_NEXT_PAYMENT_BLOCK);
-
-                // The verified reward settings to use
-                Some((mint_id, policy))
-            } else {
-                None
-            };
-
             //
             // == MUTATION SAFE ==
             //
@@ -1979,6 +1981,8 @@ decl_module! {
         ) {
             ensure_root(origin)?;
 
+            ensure!(<Mint<T>>::exists(), MSG_FILL_CURATOR_OPENING_MINT_DOES_NOT_EXIST);
+
             let mint_id = Self::mint();
 
             // Mint must exist - it is set at genesis

+ 1 - 3
runtime-modules/content-working-group/src/tests.rs

@@ -9,9 +9,7 @@ use rstd::collections::btree_set::BTreeSet;
 use sr_primitives::traits::One;
 use srml_support::{assert_err, assert_ok, StorageLinkedMap, StorageValue};
 
-/// DIRTY IMPORT BECAUSE
-/// InputValidationLengthConstraint has not been factored out yet!!!
-use forum::InputValidationLengthConstraint;
+use common::constraints::InputValidationLengthConstraint;
 
 #[test]
 fn create_channel_success() {

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

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-forum-module'
-version = '1.2.1'
+version = '1.2.2'
 authors = ['Joystream contributors']
 edition = '2018'
 

+ 1 - 1
runtime-modules/forum/src/lib.rs

@@ -19,7 +19,7 @@ use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Para
 mod mock;
 mod tests;
 
-pub use common::constraints::InputValidationLengthConstraint;
+use common::constraints::InputValidationLengthConstraint;
 use common::BlockAndTime;
 
 /// Constants

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

@@ -154,7 +154,7 @@ decl_module! {
                         hiring::ActiveOpeningStage::Deactivated {
                             cause: hiring::OpeningDeactivationCause::ReviewPeriodExpired,
                             deactivated_at_block: now,
-                            started_accepting_applicants_at_block: started_accepting_applicants_at_block,
+                            started_accepting_applicants_at_block,
                             started_review_period_at_block: Some(started_review_period_at_block),
                     });
 

+ 49 - 0
runtime-modules/proposals/codex/src/proposal_types/mod.rs

@@ -46,8 +46,18 @@ pub enum ProposalDetails<MintedBalance, CurrencyBalance, BlockNumber, AccountId,
     /// Balance for the `set content working group mint capacity` proposal
     SetContentWorkingGroupMintCapacity(MintedBalance),
 
+    /// ********** Deprecated during the Nicaea release.
+    /// It is kept only for backward compatibility in the Pioneer. **********
+    /// AccountId for the `evict storage provider` proposal
+    EvictStorageProvider(AccountId),
+
     /// Validator count for the `set validator count` proposal
     SetValidatorCount(u32),
+
+    /// ********** Deprecated during the Nicaea release.
+    /// It is kept only for backward compatibility in the Pioneer. **********
+    /// Role parameters for the `set storage role parameters` proposal
+    SetStorageRoleParameters(RoleParameters<CurrencyBalance, BlockNumber>),
 }
 
 impl<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId> Default
@@ -58,6 +68,45 @@ impl<MintedBalance, CurrencyBalance, BlockNumber, AccountId, MemberId> Default
     }
 }
 
+/// ********** Deprecated during the Nicaea release.
+/// It is kept only for backward compatibility in the Pioneer. **********
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Encode, Decode, Copy, Clone, Eq, PartialEq, Debug)]
+pub struct RoleParameters<Balance, BlockNumber> {
+    /// Minimum balance required to stake to enter a role.
+    pub min_stake: Balance,
+
+    /// Minimum actors to maintain - if role is unstaking
+    /// and remaining actors would be less that this value - prevent or punish for unstaking.
+    pub min_actors: u32,
+
+    /// The maximum number of spots available to fill for a role.
+    pub max_actors: u32,
+
+    /// Fixed amount of tokens paid to actors' primary account.
+    pub reward: Balance,
+
+    /// Payouts are made at this block interval.
+    pub reward_period: BlockNumber,
+
+    /// Minimum amount of time before being able to unstake.
+    pub bonding_period: BlockNumber,
+
+    /// How long tokens remain locked for after unstaking.
+    pub unbonding_period: BlockNumber,
+
+    /// Minimum period required to be in service. unbonding before this time is highly penalized
+    pub min_service_period: BlockNumber,
+
+    /// "Startup" time allowed for roles that need to sync their infrastructure
+    /// with other providers before they are considered in service and punishable for
+    /// not delivering required level of service.
+    pub startup_grace_period: BlockNumber,
+
+    /// Small fee burned to make a request to enter role.
+    pub entry_request_fee: Balance,
+}
+
 /// Contains proposal config parameters. Default values are used by migration and genesis config.
 pub struct ProposalsConfigParameters {
     /// 'Set validator count' proposal voting period

+ 6 - 3
runtime-modules/service-discovery/src/lib.rs

@@ -48,8 +48,11 @@ pub type IPNSIdentity = Vec<u8>;
 /// HTTP Url string to a discovery service endpoint
 pub type Url = Vec<u8>;
 
-// Alias for storage working group
-pub(crate) type StorageWorkingGroup<T> = working_group::Module<T, working_group::Instance2>;
+// The storage working group instance alias.
+pub(crate) type StorageWorkingGroupInstance = working_group::Instance2;
+
+// Alias for storage working group.
+pub(crate) type StorageWorkingGroup<T> = working_group::Module<T, StorageWorkingGroupInstance>;
 
 /// Storage provider is a worker from the  working_group module.
 pub type StorageProviderId<T> = working_group::WorkerId<T>;
@@ -68,7 +71,7 @@ pub struct AccountInfo<BlockNumber> {
 }
 
 /// The _Service discovery_ main _Trait_.
-pub trait Trait: system::Trait + working_group::Trait<working_group::Instance2> {
+pub trait Trait: system::Trait + working_group::Trait<StorageWorkingGroupInstance> {
     /// _Service discovery_ event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 }

+ 7 - 4
runtime-modules/service-discovery/src/mock.rs

@@ -11,9 +11,12 @@ pub use sr_primitives::{
 
 use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
 
+// The storage working group instance alias.
+pub type StorageWorkingGroupInstance = working_group::Instance2;
+
 mod working_group_mod {
+    pub use super::StorageWorkingGroupInstance;
     pub use working_group::Event;
-    pub use working_group::Instance2;
     pub use working_group::Trait;
 }
 
@@ -34,7 +37,7 @@ impl_outer_event! {
         discovery<T>,
         balances<T>,
         membership_mod<T>,
-         working_group_mod Instance2 <T>,
+         working_group_mod StorageWorkingGroupInstance <T>,
     }
 }
 
@@ -127,7 +130,7 @@ impl recurringrewards::Trait for Test {
     type RewardRelationshipId = u64;
 }
 
-impl working_group::Trait<working_group::Instance2> for Test {
+impl working_group::Trait<StorageWorkingGroupInstance> for Test {
     type Event = MetaEvent;
 }
 
@@ -160,7 +163,7 @@ pub(crate) fn hire_storage_provider() -> (u64, u64) {
         role_stake_profile: None,
     };
 
-    <working_group::WorkerById<Test, working_group::Instance2>>::insert(
+    <working_group::WorkerById<Test, StorageWorkingGroupInstance>>::insert(
         storage_provider_id,
         storage_provider,
     );

+ 5 - 6
runtime-modules/storage/Cargo.toml

@@ -17,7 +17,6 @@ std = [
 	'primitives/std',
 	'common/std',
 	'membership/std',
-	'minting/std',
 	'working-group/std',
 ]
 
@@ -74,11 +73,6 @@ default_features = false
 package = 'substrate-membership-module'
 path = '../membership'
 
-[dependencies.minting]
-default_features = false
-package = 'substrate-token-mint-module'
-path = '../token-minting'
-
 [dependencies.common]
 default_features = false
 package = 'substrate-common-module'
@@ -115,3 +109,8 @@ path = '../hiring'
 default_features = false
 package = 'substrate-stake-module'
 path = '../stake'
+
+[dev-dependencies.minting]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../token-minting'

+ 2 - 2
runtime-modules/storage/src/data_directory.rs

@@ -32,7 +32,7 @@ pub(crate) use common::BlockAndTime;
 
 use crate::data_object_type_registry;
 use crate::data_object_type_registry::IsActiveDataObjectType;
-use crate::{MemberId, StorageProviderId, StorageWorkingGroup};
+use crate::{MemberId, StorageProviderId, StorageWorkingGroup, StorageWorkingGroupInstance};
 
 /// The _Data directory_ main _Trait_.
 pub trait Trait:
@@ -40,7 +40,7 @@ pub trait Trait:
     + system::Trait
     + data_object_type_registry::Trait
     + membership::members::Trait
-    + working_group::Trait<working_group::Instance2>
+    + working_group::Trait<StorageWorkingGroupInstance>
 {
     /// _Data directory_ event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;

+ 3 - 35
runtime-modules/storage/src/data_object_storage_registry.rs

@@ -25,14 +25,10 @@
 use codec::{Codec, Decode, Encode};
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
-use srml_support::{
-    decl_error, decl_event, decl_module, decl_storage, ensure, print, Parameter, StorageValue,
-};
-
-use common::constraints::InputValidationLengthConstraint;
+use srml_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
 
 use crate::data_directory::{self, ContentIdExists};
-use crate::{StorageProviderId, StorageWorkingGroup};
+use crate::{StorageProviderId, StorageWorkingGroup, StorageWorkingGroupInstance};
 
 const DEFAULT_FIRST_RELATIONSHIP_ID: u32 = 1;
 
@@ -41,7 +37,7 @@ pub trait Trait:
     timestamp::Trait
     + system::Trait
     + data_directory::Trait
-    + working_group::Trait<working_group::Instance2>
+    + working_group::Trait<StorageWorkingGroupInstance>
 {
     /// _Data object storage registry_ event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
@@ -125,34 +121,6 @@ decl_storage! {
         /// Keeps a list of storage relationships per content id.
         pub RelationshipsByContentId get(relationships_by_content_id): map T::ContentId => Vec<T::DataObjectStorageRelationshipId>;
     }
-    add_extra_genesis {
-        config(storage_working_group_mint_capacity): minting::BalanceOf<T>;
-        config(opening_human_readable_text_constraint): InputValidationLengthConstraint;
-        config(worker_application_human_readable_text_constraint): InputValidationLengthConstraint;
-        config(worker_exit_rationale_text_constraint): InputValidationLengthConstraint;
-        build(|config: &GenesisConfig<T>| {
-            // Create a mint.
-            let mint_id_result =
-                <minting::Module<T>>::add_mint(config.storage_working_group_mint_capacity, None);
-
-            if let Ok(mint_id) = mint_id_result {
-                <working_group::Mint::<T, working_group::Instance2>>::put(mint_id);
-            } else {
-                print("Failed to create a mint for the storage working group");
-            }
-
-            // Create constraints
-            <working_group::OpeningHumanReadableText::<working_group::Instance2>>::put(
-                config.opening_human_readable_text_constraint
-            );
-            <working_group::WorkerApplicationHumanReadableText::<working_group::Instance2>>::put(
-                config.worker_application_human_readable_text_constraint
-            );
-            <working_group::WorkerExitRationaleText::<working_group::Instance2>>::put(
-                config.worker_exit_rationale_text_constraint
-            );
-        });
-    }
 }
 
 decl_event! {

+ 2 - 2
runtime-modules/storage/src/data_object_type_registry.rs

@@ -22,7 +22,7 @@
 // Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
 //#![warn(missing_docs)]
 
-use crate::StorageWorkingGroup;
+use crate::{StorageWorkingGroup, StorageWorkingGroupInstance};
 use codec::{Codec, Decode, Encode};
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
@@ -32,7 +32,7 @@ const DEFAULT_TYPE_DESCRIPTION: &str = "Default data object type for audio and v
 const DEFAULT_FIRST_DATA_OBJECT_TYPE_ID: u32 = 1;
 
 /// The _Data object type registry_ main _Trait_.
-pub trait Trait: system::Trait + working_group::Trait<working_group::Instance2> {
+pub trait Trait: system::Trait + working_group::Trait<StorageWorkingGroupInstance> {
     /// _Data object type registry_ event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 

+ 4 - 1
runtime-modules/storage/src/lib.rs

@@ -7,8 +7,11 @@ pub mod data_object_type_registry;
 
 mod tests;
 
+// The storage working group instance alias.
+pub type StorageWorkingGroupInstance = working_group::Instance2;
+
 // Alias for storage working group
-pub(crate) type StorageWorkingGroup<T> = working_group::Module<T, working_group::Instance2>;
+pub(crate) type StorageWorkingGroup<T> = working_group::Module<T, StorageWorkingGroupInstance>;
 
 // Alias for the member id.
 pub(crate) type MemberId<T> = <T as membership::members::Trait>::MemberId;

+ 0 - 31
runtime-modules/storage/src/tests/data_object_storage_registry.rs

@@ -1,8 +1,6 @@
 #![cfg(test)]
 
 use super::mock::*;
-use crate::StorageWorkingGroup;
-use srml_support::StorageLinkedMap;
 
 #[test]
 fn initial_state() {
@@ -157,32 +155,3 @@ fn test_toggle_ready() {
         );
     });
 }
-
-#[test]
-fn ensure_setting_genesis_storage_working_group_mint_succeeds() {
-    with_default_mock_builder(|| {
-        let mint_id = <StorageWorkingGroup<Test>>::mint();
-
-        assert!(minting::Mints::<Test>::exists(mint_id));
-
-        let mint = <minting::Module<Test>>::mints(mint_id);
-        assert_eq!(mint.capacity(), STORAGE_WORKING_GROUP_MINT_CAPACITY);
-    });
-}
-
-#[test]
-fn ensure_setting_genesis_constraints_succeeds() {
-    with_default_mock_builder(|| {
-        let default_constraint = common::constraints::InputValidationLengthConstraint::new(
-            STORAGE_WORKING_GROUP_CONSTRAINT_MIN,
-            STORAGE_WORKING_GROUP_CONSTRAINT_DIFF,
-        );
-        let opening_text_constraint = <StorageWorkingGroup<Test>>::opening_human_readable_text();
-        let worker_text_constraint = <StorageWorkingGroup<Test>>::application_human_readable_text();
-        let worker_exit_text_constraint = <StorageWorkingGroup<Test>>::worker_exit_rationale_text();
-
-        assert_eq!(opening_text_constraint, default_constraint);
-        assert_eq!(worker_text_constraint, default_constraint);
-        assert_eq!(worker_exit_text_constraint, default_constraint);
-    });
-}

+ 5 - 24
runtime-modules/storage/src/tests/mock.rs

@@ -1,7 +1,6 @@
 #![cfg(test)]
 
 pub use crate::{data_directory, data_object_storage_registry, data_object_type_registry};
-use common::constraints::InputValidationLengthConstraint;
 pub use common::currency::GovernanceCurrency;
 use membership::members;
 pub use system;
@@ -16,11 +15,12 @@ pub use sr_primitives::{
 
 use crate::data_directory::ContentIdExists;
 use crate::data_object_type_registry::IsActiveDataObjectType;
+pub use crate::StorageWorkingGroupInstance;
 use srml_support::{impl_outer_event, impl_outer_origin, parameter_types, StorageLinkedMap};
 
 mod working_group_mod {
+    pub use super::StorageWorkingGroupInstance;
     pub use working_group::Event;
-    pub use working_group::Instance2;
 }
 
 impl_outer_origin! {
@@ -34,7 +34,7 @@ impl_outer_event! {
         data_object_storage_registry<T>,
         balances<T>,
         members<T>,
-        working_group_mod Instance2 <T>,
+        working_group_mod StorageWorkingGroupInstance <T>,
     }
 }
 
@@ -146,7 +146,7 @@ impl GovernanceCurrency for Test {
     type Currency = balances::Module<Self>;
 }
 
-impl working_group::Trait<working_group::Instance2> for Test {
+impl working_group::Trait<StorageWorkingGroupInstance> for Test {
     type Event = MetaEvent;
 }
 
@@ -220,16 +220,11 @@ impl hiring::Trait for Test {
 
 pub struct ExtBuilder {
     first_data_object_type_id: u64,
-    storage_working_group_mint_capacity: u64,
     first_content_id: u64,
     first_relationship_id: u64,
     first_metadata_id: u64,
 }
 
-pub(crate) const STORAGE_WORKING_GROUP_MINT_CAPACITY: u64 = 40000;
-pub(crate) const STORAGE_WORKING_GROUP_CONSTRAINT_MIN: u16 = 1;
-pub(crate) const STORAGE_WORKING_GROUP_CONSTRAINT_DIFF: u16 = 40;
-
 impl Default for ExtBuilder {
     fn default() -> Self {
         Self {
@@ -237,7 +232,6 @@ impl Default for ExtBuilder {
             first_content_id: 2,
             first_relationship_id: 3,
             first_metadata_id: 4,
-            storage_working_group_mint_capacity: STORAGE_WORKING_GROUP_MINT_CAPACITY,
         }
     }
 }
@@ -272,19 +266,6 @@ impl ExtBuilder {
 
         data_object_storage_registry::GenesisConfig::<Test> {
             first_relationship_id: self.first_relationship_id,
-            storage_working_group_mint_capacity: self.storage_working_group_mint_capacity,
-            opening_human_readable_text_constraint: InputValidationLengthConstraint::new(
-                STORAGE_WORKING_GROUP_CONSTRAINT_MIN,
-                STORAGE_WORKING_GROUP_CONSTRAINT_DIFF,
-            ),
-            worker_application_human_readable_text_constraint: InputValidationLengthConstraint::new(
-                STORAGE_WORKING_GROUP_CONSTRAINT_MIN,
-                STORAGE_WORKING_GROUP_CONSTRAINT_DIFF,
-            ),
-            worker_exit_rationale_text_constraint: InputValidationLengthConstraint::new(
-                STORAGE_WORKING_GROUP_CONSTRAINT_MIN,
-                STORAGE_WORKING_GROUP_CONSTRAINT_DIFF,
-            ),
         }
         .assimilate_storage(&mut t)
         .unwrap();
@@ -328,7 +309,7 @@ pub(crate) fn hire_storage_provider() -> (u64, u32) {
         role_stake_profile: None,
     };
 
-    <working_group::WorkerById<Test, working_group::Instance2>>::insert(
+    <working_group::WorkerById<Test, StorageWorkingGroupInstance>>::insert(
         storage_provider_id,
         storage_provider,
     );

+ 0 - 3
runtime-modules/working-group/src/errors.rs

@@ -230,9 +230,6 @@ decl_error! {
         /// Slash amount should be greater than zero.
         StakingErrorSlashAmountShouldBeGreaterThanZero,
 
-        /// Working group mint is not set.
-        WorkingGroupMintIsNotSet,
-
         /// Cannot find mint in the minting module.
         CannotFindMint,
 

+ 56 - 18
runtime-modules/working-group/src/lib.rs

@@ -310,6 +310,20 @@ decl_storage! {
         /// Worker exit rationale text length limits.
         pub WorkerExitRationaleText get(worker_exit_rationale_text) : InputValidationLengthConstraint;
     }
+        add_extra_genesis {
+        config(phantom): rstd::marker::PhantomData<I>;
+        config(storage_working_group_mint_capacity): minting::BalanceOf<T>;
+        config(opening_human_readable_text_constraint): InputValidationLengthConstraint;
+        config(worker_application_human_readable_text_constraint): InputValidationLengthConstraint;
+        config(worker_exit_rationale_text_constraint): InputValidationLengthConstraint;
+        build(|config: &GenesisConfig<T, I>| {
+            Module::<T, I>::initialize_working_group(
+                config.opening_human_readable_text_constraint,
+                config.worker_application_human_readable_text_constraint,
+                config.worker_exit_rationale_text_constraint,
+                config.storage_working_group_mint_capacity)
+        });
+    }
 }
 
 decl_module! {
@@ -767,6 +781,24 @@ decl_module! {
 
             Self::ensure_origin_for_opening_type(origin, opening.opening_type)?;
 
+            // Ensure a mint exists if lead is providing a reward for positions being filled
+            let create_reward_settings = if let Some(policy) = reward_policy {
+                // A reward will need to be created so ensure our configured mint exists
+                let mint_id = Self::mint();
+
+                // Technically this is a bug-check and should not be here.
+                ensure!(<minting::Mints<T>>::exists(mint_id), Error::FillOpeningMintDoesNotExist);
+
+                // Make sure valid parameters are selected for next payment at block number
+                ensure!(policy.next_payment_at_block > <system::Module<T>>::block_number(),
+                    Error::FillOpeningInvalidNextPaymentBlock);
+
+                // The verified reward settings to use
+                Some((mint_id, policy))
+            } else {
+                None
+            };
+
             // Make iterator over successful worker application
             let successful_iter = successful_application_ids
                                     .iter()
@@ -805,22 +837,6 @@ decl_module! {
                 )
             )?;
 
-            let create_reward_settings = if let Some(policy) = reward_policy {
-                // A reward will need to be created so ensure our configured mint exists
-                let mint_id = Self::mint();
-
-                ensure!(<minting::Mints<T>>::exists(mint_id), Error::FillOpeningMintDoesNotExist);
-
-                // Make sure valid parameters are selected for next payment at block number
-                ensure!(policy.next_payment_at_block > <system::Module<T>>::block_number(),
-                    Error::FillOpeningInvalidNextPaymentBlock);
-
-                // The verified reward settings to use
-                Some((mint_id, policy))
-            } else {
-                None
-            };
-
             //
             // == MUTATION SAFE ==
             //
@@ -987,10 +1003,9 @@ decl_module! {
         ) {
             ensure_root(origin)?;
 
-            ensure!(<Mint<T, I>>::exists(), Error::WorkingGroupMintIsNotSet);
-
             let mint_id = Self::mint();
 
+            // Technically this is a bug-check and should not be here.
             ensure!(<minting::Mints<T>>::exists(mint_id), Error::CannotFindMint);
 
             // Mint must exist - it is set at genesis or migration.
@@ -1299,6 +1314,29 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
             )
             .map_err(|e| e.into())
     }
+
+    fn initialize_working_group(
+        opening_human_readable_text_constraint: InputValidationLengthConstraint,
+        worker_application_human_readable_text_constraint: InputValidationLengthConstraint,
+        worker_exit_rationale_text_constraint: InputValidationLengthConstraint,
+        working_group_mint_capacity: minting::BalanceOf<T>,
+    ) {
+        // Create a mint.
+        let mint_id_result = <minting::Module<T>>::add_mint(working_group_mint_capacity, None);
+
+        if let Ok(mint_id) = mint_id_result {
+            <Mint<T, I>>::put(mint_id);
+        } else {
+            panic!("Failed to create a mint for the working group");
+        }
+
+        // Create constraints
+        <OpeningHumanReadableText<I>>::put(opening_human_readable_text_constraint);
+        <WorkerApplicationHumanReadableText<I>>::put(
+            worker_application_human_readable_text_constraint,
+        );
+        <WorkerExitRationaleText<I>>::put(worker_exit_rationale_text_constraint);
+    }
 }
 
 /// Creates default text constraint.

Some files were not shown because too many files changed in this diff