Browse Source

Merge pull request #3131 from Lezek123/giza-olympia-post-merge

Olympia: Post-merge adjustments (CLI, api-scripts, integration tests, start.sh)
Mokhtar Naamani 3 years ago
parent
commit
f89f401385
81 changed files with 1558 additions and 1370 deletions
  1. 0 0
      .github/workflows-disabled/run-network-tests.yml
  2. 3 1
      cli/examples/content/CreateChannel.json
  3. 2 1
      cli/examples/content/CreateVideo.json
  4. 2 0
      cli/scripts/content-test.sh
  5. 46 50
      cli/src/Api.ts
  6. 8 8
      cli/src/QueryNodeApi.ts
  7. 0 636
      cli/src/QueryNodeApiSchema.generated.ts
  8. 5 1
      cli/src/Types.ts
  9. 10 13
      cli/src/base/AccountsCommandBase.ts
  10. 8 8
      cli/src/base/ApiCommandBase.ts
  11. 15 8
      cli/src/base/ContentDirectoryCommandBase.ts
  12. 3 3
      cli/src/base/UploadCommandBase.ts
  13. 13 4
      cli/src/commands/content/channel.ts
  14. 1 0
      cli/src/commands/content/channels.ts
  15. 17 9
      cli/src/commands/content/createChannel.ts
  16. 12 7
      cli/src/commands/content/createVideo.ts
  17. 2 2
      cli/src/commands/content/deleteChannel.ts
  18. 1 1
      cli/src/commands/content/reuploadAssets.ts
  19. 19 14
      cli/src/commands/content/updateChannel.ts
  20. 57 0
      cli/src/commands/content/updateChannelModerators.ts
  21. 4 2
      cli/src/commands/content/updateVideo.ts
  22. 2 0
      cli/src/commands/content/video.ts
  23. 2 0
      cli/src/commands/content/videos.ts
  24. 1 1
      cli/src/commands/working-groups/evictWorker.ts
  25. 1 1
      cli/src/commands/working-groups/leaveRole.ts
  26. 123 6
      cli/src/graphql/generated/queries.ts
  27. 311 442
      cli/src/graphql/generated/schema.ts
  28. 2 2
      cli/src/graphql/queries/membership.graphql
  29. 17 3
      cli/src/schemas/ContentDirectory.ts
  30. 1 1
      query-node/mappings/src/proposals.ts
  31. 1 0
      query-node/run-tests.sh
  32. 7 14
      start.sh
  33. 5 0
      storage-playground-config.sh
  34. 2 0
      tests/integration-tests/.gitignore
  35. 14 14
      tests/integration-tests/proposal-parameters.json
  36. 234 34
      tests/integration-tests/src/Api.ts
  37. 2 0
      tests/integration-tests/src/Fixture.ts
  38. 1 1
      tests/integration-tests/src/QueryNodeApi.ts
  39. 65 8
      tests/integration-tests/src/Scenario.ts
  40. 28 12
      tests/integration-tests/src/consts.ts
  41. 17 12
      tests/integration-tests/src/fixtures/council/ElectCouncilFixture.ts
  42. 3 0
      tests/integration-tests/src/fixtures/council/NotEnoughCandidatesFixture.ts
  43. 4 1
      tests/integration-tests/src/fixtures/council/NotEnoughCandidatesWithVotesFixture.ts
  44. 2 2
      tests/integration-tests/src/fixtures/council/common.ts
  45. 1 1
      tests/integration-tests/src/fixtures/forum/InitializeForumFixture.ts
  46. 1 1
      tests/integration-tests/src/fixtures/proposals/AllProposalsOutcomesFixture.ts
  47. 1 1
      tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts
  48. 1 1
      tests/integration-tests/src/fixtures/proposals/DecideOnProposalStatusFixture.ts
  49. 1 2
      tests/integration-tests/src/fixtures/workingGroups/DecreaseWorkerStakesFixture.ts
  50. 1 2
      tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts
  51. 3 3
      tests/integration-tests/src/fixtures/workingGroups/HireWorkersFixture.ts
  52. 1 2
      tests/integration-tests/src/fixtures/workingGroups/IncreaseWorkerStakesFixture.ts
  53. 1 2
      tests/integration-tests/src/fixtures/workingGroups/SlashWorkerStakesFixture.ts
  54. 1 2
      tests/integration-tests/src/fixtures/workingGroups/TerminateWorkersFixture.ts
  55. 2 2
      tests/integration-tests/src/flows/council/failToElect.ts
  56. 1 1
      tests/integration-tests/src/flows/forum/polls.ts
  57. 2 2
      tests/integration-tests/src/flows/membership/creatingMemberships.ts
  58. 2 2
      tests/integration-tests/src/flows/membership/invitingMembers.ts
  59. 2 2
      tests/integration-tests/src/flows/membership/managingStakingAccounts.ts
  60. 1 1
      tests/integration-tests/src/flows/membership/transferringInvites.ts
  61. 2 2
      tests/integration-tests/src/flows/membership/updatingAccounts.ts
  62. 1 1
      tests/integration-tests/src/flows/membership/updatingProfile.ts
  63. 1 1
      tests/integration-tests/src/flows/proposals/cancellingProposal.ts
  64. 1 1
      tests/integration-tests/src/flows/proposals/exactExecutionBlock.ts
  65. 1 1
      tests/integration-tests/src/flows/proposals/expireProposal.ts
  66. 2 2
      tests/integration-tests/src/flows/proposals/index.ts
  67. 1 1
      tests/integration-tests/src/flows/proposals/runtimeUpgradeProposal.ts
  68. 1 1
      tests/integration-tests/src/flows/proposals/vetoProposal.ts
  69. 1 1
      tests/integration-tests/src/flows/proposalsDiscussion/index.ts
  70. 222 0
      tests/integration-tests/src/flows/storage/initDistribution.ts
  71. 161 0
      tests/integration-tests/src/flows/storage/initStorage.ts
  72. 1 1
      tests/integration-tests/src/flows/working-groups/groupBudget.ts
  73. 1 1
      tests/integration-tests/src/flows/working-groups/leadOpening.ts
  74. 3 3
      tests/integration-tests/src/flows/working-groups/openingsAndApplications.ts
  75. 2 2
      tests/integration-tests/src/flows/working-groups/workerActions.ts
  76. 15 0
      tests/integration-tests/src/misc/updateAllWorkerRoleAccountsFlow.ts
  77. 14 0
      tests/integration-tests/src/misc/updateWorkerAccountsFixture.ts
  78. 21 0
      tests/integration-tests/src/scenarios/setupNewChain.ts
  79. 4 1
      tests/integration-tests/src/types.ts
  80. 4 1
      utils/api-scripts/src/initialize-lead.ts
  81. 4 1
      utils/api-scripts/src/initialize-worker.ts

+ 0 - 0
.github/workflows/run-network-tests.yml → .github/workflows-disabled/run-network-tests.yml


+ 3 - 1
cli/examples/content/CreateChannel.json

@@ -6,5 +6,7 @@
   "category": 1,
   "avatarPhotoPath": "./avatar-photo-1.png",
   "coverPhotoPath": "./cover-photo-1.png",
-  "rewardAccount": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
+  "rewardAccount": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
+  "collaborators": [],
+  "moderators": []
 }

+ 2 - 1
cli/examples/content/CreateVideo.json

@@ -16,5 +16,6 @@
   "publishedBeforeJoystream": {
     "isPublished": true,
     "date": "2020-01-01"
-  }
+  },
+  "enableComments": true
 }

+ 2 - 0
cli/scripts/content-test.sh

@@ -10,6 +10,8 @@ export AUTO_CONFIRM=true
 
 # Init content lead
 GROUP=contentWorkingGroup yarn workspace api-scripts initialize-lead
+# Add integration tests lead key (in case the script is executed after ./start.sh)
+yarn joystream-cli account:import --suri //testing//worker//Content//0 --name "Test content lead key" --password "" || true
 # Test create/update/remove category
 yarn joystream-cli content:createVideoCategory -i ./examples/content/CreateCategory.json
 yarn joystream-cli content:createVideoCategory -i ./examples/content/CreateCategory.json

+ 46 - 50
cli/src/Api.ts

@@ -19,14 +19,7 @@ import {
 } from './Types'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { CLIError } from '@oclif/errors'
-import {
-  Worker,
-  WorkerId,
-  OpeningId,
-  Application,
-  ApplicationId,
-  Opening,
-} from '@joystream/types/working-group'
+import { Worker, WorkerId, OpeningId, Application, ApplicationId, Opening } from '@joystream/types/working-group'
 import { Membership, StakingAccountMemberBinding } from '@joystream/types/members'
 import { MemberId, ChannelId, AccountId } from '@joystream/types/common'
 import {
@@ -39,6 +32,8 @@ import {
   VideoCategoryId,
 } from '@joystream/types/content'
 import { BagId, DataObject, DataObjectId } from '@joystream/types/storage'
+import QueryNodeApi from './QueryNodeApi'
+import { MembershipFieldsFragment } from './graphql/generated/queries'
 
 export const DEFAULT_API_URI = 'ws://localhost:9944/'
 
@@ -55,23 +50,16 @@ export const apiModuleByGroup = {
   [WorkingGroups.Distribution]: 'distributionWorkingGroup',
 } as const
 
-export const lockIdByWorkingGroup: { [K in WorkingGroups]: string } = {
-  [WorkingGroups.StorageProviders]: '0x0606060606060606',
-  [WorkingGroups.Curators]: '0x0707070707070707',
-  [WorkingGroups.Forum]: '0x0808080808080808',
-  [WorkingGroups.Membership]: '0x0909090909090909',
-  [WorkingGroups.Gateway]: '0x0e0e0e0e0e0e0e0e',
-  // TODO: TBD. OperationsAlpha, OperationsBeta, OperationsGamma, Distribution
-}
-
 // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
 export default class Api {
   private _api: ApiPromise
+  private _qnApi: QueryNodeApi | undefined
   public isDevelopment = false
 
-  private constructor(originalApi: ApiPromise, isDevelopment: boolean) {
+  private constructor(originalApi: ApiPromise, isDevelopment: boolean, qnApi?: QueryNodeApi) {
     this.isDevelopment = isDevelopment
     this._api = originalApi
+    this._qnApi = qnApi
   }
 
   public getOriginalApi(): ApiPromise {
@@ -103,9 +91,13 @@ export default class Api {
     return { api, properties, chainType }
   }
 
-  static async create(apiUri = DEFAULT_API_URI, metadataCache: Record<string, any>): Promise<Api> {
+  static async create(
+    apiUri = DEFAULT_API_URI,
+    metadataCache: Record<string, any>,
+    qnApi?: QueryNodeApi
+  ): Promise<Api> {
     const { api, chainType } = await Api.initApi(apiUri, metadataCache)
-    return new Api(api, chainType.isDevelopment || chainType.isLocal)
+    return new Api(api, chainType.isDevelopment || chainType.isLocal, qnApi)
   }
 
   async bestNumber(): Promise<number> {
@@ -134,10 +126,6 @@ export default class Api {
     return paymentInfo.partialFee
   }
 
-  createTransferTx(recipient: string, amount: BN) {
-    return this._api.tx.balances.transfer(recipient, amount)
-  }
-
   // Working groups
   // TODO: This is a lot of repeated logic from "/pioneer/joy-utils/transport"
   // It will be refactored to "joystream-js" soon
@@ -164,22 +152,30 @@ export default class Api {
     return new Date(blockTime.toNumber())
   }
 
-  protected workingGroupApiQuery(group: WorkingGroups) {
+  protected workingGroupApiQuery<T extends WorkingGroups>(group: T): ApiPromise['query'][typeof apiModuleByGroup[T]] {
     const module = apiModuleByGroup[group]
     return this._api.query[module]
   }
 
-  // TODO: old fetchMemberQueryNodeData moved to QueryNodeApi and will be made available here
-
-  async memberDetails(memberId: MemberId, membership: Membership): Promise<MemberDetails> {
-    const memberData = await this.fetchMemberQueryNodeData(memberId)
+  async membersDetails(entries: [MemberId, Membership][]): Promise<MemberDetails[]> {
+    const membersQnData = await this._qnApi?.membersByIds(entries.map(([id]) => id))
+    const memberQnDataById = new Map<string, MembershipFieldsFragment>()
+    membersQnData?.forEach((m) => {
+      memberQnDataById.set(m.id, m)
+    })
 
-    return {
+    return entries.map(([memberId, membership]) => ({
       id: memberId,
-      name: memberData?.metadata.name,
-      handle: memberData?.handle,
+      name: memberQnDataById.get(memberId.toString())?.metadata.name,
+      handle: memberQnDataById.get(memberId.toString())?.handle,
       membership,
-    }
+    }))
+  }
+
+  // TODO: Try to avoid fetching members "one-by-one" whenever possible
+  async memberDetails(memberId: MemberId, membership: Membership): Promise<MemberDetails> {
+    const [memberDetails] = await this.membersDetails([[memberId, membership]])
+    return memberDetails
   }
 
   protected async membershipById(memberId: MemberId): Promise<MemberDetails | null> {
@@ -196,6 +192,21 @@ export default class Api {
     return member
   }
 
+  async getMembers(ids: MemberId[] | number[]): Promise<Membership[]> {
+    return this._api.query.members.membershipById.multi(ids)
+  }
+
+  async membersDetailsByIds(ids: MemberId[] | number[]): Promise<MemberDetails[]> {
+    const memberships = await this.getMembers(ids)
+    const entries: [MemberId, Membership][] = ids.map((id, i) => [createType('MemberId', id), memberships[i]])
+    return this.membersDetails(entries)
+  }
+
+  async allMembersDetails(): Promise<MemberDetails[]> {
+    const entries = await this.entriesByIds(this._api.query.members.membershipById)
+    return this.membersDetails(entries)
+  }
+
   async groupLead(group: WorkingGroups): Promise<GroupMember | null> {
     const optLeadId = await this.workingGroupApiQuery(group).currentLead()
 
@@ -210,12 +221,10 @@ export default class Api {
   }
 
   protected async fetchStake(account: AccountId | string, group: WorkingGroups): Promise<Balance> {
+    const groupLockId = this._api.consts[apiModuleByGroup[group]].stakingHandlerLockId
     return this._api.createType(
       'Balance',
-      new BN(
-        (await this._api.query.balances.locks(account)).find((lock) => lock.id.eq(lockIdByWorkingGroup[group]))
-          ?.amount || 0
-      )
+      new BN((await this._api.query.balances.locks(account)).find((lock) => lock.id.eq(groupLockId))?.amount || 0)
     )
   }
 
@@ -446,19 +455,6 @@ export default class Api {
     ])
   }
 
-  async getMembers(ids: MemberId[] | number[]): Promise<Membership[]> {
-    return this._api.query.members.membershipById.multi(ids)
-  }
-
-  async memberEntriesByIds(ids: MemberId[] | number[]): Promise<[MemberId, Membership][]> {
-    const memberships = await this._api.query.members.membershipById.multi<Membership>(ids)
-    return ids.map((id, i) => [createType('MemberId', id), memberships[i]])
-  }
-
-  allMemberEntries(): Promise<[MemberId, Membership][]> {
-    return this.entriesByIds(this._api.query.members.membershipById)
-  }
-
   async stakingAccountStatus(account: string): Promise<StakingAccountMemberBinding | null> {
     const status = await this.getOriginalApi().query.members.stakingAccountIdMemberStatus(account)
     return status.isEmpty ? null : status

+ 8 - 8
cli/src/QueryNodeApi.ts

@@ -24,9 +24,9 @@ import {
   GetDataObjectsByChannelId,
   GetDataObjectsByChannelIdQuery,
   GetDataObjectsByChannelIdQueryVariables,
-  GetMemberById,
-  GetMemberByIdQuery,
-  GetMemberByIdQueryVariables,
+  GetMembersByIds,
+  GetMembersByIdsQuery,
+  GetMembersByIdsQueryVariables,
   MembershipFieldsFragment,
 } from './graphql/generated/queries'
 import { URL } from 'url'
@@ -128,13 +128,13 @@ export default class QueryNodeApi {
     return validNodesInfo
   }
 
-  async fetchMemberQueryNodeData(memberId: MemberId): Promise<MembershipFieldsFragment | null | undefined> {
-    return this.uniqueEntityQuery<GetMemberByIdQuery, GetMemberByIdQueryVariables>(
-      GetMemberById,
+  async membersByIds(ids: MemberId[] | string[]): Promise<MembershipFieldsFragment[]> {
+    return this.multipleEntitiesQuery<GetMembersByIdsQuery, GetMembersByIdsQueryVariables>(
+      GetMembersByIds,
       {
-        id: memberId.toString(),
+        ids: ids.map((id) => id.toString()),
       },
-      'membershipByUniqueInput'
+      'memberships'
     )
   }
 }

+ 0 - 636
cli/src/QueryNodeApiSchema.generated.ts

@@ -1,636 +0,0 @@
-export type Maybe<T> = T | null
-export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }
-export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }
-export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }
-/** All built-in and custom scalars, mapped to their actual values */
-export type Scalars = {
-  ID: string
-  String: string
-  Boolean: boolean
-  Int: number
-  Float: number
-  /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */
-  DateTime: any
-  /** GraphQL representation of BigInt */
-  BigInt: any
-}
-
-export type BaseGraphQlObject = {
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdById: Scalars['String']
-  updatedAt?: Maybe<Scalars['DateTime']>
-  updatedById?: Maybe<Scalars['String']>
-  deletedAt?: Maybe<Scalars['DateTime']>
-  deletedById?: Maybe<Scalars['String']>
-  version: Scalars['Int']
-}
-
-export type BaseModel = BaseGraphQlObject & {
-  __typename?: 'BaseModel'
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdById: Scalars['String']
-  updatedAt?: Maybe<Scalars['DateTime']>
-  updatedById?: Maybe<Scalars['String']>
-  deletedAt?: Maybe<Scalars['DateTime']>
-  deletedById?: Maybe<Scalars['String']>
-  version: Scalars['Int']
-}
-
-export type BaseModelUuid = BaseGraphQlObject & {
-  __typename?: 'BaseModelUUID'
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdById: Scalars['String']
-  updatedAt?: Maybe<Scalars['DateTime']>
-  updatedById?: Maybe<Scalars['String']>
-  deletedAt?: Maybe<Scalars['DateTime']>
-  deletedById?: Maybe<Scalars['String']>
-  version: Scalars['Int']
-}
-
-export type BaseWhereInput = {
-  id_eq?: Maybe<Scalars['String']>
-  id_in?: Maybe<Array<Scalars['String']>>
-  createdAt_eq?: Maybe<Scalars['String']>
-  createdAt_lt?: Maybe<Scalars['String']>
-  createdAt_lte?: Maybe<Scalars['String']>
-  createdAt_gt?: Maybe<Scalars['String']>
-  createdAt_gte?: Maybe<Scalars['String']>
-  createdById_eq?: Maybe<Scalars['String']>
-  updatedAt_eq?: Maybe<Scalars['String']>
-  updatedAt_lt?: Maybe<Scalars['String']>
-  updatedAt_lte?: Maybe<Scalars['String']>
-  updatedAt_gt?: Maybe<Scalars['String']>
-  updatedAt_gte?: Maybe<Scalars['String']>
-  updatedById_eq?: Maybe<Scalars['String']>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['String']>
-  deletedAt_lt?: Maybe<Scalars['String']>
-  deletedAt_lte?: Maybe<Scalars['String']>
-  deletedAt_gt?: Maybe<Scalars['String']>
-  deletedAt_gte?: Maybe<Scalars['String']>
-  deletedById_eq?: Maybe<Scalars['String']>
-}
-
-export type Block = BaseGraphQlObject & {
-  __typename?: 'Block'
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdById: Scalars['String']
-  updatedAt?: Maybe<Scalars['DateTime']>
-  updatedById?: Maybe<Scalars['String']>
-  deletedAt?: Maybe<Scalars['DateTime']>
-  deletedById?: Maybe<Scalars['String']>
-  version: Scalars['Int']
-  block: Scalars['Int']
-  executedAt: Scalars['DateTime']
-  network: Network
-  membershipregisteredAtBlock?: Maybe<Array<Membership>>
-}
-
-export type BlockConnection = {
-  __typename?: 'BlockConnection'
-  totalCount: Scalars['Int']
-  edges: Array<BlockEdge>
-  pageInfo: PageInfo
-}
-
-export type BlockCreateInput = {
-  block: Scalars['Float']
-  executedAt: Scalars['DateTime']
-  network: Network
-}
-
-export type BlockEdge = {
-  __typename?: 'BlockEdge'
-  node: Block
-  cursor: Scalars['String']
-}
-
-export enum BlockOrderByInput {
-  CreatedAtAsc = 'createdAt_ASC',
-  CreatedAtDesc = 'createdAt_DESC',
-  UpdatedAtAsc = 'updatedAt_ASC',
-  UpdatedAtDesc = 'updatedAt_DESC',
-  DeletedAtAsc = 'deletedAt_ASC',
-  DeletedAtDesc = 'deletedAt_DESC',
-  BlockAsc = 'block_ASC',
-  BlockDesc = 'block_DESC',
-  ExecutedAtAsc = 'executedAt_ASC',
-  ExecutedAtDesc = 'executedAt_DESC',
-  NetworkAsc = 'network_ASC',
-  NetworkDesc = 'network_DESC',
-}
-
-export type BlockUpdateInput = {
-  block?: Maybe<Scalars['Float']>
-  executedAt?: Maybe<Scalars['DateTime']>
-  network?: Maybe<Network>
-}
-
-export type BlockWhereInput = {
-  id_eq?: Maybe<Scalars['ID']>
-  id_in?: Maybe<Array<Scalars['ID']>>
-  createdAt_eq?: Maybe<Scalars['DateTime']>
-  createdAt_lt?: Maybe<Scalars['DateTime']>
-  createdAt_lte?: Maybe<Scalars['DateTime']>
-  createdAt_gt?: Maybe<Scalars['DateTime']>
-  createdAt_gte?: Maybe<Scalars['DateTime']>
-  createdById_eq?: Maybe<Scalars['ID']>
-  createdById_in?: Maybe<Array<Scalars['ID']>>
-  updatedAt_eq?: Maybe<Scalars['DateTime']>
-  updatedAt_lt?: Maybe<Scalars['DateTime']>
-  updatedAt_lte?: Maybe<Scalars['DateTime']>
-  updatedAt_gt?: Maybe<Scalars['DateTime']>
-  updatedAt_gte?: Maybe<Scalars['DateTime']>
-  updatedById_eq?: Maybe<Scalars['ID']>
-  updatedById_in?: Maybe<Array<Scalars['ID']>>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['DateTime']>
-  deletedAt_lt?: Maybe<Scalars['DateTime']>
-  deletedAt_lte?: Maybe<Scalars['DateTime']>
-  deletedAt_gt?: Maybe<Scalars['DateTime']>
-  deletedAt_gte?: Maybe<Scalars['DateTime']>
-  deletedById_eq?: Maybe<Scalars['ID']>
-  deletedById_in?: Maybe<Array<Scalars['ID']>>
-  block_eq?: Maybe<Scalars['Int']>
-  block_gt?: Maybe<Scalars['Int']>
-  block_gte?: Maybe<Scalars['Int']>
-  block_lt?: Maybe<Scalars['Int']>
-  block_lte?: Maybe<Scalars['Int']>
-  block_in?: Maybe<Array<Scalars['Int']>>
-  executedAt_eq?: Maybe<Scalars['DateTime']>
-  executedAt_lt?: Maybe<Scalars['DateTime']>
-  executedAt_lte?: Maybe<Scalars['DateTime']>
-  executedAt_gt?: Maybe<Scalars['DateTime']>
-  executedAt_gte?: Maybe<Scalars['DateTime']>
-  network_eq?: Maybe<Network>
-  network_in?: Maybe<Array<Network>>
-}
-
-export type BlockWhereUniqueInput = {
-  id: Scalars['ID']
-}
-
-export type DeleteResponse = {
-  id: Scalars['ID']
-}
-
-export type MembersByHandleFtsOutput = {
-  __typename?: 'MembersByHandleFTSOutput'
-  item: MembersByHandleSearchResult
-  rank: Scalars['Float']
-  isTypeOf: Scalars['String']
-  highlight: Scalars['String']
-}
-
-export type MembersByHandleSearchResult = Membership
-
-/** Stored information about a registered user */
-export type Membership = BaseGraphQlObject & {
-  __typename?: 'Membership'
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdById: Scalars['String']
-  updatedAt?: Maybe<Scalars['DateTime']>
-  updatedById?: Maybe<Scalars['String']>
-  deletedAt?: Maybe<Scalars['DateTime']>
-  deletedById?: Maybe<Scalars['String']>
-  version: Scalars['Int']
-  /** The unique handle chosen by member */
-  handle: Scalars['String']
-  /** Member's name */
-  name?: Maybe<Scalars['String']>
-  /** A Url to member's Avatar image */
-  avatarUri?: Maybe<Scalars['String']>
-  /** Short text chosen by member to share information about themselves */
-  about?: Maybe<Scalars['String']>
-  /** Member's controller account id */
-  controllerAccount: Scalars['String']
-  /** Member's root account id */
-  rootAccount: Scalars['String']
-  registeredAtBlock: Block
-  registeredAtBlockId: Scalars['String']
-  /** Timestamp when member was registered */
-  registeredAtTime: Scalars['DateTime']
-  /** How the member was registered */
-  entry: MembershipEntryMethod
-  /** Whether member has been verified by membership working group. */
-  isVerified: Scalars['Boolean']
-  /** Staking accounts bounded to membership. */
-  boundAccounts: Array<Scalars['String']>
-  /** Current count of invites left to send. */
-  inviteCount: Scalars['Int']
-  invitees: Array<Membership>
-  invitedBy?: Maybe<Membership>
-  invitedById?: Maybe<Scalars['String']>
-  referredMembers: Array<Membership>
-  referredBy?: Maybe<Membership>
-  referredById?: Maybe<Scalars['String']>
-}
-
-export type MembershipConnection = {
-  __typename?: 'MembershipConnection'
-  totalCount: Scalars['Int']
-  edges: Array<MembershipEdge>
-  pageInfo: PageInfo
-}
-
-export type MembershipCreateInput = {
-  handle: Scalars['String']
-  name?: Maybe<Scalars['String']>
-  avatarUri?: Maybe<Scalars['String']>
-  about?: Maybe<Scalars['String']>
-  controllerAccount: Scalars['String']
-  rootAccount: Scalars['String']
-  registeredAtBlockId: Scalars['ID']
-  registeredAtTime: Scalars['DateTime']
-  entry: MembershipEntryMethod
-  isVerified: Scalars['Boolean']
-  boundAccounts: Array<Scalars['String']>
-  inviteCount: Scalars['Float']
-  invitedById?: Maybe<Scalars['ID']>
-  referredById?: Maybe<Scalars['ID']>
-}
-
-export type MembershipEdge = {
-  __typename?: 'MembershipEdge'
-  node: Membership
-  cursor: Scalars['String']
-}
-
-export enum MembershipEntryMethod {
-  Paid = 'PAID',
-  Invited = 'INVITED',
-  Genesis = 'GENESIS',
-}
-
-export enum MembershipOrderByInput {
-  CreatedAtAsc = 'createdAt_ASC',
-  CreatedAtDesc = 'createdAt_DESC',
-  UpdatedAtAsc = 'updatedAt_ASC',
-  UpdatedAtDesc = 'updatedAt_DESC',
-  DeletedAtAsc = 'deletedAt_ASC',
-  DeletedAtDesc = 'deletedAt_DESC',
-  HandleAsc = 'handle_ASC',
-  HandleDesc = 'handle_DESC',
-  NameAsc = 'name_ASC',
-  NameDesc = 'name_DESC',
-  AvatarUriAsc = 'avatarUri_ASC',
-  AvatarUriDesc = 'avatarUri_DESC',
-  AboutAsc = 'about_ASC',
-  AboutDesc = 'about_DESC',
-  ControllerAccountAsc = 'controllerAccount_ASC',
-  ControllerAccountDesc = 'controllerAccount_DESC',
-  RootAccountAsc = 'rootAccount_ASC',
-  RootAccountDesc = 'rootAccount_DESC',
-  RegisteredAtBlockIdAsc = 'registeredAtBlockId_ASC',
-  RegisteredAtBlockIdDesc = 'registeredAtBlockId_DESC',
-  RegisteredAtTimeAsc = 'registeredAtTime_ASC',
-  RegisteredAtTimeDesc = 'registeredAtTime_DESC',
-  EntryAsc = 'entry_ASC',
-  EntryDesc = 'entry_DESC',
-  IsVerifiedAsc = 'isVerified_ASC',
-  IsVerifiedDesc = 'isVerified_DESC',
-  InviteCountAsc = 'inviteCount_ASC',
-  InviteCountDesc = 'inviteCount_DESC',
-  InvitedByIdAsc = 'invitedById_ASC',
-  InvitedByIdDesc = 'invitedById_DESC',
-  ReferredByIdAsc = 'referredById_ASC',
-  ReferredByIdDesc = 'referredById_DESC',
-}
-
-export type MembershipSystem = BaseGraphQlObject & {
-  __typename?: 'MembershipSystem'
-  id: Scalars['ID']
-  createdAt: Scalars['DateTime']
-  createdById: Scalars['String']
-  updatedAt?: Maybe<Scalars['DateTime']>
-  updatedById?: Maybe<Scalars['String']>
-  deletedAt?: Maybe<Scalars['DateTime']>
-  deletedById?: Maybe<Scalars['String']>
-  version: Scalars['Int']
-  /** Initial invitation count of a new member. */
-  defaultInviteCount: Scalars['Int']
-  /** Current price to buy a membership. */
-  membershipPrice: Scalars['BigInt']
-  /** Amount of tokens diverted to invitor. */
-  referralCut: Scalars['BigInt']
-  /** The initial, locked, balance credited to controller account of invitee. */
-  invitedInitialBalance: Scalars['BigInt']
-}
-
-export type MembershipSystemConnection = {
-  __typename?: 'MembershipSystemConnection'
-  totalCount: Scalars['Int']
-  edges: Array<MembershipSystemEdge>
-  pageInfo: PageInfo
-}
-
-export type MembershipSystemCreateInput = {
-  defaultInviteCount: Scalars['Float']
-  membershipPrice: Scalars['BigInt']
-  referralCut: Scalars['BigInt']
-  invitedInitialBalance: Scalars['BigInt']
-}
-
-export type MembershipSystemEdge = {
-  __typename?: 'MembershipSystemEdge'
-  node: MembershipSystem
-  cursor: Scalars['String']
-}
-
-export enum MembershipSystemOrderByInput {
-  CreatedAtAsc = 'createdAt_ASC',
-  CreatedAtDesc = 'createdAt_DESC',
-  UpdatedAtAsc = 'updatedAt_ASC',
-  UpdatedAtDesc = 'updatedAt_DESC',
-  DeletedAtAsc = 'deletedAt_ASC',
-  DeletedAtDesc = 'deletedAt_DESC',
-  DefaultInviteCountAsc = 'defaultInviteCount_ASC',
-  DefaultInviteCountDesc = 'defaultInviteCount_DESC',
-  MembershipPriceAsc = 'membershipPrice_ASC',
-  MembershipPriceDesc = 'membershipPrice_DESC',
-  ReferralCutAsc = 'referralCut_ASC',
-  ReferralCutDesc = 'referralCut_DESC',
-  InvitedInitialBalanceAsc = 'invitedInitialBalance_ASC',
-  InvitedInitialBalanceDesc = 'invitedInitialBalance_DESC',
-}
-
-export type MembershipSystemUpdateInput = {
-  defaultInviteCount?: Maybe<Scalars['Float']>
-  membershipPrice?: Maybe<Scalars['BigInt']>
-  referralCut?: Maybe<Scalars['BigInt']>
-  invitedInitialBalance?: Maybe<Scalars['BigInt']>
-}
-
-export type MembershipSystemWhereInput = {
-  id_eq?: Maybe<Scalars['ID']>
-  id_in?: Maybe<Array<Scalars['ID']>>
-  createdAt_eq?: Maybe<Scalars['DateTime']>
-  createdAt_lt?: Maybe<Scalars['DateTime']>
-  createdAt_lte?: Maybe<Scalars['DateTime']>
-  createdAt_gt?: Maybe<Scalars['DateTime']>
-  createdAt_gte?: Maybe<Scalars['DateTime']>
-  createdById_eq?: Maybe<Scalars['ID']>
-  createdById_in?: Maybe<Array<Scalars['ID']>>
-  updatedAt_eq?: Maybe<Scalars['DateTime']>
-  updatedAt_lt?: Maybe<Scalars['DateTime']>
-  updatedAt_lte?: Maybe<Scalars['DateTime']>
-  updatedAt_gt?: Maybe<Scalars['DateTime']>
-  updatedAt_gte?: Maybe<Scalars['DateTime']>
-  updatedById_eq?: Maybe<Scalars['ID']>
-  updatedById_in?: Maybe<Array<Scalars['ID']>>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['DateTime']>
-  deletedAt_lt?: Maybe<Scalars['DateTime']>
-  deletedAt_lte?: Maybe<Scalars['DateTime']>
-  deletedAt_gt?: Maybe<Scalars['DateTime']>
-  deletedAt_gte?: Maybe<Scalars['DateTime']>
-  deletedById_eq?: Maybe<Scalars['ID']>
-  deletedById_in?: Maybe<Array<Scalars['ID']>>
-  defaultInviteCount_eq?: Maybe<Scalars['Int']>
-  defaultInviteCount_gt?: Maybe<Scalars['Int']>
-  defaultInviteCount_gte?: Maybe<Scalars['Int']>
-  defaultInviteCount_lt?: Maybe<Scalars['Int']>
-  defaultInviteCount_lte?: Maybe<Scalars['Int']>
-  defaultInviteCount_in?: Maybe<Array<Scalars['Int']>>
-  membershipPrice_eq?: Maybe<Scalars['BigInt']>
-  membershipPrice_gt?: Maybe<Scalars['BigInt']>
-  membershipPrice_gte?: Maybe<Scalars['BigInt']>
-  membershipPrice_lt?: Maybe<Scalars['BigInt']>
-  membershipPrice_lte?: Maybe<Scalars['BigInt']>
-  membershipPrice_in?: Maybe<Array<Scalars['BigInt']>>
-  referralCut_eq?: Maybe<Scalars['BigInt']>
-  referralCut_gt?: Maybe<Scalars['BigInt']>
-  referralCut_gte?: Maybe<Scalars['BigInt']>
-  referralCut_lt?: Maybe<Scalars['BigInt']>
-  referralCut_lte?: Maybe<Scalars['BigInt']>
-  referralCut_in?: Maybe<Array<Scalars['BigInt']>>
-  invitedInitialBalance_eq?: Maybe<Scalars['BigInt']>
-  invitedInitialBalance_gt?: Maybe<Scalars['BigInt']>
-  invitedInitialBalance_gte?: Maybe<Scalars['BigInt']>
-  invitedInitialBalance_lt?: Maybe<Scalars['BigInt']>
-  invitedInitialBalance_lte?: Maybe<Scalars['BigInt']>
-  invitedInitialBalance_in?: Maybe<Array<Scalars['BigInt']>>
-}
-
-export type MembershipSystemWhereUniqueInput = {
-  id: Scalars['ID']
-}
-
-export type MembershipUpdateInput = {
-  handle?: Maybe<Scalars['String']>
-  name?: Maybe<Scalars['String']>
-  avatarUri?: Maybe<Scalars['String']>
-  about?: Maybe<Scalars['String']>
-  controllerAccount?: Maybe<Scalars['String']>
-  rootAccount?: Maybe<Scalars['String']>
-  registeredAtBlockId?: Maybe<Scalars['ID']>
-  registeredAtTime?: Maybe<Scalars['DateTime']>
-  entry?: Maybe<MembershipEntryMethod>
-  isVerified?: Maybe<Scalars['Boolean']>
-  boundAccounts?: Maybe<Array<Scalars['String']>>
-  inviteCount?: Maybe<Scalars['Float']>
-  invitedById?: Maybe<Scalars['ID']>
-  referredById?: Maybe<Scalars['ID']>
-}
-
-export type MembershipWhereInput = {
-  id_eq?: Maybe<Scalars['ID']>
-  id_in?: Maybe<Array<Scalars['ID']>>
-  createdAt_eq?: Maybe<Scalars['DateTime']>
-  createdAt_lt?: Maybe<Scalars['DateTime']>
-  createdAt_lte?: Maybe<Scalars['DateTime']>
-  createdAt_gt?: Maybe<Scalars['DateTime']>
-  createdAt_gte?: Maybe<Scalars['DateTime']>
-  createdById_eq?: Maybe<Scalars['ID']>
-  createdById_in?: Maybe<Array<Scalars['ID']>>
-  updatedAt_eq?: Maybe<Scalars['DateTime']>
-  updatedAt_lt?: Maybe<Scalars['DateTime']>
-  updatedAt_lte?: Maybe<Scalars['DateTime']>
-  updatedAt_gt?: Maybe<Scalars['DateTime']>
-  updatedAt_gte?: Maybe<Scalars['DateTime']>
-  updatedById_eq?: Maybe<Scalars['ID']>
-  updatedById_in?: Maybe<Array<Scalars['ID']>>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['DateTime']>
-  deletedAt_lt?: Maybe<Scalars['DateTime']>
-  deletedAt_lte?: Maybe<Scalars['DateTime']>
-  deletedAt_gt?: Maybe<Scalars['DateTime']>
-  deletedAt_gte?: Maybe<Scalars['DateTime']>
-  deletedById_eq?: Maybe<Scalars['ID']>
-  deletedById_in?: Maybe<Array<Scalars['ID']>>
-  handle_eq?: Maybe<Scalars['String']>
-  handle_contains?: Maybe<Scalars['String']>
-  handle_startsWith?: Maybe<Scalars['String']>
-  handle_endsWith?: Maybe<Scalars['String']>
-  handle_in?: Maybe<Array<Scalars['String']>>
-  name_eq?: Maybe<Scalars['String']>
-  name_contains?: Maybe<Scalars['String']>
-  name_startsWith?: Maybe<Scalars['String']>
-  name_endsWith?: Maybe<Scalars['String']>
-  name_in?: Maybe<Array<Scalars['String']>>
-  avatarUri_eq?: Maybe<Scalars['String']>
-  avatarUri_contains?: Maybe<Scalars['String']>
-  avatarUri_startsWith?: Maybe<Scalars['String']>
-  avatarUri_endsWith?: Maybe<Scalars['String']>
-  avatarUri_in?: Maybe<Array<Scalars['String']>>
-  about_eq?: Maybe<Scalars['String']>
-  about_contains?: Maybe<Scalars['String']>
-  about_startsWith?: Maybe<Scalars['String']>
-  about_endsWith?: Maybe<Scalars['String']>
-  about_in?: Maybe<Array<Scalars['String']>>
-  controllerAccount_eq?: Maybe<Scalars['String']>
-  controllerAccount_contains?: Maybe<Scalars['String']>
-  controllerAccount_startsWith?: Maybe<Scalars['String']>
-  controllerAccount_endsWith?: Maybe<Scalars['String']>
-  controllerAccount_in?: Maybe<Array<Scalars['String']>>
-  rootAccount_eq?: Maybe<Scalars['String']>
-  rootAccount_contains?: Maybe<Scalars['String']>
-  rootAccount_startsWith?: Maybe<Scalars['String']>
-  rootAccount_endsWith?: Maybe<Scalars['String']>
-  rootAccount_in?: Maybe<Array<Scalars['String']>>
-  registeredAtBlockId_eq?: Maybe<Scalars['ID']>
-  registeredAtBlockId_in?: Maybe<Array<Scalars['ID']>>
-  registeredAtTime_eq?: Maybe<Scalars['DateTime']>
-  registeredAtTime_lt?: Maybe<Scalars['DateTime']>
-  registeredAtTime_lte?: Maybe<Scalars['DateTime']>
-  registeredAtTime_gt?: Maybe<Scalars['DateTime']>
-  registeredAtTime_gte?: Maybe<Scalars['DateTime']>
-  entry_eq?: Maybe<MembershipEntryMethod>
-  entry_in?: Maybe<Array<MembershipEntryMethod>>
-  isVerified_eq?: Maybe<Scalars['Boolean']>
-  isVerified_in?: Maybe<Array<Scalars['Boolean']>>
-  inviteCount_eq?: Maybe<Scalars['Int']>
-  inviteCount_gt?: Maybe<Scalars['Int']>
-  inviteCount_gte?: Maybe<Scalars['Int']>
-  inviteCount_lt?: Maybe<Scalars['Int']>
-  inviteCount_lte?: Maybe<Scalars['Int']>
-  inviteCount_in?: Maybe<Array<Scalars['Int']>>
-  invitedById_eq?: Maybe<Scalars['ID']>
-  invitedById_in?: Maybe<Array<Scalars['ID']>>
-  referredById_eq?: Maybe<Scalars['ID']>
-  referredById_in?: Maybe<Array<Scalars['ID']>>
-}
-
-export type MembershipWhereUniqueInput = {
-  id?: Maybe<Scalars['ID']>
-  handle?: Maybe<Scalars['String']>
-}
-
-export enum Network {
-  Babylon = 'BABYLON',
-  Alexandria = 'ALEXANDRIA',
-  Rome = 'ROME',
-  Olympia = 'OLYMPIA',
-}
-
-export type PageInfo = {
-  __typename?: 'PageInfo'
-  hasNextPage: Scalars['Boolean']
-  hasPreviousPage: Scalars['Boolean']
-  startCursor?: Maybe<Scalars['String']>
-  endCursor?: Maybe<Scalars['String']>
-}
-
-export type ProcessorState = {
-  __typename?: 'ProcessorState'
-  lastCompleteBlock: Scalars['Float']
-  lastProcessedEvent: Scalars['String']
-  indexerHead: Scalars['Float']
-  chainHead: Scalars['Float']
-}
-
-export type Query = {
-  __typename?: 'Query'
-  blocks: Array<Block>
-  block?: Maybe<Block>
-  blocksConnection: BlockConnection
-  membershipSystems: Array<MembershipSystem>
-  membershipSystem?: Maybe<MembershipSystem>
-  membershipSystemsConnection: MembershipSystemConnection
-  memberships: Array<Membership>
-  membership?: Maybe<Membership>
-  membershipsConnection: MembershipConnection
-  membersByHandle: Array<MembersByHandleFtsOutput>
-}
-
-export type QueryBlocksArgs = {
-  offset?: Maybe<Scalars['Int']>
-  limit?: Maybe<Scalars['Int']>
-  where?: Maybe<BlockWhereInput>
-  orderBy?: Maybe<BlockOrderByInput>
-}
-
-export type QueryBlockArgs = {
-  where: BlockWhereUniqueInput
-}
-
-export type QueryBlocksConnectionArgs = {
-  first?: Maybe<Scalars['Int']>
-  after?: Maybe<Scalars['String']>
-  last?: Maybe<Scalars['Int']>
-  before?: Maybe<Scalars['String']>
-  where?: Maybe<BlockWhereInput>
-  orderBy?: Maybe<BlockOrderByInput>
-}
-
-export type QueryMembershipSystemsArgs = {
-  offset?: Maybe<Scalars['Int']>
-  limit?: Maybe<Scalars['Int']>
-  where?: Maybe<MembershipSystemWhereInput>
-  orderBy?: Maybe<MembershipSystemOrderByInput>
-}
-
-export type QueryMembershipSystemArgs = {
-  where: MembershipSystemWhereUniqueInput
-}
-
-export type QueryMembershipSystemsConnectionArgs = {
-  first?: Maybe<Scalars['Int']>
-  after?: Maybe<Scalars['String']>
-  last?: Maybe<Scalars['Int']>
-  before?: Maybe<Scalars['String']>
-  where?: Maybe<MembershipSystemWhereInput>
-  orderBy?: Maybe<MembershipSystemOrderByInput>
-}
-
-export type QueryMembershipsArgs = {
-  offset?: Maybe<Scalars['Int']>
-  limit?: Maybe<Scalars['Int']>
-  where?: Maybe<MembershipWhereInput>
-  orderBy?: Maybe<MembershipOrderByInput>
-}
-
-export type QueryMembershipArgs = {
-  where: MembershipWhereUniqueInput
-}
-
-export type QueryMembershipsConnectionArgs = {
-  first?: Maybe<Scalars['Int']>
-  after?: Maybe<Scalars['String']>
-  last?: Maybe<Scalars['Int']>
-  before?: Maybe<Scalars['String']>
-  where?: Maybe<MembershipWhereInput>
-  orderBy?: Maybe<MembershipOrderByInput>
-}
-
-export type QueryMembersByHandleArgs = {
-  whereMembership?: Maybe<MembershipWhereInput>
-  skip?: Maybe<Scalars['Int']>
-  limit?: Maybe<Scalars['Int']>
-  text: Scalars['String']
-}
-
-export type StandardDeleteResponse = {
-  __typename?: 'StandardDeleteResponse'
-  id: Scalars['ID']
-}
-
-export type Subscription = {
-  __typename?: 'Subscription'
-  stateSubscription: ProcessorState
-}

+ 5 - 1
cli/src/Types.ts

@@ -167,15 +167,19 @@ export type VideoFileMetadata = VideoFFProbeMetadata & {
 export type VideoInputParameters = Omit<IVideoMetadata, 'video' | 'thumbnailPhoto'> & {
   videoPath?: string
   thumbnailPhotoPath?: string
+  enableComments?: boolean
 }
 
-export type ChannelInputParameters = Omit<IChannelMetadata, 'coverPhoto' | 'avatarPhoto'> & {
+export type ChannelCreationInputParameters = Omit<IChannelMetadata, 'coverPhoto' | 'avatarPhoto'> & {
   coverPhotoPath?: string
   avatarPhotoPath?: string
   rewardAccount?: string
   collaborators?: number[]
+  moderators?: number[]
 }
 
+export type ChannelUpdateInputParameters = Omit<ChannelCreationInputParameters, 'moderators'>
+
 export type ChannelCategoryInputParameters = IChannelCategoryMetadata
 
 export type VideoCategoryInputParameters = IVideoCategoryMetadata

+ 10 - 13
cli/src/base/AccountsCommandBase.ts

@@ -35,7 +35,7 @@ export const STAKING_ACCOUNT_CANDIDATE_STAKE = new BN(200)
  * Where: APP_DATA_PATH is provided by StateAwareCommandBase and ACCOUNTS_DIRNAME is a const (see above).
  */
 export default abstract class AccountsCommandBase extends ApiCommandBase {
-  private selectedMember: [MemberId, Membership] | undefined
+  private selectedMember: MemberDetails | undefined
   private _keyring: KeyringInstance | undefined
 
   private get keyring(): KeyringInstance {
@@ -321,20 +321,20 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     }
   }
 
-  async getRequiredMemberContext(useSelected = false, allowedIds?: MemberId[]): Promise<[MemberId, Membership]> {
+  async getRequiredMemberContext(useSelected = false, allowedIds?: MemberId[]): Promise<MemberDetails> {
     if (
       useSelected &&
       this.selectedMember &&
-      (!allowedIds || allowedIds.some((id) => id.eq(this.selectedMember?.[0])))
+      (!allowedIds || allowedIds.some((id) => id.eq(this.selectedMember?.id)))
     ) {
       return this.selectedMember
     }
 
-    const membersEntries = allowedIds
-      ? await this.getApi().memberEntriesByIds(allowedIds)
-      : await this.getApi().allMemberEntries()
+    const membersDetails = allowedIds
+      ? await this.getApi().membersDetailsByIds(allowedIds)
+      : await this.getApi().allMembersDetails()
     const availableMemberships = await Promise.all(
-      membersEntries.filter(([, m]) => this.isKeyAvailable(m.controller_account.toString()))
+      membersDetails.filter((m) => this.isKeyAvailable(m.membership.controller_account.toString()))
     )
 
     if (!availableMemberships.length) {
@@ -354,15 +354,12 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     return this.selectedMember
   }
 
-  async promptForMember(
-    availableMemberships: [MemberId, Membership][],
-    message = 'Choose a member'
-  ): Promise<[MemberId, Membership]> {
+  async promptForMember(availableMemberships: MemberDetails[], message = 'Choose a member'): Promise<MemberDetails> {
     const memberIndex = await this.simplePrompt({
       type: 'list',
       message,
-      choices: availableMemberships.map(([, membership], i) => ({
-        name: membership.handle.toString(),
+      choices: availableMemberships.map((m, i) => ({
+        name: `id: ${m.id}, handle: ${memberHandle(m)}`,
         value: i,
       })),
     })

+ 8 - 8
cli/src/base/ApiCommandBase.ts

@@ -84,8 +84,8 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         apiUri = await this.promptForApiUri()
       }
 
+      // Query node api
       let queryNodeUri: string | null | undefined = this.getPreservedState().queryNodeUri
-
       if (this.requiresQueryNode && !queryNodeUri) {
         this.warn('Query node endpoint uri is required in order to run this command!')
         queryNodeUri = await this.promptForQueryNodeUri(true)
@@ -93,9 +93,15 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         this.warn("You haven't provided a Joystream query node uri for the CLI to connect to yet!")
         queryNodeUri = await this.promptForQueryNodeUri()
       }
+      this.queryNodeApi = queryNodeUri
+        ? new QueryNodeApi(queryNodeUri, (err) => {
+            this.warn(`Query node error: ${err.networkError?.message || err.graphQLErrors?.join('\n')}`)
+          })
+        : null
 
+      // Substrate api
       const { metadataCache } = this.getPreservedState()
-      this.api = await Api.create(apiUri, metadataCache, queryNodeUri === 'none' ? undefined : queryNodeUri)
+      this.api = await Api.create(apiUri, metadataCache, this.queryNodeApi || undefined)
 
       const { genesisHash, runtimeVersion } = this.getOriginalApi()
       const metadataKey = `${genesisHash}-${runtimeVersion.specVersion}`
@@ -104,12 +110,6 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         metadataCache[metadataKey] = await this.getOriginalApi().runtimeMetadata.toJSON()
         await this.setPreservedState({ metadataCache })
       }
-
-      this.queryNodeApi = queryNodeUri
-        ? new QueryNodeApi(queryNodeUri, (err) => {
-            this.warn(`Query node error: ${err.networkError?.message || err.graphQLErrors?.join('\n')}`)
-          })
-        : null
     }
   }
 

+ 15 - 8
cli/src/base/ContentDirectoryCommandBase.ts

@@ -81,7 +81,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
         return this.getCuratorContext(channel.owner.asType('Curators'))
       }
     } else {
-      const [id, membership] = await this.getRequiredMemberContext(false, [channel.owner.asType('Member')])
+      const { id, membership } = await this.getRequiredMemberContext(false, [channel.owner.asType('Member')])
       return [
         createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
         membership.controller_account.toString(),
@@ -90,13 +90,20 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
   }
 
   async getChannelCollaboratorActor(channel: Channel): Promise<[ContentActor, string]> {
-    const [id, membership] = await this.getRequiredMemberContext(false, Array.from(channel.collaborators))
+    const { id, membership } = await this.getRequiredMemberContext(false, Array.from(channel.collaborators))
     return [
-      createType<ContentActor, 'ContentActor'>('ContentActor', { Collaborator: id }),
+      createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
       membership.controller_account.toString(),
     ]
   }
 
+  isChannelOwner(channel: Channel, actor: ContentActor): boolean {
+    return channel.owner.isOfType('Curators')
+      ? (actor.isOfType('Curator') && actor.asType('Curator')[0].eq(channel.owner.asType('Curators'))) ||
+          actor.isOfType('Lead')
+      : actor.isOfType('Member') && actor.asType('Member').eq(channel.owner.asType('Member'))
+  }
+
   async getChannelManagementActor(
     channel: Channel,
     context: ChannelManagementContext
@@ -280,7 +287,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     context: Exclude<keyof typeof ContentActor.typeDefinitions, 'Collaborator'>
   ): Promise<[ContentActor, string]> {
     if (context === 'Member') {
-      const [id, membership] = await this.getRequiredMemberContext()
+      const { id, membership } = await this.getRequiredMemberContext()
       return [
         createType<ContentActor, 'ContentActor'>('ContentActor', { Member: id }),
         membership.controller_account.toString(),
@@ -299,10 +306,10 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     throw new Error(`Unrecognized context: ${context}`)
   }
 
-  async validateCollaborators(collaborators: number[] | MemberId[]): Promise<void> {
-    const collaboratorMembers = await this.getApi().getMembers(collaborators)
-    if (collaboratorMembers.length < collaborators.length || collaboratorMembers.some((m) => m.isEmpty)) {
-      this.error(`Invalid collaborator set! All collaborators must be existing members.`, {
+  async validateMemberIdsSet(ids: number[] | MemberId[], setName: 'collaborator' | 'moderator'): Promise<void> {
+    const members = await this.getApi().getMembers(ids)
+    if (members.length < ids.length || members.some((m) => m.isEmpty)) {
+      this.error(`Invalid ${setName} set! All ${setName} set members must be existing members!`, {
         exit: ExitCodes.InvalidInput,
       })
     }

+ 3 - 3
cli/src/base/UploadCommandBase.ts

@@ -26,7 +26,7 @@ import { u8aToHex, formatBalance } from '@polkadot/util'
 import { KeyringPair } from '@polkadot/keyring/types'
 import FormData from 'form-data'
 import BN from 'bn.js'
-import { createTypeFromConstructor } from '@joystream/types'
+import { createType } from '@joystream/types'
 import { StorageAssets } from '@joystream/types/content'
 
 ffmpeg.setFfprobePath(ffprobeInstaller.path)
@@ -187,7 +187,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
   }
 
   async generateDataObjectParameters(filePath: string): Promise<DataObjectCreationParameters> {
-    return createTypeFromConstructor(DataObjectCreationParameters, {
+    return createType<DataObjectCreationParameters, 'DataObjectCreationParameters'>('DataObjectCreationParameters', {
       size: this.getFileSize(filePath),
       ipfsContentId: await this.calculateFileHash(filePath),
     })
@@ -359,7 +359,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
           )} (recoverable on data object(s) removal)\n` +
           `Are you sure you want to continue?`
       )
-      return createTypeFromConstructor(StorageAssets, {
+      return createType<StorageAssets, 'StorageAssets'>('StorageAssets', {
         expected_data_size_fee: feePerMB,
         object_creation_list: resolvedAssets.map((a) => a.parameters),
       })

+ 13 - 4
cli/src/commands/content/channel.ts

@@ -1,5 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { displayCollapsedRow, displayHeader } from '../../helpers/display'
+import { displayCollapsedRow, displayHeader, memberHandle } from '../../helpers/display'
+import { BTreeSet } from '@polkadot/types'
+import { MemberId } from '@joystream/types/common'
 
 export default class ChannelCommand extends ContentDirectoryCommandBase {
   static description = 'Show Channel details by id.'
@@ -11,6 +13,12 @@ export default class ChannelCommand extends ContentDirectoryCommandBase {
     },
   ]
 
+  async displayMembersSet(set: BTreeSet<MemberId>): Promise<void> {
+    const ids = Array.from(set)
+    const members = await this.getApi().membersDetailsByIds(ids)
+    this.log(members.length ? members.map((m) => `${m.id} (${memberHandle(m)})`).join(', ') : 'NONE')
+  }
+
   async run(): Promise<void> {
     const { channelId } = this.parse(ChannelCommand).args
     const channel = await this.getApi().channelById(channelId)
@@ -28,9 +36,10 @@ export default class ChannelCommand extends ContentDirectoryCommandBase {
       })
 
       displayHeader(`Collaborators`)
-      const collaboratorIds = Array.from(channel.collaborators)
-      const collaborators = await this.getApi().getMembers(collaboratorIds)
-      this.log(collaborators.map((c, i) => `${collaboratorIds[i].toString()} (${c.handle.toString()})`).join(', '))
+      await this.displayMembersSet(channel.collaborators)
+
+      displayHeader('Moderators')
+      await this.displayMembersSet(channel.moderators)
     } else {
       this.error(`Channel not found by channel id: "${channelId}"!`)
     }

+ 1 - 0
cli/src/commands/content/channels.ts

@@ -16,6 +16,7 @@ export default class ChannelsCommand extends ContentDirectoryCommandBase {
           'IsCensored': c.is_censored.toString(),
           'RewardAccount': c.reward_account ? shortAddress(c.reward_account.toString()) : 'NONE',
           'Collaborators': c.collaborators.size,
+          'Moderators': c.moderators.size,
         })),
         3
       )

+ 17 - 9
cli/src/commands/content/createChannel.ts

@@ -1,10 +1,10 @@
 import { getInputJson } from '../../helpers/InputOutput'
-import { ChannelInputParameters } from '../../Types'
+import { ChannelCreationInputParameters } from '../../Types'
 import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { flags } from '@oclif/command'
 import { createType } from '@joystream/types'
 import { ChannelCreationParameters } from '@joystream/types/content'
-import { ChannelInputSchema } from '../../schemas/ContentDirectory'
+import { ChannelCreationInputSchema } from '../../schemas/ContentDirectory'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import UploadCommandBase from '../../base/UploadCommandBase'
 import chalk from 'chalk'
@@ -29,14 +29,19 @@ export default class CreateChannelCommand extends UploadCommandBase {
       context = await this.promptForChannelCreationContext()
     }
     const [actor, address] = await this.getContentActor(context)
-    const [memberId] = await this.getRequiredMemberContext(true)
+    const { id: memberId } = await this.getRequiredMemberContext(true)
     const keypair = await this.getDecodedPair(address)
 
-    const channelInput = await getInputJson<ChannelInputParameters>(input, ChannelInputSchema)
+    const channelInput = await getInputJson<ChannelCreationInputParameters>(input, ChannelCreationInputSchema)
     const meta = asValidatedMetadata(ChannelMetadata, channelInput)
+    const { collaborators, moderators, rewardAccount } = channelInput
 
-    if (channelInput.collaborators) {
-      await this.validateCollaborators(channelInput.collaborators)
+    if (collaborators) {
+      await this.validateMemberIdsSet(collaborators, 'collaborator')
+    }
+
+    if (moderators) {
+      await this.validateMemberIdsSet(moderators, 'moderator')
     }
 
     const { coverPhotoPath, avatarPhotoPath } = channelInput
@@ -54,12 +59,15 @@ export default class CreateChannelCommand extends UploadCommandBase {
       {
         assets,
         meta: metadataToBytes(ChannelMetadata, meta),
-        collaborators: channelInput.collaborators,
-        reward_account: channelInput.rewardAccount,
+        collaborators,
+        moderators,
+        reward_account: rewardAccount,
       }
     )
 
-    this.jsonPrettyPrint(JSON.stringify({ assets: assets?.toJSON(), metadata: meta }))
+    this.jsonPrettyPrint(
+      JSON.stringify({ assets: assets?.toJSON(), metadata: meta, collaborators, moderators, rewardAccount })
+    )
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 

+ 12 - 7
cli/src/commands/content/createVideo.ts

@@ -2,7 +2,7 @@ import UploadCommandBase from '../../base/UploadCommandBase'
 import { getInputJson } from '../../helpers/InputOutput'
 import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import { VideoInputParameters, VideoFileMetadata } from '../../Types'
-import { createTypeFromConstructor } from '@joystream/types'
+import { createType } from '@joystream/types'
 import { flags } from '@oclif/command'
 import { VideoCreationParameters } from '@joystream/types/content'
 import { IVideoMetadata, VideoMetadata } from '@joystream/metadata-protobuf'
@@ -46,12 +46,13 @@ export default class CreateVideoCommand extends UploadCommandBase {
     // Get context
     const channel = await this.getApi().channelById(channelId)
     const [actor, address] = await this.getChannelManagementActor(channel, context)
-    const [memberId] = await this.getRequiredMemberContext(true)
+    const { id: memberId } = await this.getRequiredMemberContext(true)
     const keypair = await this.getDecodedPair(address)
 
     // Get input from file
     const videoCreationParametersInput = await getInputJson<VideoInputParameters>(input, VideoInputSchema)
     let meta = asValidatedMetadata(VideoMetadata, videoCreationParametersInput)
+    const { enableComments } = videoCreationParametersInput
 
     // Assets
     const { videoPath, thumbnailPhotoPath } = videoCreationParametersInput
@@ -69,12 +70,16 @@ export default class CreateVideoCommand extends UploadCommandBase {
 
     // Preare and send the extrinsic
     const assets = await this.prepareAssetsForExtrinsic(resolvedAssets)
-    const videoCreationParameters = createTypeFromConstructor(VideoCreationParameters, {
-      assets,
-      meta: metadataToBytes(VideoMetadata, meta),
-    })
+    const videoCreationParameters = createType<VideoCreationParameters, 'VideoCreationParameters'>(
+      'VideoCreationParameters',
+      {
+        assets,
+        meta: metadataToBytes(VideoMetadata, meta),
+        enable_comments: enableComments,
+      }
+    )
 
-    this.jsonPrettyPrint(JSON.stringify({ assets: assets?.toJSON(), metadata: meta }))
+    this.jsonPrettyPrint(JSON.stringify({ assets: assets?.toJSON(), metadata: meta, enableComments }))
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 

+ 2 - 2
cli/src/commands/content/deleteChannel.ts

@@ -1,7 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { flags } from '@oclif/command'
 import chalk from 'chalk'
-import { createTypeFromConstructor } from '@joystream/types'
+import { createType } from '@joystream/types'
 import { BagId } from '@joystream/types/storage'
 import ExitCodes from '../../ExitCodes'
 import { formatBalance } from '@polkadot/util'
@@ -42,7 +42,7 @@ export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
 
   async getDataObjectsInfoFromChain(channelId: number): Promise<[string, BN][]> {
     const dataObjects = await this.getApi().dataObjectsInBag(
-      createTypeFromConstructor(BagId, { Dynamic: { Channel: channelId } })
+      createType<BagId, 'BagId'>('BagId', { Dynamic: { Channel: channelId } })
     )
 
     if (dataObjects.length) {

+ 1 - 1
cli/src/commands/content/reuploadAssets.ts

@@ -20,7 +20,7 @@ export default class ReuploadVideoAssetsCommand extends UploadCommandBase {
     const { input } = this.parse(ReuploadVideoAssetsCommand).flags
 
     // Get context
-    const [memberId, membership] = await this.getRequiredMemberContext()
+    const { id: memberId, membership } = await this.getRequiredMemberContext()
 
     // Get input from file
     const inputData = await getInputJson<AssetsInput>(input, AssetsSchema)

+ 19 - 14
cli/src/commands/content/updateChannel.ts

@@ -1,11 +1,11 @@
 import { getInputJson } from '../../helpers/InputOutput'
 import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
-import { ChannelInputParameters } from '../../Types'
+import { ChannelUpdateInputParameters } from '../../Types'
 import { flags } from '@oclif/command'
 import UploadCommandBase from '../../base/UploadCommandBase'
 import { CreateInterface, createType } from '@joystream/types'
 import { ChannelUpdateParameters } from '@joystream/types/content'
-import { ChannelInputSchema } from '../../schemas/ContentDirectory'
+import { ChannelUpdateInputSchema } from '../../schemas/ContentDirectory'
 import { ChannelMetadata } from '@joystream/metadata-protobuf'
 import { DataObjectInfoFragment } from '../../graphql/generated/queries'
 import BN from 'bn.js'
@@ -82,25 +82,25 @@ export default class UpdateChannelCommand extends UploadCommandBase {
     // Context
     const channel = await this.getApi().channelById(channelId)
     const [actor, address] = await this.getChannelManagementActor(channel, context)
-    const [memberId] = await this.getRequiredMemberContext(true)
+    const { id: memberId } = await this.getRequiredMemberContext(true)
     const keypair = await this.getDecodedPair(address)
 
-    const channelInput = await getInputJson<ChannelInputParameters>(input, ChannelInputSchema)
+    const channelInput = await getInputJson<ChannelUpdateInputParameters>(input, ChannelUpdateInputSchema)
     const meta = asValidatedMetadata(ChannelMetadata, channelInput)
+    const { collaborators, rewardAccount, coverPhotoPath, avatarPhotoPath } = channelInput
 
-    if (channelInput.rewardAccount !== undefined && actor.type === 'Collaborator') {
-      this.error("Collaborators are not allowed to update channel's reward account!", { exit: ExitCodes.AccessDenied })
+    if (rewardAccount !== undefined && !this.isChannelOwner(channel, actor)) {
+      this.error("Only channel owner is allowed to update channel's reward account!", { exit: ExitCodes.AccessDenied })
     }
 
-    if (channelInput.collaborators !== undefined && actor.type === 'Collaborator') {
-      this.error("Collaborators are not allowed to update channel's collaborators!", { exit: ExitCodes.AccessDenied })
+    if (collaborators !== undefined && !this.isChannelOwner(channel, actor)) {
+      this.error("Only channel owner is allowed to update channel's collaborators!", { exit: ExitCodes.AccessDenied })
     }
 
-    if (channelInput.collaborators) {
-      await this.validateCollaborators(channelInput.collaborators)
+    if (collaborators) {
+      await this.validateMemberIdsSet(collaborators, 'collaborator')
     }
 
-    const { coverPhotoPath, avatarPhotoPath, rewardAccount } = channelInput
     const [resolvedAssets, assetIndices] = await this.resolveAndValidateAssets(
       { coverPhotoPath, avatarPhotoPath },
       input
@@ -118,17 +118,22 @@ export default class UpdateChannelCommand extends UploadCommandBase {
       assetIndices.avatarPhotoPath
     )
 
-    const collaborators = createType('Option<BTreeSet<MemberId>>', channelInput.collaborators)
     const channelUpdateParameters: CreateInterface<ChannelUpdateParameters> = {
       assets_to_upload: assetsToUpload,
       assets_to_remove: createType('BTreeSet<DataObjectId>', assetsToRemove),
       new_meta: metadataToBytes(ChannelMetadata, meta),
       reward_account: this.parseRewardAccountInput(rewardAccount),
-      collaborators,
+      collaborators: createType('Option<BTreeSet<MemberId>>', collaborators),
     }
 
     this.jsonPrettyPrint(
-      JSON.stringify({ assetsToUpload: assetsToUpload?.toJSON(), assetsToRemove, metadata: meta, rewardAccount })
+      JSON.stringify({
+        assetsToUpload: assetsToUpload?.toJSON(),
+        assetsToRemove,
+        metadata: meta,
+        rewardAccount,
+        collaborators,
+      })
     )
 
     await this.requireConfirmation('Do you confirm the provided input?', true)

+ 57 - 0
cli/src/commands/content/updateChannelModerators.ts

@@ -0,0 +1,57 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import chalk from 'chalk'
+import { flags } from '@oclif/command'
+import { BTreeSet } from '@polkadot/types'
+import { createType } from '@joystream/types'
+import { MemberId } from '@joystream/types/common'
+
+export default class UpdateChannelModeratorsCommand extends ContentDirectoryCommandBase {
+  static description = "Update Channel's moderator set."
+  static flags = {
+    channelId: flags.integer({
+      char: 'c',
+      required: true,
+      description: 'Channel id',
+    }),
+    moderators: flags.integer({
+      char: 'm',
+      required: false,
+      multiple: true,
+      description: 'New set of moderators',
+    }),
+    ...ContentDirectoryCommandBase.flags,
+  }
+
+  static examples = ['$ content:updateChannelModerators -c 1 -m 1 2 3']
+
+  async run(): Promise<void> {
+    const {
+      flags: { channelId, moderators },
+    } = this.parse(UpdateChannelModeratorsCommand)
+
+    const channel = await this.getApi().channelById(channelId)
+    const [actor, address] = await this.getChannelOwnerActor(channel)
+
+    this.jsonPrettyPrint(
+      JSON.stringify({
+        channelId,
+        moderators: moderators || [],
+      })
+    )
+    await this.requireConfirmation('Do you confirm the provided input?', true)
+
+    await this.sendAndFollowNamedTx(await this.getDecodedPair(address), 'content', 'updateModeratorSet', [
+      actor,
+      createType<BTreeSet<MemberId>, 'BTreeSet<MemberId>'>('BTreeSet<MemberId>', moderators || []),
+      channelId,
+    ])
+
+    console.log(
+      chalk.green(
+        `Channel ${chalk.magentaBright(channelId)} moderator set successfully updated to: ${
+          moderators?.length ? moderators.map((mId) => chalk.magentaBright(mId.toString())).join(', ') : '<empty set>'
+        }!`
+      )
+    )
+  }
+}

+ 4 - 2
cli/src/commands/content/updateVideo.ts

@@ -69,11 +69,12 @@ export default class UpdateVideoCommand extends UploadCommandBase {
     const video = await this.getApi().videoById(videoId)
     const channel = await this.getApi().channelById(video.in_channel.toNumber())
     const [actor, address] = await this.getChannelManagementActor(channel, context)
-    const [memberId] = await this.getRequiredMemberContext(true)
+    const { id: memberId } = await this.getRequiredMemberContext(true)
     const keypair = await this.getDecodedPair(address)
 
     const videoInput = await getInputJson<VideoInputParameters>(input, VideoInputSchema)
     const meta = asValidatedMetadata(VideoMetadata, videoInput)
+    const { enableComments } = videoInput
 
     const { videoPath, thumbnailPhotoPath } = videoInput
     const [resolvedAssets, assetIndices] = await this.resolveAndValidateAssets({ videoPath, thumbnailPhotoPath }, input)
@@ -93,10 +94,11 @@ export default class UpdateVideoCommand extends UploadCommandBase {
       assets_to_upload: assetsToUpload,
       new_meta: metadataToBytes(VideoMetadata, meta),
       assets_to_remove: createType('BTreeSet<DataObjectId>', assetsToRemove),
+      enable_comments: enableComments,
     }
 
     this.jsonPrettyPrint(
-      JSON.stringify({ assetsToUpload: assetsToUpload?.toJSON(), newMetadata: meta, assetsToRemove })
+      JSON.stringify({ assetsToUpload: assetsToUpload?.toJSON(), newMetadata: meta, assetsToRemove, enableComments })
     )
 
     await this.requireConfirmation('Do you confirm the provided input?', true)

+ 2 - 0
cli/src/commands/content/video.ts

@@ -20,6 +20,8 @@ export default class VideoCommand extends ContentDirectoryCommandBase {
         'InChannel': aVideo.in_channel.toString(),
         'InSeries': aVideo.in_series.unwrapOr('NONE').toString(),
         'IsCensored': aVideo.is_censored.toString(),
+        'CommentsEnabled': aVideo.enable_comments.toString(),
+        'PostId': aVideo.video_post_id.toString(),
       })
     } else {
       this.error(`Video not found by channel id: "${videoId}"!`)

+ 2 - 0
cli/src/commands/content/videos.ts

@@ -28,6 +28,8 @@ export default class VideosCommand extends ContentDirectoryCommandBase {
           'InChannel': v.in_channel.toString(),
           'InSeries': v.in_series.unwrapOr('NONE').toString(),
           'IsCensored': v.is_censored.toString(),
+          'CommentsEnabled': v.enable_comments.toString(),
+          'PostId': v.video_post_id.toString(),
         })),
         3
       )

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

@@ -31,7 +31,7 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
 
   async run(): Promise<void> {
     const {
-       args,
+      args,
       flags: { penalty, rationale },
     } = this.parse(WorkingGroupsEvictWorker)
 

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

@@ -13,7 +13,7 @@ export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
     }),
   }
 
-  async run(): Promise<void>  {
+  async run(): Promise<void> {
     // Worker-only gate
     const worker = await this.getRequiredWorkerContext()
 

+ 123 - 6
cli/src/graphql/generated/queries.ts

@@ -5,11 +5,52 @@ export type MemberMetadataFieldsFragment = { name?: Types.Maybe<string>; about?:
 
 export type MembershipFieldsFragment = { id: string; handle: string; metadata: MemberMetadataFieldsFragment }
 
-export type GetMemberByIdQueryVariables = Types.Exact<{
-  id: Types.Scalars['ID']
+export type GetMembersByIdsQueryVariables = Types.Exact<{
+  ids?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
 }>
 
-export type GetMemberByIdQuery = { membershipByUniqueInput?: Types.Maybe<MembershipFieldsFragment> }
+export type GetMembersByIdsQuery = { memberships: Array<MembershipFieldsFragment> }
+
+export type StorageNodeInfoFragment = {
+  id: string
+  operatorMetadata?: Types.Maybe<{ nodeEndpoint?: Types.Maybe<string> }>
+}
+
+export type GetStorageNodesInfoByBagIdQueryVariables = Types.Exact<{
+  bagId?: Types.Maybe<Types.Scalars['ID']>
+}>
+
+export type GetStorageNodesInfoByBagIdQuery = { storageBuckets: Array<StorageNodeInfoFragment> }
+
+export type DataObjectInfoFragment = {
+  id: string
+  size: any
+  deletionPrize: any
+  type:
+    | { __typename: 'DataObjectTypeChannelAvatar'; channel?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeChannelCoverPhoto'; channel?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeVideoMedia'; video?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeVideoThumbnail'; video?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeUnknown' }
+}
+
+export type GetDataObjectsByBagIdQueryVariables = Types.Exact<{
+  bagId?: Types.Maybe<Types.Scalars['ID']>
+}>
+
+export type GetDataObjectsByBagIdQuery = { storageDataObjects: Array<DataObjectInfoFragment> }
+
+export type GetDataObjectsByChannelIdQueryVariables = Types.Exact<{
+  channelId?: Types.Maybe<Types.Scalars['ID']>
+}>
+
+export type GetDataObjectsByChannelIdQuery = { storageDataObjects: Array<DataObjectInfoFragment> }
+
+export type GetDataObjectsByVideoIdQueryVariables = Types.Exact<{
+  videoId?: Types.Maybe<Types.Scalars['ID']>
+}>
+
+export type GetDataObjectsByVideoIdQuery = { storageDataObjects: Array<DataObjectInfoFragment> }
 
 export const MemberMetadataFields = gql`
   fragment MemberMetadataFields on MemberMetadata {
@@ -27,11 +68,87 @@ export const MembershipFields = gql`
   }
   ${MemberMetadataFields}
 `
-export const GetMemberById = gql`
-  query getMemberById($id: ID!) {
-    membershipByUniqueInput(where: { id: $id }) {
+export const StorageNodeInfo = gql`
+  fragment StorageNodeInfo on StorageBucket {
+    id
+    operatorMetadata {
+      nodeEndpoint
+    }
+  }
+`
+export const DataObjectInfo = gql`
+  fragment DataObjectInfo on StorageDataObject {
+    id
+    size
+    deletionPrize
+    type {
+      __typename
+      ... on DataObjectTypeVideoMedia {
+        video {
+          id
+        }
+      }
+      ... on DataObjectTypeVideoThumbnail {
+        video {
+          id
+        }
+      }
+      ... on DataObjectTypeChannelAvatar {
+        channel {
+          id
+        }
+      }
+      ... on DataObjectTypeChannelCoverPhoto {
+        channel {
+          id
+        }
+      }
+    }
+  }
+`
+export const GetMembersByIds = gql`
+  query getMembersByIds($ids: [ID!]) {
+    memberships(where: { id_in: $ids }) {
       ...MembershipFields
     }
   }
   ${MembershipFields}
 `
+export const GetStorageNodesInfoByBagId = gql`
+  query getStorageNodesInfoByBagId($bagId: ID) {
+    storageBuckets(
+      where: {
+        operatorStatus_json: { isTypeOf_eq: "StorageBucketOperatorStatusActive" }
+        bags_some: { id_eq: $bagId }
+        operatorMetadata: { nodeEndpoint_contains: "http" }
+      }
+    ) {
+      ...StorageNodeInfo
+    }
+  }
+  ${StorageNodeInfo}
+`
+export const GetDataObjectsByBagId = gql`
+  query getDataObjectsByBagId($bagId: ID) {
+    storageDataObjects(where: { storageBag: { id_eq: $bagId } }) {
+      ...DataObjectInfo
+    }
+  }
+  ${DataObjectInfo}
+`
+export const GetDataObjectsByChannelId = gql`
+  query getDataObjectsByChannelId($channelId: ID) {
+    storageDataObjects(where: { type_json: { channelId_eq: $channelId } }) {
+      ...DataObjectInfo
+    }
+  }
+  ${DataObjectInfo}
+`
+export const GetDataObjectsByVideoId = gql`
+  query getDataObjectsByVideoId($videoId: ID) {
+    storageDataObjects(where: { type_json: { videoId_eq: $videoId } }) {
+      ...DataObjectInfo
+    }
+  }
+  ${DataObjectInfo}
+`

File diff suppressed because it is too large
+ 311 - 442
cli/src/graphql/generated/schema.ts


+ 2 - 2
cli/src/graphql/queries/membership.graphql

@@ -11,8 +11,8 @@ fragment MembershipFields on Membership {
   }
 }
 
-query getMemberById($id: ID!) {
-  membershipByUniqueInput(where: { id: $id }) {
+query getMembersByIds($ids: [ID!]) {
+  memberships(where: { id_in: $ids }) {
     ...MembershipFields
   }
 }

+ 17 - 3
cli/src/schemas/ContentDirectory.ts

@@ -1,5 +1,6 @@
 import {
-  ChannelInputParameters,
+  ChannelCreationInputParameters,
+  ChannelUpdateInputParameters,
   VideoInputParameters,
   VideoCategoryInputParameters,
   ChannelCategoryInputParameters,
@@ -18,7 +19,7 @@ export const VideoCategoryInputSchema: JsonSchema<VideoCategoryInputParameters>
 
 export const ChannelCategoryInputSchema: JsonSchema<ChannelCategoryInputParameters> = VideoCategoryInputSchema
 
-export const ChannelInputSchema: JsonSchema<ChannelInputParameters> = {
+export const ChannelCreationInputSchema: JsonSchema<ChannelCreationInputParameters> = {
   type: 'object',
   additionalProperties: false,
   properties: {
@@ -31,7 +32,14 @@ export const ChannelInputSchema: JsonSchema<ChannelInputParameters> = {
     avatarPhotoPath: { type: 'string' },
     rewardAccount: { type: ['string', 'null'] },
     collaborators: {
-      type: ['array', 'null'],
+      type: 'array',
+      items: {
+        type: 'integer',
+        min: 0,
+      },
+    },
+    moderators: {
+      type: 'array',
       items: {
         type: 'integer',
         min: 0,
@@ -40,6 +48,11 @@ export const ChannelInputSchema: JsonSchema<ChannelInputParameters> = {
   },
 }
 
+export const ChannelUpdateInputSchema: JsonSchema<ChannelUpdateInputParameters> = {
+  ...ChannelCreationInputSchema,
+}
+delete (ChannelUpdateInputSchema as Record<string, unknown>).moderators
+
 export const VideoInputSchema: JsonSchema<VideoInputParameters> = {
   type: 'object',
   additionalProperties: false,
@@ -96,5 +109,6 @@ export const VideoInputSchema: JsonSchema<VideoInputParameters> = {
     thumbnailPhotoPath: { type: 'string' },
     title: { type: 'string' },
     videoPath: { type: 'string' },
+    enableComments: { type: 'boolean' },
   },
 }

+ 1 - 1
query-node/mappings/src/proposals.ts

@@ -165,7 +165,7 @@ async function parseProposalDetails(
     const specificDetails = proposalDetails.asUpdateWorkingGroupBudget
     const [amount, workingGroup, balanceKind] = specificDetails
     details.groupId = getWorkingGroupModuleName(workingGroup)
-    details.amount = amount.muln(balanceKind.isNegative ? -1 : 1)
+    details.amount = new BN(`${balanceKind.isNegative ? '-' : ''}${amount.toString()}`)
     return details
   }
   // DecreaseWorkingGroupLeadStakeProposalDetails:

+ 1 - 0
query-node/run-tests.sh

@@ -23,6 +23,7 @@ trap cleanup EXIT
 # Clean start
 docker-compose down -v
 
+docker-compose -f ../docker-compose.yml up -d joystream-node
 ./start.sh
 
 # pass the scenario name without .ts extension

+ 7 - 14
start.sh

@@ -3,7 +3,7 @@ set -e
 
 # Run a complete joystream development network on your machine using docker
 
-INIT_CHAIN_SCENARIO=${INIT_CHAIN_SCENARIO:=setup-new-chain}
+INIT_CHAIN_SCENARIO=${INIT_CHAIN_SCENARIO:=setupNewChain}
 
 if [ "${PERSIST}" == true ]
 then
@@ -26,23 +26,16 @@ docker-compose up -d joystream-node
 
 ## Init the chain with some state
 export SKIP_MOCK_CONTENT=true
-# TODO: Move back to this approach once Giza<->Olympia integration tests merged
-# HOST_IP=$(tests/network-tests/get-host-ip.sh)
-# export COLOSSUS_1_URL="http://${HOST_IP}:3333"
-# export COLOSSUS_1_TRANSACTOR_KEY=$(docker run --rm --pull=always docker.io/parity/subkey:2.0.1 inspect ${COLOSSUS_1_TRANSACTOR_URI} --output-type json | jq .ss58Address -r)
-# export DISTRIBUTOR_1_URL="http://${HOST_IP}:3334"
-./tests/network-tests/run-test-scenario.sh ${INIT_CHAIN_SCENARIO}
-
-## Set sudo as the membership screening authority
-yarn workspace api-scripts set-sudo-as-screening-auth
+export SKIP_QUERY_NODE_CHECKS=true
+HOST_IP=$(tests/network-tests/get-host-ip.sh)
+export COLOSSUS_1_URL="http://${HOST_IP}:3333"
+export COLOSSUS_1_TRANSACTOR_KEY=$(docker run --rm --pull=always docker.io/parity/subkey:2.0.1 inspect ${COLOSSUS_1_TRANSACTOR_URI} --output-type json | jq .ss58Address -r)
+export DISTRIBUTOR_1_URL="http://${HOST_IP}:3334"
+./tests/integration-tests/run-test-scenario.sh ${INIT_CHAIN_SCENARIO}
 
 ## Member faucet
 docker-compose up -d faucet
 
-## Storage Infrastructure Configuration
-# TODO: Move back to INIT_CHAIN_SCENARIO approach once Giza<->Olympia integration tests merged
-./storage-playground-config.sh
-
 ## Query Node Infrastructure
 ./query-node/start.sh
 

+ 5 - 0
storage-playground-config.sh

@@ -16,6 +16,11 @@ HOST_IP=$(tests/network-tests/get-host-ip.sh)
 ## Colossus 1
 CLI=storage-node/bin/run
 TRANSACTOR_KEY=$(docker run --rm --pull=always docker.io/parity/subkey:2.0.1 inspect ${COLOSSUS_1_TRANSACTOR_URI} --output-type json | jq .ss58Address -r)
+# Send some funds to TRANSACTOR to cover the acceptPendingDataObjects fees
+export AUTO_CONFIRM=true
+FUNDS_SOURCE_KEY=5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
+TRANSACTOR_INITIAL_BALANCE=100000
+yarn joystream-cli account:transferTokens --amount ${TRANSACTOR_INITIAL_BALANCE} --from ${FUNDS_SOURCE_KEY}  --to ${TRANSACTOR_KEY}
 
 ${CLI} leader:update-bag-limit -l 10 --accountUri ${COLOSSUS_1_WORKER_URI}
 ${CLI} leader:update-voucher-limits -o 10000 -s 1000000000000 --accountUri ${COLOSSUS_1_WORKER_URI}

+ 2 - 0
tests/integration-tests/.gitignore

@@ -0,0 +1,2 @@
+output.json
+data/

+ 14 - 14
tests/integration-tests/proposal-parameters.json

@@ -5,7 +5,7 @@
   },
   "runtime_upgrade_proposal": {
     "voting_period": 100,
-    "grace_period": 20,
+    "grace_period": 40,
     "constitutionality": 2
   },
   "signal_proposal": {
@@ -14,7 +14,7 @@
   },
   "funding_request_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "create_working_group_lead_opening_proposal": {
       "voting_period": 20,
@@ -46,7 +46,7 @@
   },
   "amend_constitution_proposal": {
       "voting_period": 20,
-      "grace_period": 20,
+      "grace_period": 40,
       "constitutionality": 2
   },
   "cancel_working_group_lead_opening_proposal": {
@@ -55,47 +55,47 @@
   },
   "set_membership_price_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "set_council_budget_increment_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "set_councilor_reward_proposal": {
       "voting_period": 20,
-      "grace_period": 20
+      "grace_period": 40
   },
   "set_initial_invitation_balance_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "set_membership_lead_invitation_quota_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "set_referral_cut_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "set_invitation_count_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "create_blog_post_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "edit_blog_post_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "lock_blog_post_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "unlock_blog_post_proposal": {
       "voting_period": 20,
-      "grace_period": 10
+      "grace_period": 20
   },
   "veto_proposal_proposal": {
       "voting_period": 20,

+ 234 - 34
tests/integration-tests/src/Api.ts

@@ -1,17 +1,22 @@
 import { ApiPromise, WsProvider, Keyring } from '@polkadot/api'
 import { u32, BTreeMap } from '@polkadot/types'
-import { ISubmittableResult } from '@polkadot/types/types'
+import { IEvent, ISubmittableResult } from '@polkadot/types/types'
 import { KeyringPair } from '@polkadot/keyring/types'
 import { AccountId, MemberId, PostId, ThreadId } from '@joystream/types/common'
 
-import { AccountInfo, Balance, EventRecord, BlockNumber, BlockHash } from '@polkadot/types/interfaces'
+import { AccountInfo, Balance, EventRecord, BlockNumber, BlockHash, LockIdentifier } from '@polkadot/types/interfaces'
 import BN from 'bn.js'
-import { QueryableConsts, QueryableStorage, SubmittableExtrinsic, SubmittableExtrinsics } from '@polkadot/api/types'
+import {
+  AugmentedEvent,
+  QueryableConsts,
+  QueryableStorage,
+  SubmittableExtrinsic,
+  SubmittableExtrinsics,
+} from '@polkadot/api/types'
 import { Sender, LogLevel } from './sender'
 import { Utils } from './utils'
 import { types } from '@joystream/types'
 
-import { v4 as uuid } from 'uuid'
 import { extendDebug } from './Debugger'
 import { DispatchError } from '@polkadot/types/interfaces/system'
 import {
@@ -46,19 +51,51 @@ import {
 import { DeriveAllSections } from '@polkadot/api/util/decorate'
 import { ExactDerive } from '@polkadot/api-derive'
 import { ProposalId, ProposalParameters } from '@joystream/types/proposals'
-import { BLOCKTIME, proposalTypeToProposalParamsKey } from './consts'
+import {
+  BLOCKTIME,
+  KNOWN_WORKER_ROLE_ACCOUNT_DEFAULT_BALANCE,
+  proposalTypeToProposalParamsKey,
+  workingGroupNameByModuleName,
+} from './consts'
 import { CategoryId } from '@joystream/types/forum'
 
+export type KeyGenInfo = {
+  start: number
+  final: number
+  custom: string[]
+}
+
+type EventSection = keyof ApiPromise['events'] & string
+type EventMethod<Section extends EventSection> = keyof ApiPromise['events'][Section] & string
+type EventType<
+  Section extends EventSection,
+  Method extends EventMethod<Section>
+> = ApiPromise['events'][Section][Method] extends AugmentedEvent<'promise', infer T> ? IEvent<T> : never
+
 export class ApiFactory {
   private readonly api: ApiPromise
   private readonly keyring: Keyring
+  // number used as part of key derivation path
+  private keyId = 0
+  // stores names of the created custom keys
+  private customKeys: string[] = []
+  // mapping from account address to key id.
+  // To be able to re-derive keypair externally when mini-secret is known.
+  readonly addressesToKeyId: Map<string, number> = new Map()
+  // mapping from account address to suri.
+  // To be able to get the suri of a known key for the purpose of, for example, interacting with the CLIs
+  readonly addressesToSuri: Map<string, string>
+  // mini secret used in SURI key derivation path
+  private readonly miniSecret: string
+
   // source of funds for all new accounts
   private readonly treasuryAccount: string
 
   public static async create(
     provider: WsProvider,
     treasuryAccountUri: string,
-    sudoAccountUri: string
+    sudoAccountUri: string,
+    miniSecret: string
   ): Promise<ApiFactory> {
     const debug = extendDebug('api-factory')
     let connectAttempts = 0
@@ -66,16 +103,16 @@ export class ApiFactory {
       connectAttempts++
       debug(`Connecting to chain, attempt ${connectAttempts}..`)
       try {
-        const api = await ApiPromise.create({ provider, types })
+        const api = new ApiPromise({ provider, types })
 
         // Wait for api to be connected and ready
-        await api.isReady
+        await api.isReadyOrError
 
         // If a node was just started up it might take a few seconds to start producing blocks
         // Give it a few seconds to be ready.
         await Utils.wait(5000)
 
-        return new ApiFactory(api, treasuryAccountUri, sudoAccountUri)
+        return new ApiFactory(api, treasuryAccountUri, sudoAccountUri, miniSecret)
       } catch (err) {
         if (connectAttempts === 3) {
           throw new Error('Unable to connect to chain')
@@ -85,32 +122,83 @@ export class ApiFactory {
     }
   }
 
-  constructor(api: ApiPromise, treasuryAccountUri: string, sudoAccountUri: string) {
+  constructor(api: ApiPromise, treasuryAccountUri: string, sudoAccountUri: string, miniSecret: string) {
     this.api = api
     this.keyring = new Keyring({ type: 'sr25519' })
     this.treasuryAccount = this.keyring.addFromUri(treasuryAccountUri).address
     this.keyring.addFromUri(sudoAccountUri)
+    this.miniSecret = miniSecret
+    this.addressesToKeyId = new Map()
+    this.addressesToSuri = new Map()
+    this.keyId = 0
   }
 
   public getApi(label: string): Api {
-    return new Api(this.api, this.treasuryAccount, this.keyring, label)
+    return new Api(this, this.api, this.treasuryAccount, this.keyring, label)
+  }
+
+  public createKeyPairs(n: number): { key: KeyringPair; id: number }[] {
+    const keys: { key: KeyringPair; id: number }[] = []
+    for (let i = 0; i < n; i++) {
+      const id = this.keyId++
+      const key = this.createKeyPair(`${id}`)
+      keys.push({ key, id })
+      this.addressesToKeyId.set(key.address, id)
+    }
+    return keys
+  }
+
+  private createKeyPair(suriPath: string, isCustom = false): KeyringPair {
+    if (isCustom) {
+      this.customKeys.push(suriPath)
+    }
+    const uri = `${this.miniSecret}//testing//${suriPath}`
+    const pair = this.keyring.addFromUri(uri)
+    this.addressesToSuri.set(pair.address, uri)
+    return pair
+  }
+
+  public createCustomKeyPair(customPath: string): KeyringPair {
+    return this.createKeyPair(customPath, true)
   }
 
-  public close(): void {
-    this.api.disconnect()
+  public keyGenInfo(): KeyGenInfo {
+    const start = 0
+    const final = this.keyId
+    return {
+      start,
+      final,
+      custom: this.customKeys,
+    }
+  }
+
+  public getAllGeneratedAccounts(): { [k: string]: number } {
+    return Object.fromEntries(this.addressesToKeyId)
+  }
+
+  public getKeypair(address: AccountId | string): KeyringPair {
+    return this.keyring.getPair(address)
+  }
+
+  public getSuri(address: AccountId | string): string {
+    const suri = this.addressesToSuri.get(address.toString())
+    if (!suri) {
+      throw new Error(`Suri for address ${address} not available!`)
+    }
+    return suri
   }
 }
 
 export class Api {
+  private readonly factory: ApiFactory
   private readonly api: ApiPromise
   private readonly sender: Sender
-  private readonly keyring: Keyring
   // source of funds for all new accounts
   private readonly treasuryAccount: string
 
-  constructor(api: ApiPromise, treasuryAccount: string, keyring: Keyring, label: string) {
+  constructor(factory: ApiFactory, api: ApiPromise, treasuryAccount: string, keyring: Keyring, label: string) {
+    this.factory = factory
     this.api = api
-    this.keyring = keyring
     this.treasuryAccount = treasuryAccount
     this.sender = new Sender(api, keyring, label)
   }
@@ -175,20 +263,24 @@ export class Api {
     this.sender.setLogLevel(LogLevel.Verbose)
   }
 
-  // Create new keys and store them in the shared keyring
-  public async createKeyPairs(n: number, withExistentialDeposit = true): Promise<KeyringPair[]> {
-    const nKeyPairs: KeyringPair[] = []
-    for (let i = 0; i < n; i++) {
-      // What are risks of generating duplicate keys this way?
-      // Why not use a deterministic /TestKeys/N and increment N
-      nKeyPairs.push(this.keyring.addFromUri(i + uuid().substring(0, 8)))
-    }
+  public async createKeyPairs(n: number, withExistentialDeposit = true): Promise<{ key: KeyringPair; id: number }[]> {
+    const pairs = this.factory.createKeyPairs(n)
     if (withExistentialDeposit) {
-      await Promise.all(
-        nKeyPairs.map(({ address }) => this.treasuryTransferBalance(address, this.existentialDeposit()))
-      )
+      await Promise.all(pairs.map(({ key }) => this.treasuryTransferBalance(key.address, this.existentialDeposit())))
     }
-    return nKeyPairs
+    return pairs
+  }
+
+  public createCustomKeyPair(path: string): KeyringPair {
+    return this.factory.createCustomKeyPair(path)
+  }
+
+  public keyGenInfo(): KeyGenInfo {
+    return this.factory.keyGenInfo()
+  }
+
+  public getAllGeneratedAccounts(): { [k: string]: number } {
+    return this.factory.getAllGeneratedAccounts()
   }
 
   public getBlockDuration(): BN {
@@ -220,7 +312,7 @@ export class Api {
     return accountData.data.free
   }
 
-  public async getStakedBalance(address: string | AccountId, lockId?: string): Promise<BN> {
+  public async getStakedBalance(address: string | AccountId, lockId?: LockIdentifier | string): Promise<BN> {
     const locks = await this.api.query.balances.locks(address)
     if (lockId) {
       const foundLock = locks.find((l) => l.id.eq(lockId))
@@ -285,6 +377,54 @@ export class Api {
     return this.api.consts.balances.existentialDeposit
   }
 
+  public findEvent<S extends EventSection, M extends EventMethod<S>>(
+    result: ISubmittableResult | EventRecord[],
+    section: S,
+    method: M
+  ): EventType<S, M> | undefined {
+    if (Array.isArray(result)) {
+      return result.find(({ event }) => event.section === section && event.method === method)?.event as
+        | EventType<S, M>
+        | undefined
+    }
+    return result.findRecord(section, method)?.event as EventType<S, M> | undefined
+  }
+
+  public getEvent<S extends EventSection, M extends EventMethod<S>>(
+    result: ISubmittableResult | EventRecord[],
+    section: S,
+    method: M
+  ): EventType<S, M> {
+    const event = this.findEvent(result, section, method)
+    if (!event) {
+      throw new Error(
+        `Cannot find expected ${section}.${method} event in result: ${JSON.stringify(
+          Array.isArray(result) ? result.map((e) => e.toHuman()) : result.toHuman()
+        )}`
+      )
+    }
+    return event
+  }
+
+  public findEvents<S extends EventSection, M extends EventMethod<S>>(
+    result: ISubmittableResult | EventRecord[],
+    section: S,
+    method: M,
+    expectedCount?: number
+  ): EventType<S, M>[] {
+    const events = Array.isArray(result)
+      ? result.filter(({ event }) => event.section === section && event.method === method).map(({ event }) => event)
+      : result.filterRecords(section, method).map((r) => r.event)
+    if (expectedCount && events.length !== expectedCount) {
+      throw new Error(
+        `Unexpected count of ${section}.${method} events in result: ${JSON.stringify(
+          Array.isArray(result) ? result.map((e) => e.toHuman()) : result.toHuman()
+        )}. ` + `Expected: ${expectedCount}, Got: ${events.length}`
+      )
+    }
+    return (events.sort((a, b) => new BN(a.index).cmp(new BN(b.index))) as unknown) as EventType<S, M>[]
+  }
+
   // TODO: Augmentations comming with new @polkadot/typegen!
 
   public findEventRecord(events: EventRecord[], section: string, method: string): EventRecord | undefined {
@@ -418,20 +558,76 @@ export class Api {
     return opening
   }
 
-  public async getLeader(group: WorkingGroupModuleName): Promise<Worker> {
+  public async getLeader(group: WorkingGroupModuleName): Promise<[WorkerId, Worker]> {
     const leadId = await this.api.query[group].currentLead()
     if (leadId.isNone) {
-      throw new Error('Cannot get lead role key: Lead not yet hired!')
+      throw new Error(`Cannot get ${group} lead: Lead not hired!`)
+    }
+    return [leadId.unwrap(), await this.api.query[group].workerById(leadId.unwrap())]
+  }
+
+  public async getActiveWorkerIds(group: WorkingGroupModuleName): Promise<WorkerId[]> {
+    return (await this.api.query[group].workerById.entries<Worker>()).map(
+      ([
+        {
+          args: [id],
+        },
+      ]) => id
+    )
+  }
+
+  public async getWorkerRoleAccounts(workerIds: WorkerId[], module: WorkingGroupModuleName): Promise<string[]> {
+    const workers = await this.api.query[module].workerById.multi<Worker>(workerIds)
+
+    return workers.map((worker) => {
+      return worker.role_account_id.toString()
+    })
+  }
+
+  async assignWorkerRoleAccount(
+    group: WorkingGroupModuleName,
+    workerId: WorkerId,
+    account: string
+  ): Promise<ISubmittableResult> {
+    const worker = await this.api.query[group].workerById(workerId)
+    if (worker.isEmpty) {
+      throw new Error(`Worker not found by id: ${workerId}!`)
+    }
+
+    const memberController = await this.getControllerAccountOfMember(worker.member_id)
+    // there cannot be a worker associated with member that does not exist
+    if (!memberController) {
+      throw new Error('Member controller not found')
     }
-    return await this.api.query[group].workerById(leadId.unwrap())
+
+    // Expect membercontroller key is already added to keyring
+    // Is is responsibility of caller to ensure this is the case!
+
+    const updateRoleAccountCall = this.api.tx[group].updateRoleAccount(workerId, account)
+    await this.prepareAccountsForFeeExpenses(memberController, [updateRoleAccountCall])
+    return this.sender.signAndSend(updateRoleAccountCall, memberController)
+  }
+
+  async assignWorkerWellknownAccount(
+    group: WorkingGroupModuleName,
+    workerId: WorkerId,
+    initialBalance = KNOWN_WORKER_ROLE_ACCOUNT_DEFAULT_BALANCE
+  ): Promise<ISubmittableResult[]> {
+    // path to append to base SURI
+    const uri = `worker//${workingGroupNameByModuleName[group]}//${workerId.toNumber()}`
+    const account = this.createCustomKeyPair(uri).address
+    return Promise.all([
+      this.assignWorkerRoleAccount(group, workerId, account),
+      this.treasuryTransferBalance(account, initialBalance),
+    ])
   }
 
   public async getLeadRoleKey(group: WorkingGroupModuleName): Promise<string> {
-    return (await this.getLeader(group)).role_account_id.toString()
+    return (await this.getLeader(group))[1].role_account_id.toString()
   }
 
   public async getLeaderStakingKey(group: WorkingGroupModuleName): Promise<string> {
-    return (await this.getLeader(group)).staking_account_id.toString()
+    return (await this.getLeader(group))[1].staking_account_id.toString()
   }
 
   public async retrieveProposalsEngineEventDetails(
@@ -606,4 +802,8 @@ export class Api {
       postId: details.event.data[0] as PostId,
     }
   }
+
+  lockIdByGroup(group: WorkingGroupModuleName): LockIdentifier {
+    return this.api.consts[group].stakingHandlerLockId
+  }
 }

+ 2 - 0
tests/integration-tests/src/Fixture.ts

@@ -91,10 +91,12 @@ export abstract class BaseFixture {
 
 export abstract class BaseQueryNodeFixture extends BaseFixture {
   protected readonly query: QueryNodeApi
+  public readonly queryNodeChecksEnabled: boolean
 
   constructor(api: Api, query: QueryNodeApi) {
     super(api)
     this.query = query
+    this.queryNodeChecksEnabled = !process.env.SKIP_QUERY_NODE_CHECKS
   }
 
   public async runQueryNodeChecks(): Promise<void> {

+ 1 - 1
tests/integration-tests/src/QueryNodeApi.ts

@@ -346,7 +346,7 @@ export class QueryNodeApi {
       try {
         assertResultIsValid(result)
       } catch (e) {
-        debug(`Unexpected query result${e && e.message ? ` (${e.message})` : ''}`)
+        debug(`Unexpected query result${e instanceof Error ? ` (${e.message})` : ''}`)
         await retry(e)
         continue
       }

+ 65 - 8
tests/integration-tests/src/Scenario.ts

@@ -1,14 +1,15 @@
 import { WsProvider } from '@polkadot/api'
-import { ApiFactory } from './Api'
+import { ApiFactory, Api, KeyGenInfo } from './Api'
 import { QueryNodeApi } from './QueryNodeApi'
 import { config } from 'dotenv'
-import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core'
-import { extendDebug, Debugger } from './Debugger'
+import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
+import { Debugger, extendDebug } from './Debugger'
 import { Flow } from './Flow'
 import { Job } from './Job'
 import { JobManager } from './JobManager'
 import { ResourceManager } from './Resources'
 import fetch from 'cross-fetch'
+import fs, { readFileSync } from 'fs'
 
 export type ScenarioProps = {
   env: NodeJS.ProcessEnv
@@ -16,6 +17,31 @@ export type ScenarioProps = {
   job: (label: string, flows: Flow[] | Flow) => Job
 }
 
+const OUTPUT_FILE_PATH = 'output.json'
+
+type TestsOutput = {
+  accounts: { [k: string]: number }
+  keyIds: KeyGenInfo
+  miniSecret: string
+}
+
+function writeOutput(api: Api, miniSecret: string) {
+  console.error('Writing generated account to', OUTPUT_FILE_PATH)
+  // account to key ids
+  const accounts = api.getAllGeneratedAccounts()
+
+  // first and last key id used to generate keys in this scenario
+  const keyIds = api.keyGenInfo()
+
+  const output: TestsOutput = {
+    accounts,
+    keyIds,
+    miniSecret,
+  }
+
+  fs.writeFileSync(OUTPUT_FILE_PATH, JSON.stringify(output, undefined, 2))
+}
+
 export async function scenario(scene: (props: ScenarioProps) => Promise<void>): Promise<void> {
   // Load env variables
   config()
@@ -24,18 +50,35 @@ export async function scenario(scene: (props: ScenarioProps) => Promise<void>):
   // Connect api to the chain
   const nodeUrl: string = env.NODE_URL || 'ws://127.0.0.1:9944'
   const provider = new WsProvider(nodeUrl)
-
+  const miniSecret = env.SURI_MINI_SECRET || ''
   const apiFactory = await ApiFactory.create(
     provider,
     env.TREASURY_ACCOUNT_URI || '//Alice',
-    env.SUDO_ACCOUNT_URI || '//Alice'
+    env.SUDO_ACCOUNT_URI || '//Alice',
+    miniSecret
   )
 
+  const api = apiFactory.getApi('Key Generation')
+
+  // Generate all key ids based on REUSE_KEYS or START_KEY_ID (if provided)
+  const reuseKeys = Boolean(env.REUSE_KEYS)
+  let startKeyId: number
+  let customKeys: string[] = []
+  if (reuseKeys) {
+    const output = JSON.parse(readFileSync(OUTPUT_FILE_PATH).toString()) as TestsOutput
+    startKeyId = output.keyIds.final
+    customKeys = output.keyIds.custom
+  } else {
+    startKeyId = parseInt(env.START_KEY_ID || '0')
+  }
+
+  await api.createKeyPairs(startKeyId, false)
+  customKeys.forEach((k) => api.createCustomKeyPair(k))
+
   const queryNodeUrl: string = env.QUERY_NODE_URL || 'http://127.0.0.1:8081/graphql'
 
   const queryNodeProvider = new ApolloClient({
     link: new HttpLink({ uri: queryNodeUrl, fetch }),
-    // uri: queryNodeUrl,
     cache: new InMemoryCache(),
     defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
   })
@@ -50,14 +93,28 @@ export async function scenario(scene: (props: ScenarioProps) => Promise<void>):
 
   const resources = new ResourceManager()
 
+  process.on('SIGINT', () => {
+    console.error('Aborting scenario')
+    writeOutput(api, miniSecret)
+    process.exit(0)
+  })
+
+  let exitCode = 0
+
   try {
     await jobs.run(resources)
   } catch (err) {
     console.error(err)
-    process.exit(-1)
+    exitCode = -1
   }
 
+  writeOutput(api, miniSecret)
+
   // Note: disconnecting and then reconnecting to the chain in the same process
   // doesn't seem to work!
-  apiFactory.close()
+  // Disconnecting is causing error to be thrown:
+  // RPC-CORE: getStorage(key: StorageKey, at?: BlockHash): StorageData:: disconnected from ws://127.0.0.1:9944: 1000:: Normal connection closure
+  // Are there subsciptions somewhere?
+  // apiFactory.close()
+  process.exit(exitCode)
 }

+ 28 - 12
tests/integration-tests/src/consts.ts

@@ -9,6 +9,9 @@ export const validateType = <T>(obj: T) => obj
 // Test chain blocktime
 export const BLOCKTIME = 1000
 
+// Known worker role account default balance (JOY)
+export const KNOWN_WORKER_ROLE_ACCOUNT_DEFAULT_BALANCE = new BN(100000)
+
 export const MINIMUM_STAKING_ACCOUNT_BALANCE = 200
 export const MIN_APPLICATION_STAKE = new BN(2000)
 export const MIN_UNSTANKING_PERIOD = 43201
@@ -18,24 +21,31 @@ export const POST_DEPOSIT = new BN(10)
 export const PROPOSALS_POST_DEPOSIT = new BN(2000)
 export const ALL_BYTES = '0x' + Array.from({ length: 256 }, (v, i) => Buffer.from([i]).toString('hex')).join('')
 
-export const lockIdByWorkingGroup: { [K in WorkingGroupModuleName]: string } = {
-  storageWorkingGroup: '0x0606060606060606',
-  contentWorkingGroup: '0x0707070707070707',
-  forumWorkingGroup: '0x0808080808080808',
-  membershipWorkingGroup: '0x0909090909090909',
-  operationsWorkingGroup: '0x0d0d0d0d0d0d0d0d',
-  gatewayWorkingGroup: '0x0e0e0e0e0e0e0e0e',
-}
-
 export const workingGroups: WorkingGroupModuleName[] = [
   'storageWorkingGroup',
   'contentWorkingGroup',
   'forumWorkingGroup',
   'membershipWorkingGroup',
-  'operationsWorkingGroup',
+  'operationsWorkingGroupAlpha',
   'gatewayWorkingGroup',
+  'distributionWorkingGroup',
+  'operationsWorkingGroupBeta',
+  'operationsWorkingGroupGamma',
 ]
 
+export const workingGroupNameByModuleName = {
+  'storageWorkingGroup': 'Storage',
+  'contentWorkingGroup': 'Content',
+  'forumWorkingGroup': 'Forum',
+  'membershipWorkingGroup': 'Membership',
+  'operationsWorkingGroupAlpha': 'OperationsAlpha',
+  'gatewayWorkingGroup': 'Gateway',
+  'distributionWorkingGroup': 'Distribution',
+  'operationsWorkingGroupBeta': 'OperationsBeta',
+  'operationsWorkingGroupGamma': 'OperationsGamma',
+}
+validateType<{ [k in WorkingGroupModuleName]: string }>(workingGroupNameByModuleName)
+
 export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModuleName {
   if (group.isOfType('Content')) {
     return 'contentWorkingGroup'
@@ -45,10 +55,16 @@ export function getWorkingGroupModuleName(group: WorkingGroup): WorkingGroupModu
     return 'forumWorkingGroup'
   } else if (group.isOfType('Storage')) {
     return 'storageWorkingGroup'
-  } else if (group.isOfType('Operations')) {
-    return 'operationsWorkingGroup'
+  } else if (group.isOfType('OperationsAlpha')) {
+    return 'operationsWorkingGroupAlpha'
   } else if (group.isOfType('Gateway')) {
     return 'gatewayWorkingGroup'
+  } else if (group.isOfType('Distribution')) {
+    return 'distributionWorkingGroup'
+  } else if (group.isOfType('OperationsBeta')) {
+    return 'operationsWorkingGroupBeta'
+  } else if (group.isOfType('OperationsGamma')) {
+    return 'operationsWorkingGroupGamma'
   }
 
   throw new Error(`Unsupported working group: ${group}`)

+ 17 - 12
tests/integration-tests/src/fixtures/council/ElectCouncilFixture.ts

@@ -13,7 +13,7 @@ export class ElectCouncilFixture extends BaseQueryNodeFixture {
     const numberOfVoters = numberOfCandidates
 
     // Prepare memberships
-    const candidatesMemberAccounts = (await this.api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+    const candidatesMemberAccounts = (await this.api.createKeyPairs(numberOfCandidates)).map(({ key }) => key.address)
     const buyMembershipsFixture = new BuyMembershipHappyCaseFixture(api, query, candidatesMemberAccounts)
     await new FixtureRunner(buyMembershipsFixture).run()
     const candidatesMemberIds = buyMembershipsFixture.getCreatedMembers()
@@ -22,7 +22,7 @@ export class ElectCouncilFixture extends BaseQueryNodeFixture {
     const councilCandidateStake = api.consts.council.minCandidateStake
     const voteStake = api.consts.referendum.minimumStake
 
-    const candidatesStakingAccounts = (await this.api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+    const candidatesStakingAccounts = (await this.api.createKeyPairs(numberOfCandidates)).map(({ key }) => key.address)
     const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
       api,
       query,
@@ -34,7 +34,7 @@ export class ElectCouncilFixture extends BaseQueryNodeFixture {
     )
     await new FixtureRunner(addStakingAccountsFixture).run()
 
-    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map((kp) => kp.address)
+    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map(({ key }) => key.address)
     await api.treasuryTransferBalanceToAccounts(votersStakingAccounts, voteStake.addn(MINIMUM_STAKING_ACCOUNT_BALANCE))
 
     // Announcing stage
@@ -80,15 +80,17 @@ export class ElectCouncilFixture extends BaseQueryNodeFixture {
     const candidatesToWinIds = candidatesMemberIds.slice(0, councilSize.toNumber()).map((id) => id.toString())
 
     // check intermediate election winners are properly set
-    await query.tryQueryWithTimeout(
-      () => query.getReferendumIntermediateWinners(cycleId.toNumber(), councilSize.toNumber()),
-      (qnReferendumIntermediateWinners) => {
-        assert.sameMembers(
-          qnReferendumIntermediateWinners.map((item) => item.member.id.toString()),
-          candidatesToWinIds
-        )
-      }
-    )
+    if (this.queryNodeChecksEnabled) {
+      await query.tryQueryWithTimeout(
+        () => query.getReferendumIntermediateWinners(cycleId.toNumber(), councilSize.toNumber()),
+        (qnReferendumIntermediateWinners) => {
+          assert.sameMembers(
+            qnReferendumIntermediateWinners.map((item) => item.member.id.toString()),
+            candidatesToWinIds
+          )
+        }
+      )
+    }
 
     await this.api.untilCouncilStage('Idle')
 
@@ -97,7 +99,10 @@ export class ElectCouncilFixture extends BaseQueryNodeFixture {
       councilMembers.map((m) => m.membership_id.toString()),
       candidatesToWinIds
     )
+  }
 
+  public async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
     await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
   }
 }

+ 3 - 0
tests/integration-tests/src/fixtures/council/NotEnoughCandidatesFixture.ts

@@ -51,7 +51,10 @@ export class NotEnoughCandidatesFixture extends BaseQueryNodeFixture {
       councilMemberIds.map((item) => item.toString()),
       councilMembersEnding.map((item) => item.membership_id.toString())
     )
+  }
 
+  public async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
     await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
   }
 }

+ 4 - 1
tests/integration-tests/src/fixtures/council/NotEnoughCandidatesWithVotesFixture.ts

@@ -20,7 +20,7 @@ export class NotEnoughCandidatesWithVotesFixture extends BaseQueryNodeFixture {
 
     // create voters
     const voteStake = this.api.consts.referendum.minimumStake
-    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map((kp) => kp.address)
+    const votersStakingAccounts = (await this.api.createKeyPairs(numberOfVoters)).map(({ key }) => key.address)
     await this.api.treasuryTransferBalanceToAccounts(
       votersStakingAccounts,
       voteStake.addn(MINIMUM_STAKING_ACCOUNT_BALANCE)
@@ -76,7 +76,10 @@ export class NotEnoughCandidatesWithVotesFixture extends BaseQueryNodeFixture {
       councilMemberIds.map((item) => item.toString()),
       councilMembersEnding.map((item) => item.membership_id.toString())
     )
+  }
 
+  public async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
     await assertCouncilMembersRuntimeQnMatch(this.api, this.query)
   }
 }

+ 2 - 2
tests/integration-tests/src/fixtures/council/common.ts

@@ -33,7 +33,7 @@ export async function prepareFailToElectResources(api: Api, query: QueryNodeApi)
   const numberOfCandidates = councilSize.add(minNumberOfExtraCandidates).toNumber()
 
   // prepare memberships
-  const candidatesMemberAccounts = (await api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+  const candidatesMemberAccounts = (await api.createKeyPairs(numberOfCandidates)).map(({ key }) => key.address)
   const buyMembershipsFixture = new BuyMembershipHappyCaseFixture(api, query, candidatesMemberAccounts)
   await new FixtureRunner(buyMembershipsFixture).run()
   const candidatesMemberIds = buyMembershipsFixture.getCreatedMembers()
@@ -41,7 +41,7 @@ export async function prepareFailToElectResources(api: Api, query: QueryNodeApi)
   // prepare staking accounts
   const councilCandidateStake = api.consts.council.minCandidateStake
 
-  const candidatesStakingAccounts = (await api.createKeyPairs(numberOfCandidates)).map((kp) => kp.address)
+  const candidatesStakingAccounts = (await api.createKeyPairs(numberOfCandidates)).map(({ key }) => key.address)
   const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
     api,
     query,

+ 1 - 1
tests/integration-tests/src/fixtures/forum/InitializeForumFixture.ts

@@ -117,7 +117,7 @@ export class InitializeForumFixture extends BaseQueryNodeFixture {
       moderatorsPerCategory,
     } = this.config
     // Create forum members
-    const accounts = (await api.createKeyPairs(numberOfForumMembers)).map((kp) => kp.address)
+    const accounts = (await api.createKeyPairs(numberOfForumMembers)).map(({ key }) => key.address)
     const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, accounts)
     await new FixtureRunner(buyMembershipFixture).run()
     const forumMemberIds = buyMembershipFixture.getCreatedMembers()

+ 1 - 1
tests/integration-tests/src/fixtures/proposals/AllProposalsOutcomesFixture.ts

@@ -54,7 +54,7 @@ export class AllProposalsOutcomesFixture extends BaseFixture {
       }
     }
 
-    const memberKeys = (await api.createKeyPairs(testCases.length)).map((key) => key.address)
+    const memberKeys = (await api.createKeyPairs(testCases.length)).map(({ key }) => key.address)
     const membersFixture = new BuyMembershipHappyCaseFixture(api, query, memberKeys)
     await new FixtureRunner(membersFixture).run()
     const memberIds = membersFixture.getCreatedMembers()

+ 1 - 1
tests/integration-tests/src/fixtures/proposals/CreateProposalsFixture.ts

@@ -53,7 +53,7 @@ export class CreateProposalsFixture extends StandardizedFixture {
 
   protected async initStakingAccounts(): Promise<void> {
     const { api, query } = this
-    const stakingAccounts = (await this.api.createKeyPairs(this.proposalsParams.length)).map((kp) => kp.address)
+    const stakingAccounts = (await this.api.createKeyPairs(this.proposalsParams.length)).map(({ key }) => key.address)
     const addStakingAccountsFixture = new AddStakingAccountsHappyCaseFixture(
       api,
       query,

+ 1 - 1
tests/integration-tests/src/fixtures/proposals/DecideOnProposalStatusFixture.ts

@@ -76,7 +76,7 @@ export class DecideOnProposalStatusFixture extends BaseQueryNodeFixture {
     } else {
       const otherResultMinThreshold = Math.min(
         approvalThresholdPercentage.toNumber(),
-        approvalQuorumPercentage.toNumber()
+        slashingThresholdPercentage.toNumber()
       )
       const minRejectOrAbstainVotesN = Math.ceil((councilSize * (100 - otherResultMinThreshold)) / 100)
       return Array.from({ length: minRejectOrAbstainVotesN }, (v, i) => vote('Reject', i))

+ 1 - 2
tests/integration-tests/src/fixtures/workingGroups/DecreaseWorkerStakesFixture.ts

@@ -8,7 +8,6 @@ import { Worker, WorkerId } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
-import { lockIdByWorkingGroup } from '../../consts'
 import { StakeDecreasedEventFieldsFragment, WorkerFieldsFragment } from '../../graphql/generated/queries'
 
 export class DecreaseWorkerStakesFixture extends BaseWorkingGroupFixture {
@@ -36,7 +35,7 @@ export class DecreaseWorkerStakesFixture extends BaseWorkingGroupFixture {
   protected async loadWorkersData(): Promise<void> {
     this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
     this.workerStakes = await Promise.all(
-      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, lockIdByWorkingGroup[this.group]))
+      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, this.api.lockIdByGroup(this.group)))
     )
   }
 

+ 1 - 2
tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts

@@ -10,7 +10,6 @@ import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
 import { BTreeSet } from '@polkadot/types'
 import { registry } from '@joystream/types'
-import { lockIdByWorkingGroup } from '../../consts'
 import {
   LeaderSetEventFieldsFragment,
   OpeningFieldsFragment,
@@ -80,7 +79,7 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
       this.acceptedApplicationsArrays.map((acceptedApplications) =>
         Promise.all(
           acceptedApplications.map((a) =>
-            this.api.getStakedBalance(a.staking_account_id, lockIdByWorkingGroup[this.group])
+            this.api.getStakedBalance(a.staking_account_id, this.api.lockIdByGroup(this.group))
           )
         )
       )

+ 3 - 3
tests/integration-tests/src/fixtures/workingGroups/HireWorkersFixture.ts

@@ -41,9 +41,9 @@ export class HireWorkersFixture extends BaseQueryNodeFixture {
     const { stake: openingStake, metadata: openingMetadata } = DEFAULT_OPENING_PARAMS
 
     // Create the applications
-    const roleAccounts = (await this.api.createKeyPairs(this.workersN)).map((kp) => kp.address)
-    const stakingAccounts = (await this.api.createKeyPairs(this.workersN)).map((kp) => kp.address)
-    const rewardAccounts = (await this.api.createKeyPairs(this.workersN)).map((kp) => kp.address)
+    const roleAccounts = (await this.api.createKeyPairs(this.workersN)).map(({ key }) => key.address)
+    const stakingAccounts = (await this.api.createKeyPairs(this.workersN)).map(({ key }) => key.address)
+    const rewardAccounts = (await this.api.createKeyPairs(this.workersN)).map(({ key }) => key.address)
 
     const buyMembershipFixture = new BuyMembershipHappyCaseFixture(this.api, this.query, roleAccounts)
     await new FixtureRunner(buyMembershipFixture).run()

+ 1 - 2
tests/integration-tests/src/fixtures/workingGroups/IncreaseWorkerStakesFixture.ts

@@ -8,7 +8,6 @@ import { WorkerId, Worker } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
-import { lockIdByWorkingGroup } from '../../consts'
 import { StakeIncreasedEventFieldsFragment, WorkerFieldsFragment } from '../../graphql/generated/queries'
 
 export class IncreaseWorkerStakesFixture extends BaseWorkingGroupFixture {
@@ -33,7 +32,7 @@ export class IncreaseWorkerStakesFixture extends BaseWorkingGroupFixture {
   protected async loadWorkersData(): Promise<void> {
     this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
     this.workerStakes = await Promise.all(
-      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, lockIdByWorkingGroup[this.group]))
+      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, this.api.lockIdByGroup(this.group)))
     )
   }
 

+ 1 - 2
tests/integration-tests/src/fixtures/workingGroups/SlashWorkerStakesFixture.ts

@@ -8,7 +8,6 @@ import { Worker, WorkerId } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
-import { lockIdByWorkingGroup } from '../../consts'
 import { StakeSlashedEventFieldsFragment, WorkerFieldsFragment } from '../../graphql/generated/queries'
 
 export class SlashWorkerStakesFixture extends BaseWorkingGroupFixture {
@@ -36,7 +35,7 @@ export class SlashWorkerStakesFixture extends BaseWorkingGroupFixture {
   protected async loadWorkersData(): Promise<void> {
     this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
     this.workerStakes = await Promise.all(
-      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, lockIdByWorkingGroup[this.group]))
+      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, this.api.lockIdByGroup(this.group)))
     )
   }
 

+ 1 - 2
tests/integration-tests/src/fixtures/workingGroups/TerminateWorkersFixture.ts

@@ -8,7 +8,6 @@ import { Worker, WorkerId } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
-import { lockIdByWorkingGroup } from '../../consts'
 import {
   LeaderUnsetEventFieldsFragment,
   TerminatedLeaderEventFieldsFragment,
@@ -41,7 +40,7 @@ export class TerminateWorkersFixture extends BaseWorkingGroupFixture {
   protected async loadWorkersData(): Promise<void> {
     this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
     this.workerStakes = await Promise.all(
-      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, lockIdByWorkingGroup[this.group]))
+      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, this.api.lockIdByGroup(this.group)))
     )
   }
 

+ 2 - 2
tests/integration-tests/src/flows/council/failToElect.ts

@@ -11,10 +11,10 @@ export default async function failToElectCouncil({ api, query }: FlowProps): Pro
   api.enableDebugTxLogs()
 
   const notEnoughCandidatesFixture = new NotEnoughCandidatesFixture(api, query)
-  await new FixtureRunner(notEnoughCandidatesFixture).run()
+  await new FixtureRunner(notEnoughCandidatesFixture).runWithQueryNodeChecks()
 
   const notEnoughCandidatesWithVotesFixture = new NotEnoughCandidatesWithVotesFixture(api, query)
-  await new FixtureRunner(notEnoughCandidatesWithVotesFixture).run()
+  await new FixtureRunner(notEnoughCandidatesWithVotesFixture).runWithQueryNodeChecks()
 
   debug('Done')
 }

+ 1 - 1
tests/integration-tests/src/flows/forum/polls.ts

@@ -17,7 +17,7 @@ export default async function polls({ api, query }: FlowProps): Promise<void> {
   api.enableDebugTxLogs()
 
   // Create test member(s)
-  const accounts = (await api.createKeyPairs(5)).map((kp) => kp.address)
+  const accounts = (await api.createKeyPairs(5)).map(({ key }) => key.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, accounts)
   await new FixtureRunner(buyMembershipFixture).run()
   const memberIds = buyMembershipFixture.getCreatedMembers()

+ 2 - 2
tests/integration-tests/src/flows/membership/creatingMemberships.ts

@@ -14,12 +14,12 @@ export default async function creatingMemberships({ api, query, env }: FlowProps
   assert(N > 0)
 
   // Assert membership can be bought if sufficient funds are available
-  const nAccounts = (await api.createKeyPairs(N)).map((key) => key.address)
+  const nAccounts = (await api.createKeyPairs(N)).map(({ key }) => key.address)
   const happyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, nAccounts)
   await new FixtureRunner(happyCaseFixture).runWithQueryNodeChecks()
 
   // Assert account can not buy the membership with insufficient funds
-  const aAccount = (await api.createKeyPairs(1))[0].address
+  const aAccount = (await api.createKeyPairs(1))[0].key.address
   const insufficientFundsFixture = new BuyMembershipWithInsufficienFundsFixture(api, aAccount)
   await new FixtureRunner(insufficientFundsFixture).run()
 

+ 2 - 2
tests/integration-tests/src/flows/membership/invitingMembers.ts

@@ -13,12 +13,12 @@ export default async function invitingMembers({ api, query, env }: FlowProps): P
   const N: number = +env.MEMBERS_INVITE_N!
   assert(N > 0)
 
-  const [inviterAcc] = (await api.createKeyPairs(1)).map((key) => key.address)
+  const [inviterAcc] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [inviterAcc])
   await new FixtureRunner(buyMembershipHappyCaseFixture).run()
   const [inviterMemberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
 
-  const inviteesAccs = (await api.createKeyPairs(N)).map((key) => key.address)
+  const inviteesAccs = (await api.createKeyPairs(N)).map(({ key }) => key.address)
   const inviteMembersHappyCaseFixture = new InviteMembersHappyCaseFixture(
     api,
     query,

+ 2 - 2
tests/integration-tests/src/flows/membership/managingStakingAccounts.ts

@@ -14,7 +14,7 @@ export default async function managingStakingAccounts({ api, query, env }: FlowP
   debug('Started')
   api.enableDebugTxLogs()
 
-  const [account] = (await api.createKeyPairs(1)).map((key) => key.address)
+  const [account] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipHappyCaseFixture).run()
   const [memberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
@@ -22,7 +22,7 @@ export default async function managingStakingAccounts({ api, query, env }: FlowP
   const N: number = +env.STAKING_ACCOUNTS_ADD_N!
   assert(N > 0)
 
-  const stakingAccounts = (await api.createKeyPairs(N)).map((k) => k.address)
+  const stakingAccounts = (await api.createKeyPairs(N)).map(({ key }) => key.address)
   const addStakingAccountsHappyCaseFixture = new AddStakingAccountsHappyCaseFixture(
     api,
     query,

+ 1 - 1
tests/integration-tests/src/flows/membership/transferringInvites.ts

@@ -9,7 +9,7 @@ export default async function transferringInvites({ api, query, env }: FlowProps
   debug('Started')
   api.enableDebugTxLogs()
 
-  const [fromAcc, toAcc] = (await api.createKeyPairs(2)).map((key) => key.address)
+  const [fromAcc, toAcc] = (await api.createKeyPairs(2)).map(({ key }) => key.address)
   const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [fromAcc, toAcc])
   await new FixtureRunner(buyMembershipHappyCaseFixture).run()
   const [fromMemberId, toMemberId] = buyMembershipHappyCaseFixture.getCreatedMembers()

+ 2 - 2
tests/integration-tests/src/flows/membership/updatingAccounts.ts

@@ -9,11 +9,11 @@ export default async function updatingAccounts({ api, query }: FlowProps): Promi
   debug('Started')
   api.enableDebugTxLogs()
 
-  const [account] = (await api.createKeyPairs(1)).map((key) => key.address)
+  const [account] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipHappyCaseFixture).run()
   const [memberId] = buyMembershipHappyCaseFixture.getCreatedMembers()
-  const [newRootAccount, newControllerAccount] = (await api.createKeyPairs(2)).map((key) => key.address)
+  const [newRootAccount, newControllerAccount] = (await api.createKeyPairs(2)).map(({ key }) => key.address)
   const updateAccountsHappyCaseFixture = new UpdateAccountsHappyCaseFixture(
     api,
     query,

+ 1 - 1
tests/integration-tests/src/flows/membership/updatingProfile.ts

@@ -25,7 +25,7 @@ export default async function updatingProfile({ api, query }: FlowProps): Promis
     { handle: 'Updated handle', name: 'Updated name', about: 'Updated about' },
   ]
 
-  const [account] = (await api.createKeyPairs(1)).map((key) => key.address)
+  const [account] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipHappyCaseFixture).run()
   const [memberId] = buyMembershipHappyCaseFixture.getCreatedMembers()

+ 1 - 1
tests/integration-tests/src/flows/proposals/cancellingProposal.ts

@@ -12,7 +12,7 @@ export default async function cancellingProposals({ api, query, lock }: FlowProp
 
   const unlock = await lock(Resource.Proposals)
 
-  const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
+  const [account] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipFixture).run()
   const [memberId] = buyMembershipFixture.getCreatedMembers()

+ 1 - 1
tests/integration-tests/src/flows/proposals/exactExecutionBlock.ts

@@ -12,7 +12,7 @@ export default async function exactExecutionBlock({ api, query, lock }: FlowProp
 
   const unlock = await lock(Resource.Proposals)
 
-  const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
+  const [account] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipFixture).run()
   const [memberId] = buyMembershipFixture.getCreatedMembers()

+ 1 - 1
tests/integration-tests/src/flows/proposals/expireProposal.ts

@@ -12,7 +12,7 @@ export default async function expireProposal({ api, query, lock }: FlowProps): P
 
   const unlock = await lock(Resource.Proposals)
 
-  const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
+  const [account] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipFixture).run()
   const [memberId] = buyMembershipFixture.getCreatedMembers()

+ 2 - 2
tests/integration-tests/src/flows/proposals/index.ts

@@ -27,7 +27,7 @@ export default async function creatingProposals({ api, query, lock }: FlowProps)
   await new FixtureRunner(createLeadOpeningsFixture).run()
   const [openingToCancelId, openingToFillId] = createLeadOpeningsFixture.getCreatedOpeningIds()
 
-  const [applicantControllerAcc, applicantStakingAcc] = (await api.createKeyPairs(2)).map((kp) => kp.address)
+  const [applicantControllerAcc, applicantStakingAcc] = (await api.createKeyPairs(2)).map(({ key }) => key.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [applicantControllerAcc])
   await new FixtureRunner(buyMembershipFixture).run()
   const [applicantMemberId] = buyMembershipFixture.getCreatedMembers()
@@ -56,7 +56,7 @@ export default async function creatingProposals({ api, query, lock }: FlowProps)
   const [applicationId] = applyOnOpeningFixture.getCreatedApplicationsByOpeningId(openingToFillId)
   debug('Openings and applicantions created')
 
-  const accountsToFund = (await api.createKeyPairs(5)).map((key) => key.address)
+  const accountsToFund = (await api.createKeyPairs(5)).map(({ key }) => key.address)
   const proposalsToTest: TestedProposal[] = [
     { details: { AmendConstitution: 'New constitution' } },
     {

+ 1 - 1
tests/integration-tests/src/flows/proposals/runtimeUpgradeProposal.ts

@@ -28,7 +28,7 @@ export default async function runtimeUpgradeProposal({ api, query, lock, env }:
   )
 
   // Proposals to be "CancelledByRuntime"
-  const [memberAcc] = (await api.createKeyPairs(1)).map((kp) => kp.address)
+  const [memberAcc] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [memberAcc])
   await new FixtureRunner(buyMembershipFixture).run()
   const [memberId] = buyMembershipFixture.getCreatedMembers()

+ 1 - 1
tests/integration-tests/src/flows/proposals/vetoProposal.ts

@@ -12,7 +12,7 @@ export default async function vetoProposal({ api, query, lock }: FlowProps): Pro
 
   const unlocks = await Promise.all(Array.from({ length: 2 }, () => lock(Resource.Proposals)))
 
-  const [account] = (await api.createKeyPairs(1)).map((kp) => kp.address)
+  const [account] = (await api.createKeyPairs(1)).map(({ key }) => key.address)
   const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [account])
   await new FixtureRunner(buyMembershipFixture).run()
   const [memberId] = buyMembershipFixture.getCreatedMembers()

+ 1 - 1
tests/integration-tests/src/flows/proposalsDiscussion/index.ts

@@ -23,7 +23,7 @@ export default async function proposalsDiscussion({ api, query, lock }: FlowProp
   api.enableDebugTxLogs()
 
   const threadsN = 3
-  const accounts = (await api.createKeyPairs(threadsN)).map((kp) => kp.address)
+  const accounts = (await api.createKeyPairs(threadsN)).map(({ key }) => key.address)
 
   const buyMembershipsFixture = new BuyMembershipHappyCaseFixture(api, query, accounts)
   await new FixtureRunner(buyMembershipsFixture).run()

+ 222 - 0
tests/integration-tests/src/flows/storage/initDistribution.ts

@@ -0,0 +1,222 @@
+import { FlowProps } from '../../Flow'
+import { extendDebug } from '../../Debugger'
+import {
+  DistributionBucketFamilyMetadata,
+  DistributionBucketOperatorMetadata,
+  IDistributionBucketFamilyMetadata,
+  IDistributionBucketOperatorMetadata,
+} from '@joystream/metadata-protobuf'
+import { CreateInterface, createType } from '@joystream/types'
+import { BagId, DistributionBucketFamilyId, DynamicBagId, StaticBagId } from '@joystream/types/storage'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import _ from 'lodash'
+import { Utils } from '../../utils'
+import { WorkerId } from '@joystream/types/working-group'
+
+type DistributionBucketConfig = {
+  metadata: IDistributionBucketOperatorMetadata
+  staticBags?: CreateInterface<StaticBagId>[]
+  operatorId: number
+}
+
+type DistributionFamilyConfig = {
+  metadata?: IDistributionBucketFamilyMetadata
+  buckets: DistributionBucketConfig[]
+  dynamicBagPolicy: {
+    [K in keyof typeof DynamicBagId.typeDefinitions]?: number
+  }
+}
+
+type InitDistributionConfig = {
+  families: DistributionFamilyConfig[]
+}
+
+export const allStaticBags: CreateInterface<StaticBagId>[] = [
+  'Council',
+  { WorkingGroup: 'Content' },
+  { WorkingGroup: 'Distribution' },
+  { WorkingGroup: 'Gateway' },
+  { WorkingGroup: 'OperationsAlpha' },
+  { WorkingGroup: 'OperationsBeta' },
+  { WorkingGroup: 'OperationsGamma' },
+  { WorkingGroup: 'Storage' },
+]
+
+export const singleBucketConfig: InitDistributionConfig = {
+  families: [
+    {
+      metadata: { region: 'All' },
+      dynamicBagPolicy: {
+        'Channel': 1,
+        'Member': 1,
+      },
+      buckets: [
+        {
+          metadata: { endpoint: process.env.DISTRIBUTOR_1_URL || 'http://localhost:3334' },
+          staticBags: allStaticBags,
+          operatorId: parseInt(process.env.DISTRIBUTOR_1_WORKER_ID || '0'),
+        },
+      ],
+    },
+  ],
+}
+
+export const doubleBucketConfig: InitDistributionConfig = {
+  families: [
+    {
+      metadata: { region: 'Region 1' },
+      dynamicBagPolicy: {
+        'Channel': 1,
+        'Member': 1,
+      },
+      buckets: [
+        {
+          metadata: { endpoint: process.env.DISTRIBUTOR_1_URL || 'http://localhost:3334' },
+          staticBags: allStaticBags,
+          operatorId: parseInt(process.env.DISTRIBUTOR_1_WORKER_ID || '0'),
+        },
+      ],
+    },
+    {
+      metadata: { region: 'Region 2' },
+      dynamicBagPolicy: {
+        'Channel': 1,
+        'Member': 1,
+      },
+      buckets: [
+        {
+          metadata: { endpoint: process.env.DISTRIBUTOR_2_URL || 'http://localhost:3336' },
+          staticBags: allStaticBags,
+          operatorId: parseInt(process.env.DISTRIBUTOR_2_WORKER_ID || '1'),
+        },
+      ],
+    },
+  ],
+}
+
+export default function createFlow({ families }: InitDistributionConfig) {
+  return async function initDistribution({ api }: FlowProps): Promise<void> {
+    const debug = extendDebug('flow:initDistribution')
+    debug('Started')
+
+    // Get working group leaders
+    const [, distributionLeader] = await api.getLeader('distributionWorkingGroup')
+
+    const distributionLeaderKey = distributionLeader.role_account_id.toString()
+    const totalBucketsNum = families.reduce((a, b) => a + b.buckets.length, 0)
+
+    // Hire operators
+    // const hireWorkersFixture = new HireWorkesFixture(api, totalBucketsNum, WorkingGroups.Distribution)
+    // await new FixtureRunner(hireWorkersFixture).run()
+    // const operatorIds = hireWorkersFixture.getHiredWorkers()
+
+    const operatorIds = families.reduce(
+      (ids, { buckets }) => ids.concat(buckets.map((b) => createType('WorkerId', b.operatorId))),
+      [] as WorkerId[]
+    )
+    const operatorKeys = await api.getWorkerRoleAccounts(operatorIds, 'distributionWorkingGroup')
+
+    // Create families, set buckets per bag limit
+    const createFamilyTxs = families.map(() => api.tx.storage.createDistributionBucketFamily())
+    const setBucketsPerBagLimitTx = api.tx.storage.updateDistributionBucketsPerBagLimit(totalBucketsNum)
+    const [createFamilyResults] = await Promise.all([
+      api.sendExtrinsicsAndGetResults(createFamilyTxs, distributionLeaderKey),
+      api.sendExtrinsicsAndGetResults([setBucketsPerBagLimitTx], distributionLeaderKey),
+    ])
+    const familyIds = createFamilyResults
+      .map((r) => api.getEvent(r, 'storage', 'DistributionBucketFamilyCreated').data[0])
+      .sort((a, b) => a.cmp(b))
+    const familyById = new Map<number, DistributionFamilyConfig>()
+    familyIds.forEach((id, i) => familyById.set(id.toNumber(), families[i]))
+
+    // Create buckets, update families metadata, set dynamic bag policies
+    const createBucketTxs = families.reduce(
+      (txs, { buckets }, familyIndex) =>
+        txs.concat(buckets.map(() => api.tx.storage.createDistributionBucket(familyIds[familyIndex], true))),
+      [] as SubmittableExtrinsic<'promise'>[]
+    )
+    const updateFamilyMetadataTxs = familyIds.map((id, i) => {
+      const metadataBytes = Utils.metadataToBytes(DistributionBucketFamilyMetadata, families[i].metadata)
+      return api.tx.storage.setDistributionBucketFamilyMetadata(id, metadataBytes)
+    })
+    const dynamicBagPolicies = new Map<string, [DistributionBucketFamilyId, number][]>()
+    familyIds.forEach((familyId, index) => {
+      const family = families[index]
+      Object.entries(family.dynamicBagPolicy).forEach(([bagType, bucketsN]) => {
+        const current = dynamicBagPolicies.get(bagType) || []
+        dynamicBagPolicies.set(bagType, [...current, [familyId, bucketsN]])
+      })
+    })
+    const updateDynamicBagPolicyTxs = _.entries(dynamicBagPolicies).map(([bagType, policyEntries]) =>
+      api.tx.storage.updateFamiliesInDynamicBagCreationPolicy(
+        bagType as keyof typeof DynamicBagId.typeDefinitions,
+        createType('BTreeMap<DistributionBucketFamilyId, u32>', new Map(policyEntries))
+      )
+    )
+    const [createBucketResults] = await Promise.all([
+      api.sendExtrinsicsAndGetResults(createBucketTxs, distributionLeaderKey),
+      api.sendExtrinsicsAndGetResults(updateFamilyMetadataTxs, distributionLeaderKey),
+      api.sendExtrinsicsAndGetResults(updateDynamicBagPolicyTxs, distributionLeaderKey),
+    ])
+    const bucketIds = createBucketResults
+      .map((r) => {
+        const [, , bucketId] = api.getEvent(r, 'storage', 'DistributionBucketCreated').data
+        return bucketId
+      })
+      .sort(
+        (a, b) =>
+          a.distribution_bucket_family_id.cmp(b.distribution_bucket_family_id) ||
+          a.distribution_bucket_index.cmp(b.distribution_bucket_index)
+      )
+    const bucketById = new Map<string, DistributionBucketConfig>()
+    bucketIds.forEach((bucketId) => {
+      const familyId = bucketId.distribution_bucket_family_id.toNumber()
+      const bucketIndex = bucketId.distribution_bucket_index.toNumber()
+      const family = familyById.get(familyId)
+      if (!family) {
+        throw new Error(`familyById not found: ${familyId}`)
+      }
+      bucketById.set(bucketId.toString(), family.buckets[bucketIndex])
+    })
+
+    // Invite bucket operators
+    const bucketInviteTxs = bucketIds.map((bucketId, i) =>
+      api.tx.storage.inviteDistributionBucketOperator(bucketId, operatorIds[i])
+    )
+    await api.sendExtrinsicsAndGetResults(bucketInviteTxs, distributionLeaderKey)
+
+    // Accept invitations
+    const acceptInvitationTxs = bucketIds.map((bucketId, i) =>
+      api.tx.storage.acceptDistributionBucketInvitation(operatorIds[i], bucketId)
+    )
+    await api.sendExtrinsicsAndGetResults(acceptInvitationTxs, operatorKeys)
+
+    // Bucket metadata and static bags
+    const bucketSetupPromises = _.flatten(
+      bucketIds.map((bucketId, i) => {
+        const operatorId = operatorIds[i]
+        const operatorKey = operatorKeys[i]
+        const bucketConfig = bucketById.get(bucketId.toString())
+        if (!bucketConfig) {
+          throw new Error('Bucket config not found')
+        }
+        const metadataBytes = Utils.metadataToBytes(DistributionBucketOperatorMetadata, bucketConfig.metadata)
+        const setMetaTx = api.tx.storage.setDistributionOperatorMetadata(operatorId, bucketId, metadataBytes)
+        const setMetaPromise = api.sendExtrinsicsAndGetResults([setMetaTx], operatorKey)
+        const updateBagTxs = (bucketConfig.staticBags || []).map((sBagId) => {
+          return api.tx.storage.updateDistributionBucketsForBag(
+            createType<BagId, 'BagId'>('BagId', { Static: sBagId }),
+            bucketId.distribution_bucket_family_id,
+            createType('BTreeSet<DistributionBucketIndex>', [bucketId.distribution_bucket_index]),
+            createType('BTreeSet<DistributionBucketIndex>', [])
+          )
+        })
+        const updateBagsPromise = api.sendExtrinsicsAndGetResults(updateBagTxs, distributionLeaderKey)
+        return [updateBagsPromise, setMetaPromise]
+      })
+    )
+    await Promise.all(bucketSetupPromises)
+
+    debug('Done')
+  }
+}

+ 161 - 0
tests/integration-tests/src/flows/storage/initStorage.ts

@@ -0,0 +1,161 @@
+import { FlowProps } from '../../Flow'
+import { extendDebug } from '../../Debugger'
+import { IStorageBucketOperatorMetadata, StorageBucketOperatorMetadata } from '@joystream/metadata-protobuf'
+import { CreateInterface, createType } from '@joystream/types'
+import { BagId, DynamicBagId, StaticBagId } from '@joystream/types/storage'
+import _ from 'lodash'
+import { Utils } from '../../utils'
+import BN from 'bn.js'
+
+type StorageBucketConfig = {
+  metadata: IStorageBucketOperatorMetadata
+  staticBags?: CreateInterface<StaticBagId>[]
+  storageLimit: BN
+  objectsLimit: number
+  operatorId: number
+  transactorKey: string
+  transactorBalance: BN
+}
+
+type InitStorageConfig = {
+  buckets: StorageBucketConfig[]
+  dynamicBagPolicy: {
+    [K in keyof typeof DynamicBagId.typeDefinitions]?: number
+  }
+}
+
+export const allStaticBags: CreateInterface<StaticBagId>[] = [
+  'Council',
+  { WorkingGroup: 'Content' },
+  { WorkingGroup: 'Distribution' },
+  { WorkingGroup: 'Gateway' },
+  { WorkingGroup: 'OperationsAlpha' },
+  { WorkingGroup: 'OperationsBeta' },
+  { WorkingGroup: 'OperationsGamma' },
+  { WorkingGroup: 'Storage' },
+]
+
+export const singleBucketConfig: InitStorageConfig = {
+  dynamicBagPolicy: {
+    'Channel': 1,
+    'Member': 1,
+  },
+  buckets: [
+    {
+      metadata: { endpoint: process.env.COLOSSUS_1_URL || 'http://localhost:3333' },
+      staticBags: allStaticBags,
+      operatorId: parseInt(process.env.COLOSSUS_1_WORKER_ID || '0'),
+      storageLimit: new BN(1_000_000_000_000),
+      objectsLimit: 1000000000,
+      transactorKey: process.env.COLOSSUS_1_TRANSACTOR_KEY || '5DkE5YD8m5Yzno6EH2RTBnH268TDnnibZMEMjxwYemU4XevU', // //Colossus1
+      transactorBalance: new BN(100_000),
+    },
+  ],
+}
+
+export const doubleBucketConfig: InitStorageConfig = {
+  dynamicBagPolicy: {
+    'Channel': 2,
+    'Member': 2,
+  },
+  buckets: [
+    {
+      metadata: { endpoint: process.env.COLOSSUS_1_URL || 'http://localhost:3333' },
+      staticBags: allStaticBags,
+      operatorId: parseInt(process.env.COLOSSUS_1_WORKER_ID || '0'),
+      storageLimit: new BN(1_000_000_000_000),
+      objectsLimit: 1000000000,
+      transactorKey: process.env.COLOSSUS_1_TRANSACTOR_KEY || '5DkE5YD8m5Yzno6EH2RTBnH268TDnnibZMEMjxwYemU4XevU', // //Colossus1
+      transactorBalance: new BN(100_000),
+    },
+    {
+      metadata: { endpoint: process.env.STORAGE_2_URL || 'http://localhost:3335' },
+      staticBags: allStaticBags,
+      operatorId: parseInt(process.env.STORAGE_2_WORKER_ID || '1'),
+      storageLimit: new BN(1_000_000_000_000),
+      objectsLimit: 1000000000,
+      transactorKey: process.env.COLOSSUS_2_TRANSACTOR_KEY || '5FbzYmQ3HogiEEDSXPYJe58yCcmSh3vsZLodTdBB6YuLDAj7', // //Colossus2
+      transactorBalance: new BN(100_000),
+    },
+  ],
+}
+
+export default function createFlow({ buckets, dynamicBagPolicy }: InitStorageConfig) {
+  return async function initDistribution({ api }: FlowProps): Promise<void> {
+    const debug = extendDebug('flow:initStorage')
+    debug('Started')
+
+    // Get working group leaders
+    const [, storageLeader] = await api.getLeader('storageWorkingGroup')
+
+    const storageLeaderKey = storageLeader.role_account_id.toString()
+    const maxStorageLimit = buckets.sort((a, b) => b.storageLimit.cmp(a.storageLimit))[0].storageLimit
+    const maxObjectsLimit = Math.max(...buckets.map((b) => b.objectsLimit))
+
+    // Hire operators
+    // const hireWorkersFixture = new HireWorkesFixture(api, totalBucketsNum, WorkingGroups.Distribution)
+    // await new FixtureRunner(hireWorkersFixture).run()
+    // const operatorIds = hireWorkersFixture.getHiredWorkers()
+
+    const operatorIds = buckets.map((b) => createType('WorkerId', b.operatorId))
+    const operatorKeys = await api.getWorkerRoleAccounts(operatorIds, 'storageWorkingGroup')
+
+    // Set global limits and policies
+    const updateDynamicBagPolicyTxs = _.entries(dynamicBagPolicy).map(([bagType, numberOfBuckets]) =>
+      api.tx.storage.updateNumberOfStorageBucketsInDynamicBagCreationPolicy(
+        bagType as keyof typeof DynamicBagId.typeDefinitions,
+        numberOfBuckets
+      )
+    )
+    const setMaxVoucherLimitsTx = api.tx.storage.updateStorageBucketsVoucherMaxLimits(maxStorageLimit, maxObjectsLimit)
+    const setBucketPerBagLimitTx = api.tx.storage.updateStorageBucketsPerBagLimit(Math.max(5, buckets.length))
+
+    await api.sendExtrinsicsAndGetResults(
+      [...updateDynamicBagPolicyTxs, setMaxVoucherLimitsTx, setBucketPerBagLimitTx],
+      storageLeaderKey
+    )
+
+    // Create buckets
+    const createBucketTxs = buckets.map((b, i) =>
+      api.tx.storage.createStorageBucket(operatorIds[i], true, b.storageLimit, b.objectsLimit)
+    )
+    const createBucketResults = await api.sendExtrinsicsAndGetResults(createBucketTxs, storageLeaderKey)
+    const bucketById = new Map<number, StorageBucketConfig>()
+    createBucketResults.forEach((res, i) => {
+      const bucketId = api.getEvent(res, 'storage', 'StorageBucketCreated').data[0]
+      bucketById.set(bucketId.toNumber(), buckets[i])
+    })
+
+    // Accept invitations
+    const acceptInvitationTxs = Array.from(bucketById.entries()).map(([bucketId, bucketConfig], i) =>
+      api.tx.storage.acceptStorageBucketInvitation(operatorIds[i], bucketId, bucketConfig.transactorKey)
+    )
+    await api.sendExtrinsicsAndGetResults(acceptInvitationTxs, operatorKeys)
+
+    // Bucket metadata, static bags, transactor balances
+    const bucketSetupPromises = _.flatten(
+      Array.from(bucketById.entries()).map(([bucketId, bucketConfig], i) => {
+        const operatorId = operatorIds[i]
+        const operatorKey = operatorKeys[i]
+        const metadataBytes = Utils.metadataToBytes(StorageBucketOperatorMetadata, bucketConfig.metadata)
+        const setMetaTx = api.tx.storage.setStorageOperatorMetadata(operatorId, bucketId, metadataBytes)
+        const setMetaPromise = api.sendExtrinsicsAndGetResults([setMetaTx], operatorKey)
+        const updateBagTxs = (bucketConfig.staticBags || []).map((sBagId) => {
+          return api.tx.storage.updateStorageBucketsForBag(
+            createType<BagId, 'BagId'>('BagId', { Static: sBagId }),
+            createType('BTreeSet<StorageBucketId>', [bucketId]),
+            createType('BTreeSet<StorageBucketId>', [])
+          )
+        })
+        const updateBagsPromise = api.sendExtrinsicsAndGetResults(updateBagTxs, storageLeaderKey)
+        const setupTransactorBalancePromise = (async () => [
+          await api.treasuryTransferBalance(bucketConfig.transactorKey, bucketConfig.transactorBalance),
+        ])()
+        return [updateBagsPromise, setMetaPromise, setupTransactorBalancePromise]
+      })
+    )
+    await Promise.all(bucketSetupPromises)
+
+    debug('Done')
+  }
+}

+ 1 - 1
tests/integration-tests/src/flows/working-groups/groupBudget.ts

@@ -18,7 +18,7 @@ export default async function groupBudget({ api, query }: FlowProps): Promise<vo
       const setGroupBudgetFixture = new SetBudgetFixture(api, query, group, budgets)
       await new FixtureRunner(setGroupBudgetFixture).runWithQueryNodeChecks()
 
-      const recievers = (await api.createKeyPairs(5)).map((kp) => kp.address)
+      const recievers = (await api.createKeyPairs(5)).map(({ key }) => key.address)
       const amounts = recievers.map((reciever, i) => new BN(10000 * (i + 1)))
       const spendGroupBudgetFixture = new SpendBudgetFixture(api, query, group, recievers, amounts)
       await new FixtureRunner(spendGroupBudgetFixture).runWithQueryNodeChecks()

+ 1 - 1
tests/integration-tests/src/flows/working-groups/leadOpening.ts

@@ -25,7 +25,7 @@ export default async function leadOpening({ api, query, env }: FlowProps): Promi
       const [openingId] = createOpeningFixture.getCreatedOpeningIds()
       const { stake: openingStake, metadata: openingMetadata } = DEFAULT_OPENING_PARAMS
 
-      const [roleAccount, stakingAccount, rewardAccount] = (await api.createKeyPairs(3)).map((kp) => kp.address)
+      const [roleAccount, stakingAccount, rewardAccount] = (await api.createKeyPairs(3)).map(({ key }) => key.address)
       const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, [roleAccount])
       await new FixtureRunner(buyMembershipFixture).run()
       const [memberId] = buyMembershipFixture.getCreatedMembers()

+ 3 - 3
tests/integration-tests/src/flows/working-groups/openingsAndApplications.ts

@@ -85,9 +85,9 @@ export default async function openingsAndApplications({ api, query, env }: FlowP
       const { stake: openingStake, metadata: openingMetadata } = DEFAULT_OPENING_PARAMS
 
       // Create some applications
-      const roleAccounts = (await api.createKeyPairs(APPLICATION_CREATE_N)).map((kp) => kp.address)
-      const stakingAccounts = (await api.createKeyPairs(APPLICATION_CREATE_N)).map((kp) => kp.address)
-      const rewardAccounts = (await api.createKeyPairs(APPLICATION_CREATE_N)).map((kp) => kp.address)
+      const roleAccounts = (await api.createKeyPairs(APPLICATION_CREATE_N)).map(({ key }) => key.address)
+      const stakingAccounts = (await api.createKeyPairs(APPLICATION_CREATE_N)).map(({ key }) => key.address)
+      const rewardAccounts = (await api.createKeyPairs(APPLICATION_CREATE_N)).map(({ key }) => key.address)
 
       const buyMembershipFixture = new BuyMembershipHappyCaseFixture(api, query, roleAccounts)
       await new FixtureRunner(buyMembershipFixture).run()

+ 2 - 2
tests/integration-tests/src/flows/working-groups/workerActions.ts

@@ -36,7 +36,7 @@ export default async function workerActions({ api, query, env }: FlowProps): Pro
       // Independent updates that don't interfere with each other
       const workerUpdatesRunners: FixtureRunner[] = []
 
-      const newRoleAccounts = (await api.createKeyPairs(WORKERS_N)).map((kp) => kp.address)
+      const newRoleAccounts = (await api.createKeyPairs(WORKERS_N)).map(({ key }) => key.address)
       const updateRoleAccountsFixture = new UpdateWorkerRoleAccountsFixture(
         api,
         query,
@@ -48,7 +48,7 @@ export default async function workerActions({ api, query, env }: FlowProps): Pro
       await updateRoleAccountsRunner.run()
       workerUpdatesRunners.push(updateRoleAccountsRunner)
 
-      const newRewardAccounts = (await api.createKeyPairs(WORKERS_N)).map((kp) => kp.address)
+      const newRewardAccounts = (await api.createKeyPairs(WORKERS_N)).map(({ key }) => key.address)
       const updateRewardAccountsFixture = new UpdateWorkerRewardAccountsFixture(
         api,
         query,

+ 15 - 0
tests/integration-tests/src/misc/updateAllWorkerRoleAccountsFlow.ts

@@ -0,0 +1,15 @@
+import { UpdateWorkerAccountsFixture } from './updateWorkerAccountsFixture'
+
+import { FlowProps } from '../Flow'
+import { FixtureRunner } from '../Fixture'
+import { extendDebug } from '../Debugger'
+
+export default async function updateAllWorkerRoleAccounts({ api }: FlowProps): Promise<void> {
+  const debug = extendDebug('flow:updateAllWorkerRoleAccounts')
+  debug('Started')
+
+  const updateAccounts = new UpdateWorkerAccountsFixture(api)
+  await new FixtureRunner(updateAccounts).run()
+
+  debug('Done')
+}

+ 14 - 0
tests/integration-tests/src/misc/updateWorkerAccountsFixture.ts

@@ -0,0 +1,14 @@
+import { BaseFixture } from '../Fixture'
+import { workingGroups } from '../consts'
+
+export class UpdateWorkerAccountsFixture extends BaseFixture {
+  public async execute(): Promise<void> {
+    await Promise.all(
+      workingGroups.map(async (group) =>
+        Promise.all(
+          (await this.api.getActiveWorkerIds(group)).map((id) => this.api.assignWorkerWellknownAccount(group, id))
+        )
+      )
+    )
+  }
+}

+ 21 - 0
tests/integration-tests/src/scenarios/setupNewChain.ts

@@ -0,0 +1,21 @@
+import electCouncil from '../flows/council/elect'
+import leaderSetup from '../flows/working-groups/leadOpening'
+import updateAccountsFlow from '../misc/updateAllWorkerRoleAccountsFlow'
+import initStorage, { singleBucketConfig as defaultStorageConfig } from '../flows/storage/initStorage'
+import initDistribution, { singleBucketConfig as defaultDistributionConfig } from '../flows/storage/initDistribution'
+import { scenario } from '../Scenario'
+
+scenario(async ({ job }) => {
+  job('Elect Council', electCouncil)
+  const leads = job('Set WorkingGroup Leads', leaderSetup)
+  const updateWorkerAccounts = job('Update worker accounts', updateAccountsFlow).after(leads)
+
+  if (!process.env.SKIP_STORAGE_AND_DISTRIBUTION) {
+    job('initialize storage system', initStorage(defaultStorageConfig)).requires(updateWorkerAccounts)
+    job('initialize distribution system', initDistribution(defaultDistributionConfig)).requires(updateWorkerAccounts)
+  }
+
+  // TODO: Mock content
+  // assign members known accounts?
+  // assign council known accounts?
+})

+ 4 - 1
tests/integration-tests/src/types.ts

@@ -99,8 +99,11 @@ export type WorkingGroupModuleName =
   | 'contentWorkingGroup'
   | 'forumWorkingGroup'
   | 'membershipWorkingGroup'
-  | 'operationsWorkingGroup'
+  | 'operationsWorkingGroupAlpha'
   | 'gatewayWorkingGroup'
+  | 'distributionWorkingGroup'
+  | 'operationsWorkingGroupBeta'
+  | 'operationsWorkingGroupGamma'
 
 // Proposals:
 

+ 4 - 1
utils/api-scripts/src/initialize-lead.ts

@@ -12,8 +12,11 @@ const workingGroupModules = [
   'contentWorkingGroup',
   'forumWorkingGroup',
   'membershipWorkingGroup',
-  'operationsWorkingGroup',
   'gatewayWorkingGroup',
+  'operationsWorkingGroupAlpha',
+  'operationsWorkingGroupBeta',
+  'operationsWorkingGroupGamma',
+  'distributionWorkingGroup',
 ] as const
 
 type WorkingGroupModuleName = typeof workingGroupModules[number]

+ 4 - 1
utils/api-scripts/src/initialize-worker.ts

@@ -11,8 +11,11 @@ const workingGroupModules = [
   'contentWorkingGroup',
   'forumWorkingGroup',
   'membershipWorkingGroup',
-  'operationsWorkingGroup',
   'gatewayWorkingGroup',
+  'operationsWorkingGroupAlpha',
+  'operationsWorkingGroupBeta',
+  'operationsWorkingGroupGamma',
+  'distributionWorkingGroup',
 ] as const
 
 type WorkingGroupModuleName = typeof workingGroupModules[number]

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