Browse Source

merged nicaea

Gleb Urvanov 4 years ago
parent
commit
f64ec6882b
100 changed files with 2568 additions and 1395 deletions
  1. 20 7
      Cargo.lock
  2. 5 1
      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. 28 8
      cli/src/base/AccountsCommandBase.ts
  7. 78 0
      cli/src/base/WorkingGroupsCommandBase.ts
  8. 10 2
      cli/src/commands/account/choose.ts
  9. 1 1
      cli/src/commands/council/info.ts
  10. 38 0
      cli/src/commands/working-groups/overview.ts
  11. 14 1
      cli/src/helpers/display.ts
  12. 2 1
      cli/tsconfig.json
  13. 13 21
      pioneer/packages/apps-routing/src/index.ts
  14. 1 1
      pioneer/packages/apps/src/SideBar/Item.tsx
  15. 1 1
      pioneer/packages/joy-election/src/Applicant.tsx
  16. 1 1
      pioneer/packages/joy-election/src/ApplyForm.tsx
  17. 1 1
      pioneer/packages/joy-election/src/Council.tsx
  18. 1 1
      pioneer/packages/joy-election/src/Dashboard.tsx
  19. 1 1
      pioneer/packages/joy-election/src/SealedVote.tsx
  20. 1 1
      pioneer/packages/joy-election/src/index.tsx
  21. 2 1
      pioneer/packages/joy-forum/src/CategoryList.tsx
  22. 4 3
      pioneer/packages/joy-forum/src/Context.tsx
  23. 2 1
      pioneer/packages/joy-forum/src/EditReply.tsx
  24. 2 1
      pioneer/packages/joy-forum/src/EditThread.tsx
  25. 2 1
      pioneer/packages/joy-forum/src/Moderate.tsx
  26. 2 1
      pioneer/packages/joy-forum/src/ViewThread.tsx
  27. 2 1
      pioneer/packages/joy-forum/src/utils.tsx
  28. 1 1
      pioneer/packages/joy-forum/src/validation.tsx
  29. 5 5
      pioneer/packages/joy-media/src/DiscoveryProvider.tsx
  30. 4 4
      pioneer/packages/joy-media/src/Upload.tsx
  31. 24 4
      pioneer/packages/joy-media/src/common/MediaPlayerWithResolver.tsx
  32. 1 1
      pioneer/packages/joy-media/src/transport.substrate.ts
  33. 1 1
      pioneer/packages/joy-media/src/upload/UploadVideo.tsx
  34. 1 1
      pioneer/packages/joy-members/src/Details.tsx
  35. 1 1
      pioneer/packages/joy-members/src/EditForm.tsx
  36. 1 1
      pioneer/packages/joy-members/src/MemberPreview.tsx
  37. 7 3
      pioneer/packages/joy-proposals/src/Proposal/Body.tsx
  38. 4 4
      pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx
  39. 1 1
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx
  40. 5 7
      pioneer/packages/joy-proposals/src/Proposal/Votes.tsx
  41. 1 1
      pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx
  42. 1 1
      pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx
  43. 0 2
      pioneer/packages/joy-proposals/src/validationSchema.ts
  44. 4 0
      pioneer/packages/joy-roles/src/flows/apply.controller.tsx
  45. 1 0
      pioneer/packages/joy-roles/src/index.tsx
  46. 4 1
      pioneer/packages/joy-roles/src/tabs/Opportunities.controller.tsx
  47. 49 4
      pioneer/packages/joy-roles/src/tabs/Opportunities.tsx
  48. 9 3
      pioneer/packages/joy-roles/src/tabs/Opportunity.controller.tsx
  49. 1 1
      pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx
  50. 7 3
      pioneer/packages/joy-roles/src/transport.mock.ts
  51. 87 26
      pioneer/packages/joy-roles/src/transport.substrate.ts
  52. 3 1
      pioneer/packages/joy-roles/src/transport.ts
  53. 6 0
      pioneer/packages/joy-roles/src/working_groups.ts
  54. 2 1
      pioneer/packages/joy-storage/src/AvailableRoles/index.tsx
  55. 2 1
      pioneer/packages/joy-storage/src/MyRequests/index.tsx
  56. 2 1
      pioneer/packages/joy-storage/src/index.tsx
  57. 2 1
      pioneer/packages/joy-storage/src/props.ts
  58. 1 1
      pioneer/packages/joy-utils/src/index.ts
  59. 3 3
      pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx
  60. 14 2
      pioneer/packages/joy-utils/src/transport/council.ts
  61. 1 1
      pioneer/packages/joy-utils/src/transport/index.ts
  62. 4 0
      pioneer/packages/joy-utils/src/transport/members.ts
  63. 31 11
      pioneer/packages/joy-utils/src/transport/proposals.ts
  64. 6 1
      pioneer/packages/joy-utils/src/types/proposals.ts
  65. 19 10
      runtime-modules/bureaucracy/src/lib.rs
  66. 9 3
      runtime-modules/bureaucracy/src/tests/fixtures.rs
  67. 35 14
      runtime-modules/bureaucracy/src/tests/mod.rs
  68. 9 2
      runtime-modules/common/Cargo.toml
  69. 26 0
      runtime-modules/common/src/lib.rs
  70. 1 1
      runtime-modules/common/src/origin_validator.rs
  71. 1 1
      runtime-modules/forum/Cargo.toml
  72. 12 26
      runtime-modules/forum/src/lib.rs
  73. 3 4
      runtime-modules/forum/src/mock.rs
  74. 47 5
      runtime-modules/service-discovery/Cargo.toml
  75. 0 127
      runtime-modules/service-discovery/src/discovery.rs
  76. 177 2
      runtime-modules/service-discovery/src/lib.rs
  77. 100 21
      runtime-modules/service-discovery/src/mock.rs
  78. 69 34
      runtime-modules/service-discovery/src/tests.rs
  79. 26 6
      runtime-modules/storage/Cargo.toml
  80. 219 208
      runtime-modules/storage/src/data_directory.rs
  81. 148 187
      runtime-modules/storage/src/data_object_storage_registry.rs
  82. 127 205
      runtime-modules/storage/src/data_object_type_registry.rs
  83. 10 2
      runtime-modules/storage/src/lib.rs
  84. 0 206
      runtime-modules/storage/src/tests.rs
  85. 171 0
      runtime-modules/storage/src/tests/data_directory.rs
  86. 157 0
      runtime-modules/storage/src/tests/data_object_storage_registry.rs
  87. 285 0
      runtime-modules/storage/src/tests/data_object_type_registry.rs
  88. 79 52
      runtime-modules/storage/src/tests/mock.rs
  89. 6 0
      runtime-modules/storage/src/tests/mod.rs
  90. 0 20
      runtime-modules/storage/src/traits.rs
  91. 3 3
      runtime/Cargo.toml
  92. 1 0
      runtime/src/integration/mod.rs
  93. 36 0
      runtime/src/integration/storage.rs
  94. 25 75
      runtime/src/lib.rs
  95. 0 5
      runtime/src/test/mod.rs
  96. 14 0
      runtime/src/tests/mod.rs
  97. 2 8
      runtime/src/tests/proposals_integration.rs
  98. 43 0
      runtime/src/tests/storage_integration.rs
  99. 1 1
      tests/network-tests/src/constantinople/tests/impl/electingCouncil.ts
  100. 1 1
      tests/network-tests/src/constantinople/utils/apiWrapper.ts

+ 20 - 7
Cargo.lock

@@ -1614,7 +1614,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node-runtime"
-version = "6.15.0"
+version = "6.16.0"
 dependencies = [
  "parity-scale-codec",
  "safe-mix",
@@ -4602,13 +4602,14 @@ dependencies = [
 
 [[package]]
 name = "substrate-common-module"
-version = "1.0.0"
+version = "1.1.0"
 dependencies = [
  "parity-scale-codec",
  "serde",
  "sr-primitives",
  "srml-support",
  "srml-system",
+ "srml-timestamp",
 ]
 
 [[package]]
@@ -4828,7 +4829,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-forum-module"
-version = "1.2.0"
+version = "1.2.1"
 dependencies = [
  "hex-literal 0.1.4",
  "parity-scale-codec",
@@ -5367,17 +5368,25 @@ dependencies = [
 
 [[package]]
 name = "substrate-service-discovery-module"
-version = "1.0.0"
+version = "2.0.0"
 dependencies = [
  "parity-scale-codec",
  "serde",
  "sr-io",
  "sr-primitives",
  "sr-std",
+ "srml-balances",
  "srml-support",
  "srml-system",
+ "srml-timestamp",
+ "substrate-bureaucracy-module",
+ "substrate-common-module",
+ "substrate-hiring-module",
+ "substrate-membership-module",
  "substrate-primitives",
- "substrate-roles-module",
+ "substrate-recurring-reward-module",
+ "substrate-stake-module",
+ "substrate-token-mint-module",
 ]
 
 [[package]]
@@ -5443,7 +5452,7 @@ dependencies = [
 
 [[package]]
 name = "substrate-storage-module"
-version = "1.0.0"
+version = "2.0.0"
 dependencies = [
  "parity-scale-codec",
  "serde",
@@ -5454,10 +5463,14 @@ dependencies = [
  "srml-support",
  "srml-system",
  "srml-timestamp",
+ "substrate-bureaucracy-module",
  "substrate-common-module",
+ "substrate-hiring-module",
  "substrate-membership-module",
  "substrate-primitives",
- "substrate-roles-module",
+ "substrate-recurring-reward-module",
+ "substrate-stake-module",
+ "substrate-token-mint-module",
 ]
 
 [[package]]

+ 5 - 1
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",
@@ -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;
+}

+ 28 - 8
cli/src/base/AccountsCommandBase.ts

@@ -12,6 +12,7 @@ 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.
@@ -25,12 +26,12 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
         return path.join(this.config.dataDir, 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();
         }

+ 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/**/*"

+ 13 - 21
pioneer/packages/apps-routing/src/index.ts

@@ -39,46 +39,38 @@ 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,
+  storageRoles,
   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
 };
 

+ 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';

+ 4 - 3
pioneer/packages/joy-forum/src/Context.tsx

@@ -2,7 +2,8 @@
 // NOTE: The purpose of this context is to immitate a Substrate storage for the forum until it's implemented as a substrate runtime module.
 
 import React, { useReducer, createContext, useContext } from 'react';
-import { Category, Thread, Reply, ModerationAction, BlockchainTimestamp } from '@joystream/types/forum';
+import { Category, Thread, Reply, ModerationAction } from '@joystream/types/forum';
+import { BlockAndTime } from '@joystream/types/common';
 import { Option, Text, GenericAccountId } from '@polkadot/types';
 
 type CategoryId = number;
@@ -220,7 +221,7 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
 
       const thread = threadById.get(id) as Thread;
       const moderation = new ModerationAction({
-        moderated_at: BlockchainTimestamp.newEmpty(),
+        moderated_at: BlockAndTime.newEmpty(),
         moderator_id: new GenericAccountId(moderator),
         rationale: new Text(rationale)
       });
@@ -285,7 +286,7 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
 
       const reply = replyById.get(id) as Reply;
       const moderation = new ModerationAction({
-        moderated_at: BlockchainTimestamp.newEmpty(),
+        moderated_at: BlockAndTime.newEmpty(),
         moderator_id: new GenericAccountId(moderator),
         rationale: new Text(rationale)
       });

+ 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!')

+ 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">

+ 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))} />

+ 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} />
   );

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

@@ -33,7 +33,7 @@ export const ContentCurators = Loadable<WorkingGroupMembership>(
           <p>
             There are openings for new content curators. This is a great way to support Joystream!
           </p>
-          <Link to="/working-groups/opportunities">
+          <Link to="/working-groups/opportunities/curators">
             <Button icon labelPosition="right" color="green" positive>
               Find out more
               <Icon name={'right arrow' as SemanticICONS} />

+ 7 - 3
pioneer/packages/joy-roles/src/transport.mock.ts

@@ -5,8 +5,8 @@ import { Option, Text, 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 { IProfile, MemberId, Role } from '@joystream/types/members';
+import { Actor } from '@joystream/types/roles';
 import {
   Opening,
   AcceptingApplications,
@@ -14,7 +14,6 @@ import {
   ApplicationRationingPolicy,
   StakingPolicy
 } from '@joystream/types/hiring';
-import { IProfile, MemberId } from '@joystream/types/members';
 
 import { WorkingGroupMembership, StorageAndDistributionMembership, GroupLeadStatus } from './tabs/WorkingGroup';
 import { CuratorId } from '@joystream/types/content-working-group';
@@ -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> {
@@ -301,6 +301,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++) {

+ 87 - 26
pioneer/packages/joy-roles/src/transport.substrate.ts

@@ -3,7 +3,7 @@ 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 { Moment } from '@polkadot/types/interfaces/runtime';
 import { QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
@@ -14,7 +14,6 @@ import { APIQueryCache, MultipleLinkedMapEntry, SingleLinkedMapEntry, Subscribab
 import { ITransport } from './transport';
 import { GroupMember } from './elements';
 
-import { Role } from '@joystream/types/roles';
 import {
   Curator, CuratorId,
   CuratorApplication, CuratorApplicationId,
@@ -24,10 +23,14 @@ import {
   Lead, LeadId
 } from '@joystream/types/content-working-group';
 
-import { Application, Opening, OpeningId } from '@joystream/types/hiring';
+import {
+  WorkerApplication, WorkerApplicationId, WorkerOpening, WorkerOpeningId
+} from '@joystream/types/bureaucracy';
+
+import { Application, Opening } 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 { 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';
@@ -42,7 +45,7 @@ import {
   classifyOpeningStakes,
   isApplicationHired
 } from './classifiers';
-import { WorkingGroups } from './working_groups';
+import { WorkingGroups, AvailableGroups } from './working_groups';
 import { Sort, Sum, Zero } from './balances';
 
 type WorkingGroupPair<HiringModuleType, WorkingGroupType> = {
@@ -62,6 +65,36 @@ interface IRoleAccounter {
   reward_relationship: Option<RewardRelationshipId>;
 }
 
+type WGApiMethodType = 'nextOpeningId' | 'openingById' | 'nextApplicationId' | 'applicationById';
+type WGApiMethodsMapping = { [key in WGApiMethodType]: string };
+type WGToApiMethodsMapping = { [key in WorkingGroups]: { module: string; methods: WGApiMethodsMapping } };
+
+type GroupApplication = CuratorApplication | WorkerApplication;
+type GroupApplicationId = CuratorApplicationId | WorkerApplicationId;
+type GroupOpening = CuratorOpening | WorkerOpening;
+type GroupOpeningId = CuratorOpeningId | WorkerOpeningId;
+
+const wgApiMethodsMapping: WGToApiMethodsMapping = {
+  [WorkingGroups.StorageProviders]: {
+    module: 'storageBureaucracy',
+    methods: {
+      nextOpeningId: 'nextWorkerOpeningId',
+      openingById: 'workerOpeningById',
+      nextApplicationId: 'nextWorkerApplicationId',
+      applicationById: 'workerApplicationById'
+    }
+  },
+  [WorkingGroups.ContentCurators]: {
+    module: 'contentWorkingGroup',
+    methods: {
+      nextOpeningId: 'nextCuratorOpeningId',
+      openingById: 'curatorOpeningById',
+      nextApplicationId: 'nextCuratorApplicationId',
+      applicationById: 'curatorApplicationById'
+    }
+  }
+};
+
 export class Transport extends TransportBase implements ITransport {
   protected api: ApiPromise
   protected cachedApi: APIQueryCache
@@ -74,6 +107,13 @@ export class Transport extends TransportBase implements ITransport {
     this.queueExtrinsic = queueExtrinsic;
   }
 
+  cachedApiMethodByGroup (group: WorkingGroups, method: WGApiMethodType) {
+    const apiModule = wgApiMethodsMapping[group].module;
+    const apiMethod = wgApiMethodsMapping[group].methods[method];
+
+    return this.cachedApi.query[apiModule][apiMethod];
+  }
+
   unsubscribe () {
     this.cachedApi.unsubscribe();
   }
@@ -113,7 +153,7 @@ export class Transport extends TransportBase implements ITransport {
     return recipient.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,14 +168,14 @@ 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
     );
   }
@@ -263,22 +303,32 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
-  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 +338,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>(
+        group === WorkingGroups.ContentCurators ? CuratorApplication : WorkerApplication,
+        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 +369,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>(
+      group === WorkingGroups.ContentCurators ? CuratorOpening : WorkerOpening,
+      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 +411,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>>();
 

+ 3 - 1
pioneer/packages/joy-roles/src/transport.ts

@@ -1,12 +1,13 @@
 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 { 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>>;
@@ -14,6 +15,7 @@ export interface ITransport {
   curationGroup: () => Promise<WorkingGroupMembership>;
   storageGroup: () => Promise<StorageAndDistributionMembership>;
   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;

+ 2 - 1
pioneer/packages/joy-storage/src/AvailableRoles/index.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 import { BareProps } from '@polkadot/react-components/types';
 import { ComponentProps } from '../props';
-import { Role, RoleParameters } from '@joystream/types/roles';
+import { Role } from '@joystream/types/members';
+import { RoleParameters } from '@joystream/types/roles';
 import { Option } from '@polkadot/types';
 import { AccountId } from '@polkadot/types/interfaces';
 import { withCalls } from '@polkadot/react-api/index';

+ 2 - 1
pioneer/packages/joy-storage/src/MyRequests/index.tsx

@@ -3,7 +3,8 @@ 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 { Role } from '@joystream/types/members';
+import { Request, RoleParameters } from '@joystream/types/roles';
 import { Option } from '@polkadot/types';
 import { AccountId, Balance } from '@polkadot/types/interfaces';
 import TxButton from '@polkadot/joy-utils/TxButton';

+ 2 - 1
pioneer/packages/joy-storage/src/index.tsx

@@ -2,7 +2,8 @@ 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 { Role } from '@joystream/types/members';
+import { Request } from '@joystream/types/roles';
 
 import React from 'react';
 import { Route, Switch } from 'react-router';

+ 2 - 1
pioneer/packages/joy-storage/src/props.ts

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

+ 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',

+ 19 - 10
runtime-modules/bureaucracy/src/lib.rs

@@ -432,7 +432,7 @@ decl_module! {
         ) {
 
             // Ensure lead is set and is origin signer
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             // Ensuring worker actually exists
             let worker = Self::ensure_worker_exists(&worker_id)?;
@@ -463,7 +463,7 @@ decl_module! {
             human_readable_text: Vec<u8>
         ){
             // Ensure lead is set and is origin signer
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             Self::ensure_opening_human_readable_text_is_valid(&human_readable_text)?;
 
@@ -509,7 +509,7 @@ decl_module! {
         pub fn accept_worker_applications(origin, worker_opening_id: WorkerOpeningId<T>)  {
 
             // Ensure lead is set and is origin signer
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             // Ensure opening exists in this working group
             // NB: Even though call to hiring module will have implicit check for
@@ -664,7 +664,7 @@ decl_module! {
         ) {
 
             // Ensure lead is set and is origin signer
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             // Ensuring worker application actually exists
             let (worker_application, _, worker_opening) = Self::ensure_worker_application_exists(&worker_application_id)?;
@@ -691,7 +691,7 @@ decl_module! {
         pub fn begin_worker_applicant_review(origin, worker_opening_id: WorkerOpeningId<T>) {
 
             // Ensure lead is set and is origin signer
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             // Ensure opening exists
             // NB: Even though call to hiring modul will have implicit check for
@@ -721,7 +721,7 @@ decl_module! {
             reward_policy: Option<RewardPolicy<minting::BalanceOf<T>, T::BlockNumber>>
         ) {
             // Ensure lead is set and is origin signer
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             // Ensure worker opening exists
             let (worker_opening, _) = Self::ensure_worker_opening_exists(&worker_opening_id)?;
@@ -862,7 +862,7 @@ decl_module! {
         /// Slashes the worker stake, demands a leader origin. No limits, no actions on zero stake.
         /// If slashing balance greater than the existing stake - stake is slashed to zero.
         pub fn slash_worker_stake(origin, worker_id: WorkerId<T>, balance: BalanceOf<T>) {
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             let worker = Self::ensure_worker_exists(&worker_id)?;
 
@@ -889,7 +889,7 @@ decl_module! {
         /// Decreases the worker stake and returns the remainder to the worker role_account,
         /// demands a leader origin. Can be decreased to zero, no actions on zero stake.
         pub fn decrease_worker_stake(origin, worker_id: WorkerId<T>, balance: BalanceOf<T>) {
-            Self::ensure_origin_is_set_lead(origin)?;
+            Self::ensure_origin_is_active_leader(origin)?;
 
             let worker = Self::ensure_worker_exists(&worker_id)?;
 
@@ -975,6 +975,13 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
         Ok(())
     }
 
+    /// Returns all existing worker id list.
+    pub fn get_all_worker_ids() -> Vec<WorkerId<T>> {
+        <WorkerById<T, I>>::enumerate()
+            .map(|(worker_id, _)| worker_id)
+            .collect()
+    }
+
     fn ensure_lead_is_set() -> Result<Lead<MemberId<T>, T::AccountId>, Error> {
         let lead = <CurrentLead<T, I>>::get();
 
@@ -995,7 +1002,8 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
             .map_err(|e| e.into())
     }
 
-    fn ensure_origin_is_set_lead(origin: T::Origin) -> Result<(), Error> {
+    // Ensures origin is signed by the leader.
+    pub fn ensure_origin_is_active_leader(origin: T::Origin) -> Result<(), Error> {
         // Ensure is signed
         let signer = ensure_signed(origin)?;
 
@@ -1125,7 +1133,8 @@ impl<T: Trait<I>, I: Instance> Module<T, I> {
         Ok((worker_application, *worker_application_id, worker_opening))
     }
 
-    fn ensure_worker_signed(
+    /// Ensures the origin contains signed account that belongs to existing worker.
+    pub fn ensure_worker_signed(
         origin: T::Origin,
         worker_id: &WorkerId<T>,
     ) -> Result<WorkerOf<T>, Error> {

+ 9 - 3
runtime-modules/bureaucracy/src/tests/fixtures.rs

@@ -259,7 +259,7 @@ impl FillWorkerOpeningFixture {
         }
     }
 
-    pub fn call_and_assert(&self, expected_result: Result<(), Error>) {
+    pub fn call_and_assert(&self, expected_result: Result<(), Error>) -> u64 {
         let saved_worker_next_id = Bureaucracy1::next_worker_id();
         let actual_result = Bureaucracy1::fill_worker_opening(
             self.origin.clone().into(),
@@ -307,6 +307,8 @@ impl FillWorkerOpeningFixture {
 
             assert_eq!(actual_worker, expected_worker);
         }
+
+        saved_worker_next_id
     }
 }
 
@@ -462,7 +464,7 @@ impl ApplyOnWorkerOpeningFixture {
         }
     }
 
-    pub fn call_and_assert(&self, expected_result: Result<(), Error>) {
+    pub fn call_and_assert(&self, expected_result: Result<(), Error>) -> u64 {
         let saved_application_next_id = Bureaucracy1::next_worker_application_id();
         let actual_result = Bureaucracy1::apply_on_worker_opening(
             self.origin.clone().into(),
@@ -498,6 +500,8 @@ impl ApplyOnWorkerOpeningFixture {
                 .worker_applications
                 .contains(&application_id));
         }
+
+        saved_application_next_id
     }
 }
 
@@ -572,7 +576,7 @@ impl AddWorkerOpeningFixture {
         }
     }
 
-    pub fn call_and_assert(&self, expected_result: Result<(), Error>) {
+    pub fn call_and_assert(&self, expected_result: Result<(), Error>) -> u64 {
         let saved_opening_next_id = Bureaucracy1::next_worker_opening_id();
         let actual_result = Bureaucracy1::add_worker_opening(
             self.origin.clone().into(),
@@ -599,6 +603,8 @@ impl AddWorkerOpeningFixture {
 
             assert_eq!(actual_opening, expected_opening);
         }
+
+        saved_opening_next_id
     }
 
     pub fn with_text(self, text: Vec<u8>) -> Self {

+ 35 - 14
runtime-modules/bureaucracy/src/tests/mod.rs

@@ -1042,11 +1042,12 @@ fn fill_default_worker_position() -> u64 {
             payout_interval: None,
         }),
         None,
+        true,
     )
 }
 
 fn fill_worker_position_with_no_reward() -> u64 {
-    fill_worker_position(None, None)
+    fill_worker_position(None, None, true)
 }
 
 fn fill_worker_position_with_stake(stake: u64) -> u64 {
@@ -1057,18 +1058,22 @@ fn fill_worker_position_with_stake(stake: u64) -> u64 {
             payout_interval: None,
         }),
         Some(stake),
+        true,
     )
 }
 
 fn fill_worker_position(
     reward_policy: Option<RewardPolicy<u64, u64>>,
     role_stake: Option<u64>,
+    setup_environment: bool,
 ) -> u64 {
-    let lead_account_id = 1;
+    if setup_environment {
+        let lead_account_id = 1;
 
-    SetLeadFixture::set_lead(lead_account_id);
-    increase_total_balance_issuance_using_account_id(1, 10000);
-    setup_members(2);
+        SetLeadFixture::set_lead(lead_account_id);
+        increase_total_balance_issuance_using_account_id(1, 10000);
+        setup_members(2);
+    }
 
     let mut add_worker_opening_fixture = AddWorkerOpeningFixture::default();
     if let Some(stake) = role_stake.clone() {
@@ -1084,18 +1089,14 @@ fn fill_worker_position(
             });
     }
 
-    add_worker_opening_fixture.call_and_assert(Ok(()));
-
-    let opening_id = 0; // newly created opening
+    let opening_id = add_worker_opening_fixture.call_and_assert(Ok(()));
 
     let mut appy_on_worker_opening_fixture =
         ApplyOnWorkerOpeningFixture::default_for_opening_id(opening_id);
     if let Some(stake) = role_stake.clone() {
         appy_on_worker_opening_fixture = appy_on_worker_opening_fixture.with_role_stake(stake);
     }
-    appy_on_worker_opening_fixture.call_and_assert(Ok(()));
-
-    let application_id = 0; // newly created application
+    let application_id = appy_on_worker_opening_fixture.call_and_assert(Ok(()));
 
     let begin_review_worker_applications_fixture =
         BeginReviewWorkerApplicationsFixture::default_for_opening_id(opening_id);
@@ -1111,9 +1112,7 @@ fn fill_worker_position(
         fill_worker_opening_fixture = fill_worker_opening_fixture.with_reward_policy(reward_policy);
     }
 
-    fill_worker_opening_fixture.call_and_assert(Ok(()));
-
-    let worker_id = 0; // newly created worker
+    let worker_id = fill_worker_opening_fixture.call_and_assert(Ok(()));
 
     worker_id
 }
@@ -1529,3 +1528,25 @@ fn slash_worker_stake_fails_with_not_set_lead() {
         slash_stake_fixture.call_and_assert(Err(Error::CurrentLeadNotSet));
     });
 }
+
+#[test]
+fn get_all_worker_ids_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let worker_ids = Bureaucracy1::get_all_worker_ids();
+        assert_eq!(worker_ids, Vec::new());
+
+        let worker_id1 = fill_worker_position(None, None, true);
+        let worker_id2 = fill_worker_position(None, None, false);
+
+        let mut expected_ids = vec![worker_id1, worker_id2];
+        expected_ids.sort();
+
+        let mut worker_ids = Bureaucracy1::get_all_worker_ids();
+        worker_ids.sort();
+        assert_eq!(worker_ids, expected_ids);
+
+        <crate::WorkerById<Test, crate::Instance1>>::remove(worker_id1);
+        let worker_ids = Bureaucracy1::get_all_worker_ids();
+        assert_eq!(worker_ids, vec![worker_id2]);
+    });
+}

+ 9 - 2
runtime-modules/common/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-common-module'
-version = '1.0.0'
+version = '1.1.0'
 authors = ['Joystream contributors']
 edition = '2018'
 
@@ -10,6 +10,7 @@ std = [
 	'sr-primitives/std',
 	'srml-support/std',
 	'system/std',
+	'timestamp/std',
 	'codec/std',
 	'serde'
 ]
@@ -42,4 +43,10 @@ version = '1.0.0'
 [dependencies.serde]
 features = ['derive']
 optional = true
-version = '1.0.101'
+version = '1.0.101'
+
+[dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 26 - 0
runtime-modules/common/src/lib.rs

@@ -4,3 +4,29 @@
 pub mod constraints;
 pub mod currency;
 pub mod origin_validator;
+
+use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+/// Defines time in both block number and substrate time abstraction.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
+#[derive(Clone, Encode, Decode, PartialEq, Eq, Debug, Default)]
+pub struct BlockAndTime<BlockNumber, Moment> {
+    /// Defines chain block
+    pub block: BlockNumber,
+
+    /// Defines time
+    pub time: Moment,
+}
+
+/// Gathers current block and time information for the runtime.
+/// If this function is used inside a config() at genesis the timestamp will be 0
+/// because the timestamp is actually produced by validators.
+pub fn current_block_time<T: system::Trait + timestamp::Trait>(
+) -> BlockAndTime<T::BlockNumber, T::Moment> {
+    BlockAndTime {
+        block: <system::Module<T>>::block_number(),
+        time: <timestamp::Module<T>>::now(),
+    }
+}

+ 1 - 1
runtime-modules/common/src/origin_validator.rs

@@ -1,5 +1,5 @@
 /// Abstract validator for the origin(account_id) and actor_id (eg.: thread author id).
 pub trait ActorOriginValidator<Origin, ActorId, AccountId> {
-    /// Check for valid combination of origin and actor_id
+    /// Check for valid combination of origin and actor_id.
     fn ensure_actor_origin(origin: Origin, actor_id: ActorId) -> Result<AccountId, &'static str>;
 }

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

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

+ 12 - 26
runtime-modules/forum/src/lib.rs

@@ -18,6 +18,7 @@ use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Para
 use system::{ensure_signed, RawOrigin};
 
 pub use common::constraints::InputValidationLengthConstraint;
+use common::BlockAndTime;
 
 mod mock;
 mod tests;
@@ -72,20 +73,12 @@ pub trait ForumUserRegistry<AccountId> {
     fn get_forum_user(id: &AccountId) -> Option<ForumUser<AccountId>>;
 }
 
-/// Convenient composite time stamp
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-pub struct BlockchainTimestamp<BlockNumber, Moment> {
-    block: BlockNumber,
-    time: Moment,
-}
-
 /// Represents a moderation outcome applied to a post or a thread.
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
 pub struct ModerationAction<BlockNumber, Moment, AccountId> {
     /// When action occured.
-    moderated_at: BlockchainTimestamp<BlockNumber, Moment>,
+    moderated_at: BlockAndTime<BlockNumber, Moment>,
 
     /// Account forum sudo which acted.
     moderator_id: AccountId,
@@ -99,7 +92,7 @@ pub struct ModerationAction<BlockNumber, Moment, AccountId> {
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
 pub struct PostTextChange<BlockNumber, Moment> {
     /// When this expiration occured
-    expired_at: BlockchainTimestamp<BlockNumber, Moment>,
+    expired_at: BlockAndTime<BlockNumber, Moment>,
 
     /// Text that expired
     text: Vec<u8>,
@@ -132,7 +125,7 @@ pub struct Post<BlockNumber, Moment, AccountId, ThreadId, PostId> {
     text_change_history: Vec<PostTextChange<BlockNumber, Moment>>,
 
     /// When post was submitted.
-    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+    created_at: BlockAndTime<BlockNumber, Moment>,
 
     /// Author of post.
     author_id: AccountId,
@@ -175,7 +168,7 @@ pub struct Thread<BlockNumber, Moment, AccountId, ThreadId> {
     num_moderated_posts: u32,
 
     /// When thread was established.
-    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+    created_at: BlockAndTime<BlockNumber, Moment>,
 
     /// Author of post.
     author_id: AccountId,
@@ -216,7 +209,7 @@ pub struct Category<BlockNumber, Moment, AccountId> {
     description: Vec<u8>,
 
     /// When category was established.
-    created_at: BlockchainTimestamp<BlockNumber, Moment>,
+    created_at: BlockAndTime<BlockNumber, Moment>,
 
     /// Whether category is deleted.
     deleted: bool,
@@ -443,7 +436,7 @@ decl_module! {
                 id : next_category_id,
                 title,
                 description,
-                created_at : Self::current_block_and_time(),
+                created_at : common::current_block_time::<T>(),
                 deleted: false,
                 archived: false,
                 num_direct_subcategories: 0,
@@ -603,7 +596,7 @@ decl_module! {
 
             // Add moderation to thread
             thread.moderation = Some(ModerationAction {
-                moderated_at: Self::current_block_and_time(),
+                moderated_at: common::current_block_time::<T>(),
                 moderator_id: who,
                 rationale
             });
@@ -689,7 +682,7 @@ decl_module! {
             <PostById<T>>::mutate(post_id, |p| {
 
                 let expired_post_text = PostTextChange {
-                    expired_at: Self::current_block_and_time(),
+                    expired_at: common::current_block_time::<T>(),
                     text: post.current_text.clone()
                 };
 
@@ -726,7 +719,7 @@ decl_module! {
 
             // Update moderation action on post
             let moderation_action = ModerationAction{
-                moderated_at: Self::current_block_and_time(),
+                moderated_at: common::current_block_time::<T>(),
                 moderator_id: who,
                 rationale
             };
@@ -799,13 +792,6 @@ impl<T: Trait> Module<T> {
         )
     }
 
-    fn current_block_and_time() -> BlockchainTimestamp<T::BlockNumber, T::Moment> {
-        BlockchainTimestamp {
-            block: <system::Module<T>>::block_number(),
-            time: <timestamp::Module<T>>::now(),
-        }
-    }
-
     fn ensure_post_is_mutable(
         post_id: T::PostId,
     ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>, &'static str>
@@ -991,7 +977,7 @@ impl<T: Trait> Module<T> {
             moderation: None,
             num_unmoderated_posts: 0,
             num_moderated_posts: 0,
-            created_at: Self::current_block_and_time(),
+            created_at: common::current_block_time::<T>(),
             author_id: author_id.clone(),
         };
 
@@ -1031,7 +1017,7 @@ impl<T: Trait> Module<T> {
             current_text: text.to_owned(),
             moderation: None,
             text_change_history: vec![],
-            created_at: Self::current_block_and_time(),
+            created_at: common::current_block_time::<T>(),
             author_id: author_id.clone(),
         };
 

+ 3 - 4
runtime-modules/forum/src/mock.rs

@@ -1,6 +1,7 @@
 #![cfg(test)]
 
 use crate::*;
+use common::BlockAndTime;
 
 use primitives::H256;
 
@@ -544,10 +545,8 @@ pub type RuntimePost = Post<
     RuntimeThreadId,
     RuntimePostId,
 >;
-pub type RuntimeBlockchainTimestamp = BlockchainTimestamp<
-    <Runtime as system::Trait>::BlockNumber,
-    <Runtime as timestamp::Trait>::Moment,
->;
+pub type RuntimeBlockchainTimestamp =
+    BlockAndTime<<Runtime as system::Trait>::BlockNumber, <Runtime as timestamp::Trait>::Moment>;
 pub type RuntimeThreadId = <Runtime as Trait>::ThreadId;
 pub type RuntimePostId = <Runtime as Trait>::PostId;
 

+ 47 - 5
runtime-modules/service-discovery/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-service-discovery-module'
-version = '1.0.0'
+version = '2.0.0'
 authors = ['Joystream contributors']
 edition = '2018'
 
@@ -14,7 +14,7 @@ std = [
 	'serde',
     'codec/std',
     'primitives/std',
-    'roles/std',
+    'bureaucracy/std',
 ]
 
 [dependencies.sr-primitives]
@@ -29,6 +29,11 @@ git = 'https://github.com/paritytech/substrate.git'
 package = 'srml-support'
 rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
 
+[dependencies.common]
+default_features = false
+package = 'substrate-common-module'
+path = '../common'
+
 [dependencies.system]
 default_features = false
 git = 'https://github.com/paritytech/substrate.git'
@@ -64,7 +69,44 @@ git = 'https://github.com/paritytech/substrate.git'
 package = 'sr-io'
 rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
 
-[dependencies.roles]
+[dependencies.bureaucracy]
+default_features = false
+package = 'substrate-bureaucracy-module'
+path = '../bureaucracy'
+
+[dev-dependencies.balances]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-balances'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.recurringrewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../recurring-reward'
+
+[dev-dependencies.hiring]
+default_features = false
+package = 'substrate-hiring-module'
+path = '../hiring'
+
+[dev-dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../stake'
+
+[dev-dependencies.minting]
 default_features = false
-package = 'substrate-roles-module'
-path = '../roles'
+package = 'substrate-token-mint-module'
+path = '../token-minting'
+
+[dev-dependencies.membership]
+default_features = false
+package = 'substrate-membership-module'
+path = '../membership'
+
+[dev-dependencies.timestamp]
+default_features = false
+git = 'https://github.com/paritytech/substrate.git'
+package = 'srml-timestamp'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'

+ 0 - 127
runtime-modules/service-discovery/src/discovery.rs

@@ -1,127 +0,0 @@
-use codec::{Decode, Encode};
-use roles::traits::Roles;
-use rstd::prelude::*;
-#[cfg(feature = "std")]
-use serde::{Deserialize, Serialize};
-
-use srml_support::{decl_event, decl_module, decl_storage, ensure};
-use system::{self, ensure_root, ensure_signed};
-/*
-  Although there is support for ed25519 keys as the IPNS identity key and we could potentially
-  reuse the same key for the role account and ipns (and make this discovery module obselete)
-  it is probably better to separate concerns.
-  Why not to use a fixed size 32byte -> SHA256 hash of public key: because we would have to force
-  specific key type on ipfs side.
-  pub struct IPNSIdentity(pub [u8; 32]); // we loose the key type!
-  pub type IPNSIdentity(pub u8, pub [u8; 32]); // we could add the keytype?
-  can we use rust library in wasm runtime?
-  https://github.com/multiformats/rust-multihash
-  https://github.com/multiformats/multicodec/
-  https://github.com/multiformats/multihash/
-*/
-/// base58 encoded IPNS identity multihash codec
-pub type IPNSIdentity = Vec<u8>;
-
-/// HTTP Url string to a discovery service endpoint
-pub type Url = Vec<u8>;
-
-pub const MINIMUM_LIFETIME: u32 = 600; // 1hr assuming 6s block times
-pub const DEFAULT_LIFETIME: u32 = MINIMUM_LIFETIME * 24; // 24hr
-
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-pub struct AccountInfo<BlockNumber> {
-    /// IPNS Identity
-    pub identity: IPNSIdentity,
-    /// Block at which information expires
-    pub expires_at: BlockNumber,
-}
-
-pub trait Trait: system::Trait {
-    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
-
-    type Roles: Roles<Self>;
-}
-
-decl_storage! {
-    trait Store for Module<T: Trait> as Discovery {
-        /// Bootstrap endpoints maintained by root
-        pub BootstrapEndpoints get(bootstrap_endpoints): Vec<Url>;
-        /// Mapping of service providers' AccountIds to their AccountInfo
-        pub AccountInfoByAccountId get(account_info_by_account_id): map T::AccountId => AccountInfo<T::BlockNumber>;
-        /// Lifetime of an AccountInfo record in AccountInfoByAccountId map
-        pub DefaultLifetime get(default_lifetime) config(): T::BlockNumber = T::BlockNumber::from(DEFAULT_LIFETIME);
-    }
-}
-
-decl_event! {
-    pub enum Event<T> where <T as system::Trait>::AccountId {
-        AccountInfoUpdated(AccountId, IPNSIdentity),
-        AccountInfoRemoved(AccountId),
-    }
-}
-
-impl<T: Trait> Module<T> {
-    pub fn remove_account_info(accountid: &T::AccountId) {
-        if <AccountInfoByAccountId<T>>::exists(accountid) {
-            <AccountInfoByAccountId<T>>::remove(accountid);
-            Self::deposit_event(RawEvent::AccountInfoRemoved(accountid.clone()));
-        }
-    }
-
-    pub fn is_account_info_expired(accountid: &T::AccountId) -> bool {
-        !<AccountInfoByAccountId<T>>::exists(accountid)
-            || <system::Module<T>>::block_number()
-                > <AccountInfoByAccountId<T>>::get(accountid).expires_at
-    }
-}
-
-decl_module! {
-    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
-        fn deposit_event() = default;
-
-        pub fn set_ipns_id(origin, id: Vec<u8>, lifetime: Option<T::BlockNumber>) {
-            let sender = ensure_signed(origin)?;
-            ensure!(T::Roles::is_role_account(&sender), "only role accounts can set ipns id");
-
-            // TODO: ensure id is a valid base58 encoded IPNS identity
-
-            let ttl = match lifetime {
-                Some(value) => if value >= T::BlockNumber::from(MINIMUM_LIFETIME) {
-                    value
-                } else {
-                    T::BlockNumber::from(MINIMUM_LIFETIME)
-                },
-                _ => Self::default_lifetime()
-            };
-
-            <AccountInfoByAccountId<T>>::insert(&sender, AccountInfo {
-                identity: id.clone(),
-                expires_at: <system::Module<T>>::block_number() + ttl,
-            });
-
-            Self::deposit_event(RawEvent::AccountInfoUpdated(sender, id));
-        }
-
-        pub fn unset_ipns_id(origin) {
-            let sender = ensure_signed(origin)?;
-            Self::remove_account_info(&sender);
-        }
-
-        // privileged methods
-
-        pub fn set_default_lifetime(origin, lifetime: T::BlockNumber) {
-            // although not strictly required to have an origin parameter and ensure_root
-            // decl_module! macro takes care of it.. its required for unit tests to work correctly
-            // otherwise it complains the method
-            ensure_root(origin)?;
-            ensure!(lifetime >= T::BlockNumber::from(MINIMUM_LIFETIME), "discovery: default lifetime must be gte minimum lifetime");
-            <DefaultLifetime<T>>::put(lifetime);
-        }
-
-        pub fn set_bootstrap_endpoints(origin, endpoints: Vec<Vec<u8>>) {
-            ensure_root(origin)?;
-            BootstrapEndpoints::put(endpoints);
-        }
-    }
-}

+ 177 - 2
runtime-modules/service-discovery/src/lib.rs

@@ -1,7 +1,182 @@
+//! # Service discovery module
+//! Service discovery module for the Joystream platform supports the storage providers.
+//! It registers their 'pings' in the system with the expiration time, and stores the bootstrap
+//! nodes for the Colossus.
+//!
+//! ## Comments
+//!
+//! Service discovery module uses bureaucracy module to authorize actions. It is generally used by
+//! the Colossus service.
+//!
+//! ## Supported extrinsics
+//!
+//! - [set_ipns_id](./struct.Module.html#method.set_ipns_id) - Creates the AccountInfo to save an IPNS identity for the storage provider.
+//! - [unset_ipns_id](./struct.Module.html#method.unset_ipns_id) - Deletes the AccountInfo with the IPNS identity for the storage provider.
+//! - [set_default_lifetime](./struct.Module.html#method.set_default_lifetime) - Sets default lifetime for storage providers accounts info.
+//! - [set_bootstrap_endpoints](./struct.Module.html#method.set_bootstrap_endpoints) - Sets bootstrap endpoints for the Colossus.
+//!
+
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 
-pub mod discovery;
-
 mod mock;
 mod tests;
+
+use codec::{Decode, Encode};
+use rstd::prelude::*;
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+use srml_support::{decl_event, decl_module, decl_storage, ensure};
+use system::{self, ensure_root};
+/*
+  Although there is support for ed25519 keys as the IPNS identity key and we could potentially
+  reuse the same key for the role account and ipns (and make this discovery module obselete)
+  it is probably better to separate concerns.
+  Why not to use a fixed size 32byte -> SHA256 hash of public key: because we would have to force
+  specific key type on ipfs side.
+  pub struct IPNSIdentity(pub [u8; 32]); // we loose the key type!
+  pub type IPNSIdentity(pub u8, pub [u8; 32]); // we could add the keytype?
+  can we use rust library in wasm runtime?
+  https://github.com/multiformats/rust-multihash
+  https://github.com/multiformats/multicodec/
+  https://github.com/multiformats/multihash/
+*/
+/// base58 encoded IPNS identity multihash codec
+pub type IPNSIdentity = Vec<u8>;
+
+/// HTTP Url string to a discovery service endpoint
+pub type Url = Vec<u8>;
+
+// Alias for storage working group bureaucracy
+pub(crate) type StorageBureaucracy<T> = bureaucracy::Module<T, bureaucracy::Instance2>;
+
+/// Storage provider is a worker from the bureaucracy module.
+pub type StorageProviderId<T> = bureaucracy::WorkerId<T>;
+
+pub(crate) const MINIMUM_LIFETIME: u32 = 600; // 1hr assuming 6s block times
+pub(crate) const DEFAULT_LIFETIME: u32 = MINIMUM_LIFETIME * 24; // 24hr
+
+/// Defines the expiration date for the storage provider.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
+pub struct AccountInfo<BlockNumber> {
+    /// IPNS Identity.
+    pub identity: IPNSIdentity,
+    /// Block at which information expires.
+    pub expires_at: BlockNumber,
+}
+
+/// The _Service discovery_ main _Trait_.
+pub trait Trait: system::Trait + bureaucracy::Trait<bureaucracy::Instance2> {
+    /// _Service discovery_ event type.
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Discovery {
+        /// Bootstrap endpoints maintained by root
+        pub BootstrapEndpoints get(bootstrap_endpoints): Vec<Url>;
+
+        /// Mapping of service providers' storage provider id to their AccountInfo
+        pub AccountInfoByStorageProviderId get(account_info_by_storage_provider_id):
+            map StorageProviderId<T> => AccountInfo<T::BlockNumber>;
+
+        /// Lifetime of an AccountInfo record in AccountInfoByAccountId map
+        pub DefaultLifetime get(default_lifetime) config():
+            T::BlockNumber = T::BlockNumber::from(DEFAULT_LIFETIME);
+    }
+}
+
+decl_event! {
+    /// _Service discovery_ events
+    pub enum Event<T> where
+        StorageProviderId = StorageProviderId<T>
+       {
+        /// Emits on updating of the account info.
+        /// Params:
+        /// - Id of the storage provider.
+        /// - Id of the IPNS.
+        AccountInfoUpdated(StorageProviderId, IPNSIdentity),
+
+        /// Emits on removing of the account info.
+        /// Params:
+        /// - Id of the storage provider.
+        AccountInfoRemoved(StorageProviderId),
+    }
+}
+
+decl_module! {
+    /// _Service discovery_ substrate module.
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Default deposit_event() handler
+        fn deposit_event() = default;
+
+        /// Creates the AccountInfo to save an IPNS identity for the storage provider.
+        /// Requires signed storage provider credentials.
+        pub fn set_ipns_id(
+            origin,
+            storage_provider_id: StorageProviderId<T>,
+            id: Vec<u8>,
+        ) {
+            <StorageBureaucracy<T>>::ensure_worker_signed(origin, &storage_provider_id)?;
+
+            // TODO: ensure id is a valid base58 encoded IPNS identity
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <AccountInfoByStorageProviderId<T>>::insert(storage_provider_id, AccountInfo {
+                identity: id.clone(),
+                expires_at: <system::Module<T>>::block_number() + Self::default_lifetime(),
+            });
+
+            Self::deposit_event(RawEvent::AccountInfoUpdated(storage_provider_id, id));
+        }
+
+        /// Deletes the AccountInfo with the IPNS identity for the storage provider.
+        /// Requires signed storage provider credentials.
+        pub fn unset_ipns_id(origin, storage_provider_id: StorageProviderId<T>) {
+            <StorageBureaucracy<T>>::ensure_worker_signed(origin, &storage_provider_id)?;
+
+            // == MUTATION SAFE ==
+
+            if <AccountInfoByStorageProviderId<T>>::exists(storage_provider_id) {
+                <AccountInfoByStorageProviderId<T>>::remove(storage_provider_id);
+                Self::deposit_event(RawEvent::AccountInfoRemoved(storage_provider_id));
+            }
+        }
+
+        // Privileged methods
+
+        /// Sets default lifetime for storage providers accounts info. Requires root privileges.
+        pub fn set_default_lifetime(origin, lifetime: T::BlockNumber) {
+            ensure_root(origin)?;
+            ensure!(lifetime >= T::BlockNumber::from(MINIMUM_LIFETIME),
+                "discovery: default lifetime must be gte minimum lifetime");
+
+            // == MUTATION SAFE ==
+
+            <DefaultLifetime<T>>::put(lifetime);
+        }
+
+        /// Sets bootstrap endpoints for the Colossus. Requires root privileges.
+        pub fn set_bootstrap_endpoints(origin, endpoints: Vec<Url>) {
+            ensure_root(origin)?;
+
+            // == MUTATION SAFE ==
+
+            BootstrapEndpoints::put(endpoints);
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    /// Verifies that account info for the storage provider is still valid.
+    pub fn is_account_info_expired(storage_provider_id: &StorageProviderId<T>) -> bool {
+        !<AccountInfoByStorageProviderId<T>>::exists(storage_provider_id)
+            || <system::Module<T>>::block_number()
+                > <AccountInfoByStorageProviderId<T>>::get(storage_provider_id).expires_at
+    }
+}

+ 100 - 21
runtime-modules/service-discovery/src/mock.rs

@@ -1,8 +1,6 @@
 #![cfg(test)]
 
-pub use super::discovery;
-pub use roles::actors;
-use roles::traits::Roles;
+pub use crate::*;
 
 pub use primitives::{Blake2Hasher, H256};
 pub use sr_primitives::{
@@ -13,6 +11,20 @@ pub use sr_primitives::{
 
 use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
 
+mod bureaucracy_mod {
+    pub use bureaucracy::Event;
+    pub use bureaucracy::Instance2;
+    pub use bureaucracy::Trait;
+}
+
+mod membership_mod {
+    pub use membership::members::Event;
+}
+
+mod discovery {
+    pub use crate::Event;
+}
+
 impl_outer_origin! {
     pub enum Origin for Test {}
 }
@@ -20,6 +32,9 @@ impl_outer_origin! {
 impl_outer_event! {
     pub enum MetaEvent for Test {
         discovery<T>,
+        balances<T>,
+        membership_mod<T>,
+        bureaucracy_mod Instance2 <T>,
     }
 }
 
@@ -31,6 +46,12 @@ parameter_types! {
     pub const MaximumBlockWeight: u32 = 1024;
     pub const MaximumBlockLength: u32 = 2 * 1024;
     pub const AvailableBlockRatio: Perbill = Perbill::one();
+    pub const MinimumPeriod: u64 = 5;
+    pub const InitialMembersBalance: u64 = 2000;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
+    pub const ExistentialDeposit: u32 = 0;
+    pub const TransferFee: u32 = 0;
+    pub const CreationFee: u32 = 0;
 }
 
 impl system::Trait for Test {
@@ -51,31 +72,69 @@ impl system::Trait for Test {
     type Version = ();
 }
 
-pub fn alice_account() -> u64 {
-    1
+impl Trait for Test {
+    type Event = MetaEvent;
+}
+
+impl hiring::Trait for Test {
+    type OpeningId = u64;
+    type ApplicationId = u64;
+    type ApplicationDeactivatedHandler = ();
+    type StakeHandlerProvider = hiring::Module<Self>;
 }
-pub fn bob_account() -> u64 {
-    2
+
+impl minting::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
 }
 
-impl discovery::Trait for Test {
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+impl membership::members::Trait for Test {
     type Event = MetaEvent;
-    type Roles = MockRoles;
+    type MemberId = u64;
+    type PaidTermId = u64;
+    type SubscriptionId = u64;
+    type ActorId = u64;
+    type InitialMembersBalance = InitialMembersBalance;
 }
 
-pub struct MockRoles {}
-impl Roles<Test> for MockRoles {
-    fn is_role_account(account_id: &u64) -> bool {
-        *account_id == alice_account()
-    }
+impl common::currency::GovernanceCurrency for Test {
+    type Currency = Balances;
+}
 
-    fn account_has_role(_account_id: &u64, _role: actors::Role) -> bool {
-        false
-    }
+impl balances::Trait for Test {
+    type Balance = u64;
+    type OnFreeBalanceZero = ();
+    type OnNewAccount = ();
+    type TransferPayment = ();
+    type DustRemoval = ();
+    type Event = MetaEvent;
+    type ExistentialDeposit = ExistentialDeposit;
+    type TransferFee = TransferFee;
+    type CreationFee = CreationFee;
+}
 
-    fn random_account_for_role(_role: actors::Role) -> Result<u64, &'static str> {
-        Err("not implemented")
-    }
+impl recurringrewards::Trait for Test {
+    type PayoutStatusHandler = ();
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+impl bureaucracy::Trait<bureaucracy::Instance2> for Test {
+    type Event = MetaEvent;
+}
+
+impl timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+    type MinimumPeriod = MinimumPeriod;
 }
 
 pub fn initial_test_ext() -> runtime_io::TestExternalities {
@@ -86,5 +145,25 @@ pub fn initial_test_ext() -> runtime_io::TestExternalities {
     t.into()
 }
 
+pub type Balances = balances::Module<Test>;
 pub type System = system::Module<Test>;
-pub type Discovery = discovery::Module<Test>;
+pub type Discovery = Module<Test>;
+
+pub(crate) fn hire_storage_provider() -> (u64, u64) {
+    let storage_provider_id = 1;
+    let role_account_id = 1;
+
+    let storage_provider = bureaucracy::Worker {
+        member_id: 1,
+        role_account: role_account_id,
+        reward_relationship: None,
+        role_stake_profile: None,
+    };
+
+    <bureaucracy::WorkerById<Test, bureaucracy::Instance2>>::insert(
+        storage_provider_id,
+        storage_provider,
+    );
+
+    (role_account_id, storage_provider_id)
+}

+ 69 - 34
runtime-modules/service-discovery/src/tests.rs

@@ -11,16 +11,24 @@ fn set_ipns_id() {
         let current_block_number = 1000;
         System::set_block_number(current_block_number);
 
-        let alice = alice_account();
-        let identity = "alice".as_bytes().to_vec();
-        let ttl = <Test as system::Trait>::BlockNumber::from(discovery::MINIMUM_LIFETIME + 100);
-        assert!(Discovery::set_ipns_id(Origin::signed(alice), identity.clone(), Some(ttl)).is_ok());
+        let (storage_provider_account_id, storage_provider_id) = hire_storage_provider();
 
-        assert!(<discovery::AccountInfoByAccountId<Test>>::exists(&alice));
-        let account_info = Discovery::account_info_by_account_id(&alice);
+        let identity = "alice".as_bytes().to_vec();
+        let ttl = <Test as system::Trait>::BlockNumber::from(DEFAULT_LIFETIME);
+        assert!(Discovery::set_ipns_id(
+            Origin::signed(storage_provider_account_id),
+            storage_provider_id,
+            identity.clone(),
+        )
+        .is_ok());
+
+        assert!(<AccountInfoByStorageProviderId<Test>>::exists(
+            &storage_provider_id
+        ));
+        let account_info = Discovery::account_info_by_storage_provider_id(&storage_provider_id);
         assert_eq!(
             account_info,
-            discovery::AccountInfo {
+            AccountInfo {
                 identity: identity.clone(),
                 expires_at: current_block_number + ttl
             }
@@ -30,78 +38,105 @@ fn set_ipns_id() {
             *System::events().last().unwrap(),
             EventRecord {
                 phase: Phase::ApplyExtrinsic(0),
-                event: MetaEvent::discovery(discovery::RawEvent::AccountInfoUpdated(
-                    alice,
+                event: MetaEvent::discovery(RawEvent::AccountInfoUpdated(
+                    storage_provider_id,
                     identity.clone()
                 )),
                 topics: vec![]
             }
         );
 
-        // Non role account trying to set account into should fail
-        let bob = bob_account();
-        assert!(Discovery::set_ipns_id(Origin::signed(bob), identity.clone(), None).is_err());
-        assert!(!<discovery::AccountInfoByAccountId<Test>>::exists(&bob));
+        // Invalid storage provider data
+        let invalid_storage_provider_id = 2;
+        let invalid_storage_provider_account_id = 2;
+        assert!(Discovery::set_ipns_id(
+            Origin::signed(invalid_storage_provider_id),
+            invalid_storage_provider_account_id,
+            identity.clone(),
+        )
+        .is_err());
+        assert!(!<AccountInfoByStorageProviderId<Test>>::exists(
+            &invalid_storage_provider_id
+        ));
     });
 }
 
 #[test]
 fn unset_ipns_id() {
     initial_test_ext().execute_with(|| {
-        let alice = alice_account();
+        let (storage_provider_account_id, storage_provider_id) = hire_storage_provider();
 
-        <discovery::AccountInfoByAccountId<Test>>::insert(
-            &alice,
-            discovery::AccountInfo {
+        <AccountInfoByStorageProviderId<Test>>::insert(
+            &storage_provider_id,
+            AccountInfo {
                 expires_at: 1000,
                 identity: "alice".as_bytes().to_vec(),
             },
         );
 
-        assert!(<discovery::AccountInfoByAccountId<Test>>::exists(&alice));
+        assert!(<AccountInfoByStorageProviderId<Test>>::exists(
+            &storage_provider_account_id
+        ));
 
-        assert!(Discovery::unset_ipns_id(Origin::signed(alice)).is_ok());
-        assert!(!<discovery::AccountInfoByAccountId<Test>>::exists(&alice));
+        assert!(Discovery::unset_ipns_id(
+            Origin::signed(storage_provider_account_id),
+            storage_provider_id
+        )
+        .is_ok());
+        assert!(!<AccountInfoByStorageProviderId<Test>>::exists(
+            &storage_provider_account_id
+        ));
 
         assert_eq!(
             *System::events().last().unwrap(),
             EventRecord {
                 phase: Phase::ApplyExtrinsic(0),
-                event: MetaEvent::discovery(discovery::RawEvent::AccountInfoRemoved(alice)),
+                event: MetaEvent::discovery(RawEvent::AccountInfoRemoved(storage_provider_id)),
                 topics: vec![]
             }
         );
+
+        // Invalid storage provider data
+        let invalid_storage_provider_id = 2;
+        let invalid_storage_provider_account_id = 2;
+        assert!(Discovery::unset_ipns_id(
+            Origin::signed(invalid_storage_provider_id),
+            invalid_storage_provider_account_id,
+        )
+        .is_err());
+        assert!(!<AccountInfoByStorageProviderId<Test>>::exists(
+            &invalid_storage_provider_id
+        ));
     });
 }
 
 #[test]
 fn is_account_info_expired() {
     initial_test_ext().execute_with(|| {
-        let alice = alice_account();
+        let storage_provider_id = 1;
         let expires_at = 1000;
         let id = "alice".as_bytes().to_vec();
-        <discovery::AccountInfoByAccountId<Test>>::insert(
-            &alice,
-            discovery::AccountInfo {
+        <AccountInfoByStorageProviderId<Test>>::insert(
+            &storage_provider_id,
+            AccountInfo {
                 expires_at,
                 identity: id.clone(),
             },
         );
 
         System::set_block_number(expires_at - 10);
-        assert!(!Discovery::is_account_info_expired(&alice));
+        assert!(!Discovery::is_account_info_expired(&storage_provider_id));
 
         System::set_block_number(expires_at + 10);
-        assert!(Discovery::is_account_info_expired(&alice));
+        assert!(Discovery::is_account_info_expired(&storage_provider_id));
     });
 }
 
 #[test]
 fn set_default_lifetime() {
     initial_test_ext().execute_with(|| {
-        let lifetime =
-            <Test as system::Trait>::BlockNumber::from(discovery::MINIMUM_LIFETIME + 2000);
-        // priviliged method should fail if not from root origin
+        let lifetime = <Test as system::Trait>::BlockNumber::from(MINIMUM_LIFETIME + 2000);
+        // privileged method should fail if not from root origin
         assert!(
             Discovery::set_default_lifetime(Origin::signed(1), lifetime).is_err(),
             ""
@@ -113,10 +148,10 @@ fn set_default_lifetime() {
         assert_eq!(Discovery::default_lifetime(), lifetime, "");
 
         // cannot set default lifetime to less than minimum
-        let less_than_min_liftime =
-            <Test as system::Trait>::BlockNumber::from(discovery::MINIMUM_LIFETIME - 1);
+        let less_than_min_lifetime =
+            <Test as system::Trait>::BlockNumber::from(MINIMUM_LIFETIME - 1);
         assert!(
-            Discovery::set_default_lifetime(Origin::ROOT, less_than_min_liftime).is_err(),
+            Discovery::set_default_lifetime(Origin::ROOT, less_than_min_lifetime).is_err(),
             ""
         );
     });
@@ -126,7 +161,7 @@ fn set_default_lifetime() {
 fn set_bootstrap_endpoints() {
     initial_test_ext().execute_with(|| {
         let endpoints = vec!["endpoint1".as_bytes().to_vec()];
-        // priviliged method should fail if not from root origin
+        // privileged method should fail if not from root origin
         assert!(
             Discovery::set_bootstrap_endpoints(Origin::signed(1), endpoints.clone()).is_err(),
             ""

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

@@ -1,6 +1,6 @@
 [package]
 name = 'substrate-storage-module'
-version = '1.0.0'
+version = '2.0.0'
 authors = ['Joystream contributors']
 edition = '2018'
 
@@ -17,7 +17,7 @@ std = [
 	'primitives/std',
 	'common/std',
 	'membership/std',
-	'roles/std',
+	'bureaucracy/std',
 ]
 
 
@@ -78,10 +78,10 @@ default_features = false
 package = 'substrate-common-module'
 path = '../common'
 
-[dependencies.roles]
+[dependencies.bureaucracy]
 default_features = false
-package = 'substrate-roles-module'
-path = '../roles'
+package = 'substrate-bureaucracy-module'
+path = '../bureaucracy'
 
 [dev-dependencies.runtime-io]
 default_features = false
@@ -93,4 +93,24 @@ rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
 default_features = false
 git = 'https://github.com/paritytech/substrate.git'
 package = 'srml-balances'
-rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
+
+[dev-dependencies.recurringrewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../recurring-reward'
+
+[dev-dependencies.hiring]
+default_features = false
+package = 'substrate-hiring-module'
+path = '../hiring'
+
+[dev-dependencies.stake]
+default_features = false
+package = 'substrate-stake-module'
+path = '../stake'
+
+[dev-dependencies.minting]
+default_features = false
+package = 'substrate-token-mint-module'
+path = '../token-minting'

+ 219 - 208
runtime-modules/storage/src/data_directory.rs

@@ -1,47 +1,112 @@
-use crate::data_object_type_registry::Trait as DOTRTrait;
-use crate::traits::{ContentIdExists, IsActiveDataObjectType};
-use codec::{Codec, Decode, Encode};
-use roles::actors;
-use roles::traits::Roles;
+//! # Data directory module
+//! Data directory module for the Joystream platform manages IPFS content id, storage providers,
+//! owners of the content. It allows to add and accept or reject the content in the system.
+//!
+//! ## Comments
+//!
+//! Data object type registry module uses bureaucracy module to authorize actions.
+//!
+//! ## Supported extrinsics
+//!
+//! ### Public extrinsic
+//! - [add_content](./struct.Module.html#method.add_content) - Adds the content to the system.
+//!
+//! ### Private extrinsics
+//! - accept_content - Storage provider accepts a content.
+//! - reject_content - Storage provider rejects a content.
+//! - remove_known_content_id - Removes the content id from the list of known content ids. Requires root privileges.
+//! - set_known_content_id - Sets the content id from the list of known content ids. Requires root privileges.
+//!
+
+// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
+//#![warn(missing_docs)]
+
+use codec::{Decode, Encode};
 use rstd::prelude::*;
-use sr_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
-use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Parameter};
-use system::{self, ensure_root, ensure_signed};
-
-pub trait Trait: timestamp::Trait + system::Trait + DOTRTrait + membership::members::Trait {
+use sr_primitives::traits::{MaybeSerialize, Member};
+use srml_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
+use system::{self, ensure_root};
+
+use common::origin_validator::ActorOriginValidator;
+pub(crate) use common::BlockAndTime;
+
+use crate::data_object_type_registry;
+use crate::data_object_type_registry::IsActiveDataObjectType;
+use crate::{MemberId, StorageBureaucracy, StorageProviderId};
+
+/// The _Data directory_ main _Trait_.
+pub trait Trait:
+    timestamp::Trait
+    + system::Trait
+    + data_object_type_registry::Trait
+    + membership::members::Trait
+    + bureaucracy::Trait<bureaucracy::Instance2>
+{
+    /// _Data directory_ event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
+    /// Content id.
     type ContentId: Parameter + Member + MaybeSerialize + Copy + Ord + Default;
 
-    type SchemaId: Parameter
-        + Member
-        + SimpleArithmetic
-        + Codec
-        + Default
-        + Copy
-        + MaybeSerialize
-        + PartialEq;
-
-    type Roles: Roles<Self>;
-    type IsActiveDataObjectType: IsActiveDataObjectType<Self>;
+    /// Provides random storage provider id.
+    type StorageProviderHelper: StorageProviderHelper<Self>;
+
+    ///Active data object type validator.
+    type IsActiveDataObjectType: data_object_type_registry::IsActiveDataObjectType<Self>;
+
+    /// Validates member id and origin combination.
+    type MemberOriginValidator: ActorOriginValidator<Self::Origin, MemberId<Self>, Self::AccountId>;
 }
 
-static MSG_CID_NOT_FOUND: &str = "Content with this ID not found.";
-static MSG_LIAISON_REQUIRED: &str = "Only the liaison for the content may modify its status.";
-static MSG_CREATOR_MUST_BE_MEMBER: &str = "Only active members may create content.";
-static MSG_DO_TYPE_MUST_BE_ACTIVE: &str =
-    "Cannot create content for inactive or missing data object type.";
+decl_error! {
+    /// _Data object storage registry_ module predefined errors.
+    pub enum Error {
+        /// Content with this ID not found.
+        CidNotFound,
 
-#[derive(Clone, Encode, Decode, PartialEq, Debug)]
-pub struct BlockAndTime<T: Trait> {
-    pub block: T::BlockNumber,
-    pub time: T::Moment,
+        /// Only the liaison for the content may modify its status.
+        LiaisonRequired,
+
+        /// Cannot create content for inactive or missing data object type.
+        DataObjectTypeMustBeActive,
+
+        /// "Data object already added under this content id".
+        DataObjectAlreadyAdded,
+
+        /// Require root origin in extrinsics.
+        RequireRootOrigin,
+    }
 }
 
+impl From<system::Error> for Error {
+    fn from(error: system::Error) -> Self {
+        match error {
+            system::Error::Other(msg) => Error::Other(msg),
+            system::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+impl From<bureaucracy::Error> for Error {
+    fn from(error: bureaucracy::Error) -> Self {
+        match error {
+            bureaucracy::Error::Other(msg) => Error::Other(msg),
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+/// The decision of the storage provider when it acts as liaison.
 #[derive(Clone, Encode, Decode, PartialEq, Debug)]
 pub enum LiaisonJudgement {
+    /// Content awaits for a judgment.
     Pending,
+
+    /// Content accepted.
     Accepted,
+
+    /// Content rejected.
     Rejected,
 }
 
@@ -51,137 +116,162 @@ impl Default for LiaisonJudgement {
     }
 }
 
+/// Manages content ids, type and storage provider decision about it.
 #[derive(Clone, Encode, Decode, PartialEq, Debug)]
 pub struct DataObject<T: Trait> {
-    pub owner: T::AccountId,
-    pub added_at: BlockAndTime<T>,
-    pub type_id: <T as DOTRTrait>::DataObjectTypeId,
+    /// Content owner.
+    pub owner: MemberId<T>,
+
+    /// Content added at.
+    pub added_at: BlockAndTime<T::BlockNumber, T::Moment>,
+
+    /// Content type id.
+    pub type_id: <T as data_object_type_registry::Trait>::DataObjectTypeId,
+
+    /// Content size in bytes.
     pub size: u64,
-    pub liaison: T::AccountId,
-    pub liaison_judgement: LiaisonJudgement,
-    pub ipfs_content_id: Vec<u8>, // shoule we use rust multi-format crate?
-                                  // TODO signing_key: public key supplied by the uploader,
-                                  // they sigh the content with this key
 
-                                  // TODO add support for this field (Some if judgment == Rejected)
-                                  // pub rejection_reason: Option<Vec<u8>>,
-}
+    /// Storage provider id of the liaison.
+    pub liaison: StorageProviderId<T>,
 
-#[derive(Clone, Encode, Decode, PartialEq, Debug)]
-pub enum ContentVisibility {
-    Draft, // TODO rename to Unlisted?
-    Public,
-}
+    /// Storage provider as liaison judgment.
+    pub liaison_judgement: LiaisonJudgement,
 
-impl Default for ContentVisibility {
-    fn default() -> Self {
-        ContentVisibility::Draft // TODO make Public by default?
-    }
+    /// IPFS content id.
+    pub ipfs_content_id: Vec<u8>,
 }
 
 decl_storage! {
     trait Store for Module<T: Trait> as DataDirectory {
+        /// List of ids known to the system.
+        pub KnownContentIds get(known_content_ids): Vec<T::ContentId> = Vec::new();
 
-        // TODO default_liaison = Joystream storage account id.
-
-        // TODO this list of ids should be moved off-chain once we have Content Indexer.
-        // TODO deprecated, moved tp storage relationship
-        pub KnownContentIds get(known_content_ids): Vec<T::ContentId> = vec![];
-
+        /// Maps data objects by their content id.
         pub DataObjectByContentId get(data_object_by_content_id):
             map T::ContentId => Option<DataObject<T>>;
-
-        // Default storage provider account id, overrides all active storage providers as liason if set
-        pub PrimaryLiaisonAccountId get(primary_liaison_account_id): Option<T::AccountId>;
     }
 }
 
 decl_event! {
+    /// _Data directory_ events
     pub enum Event<T> where
         <T as Trait>::ContentId,
-        <T as system::Trait>::AccountId
+        MemberId = MemberId<T>,
+        StorageProviderId = StorageProviderId<T>
     {
-        // The account is the one who uploaded the content.
-        ContentAdded(ContentId, AccountId),
-
-        // The account is the liaison - only they can reject or accept
-        ContentAccepted(ContentId, AccountId),
-        ContentRejected(ContentId, AccountId),
+        /// Emits on adding of the content.
+        /// Params:
+        /// - Id of the relationship.
+        /// - Id of the member.
+        ContentAdded(ContentId, MemberId),
+
+        /// Emits when the storage provider accepts a content.
+        /// Params:
+        /// - Id of the relationship.
+        /// - Id of the storage provider.
+        ContentAccepted(ContentId, StorageProviderId),
+
+        /// Emits when the storage provider rejects a content.
+        /// Params:
+        /// - Id of the relationship.
+        /// - Id of the storage provider.
+        ContentRejected(ContentId, StorageProviderId),
     }
 }
 
 decl_module! {
+    /// _Data directory_ substrate module.
     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Default deposit_event() handler
         fn deposit_event() = default;
 
-        // TODO send file_name as param so we could create a Draft metadata in this fn
+        /// Predefined errors.
+        type Error = Error;
+
+
+        /// Adds the content to the system. Member id should match its origin. The created DataObject
+        /// awaits liaison to accept or reject it.
         pub fn add_content(
             origin,
+            member_id: MemberId<T>,
             content_id: T::ContentId,
-            type_id: <T as DOTRTrait>::DataObjectTypeId,
+            type_id: <T as data_object_type_registry::Trait>::DataObjectTypeId,
             size: u64,
             ipfs_content_id: Vec<u8>
         ) {
-            let who = ensure_signed(origin)?;
-            ensure!(<membership::members::Module<T>>::is_member_account(&who), MSG_CREATOR_MUST_BE_MEMBER);
+            T::MemberOriginValidator::ensure_actor_origin(
+                origin,
+                member_id,
+            )?;
 
             ensure!(T::IsActiveDataObjectType::is_active_data_object_type(&type_id),
-                MSG_DO_TYPE_MUST_BE_ACTIVE);
+                Error::DataObjectTypeMustBeActive);
 
             ensure!(!<DataObjectByContentId<T>>::exists(content_id),
-                "Data object aready added under this content id");
+                Error::DataObjectAlreadyAdded);
 
-            let liaison = match Self::primary_liaison_account_id() {
-                // Select primary liaison if set
-                Some(primary_liaison) => primary_liaison,
-
-                // Select liaison from staked roles if available
-                _ => T::Roles::random_account_for_role(actors::Role::StorageProvider)?
-            };
+            let liaison = T::StorageProviderHelper::get_random_storage_provider()?;
 
             // Let's create the entry then
             let data: DataObject<T> = DataObject {
                 type_id,
                 size,
-                added_at: Self::current_block_and_time(),
-                owner: who.clone(),
+                added_at: common::current_block_time::<T>(),
+                owner: member_id,
                 liaison,
                 liaison_judgement: LiaisonJudgement::Pending,
                 ipfs_content_id,
             };
 
+            //
+            // == MUTATION SAFE ==
+            //
+
             <DataObjectByContentId<T>>::insert(&content_id, data);
-            Self::deposit_event(RawEvent::ContentAdded(content_id, who));
+            Self::deposit_event(RawEvent::ContentAdded(content_id, member_id));
         }
 
-        // The LiaisonJudgement can be updated, but only by the liaison.
-        fn accept_content(origin, content_id: T::ContentId) {
-            let who = ensure_signed(origin)?;
-            Self::update_content_judgement(&who, content_id, LiaisonJudgement::Accepted)?;
+        /// Storage provider accepts a content. Requires signed storage provider account and its id.
+        /// The LiaisonJudgement can be updated, but only by the liaison.
+        pub(crate) fn accept_content(
+            origin,
+            storage_provider_id: StorageProviderId<T>,
+            content_id: T::ContentId
+        ) {
+            <StorageBureaucracy<T>>::ensure_worker_signed(origin, &storage_provider_id)?;
+
+            // == MUTATION SAFE ==
+
+            Self::update_content_judgement(&storage_provider_id, content_id, LiaisonJudgement::Accepted)?;
+
             <KnownContentIds<T>>::mutate(|ids| ids.push(content_id));
-            Self::deposit_event(RawEvent::ContentAccepted(content_id, who));
-        }
 
-        fn reject_content(origin, content_id: T::ContentId) {
-            let who = ensure_signed(origin)?;
-            Self::update_content_judgement(&who, content_id, LiaisonJudgement::Rejected)?;
-            Self::deposit_event(RawEvent::ContentRejected(content_id, who));
+            Self::deposit_event(RawEvent::ContentAccepted(content_id, storage_provider_id));
         }
 
-        // Sudo methods
+        /// Storage provider rejects a content. Requires signed storage provider account and its id.
+        /// The LiaisonJudgement can be updated, but only by the liaison.
+        pub(crate) fn reject_content(
+            origin,
+            storage_provider_id: StorageProviderId<T>,
+            content_id: T::ContentId
+        ) {
+            <StorageBureaucracy<T>>::ensure_worker_signed(origin, &storage_provider_id)?;
 
-        fn set_primary_liaison_account_id(origin, account: T::AccountId) {
-            ensure_root(origin)?;
-            <PrimaryLiaisonAccountId<T>>::put(account);
-        }
+            // == MUTATION SAFE ==
 
-        fn unset_primary_liaison_account_id(origin) {
-            ensure_root(origin)?;
-            <PrimaryLiaisonAccountId<T>>::take();
+            Self::update_content_judgement(&storage_provider_id, content_id, LiaisonJudgement::Rejected)?;
+            Self::deposit_event(RawEvent::ContentRejected(content_id, storage_provider_id));
         }
 
+        // Sudo methods
+
+        /// Removes the content id from the list of known content ids. Requires root privileges.
         fn remove_known_content_id(origin, content_id: T::ContentId) {
             ensure_root(origin)?;
+
+            // == MUTATION SAFE ==
+
             let upd_content_ids: Vec<T::ContentId> = Self::known_content_ids()
                 .into_iter()
                 .filter(|&id| id != content_id)
@@ -189,43 +279,27 @@ decl_module! {
             <KnownContentIds<T>>::put(upd_content_ids);
         }
 
+        /// Sets the content id from the list of known content ids. Requires root privileges.
         fn set_known_content_id(origin, content_ids: Vec<T::ContentId>) {
             ensure_root(origin)?;
-            <KnownContentIds<T>>::put(content_ids);
-        }
-    }
-}
 
-impl<T: Trait> ContentIdExists<T> for Module<T> {
-    fn has_content(content_id: &T::ContentId) -> bool {
-        Self::data_object_by_content_id(*content_id).is_some()
-    }
+            // == MUTATION SAFE ==
 
-    fn get_data_object(content_id: &T::ContentId) -> Result<DataObject<T>, &'static str> {
-        match Self::data_object_by_content_id(*content_id) {
-            Some(data) => Ok(data),
-            None => Err(MSG_CID_NOT_FOUND),
+            <KnownContentIds<T>>::put(content_ids);
         }
     }
 }
 
 impl<T: Trait> Module<T> {
-    fn current_block_and_time() -> BlockAndTime<T> {
-        BlockAndTime {
-            block: <system::Module<T>>::block_number(),
-            time: <timestamp::Module<T>>::now(),
-        }
-    }
-
     fn update_content_judgement(
-        who: &T::AccountId,
+        storage_provider_id: &StorageProviderId<T>,
         content_id: T::ContentId,
         judgement: LiaisonJudgement,
-    ) -> dispatch::Result {
-        let mut data = Self::data_object_by_content_id(&content_id).ok_or(MSG_CID_NOT_FOUND)?;
+    ) -> Result<(), Error> {
+        let mut data = Self::data_object_by_content_id(&content_id).ok_or(Error::CidNotFound)?;
 
         // Make sure the liaison matches
-        ensure!(data.liaison == *who, MSG_LIAISON_REQUIRED);
+        ensure!(data.liaison == *storage_provider_id, Error::LiaisonRequired);
 
         data.liaison_judgement = judgement;
         <DataObjectByContentId<T>>::insert(content_id, data);
@@ -234,93 +308,30 @@ impl<T: Trait> Module<T> {
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use crate::mock::*;
-
-    #[test]
-    fn succeed_adding_content() {
-        with_default_mock_builder(|| {
-            let sender = 1 as u64;
-            // Register a content with 1234 bytes of type 1, which should be recognized.
-            let res = TestDataDirectory::add_content(
-                Origin::signed(sender),
-                1,
-                1234,
-                0,
-                vec![1, 3, 3, 7],
-            );
-            assert!(res.is_ok());
-        });
-    }
+/// Provides random storage provider id. We use it when assign the content to the storage provider.
+pub trait StorageProviderHelper<T: Trait> {
+    /// Provides random storage provider id.
+    fn get_random_storage_provider() -> Result<StorageProviderId<T>, &'static str>;
+}
 
-    #[test]
-    fn accept_content_as_liaison() {
-        with_default_mock_builder(|| {
-            let sender = 1 as u64;
-            let res = TestDataDirectory::add_content(
-                Origin::signed(sender),
-                1,
-                1234,
-                0,
-                vec![1, 2, 3, 4],
-            );
-            assert!(res.is_ok());
-
-            // An appropriate event should have been fired.
-            let (content_id, creator) = match System::events().last().unwrap().event {
-                MetaEvent::data_directory(data_directory::RawEvent::ContentAdded(
-                    content_id,
-                    creator,
-                )) => (content_id, creator),
-                _ => (0u64, 0xdeadbeefu64), // invalid value, unlikely to match
-            };
-            assert_ne!(creator, 0xdeadbeefu64);
-            assert_eq!(creator, sender);
-
-            // Accepting content should not work with some random origin
-            let res = TestDataDirectory::accept_content(Origin::signed(1), content_id);
-            assert!(res.is_err());
-
-            // However, with the liaison as origin it should.
-            let res =
-                TestDataDirectory::accept_content(Origin::signed(TEST_MOCK_LIAISON), content_id);
-            assert!(res.is_ok());
-        });
+/// Content access helper.
+pub trait ContentIdExists<T: Trait> {
+    /// Verifies the content existence.
+    fn has_content(id: &T::ContentId) -> bool;
+
+    /// Returns the data object for the provided content id.
+    fn get_data_object(id: &T::ContentId) -> Result<DataObject<T>, &'static str>;
+}
+
+impl<T: Trait> ContentIdExists<T> for Module<T> {
+    fn has_content(content_id: &T::ContentId) -> bool {
+        Self::data_object_by_content_id(*content_id).is_some()
     }
 
-    #[test]
-    fn reject_content_as_liaison() {
-        with_default_mock_builder(|| {
-            let sender = 1 as u64;
-            let res = TestDataDirectory::add_content(
-                Origin::signed(sender),
-                1,
-                1234,
-                0,
-                vec![1, 2, 3, 4],
-            );
-            assert!(res.is_ok());
-
-            // An appropriate event should have been fired.
-            let (content_id, creator) = match System::events().last().unwrap().event {
-                MetaEvent::data_directory(data_directory::RawEvent::ContentAdded(
-                    content_id,
-                    creator,
-                )) => (content_id, creator),
-                _ => (0u64, 0xdeadbeefu64), // invalid value, unlikely to match
-            };
-            assert_ne!(creator, 0xdeadbeefu64);
-            assert_eq!(creator, sender);
-
-            // Rejecting content should not work with some random origin
-            let res = TestDataDirectory::reject_content(Origin::signed(1), content_id);
-            assert!(res.is_err());
-
-            // However, with the liaison as origin it should.
-            let res =
-                TestDataDirectory::reject_content(Origin::signed(TEST_MOCK_LIAISON), content_id);
-            assert!(res.is_ok());
-        });
+    fn get_data_object(content_id: &T::ContentId) -> Result<DataObject<T>, &'static str> {
+        match Self::data_object_by_content_id(*content_id) {
+            Some(data) => Ok(data),
+            None => Err(Error::LiaisonRequired.into()),
+        }
     }
 }

+ 148 - 187
runtime-modules/storage/src/data_object_storage_registry.rs

@@ -1,21 +1,48 @@
-// Clippy linter requirement
-#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design
-                                          // example:  pub NextRelationshipId get(next_relationship_id) build(|config: &GenesisConfig<T>|
+//! # Data object storage registry module
+//! Data object storage registry module for the Joystream platform allows to set relationships
+//! between the content and the storage providers. All extrinsics require storage working group registration.
+//!
+//! ## Comments
+//!
+//! Data object storage registry module uses bureaucracy module to authorize actions.
+//! Only registered storage providers can call extrinsics.
+//!
+//! ## Supported extrinsics
+//!
+//! - [add_relationship](./struct.Module.html#method.add_relationship) - Add storage provider-to-content relationship.
+//! - [set_relationship_ready](./struct.Module.html#method.set_relationship_ready)- Activates storage provider-to-content relationship.
+//! - [unset_relationship_ready](./struct.Module.html#method.unset_relationship_ready) - Deactivates storage provider-to-content relationship.
+//!
+
+// Clippy linter requirement.
+// Disable it because of the substrate lib design. Example:
+//  pub NextRelationshipId get(next_relationship_id) build(|config: &GenesisConfig<T>|
+#![allow(clippy::redundant_closure_call)]
+
+// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
+//#![warn(missing_docs)]
 
-use crate::data_directory::Trait as DDTrait;
-use crate::traits::{ContentHasStorage, ContentIdExists};
 use codec::{Codec, Decode, Encode};
-use roles::actors;
-use roles::traits::Roles;
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
-use srml_support::{decl_event, decl_module, decl_storage, ensure, Parameter};
-use system::{self, ensure_signed};
+use srml_support::{decl_error, decl_event, decl_module, decl_storage, ensure, Parameter};
 
-pub trait Trait: timestamp::Trait + system::Trait + DDTrait {
+use crate::data_directory::{self, ContentIdExists};
+use crate::{StorageBureaucracy, StorageProviderId};
+
+const DEFAULT_FIRST_RELATIONSHIP_ID: u32 = 1;
+
+/// The _Data object storage registry_ main _Trait_.
+pub trait Trait:
+    timestamp::Trait
+    + system::Trait
+    + data_directory::Trait
+    + bureaucracy::Trait<bureaucracy::Instance2>
+{
+    /// _Data object storage registry_ event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
-    // TODO deprecated
+    /// Type for data object storage relationship id
     type DataObjectStorageRelationshipId: Parameter
         + Member
         + SimpleArithmetic
@@ -25,130 +52,133 @@ pub trait Trait: timestamp::Trait + system::Trait + DDTrait {
         + MaybeSerialize
         + PartialEq;
 
-    type Roles: Roles<Self>;
-    type ContentIdExists: ContentIdExists<Self>;
+    /// Ensures that a content exists
+    type ContentIdExists: data_directory::ContentIdExists<Self>;
 }
 
-static MSG_CID_NOT_FOUND: &str = "Content with this ID not found.";
-static MSG_DOSR_NOT_FOUND: &str = "No data object storage relationship found for this ID.";
-static MSG_ONLY_STORAGE_PROVIDER_MAY_CREATE_DOSR: &str =
-    "Only storage providers can create data object storage relationships.";
-static MSG_ONLY_STORAGE_PROVIDER_MAY_CLAIM_READY: &str =
-    "Only the storage provider in a DOSR can decide whether they're ready.";
+decl_error! {
+    /// _Data object storage registry_ module predefined errors
+    pub enum Error {
+        /// Content with this ID not found.
+        CidNotFound,
 
-// TODO deprecated
-const DEFAULT_FIRST_RELATIONSHIP_ID: u32 = 1;
+        /// No data object storage relationship found for this ID.
+        DataObjectStorageRelationshipNotFound,
 
-// TODO deprecated
+        /// Only the storage provider in a DOSR can decide whether they're ready.
+        OnlyStorageProviderMayClaimReady,
+
+        /// Require root origin in extrinsics
+        RequireRootOrigin,
+    }
+}
+
+impl From<system::Error> for Error {
+    fn from(error: system::Error) -> Self {
+        match error {
+            system::Error::Other(msg) => Error::Other(msg),
+            system::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+impl From<bureaucracy::Error> for Error {
+    fn from(error: bureaucracy::Error) -> Self {
+        match error {
+            bureaucracy::Error::Other(msg) => Error::Other(msg),
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+/// Defines a relationship between the content and the storage provider
 #[derive(Clone, Encode, Decode, PartialEq, Debug)]
 pub struct DataObjectStorageRelationship<T: Trait> {
-    pub content_id: <T as DDTrait>::ContentId,
-    pub storage_provider: T::AccountId,
+    /// Content id.
+    pub content_id: <T as data_directory::Trait>::ContentId,
+
+    /// Storge provider id.
+    pub storage_provider_id: StorageProviderId<T>,
+
+    /// Active state (True=Active)
     pub ready: bool,
 }
 
 decl_storage! {
     trait Store for Module<T: Trait> as DataObjectStorageRegistry {
 
-        // TODO deprecated
-        // Start at this value
-        pub FirstRelationshipId get(first_relationship_id) config(first_relationship_id): T::DataObjectStorageRelationshipId = T::DataObjectStorageRelationshipId::from(DEFAULT_FIRST_RELATIONSHIP_ID);
+        /// Defines first relationship id.
+        pub FirstRelationshipId get(first_relationship_id) config(first_relationship_id):
+            T::DataObjectStorageRelationshipId = T::DataObjectStorageRelationshipId::from(DEFAULT_FIRST_RELATIONSHIP_ID);
 
-        // TODO deprecated
-        // Increment
+        /// Defines next relationship id.
         pub NextRelationshipId get(next_relationship_id) build(|config: &GenesisConfig<T>| config.first_relationship_id): T::DataObjectStorageRelationshipId = T::DataObjectStorageRelationshipId::from(DEFAULT_FIRST_RELATIONSHIP_ID);
 
-        // TODO deprecated
-        // Mapping of Data object types
+        /// Mapping of Data object types
         pub Relationships get(relationships): map T::DataObjectStorageRelationshipId => Option<DataObjectStorageRelationship<T>>;
 
-        // TODO deprecated
-        // Keep a list of storage relationships per CID
+        /// Keeps a list of storage relationships per content id.
         pub RelationshipsByContentId get(relationships_by_content_id): map T::ContentId => Vec<T::DataObjectStorageRelationshipId>;
-
-        // ------------------------------------------
-        // TODO use next storage items insteam:
-
-        // TODO save only if metadata exists and there is at least one relation w/ ready == true.
-        ReadyContentIds get(ready_content_ids): Vec<T::ContentId> = vec![];
-
-        // TODO need? it can be expressed via StorageProvidersByContentId
-        pub StorageProviderServesContent get(storage_provider_serves_content):
-            map (T::AccountId, T::ContentId) => bool;
-
-        pub StorageProvidersByContentId get(storage_providers_by_content_id):
-            map T::ContentId => Vec<T::AccountId>;
     }
 }
 
 decl_event! {
+    /// _Data object storage registry_ events
     pub enum Event<T> where
-        <T as DDTrait>::ContentId,
+        <T as data_directory::Trait>::ContentId,
         <T as Trait>::DataObjectStorageRelationshipId,
-        <T as system::Trait>::AccountId
+        StorageProviderId = StorageProviderId<T>
     {
-        // TODO deprecated
-        DataObjectStorageRelationshipAdded(DataObjectStorageRelationshipId, ContentId, AccountId),
+        /// Emits on adding of the data object storage relationship.
+        /// Params:
+        /// - Id of the relationship.
+        /// - Id of the content.
+        /// - Id of the storage provider.
+        DataObjectStorageRelationshipAdded(DataObjectStorageRelationshipId, ContentId, StorageProviderId),
+
+        /// Emits on adding of the data object storage relationship.
+        /// Params:
+        /// - Id of the relationship.
+        /// - Current state of the relationship (True=Active).
         DataObjectStorageRelationshipReadyUpdated(DataObjectStorageRelationshipId, bool),
-
-        // NEW & COOL
-        StorageProviderAddedContent(AccountId, ContentId),
-        StorageProviderRemovedContent(AccountId, ContentId),
-    }
-}
-
-impl<T: Trait> ContentHasStorage<T> for Module<T> {
-    // TODO deprecated
-    fn has_storage_provider(which: &T::ContentId) -> bool {
-        let dosr_list = Self::relationships_by_content_id(which);
-        dosr_list.iter().any(|&dosr_id| {
-            let res = Self::relationships(dosr_id);
-            if res.is_none() {
-                return false;
-            }
-            let dosr = res.unwrap();
-            dosr.ready
-        })
-    }
-
-    // TODO deprecated
-    fn is_ready_at_storage_provider(which: &T::ContentId, provider: &T::AccountId) -> bool {
-        let dosr_list = Self::relationships_by_content_id(which);
-        dosr_list.iter().any(|&dosr_id| {
-            let res = Self::relationships(dosr_id);
-            if res.is_none() {
-                return false;
-            }
-            let dosr = res.unwrap();
-            dosr.storage_provider == *provider && dosr.ready
-        })
     }
 }
 
 decl_module! {
+    /// _Data object storage registry_ substrate module.
     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Default deposit_event() handler.
         fn deposit_event() = default;
 
-        pub fn add_relationship(origin, cid: T::ContentId) {
-            // Origin has to be a storage provider
-            let who = ensure_signed(origin)?;
+        /// Predefined errors.
+        type Error = Error;
 
-            // Check that the origin is a storage provider
-            ensure!(<T as Trait>::Roles::account_has_role(&who, actors::Role::StorageProvider), MSG_ONLY_STORAGE_PROVIDER_MAY_CREATE_DOSR);
+        /// Add storage provider-to-content relationship. The storage provider should be registered
+        /// in the storage working group.
+        pub fn add_relationship(origin, storage_provider_id: StorageProviderId<T>, cid: T::ContentId) {
+            // Origin should match storage provider.
+            <StorageBureaucracy<T>>::ensure_worker_signed(origin, &storage_provider_id)?;
 
             // Content ID must exist
-            ensure!(T::ContentIdExists::has_content(&cid), MSG_CID_NOT_FOUND);
+            ensure!(T::ContentIdExists::has_content(&cid), Error::CidNotFound);
 
             // Create new ID, data.
             let new_id = Self::next_relationship_id();
             let dosr: DataObjectStorageRelationship<T> = DataObjectStorageRelationship {
                 content_id: cid,
-                storage_provider: who.clone(),
+                storage_provider_id,
                 ready: false,
             };
 
+            //
+            // == MUTATION SAFE ==
+            //
+
             <Relationships<T>>::insert(new_id, dosr);
-            <NextRelationshipId<T>>::mutate(|n| { *n += T::DataObjectStorageRelationshipId::from(1); });
+            <NextRelationshipId<T>>::mutate(|n| {
+                *n += T::DataObjectStorageRelationshipId::from(1);
+            });
 
             // Also add the DOSR to the list of DOSRs for the CID. Uniqueness is guaranteed
             // by the map, so we can just append the new_id to the list.
@@ -157,16 +187,29 @@ decl_module! {
             <RelationshipsByContentId<T>>::insert(cid, dosr_list);
 
             // Emit event
-            Self::deposit_event(RawEvent::DataObjectStorageRelationshipAdded(new_id, cid, who));
+            Self::deposit_event(
+                RawEvent::DataObjectStorageRelationshipAdded(new_id, cid, storage_provider_id)
+            );
         }
 
-        // A storage provider may flip their own ready state, but nobody else.
-        pub fn set_relationship_ready(origin, id: T::DataObjectStorageRelationshipId) {
-            Self::toggle_dosr_ready(origin, id, true)?;
+        /// Activates storage provider-to-content relationship. The storage provider should be registered
+        /// in the storage working group. A storage provider may flip their own ready state, but nobody else.
+        pub fn set_relationship_ready(
+            origin,
+            storage_provider_id: StorageProviderId<T>,
+            id: T::DataObjectStorageRelationshipId
+        ) {
+            Self::toggle_dosr_ready(origin, storage_provider_id, id, true)?;
         }
 
-        pub fn unset_relationship_ready(origin, id: T::DataObjectStorageRelationshipId) {
-            Self::toggle_dosr_ready(origin, id, false)?;
+        /// Deactivates storage provider-to-content relationship. The storage provider should be registered
+        /// in the storage working group. A storage provider may flip their own ready state, but nobody else.
+        pub fn unset_relationship_ready(
+            origin,
+            storage_provider_id: StorageProviderId<T>,
+            id: T::DataObjectStorageRelationshipId
+        ) {
+            Self::toggle_dosr_ready(origin, storage_provider_id, id, false)?;
         }
     }
 }
@@ -174,17 +217,19 @@ decl_module! {
 impl<T: Trait> Module<T> {
     fn toggle_dosr_ready(
         origin: T::Origin,
+        storage_provider_id: StorageProviderId<T>,
         id: T::DataObjectStorageRelationshipId,
         ready: bool,
-    ) -> Result<(), &'static str> {
-        // Origin has to be the storage provider mentioned in the DOSR
-        let who = ensure_signed(origin)?;
+    ) -> Result<(), Error> {
+        <StorageBureaucracy<T>>::ensure_worker_signed(origin, &storage_provider_id)?;
 
         // For that, we need to fetch the identified DOSR
-        let mut dosr = Self::relationships(id).ok_or(MSG_DOSR_NOT_FOUND)?;
+        let mut dosr =
+            Self::relationships(id).ok_or(Error::DataObjectStorageRelationshipNotFound)?;
+
         ensure!(
-            dosr.storage_provider == who,
-            MSG_ONLY_STORAGE_PROVIDER_MAY_CLAIM_READY
+            dosr.storage_provider_id == storage_provider_id,
+            Error::OnlyStorageProviderMayClaimReady
         );
 
         // Flip to ready
@@ -193,93 +238,9 @@ impl<T: Trait> Module<T> {
         // Update DOSR and fire event.
         <Relationships<T>>::insert(id, dosr);
         Self::deposit_event(RawEvent::DataObjectStorageRelationshipReadyUpdated(
-            id, true,
+            id, ready,
         ));
 
         Ok(())
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use crate::mock::*;
-
-    #[test]
-    fn initial_state() {
-        with_default_mock_builder(|| {
-            assert_eq!(
-                TestDataObjectStorageRegistry::first_relationship_id(),
-                TEST_FIRST_RELATIONSHIP_ID
-            );
-        });
-    }
-
-    #[test]
-    fn test_add_relationship() {
-        with_default_mock_builder(|| {
-            // The content needs to exist - in our mock, that's with the content ID TEST_MOCK_EXISTING_CID
-            let res = TestDataObjectStorageRegistry::add_relationship(
-                Origin::signed(TEST_MOCK_LIAISON),
-                TEST_MOCK_EXISTING_CID,
-            );
-            assert!(res.is_ok());
-        });
-    }
-
-    #[test]
-    fn test_fail_adding_relationship_with_bad_content() {
-        with_default_mock_builder(|| {
-            let res = TestDataObjectStorageRegistry::add_relationship(Origin::signed(1), 24);
-            assert!(res.is_err());
-        });
-    }
-
-    #[test]
-    fn test_toggle_ready() {
-        with_default_mock_builder(|| {
-            // Create a DOSR
-            let res = TestDataObjectStorageRegistry::add_relationship(
-                Origin::signed(TEST_MOCK_LIAISON),
-                TEST_MOCK_EXISTING_CID,
-            );
-            assert!(res.is_ok());
-
-            // Grab DOSR ID from event
-            let dosr_id = match System::events().last().unwrap().event {
-                MetaEvent::data_object_storage_registry(
-                    data_object_storage_registry::RawEvent::DataObjectStorageRelationshipAdded(
-                        dosr_id,
-                        _content_id,
-                        _account_id,
-                    ),
-                ) => dosr_id,
-                _ => 0xdeadbeefu64, // invalid value, unlikely to match
-            };
-            assert_ne!(dosr_id, 0xdeadbeefu64);
-
-            // Toggling from a different account should fail
-            let res =
-                TestDataObjectStorageRegistry::set_relationship_ready(Origin::signed(2), dosr_id);
-            assert!(res.is_err());
-
-            // Toggling with the wrong ID should fail.
-            let res = TestDataObjectStorageRegistry::set_relationship_ready(
-                Origin::signed(TEST_MOCK_LIAISON),
-                dosr_id + 1,
-            );
-            assert!(res.is_err());
-
-            // Toggling with the correct ID and origin should succeed
-            let res = TestDataObjectStorageRegistry::set_relationship_ready(
-                Origin::signed(TEST_MOCK_LIAISON),
-                dosr_id,
-            );
-            assert!(res.is_ok());
-            assert_eq!(System::events().last().unwrap().event,
-                MetaEvent::data_object_storage_registry(data_object_storage_registry::RawEvent::DataObjectStorageRelationshipReadyUpdated(
-                    dosr_id,
-                    true,
-                )));
-        });
-    }
-}

+ 127 - 205
runtime-modules/storage/src/data_object_type_registry.rs

@@ -1,17 +1,42 @@
-// Clippy linter requirement
-#![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design
-                                          // example:   NextDataObjectTypeId get(next_data_object_type_id) build(|config: &GenesisConfig<T>|
-
-use crate::traits;
+//! # Data object type registry module
+//! Data object type registry module for the Joystream platform allows to set constraints for the data objects. All extrinsics require leader.
+//!
+//! ## Comments
+//!
+//! Data object type registry module uses bureaucracy module to authorize actions. Only leader can
+//! call extrinsics.
+//!
+//! ## Supported extrinsics
+//!
+//! - [register_data_object_type](./struct.Module.html#method.register_data_object_type) - Registers the new data object type.
+//! - [update_data_object_type](./struct.Module.html#method.update_data_object_type)- Updates existing data object type.
+//! - [activate_data_object_type](./struct.Module.html#method.activate_data_object_type) -  Activates existing data object type.
+//! - [deactivate_data_object_type](./struct.Module.html#method.deactivate_data_object_type) -  Deactivates existing data object type.
+//!
+
+// Clippy linter requirement.
+// Disable it because of the substrate lib design. Example:
+//   NextDataObjectTypeId get(next_data_object_type_id) build(|config: &GenesisConfig<T>|
+#![allow(clippy::redundant_closure_call)]
+
+// Do not delete! Cannot be uncommented by default, because of Parity decl_module! issue.
+//#![warn(missing_docs)]
+
+use crate::StorageBureaucracy;
 use codec::{Codec, Decode, Encode};
 use rstd::prelude::*;
 use sr_primitives::traits::{MaybeSerialize, Member, SimpleArithmetic};
-use srml_support::{decl_event, decl_module, decl_storage, Parameter};
-use system::ensure_root;
+use srml_support::{decl_error, decl_event, decl_module, decl_storage, Parameter};
+
+const DEFAULT_TYPE_DESCRIPTION: &str = "Default data object type for audio and video content.";
+const DEFAULT_FIRST_DATA_OBJECT_TYPE_ID: u32 = 1;
 
-pub trait Trait: system::Trait {
+/// The _Data object type registry_ main _Trait_.
+pub trait Trait: system::Trait + bureaucracy::Trait<bureaucracy::Instance2> {
+    /// _Data object type registry_ event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
+    /// _Data object type id_ type
     type DataObjectTypeId: Parameter
         + Member
         + SimpleArithmetic
@@ -22,73 +47,98 @@ pub trait Trait: system::Trait {
         + PartialEq;
 }
 
-static MSG_DO_TYPE_NOT_FOUND: &str = "Data Object Type with the given ID not found.";
+decl_error! {
+    /// _Data object type registry_ module predefined errors
+    pub enum Error {
+        /// Data Object Type with the given ID not found.
+        DataObjectTypeNotFound,
 
-const DEFAULT_TYPE_DESCRIPTION: &str = "Default data object type for audio and video content.";
-const DEFAULT_TYPE_ACTIVE: bool = true;
-const CREATE_DETAULT_TYPE: bool = true;
+        /// Require root origin in extrinsics
+        RequireRootOrigin,
+    }
+}
 
-const DEFAULT_FIRST_DATA_OBJECT_TYPE_ID: u32 = 1;
+impl From<system::Error> for Error {
+    fn from(error: system::Error) -> Self {
+        match error {
+            system::Error::Other(msg) => Error::Other(msg),
+            system::Error::RequireRootOrigin => Error::RequireRootOrigin,
+            _ => Error::Other(error.into()),
+        }
+    }
+}
 
+impl From<bureaucracy::Error> for Error {
+    fn from(error: bureaucracy::Error) -> Self {
+        match error {
+            bureaucracy::Error::Other(msg) => Error::Other(msg),
+            _ => Error::Other(error.into()),
+        }
+    }
+}
+
+/// Contains description and constrains for the data object.
 #[derive(Clone, Encode, Decode, PartialEq, Debug)]
 pub struct DataObjectType {
+    /// Data object description.
     pub description: Vec<u8>,
+
+    /// Active/Disabled flag.
     pub active: bool,
-    // TODO in future releases
-    // - maximum size
-    // - replication factor
-    // - storage tranches (empty is ok)
 }
 
 impl Default for DataObjectType {
     fn default() -> Self {
         DataObjectType {
             description: DEFAULT_TYPE_DESCRIPTION.as_bytes().to_vec(),
-            active: DEFAULT_TYPE_ACTIVE,
+            active: true,
         }
     }
 }
 
 decl_storage! {
     trait Store for Module<T: Trait> as DataObjectTypeRegistry {
+        /// Data object type ids should start at this value.
+        pub FirstDataObjectTypeId get(first_data_object_type_id) config(first_data_object_type_id):
+            T::DataObjectTypeId = T::DataObjectTypeId::from(DEFAULT_FIRST_DATA_OBJECT_TYPE_ID);
 
-        // TODO hardcode data object type for ID 1
+        /// Provides id counter for the data object types.
+        pub NextDataObjectTypeId get(next_data_object_type_id) build(|config: &GenesisConfig<T>|
+            config.first_data_object_type_id): T::DataObjectTypeId = T::DataObjectTypeId::from(DEFAULT_FIRST_DATA_OBJECT_TYPE_ID);
 
-        // Start at this value
-        pub FirstDataObjectTypeId get(first_data_object_type_id) config(first_data_object_type_id): T::DataObjectTypeId = T::DataObjectTypeId::from(DEFAULT_FIRST_DATA_OBJECT_TYPE_ID);
-
-        // Increment
-        pub NextDataObjectTypeId get(next_data_object_type_id) build(|config: &GenesisConfig<T>| config.first_data_object_type_id): T::DataObjectTypeId = T::DataObjectTypeId::from(DEFAULT_FIRST_DATA_OBJECT_TYPE_ID);
-
-        // Mapping of Data object types
+        /// Mapping of Data object types.
         pub DataObjectTypes get(data_object_types): map T::DataObjectTypeId => Option<DataObjectType>;
     }
 }
 
 decl_event! {
+    /// _Data object type registry_ events
     pub enum Event<T> where
         <T as Trait>::DataObjectTypeId {
+        /// Emits on the data object type registration.
+        /// Params:
+        /// - Id of the new data object type.
         DataObjectTypeRegistered(DataObjectTypeId),
-        DataObjectTypeUpdated(DataObjectTypeId),
-    }
-}
 
-impl<T: Trait> traits::IsActiveDataObjectType<T> for Module<T> {
-    fn is_active_data_object_type(which: &T::DataObjectTypeId) -> bool {
-        match Self::ensure_data_object_type(*which) {
-            Ok(do_type) => do_type.active,
-            Err(_err) => false,
-        }
+        /// Emits on the data object type update.
+        /// Params:
+        /// - Id of the updated data object type.
+        DataObjectTypeUpdated(DataObjectTypeId),
     }
 }
 
 decl_module! {
+    /// _Data object type registry_ substrate module.
     pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        /// Default deposit_event() handler
         fn deposit_event() = default;
 
+        /// Predefined errors
+        type Error = Error;
+
         fn on_initialize() {
             // Create a default data object type if it was not created yet.
-            if CREATE_DETAULT_TYPE && !<DataObjectTypes<T>>::exists(Self::first_data_object_type_id()) {
+            if !<DataObjectTypes<T>>::exists(Self::first_data_object_type_id()) {
                 let do_type: DataObjectType = DataObjectType::default();
                 let new_type_id = Self::next_data_object_type_id();
 
@@ -97,54 +147,73 @@ decl_module! {
             }
         }
 
+        /// Registers the new data object type. Requires leader privileges.
         pub fn register_data_object_type(origin, data_object_type: DataObjectType) {
-            ensure_root(origin)?;
+            <StorageBureaucracy<T>>::ensure_origin_is_active_leader(origin)?;
+
             let new_do_type_id = Self::next_data_object_type_id();
             let do_type: DataObjectType = DataObjectType {
                 description: data_object_type.description.clone(),
                 active: data_object_type.active,
             };
 
+            //
+            // == MUTATION SAFE ==
+            //
+
             <DataObjectTypes<T>>::insert(new_do_type_id, do_type);
             <NextDataObjectTypeId<T>>::mutate(|n| { *n += T::DataObjectTypeId::from(1); });
 
             Self::deposit_event(RawEvent::DataObjectTypeRegistered(new_do_type_id));
         }
 
-        // TODO use DataObjectTypeUpdate
+        /// Updates existing data object type. Requires leader privileges.
         pub fn update_data_object_type(origin, id: T::DataObjectTypeId, data_object_type: DataObjectType) {
-            ensure_root(origin)?;
+            <StorageBureaucracy<T>>::ensure_origin_is_active_leader(origin)?;
+
             let mut do_type = Self::ensure_data_object_type(id)?;
 
             do_type.description = data_object_type.description.clone();
             do_type.active = data_object_type.active;
 
+            //
+            // == MUTATION SAFE ==
+            //
+
             <DataObjectTypes<T>>::insert(id, do_type);
 
             Self::deposit_event(RawEvent::DataObjectTypeUpdated(id));
         }
 
-        // Activate and deactivate functions as separate functions, because
-        // toggling DO types is likely a more common operation than updating
-        // other aspects.
-        // TODO deprecate or express via update_data_type
+        /// Activates existing data object type. Requires leader privileges.
         pub fn activate_data_object_type(origin, id: T::DataObjectTypeId) {
-            ensure_root(origin)?;
+            <StorageBureaucracy<T>>::ensure_origin_is_active_leader(origin)?;
+
             let mut do_type = Self::ensure_data_object_type(id)?;
 
             do_type.active = true;
 
+            //
+            // == MUTATION SAFE ==
+            //
+
             <DataObjectTypes<T>>::insert(id, do_type);
 
             Self::deposit_event(RawEvent::DataObjectTypeUpdated(id));
         }
 
+        /// Deactivates existing data object type. Requires leader privileges.
         pub fn deactivate_data_object_type(origin, id: T::DataObjectTypeId) {
-            ensure_root(origin)?;
+            <StorageBureaucracy<T>>::ensure_origin_is_active_leader(origin)?;
+
             let mut do_type = Self::ensure_data_object_type(id)?;
 
             do_type.active = false;
 
+            //
+            // == MUTATION SAFE ==
+            //
+
             <DataObjectTypes<T>>::insert(id, do_type);
 
             Self::deposit_event(RawEvent::DataObjectTypeUpdated(id));
@@ -153,169 +222,22 @@ decl_module! {
 }
 
 impl<T: Trait> Module<T> {
-    fn ensure_data_object_type(id: T::DataObjectTypeId) -> Result<DataObjectType, &'static str> {
-        Self::data_object_types(&id).ok_or(MSG_DO_TYPE_NOT_FOUND)
+    fn ensure_data_object_type(id: T::DataObjectTypeId) -> Result<DataObjectType, Error> {
+        Self::data_object_types(&id).ok_or(Error::DataObjectTypeNotFound)
     }
 }
 
-#[cfg(test)]
-mod tests {
-    //use super::*;
-    use crate::mock::*;
-
-    use system::{self, EventRecord, Phase};
-
-    #[test]
-    fn initial_state() {
-        with_default_mock_builder(|| {
-            assert_eq!(
-                TestDataObjectTypeRegistry::first_data_object_type_id(),
-                TEST_FIRST_DATA_OBJECT_TYPE_ID
-            );
-        });
-    }
-
-    #[test]
-    fn succeed_register() {
-        with_default_mock_builder(|| {
-            let data: TestDataObjectType = TestDataObjectType {
-                description: "foo".as_bytes().to_vec(),
-                active: false,
-            };
-            let res = TestDataObjectTypeRegistry::register_data_object_type(
-                system::RawOrigin::Root.into(),
-                data,
-            );
-            assert!(res.is_ok());
-        });
-    }
-
-    #[test]
-    fn update_existing() {
-        with_default_mock_builder(|| {
-            // First register a type
-            let data: TestDataObjectType = TestDataObjectType {
-                description: "foo".as_bytes().to_vec(),
-                active: false,
-            };
-            let id_res = TestDataObjectTypeRegistry::register_data_object_type(
-                system::RawOrigin::Root.into(),
-                data,
-            );
-            assert!(id_res.is_ok());
-
-            let dot_id = match System::events().last().unwrap().event {
-                MetaEvent::data_object_type_registry(
-                    data_object_type_registry::RawEvent::DataObjectTypeRegistered(dot_id),
-                ) => dot_id,
-                _ => 0xdeadbeefu64, // unlikely value
-            };
-            assert_ne!(dot_id, 0xdeadbeefu64);
-
-            // Now update it with new data - we need the ID to be the same as in
-            // returned by the previous call. First, though, try and fail with a bad ID
-            let updated1: TestDataObjectType = TestDataObjectType {
-                description: "bar".as_bytes().to_vec(),
-                active: false,
-            };
-            let res = TestDataObjectTypeRegistry::update_data_object_type(
-                system::RawOrigin::Root.into(),
-                dot_id + 1,
-                updated1,
-            );
-            assert!(res.is_err());
-
-            // Finally with an existing ID, it should work.
-            let updated3: TestDataObjectType = TestDataObjectType {
-                description: "bar".as_bytes().to_vec(),
-                active: false,
-            };
-            let res = TestDataObjectTypeRegistry::update_data_object_type(
-                system::RawOrigin::Root.into(),
-                dot_id,
-                updated3,
-            );
-            assert!(res.is_ok());
-            assert_eq!(
-                *System::events().last().unwrap(),
-                EventRecord {
-                    phase: Phase::ApplyExtrinsic(0),
-                    event: MetaEvent::data_object_type_registry(
-                        data_object_type_registry::RawEvent::DataObjectTypeUpdated(dot_id)
-                    ),
-                    topics: vec![],
-                }
-            );
-        });
-    }
+/// Active data object type validator trait.
+pub trait IsActiveDataObjectType<T: Trait> {
+    /// Ensures that data object type with given id is active.
+    fn is_active_data_object_type(id: &T::DataObjectTypeId) -> bool;
+}
 
-    #[test]
-    fn activate_existing() {
-        with_default_mock_builder(|| {
-            // First register a type
-            let data: TestDataObjectType = TestDataObjectType {
-                description: "foo".as_bytes().to_vec(),
-                active: false,
-            };
-            let id_res = TestDataObjectTypeRegistry::register_data_object_type(
-                system::RawOrigin::Root.into(),
-                data,
-            );
-            assert!(id_res.is_ok());
-            assert_eq!(
-                *System::events().last().unwrap(),
-                EventRecord {
-                    phase: Phase::ApplyExtrinsic(0),
-                    event: MetaEvent::data_object_type_registry(
-                        data_object_type_registry::RawEvent::DataObjectTypeRegistered(
-                            TEST_FIRST_DATA_OBJECT_TYPE_ID
-                        )
-                    ),
-                    topics: vec![],
-                }
-            );
-
-            // Retrieve, and ensure it's not active.
-            let data =
-                TestDataObjectTypeRegistry::data_object_types(TEST_FIRST_DATA_OBJECT_TYPE_ID);
-            assert!(data.is_some());
-            assert!(!data.unwrap().active);
-
-            // Now activate the data object type
-            let res = TestDataObjectTypeRegistry::activate_data_object_type(
-                system::RawOrigin::Root.into(),
-                TEST_FIRST_DATA_OBJECT_TYPE_ID,
-            );
-            assert!(res.is_ok());
-            assert_eq!(
-                *System::events().last().unwrap(),
-                EventRecord {
-                    phase: Phase::ApplyExtrinsic(0),
-                    event: MetaEvent::data_object_type_registry(
-                        data_object_type_registry::RawEvent::DataObjectTypeUpdated(
-                            TEST_FIRST_DATA_OBJECT_TYPE_ID
-                        )
-                    ),
-                    topics: vec![],
-                }
-            );
-
-            // Ensure that the item is actually activated.
-            let data =
-                TestDataObjectTypeRegistry::data_object_types(TEST_FIRST_DATA_OBJECT_TYPE_ID);
-            assert!(data.is_some());
-            assert!(data.unwrap().active);
-
-            // Deactivate again.
-            let res = TestDataObjectTypeRegistry::deactivate_data_object_type(
-                system::RawOrigin::Root.into(),
-                TEST_FIRST_DATA_OBJECT_TYPE_ID,
-            );
-            assert!(res.is_ok());
-            let data =
-                TestDataObjectTypeRegistry::data_object_types(TEST_FIRST_DATA_OBJECT_TYPE_ID);
-            assert!(data.is_some());
-            assert!(!data.unwrap().active);
-        });
+impl<T: Trait> IsActiveDataObjectType<T> for Module<T> {
+    fn is_active_data_object_type(id: &T::DataObjectTypeId) -> bool {
+        match Self::ensure_data_object_type(*id) {
+            Ok(do_type) => do_type.active,
+            Err(_err) => false,
+        }
     }
 }

+ 10 - 2
runtime-modules/storage/src/lib.rs

@@ -4,6 +4,14 @@
 pub mod data_directory;
 pub mod data_object_storage_registry;
 pub mod data_object_type_registry;
-pub mod traits;
 
-mod mock;
+mod tests;
+
+// Alias for storage working group bureaucracy
+pub(crate) type StorageBureaucracy<T> = bureaucracy::Module<T, bureaucracy::Instance2>;
+
+// Alias for the member id.
+pub(crate) type MemberId<T> = <T as membership::members::Trait>::MemberId;
+
+/// Storage provider is a worker from the bureaucracy module.
+pub type StorageProviderId<T> = bureaucracy::WorkerId<T>;

+ 0 - 206
runtime-modules/storage/src/tests.rs

@@ -1,206 +0,0 @@
-#![cfg(test)]
-
-use super::mock::*;
-use super::*;
-
-use runtime_io::with_externalities;
-use system::{self, EventRecord, Phase};
-
-#[test]
-fn initial_state() {
-    const DEFAULT_FIRST_ID: u64 = 1000;
-
-    with_externalities(
-        &mut ExtBuilder::default()
-            .first_data_object_type_id(DEFAULT_FIRST_ID)
-            .build(),
-        || {
-            assert_eq!(
-                TestDataObjectTypeRegistry::first_data_object_type_id(),
-                DEFAULT_FIRST_ID
-            );
-        },
-    );
-}
-
-#[test]
-fn fail_register_without_root() {
-    const DEFAULT_FIRST_ID: u64 = 1000;
-
-    with_externalities(
-        &mut ExtBuilder::default()
-            .first_data_object_type_id(DEFAULT_FIRST_ID)
-            .build(),
-        || {
-            let data: TestDataObjectType = TestDataObjectType {
-                id: None,
-                description: "foo".as_bytes().to_vec(),
-                active: false,
-            };
-            let res =
-                TestDataObjectTypeRegistry::register_data_object_type(Origin::signed(1), data);
-            assert!(res.is_err());
-        },
-    );
-}
-
-#[test]
-fn succeed_register_as_root() {
-    const DEFAULT_FIRST_ID: u64 = 1000;
-
-    with_externalities(
-        &mut ExtBuilder::default()
-            .first_data_object_type_id(DEFAULT_FIRST_ID)
-            .build(),
-        || {
-            let data: TestDataObjectType = TestDataObjectType {
-                id: None,
-                description: "foo".as_bytes().to_vec(),
-                active: false,
-            };
-            let res = TestDataObjectTypeRegistry::register_data_object_type(Origin::ROOT, data);
-            assert!(res.is_ok());
-        },
-    );
-}
-
-#[test]
-fn update_existing() {
-    const DEFAULT_FIRST_ID: u64 = 1000;
-
-    with_externalities(
-        &mut ExtBuilder::default()
-            .first_data_object_type_id(DEFAULT_FIRST_ID)
-            .build(),
-        || {
-            // First register a type
-            let data: TestDataObjectType = TestDataObjectType {
-                id: None,
-                description: "foo".as_bytes().to_vec(),
-                active: false,
-            };
-            let id_res = TestDataObjectTypeRegistry::register_data_object_type(Origin::ROOT, data);
-            assert!(id_res.is_ok());
-            assert_eq!(
-                *System::events().last().unwrap(),
-                EventRecord {
-                    phase: Phase::ApplyExtrinsic(0),
-                    event: MetaEvent::data_object_type_registry(
-                        data_object_type_registry::RawEvent::DataObjectTypeRegistered(
-                            DEFAULT_FIRST_ID
-                        )
-                    ),
-                }
-            );
-
-            // Now update it with new data - we need the ID to be the same as in
-            // returned by the previous call. First, though, try and fail without
-            let updated1: TestDataObjectType = TestDataObjectType {
-                id: None,
-                description: "bar".as_bytes().to_vec(),
-                active: false,
-            };
-            let res = TestDataObjectTypeRegistry::update_data_object_type(Origin::ROOT, updated1);
-            assert!(res.is_err());
-
-            // Now try with a bad ID
-            let updated2: TestDataObjectType = TestDataObjectType {
-                id: Some(DEFAULT_FIRST_ID + 1),
-                description: "bar".as_bytes().to_vec(),
-                active: false,
-            };
-            let res = TestDataObjectTypeRegistry::update_data_object_type(Origin::ROOT, updated2);
-            assert!(res.is_err());
-
-            // Finally with an existing ID, it should work.
-            let updated3: TestDataObjectType = TestDataObjectType {
-                id: Some(DEFAULT_FIRST_ID),
-                description: "bar".as_bytes().to_vec(),
-                active: false,
-            };
-            let res = TestDataObjectTypeRegistry::update_data_object_type(Origin::ROOT, updated3);
-            assert!(res.is_ok());
-            assert_eq!(
-                *System::events().last().unwrap(),
-                EventRecord {
-                    phase: Phase::ApplyExtrinsic(0),
-                    event: MetaEvent::data_object_type_registry(
-                        data_object_type_registry::RawEvent::DataObjectTypeUpdated(
-                            DEFAULT_FIRST_ID
-                        )
-                    ),
-                }
-            );
-        },
-    );
-}
-
-#[test]
-fn activate_existing() {
-    const DEFAULT_FIRST_ID: u64 = 1000;
-
-    with_externalities(
-        &mut ExtBuilder::default()
-            .first_data_object_type_id(DEFAULT_FIRST_ID)
-            .build(),
-        || {
-            // First register a type
-            let data: TestDataObjectType = TestDataObjectType {
-                id: None,
-                description: "foo".as_bytes().to_vec(),
-                active: false,
-            };
-            let id_res = TestDataObjectTypeRegistry::register_data_object_type(Origin::ROOT, data);
-            assert!(id_res.is_ok());
-            assert_eq!(
-                *System::events().last().unwrap(),
-                EventRecord {
-                    phase: Phase::ApplyExtrinsic(0),
-                    event: MetaEvent::data_object_type_registry(
-                        data_object_type_registry::RawEvent::DataObjectTypeRegistered(
-                            DEFAULT_FIRST_ID
-                        )
-                    ),
-                }
-            );
-
-            // Retrieve, and ensure it's not active.
-            let data = TestDataObjectTypeRegistry::data_object_type(DEFAULT_FIRST_ID);
-            assert!(data.is_some());
-            assert!(!data.unwrap().active);
-
-            // Now activate the data object type
-            let res = TestDataObjectTypeRegistry::activate_data_object_type(
-                Origin::ROOT,
-                DEFAULT_FIRST_ID,
-            );
-            assert!(res.is_ok());
-            assert_eq!(
-                *System::events().last().unwrap(),
-                EventRecord {
-                    phase: Phase::ApplyExtrinsic(0),
-                    event: MetaEvent::data_object_type_registry(
-                        data_object_type_registry::RawEvent::DataObjectTypeUpdated(
-                            DEFAULT_FIRST_ID
-                        )
-                    ),
-                }
-            );
-
-            // Ensure that the item is actually activated.
-            let data = TestDataObjectTypeRegistry::data_object_type(DEFAULT_FIRST_ID);
-            assert!(data.is_some());
-            assert!(data.unwrap().active);
-
-            // Deactivate again.
-            let res = TestDataObjectTypeRegistry::deactivate_data_object_type(
-                Origin::ROOT,
-                DEFAULT_FIRST_ID,
-            );
-            assert!(res.is_ok());
-            let data = TestDataObjectTypeRegistry::data_object_type(DEFAULT_FIRST_ID);
-            assert!(data.is_some());
-            assert!(!data.unwrap().active);
-        },
-    );
-}

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

@@ -0,0 +1,171 @@
+#![cfg(test)]
+
+use super::mock::*;
+use crate::data_directory::Error;
+use system::RawOrigin;
+
+#[test]
+fn succeed_adding_content() {
+    with_default_mock_builder(|| {
+        let sender = 1u64;
+        let member_id = 1u64;
+        // Register a content with 1234 bytes of type 1, which should be recognized.
+        let res = TestDataDirectory::add_content(
+            Origin::signed(sender),
+            member_id,
+            1,
+            1234,
+            0,
+            vec![1, 3, 3, 7],
+        );
+        assert!(res.is_ok());
+    });
+}
+
+#[test]
+fn add_content_fails_with_invalid_origin() {
+    with_default_mock_builder(|| {
+        let member_id = 1u64;
+        // Register a content with 1234 bytes of type 1, which should be recognized.
+        let res = TestDataDirectory::add_content(
+            RawOrigin::Root.into(),
+            member_id,
+            1,
+            1234,
+            0,
+            vec![1, 3, 3, 7],
+        );
+        assert_eq!(res, Err(Error::Other("RequireSignedOrigin")));
+    });
+}
+
+#[test]
+fn accept_and_reject_content_fail_with_invalid_storage_provider() {
+    with_default_mock_builder(|| {
+        let sender = 1u64;
+        let member_id = 1u64;
+
+        let res = TestDataDirectory::add_content(
+            Origin::signed(sender),
+            member_id,
+            1,
+            1234,
+            0,
+            vec![1, 2, 3, 4],
+        );
+        assert!(res.is_ok());
+
+        let (content_id, _) = match System::events().last().unwrap().event {
+            MetaEvent::data_directory(data_directory::RawEvent::ContentAdded(
+                content_id,
+                creator,
+            )) => (content_id, creator),
+            _ => (0u64, 0xdeadbeefu64), // invalid value, unlikely to match
+        };
+
+        //  invalid data
+        let (storage_provider_account_id, storage_provider_id) = (1, 5);
+
+        let res = TestDataDirectory::accept_content(
+            Origin::signed(storage_provider_account_id),
+            storage_provider_id,
+            content_id,
+        );
+        assert_eq!(res, Err(Error::Other("WorkerDoesNotExist")));
+
+        let res = TestDataDirectory::reject_content(
+            Origin::signed(storage_provider_account_id),
+            storage_provider_id,
+            content_id,
+        );
+        assert_eq!(res, Err(Error::Other("WorkerDoesNotExist")));
+    });
+}
+
+#[test]
+fn accept_content_as_liaison() {
+    with_default_mock_builder(|| {
+        let sender = 1u64;
+        let member_id = 1u64;
+
+        let res = TestDataDirectory::add_content(
+            Origin::signed(sender),
+            member_id,
+            1,
+            1234,
+            0,
+            vec![1, 2, 3, 4],
+        );
+        assert!(res.is_ok());
+
+        // An appropriate event should have been fired.
+        let (content_id, creator) = match System::events().last().unwrap().event {
+            MetaEvent::data_directory(data_directory::RawEvent::ContentAdded(
+                content_id,
+                creator,
+            )) => (content_id, creator),
+            _ => (0u64, 0xdeadbeefu64), // invalid value, unlikely to match
+        };
+        assert_ne!(creator, 0xdeadbeefu64);
+        assert_eq!(creator, sender);
+
+        let (storage_provider_account_id, storage_provider_id) = hire_storage_provider();
+
+        // Accepting content should not work with some random origin
+        let res =
+            TestDataDirectory::accept_content(Origin::signed(55), storage_provider_id, content_id);
+        assert!(res.is_err());
+
+        // However, with the liaison as origin it should.
+        let res = TestDataDirectory::accept_content(
+            Origin::signed(storage_provider_account_id),
+            storage_provider_id,
+            content_id,
+        );
+        assert_eq!(res, Ok(()));
+    });
+}
+
+#[test]
+fn reject_content_as_liaison() {
+    with_default_mock_builder(|| {
+        let sender = 1u64;
+        let member_id = 1u64;
+
+        let res = TestDataDirectory::add_content(
+            Origin::signed(sender),
+            member_id,
+            1,
+            1234,
+            0,
+            vec![1, 2, 3, 4],
+        );
+        assert!(res.is_ok());
+
+        // An appropriate event should have been fired.
+        let (content_id, creator) = match System::events().last().unwrap().event {
+            MetaEvent::data_directory(data_directory::RawEvent::ContentAdded(
+                content_id,
+                creator,
+            )) => (content_id, creator),
+            _ => (0u64, 0xdeadbeefu64), // invalid value, unlikely to match
+        };
+        assert_ne!(creator, 0xdeadbeefu64);
+        assert_eq!(creator, sender);
+
+        let (storage_provider_account_id, storage_provider_id) = hire_storage_provider();
+
+        // Rejecting content should not work with some random origin
+        let res =
+            TestDataDirectory::reject_content(Origin::signed(55), storage_provider_id, content_id);
+        assert!(res.is_err());
+
+        // However, with the liaison as origin it should.
+        let res = TestDataDirectory::reject_content(
+            Origin::signed(storage_provider_account_id),
+            storage_provider_id,
+            content_id,
+        );
+        assert_eq!(res, Ok(()));
+    });
+}

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

@@ -0,0 +1,157 @@
+#![cfg(test)]
+
+use super::mock::*;
+
+#[test]
+fn initial_state() {
+    with_default_mock_builder(|| {
+        assert_eq!(
+            TestDataObjectStorageRegistry::first_relationship_id(),
+            TEST_FIRST_RELATIONSHIP_ID
+        );
+    });
+}
+
+#[test]
+fn add_relationship_fails_with_invalid_authorization() {
+    with_default_mock_builder(|| {
+        let (account_id, storage_provider_id) = (2, 2);
+        // The content needs to exist - in our mock, that's with the content ID TEST_MOCK_EXISTING_CID
+        let res = TestDataObjectStorageRegistry::add_relationship(
+            Origin::signed(account_id),
+            storage_provider_id,
+            TEST_MOCK_EXISTING_CID,
+        );
+        assert_eq!(res, Err(bureaucracy::Error::WorkerDoesNotExist.into()));
+    });
+}
+
+#[test]
+fn set_relationship_ready_fails_with_invalid_authorization() {
+    with_default_mock_builder(|| {
+        let (account_id, storage_provider_id) = hire_storage_provider();
+        // The content needs to exist - in our mock, that's with the content ID TEST_MOCK_EXISTING_CID
+        let res = TestDataObjectStorageRegistry::add_relationship(
+            Origin::signed(account_id),
+            storage_provider_id,
+            TEST_MOCK_EXISTING_CID,
+        );
+        assert!(res.is_ok());
+
+        let (invalid_account_id, invalid_storage_provider_id) = (2, 2);
+        let res = TestDataObjectStorageRegistry::set_relationship_ready(
+            Origin::signed(invalid_account_id),
+            invalid_storage_provider_id,
+            TEST_MOCK_EXISTING_CID,
+        );
+        assert_eq!(res, Err(bureaucracy::Error::WorkerDoesNotExist.into()));
+    });
+}
+
+#[test]
+fn unset_relationship_ready_fails_with_invalid_authorization() {
+    with_default_mock_builder(|| {
+        let (account_id, storage_provider_id) = hire_storage_provider();
+        // The content needs to exist - in our mock, that's with the content ID TEST_MOCK_EXISTING_CID
+        let res = TestDataObjectStorageRegistry::add_relationship(
+            Origin::signed(account_id),
+            storage_provider_id,
+            TEST_MOCK_EXISTING_CID,
+        );
+        assert!(res.is_ok());
+
+        let (invalid_account_id, invalid_storage_provider_id) = (2, 2);
+        let res = TestDataObjectStorageRegistry::unset_relationship_ready(
+            Origin::signed(invalid_account_id),
+            invalid_storage_provider_id,
+            TEST_MOCK_EXISTING_CID,
+        );
+        assert_eq!(res, Err(bureaucracy::Error::WorkerDoesNotExist.into()));
+    });
+}
+
+#[test]
+fn test_add_relationship() {
+    with_default_mock_builder(|| {
+        let (account_id, storage_provider_id) = hire_storage_provider();
+        // The content needs to exist - in our mock, that's with the content ID TEST_MOCK_EXISTING_CID
+        let res = TestDataObjectStorageRegistry::add_relationship(
+            Origin::signed(account_id),
+            storage_provider_id,
+            TEST_MOCK_EXISTING_CID,
+        );
+        assert_eq!(res, Ok(()));
+    });
+}
+
+#[test]
+fn test_fail_adding_relationship_with_bad_content() {
+    with_default_mock_builder(|| {
+        let (account_id, storage_provider_id) = hire_storage_provider();
+        let res = TestDataObjectStorageRegistry::add_relationship(
+            Origin::signed(account_id),
+            storage_provider_id,
+            24,
+        );
+        assert!(res.is_err());
+    });
+}
+
+#[test]
+fn test_toggle_ready() {
+    with_default_mock_builder(|| {
+        let (account_id, storage_provider_id) = hire_storage_provider();
+        // Create a DOSR
+        let res = TestDataObjectStorageRegistry::add_relationship(
+            Origin::signed(account_id),
+            storage_provider_id,
+            TEST_MOCK_EXISTING_CID,
+        );
+        assert!(res.is_ok());
+
+        // Grab DOSR ID from event
+        let dosr_id = match System::events().last().unwrap().event {
+            MetaEvent::data_object_storage_registry(
+                data_object_storage_registry::RawEvent::DataObjectStorageRelationshipAdded(
+                    dosr_id,
+                    _content_id,
+                    _account_id,
+                ),
+            ) => dosr_id,
+            _ => 0xdeadbeefu64, // invalid value, unlikely to match
+        };
+        assert_ne!(dosr_id, 0xdeadbeefu64);
+
+        // Toggling from a different account should fail
+        let res = TestDataObjectStorageRegistry::set_relationship_ready(
+            Origin::signed(2),
+            storage_provider_id,
+            dosr_id,
+        );
+        assert!(res.is_err());
+
+        // Toggling with the wrong ID should fail.
+        let res = TestDataObjectStorageRegistry::set_relationship_ready(
+            Origin::signed(account_id),
+            storage_provider_id,
+            dosr_id + 1,
+        );
+        assert!(res.is_err());
+
+        // Toggling with the correct ID and origin should succeed
+        let res = TestDataObjectStorageRegistry::set_relationship_ready(
+            Origin::signed(account_id),
+            storage_provider_id,
+            dosr_id,
+        );
+        assert!(res.is_ok());
+        assert_eq!(
+            System::events().last().unwrap().event,
+            MetaEvent::data_object_storage_registry(
+                data_object_storage_registry::RawEvent::DataObjectStorageRelationshipReadyUpdated(
+                    dosr_id, true,
+                )
+            )
+        );
+    });
+}

+ 285 - 0
runtime-modules/storage/src/tests/data_object_type_registry.rs

@@ -0,0 +1,285 @@
+#![cfg(test)]
+
+use super::mock::*;
+use crate::StorageBureaucracy;
+use system::{self, EventRecord, Phase, RawOrigin};
+
+const DEFAULT_LEADER_ACCOUNT_ID: u64 = 1;
+const DEFAULT_LEADER_MEMBER_ID: u64 = 1;
+
+struct SetLeadFixture;
+impl SetLeadFixture {
+    fn set_default_lead() {
+        let set_lead_result = <StorageBureaucracy<Test>>::set_lead(
+            RawOrigin::Root.into(),
+            DEFAULT_LEADER_MEMBER_ID,
+            DEFAULT_LEADER_ACCOUNT_ID,
+        );
+        assert!(set_lead_result.is_ok());
+    }
+}
+
+fn get_last_data_object_type_id() -> u64 {
+    let dot_id = match System::events().last().unwrap().event {
+        MetaEvent::data_object_type_registry(
+            data_object_type_registry::RawEvent::DataObjectTypeRegistered(dot_id),
+        ) => dot_id,
+        _ => 0xdeadbeefu64, // unlikely value
+    };
+    assert_ne!(dot_id, 0xdeadbeefu64);
+
+    dot_id
+}
+
+#[test]
+fn initial_state() {
+    with_default_mock_builder(|| {
+        assert_eq!(
+            TestDataObjectTypeRegistry::first_data_object_type_id(),
+            TEST_FIRST_DATA_OBJECT_TYPE_ID
+        );
+    });
+}
+
+#[test]
+fn succeed_register() {
+    with_default_mock_builder(|| {
+        SetLeadFixture::set_default_lead();
+
+        let data: TestDataObjectType = TestDataObjectType {
+            description: "foo".as_bytes().to_vec(),
+            active: false,
+        };
+        let res = TestDataObjectTypeRegistry::register_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            data,
+        );
+        assert!(res.is_ok());
+    });
+}
+
+#[test]
+fn activate_data_object_type_fails_with_invalid_lead() {
+    with_default_mock_builder(|| {
+        SetLeadFixture::set_default_lead();
+
+        // First register a type
+        let data: TestDataObjectType = TestDataObjectType {
+            description: "foo".as_bytes().to_vec(),
+            active: false,
+        };
+        let id_res = TestDataObjectTypeRegistry::register_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            data,
+        );
+        assert!(id_res.is_ok());
+
+        let dot_id = get_last_data_object_type_id();
+
+        let invalid_leader_account_id = 2;
+        let res = TestDataObjectTypeRegistry::activate_data_object_type(
+            RawOrigin::Signed(invalid_leader_account_id).into(),
+            dot_id,
+        );
+        assert_eq!(res, Err(bureaucracy::Error::IsNotLeadAccount.into()));
+    });
+}
+
+#[test]
+fn deactivate_data_object_type_fails_with_invalid_lead() {
+    with_default_mock_builder(|| {
+        SetLeadFixture::set_default_lead();
+
+        // First register a type
+        let data: TestDataObjectType = TestDataObjectType {
+            description: "foo".as_bytes().to_vec(),
+            active: true,
+        };
+        let id_res = TestDataObjectTypeRegistry::register_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            data,
+        );
+        assert!(id_res.is_ok());
+
+        let dot_id = get_last_data_object_type_id();
+
+        let invalid_leader_account_id = 2;
+        let res = TestDataObjectTypeRegistry::deactivate_data_object_type(
+            RawOrigin::Signed(invalid_leader_account_id).into(),
+            dot_id,
+        );
+        assert_eq!(res, Err(bureaucracy::Error::IsNotLeadAccount.into()));
+    });
+}
+
+#[test]
+fn update_data_object_type_fails_with_invalid_lead() {
+    with_default_mock_builder(|| {
+        SetLeadFixture::set_default_lead();
+
+        // First register a type
+        let data: TestDataObjectType = TestDataObjectType {
+            description: "foo".as_bytes().to_vec(),
+            active: false,
+        };
+        let id_res = TestDataObjectTypeRegistry::register_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            data,
+        );
+        assert!(id_res.is_ok());
+
+        let dot_id = get_last_data_object_type_id();
+        let updated1: TestDataObjectType = TestDataObjectType {
+            description: "bar".as_bytes().to_vec(),
+            active: false,
+        };
+
+        let invalid_leader_account_id = 2;
+        let res = TestDataObjectTypeRegistry::update_data_object_type(
+            RawOrigin::Signed(invalid_leader_account_id).into(),
+            dot_id,
+            updated1,
+        );
+        assert_eq!(res, Err(bureaucracy::Error::IsNotLeadAccount.into()));
+    });
+}
+
+#[test]
+fn update_existing() {
+    with_default_mock_builder(|| {
+        SetLeadFixture::set_default_lead();
+
+        // First register a type
+        let data: TestDataObjectType = TestDataObjectType {
+            description: "foo".as_bytes().to_vec(),
+            active: false,
+        };
+        let id_res = TestDataObjectTypeRegistry::register_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            data,
+        );
+        assert!(id_res.is_ok());
+
+        let dot_id = get_last_data_object_type_id();
+
+        // Now update it with new data - we need the ID to be the same as in
+        // returned by the previous call. First, though, try and fail with a bad ID
+        let updated1: TestDataObjectType = TestDataObjectType {
+            description: "bar".as_bytes().to_vec(),
+            active: false,
+        };
+        let res = TestDataObjectTypeRegistry::update_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            dot_id + 1,
+            updated1,
+        );
+        assert!(res.is_err());
+
+        // Finally with an existing ID, it should work.
+        let updated3: TestDataObjectType = TestDataObjectType {
+            description: "bar".as_bytes().to_vec(),
+            active: false,
+        };
+        let res = TestDataObjectTypeRegistry::update_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            dot_id,
+            updated3,
+        );
+        assert!(res.is_ok());
+        assert_eq!(
+            *System::events().last().unwrap(),
+            EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: MetaEvent::data_object_type_registry(
+                    data_object_type_registry::RawEvent::DataObjectTypeUpdated(dot_id)
+                ),
+                topics: vec![],
+            }
+        );
+    });
+}
+
+#[test]
+fn register_data_object_type_failed_with_no_lead() {
+    with_default_mock_builder(|| {
+        // First register a type
+        let data: TestDataObjectType = TestDataObjectType {
+            description: "foo".as_bytes().to_vec(),
+            active: false,
+        };
+        let id_res = TestDataObjectTypeRegistry::register_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            data,
+        );
+        assert!(!id_res.is_ok());
+    });
+}
+
+#[test]
+fn activate_existing() {
+    with_default_mock_builder(|| {
+        SetLeadFixture::set_default_lead();
+
+        // First register a type
+        let data: TestDataObjectType = TestDataObjectType {
+            description: "foo".as_bytes().to_vec(),
+            active: false,
+        };
+        let id_res = TestDataObjectTypeRegistry::register_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            data,
+        );
+        assert!(id_res.is_ok());
+        assert_eq!(
+            *System::events().last().unwrap(),
+            EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: MetaEvent::data_object_type_registry(
+                    data_object_type_registry::RawEvent::DataObjectTypeRegistered(
+                        TEST_FIRST_DATA_OBJECT_TYPE_ID
+                    )
+                ),
+                topics: vec![],
+            }
+        );
+
+        // Retrieve, and ensure it's not active.
+        let data = TestDataObjectTypeRegistry::data_object_types(TEST_FIRST_DATA_OBJECT_TYPE_ID);
+        assert!(data.is_some());
+        assert!(!data.unwrap().active);
+
+        // Now activate the data object type
+        let res = TestDataObjectTypeRegistry::activate_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            TEST_FIRST_DATA_OBJECT_TYPE_ID,
+        );
+        assert!(res.is_ok());
+        assert_eq!(
+            *System::events().last().unwrap(),
+            EventRecord {
+                phase: Phase::ApplyExtrinsic(0),
+                event: MetaEvent::data_object_type_registry(
+                    data_object_type_registry::RawEvent::DataObjectTypeUpdated(
+                        TEST_FIRST_DATA_OBJECT_TYPE_ID
+                    )
+                ),
+                topics: vec![],
+            }
+        );
+
+        // Ensure that the item is actually activated.
+        let data = TestDataObjectTypeRegistry::data_object_types(TEST_FIRST_DATA_OBJECT_TYPE_ID);
+        assert!(data.is_some());
+        assert!(data.unwrap().active);
+
+        // Deactivate again.
+        let res = TestDataObjectTypeRegistry::deactivate_data_object_type(
+            RawOrigin::Signed(DEFAULT_LEADER_ACCOUNT_ID).into(),
+            TEST_FIRST_DATA_OBJECT_TYPE_ID,
+        );
+        assert!(res.is_ok());
+        let data = TestDataObjectTypeRegistry::data_object_types(TEST_FIRST_DATA_OBJECT_TYPE_ID);
+        assert!(data.is_some());
+        assert!(!data.unwrap().active);
+    });
+}

+ 79 - 52
runtime-modules/storage/src/mock.rs → runtime-modules/storage/src/tests/mock.rs

@@ -1,10 +1,8 @@
 #![cfg(test)]
 
-pub use super::{data_directory, data_object_storage_registry, data_object_type_registry};
-use crate::traits;
+pub use crate::{data_directory, data_object_storage_registry, data_object_type_registry};
 pub use common::currency::GovernanceCurrency;
 use membership::members;
-use roles::actors;
 pub use system;
 
 pub use primitives::{Blake2Hasher, H256};
@@ -15,7 +13,14 @@ pub use sr_primitives::{
     BuildStorage, Perbill,
 };
 
-use srml_support::{impl_outer_event, impl_outer_origin, parameter_types};
+use crate::data_directory::ContentIdExists;
+use crate::data_object_type_registry::IsActiveDataObjectType;
+use srml_support::{impl_outer_event, impl_outer_origin, parameter_types, StorageLinkedMap};
+
+mod bureaucracy_mod {
+    pub use bureaucracy::Event;
+    pub use bureaucracy::Instance2;
+}
 
 impl_outer_origin! {
     pub enum Origin for Test {}
@@ -26,9 +31,9 @@ impl_outer_event! {
         data_object_type_registry<T>,
         data_directory<T>,
         data_object_storage_registry<T>,
-        actors<T>,
         balances<T>,
         members<T>,
+        bureaucracy_mod Instance2 <T>,
     }
 }
 
@@ -37,41 +42,18 @@ pub const TEST_FIRST_CONTENT_ID: u64 = 2000;
 pub const TEST_FIRST_RELATIONSHIP_ID: u64 = 3000;
 pub const TEST_FIRST_METADATA_ID: u64 = 4000;
 
-pub const TEST_MOCK_LIAISON: u64 = 0xd00du64;
+pub const TEST_MOCK_LIAISON_STORAGE_PROVIDER_ID: u32 = 1;
 pub const TEST_MOCK_EXISTING_CID: u64 = 42;
 
-pub struct MockRoles {}
-impl roles::traits::Roles<Test> for MockRoles {
-    fn is_role_account(_account_id: &<Test as system::Trait>::AccountId) -> bool {
-        false
-    }
-
-    fn account_has_role(
-        account_id: &<Test as system::Trait>::AccountId,
-        _role: actors::Role,
-    ) -> bool {
-        *account_id == TEST_MOCK_LIAISON
-    }
-
-    fn random_account_for_role(
-        _role: actors::Role,
-    ) -> Result<<Test as system::Trait>::AccountId, &'static str> {
-        // We "randomly" select an account Id.
-        Ok(TEST_MOCK_LIAISON)
-    }
-}
-
 pub struct AnyDataObjectTypeIsActive {}
-impl<T: data_object_type_registry::Trait> traits::IsActiveDataObjectType<T>
-    for AnyDataObjectTypeIsActive
-{
+impl<T: data_object_type_registry::Trait> IsActiveDataObjectType<T> for AnyDataObjectTypeIsActive {
     fn is_active_data_object_type(_which: &T::DataObjectTypeId) -> bool {
         true
     }
 }
 
 pub struct MockContent {}
-impl traits::ContentIdExists<Test> for MockContent {
+impl ContentIdExists<Test> for MockContent {
     fn has_content(which: &<Test as data_directory::Trait>::ContentId) -> bool {
         *which == TEST_MOCK_EXISTING_CID
     }
@@ -88,7 +70,7 @@ impl traits::ContentIdExists<Test> for MockContent {
                     time: 1024,
                 },
                 owner: 1,
-                liaison: TEST_MOCK_LIAISON,
+                liaison: TEST_MOCK_LIAISON_STORAGE_PROVIDER_ID,
                 liaison_judgement: data_directory::LiaisonJudgement::Pending,
                 ipfs_content_id: vec![],
             }),
@@ -139,6 +121,7 @@ parameter_types! {
     pub const TransactionBaseFee: u32 = 1;
     pub const TransactionByteFee: u32 = 0;
     pub const InitialMembersBalance: u32 = 2000;
+    pub const StakePoolId: [u8; 8] = *b"joystake";
 }
 
 impl balances::Trait for Test {
@@ -162,6 +145,10 @@ impl GovernanceCurrency for Test {
     type Currency = balances::Module<Self>;
 }
 
+impl bureaucracy::Trait<bureaucracy::Instance2> for Test {
+    type Event = MetaEvent;
+}
+
 impl data_object_type_registry::Trait for Test {
     type Event = MetaEvent;
     type DataObjectTypeId = u64;
@@ -170,34 +157,64 @@ impl data_object_type_registry::Trait for Test {
 impl data_directory::Trait for Test {
     type Event = MetaEvent;
     type ContentId = u64;
-    type Roles = MockRoles;
+    type StorageProviderHelper = ();
     type IsActiveDataObjectType = AnyDataObjectTypeIsActive;
-    type SchemaId = u64;
+    type MemberOriginValidator = ();
+}
+
+impl crate::data_directory::StorageProviderHelper<Test> for () {
+    fn get_random_storage_provider() -> Result<u32, &'static str> {
+        Ok(1)
+    }
+}
+
+impl common::origin_validator::ActorOriginValidator<Origin, u64, u64> for () {
+    fn ensure_actor_origin(origin: Origin, _account_id: u64) -> Result<u64, &'static str> {
+        let signed_account_id = system::ensure_signed(origin)?;
+
+        Ok(signed_account_id)
+    }
 }
 
 impl data_object_storage_registry::Trait for Test {
     type Event = MetaEvent;
     type DataObjectStorageRelationshipId = u64;
-    type Roles = MockRoles;
     type ContentIdExists = MockContent;
 }
 
 impl members::Trait for Test {
     type Event = MetaEvent;
-    type MemberId = u32;
+    type MemberId = u64;
     type SubscriptionId = u32;
     type PaidTermId = u32;
     type ActorId = u32;
     type InitialMembersBalance = InitialMembersBalance;
 }
 
-impl actors::Trait for Test {
-    type Event = MetaEvent;
-    type OnActorRemoved = ();
+impl stake::Trait for Test {
+    type Currency = Balances;
+    type StakePoolId = StakePoolId;
+    type StakingEventsHandler = ();
+    type StakeId = u64;
+    type SlashId = u64;
+}
+
+impl minting::Trait for Test {
+    type Currency = Balances;
+    type MintId = u64;
 }
 
-impl actors::ActorRemoved<Test> for () {
-    fn actor_removed(_: &u64) {}
+impl recurringrewards::Trait for Test {
+    type PayoutStatusHandler = ();
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
+
+impl hiring::Trait for Test {
+    type OpeningId = u64;
+    type ApplicationId = u64;
+    type ApplicationDeactivatedHandler = ();
+    type StakeHandlerProvider = hiring::Module<Self>;
 }
 
 pub struct ExtBuilder {
@@ -263,13 +280,12 @@ impl ExtBuilder {
     }
 }
 
+pub type Balances = balances::Module<Test>;
 pub type System = system::Module<Test>;
 pub type TestDataObjectTypeRegistry = data_object_type_registry::Module<Test>;
 pub type TestDataObjectType = data_object_type_registry::DataObjectType;
 pub type TestDataDirectory = data_directory::Module<Test>;
-// pub type TestDataObject = data_directory::DataObject<Test>;
 pub type TestDataObjectStorageRegistry = data_object_storage_registry::Module<Test>;
-pub type TestActors = actors::Module<Test>;
 
 pub fn with_default_mock_builder<R, F: FnOnce() -> R>(f: F) -> R {
     ExtBuilder::default()
@@ -278,13 +294,24 @@ pub fn with_default_mock_builder<R, F: FnOnce() -> R>(f: F) -> R {
         .first_relationship_id(TEST_FIRST_RELATIONSHIP_ID)
         .first_metadata_id(TEST_FIRST_METADATA_ID)
         .build()
-        .execute_with(|| {
-            let roles: Vec<actors::Role> = vec![actors::Role::StorageProvider];
-            assert!(
-                TestActors::set_available_roles(system::RawOrigin::Root.into(), roles).is_ok(),
-                ""
-            );
-
-            f()
-        })
+        .execute_with(|| f())
+}
+
+pub(crate) fn hire_storage_provider() -> (u64, u32) {
+    let storage_provider_id = 1;
+    let role_account_id = 1;
+
+    let storage_provider = bureaucracy::Worker {
+        member_id: 1,
+        role_account: role_account_id,
+        reward_relationship: None,
+        role_stake_profile: None,
+    };
+
+    <bureaucracy::WorkerById<Test, bureaucracy::Instance2>>::insert(
+        storage_provider_id,
+        storage_provider,
+    );
+
+    (role_account_id, storage_provider_id)
 }

+ 6 - 0
runtime-modules/storage/src/tests/mod.rs

@@ -0,0 +1,6 @@
+#![cfg(test)]
+
+mod data_directory;
+mod data_object_storage_registry;
+mod data_object_type_registry;
+mod mock;

+ 0 - 20
runtime-modules/storage/src/traits.rs

@@ -1,20 +0,0 @@
-use crate::{data_directory, data_object_storage_registry, data_object_type_registry};
-
-// Storage
-pub trait IsActiveDataObjectType<T: data_object_type_registry::Trait> {
-    fn is_active_data_object_type(_which: &T::DataObjectTypeId) -> bool;
-}
-
-pub trait ContentIdExists<T: data_directory::Trait> {
-    fn has_content(_which: &T::ContentId) -> bool;
-
-    fn get_data_object(
-        _which: &T::ContentId,
-    ) -> Result<data_directory::DataObject<T>, &'static str>;
-}
-
-pub trait ContentHasStorage<T: data_object_storage_registry::Trait> {
-    fn has_storage_provider(_which: &T::ContentId) -> bool;
-
-    fn is_ready_at_storage_provider(_which: &T::ContentId, _provider: &T::AccountId) -> bool;
-}

+ 3 - 3
runtime/Cargo.toml

@@ -5,7 +5,7 @@ edition = '2018'
 name = 'joystream-node-runtime'
 # Follow convention: https://github.com/Joystream/substrate-runtime-joystream/issues/1
 # {Authoring}.{Spec}.{Impl} of the RuntimeVersion
-version = '6.15.0'
+version = '6.16.0'
 
 [features]
 default = ['std']
@@ -350,13 +350,13 @@ version = '1.0.0'
 default_features = false
 package = 'substrate-service-discovery-module'
 path = '../runtime-modules/service-discovery'
-version = '1.0.0'
+version = '2.0.0'
 
 [dependencies.storage]
 default_features = false
 package = 'substrate-storage-module'
 path = '../runtime-modules/storage'
-version = '1.0.0'
+version = '2.0.0'
 
 [dependencies.proposals_engine]
 default_features = false

+ 1 - 0
runtime/src/integration/mod.rs

@@ -1 +1,2 @@
 pub mod proposals;
+pub mod storage;

+ 36 - 0
runtime/src/integration/storage.rs

@@ -0,0 +1,36 @@
+use rstd::vec::Vec;
+use srml_support::traits::Randomness;
+
+use crate::{ActorId, Runtime};
+
+/// Provides random storage provider id. We use it when assign the content to the storage provider.
+pub struct StorageProviderHelper;
+
+impl storage::data_directory::StorageProviderHelper<Runtime> for StorageProviderHelper {
+    fn get_random_storage_provider() -> Result<ActorId, &'static str> {
+        let ids = crate::StorageBureaucracy::get_all_worker_ids();
+
+        let live_ids: Vec<ActorId> = ids
+            .into_iter()
+            .filter(|id| !<service_discovery::Module<Runtime>>::is_account_info_expired(id))
+            .collect();
+
+        if live_ids.is_empty() {
+            Err("No valid storage provider found.")
+        } else {
+            let index = Self::random_index(live_ids.len());
+            Ok(live_ids[index])
+        }
+    }
+}
+
+impl StorageProviderHelper {
+    fn random_index(upper_bound: usize) -> usize {
+        let seed = crate::RandomnessCollectiveFlip::random_seed();
+        let mut rand: u64 = 0;
+        for offset in 0..8 {
+            rand += (seed.as_ref()[offset] as u64) << offset;
+        }
+        (rand as usize) % upper_bound
+    }
+}

+ 25 - 75
runtime/src/lib.rs

@@ -8,7 +8,7 @@
 #![allow(array_into_iter)]
 
 // Runtime integration tests
-mod test;
+mod tests;
 
 // Make the WASM binary available.
 // This is required only by the node build.
@@ -110,6 +110,9 @@ pub type ThreadId = u64;
 /// See the Note about ThreadId
 pub type PostId = u64;
 
+/// Represent an actor in membership group, which is the same in the working groups.
+pub type ActorId = u64;
+
 /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know
 /// the specifics of the runtime. They can then be made to be agnostic over specific formats
 /// of data like extrinsics, allowing for them to continue syncing the network through upgrades
@@ -142,7 +145,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
     spec_name: create_runtime_str!("joystream-node"),
     impl_name: create_runtime_str!("joystream-node"),
     authoring_version: 6,
-    spec_version: 15,
+    spec_version: 16,
     impl_version: 0,
     apis: RUNTIME_API_VERSIONS,
 };
@@ -432,7 +435,6 @@ pub use versioned_store;
 pub use content_working_group as content_wg;
 mod migration;
 use roles::actors;
-use service_discovery::discovery;
 
 /// Alias for ContentId, used in various places.
 pub type ContentId = primitives::H256;
@@ -534,20 +536,6 @@ impl versioned_store_permissions::CreateClassPermissionsChecker<Runtime>
     }
 }
 
-// Impl this in the permissions module - can't be done here because
-// neither CreateClassPermissionsChecker or (X, Y) are local types?
-// impl<
-//         T: versioned_store_permissions::Trait,
-//         X: versioned_store_permissions::CreateClassPermissionsChecker<T>,
-//         Y: versioned_store_permissions::CreateClassPermissionsChecker<T>,
-//     > versioned_store_permissions::CreateClassPermissionsChecker<T> for (X, Y)
-// {
-//     fn account_can_create_class_permissions(account: &T::AccountId) -> bool {
-//         X::account_can_create_class_permissions(account)
-//             || Y::account_can_create_class_permissions(account)
-//     }
-// }
-
 pub struct ContentLeadOrSudoKeyCanCreateClasses {}
 impl versioned_store_permissions::CreateClassPermissionsChecker<Runtime>
     for ContentLeadOrSudoKeyCanCreateClasses
@@ -705,65 +693,23 @@ impl storage::data_object_type_registry::Trait for Runtime {
 impl storage::data_directory::Trait for Runtime {
     type Event = Event;
     type ContentId = ContentId;
-    type SchemaId = u64;
-    type Roles = LookupRoles;
+    type StorageProviderHelper = integration::storage::StorageProviderHelper;
     type IsActiveDataObjectType = DataObjectTypeRegistry;
+    type MemberOriginValidator = MembershipOriginValidator<Self>;
 }
 
 impl storage::data_object_storage_registry::Trait for Runtime {
     type Event = Event;
     type DataObjectStorageRelationshipId = u64;
-    type Roles = LookupRoles;
     type ContentIdExists = DataDirectory;
 }
 
-fn random_index(upper_bound: usize) -> usize {
-    let seed = RandomnessCollectiveFlip::random_seed();
-    let mut rand: u64 = 0;
-    for offset in 0..8 {
-        rand += (seed.as_ref()[offset] as u64) << offset;
-    }
-    (rand as usize) % upper_bound
-}
-
-pub struct LookupRoles {}
-impl roles::traits::Roles<Runtime> for LookupRoles {
-    fn is_role_account(account_id: &<Runtime as system::Trait>::AccountId) -> bool {
-        <actors::Module<Runtime>>::is_role_account(account_id)
-    }
-
-    fn account_has_role(
-        account_id: &<Runtime as system::Trait>::AccountId,
-        role: actors::Role,
-    ) -> bool {
-        <actors::Module<Runtime>>::account_has_role(account_id, role)
-    }
-
-    fn random_account_for_role(
-        role: actors::Role,
-    ) -> Result<<Runtime as system::Trait>::AccountId, &'static str> {
-        let ids = <actors::AccountIdsByRole<Runtime>>::get(role);
-
-        let live_ids: Vec<<Runtime as system::Trait>::AccountId> = ids
-            .into_iter()
-            .filter(|id| !<discovery::Module<Runtime>>::is_account_info_expired(id))
-            .collect();
-
-        if live_ids.is_empty() {
-            Err("no staked account found")
-        } else {
-            let index = random_index(live_ids.len());
-            Ok(live_ids[index].clone())
-        }
-    }
-}
-
 impl members::Trait for Runtime {
     type Event = Event;
     type MemberId = u64;
     type PaidTermId = u64;
     type SubscriptionId = u64;
-    type ActorId = u64;
+    type ActorId = ActorId;
     type InitialMembersBalance = InitialMembersBalance;
 }
 
@@ -814,21 +760,23 @@ impl bureaucracy::Trait<bureaucracy::Instance1> for Runtime {
     type Event = Event;
 }
 
+// Storage working group bureaucracy
+impl bureaucracy::Trait<bureaucracy::Instance2> for Runtime {
+    type Event = Event;
+}
+
 impl actors::Trait for Runtime {
     type Event = Event;
-    type OnActorRemoved = HandleActorRemoved;
+    type OnActorRemoved = ();
 }
 
-pub struct HandleActorRemoved {}
-impl actors::ActorRemoved<Runtime> for HandleActorRemoved {
-    fn actor_removed(actor: &<Runtime as system::Trait>::AccountId) {
-        Discovery::remove_account_info(actor);
-    }
+//TODO: SWG -  remove with roles module deletion
+impl actors::ActorRemoved<Runtime> for () {
+    fn actor_removed(_: &AccountId) {}
 }
 
-impl discovery::Trait for Runtime {
+impl service_discovery::Trait for Runtime {
     type Event = Event;
-    type Roles = LookupRoles;
 }
 
 parameter_types! {
@@ -920,10 +868,6 @@ construct_runtime!(
         Members: members::{Module, Call, Storage, Event<T>, Config<T>},
         Forum: forum::{Module, Call, Storage, Event<T>, Config<T>},
         Actors: actors::{Module, Call, Storage, Event<T>, Config},
-        DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event<T>, Config<T>},
-        DataDirectory: data_directory::{Module, Call, Storage, Event<T>},
-        DataObjectStorageRegistry: data_object_storage_registry::{Module, Call, Storage, Event<T>, Config<T>},
-        Discovery: discovery::{Module, Call, Storage, Event<T>},
         VersionedStore: versioned_store::{Module, Call, Storage, Event<T>, Config},
         VersionedStorePermissions: versioned_store_permissions::{Module, Call, Storage},
         Stake: stake::{Module, Call, Storage},
@@ -931,12 +875,18 @@ construct_runtime!(
         RecurringRewards: recurringrewards::{Module, Call, Storage},
         Hiring: hiring::{Module, Call, Storage},
         ContentWorkingGroup: content_wg::{Module, Call, Storage, Event<T>, Config<T>},
+        // --- Storage
+        DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event<T>, Config<T>},
+        DataDirectory: data_directory::{Module, Call, Storage, Event<T>},
+        DataObjectStorageRegistry: data_object_storage_registry::{Module, Call, Storage, Event<T>, Config<T>},
+        Discovery: service_discovery::{Module, Call, Storage, Event<T>},
         // --- Proposals
         ProposalsEngine: proposals_engine::{Module, Call, Storage, Event<T>},
         ProposalsDiscussion: proposals_discussion::{Module, Call, Storage, Event<T>},
         ProposalsCodex: proposals_codex::{Module, Call, Storage, Error, Config<T>},
-        // ---
+        // --- Bureaucracy
         ForumBureaucracy: bureaucracy::<Instance1>::{Module, Call, Storage, Event<T>},
+        StorageBureaucracy: bureaucracy::<Instance2>::{Module, Call, Storage, Event<T>},
     }
 );
 

+ 0 - 5
runtime/src/test/mod.rs

@@ -1,5 +0,0 @@
-//! The Joystream Substrate Node runtime integration tests.
-
-#![cfg(test)]
-
-mod proposals_integration;

+ 14 - 0
runtime/src/tests/mod.rs

@@ -0,0 +1,14 @@
+//! The Joystream Substrate Node runtime integration tests.
+
+#![cfg(test)]
+
+mod proposals_integration;
+mod storage_integration;
+
+pub(crate) fn initial_test_ext() -> runtime_io::TestExternalities {
+    let t = system::GenesisConfig::default()
+        .build_storage::<crate::Runtime>()
+        .unwrap();
+
+    t.into()
+}

+ 2 - 8
runtime/src/test/proposals_integration.rs → runtime/src/tests/proposals_integration.rs

@@ -20,15 +20,9 @@ use srml_support::traits::Currency;
 use srml_support::{StorageLinkedMap, StorageMap, StorageValue};
 use system::RawOrigin;
 
-use crate::CouncilManager;
-
-fn initial_test_ext() -> runtime_io::TestExternalities {
-    let t = system::GenesisConfig::default()
-        .build_storage::<Runtime>()
-        .unwrap();
+use super::initial_test_ext;
 
-    t.into()
-}
+use crate::CouncilManager;
 
 type Balances = balances::Module<Runtime>;
 type System = system::Module<Runtime>;

+ 43 - 0
runtime/src/tests/storage_integration.rs

@@ -0,0 +1,43 @@
+use super::initial_test_ext;
+use crate::integration::storage::StorageProviderHelper;
+use crate::Runtime;
+
+use bureaucracy::{Instance2, Worker};
+use srml_support::{StorageLinkedMap, StorageMap};
+
+#[test]
+fn storage_provider_helper_succeeds() {
+    initial_test_ext().execute_with(|| {
+
+		// Error - no workers.
+		let random_provider_result = <StorageProviderHelper
+			as storage::data_directory::StorageProviderHelper<Runtime>>::get_random_storage_provider();
+		assert!(random_provider_result.is_err());
+
+		let worker_id1 = 1;
+		let worker_id2 = 7;
+		let worker_id3 = 19;
+
+		<bureaucracy::WorkerById<Runtime, Instance2>>::insert(worker_id1, Worker::default());
+		<bureaucracy::WorkerById<Runtime, Instance2>>::insert(worker_id2, Worker::default());
+		<bureaucracy::WorkerById<Runtime, Instance2>>::insert(worker_id3, Worker::default());
+
+		// Still error - not registered in the service discovery.
+		let random_provider_result = <StorageProviderHelper as storage::data_directory::StorageProviderHelper<Runtime>>::get_random_storage_provider();
+		assert!(random_provider_result.is_err());
+
+		let account_info = service_discovery::AccountInfo{
+			identity: Vec::new(),
+			expires_at: 1000
+		};
+
+		<service_discovery::AccountInfoByStorageProviderId<Runtime>>::insert(worker_id1,account_info.clone());
+		<service_discovery::AccountInfoByStorageProviderId<Runtime>>::insert(worker_id2,account_info.clone());
+		<service_discovery::AccountInfoByStorageProviderId<Runtime>>::insert(worker_id3,account_info);
+
+		// Should work now.
+		let worker_ids = vec![worker_id1, worker_id2, worker_id3];
+		let random_provider_id = <StorageProviderHelper as storage::data_directory::StorageProviderHelper<Runtime>>::get_random_storage_provider().unwrap();
+		assert!(worker_ids.contains(&random_provider_id));
+	});
+}

+ 1 - 1
tests/network-tests/src/constantinople/tests/impl/electingCouncil.ts

@@ -2,7 +2,7 @@ import { KeyringPair } from '@polkadot/keyring/types';
 import { ApiWrapper } from '../../utils/apiWrapper';
 import { Keyring } from '@polkadot/api';
 import BN from 'bn.js';
-import { Seat } from '@constantinople/types';
+import { Seat } from '@constantinople/types/lib/council';
 import { assert } from 'chai';
 import { v4 as uuid } from 'uuid';
 import { Utils } from '../../utils/utils';

+ 1 - 1
tests/network-tests/src/constantinople/utils/apiWrapper.ts

@@ -6,7 +6,7 @@ import { UserInfo, PaidMembershipTerms, MemberId } from '@constantinople/types/l
 import { Mint, MintId } from '@constantinople/types/lib/mint';
 import { Lead, LeadId } from '@constantinople/types/lib/content-working-group';
 import { RoleParameters } from '@constantinople/types/lib/roles';
-import { Seat } from '@constantinople/types';
+import { Seat } from '@constantinople/types/lib/council';
 import { Balance, EventRecord, AccountId, BlockNumber, BalanceOf } from '@polkadot/types/interfaces';
 import BN from 'bn.js';
 import { SubmittableExtrinsic } from '@polkadot/api/types';

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