Browse Source

Merge branch 'master' into olympia-update-from-master

Mokhtar Naamani 4 years ago
parent
commit
b5401c2c34
68 changed files with 1077 additions and 603 deletions
  1. 1 5
      README.md
  2. 37 1
      cli/README.md
  3. 2 1
      cli/package.json
  4. 124 2
      cli/src/base/ContentDirectoryCommandBase.ts
  5. 1 1
      cli/src/base/MediaCommandBase.ts
  6. 58 0
      cli/src/commands/content-directory/createEntity.ts
  7. 3 15
      cli/src/commands/content-directory/removeEntity.ts
  8. 61 0
      cli/src/commands/content-directory/updateEntityPropertyValues.ts
  9. 1 1
      cli/src/commands/media/featuredVideos.ts
  10. 1 1
      cli/src/commands/media/removeChannel.ts
  11. 1 1
      cli/src/commands/media/removeVideo.ts
  12. 1 1
      cli/src/commands/media/updateChannel.ts
  13. 1 1
      cli/src/commands/media/updateVideo.ts
  14. 1 1
      cli/src/commands/media/updateVideoLicense.ts
  15. 2 2
      content-directory-schemas/inputs/classes/ChannelClass.json
  16. 2 2
      content-directory-schemas/inputs/classes/ContentCategoryClass.json
  17. 2 2
      content-directory-schemas/inputs/classes/FeaturedVideoClass.json
  18. 2 2
      content-directory-schemas/inputs/classes/HttpMediaLocationClass.json
  19. 2 2
      content-directory-schemas/inputs/classes/JoystreamMediaLocationClass.json
  20. 2 2
      content-directory-schemas/inputs/classes/KnownLicenseClass.json
  21. 2 2
      content-directory-schemas/inputs/classes/LanguageClass.json
  22. 2 2
      content-directory-schemas/inputs/classes/LicenseClass.json
  23. 2 2
      content-directory-schemas/inputs/classes/MediaLocationClass.json
  24. 2 2
      content-directory-schemas/inputs/classes/UserDefinedLicenseClass.json
  25. 2 2
      content-directory-schemas/inputs/classes/VideoClass.json
  26. 2 2
      content-directory-schemas/inputs/classes/VideoMediaClass.json
  27. 8 0
      content-directory-schemas/inputs/entityBatches/KnownLicenseBatch.json
  28. 6 2
      content-directory-schemas/package.json
  29. 0 88
      content-directory-schemas/scripts/devInitAliceLead.ts
  30. 105 0
      content-directory-schemas/scripts/devInitContentLead.ts
  31. 9 6
      content-directory-schemas/scripts/initializeContentDir.ts
  32. 8 1
      content-directory-schemas/src/helpers/extrinsics.ts
  33. 1 1
      package.json
  34. 3 0
      pioneer/packages/apps-routing/src/index.ts
  35. 13 0
      pioneer/packages/apps-routing/src/joy-media.ts
  36. 5 0
      pioneer/packages/apps/src/wp-jpg.d.ts
  37. 13 0
      pioneer/packages/joy-media/package.json
  38. BIN
      pioneer/packages/joy-media/src/assets/atlas-screenshot.jpg
  39. 118 0
      pioneer/packages/joy-media/src/index.tsx
  40. 3 0
      pioneer/packages/joy-media/src/translate.ts
  41. 5 3
      pioneer/tsconfig.json
  42. 2 2
      query-node/mappings/content-directory/content-dir-consts.ts
  43. 15 37
      query-node/mappings/content-directory/entity/index.ts
  44. 21 13
      query-node/mappings/content-directory/entity/remove.ts
  45. 15 0
      query-node/mappings/content-directory/get-or-create.ts
  46. 5 0
      query-node/mappings/types.ts
  47. 3 2
      query-node/package.json
  48. 4 5
      query-node/run-tests.sh
  49. 8 0
      runtime-modules/content-directory/src/lib.rs
  50. 3 0
      runtime/CHANGELOG.md
  51. 2 2
      runtime/src/lib.rs
  52. 7 3
      setup.sh
  53. 4 1
      storage-node/packages/cli/src/cli.ts
  54. 128 1
      storage-node/packages/cli/src/commands/dev.ts
  55. 35 21
      storage-node/packages/colossus/bin/cli.js
  56. 2 1
      storage-node/packages/colossus/lib/app.js
  57. 23 18
      storage-node/packages/colossus/lib/sync.js
  58. 6 1
      storage-node/packages/colossus/paths/asset/v0/{id}.js
  59. 8 0
      storage-node/packages/runtime-api/identities.js
  60. 7 7
      storage-node/packages/runtime-api/workers.js
  61. 3 2
      tests/network-tests/package.json
  62. 12 16
      tests/network-tests/run-storage-node-tests.sh
  63. 13 0
      tests/network-tests/run-test-scenario.sh
  64. 3 6
      tests/network-tests/run-tests.sh
  65. 127 302
      tests/network-tests/src/Api.ts
  66. 2 2
      tests/network-tests/src/flows/storageNode/getContentFromStorageNode.ts
  67. 1 1
      tests/network-tests/tsconfig.json
  68. 4 4
      yarn.lock

+ 1 - 5
README.md

@@ -1,4 +1,4 @@
-# Joystream [![Build Status](https://travis-ci.org/Joystream/joystream.svg?branch=master)](https://travis-ci.org/Joystream/joystream)
+# Joystream
 
 This is the main code repository for all Joystream software. In this mono-repo you will find all the software required to run a Joystream network: The Joystream full node, runtime and all reusable substrate runtime modules that make up the Joystream runtime. In addition to all front-end apps and infrastructure servers necessary for operating the network.
 
@@ -7,10 +7,6 @@ This is the main code repository for all Joystream software. In this mono-repo y
 The Joystream network builds on a pre-release version of [substrate v2.0](https://substrate.dev/) and adds additional
 functionality to support the [various roles](https://www.joystream.org/roles) that can be entered into on the platform.
 
-## Build Status
-
-Development [![Development Branch Build Status](https://travis-ci.org/Joystream/joystream.svg?branch=development)](https://travis-ci.org/Joystream/joystream) - build history on [Travis](https://travis-ci.org/github/Joystream/joystream/builds)
-
 ## Development Tools
 
 The following tools are required for building, testing and contributing to this repo:

+ 37 - 1
cli/README.md

@@ -44,7 +44,7 @@ $ npm install -g @joystream/cli
 $ joystream-cli COMMAND
 running command...
 $ joystream-cli (-v|--version|version)
-@joystream/cli/0.2.0 linux-x64 node-v13.12.0
+@joystream/cli/0.3.0 linux-x64 node-v12.18.2
 $ joystream-cli --help [COMMAND]
 USAGE
   $ joystream-cli COMMAND
@@ -83,6 +83,7 @@ When using the CLI for the first time there are a few common steps you might wan
 * [`joystream-cli content-directory:classes`](#joystream-cli-content-directoryclasses)
 * [`joystream-cli content-directory:createClass`](#joystream-cli-content-directorycreateclass)
 * [`joystream-cli content-directory:createCuratorGroup`](#joystream-cli-content-directorycreatecuratorgroup)
+* [`joystream-cli content-directory:createEntity CLASSNAME`](#joystream-cli-content-directorycreateentity-classname)
 * [`joystream-cli content-directory:curatorGroup ID`](#joystream-cli-content-directorycuratorgroup-id)
 * [`joystream-cli content-directory:curatorGroups`](#joystream-cli-content-directorycuratorgroups)
 * [`joystream-cli content-directory:entities CLASSNAME [PROPERTIES]`](#joystream-cli-content-directoryentities-classname-properties)
@@ -94,6 +95,7 @@ When using the CLI for the first time there are a few common steps you might wan
 * [`joystream-cli content-directory:removeMaintainerFromClass [CLASSNAME] [GROUPID]`](#joystream-cli-content-directoryremovemaintainerfromclass-classname-groupid)
 * [`joystream-cli content-directory:setCuratorGroupStatus [ID] [STATUS]`](#joystream-cli-content-directorysetcuratorgroupstatus-id-status)
 * [`joystream-cli content-directory:updateClassPermissions [CLASSNAME]`](#joystream-cli-content-directoryupdateclasspermissions-classname)
+* [`joystream-cli content-directory:updateEntityPropertyValues ID`](#joystream-cli-content-directoryupdateentitypropertyvalues-id)
 * [`joystream-cli council:info`](#joystream-cli-councilinfo)
 * [`joystream-cli help [COMMAND]`](#joystream-cli-help-command)
 * [`joystream-cli media:createChannel`](#joystream-cli-mediacreatechannel)
@@ -423,6 +425,23 @@ ALIASES
 
 _See code: [src/commands/content-directory/createCuratorGroup.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/createCuratorGroup.ts)_
 
+## `joystream-cli content-directory:createEntity CLASSNAME`
+
+Creates a new entity in the specified class (can be executed in Member, Curator or Lead context)
+
+```
+USAGE
+  $ joystream-cli content-directory:createEntity CLASSNAME
+
+ARGUMENTS
+  CLASSNAME  Name or ID of the Class
+
+OPTIONS
+  --context=(Member|Curator|Lead)  Actor context to execute the command in (Member/Curator/Lead)
+```
+
+_See code: [src/commands/content-directory/createEntity.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/createEntity.ts)_
+
 ## `joystream-cli content-directory:curatorGroup ID`
 
 Show Curator Group details by ID.
@@ -588,6 +607,23 @@ ARGUMENTS
 
 _See code: [src/commands/content-directory/updateClassPermissions.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/updateClassPermissions.ts)_
 
+## `joystream-cli content-directory:updateEntityPropertyValues ID`
+
+Updates the property values of the specified entity (can be executed in Member, Curator or Lead context)
+
+```
+USAGE
+  $ joystream-cli content-directory:updateEntityPropertyValues ID
+
+ARGUMENTS
+  ID  ID of the Entity
+
+OPTIONS
+  --context=(Member|Curator|Lead)  Actor context to execute the command in (Member/Curator/Lead)
+```
+
+_See code: [src/commands/content-directory/updateEntityPropertyValues.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/updateEntityPropertyValues.ts)_
+
 ## `joystream-cli council:info`
 
 Get current council and council elections information

+ 2 - 1
cli/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@joystream/cli",
   "description": "Command Line Interface for Joystream community and governance activities",
-  "version": "0.2.0",
+  "version": "0.3.1",
   "author": "Leszek Wiesner",
   "bin": {
     "joystream-cli": "./bin/run"
@@ -11,6 +11,7 @@
     "@apidevtools/json-schema-ref-parser": "^9.0.6",
     "@ffprobe-installer/ffprobe": "^1.1.0",
     "@joystream/types": "^0.14.0",
+    "@joystream/cd-schemas": "^0.1.0",
     "@oclif/command": "^1.5.19",
     "@oclif/config": "^1.14.0",
     "@oclif/plugin-autocomplete": "^0.2.0",

+ 124 - 2
cli/src/base/ContentDirectoryCommandBase.ts

@@ -12,6 +12,7 @@ import {
   EntityId,
   Actor,
   PropertyType,
+  Property,
 } from '@joystream/types/content-directory'
 import { Worker } from '@joystream/types/working-group'
 import { CLIError } from '@oclif/errors'
@@ -23,6 +24,7 @@ import { RolesCommandBase } from './WorkingGroupsCommandBase'
 import { createType } from '@joystream/types'
 import chalk from 'chalk'
 import { flags } from '@oclif/command'
+import { DistinctQuestion } from 'inquirer'
 
 const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
 type Context = typeof CONTEXTS[number]
@@ -247,7 +249,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
 
   async getAndParseKnownEntity<T>(id: string | number, className?: string): Promise<FlattenRelations<T>> {
     const entity = await this.getEntity(id, className)
-    return this.parseToKnownEntityJson<T>(entity)
+    return this.parseToEntityJson<T>(entity)
   }
 
   async entitiesByClassAndOwner(classNameOrId: number | string, ownerMemberId?: number): Promise<[EntityId, Entity][]> {
@@ -339,7 +341,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     }, {} as Record<string, ParsedPropertyValue>)
   }
 
-  async parseToKnownEntityJson<T>(entity: Entity): Promise<FlattenRelations<T>> {
+  async parseToEntityJson<T = unknown>(entity: Entity): Promise<FlattenRelations<T>> {
     const entityClass = (await this.classEntryByNameOrId(entity.class_id.toString()))[1]
     return (_.mapValues(this.parseEntityPropertyValues(entity, entityClass), (v) =>
       this.parseStoredPropertyInnerValue(v.value)
@@ -376,4 +378,124 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
 
     return parsedEntities.filter((entity) => filters.every(([pName, pValue]) => entity[pName] === pValue))
   }
+
+  async getActor(context: typeof CONTEXTS[number], pickedClass: Class) {
+    let actor: Actor
+    if (context === 'Member') {
+      const memberId = await this.getRequiredMemberId()
+      actor = this.createType('Actor', { Member: memberId })
+    } else if (context === 'Curator') {
+      actor = await this.getCuratorContext([pickedClass.name.toString()])
+    } else {
+      await this.getRequiredLead()
+
+      actor = this.createType('Actor', { Lead: null })
+    }
+
+    return actor
+  }
+
+  isActorEntityController(actor: Actor, entity: Entity, isMaintainer: boolean): boolean {
+    const entityController = entity.entity_permissions.controller
+    return (
+      (isMaintainer && entityController.isOfType('Maintainers')) ||
+      (entityController.isOfType('Member') &&
+        actor.isOfType('Member') &&
+        entityController.asType('Member').eq(actor.asType('Member'))) ||
+      (entityController.isOfType('Lead') && actor.isOfType('Lead'))
+    )
+  }
+
+  async isEntityPropertyEditableByActor(entity: Entity, classPropertyId: number, actor: Actor): Promise<boolean> {
+    const [, entityClass] = await this.classEntryByNameOrId(entity.class_id.toString())
+
+    const isActorMaintainer =
+      actor.isOfType('Curator') &&
+      entityClass.class_permissions.maintainers.toArray().some((groupId) => groupId.eq(actor.asType('Curator')[0]))
+
+    const isActorController = this.isActorEntityController(actor, entity, isActorMaintainer)
+
+    const {
+      is_locked_from_controller: isLockedFromController,
+      is_locked_from_maintainer: isLockedFromMaintainer,
+    } = entityClass.properties[classPropertyId].locking_policy
+
+    return (
+      (isActorController && !isLockedFromController.valueOf()) ||
+      (isActorMaintainer && !isLockedFromMaintainer.valueOf())
+    )
+  }
+
+  getQuestionsFromProperties(properties: Property[], defaults?: { [key: string]: unknown }): DistinctQuestion[] {
+    return properties.reduce((previousValue, { name, property_type: propertyType, required }) => {
+      const propertySubtype = propertyType.subtype
+      const questionType = propertySubtype === 'Bool' ? 'list' : 'input'
+      const isSubtypeNumber = propertySubtype.toLowerCase().includes('int')
+      const isSubtypeReference = propertyType.isOfType('Single') && propertyType.asType('Single').isOfType('Reference')
+
+      const validate = async (answer: string | number | null) => {
+        if (answer === null) {
+          return true // Can only happen through "filter" if property is not required
+        }
+
+        if ((isSubtypeNumber || isSubtypeReference) && parseInt(answer.toString()).toString() !== answer.toString()) {
+          return `Expected integer value!`
+        }
+
+        if (isSubtypeReference) {
+          try {
+            await this.getEntity(+answer, propertyType.asType('Single').asType('Reference')[0].toString())
+          } catch (e) {
+            return e.message || JSON.stringify(e)
+          }
+        }
+
+        return true
+      }
+
+      const optionalQuestionProperties = {
+        ...{
+          filter: async (answer: string) => {
+            if (required.isFalse && !answer) {
+              return null
+            }
+
+            // Only cast to number if valid
+            // Prevents inquirer bug not allowing to edit invalid values when casted to number
+            // See: https://github.com/SBoudrias/Inquirer.js/issues/866
+            if ((isSubtypeNumber || isSubtypeReference) && (await validate(answer)) === true) {
+              return parseInt(answer)
+            }
+
+            return answer
+          },
+          validate,
+        },
+        ...(propertySubtype === 'Bool' && {
+          choices: ['true', 'false'],
+          filter: (answer: string) => {
+            return answer === 'true' || false
+          },
+        }),
+      }
+
+      const isQuestionOptional = propertySubtype === 'Bool' ? '' : required.isTrue ? '(required)' : '(optional)'
+      const classId = isSubtypeReference
+        ? ` [Class Id: ${propertyType.asType('Single').asType('Reference')[0].toString()}]`
+        : ''
+
+      return [
+        ...previousValue,
+        {
+          name: name.toString(),
+          message: `${name} - ${propertySubtype}${classId} ${isQuestionOptional}`,
+          type: questionType,
+          ...optionalQuestionProperties,
+          ...(defaults && {
+            default: propertySubtype === 'Bool' ? JSON.stringify(defaults[name.toString()]) : defaults[name.toString()],
+          }),
+        },
+      ]
+    }, [] as DistinctQuestion[])
+  }
 }

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

@@ -23,7 +23,7 @@ export default abstract class MediaCommandBase extends ContentDirectoryCommandBa
     })
     if (licenseType === 'known') {
       const [id, knownLicenseEntity] = await this.promptForEntityEntry('Choose License', 'KnownLicense', 'code')
-      const knownLicense = await this.parseToKnownEntityJson<KnownLicenseEntity>(knownLicenseEntity)
+      const knownLicense = await this.parseToEntityJson<KnownLicenseEntity>(knownLicenseEntity)
       licenseInput = { knownLicense: id.toNumber() }
       if (knownLicense.attributionRequired) {
         licenseInput.attribution = await this.simplePrompt({ message: 'Attribution' })

+ 58 - 0
cli/src/commands/content-directory/createEntity.ts

@@ -0,0 +1,58 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import inquirer from 'inquirer'
+import { InputParser } from '@joystream/cd-schemas'
+import ExitCodes from '../../ExitCodes'
+
+export default class CreateEntityCommand extends ContentDirectoryCommandBase {
+  static description =
+    'Creates a new entity in the specified class (can be executed in Member, Curator or Lead context)'
+
+  static args = [
+    {
+      name: 'className',
+      required: true,
+      description: 'Name or ID of the Class',
+    },
+  ]
+
+  static flags = {
+    context: ContentDirectoryCommandBase.contextFlag,
+  }
+
+  async run() {
+    const { className } = this.parse(CreateEntityCommand).args
+    let { context } = this.parse(CreateEntityCommand).flags
+
+    if (!context) {
+      context = await this.promptForContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+    const [, entityClass] = await this.classEntryByNameOrId(className)
+
+    const actor = await this.getActor(context, entityClass)
+
+    if (actor.isOfType('Member') && entityClass.class_permissions.any_member.isFalse) {
+      this.error('Choosen actor has no access to create an entity of this type', { exit: ExitCodes.AccessDenied })
+    }
+
+    const answers: {
+      [key: string]: string | number | null
+    } = await inquirer.prompt(this.getQuestionsFromProperties(entityClass.properties.toArray()))
+
+    this.jsonPrettyPrint(JSON.stringify(answers))
+    await this.requireConfirmation('Do you confirm the provided input?')
+
+    const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
+      {
+        className: entityClass.name.toString(),
+        entries: [answers],
+      },
+    ])
+
+    const operations = await inputParser.getEntityBatchOperations()
+
+    await this.sendAndFollowNamedTx(currentAccount, 'contentDirectory', 'transaction', [actor, operations])
+  }
+}

+ 3 - 15
cli/src/commands/content-directory/removeEntity.ts

@@ -1,6 +1,5 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { Actor } from '@joystream/types/content-directory'
-import { createType } from '@joystream/types'
 import ExitCodes from '../../ExitCodes'
 
 export default class RemoveEntityCommand extends ContentDirectoryCommandBase {
@@ -31,20 +30,9 @@ export default class RemoveEntityCommand extends ContentDirectoryCommandBase {
     }
 
     const account = await this.getRequiredSelectedAccount()
-    let actor: Actor
-    if (context === 'Curator') {
-      actor = await this.getCuratorContext([entityClass.name.toString()])
-    } else if (context === 'Member') {
-      const memberId = await this.getRequiredMemberId()
-      if (
-        !entity.entity_permissions.controller.isOfType('Member') ||
-        entity.entity_permissions.controller.asType('Member').toNumber() !== memberId
-      ) {
-        this.error('You are not the entity controller!', { exit: ExitCodes.AccessDenied })
-      }
-      actor = createType('Actor', { Member: memberId })
-    } else {
-      actor = createType('Actor', { Lead: null })
+    const actor: Actor = await this.getActor(context, entityClass)
+    if (!actor.isOfType('Curator') && !this.isActorEntityController(actor, entity, false)) {
+      this.error('You are not the entity controller!', { exit: ExitCodes.AccessDenied })
     }
 
     await this.requireConfirmation(

+ 61 - 0
cli/src/commands/content-directory/updateEntityPropertyValues.ts

@@ -0,0 +1,61 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import inquirer from 'inquirer'
+import { InputParser } from '@joystream/cd-schemas'
+import ExitCodes from '../../ExitCodes'
+
+export default class UpdateEntityPropertyValues extends ContentDirectoryCommandBase {
+  static description =
+    'Updates the property values of the specified entity (can be executed in Member, Curator or Lead context)'
+
+  static args = [
+    {
+      name: 'id',
+      required: true,
+      description: 'ID of the Entity',
+    },
+  ]
+
+  static flags = {
+    context: ContentDirectoryCommandBase.contextFlag,
+  }
+
+  async run() {
+    const { id } = this.parse(UpdateEntityPropertyValues).args
+    let { context } = this.parse(UpdateEntityPropertyValues).flags
+
+    if (!context) {
+      context = await this.promptForContext()
+    }
+
+    const currentAccount = await this.getRequiredSelectedAccount()
+    await this.requestAccountDecoding(currentAccount)
+
+    const entity = await this.getEntity(id)
+    const [, entityClass] = await this.classEntryByNameOrId(entity.class_id.toString())
+    const defaults = await this.parseToEntityJson(entity)
+
+    const actor = await this.getActor(context, entityClass)
+
+    const isPropertEditableByIndex = await Promise.all(
+      entityClass.properties.map((p, i) => this.isEntityPropertyEditableByActor(entity, i, actor))
+    )
+    const filteredProperties = entityClass.properties.filter((p, i) => isPropertEditableByIndex[i])
+
+    if (!filteredProperties.length) {
+      this.error('No entity properties are editable by choosen actor', { exit: ExitCodes.AccessDenied })
+    }
+
+    const answers: {
+      [key: string]: string | number | null
+    } = await inquirer.prompt(this.getQuestionsFromProperties(filteredProperties, defaults))
+
+    this.jsonPrettyPrint(JSON.stringify(answers))
+    await this.requireConfirmation('Do you confirm the provided input?')
+
+    const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
+
+    const operations = await inputParser.getEntityUpdateOperations(answers, entityClass.name.toString(), +id)
+
+    await this.sendAndFollowNamedTx(currentAccount, 'contentDirectory', 'transaction', [actor, operations])
+  }
+}

+ 1 - 1
cli/src/commands/media/featuredVideos.ts

@@ -11,7 +11,7 @@ export default class FeaturedVideosCommand extends ContentDirectoryCommandBase {
     const featured = await Promise.all(
       featuredEntries
         .filter(([, entity]) => entity.supported_schemas.toArray().length) // Ignore FeaturedVideo entities without schema
-        .map(([, entity]) => this.parseToKnownEntityJson<FeaturedVideoEntity>(entity))
+        .map(([, entity]) => this.parseToEntityJson<FeaturedVideoEntity>(entity))
     )
 
     const videoIds: number[] = featured.map(({ video: videoId }) => videoId)

+ 1 - 1
cli/src/commands/media/removeChannel.ts

@@ -33,7 +33,7 @@ export default class RemoveChannelCommand extends ContentDirectoryCommandBase {
       channelId = id.toNumber()
       channelEntity = channel
     }
-    const channel = await this.parseToKnownEntityJson<ChannelEntity>(channelEntity)
+    const channel = await this.parseToEntityJson<ChannelEntity>(channelEntity)
 
     await this.requireConfirmation(`Are you sure you want to remove "${channel.handle}" channel?`)
 

+ 1 - 1
cli/src/commands/media/removeVideo.ts

@@ -34,7 +34,7 @@ export default class RemoveVideoCommand extends ContentDirectoryCommandBase {
       videoEntity = video
     }
 
-    const video = await this.parseToKnownEntityJson<VideoEntity>(videoEntity)
+    const video = await this.parseToEntityJson<VideoEntity>(videoEntity)
 
     await this.requireConfirmation(`Are you sure you want to remove the "${video.title}" video?`)
 

+ 1 - 1
cli/src/commands/media/updateChannel.ts

@@ -57,7 +57,7 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
       channelEntity = channel
     }
 
-    const currentValues = await this.parseToKnownEntityJson<ChannelEntity>(channelEntity)
+    const currentValues = await this.parseToEntityJson<ChannelEntity>(channelEntity)
     this.jsonPrettyPrint(JSON.stringify(currentValues))
 
     const channelJsonSchema = (ChannelEntitySchema as unknown) as JSONSchema

+ 1 - 1
cli/src/commands/media/updateVideo.ts

@@ -55,7 +55,7 @@ export default class UpdateVideoCommand extends MediaCommandBase {
       videoEntity = video
     }
 
-    const currentValues = await this.parseToKnownEntityJson<VideoEntity>(videoEntity)
+    const currentValues = await this.parseToEntityJson<VideoEntity>(videoEntity)
     const videoJsonSchema = (VideoEntitySchema as unknown) as JSONSchema
 
     const {

+ 1 - 1
cli/src/commands/media/updateVideoLicense.ts

@@ -37,7 +37,7 @@ export default class UpdateVideoLicenseCommand extends MediaCommandBase {
       videoEntity = video
     }
 
-    const video = await this.parseToKnownEntityJson<VideoEntity>(videoEntity)
+    const video = await this.parseToEntityJson<VideoEntity>(videoEntity)
     const currentLicense = await this.getAndParseKnownEntity<LicenseEntity>(video.license)
 
     this.log('Current license:', currentLicense)

+ 2 - 2
content-directory-schemas/inputs/classes/ChannelClass.json

@@ -1,7 +1,7 @@
 {
   "name": "Channel",
   "description": "A channel belonging to certain member. Members can publish certain type of content (ie. videos) through channels.",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 25,
   "class_permissions": { "any_member": true }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/ContentCategoryClass.json

@@ -1,6 +1,6 @@
 {
   "name": "ContentCategory",
   "description": "A category the content may be published under",
-  "maximum_entities_count": 100,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 500,
+  "default_entity_creation_voucher_upper_bound": 500
 }

+ 2 - 2
content-directory-schemas/inputs/classes/FeaturedVideoClass.json

@@ -1,6 +1,6 @@
 {
   "name": "FeaturedVideo",
   "description": "Featured video references",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 10,
+  "default_entity_creation_voucher_upper_bound": 10
 }

+ 2 - 2
content-directory-schemas/inputs/classes/HttpMediaLocationClass.json

@@ -1,7 +1,7 @@
 {
   "name": "HttpMediaLocation",
   "description": "An object describing http location of media object",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/JoystreamMediaLocationClass.json

@@ -1,7 +1,7 @@
 {
   "name": "JoystreamMediaLocation",
   "description": "An object describing location of media object in a format specific to Joystream platform",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/KnownLicenseClass.json

@@ -1,6 +1,6 @@
 {
   "name": "KnownLicense",
   "description": "A commonly recognized license (ie. CC_BY_SA)",
-  "maximum_entities_count": 100,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 500,
+  "default_entity_creation_voucher_upper_bound": 500
 }

+ 2 - 2
content-directory-schemas/inputs/classes/LanguageClass.json

@@ -1,6 +1,6 @@
 {
   "name": "Language",
   "description": "A language in which the content on the platform may be published",
-  "maximum_entities_count": 100,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 500,
+  "default_entity_creation_voucher_upper_bound": 500
 }

+ 2 - 2
content-directory-schemas/inputs/classes/LicenseClass.json

@@ -1,7 +1,7 @@
 {
   "name": "License",
   "description": "Describes a license the media can be published under",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/MediaLocationClass.json

@@ -1,7 +1,7 @@
 {
   "name": "MediaLocation",
   "description": "An object describing how the related media object can be accessed",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/UserDefinedLicenseClass.json

@@ -1,7 +1,7 @@
 {
   "name": "UserDefinedLicense",
   "description": "Custom license defined by the user",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/VideoClass.json

@@ -1,7 +1,7 @@
 {
   "name": "Video",
   "description": "Describes a Video",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/VideoMediaClass.json

@@ -1,7 +1,7 @@
 {
   "name": "VideoMedia",
   "description": "Describes a video media object",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
 }

+ 8 - 0
content-directory-schemas/inputs/entityBatches/KnownLicenseBatch.json

@@ -4,48 +4,56 @@
     {
       "code": "PDM",
       "name": "Public Domain",
+      "description": "For items which are not protected by copyright. This is not a license, but rather a copyright status. Some government-produced works, items with expired copyrights, and those which are ineligible for copyright protection may be included in this category.",
       "url": "https://creativecommons.org/share-your-work/public-domain/pdm",
       "attributionRequired": false
     },
     {
       "code": "CC0",
       "name": "Public Domain Dedication",
+      "description": "The CC0 (Public Domain Dedication) License allows creators to waive all rights to their creations and release them into the Public Domain.",
       "url": "https://creativecommons.org/share-your-work/public-domain/cc0",
       "attributionRequired": true
     },
     {
       "code": "CC_BY",
       "name": "Creative Commons Attribution License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Read the License Deed for more information.",
       "url": "https://creativecommons.org/licenses/by/4.0",
       "attributionRequired": true
     },
     {
       "code": "CC_BY_SA",
       "name": "Creative Commons Attribution-ShareAlike License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Any derivative works must be distributed under the same license. Read the License Deed for more information.",
       "url": "https://creativecommons.org/licenses/by-sa/4.0",
       "attributionRequired": true
     },
     {
       "code": "CC_BY_ND",
       "name": "Creative Commons Attribution-NoDerivs License",
+      "description": "Sharing this content is permitted, but attribution must be provided. You may not remix, transform, or build upon the material. Read the License Deed for more information.",
       "url": "https://creativecommons.org/licenses/by-nd/4.0",
       "attributionRequired": true
     },
     {
       "code": "CC_BY_NC",
       "name": "Creative Commons Attribution-NonCommercial License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Commercial use is not permitted. Read the License Deed for more information.",
       "url": "https://creativecommons.org/licenses/by-nc/4.0",
       "attributionRequired": true
     },
     {
       "code": "CC_BY_NC_SA",
       "name": "Creative Commons Attribution-NonCommercial-ShareAlike License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Any derivative works must be distributed under the same license. Commercial use is not permitted. Read the License Deed for more information.",
       "url": "https://creativecommons.org/licenses/by-nc-sa/4.0",
       "attributionRequired": true
     },
     {
       "code": "CC_BY_NC_ND",
       "name": "Creative Commons Attribution-NonCommercial-NoDerivs License",
+      "description": "Sharing this content is permitted, but attribution must be provided. You may not remix, transform, or build upon the material. Commercial use is not permitted. Read the License Deed for more information.",
       "url": "https://creativecommons.org/licenses/by-nc-nd/4.0",
       "attributionRequired": true
     }

+ 6 - 2
content-directory-schemas/package.json

@@ -14,9 +14,9 @@
     "generate:types": "ts-node --files ./scripts/schemasToTS.ts",
     "generate:entity-schemas": "ts-node ./scripts/inputSchemasToEntitySchemas.ts",
     "generate:all": "yarn generate:entity-schemas && yarn generate:types",
-    "initialize:alice-as-lead": "ts-node ./scripts/devInitAliceLead.ts",
+    "initialize:lead": "ts-node ./scripts/devInitContentLead.ts",
     "initialize:content-dir": "ts-node ./scripts/initializeContentDir.ts",
-    "initialize:dev": "yarn initialize:alice-as-lead && yarn initialize:content-dir",
+    "initialize:dev": "yarn initialize:lead && yarn initialize:content-dir",
     "example:createChannel": "ts-node ./examples/createChannel.ts",
     "example:createVideo": "ts-node ./examples/createVideo.ts",
     "example:updateChannelHandle": "ts-node ./examples/updateChannelHandle.ts",
@@ -48,5 +48,9 @@
   "volta": {
     "node": "12.18.2",
     "yarn": "1.22.4"
+  },
+  "publishConfig": {
+    "access": "public",
+    "registry": "https://registry.npmjs.org"
   }
 }

+ 0 - 88
content-directory-schemas/scripts/devInitAliceLead.ts

@@ -1,88 +0,0 @@
-import { types } from '@joystream/types'
-import { ApiPromise, WsProvider } from '@polkadot/api'
-import { SubmittableExtrinsic } from '@polkadot/api/types'
-import { ExtrinsicsHelper, getAlicePair } from '../src/helpers/extrinsics'
-
-async function main() {
-  // Init api
-  const WS_URI = process.env.WS_URI || 'ws://127.0.0.1:9944'
-  console.log(`Initializing the api (${WS_URI})...`)
-  const provider = new WsProvider(WS_URI)
-  const api = await ApiPromise.create({ provider, types })
-
-  const ALICE = getAlicePair()
-
-  const txHelper = new ExtrinsicsHelper(api)
-
-  const sudo = (tx: SubmittableExtrinsic<'promise'>) => api.tx.sudo.sudo(tx)
-  const extrinsics: SubmittableExtrinsic<'promise'>[] = []
-
-  // Create membership if not already created
-  let aliceMemberId: number | undefined = (await api.query.members.memberIdsByControllerAccountId(ALICE.address))
-    .toArray()[0]
-    ?.toNumber()
-
-  if (aliceMemberId === undefined) {
-    console.log('Perparing Alice member account creation extrinsic...')
-    aliceMemberId = (await api.query.members.nextMemberId()).toNumber()
-    extrinsics.push(api.tx.members.buyMembership(0, 'alice', null, null))
-  } else {
-    console.log(`Alice member id found: ${aliceMemberId}...`)
-  }
-
-  // Set Alice as lead if lead not already set
-  if ((await api.query.contentDirectoryWorkingGroup.currentLead()).isNone) {
-    const newOpeningId = (await api.query.contentDirectoryWorkingGroup.nextOpeningId()).toNumber()
-    const newApplicationId = (await api.query.contentDirectoryWorkingGroup.nextApplicationId()).toNumber()
-    // Create curator lead opening
-    console.log('Perparing Create Curator Lead Opening extrinsic...')
-    extrinsics.push(
-      sudo(
-        api.tx.contentDirectoryWorkingGroup.addOpening(
-          { CurrentBlock: null }, // activate_at
-          { max_review_period_length: 9999 }, // OpeningPolicyCommitment
-          'api-examples curator opening', // human_readable_text
-          'Leader' // opening_type
-        )
-      )
-    )
-
-    // Apply to lead opening
-    console.log('Perparing Apply to Curator Lead Opening as Alice extrinsic...')
-    extrinsics.push(
-      api.tx.contentDirectoryWorkingGroup.applyOnOpening(
-        aliceMemberId, // member id
-        newOpeningId, // opening id
-        ALICE.address, // address
-        null, // opt role stake
-        null, // opt appl. stake
-        'api-examples curator opening appl.' // human_readable_text
-      )
-    )
-
-    // Begin review period
-    console.log('Perparing Begin Applicant Review extrinsic...')
-    extrinsics.push(sudo(api.tx.contentDirectoryWorkingGroup.beginApplicantReview(newOpeningId)))
-
-    // Fill opening
-    console.log('Perparing Fill Opening extrinsic...')
-    extrinsics.push(
-      sudo(
-        api.tx.contentDirectoryWorkingGroup.fillOpening(
-          newOpeningId, // opening id
-          api.createType('ApplicationIdSet', [newApplicationId]), // succesful applicants
-          null // reward policy
-        )
-      )
-    )
-
-    console.log('Sending extrinsics...')
-    await txHelper.sendAndCheck(ALICE, extrinsics, 'Failed to initialize Alice as Content Curators Lead!')
-  } else {
-    console.log('Curators lead already exists, skipping...')
-  }
-}
-
-main()
-  .then(() => process.exit())
-  .catch((e) => console.error(e))

+ 105 - 0
content-directory-schemas/scripts/devInitContentLead.ts

@@ -0,0 +1,105 @@
+import { types } from '@joystream/types'
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ExtrinsicsHelper, getAlicePair, getKeyFromSuri } from '../src/helpers/extrinsics'
+
+async function main() {
+  // Init api
+  const WS_URI = process.env.WS_URI || 'ws://127.0.0.1:9944'
+  console.log(`Initializing the api (${WS_URI})...`)
+  const provider = new WsProvider(WS_URI)
+  const api = await ApiPromise.create({ provider, types })
+
+  const LeadKeyPair = process.env.LEAD_URI ? getKeyFromSuri(process.env.LEAD_URI) : getAlicePair()
+  const SudoKeyPair = process.env.SUDO_URI ? getKeyFromSuri(process.env.SUDO_URI) : getAlicePair()
+
+  const txHelper = new ExtrinsicsHelper(api)
+
+  const sudo = (tx: SubmittableExtrinsic<'promise'>) => api.tx.sudo.sudo(tx)
+
+  // Create membership if not already created
+  let memberId: number | undefined = (await api.query.members.memberIdsByControllerAccountId(LeadKeyPair.address))
+    .toArray()[0]
+    ?.toNumber()
+
+  // Only buy membership if LEAD_URI is not provided
+  if (memberId === undefined && process.env.LEAD_URI) {
+    throw new Error('Make sure Controller key LEAD_URI is for a member')
+  }
+
+  if (memberId === undefined) {
+    console.log('Perparing member account creation extrinsic...')
+    memberId = (await api.query.members.nextMemberId()).toNumber()
+    await txHelper.sendAndCheck(
+      LeadKeyPair,
+      [api.tx.members.buyMembership(0, 'alice', null, null)],
+      'Failed to setup member account'
+    )
+  }
+
+  console.log(`Making member id: ${memberId} the content lead.`)
+
+  // Create a new lead opening
+  if ((await api.query.contentDirectoryWorkingGroup.currentLead()).isNone) {
+    const newOpeningId = (await api.query.contentDirectoryWorkingGroup.nextOpeningId()).toNumber()
+    const newApplicationId = (await api.query.contentDirectoryWorkingGroup.nextApplicationId()).toNumber()
+    // Create curator lead opening
+    console.log('Perparing Create Curator Lead Opening extrinsic...')
+    await txHelper.sendAndCheck(
+      SudoKeyPair,
+      [
+        sudo(
+          api.tx.contentDirectoryWorkingGroup.addOpening(
+            { CurrentBlock: null }, // activate_at
+            { max_review_period_length: 9999 }, // OpeningPolicyCommitment
+            'bootstrap curator opening', // human_readable_text
+            'Leader' // opening_type
+          )
+        ),
+      ],
+      'Failed to create Content Curators Lead opening!'
+    )
+
+    // Apply to lead opening
+    console.log('Perparing Apply to Curator Lead Opening as extrinsic...')
+    await txHelper.sendAndCheck(
+      LeadKeyPair,
+      [
+        api.tx.contentDirectoryWorkingGroup.applyOnOpening(
+          memberId, // member id
+          newOpeningId, // opening id
+          LeadKeyPair.address, // address
+          null, // opt role stake
+          null, // opt appl. stake
+          'bootstrap curator opening' // human_readable_text
+        ),
+      ],
+      'Failed to apply on lead opening!'
+    )
+
+    const extrinsics: SubmittableExtrinsic<'promise'>[] = []
+    // Begin review period
+    console.log('Perparing Begin Applicant Review extrinsic...')
+    extrinsics.push(sudo(api.tx.contentDirectoryWorkingGroup.beginApplicantReview(newOpeningId)))
+
+    // Fill opening
+    console.log('Perparing Fill Opening extrinsic...')
+    extrinsics.push(
+      sudo(
+        api.tx.contentDirectoryWorkingGroup.fillOpening(
+          newOpeningId, // opening id
+          api.createType('ApplicationIdSet', [newApplicationId]), // succesful applicants
+          null // reward policy
+        )
+      )
+    )
+
+    await txHelper.sendAndCheck(SudoKeyPair, extrinsics, 'Failed to initialize Content Curators Lead!')
+  } else {
+    console.log('Curators lead already exists, skipping...')
+  }
+}
+
+main()
+  .then(() => process.exit())
+  .catch((e) => console.error(e))

+ 9 - 6
content-directory-schemas/scripts/initializeContentDir.ts

@@ -4,7 +4,7 @@ import { getInitializationInputs } from '../src/helpers/inputs'
 import fs from 'fs'
 import path from 'path'
 import { InputParser } from '../src/helpers/InputParser'
-import { ExtrinsicsHelper, getAlicePair } from '../src/helpers/extrinsics'
+import { ExtrinsicsHelper, getAlicePair, getKeyFromSuri } from '../src/helpers/extrinsics'
 
 // Save entity operations output here for easier debugging
 const ENTITY_OPERATIONS_OUTPUT_PATH = path.join(__dirname, '../operations.json')
@@ -18,7 +18,7 @@ async function main() {
   const provider = new WsProvider(WS_URI)
   const api = await ApiPromise.create({ provider, types })
 
-  const ALICE = getAlicePair()
+  const LeadKeyPair = process.env.LEAD_URI ? getKeyFromSuri(process.env.LEAD_URI) : getAlicePair()
 
   // Emptiness check
   if ((await api.query.contentDirectory.classById.keys()).length > 0) {
@@ -31,11 +31,11 @@ async function main() {
 
   console.log(`Initializing classes (${classInputs.length} input files found)...\n`)
   const classExtrinsics = parser.getCreateClassExntrinsics()
-  await txHelper.sendAndCheck(ALICE, classExtrinsics, 'Class initialization failed!')
+  await txHelper.sendAndCheck(LeadKeyPair, classExtrinsics, 'Class initialization failed!')
 
   console.log(`Initializing schemas (${schemaInputs.length} input files found)...\n`)
   const schemaExtrinsics = await parser.getAddSchemaExtrinsics()
-  await txHelper.sendAndCheck(ALICE, schemaExtrinsics, 'Schemas initialization failed!')
+  await txHelper.sendAndCheck(LeadKeyPair, schemaExtrinsics, 'Schemas initialization failed!')
 
   console.log(`Initializing entities (${entityBatchInputs.length} input files found)`)
   const entityOperations = await parser.getEntityBatchOperations()
@@ -51,7 +51,7 @@ async function main() {
   )
   console.log(`Sending Transaction extrinsic (${entityOperations.length} operations)...`)
   await txHelper.sendAndCheck(
-    ALICE,
+    LeadKeyPair,
     [api.tx.contentDirectory.transaction({ Lead: null }, entityOperations)],
     'Entity initialization failed!'
   )
@@ -59,4 +59,7 @@ async function main() {
 
 main()
   .then(() => process.exit())
-  .catch((e) => console.error(e))
+  .catch((e) => {
+    console.error(e)
+    process.exit(-1)
+  })

+ 8 - 1
content-directory-schemas/src/helpers/extrinsics.ts

@@ -7,7 +7,7 @@ import { TypeRegistry } from '@polkadot/types'
 
 // TODO: Move to @joystream/js soon
 
-export function getAlicePair() {
+export function getAlicePair(): KeyringPair {
   const keyring = new Keyring({ type: 'sr25519' })
   keyring.addFromUri('//Alice', { name: 'Alice' })
   const ALICE = keyring.getPairs()[0]
@@ -15,6 +15,13 @@ export function getAlicePair() {
   return ALICE
 }
 
+export function getKeyFromSuri(suri: string): KeyringPair {
+  const keyring = new Keyring({ type: 'sr25519' })
+
+  // Assume a SURI, add to keyring and return keypair
+  return keyring.addFromUri(suri)
+}
+
 export class ExtrinsicsHelper {
   api: ApiPromise
   noncesByAddress: Map<string, number>

+ 1 - 1
package.json

@@ -36,7 +36,7 @@
     "babel-core": "^7.0.0-bridge.0",
     "typescript": "^3.9.7",
     "bn.js": "^5.1.2",
-    "@dzlzv/hydra-indexer-lib": "0.0.21-legacy.1.26.1"
+    "@dzlzv/hydra-indexer-lib": "0.0.22-legacy.1.26.1"
   },
   "devDependencies": {
     "eslint": "^7.6.0",

+ 3 - 0
pioneer/packages/apps-routing/src/index.ts

@@ -27,10 +27,12 @@ import proposals from './joy-proposals';
 import roles from './joy-roles';
 import forum from './joy-forum';
 import tokenomics from './joy-tokenomics';
+import media from './joy-media';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
     ? [
+      media(t),
       tokenomics(t),
       members(t),
       roles(t),
@@ -47,6 +49,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
       privacyPolicy(t)
     ]
     : [
+      media(t),
       tokenomics(t),
       members(t),
       roles(t),

+ 13 - 0
pioneer/packages/apps-routing/src/joy-media.ts

@@ -0,0 +1,13 @@
+import { Route } from './types';
+
+import Media from '@polkadot/joy-media';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Media,
+    display: {},
+    text: t<string>('nav.media', 'Media', { ns: 'apps-routing' }),
+    icon: 'play-circle',
+    name: 'media'
+  };
+}

+ 5 - 0
pioneer/packages/apps/src/wp-jpg.d.ts

@@ -0,0 +1,5 @@
+declare module '*.jpg' {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const content: any;
+  export default content;
+}

+ 13 - 0
pioneer/packages/joy-media/package.json

@@ -0,0 +1,13 @@
+{
+  "name": "@polkadot/joy-media",
+  "version": "0.1.0",
+  "description": "Pioneer's media page",
+  "main": "index.js",
+  "scripts": {},
+  "author": "Joystream contributors",
+  "maintainers": [],
+  "dependencies": {
+    "@babel/runtime": "^7.10.5",
+    "@polkadot/react-components": "0.51.1"
+  }
+}

BIN
pioneer/packages/joy-media/src/assets/atlas-screenshot.jpg


+ 118 - 0
pioneer/packages/joy-media/src/index.tsx

@@ -0,0 +1,118 @@
+import React from 'react';
+import styled from 'styled-components';
+
+import { I18nProps } from '@polkadot/react-components/types';
+import _ from 'lodash';
+
+import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
+import translate from './translate';
+import { Button, Grid, Message, Icon, Image } from 'semantic-ui-react';
+
+import AtlasScreenShot from './assets/atlas-screenshot.jpg';
+
+const MediaMain = styled.main`
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 2em;
+  font-size: 1.2em;
+  p {
+    margin: 0.25em;
+    padding: 0;
+  }
+`;
+
+const Header = styled.header`
+  margin-bottom: 1em;
+  h1 {
+    color: #222 !important;
+  }
+`;
+
+const StyledMessage = styled(Message)`
+  font-size: 1.2em;
+  display: flex;
+  flex-direction: column;
+  background: #fff !important;
+  .header, .content {
+    margin-bottom: 0.5em !important;
+  }
+  .button {
+    margin-top: auto;
+    margin-right: auto !important;
+  }
+`;
+
+const Screenshot = styled(Image)`
+  margin: 0.5em 0;
+  transition: opacity 0.5s;
+  :hover { opacity: 0.7; }
+`;
+
+interface Props extends AppMainRouteProps, I18nProps {}
+
+const App: React.FC<Props> = () => {
+  return (
+    <MediaMain>
+      <Header>
+        <h1>Hello there!</h1>
+        <p>
+          We have now upgraded to the Babylon chain.
+        </p>
+        <p>
+          Pioneer consequently <b>no longer supports</b> media uploads and consumption.
+        </p>
+      </Header>
+      <Grid stackable>
+        <Grid.Row columns={2}>
+          <Grid.Column>
+            <StyledMessage>
+              <Message.Header>Media consumption</Message.Header>
+              <Message.Content>
+                Media consumption has been migrated over to our new consumer interface.
+                <Screenshot
+                  src={AtlasScreenShot as string}
+                  href='https://play.joystream.org'
+                  target='_blank'
+                  rel='noopener noreferrer'/>
+              </Message.Content>
+              <Button
+                size='big'
+                primary
+                icon
+                labelPosition='right'
+                href='https://play.joystream.org'
+                target='_blank'
+                rel='noopener noreferrer'>
+                Launch Atlas
+                <Icon name='arrow right' />
+              </Button>
+            </StyledMessage>
+          </Grid.Column>
+          <Grid.Column>
+            <StyledMessage>
+              <Message.Header>Uploading content</Message.Header>
+              <Message.Content>
+                Uploading has been migrated over to the Joystream CLI.
+                Instructions on how to use the CLI can be found in our helpdesk.
+              </Message.Content>
+              <Button
+                size='big'
+                primary
+                href='https://github.com/Joystream/helpdesk/tree/master/roles/content-creators'
+                icon
+                labelPosition='right'
+                target='_blank'
+                rel='noopener noreferrer'>
+                Explore Joystream CLI
+                <Icon name='arrow right' />
+              </Button>
+            </StyledMessage>
+          </Grid.Column>
+        </Grid.Row>
+      </Grid>
+    </MediaMain>
+  );
+};
+
+export default translate(App);

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

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

+ 5 - 3
pioneer/tsconfig.json

@@ -28,6 +28,10 @@
       "@polkadot/joy-utils/*": [ "packages/joy-utils/src/*" ],
       "@polkadot/joy-forum": [ "packages/joy-forum/src/" ],
       "@polkadot/joy-forum/*": [ "packages/joy-forum/src/*" ],
+      "@polkadot/joy-media": [ "packages/joy-media/src" ],
+      "@polkadot/joy-media/*": [ "packages/joy-media/src/*" ],
+      "@polkadot/joy-tokenomics": [ "packages/joy-tokenomics/src" ],
+      "@polkadot/joy-tokenomics/*": [ "packages/joy-tokenomics/src/*" ],
       "@polkadot/apps/*": ["packages/apps/src/*"],
       "@polkadot/apps": ["packages/apps/src"],
       "@polkadot/apps-config/*": [ "packages/apps-config/src/*" ],
@@ -86,9 +90,7 @@
       "@polkadot/react-query": [ "packages/react-query/src" ],
       "@polkadot/react-query/*": [ "packages/react-query/src/*" ],
       "@polkadot/react-signer": [ "packages/react-signer/src" ],
-      "@polkadot/react-signer/*": [ "packages/react-signer/src/*" ],
-      "@polkadot/joy-tokenomics": [ "packages/joy-tokenomics/src" ],
-      "@polkadot/joy-tokenomics/*": [ "packages/joy-tokenomics/src/*" ]
+      "@polkadot/react-signer/*": [ "packages/react-signer/src/*" ]
     },
     "skipLibCheck": true,
     "typeRoots": [

+ 2 - 2
query-node/mappings/content-directory/content-dir-consts.ts

@@ -1,4 +1,4 @@
-import { IPropertyWithId } from '../types'
+import { IKnownClass, IPropertyWithId } from '../types'
 
 // Content directory predefined class names
 export enum ContentDirectoryKnownClasses {
@@ -18,7 +18,7 @@ export enum ContentDirectoryKnownClasses {
 }
 
 // Predefined content-directory classes, classId may change after the runtime seeding
-export const contentDirectoryClassNamesWithId: { classId: number; name: string }[] = [
+export const contentDirectoryClassNamesWithId: IKnownClass[] = [
   { name: ContentDirectoryKnownClasses.CHANNEL, classId: 1 },
   { name: ContentDirectoryKnownClasses.CATEGORY, classId: 2 },
   { name: ContentDirectoryKnownClasses.HTTPMEDIALOCATION, classId: 3 },

+ 15 - 37
query-node/mappings/content-directory/entity/index.ts

@@ -57,7 +57,6 @@ import {
   userDefinedLicensePropertyNamesWithId,
   videoMediaEncodingPropertyNamesWithId,
   videoPropertyNamesWithId,
-  contentDirectoryClassNamesWithId,
   ContentDirectoryKnownClasses,
   featuredVideoPropertyNamesWithId,
 } from '../content-dir-consts'
@@ -80,7 +79,7 @@ import {
   IMediaLocation,
   IFeaturedVideo,
 } from '../../types'
-import { getOrCreate } from '../get-or-create'
+import { getOrCreate, getKnownClass } from '../get-or-create'
 
 const debug = Debug('mappings:content-directory')
 
@@ -91,22 +90,13 @@ async function contentDirectory_EntitySchemaSupportAdded(db: DB, event: Substrat
 
   const { blockNumber: block } = event
   const entityId = decode.stringIfyEntityId(event)
-  const classEntity = await db.get(ClassEntity, { where: { id: entityId } })
 
-  if (classEntity === undefined) {
-    console.log(`Class not found for the EntityId: ${entityId}`)
-    return
-  }
-
-  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === classEntity.classId)
-  if (cls === undefined) {
-    console.log('Not recognized class')
-    return
-  }
+  const [knownClass] = await getKnownClass(db, { where: { id: entityId } })
+  if (!knownClass) return
 
   const arg: IDBBlockId = { db, block, id: entityId }
 
-  switch (cls.name) {
+  switch (knownClass.name) {
     case ContentDirectoryKnownClasses.CHANNEL:
       await createChannel(
         arg,
@@ -183,7 +173,7 @@ async function contentDirectory_EntitySchemaSupportAdded(db: DB, event: Substrat
       break
 
     default:
-      throw new Error(`Unknown class name: ${cls.name}`)
+      throw new Error(`Unknown class name: ${knownClass.name}`)
   }
 }
 
@@ -194,19 +184,10 @@ async function contentDirectory_EntityRemoved(db: DB, event: SubstrateEvent): Pr
   const entityId = decode.stringIfyEntityId(event)
   const where: IWhereCond = { where: { id: entityId } }
 
-  const classEntity = await db.get(ClassEntity, where)
-  if (classEntity === undefined) {
-    console.log(`Class not found for the EntityId: ${entityId}`)
-    return
-  }
-
-  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === classEntity.classId)
-  if (cls === undefined) {
-    console.log('Unknown class')
-    return
-  }
+  const [knownClass, classEntity] = await getKnownClass(db, where)
+  if (!knownClass) return
 
-  switch (cls.name) {
+  switch (knownClass.name) {
     case ContentDirectoryKnownClasses.CHANNEL:
       await removeChannel(db, where)
       break
@@ -259,8 +240,9 @@ async function contentDirectory_EntityRemoved(db: DB, event: SubstrateEvent): Pr
       break
 
     default:
-      throw new Error(`Unknown class name: ${cls.name}`)
+      throw new Error(`Unknown class name: ${knownClass.name}`)
   }
+  await db.remove<ClassEntity>(classEntity)
 }
 
 // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -290,21 +272,17 @@ async function contentDirectory_EntityPropertyValuesUpdated(db: DB, event: Subst
 
   const { 2: newPropertyValues } = extrinsic.args
   const entityId = decode.stringIfyEntityId(event)
-
-  const ce = await db.get(ClassEntity, { where: { id: entityId } })
-  if (ce === undefined) throw Error(`Class not found for the entity id: ${entityId}`)
-
-  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === ce.classId)
-  if (cls === undefined) throw Error(`Not known class id: ${ce.classId}`)
-
   const where: IWhereCond = { where: { id: entityId } }
 
+  const [knownClass] = await getKnownClass(db, where)
+  if (!knownClass) return
+
   // TODO: change setProperties method signature to accecpt SubstrateExtrinsic, then remove the following
   // line. The reason we push the same arg is beacuse of the setProperties method check the 3rd indices
   // to get properties values
   extrinsic.args.push(newPropertyValues)
 
-  switch (cls.name) {
+  switch (knownClass.name) {
     case ContentDirectoryKnownClasses.CHANNEL:
       updateChannelEntityPropertyValues(db, where, decode.setProperties<IChannel>(event, channelPropertyNamesWithId), 0)
       break
@@ -406,7 +384,7 @@ async function contentDirectory_EntityPropertyValuesUpdated(db: DB, event: Subst
       break
 
     default:
-      throw new Error(`Unknown class name: ${cls.name}`)
+      throw new Error(`Unknown class name: ${knownClass.name}`)
   }
 }
 

+ 21 - 13
query-node/mappings/content-directory/entity/remove.ts

@@ -1,4 +1,5 @@
 import assert from 'assert'
+import Debug from 'debug'
 
 import { DB } from '../../../generated/indexer'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
@@ -17,63 +18,70 @@ import { FeaturedVideo } from '../../../generated/graphql-server/src/modules/fea
 
 import { IWhereCond } from '../../types'
 
+const debug = Debug(`mappings:remove-entity`)
+
 function assertKeyViolation(entityName: string, entityId: string) {
   assert(false, `Can not remove ${entityName}(${entityId})! There are references to this entity`)
 }
 
+function logEntityNotFound(className: string, where: IWhereCond) {
+  debug(`${className}(${where.where.id}) not found. This happen when schema support is not added for the entity.`)
+}
+
 async function removeChannel(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Channel, where)
-  if (!record) throw Error(`Channel(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`Channel`, where)
   if (record.videos && record.videos.length) assertKeyViolation(`Channel`, record.id)
   await db.remove<Channel>(record)
 }
 
 async function removeCategory(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Category, where)
-  if (!record) throw Error(`Category(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`Category`, where)
   if (record.videos && record.videos.length) assertKeyViolation(`Category`, record.id)
   await db.remove<Category>(record)
 }
 async function removeVideoMedia(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(VideoMedia, where)
-  if (!record) throw Error(`VideoMedia(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`VideoMedia`, where)
   if (record.video) assertKeyViolation(`VideoMedia`, record.id)
   await db.remove<VideoMedia>(record)
 }
+
 async function removeVideo(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Video, where)
-  if (!record) throw Error(`Video(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`Video`, where)
   await db.remove<Video>(record)
 }
 
 async function removeLicense(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(LicenseEntity, where)
-  if (!record) throw Error(`License(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`License`, where)
   if (record.videolicense && record.videolicense.length) assertKeyViolation(`License`, record.id)
   await db.remove<LicenseEntity>(record)
 }
 
 async function removeUserDefinedLicense(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(UserDefinedLicenseEntity, where)
-  if (!record) throw Error(`UserDefinedLicense(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`UserDefinedLicense`, where)
   await db.remove<UserDefinedLicenseEntity>(record)
 }
 
 async function removeKnownLicense(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(KnownLicenseEntity, where)
-  if (!record) throw Error(`KnownLicense(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`KnownLicense`, where)
   await db.remove<KnownLicenseEntity>(record)
 }
 async function removeMediaLocation(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(MediaLocationEntity, where)
-  if (!record) throw Error(`MediaLocation(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`MediaLocation`, where)
   if (record.videoMedia) assertKeyViolation('MediaLocation', record.id)
   await db.remove<MediaLocationEntity>(record)
 }
 
 async function removeHttpMediaLocation(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(HttpMediaLocationEntity, where)
-  if (!record) throw Error(`HttpMediaLocation(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`HttpMediaLocation`, where)
   if (record.medialocationentityhttpMediaLocation && record.medialocationentityhttpMediaLocation.length) {
     assertKeyViolation('HttpMediaLocation', record.id)
   }
@@ -82,7 +90,7 @@ async function removeHttpMediaLocation(db: DB, where: IWhereCond): Promise<void>
 
 async function removeJoystreamMediaLocation(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(JoystreamMediaLocationEntity, where)
-  if (!record) throw Error(`JoystreamMediaLocation(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`JoystreamMediaLocation`, where)
   if (record.medialocationentityjoystreamMediaLocation && record.medialocationentityjoystreamMediaLocation.length) {
     assertKeyViolation('JoystreamMediaLocation', record.id)
   }
@@ -91,7 +99,7 @@ async function removeJoystreamMediaLocation(db: DB, where: IWhereCond): Promise<
 
 async function removeLanguage(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Language, where)
-  if (!record) throw Error(`Language(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`Language`, where)
   if (record.channellanguage && record.channellanguage.length) assertKeyViolation('Language', record.id)
   if (record.videolanguage && record.videolanguage.length) assertKeyViolation('Language', record.id)
   await db.remove<Language>(record)
@@ -99,13 +107,13 @@ async function removeLanguage(db: DB, where: IWhereCond): Promise<void> {
 
 async function removeVideoMediaEncoding(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(VideoMediaEncoding, where)
-  if (!record) throw Error(`VideoMediaEncoding(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`VideoMediaEncoding`, where)
   await db.remove<VideoMediaEncoding>(record)
 }
 
 async function removeFeaturedVideo(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(FeaturedVideo, { ...where, relations: ['video'] })
-  if (!record) throw Error(`FeaturedVideo(${where.where.id}) not found`)
+  if (!record) return logEntityNotFound(`FeaturedVideo`, where)
 
   record.video.isFeatured = false
   record.video.featured = undefined

+ 15 - 0
query-node/mappings/content-directory/get-or-create.ts

@@ -11,11 +11,13 @@ import { LicenseEntity } from '../../generated/graphql-server/src/modules/licens
 import { MediaLocationEntity } from '../../generated/graphql-server/src/modules/media-location-entity/media-location-entity.model'
 import { Video } from '../../generated/graphql-server/src/modules/video/video.model'
 import { NextEntityId } from '../../generated/graphql-server/src/modules/next-entity-id/next-entity-id.model'
+import { ClassEntity } from '../../generated/graphql-server/src/modules/class-entity/class-entity.model'
 
 import { decode } from './decode'
 import {
   categoryPropertyNamesWithId,
   channelPropertyNamesWithId,
+  contentDirectoryClassNamesWithId,
   httpMediaLocationPropertyNamesWithId,
   joystreamMediaLocationPropertyNamesWithId,
   knownLicensePropertyNamesWIthId,
@@ -34,6 +36,7 @@ import {
   IEntity,
   IHttpMediaLocation,
   IJoystreamMediaLocation,
+  IKnownClass,
   IKnownLicense,
   ILanguage,
   ILicense,
@@ -43,6 +46,7 @@ import {
   IVideo,
   IVideoMedia,
   IVideoMediaEncoding,
+  IWhereCond,
 } from '../types'
 
 import {
@@ -423,6 +427,17 @@ async function video(
   )
 }
 
+export async function getKnownClass(db: DB, where: IWhereCond): Promise<[IKnownClass | undefined, ClassEntity]> {
+  const ce = await db.get(ClassEntity, where)
+  if (!ce) {
+    throw Error(`Class not found for the EntityId: ${where.where.id} or the entity has not been created.`)
+  }
+
+  const knownClass = contentDirectoryClassNamesWithId.find((c) => c.classId === ce.classId)
+  if (!knownClass) console.log('Unknown class')
+  return [knownClass, ce]
+}
+
 export const getOrCreate = {
   language,
   videoMediaEncoding,

+ 5 - 0
query-node/mappings/types.ts

@@ -202,3 +202,8 @@ export type ClassEntityMap = Map<string, IEntity[]>
 export interface IFeaturedVideo {
   video?: IReference
 }
+
+export interface IKnownClass {
+  name: string
+  classId: number
+}

+ 3 - 2
query-node/package.json

@@ -18,7 +18,8 @@
 		"db:indexer:migrate": "(cd ./generated/indexer && yarn db:migrate)",
 		"codegen:indexer": "yarn hydra-cli codegen --no-install --no-graphql && cp indexer-tsconfig.json generated/indexer/tsconfig.json",
 		"codegen:server": "yarn hydra-cli codegen --no-install --no-indexer",
-		"cd-classes": "ts-node scripts/get-class-id-and-name.ts"
+		"cd-classes": "ts-node scripts/get-class-id-and-name.ts",
+		"integration-tests": "./run-tests.sh"
 	},
 	"author": "",
 	"license": "ISC",
@@ -26,7 +27,7 @@
 		"@dzlzv/hydra-cli": "^0.0.24"
 	},
 	"dependencies": {
-		"@dzlzv/hydra-indexer-lib": "^0.0.21-legacy.1.26.1",
+		"@dzlzv/hydra-indexer-lib": "^0.0.22-legacy.1.26.1",
 		"@joystream/types": "^0.14.0",
 		"@types/bn.js": "^4.11.6",
 		"@types/debug": "^4.1.5",

+ 4 - 5
query-node/run-tests.sh

@@ -29,9 +29,6 @@ function cleanup() {
 
 trap cleanup EXIT
 
-# We expect docker image to be started by test runner
-export WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944/
-
 # Bring up db
 docker-compose up -d db
 
@@ -40,8 +37,10 @@ yarn workspace query-node-root db:migrate
 
 docker-compose up -d graphql-server
 
+# Start the joystream-node before the indexer
+docker-compose up -d joystream-node
+
 # Starting up processor will bring up all services it depends on
 docker-compose up -d processor
 
-# Run tests
-ATTACH_TO_NETWORK=joystream_default ../tests/network-tests/run-tests.sh content-directory
+time yarn workspace network-tests run-test-scenario content-directory

+ 8 - 0
runtime-modules/content-directory/src/lib.rs

@@ -2867,6 +2867,14 @@ impl<T: Trait> Module<T> {
     }
 }
 
+impl<T: Trait> Module<T> {
+    pub fn set_initial_ids_to_one() {
+        <NextEntityId<T>>::put(T::EntityId::one());
+        <NextClassId<T>>::put(T::ClassId::one());
+        <NextCuratorGroupId<T>>::put(T::CuratorGroupId::one());
+    }
+}
+
 decl_event!(
     pub enum Event<T>
     where

+ 3 - 0
runtime/CHANGELOG.md

@@ -1,3 +1,6 @@
+### Version 7.9.0 - Babylon - runtime upgrade - December 21 2020
+- Introduction of new and improved content directory
+
 ### Version 7.4.0 - Alexandria - new chain - September 21 2020
 - Update to substrate v2.0.0-rc4
 

+ 2 - 2
runtime/src/lib.rs

@@ -421,13 +421,13 @@ parameter_types! {
     pub const MaxNumberOfMaintainersPerClass: MaxNumber = 10;
     pub const MaxNumberOfSchemasPerClass: MaxNumber = 20;
     pub const MaxNumberOfPropertiesPerSchema: MaxNumber = 40;
-    pub const MaxNumberOfEntitiesPerClass: MaxNumber = 400;
+    pub const MaxNumberOfEntitiesPerClass: MaxNumber = 5000;
     pub const MaxNumberOfCuratorsPerGroup: MaxNumber = 50;
     pub const MaxNumberOfOperationsDuringAtomicBatching: MaxNumber = 500;
     pub const VecMaxLengthConstraint: VecMaxLength = 200;
     pub const TextMaxLengthConstraint: TextMaxLength = 5000;
     pub const HashedTextMaxLengthConstraint: HashedTextMaxLength = Some(25000);
-    pub const IndividualEntitiesCreationLimit: EntityId = 50;
+    pub const IndividualEntitiesCreationLimit: EntityId = 500;
 }
 
 impl content_directory::Trait for Runtime {

+ 7 - 3
setup.sh

@@ -31,6 +31,10 @@ fi
 # Volta nodejs, npm, yarn tools manager
 curl https://get.volta.sh | bash
 
-volta install node@12
-volta install yarn
-volta install npx
+# After installing volta the .profile and .bash_profile are updated
+# to add it to the PATH, so we start new shell to use it
+env bash -c "volta install node@12"
+env bash -c "volta install yarn"
+env bash -c "volta install npx"
+
+echo "Open a new terminal to start using newly installed tools"

+ 4 - 1
storage-node/packages/cli/src/cli.ts

@@ -49,7 +49,7 @@ const usage = `
   Dev Commands:       Commands to run on a development chain.
     dev-init          Setup chain with Alice as lead and storage provider.
     dev-check         Check the chain is setup with Alice as lead and storage provider.
-    vstore-init      Initialize versioned store, Requires SURI of ContentWorking Lead.
+    sudo-create-sp    Initialize the chain with a lead storage provider.
     
   Type 'storage-cli command' for the exact command usage examples.
   `
@@ -72,6 +72,9 @@ const commands = {
   'dev-check': async (api) => {
     return dev.check(api)
   },
+  'sudo-create-sp': async (api) => {
+    return dev.makeMemberInitialLeadAndStorageProvider(api)
+  },
   // Uploads the file to the system. Registers new data object in the runtime, obtains proper colossus instance URL.
   upload: async (
     api: any,

+ 128 - 1
storage-node/packages/cli/src/commands/dev.ts

@@ -3,6 +3,8 @@
 import dbug from 'debug'
 import { KeyringPair } from '@polkadot/keyring/types'
 import { RuntimeApi } from '@joystream/storage-runtime-api'
+import { GenericJoyStreamRoleSchema as HRTJson } from '@joystream/types/hiring/schemas/role.schema.typings'
+
 const debug = dbug('joystream:storage-cli:dev')
 
 // Derivation path appended to well known development seed used on
@@ -142,4 +144,129 @@ const init = async (api: RuntimeApi): Promise<any> => {
   return check(api)
 }
 
-export { init, check, aliceKeyPair, roleKeyPair, developmentPort }
+// Using sudo to create initial storage lead and worker with given keys taken from env variables.
+// Used to quickly setup a storage provider on a new chain before a council is ready.
+const makeMemberInitialLeadAndStorageProvider = async (api: RuntimeApi): Promise<any> => {
+  if (api.workers.getLeadRoleAccount()) {
+    throw new Error('The Storage Lead is already set!')
+  }
+
+  if (!process.env.SUDO_URI) {
+    throw new Error('required SUDO_URI env variable was not set')
+  }
+
+  if (!process.env.MEMBER_ID) {
+    throw new Error('required MEMBER_ID env variable was not set')
+  }
+
+  if (!process.env.MEMBER_CONTROLLER_URI) {
+    throw new Error('required MEMBER_CONTROLLER_URI env variable was not set')
+  }
+
+  if (!process.env.STORAGE_WORKER_ADDRESS) {
+    throw new Error('required STORAGE_WORKER_ADDRESS env variable was not set')
+  }
+
+  const sudoKey = getKeyFromAddressOrSuri(api, process.env.SUDO_URI)
+  const memberId = parseInt(process.env.MEMBER_ID)
+  const memberController = getKeyFromAddressOrSuri(api, process.env.MEMBER_CONTROLLER_URI).address
+  const leadAccount = memberController
+  const workerAccount = process.env.STORAGE_WORKER_ADDRESS
+
+  const sudo = await api.identities.getSudoAccount()
+
+  // Ensure correct sudo key was provided
+  if (!sudo.eq(sudoKey.address)) {
+    throw new Error('Provided SUDO_URI is not the chain sudo')
+  }
+
+  // Ensure MEMBER_ID and MEMBER_CONTROLLER_URI are valid
+  const memberIds = await api.identities.memberIdsOfController(memberController)
+  if (memberIds.find((id) => id.eq(memberId)) === undefined) {
+    throw new Error(
+      'MEMBER_ID and MEMBER_CONTROLLER_URI do not correspond to a registered member and their controller account'
+    )
+  }
+
+  // Ensure STORAGE_WORKER_ADDRESS is a valid Address
+  api.identities.keyring.decodeAddress(workerAccount)
+
+  debug(`Creating Leader with role key: ${leadAccount}`)
+  debug('Creating Lead Opening')
+  const leadOpeningId = await api.workers.devAddStorageLeadOpening(JSON.stringify(getLeadOpeningInfo()))
+  debug('Applying')
+  const leadApplicationId = await api.workers.devApplyOnOpening(leadOpeningId, memberId, memberController, leadAccount)
+  debug('Starting Review')
+  api.workers.devBeginLeadOpeningReview(leadOpeningId)
+  debug('Filling Opening')
+  await api.workers.devFillLeadOpening(leadOpeningId, leadApplicationId)
+
+  const setLeadAccount = await api.workers.getLeadRoleAccount()
+  if (!setLeadAccount.eq(leadAccount)) {
+    throw new Error('Setting Lead failed!')
+  }
+
+  // Create a storage openinging, apply, start review, and fill opening
+  debug(`Making ${workerAccount} account a storage provider.`)
+
+  const openingId = await api.workers.devAddStorageOpening(JSON.stringify(getWorkerOpeningInfo()))
+  debug(`Created new storage opening: ${openingId}`)
+
+  const applicationId = await api.workers.devApplyOnOpening(openingId, memberId, memberController, workerAccount)
+  debug(`Applied with application id: ${applicationId}`)
+
+  api.workers.devBeginStorageOpeningReview(openingId)
+
+  debug(`Filling storage opening.`)
+  const providerId = await api.workers.devFillStorageOpening(openingId, applicationId)
+
+  debug(`Assigned storage provider id: ${providerId}`)
+}
+
+function getLeadOpeningInfo(): HRTJson {
+  return {
+    'version': 1,
+    'headline': 'Initial Storage Lead',
+    'job': {
+      'title': 'Bootstrap Lead',
+      'description': 'Starting opportunity to bootstrap the network',
+    },
+    'application': {
+      'sections': [],
+    },
+    'reward': 'None',
+    'creator': {
+      'membership': {
+        'handle': 'mokhtar',
+      },
+    },
+    'process': {
+      'details': ['automated'],
+    },
+  }
+}
+
+function getWorkerOpeningInfo(): HRTJson {
+  return {
+    'version': 1,
+    'headline': 'Initial Storage Worker',
+    'job': {
+      'title': 'Bootstrap Worker',
+      'description': 'Starting opportunity to bootstrap the network',
+    },
+    'application': {
+      'sections': [],
+    },
+    'reward': 'None',
+    'creator': {
+      'membership': {
+        'handle': 'mokhtar',
+      },
+    },
+    'process': {
+      'details': ['automated'],
+    },
+  }
+}
+
+export { init, check, aliceKeyPair, roleKeyPair, developmentPort, makeMemberInitialLeadAndStorageProvider }

+ 35 - 21
storage-node/packages/colossus/bin/cli.js

@@ -31,18 +31,22 @@ const FLAG_DEFINITIONS = {
   keyFile: {
     type: 'string',
     isRequired: (flags, input) => {
-      // Only required if running server command and not in dev mode
-      const serverCmd = input[0] === 'server'
-      return !flags.dev && serverCmd
+      // Only required if running server command and not in dev or anonymous mode
+      if (flags.anonymous || flags.dev) {
+        return false
+      }
+      return input[0] === 'server'
     },
   },
   publicUrl: {
     type: 'string',
     alias: 'u',
     isRequired: (flags, input) => {
-      // Only required if running server command and not in dev mode
-      const serverCmd = input[0] === 'server'
-      return !flags.dev && serverCmd
+      // Only required if running server command and not in dev or anonymous mode
+      if (flags.anonymous || flags.dev) {
+        return false
+      }
+      return input[0] === 'server'
     },
   },
   passphrase: {
@@ -56,15 +60,21 @@ const FLAG_DEFINITIONS = {
     type: 'number',
     alias: 'i',
     isRequired: (flags, input) => {
-      // Only required if running server command and not in dev mode
-      const serverCmd = input[0] === 'server'
-      return !flags.dev && serverCmd
+      // Only required if running server command and not in dev or anonymous mode
+      if (flags.anonymous || flags.dev) {
+        return false
+      }
+      return input[0] === 'server'
     },
   },
   ipfsHost: {
     type: 'string',
     default: 'localhost',
   },
+  anonymous: {
+    type: 'boolean',
+    default: false,
+  },
 }
 
 const cli = meow(
@@ -77,7 +87,7 @@ const cli = meow(
                   This is the default command if not specified.
     discovery     Run the discovery service only.
 
-  Arguments (required for server. Ignored if running server with --dev option):
+  Arguments (required for with server command, unless --dev or --anonymous args are used):
     --provider-id ID, -i ID     StorageProviderId assigned to you in working group.
     --key-file FILE             JSON key export file to use as the storage provider (role account).
     --public-url=URL, -u URL    API Public URL to announce.
@@ -88,6 +98,8 @@ const cli = meow(
     --port=PORT, -p PORT    Port number to listen on, defaults to 3000.
     --ws-provider WS_URL    Joystream-node websocket provider, defaults to ws://localhost:9944
     --ipfs-host   hostname  ipfs host to use, default to 'localhost'. Default port 5001 is always used
+    --anonymous             Runs server in anonymous mode. Replicates content without need to register
+                            on-chain, and can serve content. Cannot be used to upload content.
   `,
   { flags: FLAG_DEFINITIONS }
 )
@@ -116,8 +128,8 @@ function startExpressApp(app, port) {
 }
 
 // Start app
-function startAllServices({ store, api, port, discoveryClient, ipfsHttpGatewayUrl }) {
-  const app = require('../lib/app')(PROJECT_ROOT, store, api, discoveryClient, ipfsHttpGatewayUrl)
+function startAllServices({ store, api, port, discoveryClient, ipfsHttpGatewayUrl, anonymous }) {
+  const app = require('../lib/app')(PROJECT_ROOT, store, api, discoveryClient, ipfsHttpGatewayUrl, anonymous)
   return startExpressApp(app, port)
 }
 
@@ -149,7 +161,7 @@ function getStorage(runtimeApi, { ipfsHost }) {
   return Storage.create(options)
 }
 
-async function initApiProduction({ wsProvider, providerId, keyFile, passphrase }) {
+async function initApiProduction({ wsProvider, providerId, keyFile, passphrase, anonymous }) {
   // Load key information
   const { RuntimeApi } = require('@joystream/storage-runtime-api')
 
@@ -160,7 +172,7 @@ async function initApiProduction({ wsProvider, providerId, keyFile, passphrase }
     storageProviderId: providerId,
   })
 
-  if (!api.identities.key) {
+  if (!anonymous && !api.identities.key) {
     throw new Error('Failed to unlock storage provider account')
   }
 
@@ -168,7 +180,7 @@ async function initApiProduction({ wsProvider, providerId, keyFile, passphrase }
 
   // We allow the node to startup without correct provider id and account, but syncing and
   // publishing of identity will be skipped.
-  if (!(await api.providerIsActiveWorker())) {
+  if (!anonymous && !(await api.providerIsActiveWorker())) {
     debug('storage provider role account and storageProviderId are not associated with a worker')
   }
 
@@ -295,17 +307,19 @@ const commands = {
 
     const ipfsHost = cli.flags.ipfsHost
     const ipfs = require('ipfs-http-client')(ipfsHost, '5001', { protocol: 'http' })
-    const { PublisherClient, DiscoveryClient } = require('@joystream/service-discovery')
-    const publisherClient = new PublisherClient(ipfs)
-    const discoveryClient = new DiscoveryClient({ ipfs, api })
     const ipfsHttpGatewayUrl = `http://${ipfsHost}:8080/`
 
     const { startSyncing } = require('../lib/sync')
-    startSyncing(api, { syncPeriod: SYNC_PERIOD_MS }, store)
+    startSyncing(api, { syncPeriod: SYNC_PERIOD_MS, anonymous: cli.flags.anonymous }, store)
 
-    announcePublicUrl(api, publicUrl, publisherClient)
+    if (!cli.flags.anonymous) {
+      const { PublisherClient } = require('@joystream/service-discovery')
+      announcePublicUrl(api, publicUrl, new PublisherClient(ipfs))
+    }
 
-    return startAllServices({ store, api, port, discoveryClient, ipfsHttpGatewayUrl })
+    const { DiscoveryClient } = require('@joystream/service-discovery')
+    const discoveryClient = new DiscoveryClient({ ipfs, api })
+    return startAllServices({ store, api, port, discoveryClient, ipfsHttpGatewayUrl, anonymous: cli.flags.anonymous })
   },
   discovery: async () => {
     banner()

+ 2 - 1
storage-node/packages/colossus/lib/app.js

@@ -35,7 +35,7 @@ const fileUploads = require('./middleware/file_uploads')
 const pagination = require('@joystream/storage-utils/pagination')
 
 // Configure app
-function createApp(projectRoot, storage, runtime, discoveryClient, ipfsHttpGatewayUrl) {
+function createApp(projectRoot, storage, runtime, discoveryClient, ipfsHttpGatewayUrl, anonymous) {
   const app = express()
   app.use(cors())
   app.use(bodyParser.json())
@@ -61,6 +61,7 @@ function createApp(projectRoot, storage, runtime, discoveryClient, ipfsHttpGatew
       runtime,
       discoveryClient,
       ipfsHttpGatewayUrl,
+      anonymous,
     },
   })
 

+ 23 - 18
storage-node/packages/colossus/lib/sync.js

@@ -132,30 +132,35 @@ async function syncPeriodic({ api, flags, storage, contentBeingSynced, contentCo
       return retry()
     }
 
-    // Retry later if provider is not active
-    if (!(await api.providerIsActiveWorker())) {
-      debug(
-        'storage provider role account and storageProviderId are not associated with a worker. Postponing sync run.'
-      )
-      return retry()
-    }
+    if (!flags.anonymous) {
+      // Retry later if provider is not active
+      if (!(await api.providerIsActiveWorker())) {
+        debug(
+          'storage provider role account and storageProviderId are not associated with a worker. Postponing sync run.'
+        )
+        return retry()
+      }
 
-    const recommendedBalance = await api.providerHasMinimumBalance(300)
-    if (!recommendedBalance) {
-      debug('Warning: Provider role account is running low on balance.')
-    }
+      const recommendedBalance = await api.providerHasMinimumBalance(300)
+      if (!recommendedBalance) {
+        debug('Warning: Provider role account is running low on balance.')
+      }
 
-    const sufficientBalance = await api.providerHasMinimumBalance(100)
-    if (!sufficientBalance) {
-      debug('Provider role account does not have sufficient balance. Postponing sync run!')
-      return retry()
+      const sufficientBalance = await api.providerHasMinimumBalance(100)
+      if (!sufficientBalance) {
+        debug('Provider role account does not have sufficient balance. Postponing sync run!')
+        return retry()
+      }
     }
 
     await syncContent({ api, storage, contentBeingSynced, contentCompleteSynced })
-    const relationshipIds = await createNewRelationships({ api, contentCompleteSynced })
-    await setRelationshipsReady({ api, relationshipIds })
 
-    debug(`Sync run completed, set ${relationshipIds.length} new relationships to ready`)
+    // Only update on chain state if not in anonymous mode
+    if (!flags.anonymous) {
+      const relationshipIds = await createNewRelationships({ api, contentCompleteSynced })
+      await setRelationshipsReady({ api, relationshipIds })
+      debug(`Sync run completed, set ${relationshipIds.length} new relationships to ready`)
+    }
   } catch (err) {
     debug(`Error in sync run ${err.stack}`)
   }

+ 6 - 1
storage-node/packages/colossus/paths/asset/v0/{id}.js

@@ -27,7 +27,7 @@ function errorHandler(response, err, code) {
   response.status(err.code || code || 500).send({ message: err.toString() })
 }
 
-module.exports = function (storage, runtime, ipfsHttpGatewayUrl) {
+module.exports = function (storage, runtime, ipfsHttpGatewayUrl, anonymous) {
   // Creat the IPFS HTTP Gateway proxy middleware
   const proxy = ipfsProxy.createProxy(storage, ipfsHttpGatewayUrl)
 
@@ -47,6 +47,11 @@ module.exports = function (storage, runtime, ipfsHttpGatewayUrl) {
 
     // Put for uploads
     async put(req, res) {
+      if (anonymous) {
+        errorHandler(res, 'Uploads Not Permitted in Anonymous Mode', 400)
+        return
+      }
+
       const id = req.params.id // content id
 
       // First check if we're the liaison for the name, otherwise we can bail

+ 8 - 0
storage-node/packages/runtime-api/identities.js

@@ -135,6 +135,14 @@ class IdentitiesApi {
     return this.base.api.query.members.memberIdsByRootAccountId(decoded)
   }
 
+  /*
+   * Return all the member IDs of an account by the controller account id
+   */
+  async memberIdsOfController(accountId) {
+    const decoded = this.keyring.decodeAddress(accountId)
+    return this.base.api.query.members.memberIdsByControllerAccountId(decoded)
+  }
+
   /*
    * Return the first member ID of an account, or undefined if not a member root account.
    */

+ 7 - 7
storage-node/packages/runtime-api/workers.js

@@ -150,8 +150,8 @@ class WorkersApi {
    * Add a new storage group opening using the lead account. Returns the
    * new opening id.
    */
-  async devAddStorageOpening() {
-    const openTx = this.devMakeAddOpeningTx('Worker')
+  async devAddStorageOpening(info) {
+    const openTx = this.devMakeAddOpeningTx('Worker', info)
     return this.devSubmitAddOpeningTx(openTx, await this.getLeadRoleAccount())
   }
 
@@ -159,8 +159,8 @@ class WorkersApi {
    * Add a new storage working group lead opening using sudo account. Returns the
    * new opening id.
    */
-  async devAddStorageLeadOpening() {
-    const openTx = this.devMakeAddOpeningTx('Leader')
+  async devAddStorageLeadOpening(info) {
+    const openTx = this.devMakeAddOpeningTx('Leader', info)
     const sudoTx = this.base.api.tx.sudo.sudo(openTx)
     return this.devSubmitAddOpeningTx(sudoTx, await this.base.identities.getSudoAccount())
   }
@@ -168,17 +168,17 @@ class WorkersApi {
   /*
    * Constructs an addOpening tx of openingType
    */
-  devMakeAddOpeningTx(openingType) {
+  devMakeAddOpeningTx(openingType, info) {
     return this.base.api.tx.storageWorkingGroup.addOpening(
       'CurrentBlock',
       {
         application_rationing_policy: {
           max_active_applicants: 1,
         },
-        max_review_period_length: 1000,
+        max_review_period_length: 10,
         // default values for everything else..
       },
-      'dev-opening',
+      info || 'dev-opening',
       openingType
     )
   }

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

@@ -4,8 +4,9 @@
   "license": "GPL-3.0-only",
   "scripts": {
     "build": "tsc --noEmit",
-    "run-tests": "./run-tests.sh",
-    "test-run": "node -r ts-node/register --unhandled-rejections=strict",
+    "test": "./run-tests.sh",
+    "run-test-scenario": "./run-test-scenario.sh",
+    "node-ts-strict": "node -r ts-node/register --unhandled-rejections=strict",
     "lint": "eslint . --quiet --ext .ts",
     "checks": "tsc --noEmit --pretty && prettier ./ --check && yarn lint",
     "format": "prettier ./ --write "

+ 12 - 16
tests/network-tests/run-storage-node-tests.sh

@@ -1,8 +1,12 @@
 #!/usr/bin/env bash
 set -e
 
-# SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
-# cd $SCRIPT_PATH
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+cd $SCRIPT_PATH
+
+set -a
+. ../../.env
+set +a
 
 function cleanup() {
     # Show tail end of logs for the processor and indexer containers to
@@ -14,15 +18,6 @@ function cleanup() {
 
 trap cleanup EXIT
 
-export WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944/
-
-# Only run codegen if no generated files found
-[ ! -d "query-node/generated/" ] && yarn workspace query-node-root build
-
-# Make sure typeorm is available.. it get removed again when yarn is run again
-# typeorm commandline is used by db:migrate step below.
-ln -s ../../../../../node_modules/typeorm/cli.js query-node/generated/graphql-server/node_modules/.bin/typeorm || :
-
 # clean start
 docker-compose down -v
 
@@ -32,6 +27,7 @@ docker-compose up -d joystream-node
 DEBUG=joystream:storage-cli:dev yarn storage-cli dev-init
 docker-compose up -d colossus
 
+# Query node is expected to have been already built
 docker-compose up -d db
 yarn workspace query-node-root db:migrate
 docker-compose up -d graphql-server
@@ -41,15 +37,15 @@ docker-compose up -d processor
 yarn workspace @joystream/cd-schemas initialize:dev
 
 # Fixes Error: No active storage providers available
-sleep 1m 
+sleep 3m
 
 echo "Creating channel..."
 yarn joystream-cli media:createChannel \
-  --input ./tests/network-tests/assets/TestChannel.json --confirm
+  --input ./assets/TestChannel.json --confirm
 
 echo "Uploading video..."
-yes | yarn joystream-cli media:uploadVideo ./tests/network-tests/assets/joystream.MOV \
-  --input ./tests/network-tests/assets/TestVideo.json \
+yes | yarn joystream-cli media:uploadVideo ./assets/joystream.MOV \
+  --input ./assets/TestVideo.json \
   --confirm 
 
-time DEBUG=* yarn workspace network-tests test-run src/scenarios/storage-node.ts
+time DEBUG=* yarn workspace network-tests run-test-scenario storage-node

+ 13 - 0
tests/network-tests/run-test-scenario.sh

@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+cd $SCRIPT_PATH
+
+# pass the scenario name without .ts extension
+SCENARIO=$1
+# fallback to full.ts scenario if not specified
+SCENARIO=${SCENARIO:=full}
+
+# Execute the tests
+time DEBUG=* yarn workspace network-tests node-ts-strict src/scenarios/${SCENARIO}.ts

+ 3 - 6
tests/network-tests/run-tests.sh

@@ -102,10 +102,7 @@ fi
 # Display runtime version
 yarn workspace api-scripts tsnode-strict src/status.ts | grep Runtime
 
-# pass the scenario name without .ts extension
-SCENARIO=$1
-# fallback to full.ts scenario if not specified
-SCENARIO=${SCENARIO:=full}
+echo "Waiting for chain to startup..."
+sleep 5s
 
-# Execute the tests
-time DEBUG=* yarn workspace network-tests test-run src/scenarios/${SCENARIO}.ts
+./run-test-scenario.sh $1

+ 127 - 302
tests/network-tests/src/Api.ts

@@ -13,7 +13,7 @@ import {
   Opening as WorkingGroupOpening,
 } from '@joystream/types/working-group'
 import { ElectionStake, Seat } from '@joystream/types/council'
-import { AccountInfo, Hash, Balance, BalanceOf, BlockNumber, Event, EventRecord } from '@polkadot/types/interfaces'
+import { AccountInfo, Balance, BalanceOf, BlockNumber, Event, EventRecord } from '@polkadot/types/interfaces'
 import BN from 'bn.js'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { Sender } from './sender'
@@ -32,7 +32,7 @@ import { FillOpeningParameters, ProposalId } from '@joystream/types/proposals'
 import { v4 as uuid } from 'uuid'
 import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
 import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
-import { initializeContentDir, InputParser, ExtrinsicsHelper } from '@joystream/cd-schemas'
+import { initializeContentDir, InputParser } from '@joystream/cd-schemas'
 import { OperationType } from '@joystream/types/content-directory'
 import { gql, ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client'
 import { ContentId, DataObject } from '@joystream/types/media'
@@ -86,7 +86,7 @@ export class Api {
     this.sender = new Sender(api, this.keyring)
   }
 
-  public close() {
+  public close(): void {
     this.api.disconnect()
   }
 
@@ -126,12 +126,7 @@ export class Api {
     expectFailure = false
   ): Promise<ISubmittableResult> {
     return this.sender.signAndSend(
-      (this.api.tx.members.buyMembership(
-        paidTermsId,
-        /* Handle: */ name,
-        /* Avatar uri: */ '',
-        /* About: */ ''
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx.members.buyMembership(paidTermsId, /* Handle: */ name, /* Avatar uri: */ '', /* About: */ ''),
       account,
       expectFailure
     )
@@ -167,24 +162,15 @@ export class Api {
     return terms.fee
   }
 
-  private getBaseTxFee(): BN {
-    return this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionBaseFee)
-  }
-
+  // This method does not take into account weights and the runtime weight to fees computation!
   private estimateTxFee(tx: SubmittableExtrinsic<'promise'>): BN {
-    const baseFee: BN = this.getBaseTxFee()
     const byteFee: BN = this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionByteFee)
-    return Utils.calcTxLength(tx).mul(byteFee).add(baseFee)
+    return Utils.calcTxLength(tx).mul(byteFee)
   }
 
   public estimateBuyMembershipFee(account: string, paidTermsId: PaidTermId, name: string): BN {
     return this.estimateTxFee(
-      (this.api.tx.members.buyMembership(
-        paidTermsId,
-        /* Handle: */ name,
-        /* Avatar uri: */ '',
-        /* About: */ ''
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      this.api.tx.members.buyMembership(paidTermsId, /* Handle: */ name, /* Avatar uri: */ '', /* About: */ '')
     )
   }
 
@@ -230,12 +216,6 @@ export class Api {
     )
   }
 
-  public estimateProposeLeadFee(title: string, description: string, stake: BN, address: string): BN {
-    return this.estimateTxFee(
-      this.api.tx.proposalsCodex.createSetLeadProposal(stake, title, description, stake, { stake, address })
-    )
-  }
-
   public estimateProposeElectionParametersFee(
     title: string,
     description: string,
@@ -250,26 +230,26 @@ export class Api {
     minVotingStake: BN
   ): BN {
     return this.estimateTxFee(
-      this.api.tx.proposalsCodex.createSetElectionParametersProposal(stake, title, description, stake, [
-        announcingPeriod,
-        votingPeriod,
-        revealingPeriod,
-        councilSize,
-        candidacyLimit,
-        newTermDuration,
-        minCouncilStake,
-        minVotingStake,
-      ])
+      this.api.tx.proposalsCodex.createSetElectionParametersProposal(stake, title, description, stake, {
+        announcing_period: announcingPeriod,
+        voting_period: votingPeriod,
+        revealing_period: revealingPeriod,
+        council_size: councilSize,
+        candidacy_limit: candidacyLimit,
+        new_term_duration: newTermDuration,
+        min_council_stake: minCouncilStake,
+        min_voting_stake: minVotingStake,
+      })
     )
   }
 
   public estimateVoteForProposalFee(): BN {
     return this.estimateTxFee(
-      (this.api.tx.proposalsEngine.vote(
+      this.api.tx.proposalsEngine.vote(
         this.api.createType('MemberId', 0),
         this.api.createType('ProposalId', 0),
         'Approve'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
@@ -313,12 +293,7 @@ export class Api {
     })
 
     return this.estimateTxFee(
-      (this.api.tx[module].addOpening(
-        'CurrentBlock',
-        commitment,
-        'Human readable text',
-        'Worker'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      this.api.tx[module].addOpening('CurrentBlock', commitment, 'Human readable text', 'Worker')
     )
   }
 
@@ -332,32 +307,28 @@ export class Api {
 
   public estimateApplyOnOpeningFee(account: string, module: WorkingGroups): BN {
     return this.estimateTxFee(
-      (this.api.tx[module].applyOnOpening(
+      this.api.tx[module].applyOnOpening(
         this.api.createType('MemberId', 0),
         this.api.createType('OpeningId', 0),
         account,
-        0,
-        0,
+        null,
+        null,
         'Some testing text used for estimation purposes which is longer than text expected during the test'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
   public estimateBeginApplicantReviewFee(module: WorkingGroups): BN {
-    return this.estimateTxFee(
-      (this.api.tx[module].beginApplicantReview(
-        this.api.createType('OpeningId', 0)
-      ) as unknown) as SubmittableExtrinsic<'promise'>
-    )
+    return this.estimateTxFee(this.api.tx[module].beginApplicantReview(0))
   }
 
   public estimateFillOpeningFee(module: WorkingGroups): BN {
     return this.estimateTxFee(
-      (this.api.tx[module].fillOpening(this.api.createType('OpeningId', 0), [this.api.createType('ApplicationId', 0)], {
+      this.api.tx[module].fillOpening(0, this.api.createType('ApplicationIdSet', [0]), {
         'amount_per_payout': 0,
         'next_payment_at_block': 0,
         'payout_interval': 0,
-      }) as unknown) as SubmittableExtrinsic<'promise'>
+      })
     )
   }
 
@@ -378,46 +349,25 @@ export class Api {
   }
 
   public estimateUpdateRoleAccountFee(address: string, module: WorkingGroups): BN {
-    return this.estimateTxFee(
-      (this.api.tx[module].updateRoleAccount(
-        this.api.createType('WorkerId', 0),
-        address
-      ) as unknown) as SubmittableExtrinsic<'promise'>
-    )
+    return this.estimateTxFee(this.api.tx[module].updateRoleAccount(this.api.createType('WorkerId', 0), address))
   }
 
   public estimateUpdateRewardAccountFee(address: string, module: WorkingGroups): BN {
-    return this.estimateTxFee(
-      (this.api.tx[module].updateRewardAccount(
-        this.api.createType('WorkerId', 0),
-        address
-      ) as unknown) as SubmittableExtrinsic<'promise'>
-    )
+    return this.estimateTxFee(this.api.tx[module].updateRewardAccount(this.api.createType('WorkerId', 0), address))
   }
 
   public estimateLeaveRoleFee(module: WorkingGroups): BN {
     return this.estimateTxFee(
-      (this.api.tx[module].leaveRole(
-        this.api.createType('WorkerId', 0),
-        'Long justification text'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      this.api.tx[module].leaveRole(this.api.createType('WorkerId', 0), 'Long justification text')
     )
   }
 
   public estimateWithdrawApplicationFee(module: WorkingGroups): BN {
-    return this.estimateTxFee(
-      (this.api.tx[module].withdrawApplication(
-        this.api.createType('ApplicationId', 0)
-      ) as unknown) as SubmittableExtrinsic<'promise'>
-    )
+    return this.estimateTxFee(this.api.tx[module].withdrawApplication(this.api.createType('ApplicationId', 0)))
   }
 
   public estimateTerminateApplicationFee(module: WorkingGroups): BN {
-    return this.estimateTxFee(
-      (this.api.tx[module].terminateApplication(
-        this.api.createType('ApplicationId', 0)
-      ) as unknown) as SubmittableExtrinsic<'promise'>
-    )
+    return this.estimateTxFee(this.api.tx[module].terminateApplication(this.api.createType('ApplicationId', 0)))
   }
 
   public estimateSlashStakeFee(module: WorkingGroups): BN {
@@ -430,11 +380,11 @@ export class Api {
 
   public estimateTerminateRoleFee(module: WorkingGroups): BN {
     return this.estimateTxFee(
-      (this.api.tx[module].terminateRole(
+      this.api.tx[module].terminateRole(
         this.api.createType('WorkerId', 0),
         'Long justification text explaining why the worker role will be terminated',
         false
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
@@ -478,31 +428,31 @@ export class Api {
     })
 
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createAddWorkingGroupLeaderOpeningProposal(
-        this.api.createType('MemberId', 0),
+      this.api.tx.proposalsCodex.createAddWorkingGroupLeaderOpeningProposal(
+        0,
         'some long title for the purpose of testing',
         'some long description for the purpose of testing',
-        0,
+        null,
         {
-          'activate_at': 'CurrentBlock',
-          'commitment': commitment,
-          'human_readable_text': 'Opening readable text',
-          'working_group': 'Storage',
+          activate_at: 'CurrentBlock',
+          commitment: commitment,
+          human_readable_text: 'Opening readable text',
+          working_group: 'Storage',
         }
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
   public estimateProposeBeginWorkingGroupLeaderApplicationReviewFee(): BN {
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createBeginReviewWorkingGroupLeaderApplicationsProposal(
+      this.api.tx.proposalsCodex.createBeginReviewWorkingGroupLeaderApplicationsProposal(
         this.api.createType('MemberId', 0),
         'Some testing text used for estimation purposes which is longer than text expected during the test',
         'Some testing text used for estimation purposes which is longer than text expected during the test',
-        0,
+        null,
         this.api.createType('OpeningId', 0),
         'Storage'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
@@ -519,85 +469,85 @@ export class Api {
     })
 
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createFillWorkingGroupLeaderOpeningProposal(
+      this.api.tx.proposalsCodex.createFillWorkingGroupLeaderOpeningProposal(
         this.api.createType('MemberId', 0),
         'Some testing text used for estimation purposes which is longer than text expected during the test',
         'Some testing text used for estimation purposes which is longer than text expected during the test',
-        0,
+        null,
         fillOpeningParameters
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
   public estimateProposeTerminateLeaderRoleFee(): BN {
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createTerminateWorkingGroupLeaderRoleProposal(
+      this.api.tx.proposalsCodex.createTerminateWorkingGroupLeaderRoleProposal(
         this.api.createType('MemberId', 0),
         'Some testing text used for estimation purposes which is longer than text expected during the test',
         'Some testing text used for estimation purposes which is longer than text expected during the test',
-        0,
+        null,
         {
           'worker_id': this.api.createType('WorkerId', 0),
           'rationale': 'Exceptionaly long and extraordinary descriptive rationale',
           'slash': true,
           'working_group': 'Storage',
         }
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
   public estimateProposeLeaderRewardFee(): BN {
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createSetWorkingGroupLeaderRewardProposal(
+      this.api.tx.proposalsCodex.createSetWorkingGroupLeaderRewardProposal(
         this.api.createType('MemberId', 0),
         'Some testing text used for estimation purposes which is longer than text expected during the test',
         'Some testing text used for estimation purposes which is longer than text expected during the test',
-        0,
+        null,
         this.api.createType('WorkerId', 0),
         0,
         'Storage'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
   public estimateProposeDecreaseLeaderStakeFee(): BN {
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createDecreaseWorkingGroupLeaderStakeProposal(
+      this.api.tx.proposalsCodex.createDecreaseWorkingGroupLeaderStakeProposal(
         this.api.createType('MemberId', 0),
         'Some testing text used for estimation purposes which is longer than text expected during the test',
         'Some testing text used for estimation purposes which is longer than text expected during the test',
-        0,
+        null,
         this.api.createType('WorkerId', 0),
         0,
         'Storage'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
   public estimateProposeSlashLeaderStakeFee(): BN {
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createSlashWorkingGroupLeaderStakeProposal(
+      this.api.tx.proposalsCodex.createSlashWorkingGroupLeaderStakeProposal(
         this.api.createType('MemberId', 0),
         'Some testing text used for estimation purposes which is longer than text expected during the test',
         'Some testing text used for estimation purposes which is longer than text expected during the test',
-        0,
+        null,
         this.api.createType('WorkerId', 0),
         0,
         'Storage'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
   public estimateProposeWorkingGroupMintCapacityFee(): BN {
     return this.estimateTxFee(
-      (this.api.tx.proposalsCodex.createSetWorkingGroupMintCapacityProposal(
+      this.api.tx.proposalsCodex.createSetWorkingGroupMintCapacityProposal(
         this.api.createType('MemberId', 0),
         'Some testing text used for estimation purposes which is longer than text expected during the test',
         'Some testing text used for estimation purposes which is longer than text expected during the test',
-        0,
+        null,
         0,
         'Storage'
-      ) as unknown) as SubmittableExtrinsic<'promise'>
+      )
     )
   }
 
@@ -674,9 +624,11 @@ export class Api {
     return council.map((seat) => seat.member.toString())
   }
 
-  public getRuntime(): Promise<Bytes> {
-    return this.api.query.substrate.code<Bytes>()
-  }
+  // This method is deprecated. Is there a replacement in newer versions of substrate?
+  // Do we even use this method?
+  // public getRuntime(): Promise<Bytes> {
+  //   return this.api.query.substrate.code<Bytes>()
+  // }
 
   public async proposeRuntime(
     account: string,
@@ -687,13 +639,7 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(
-        memberId,
-        name,
-        description,
-        stake,
-        runtime
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(memberId, name, description, stake, runtime),
       account,
       false
     )
@@ -708,13 +654,7 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createTextProposal(
-        memberId,
-        name,
-        description,
-        stake,
-        text
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx.proposalsCodex.createTextProposal(memberId, name, description, stake, text),
       account,
       false
     )
@@ -730,14 +670,7 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createSpendingProposal(
-        memberId,
-        title,
-        description,
-        stake,
-        balance,
-        destination
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx.proposalsCodex.createSpendingProposal(memberId, title, description, stake, balance, destination),
       account,
       false
     )
@@ -752,32 +685,7 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createSetValidatorCountProposal(
-        memberId,
-        title,
-        description,
-        stake,
-        validatorCount
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
-      account,
-      false
-    )
-  }
-
-  public async proposeLead(
-    account: string,
-    title: string,
-    description: string,
-    stake: BN,
-    leadAccount: string
-  ): Promise<ISubmittableResult> {
-    const memberId: MemberId = (await this.getMemberIds(account))[0]
-    const leadMemberId: MemberId = (await this.getMemberIds(leadAccount))[0]
-    return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createSetLeadProposal(memberId, title, description, stake, [
-        leadMemberId,
-        leadAccount,
-      ]) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx.proposalsCodex.createSetValidatorCountProposal(memberId, title, description, stake, validatorCount),
       account,
       false
     )
@@ -799,16 +707,16 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createSetElectionParametersProposal(memberId, title, description, stake, [
-        announcingPeriod,
-        votingPeriod,
-        revealingPeriod,
-        councilSize,
-        candidacyLimit,
-        newTermDuration,
-        minCouncilStake,
-        minVotingStake,
-      ]) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx.proposalsCodex.createSetElectionParametersProposal(memberId, title, description, stake, {
+        announcing_period: announcingPeriod,
+        voting_period: votingPeriod,
+        revealing_period: revealingPeriod,
+        council_size: councilSize,
+        candidacy_limit: candidacyLimit,
+        new_term_duration: newTermDuration,
+        min_council_stake: minCouncilStake,
+        min_voting_stake: minVotingStake,
+      }),
       account,
       false
     )
@@ -824,25 +732,21 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createBeginReviewWorkingGroupLeaderApplicationsProposal(
+      this.api.tx.proposalsCodex.createBeginReviewWorkingGroupLeaderApplicationsProposal(
         memberId,
         title,
         description,
         stake,
         openingId,
-        workingGroup
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+        this.api.createType('WorkingGroup', workingGroup)
+      ),
       account,
       false
     )
   }
 
   public approveProposal(account: string, memberId: MemberId, proposal: ProposalId): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx.proposalsEngine.vote(memberId, proposal, 'Approve') as unknown) as SubmittableExtrinsic<'promise'>,
-      account,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx.proposalsEngine.vote(memberId, proposal, 'Approve'), account, false)
   }
 
   public async batchApproveProposal(proposal: ProposalId): Promise<void[]> {
@@ -859,7 +763,7 @@ export class Api {
     return this.api.createType('Moment', this.api.consts.babe.expectedBlockTime)
   }
 
-  public durationInMsFromBlocks(durationInBlocks: number) {
+  public durationInMsFromBlocks(durationInBlocks: number): number {
     return this.getBlockDuration().muln(durationInBlocks).toNumber()
   }
 
@@ -1290,7 +1194,7 @@ export class Api {
 
     const memberId: MemberId = (await this.getMemberIds(leaderOpening.account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createAddWorkingGroupLeaderOpeningProposal(
+      this.api.tx.proposalsCodex.createAddWorkingGroupLeaderOpeningProposal(
         memberId,
         leaderOpening.title,
         leaderOpening.description,
@@ -1301,7 +1205,7 @@ export class Api {
           human_readable_text: leaderOpening.text,
           working_group: leaderOpening.workingGroup,
         }
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      ),
       leaderOpening.account,
       false
     )
@@ -1333,13 +1237,13 @@ export class Api {
     })
 
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createFillWorkingGroupLeaderOpeningProposal(
+      this.api.tx.proposalsCodex.createFillWorkingGroupLeaderOpeningProposal(
         memberId,
         fillOpening.title,
         fillOpening.description,
         fillOpening.proposalStake,
         fillOpeningParameters
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      ),
       fillOpening.account,
       false
     )
@@ -1357,7 +1261,7 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createTerminateWorkingGroupLeaderRoleProposal(
+      this.api.tx.proposalsCodex.createTerminateWorkingGroupLeaderRoleProposal(
         memberId,
         title,
         description,
@@ -1368,7 +1272,7 @@ export class Api {
           slash,
           'working_group': workingGroup,
         }
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      ),
       account,
       false
     )
@@ -1385,15 +1289,15 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createSetWorkingGroupLeaderRewardProposal(
+      this.api.tx.proposalsCodex.createSetWorkingGroupLeaderRewardProposal(
         memberId,
         title,
         description,
         proposalStake,
         workerId,
         rewardAmount,
-        workingGroup
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+        this.api.createType('WorkingGroup', workingGroup)
+      ),
       account,
       false
     )
@@ -1410,15 +1314,15 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createDecreaseWorkingGroupLeaderStakeProposal(
+      this.api.tx.proposalsCodex.createDecreaseWorkingGroupLeaderStakeProposal(
         memberId,
         title,
         description,
         proposalStake,
         workerId,
         rewardAmount,
-        workingGroup
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+        this.api.createType('WorkingGroup', workingGroup)
+      ),
       account,
       false
     )
@@ -1435,15 +1339,15 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createSlashWorkingGroupLeaderStakeProposal(
+      this.api.tx.proposalsCodex.createSlashWorkingGroupLeaderStakeProposal(
         memberId,
         title,
         description,
         proposalStake,
         workerId,
         rewardAmount,
-        workingGroup
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+        this.api.createType('WorkingGroup', workingGroup)
+      ),
       account,
       false
     )
@@ -1459,14 +1363,14 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx.proposalsCodex.createSetWorkingGroupMintCapacityProposal(
+      this.api.tx.proposalsCodex.createSetWorkingGroupMintCapacityProposal(
         memberId,
         title,
         description,
         proposalStake,
         mintCapacity,
-        workingGroup
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+        this.api.createType('WorkingGroup', workingGroup)
+      ),
       account,
       false
     )
@@ -1479,9 +1383,7 @@ export class Api {
     type: string,
     module: WorkingGroups
   ): SubmittableExtrinsic<'promise'> {
-    return (this.api.tx[module].addOpening(actiavteAt, commitment, text, type) as unknown) as SubmittableExtrinsic<
-      'promise'
-    >
+    return this.api.tx[module].addOpening(actiavteAt, commitment, text, this.api.createType('OpeningType', type))
   }
 
   public async acceptApplications(
@@ -1489,11 +1391,7 @@ export class Api {
     openingId: OpeningId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].acceptApplications(openingId) as unknown) as SubmittableExtrinsic<'promise'>,
-      leader,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx[module].acceptApplications(openingId), leader, false)
   }
 
   public async beginApplicantReview(
@@ -1501,18 +1399,11 @@ export class Api {
     openingId: OpeningId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].beginApplicantReview(openingId) as unknown) as SubmittableExtrinsic<'promise'>,
-      leader,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx[module].beginApplicantReview(openingId), leader, false)
   }
 
   public async sudoBeginApplicantReview(openingId: OpeningId, module: WorkingGroups): Promise<ISubmittableResult> {
-    return this.makeSudoCall(
-      (this.api.tx[module].beginApplicantReview(openingId) as unknown) as SubmittableExtrinsic<'promise'>,
-      false
-    )
+    return this.makeSudoCall(this.api.tx[module].beginApplicantReview(openingId), false)
   }
 
   public async applyOnOpening(
@@ -1527,14 +1418,7 @@ export class Api {
   ): Promise<ISubmittableResult> {
     const memberId: MemberId = (await this.getMemberIds(account))[0]
     return this.sender.signAndSend(
-      (this.api.tx[module].applyOnOpening(
-        memberId,
-        openingId,
-        roleAccountAddress,
-        roleStake,
-        applicantStake,
-        text
-      ) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx[module].applyOnOpening(memberId, openingId, roleAccountAddress, roleStake, applicantStake, text),
       account,
       expectFailure
     )
@@ -1559,18 +1443,18 @@ export class Api {
   public async fillOpening(
     leader: string,
     openingId: OpeningId,
-    applicationId: ApplicationId[],
+    applicationIds: ApplicationId[],
     amountPerPayout: BN,
     nextPaymentBlock: BN,
     payoutInterval: BN,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
     return this.sender.signAndSend(
-      (this.api.tx[module].fillOpening(openingId, applicationId, {
-        'amount_per_payout': amountPerPayout,
-        'next_payment_at_block': nextPaymentBlock,
-        'payout_interval': payoutInterval,
-      }) as unknown) as SubmittableExtrinsic<'promise'>,
+      this.api.tx[module].fillOpening(openingId, this.api.createType('ApplicationIdSet', applicationIds), {
+        amount_per_payout: amountPerPayout,
+        next_payment_at_block: nextPaymentBlock,
+        payout_interval: payoutInterval,
+      }),
       leader,
       false
     )
@@ -1578,14 +1462,14 @@ export class Api {
 
   public async sudoFillOpening(
     openingId: OpeningId,
-    applicationId: ApplicationId[],
+    applicationIds: ApplicationId[],
     amountPerPayout: BN,
     nextPaymentBlock: BN,
     payoutInterval: BN,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
     return this.makeSudoCall(
-      this.api.tx[module].fillOpening(openingId, applicationId, {
+      this.api.tx[module].fillOpening(openingId, this.api.createType('ApplicationIdSet', applicationIds), {
         'amount_per_payout': amountPerPayout,
         'next_payment_at_block': nextPaymentBlock,
         'payout_interval': payoutInterval,
@@ -1600,11 +1484,7 @@ export class Api {
     stake: BN,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].increaseStake(workerId, stake) as unknown) as SubmittableExtrinsic<'promise'>,
-      worker,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx[module].increaseStake(workerId, stake), worker, false)
   }
 
   public async decreaseStake(
@@ -1614,11 +1494,7 @@ export class Api {
     module: WorkingGroups,
     expectFailure: boolean
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].decreaseStake(workerId, stake) as unknown) as SubmittableExtrinsic<'promise'>,
-      leader,
-      expectFailure
-    )
+    return this.sender.signAndSend(this.api.tx[module].decreaseStake(workerId, stake), leader, expectFailure)
   }
 
   public async slashStake(
@@ -1628,11 +1504,7 @@ export class Api {
     module: WorkingGroups,
     expectFailure: boolean
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].slashStake(workerId, stake) as unknown) as SubmittableExtrinsic<'promise'>,
-      leader,
-      expectFailure
-    )
+    return this.sender.signAndSend(this.api.tx[module].slashStake(workerId, stake), leader, expectFailure)
   }
 
   public async updateRoleAccount(
@@ -1641,11 +1513,7 @@ export class Api {
     newRoleAccount: string,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].updateRoleAccount(workerId, newRoleAccount) as unknown) as SubmittableExtrinsic<'promise'>,
-      worker,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx[module].updateRoleAccount(workerId, newRoleAccount), worker, false)
   }
 
   public async updateRewardAccount(
@@ -1654,13 +1522,7 @@ export class Api {
     newRewardAccount: string,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].updateRewardAccount(workerId, newRewardAccount) as unknown) as SubmittableExtrinsic<
-        'promise'
-      >,
-      worker,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx[module].updateRewardAccount(workerId, newRewardAccount), worker, false)
   }
 
   public async withdrawApplication(
@@ -1668,11 +1530,7 @@ export class Api {
     applicationId: ApplicationId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].withdrawApplication(applicationId) as unknown) as SubmittableExtrinsic<'promise'>,
-      account,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx[module].withdrawApplication(applicationId), account, false)
   }
 
   public async batchWithdrawActiveApplications(
@@ -1694,37 +1552,12 @@ export class Api {
     )
   }
 
-  /*
-    public async getApplicantRoleAccounts(filterActiveIds: ApplicationId[], module: WorkingGroups): Promise<string[]> {
-    const entries: [StorageKey, Application][] = await this.api.query[module].applicationById.entries<Application>()
-
-    const applications = entries
-      .filter(([idKey]) => {
-        return filterActiveIds.includes(idKey.args[0] as ApplicationId)
-      })
-      .map(([, application]) => application)
-
-    return (
-      await Promise.all(
-        applications.map(async (application) => {
-          const active = (await this.getHiringApplicationById(application.application_id)).stage.type === 'Active'
-          return active ? application.role_account_id.toString() : ''
-        })
-      )
-    ).filter((addr) => addr !== '')
-  }
-  */
-
   public async terminateApplication(
     leader: string,
     applicationId: ApplicationId,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].terminateApplication(applicationId) as unknown) as SubmittableExtrinsic<'promise'>,
-      leader,
-      false
-    )
+    return this.sender.signAndSend(this.api.tx[module].terminateApplication(applicationId), leader, false)
   }
 
   public async batchTerminateApplication(
@@ -1742,11 +1575,7 @@ export class Api {
     module: WorkingGroups,
     expectFailure: boolean
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].terminateRole(workerId, text, false) as unknown) as SubmittableExtrinsic<'promise'>,
-      leader,
-      expectFailure
-    )
+    return this.sender.signAndSend(this.api.tx[module].terminateRole(workerId, text, false), leader, expectFailure)
   }
 
   public async leaveRole(
@@ -1756,11 +1585,7 @@ export class Api {
     expectFailure: boolean,
     module: WorkingGroups
   ): Promise<ISubmittableResult> {
-    return this.sender.signAndSend(
-      (this.api.tx[module].leaveRole(workerId, text) as unknown) as SubmittableExtrinsic<'promise'>,
-      account,
-      expectFailure
-    )
+    return this.sender.signAndSend(this.api.tx[module].leaveRole(workerId, text), account, expectFailure)
   }
 
   public async batchLeaveRole(

+ 2 - 2
tests/network-tests/src/flows/storageNode/getContentFromStorageNode.ts

@@ -9,8 +9,8 @@ import { Utils } from '../../utils'
 export default async function getContentFromStorageNode(api: QueryNodeApi): Promise<void> {
   const videoTitle = 'Storage node test'
 
-  // Temporary solution (wait 1 minute)
-  await Utils.wait(60000)
+  // Temporary solution (wait 2 minutes)
+  await Utils.wait(120000)
 
   // Query video by title with where expression
   const videoWhereQueryResult = await api.performWhereQueryByVideoTitle(videoTitle)

+ 1 - 1
tests/network-tests/tsconfig.json

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

+ 4 - 4
yarn.lock

@@ -1416,10 +1416,10 @@
     typeorm-model-generator "^0.4.2"
     warthog "https://github.com/metmirr/warthog/releases/download/v2.22.0/warthog-v2.22.0.tgz"
 
-"@dzlzv/hydra-indexer-lib@0.0.21-legacy.1.26.1", "@dzlzv/hydra-indexer-lib@^0.0.21-legacy.1.26.1":
-  version "0.0.21-legacy.1.26.1"
-  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-indexer-lib/-/hydra-indexer-lib-0.0.21-legacy.1.26.1.tgz#c6a3ff3905cce03e139852900e074abb67c19efd"
-  integrity sha512-yUDqhl+ZlSo+t3qtIqqsYwh/vXUGwgsBOOToV+Bdry1Ngzw1+zQ0jh4fVItPMZMAAjGiw8AKoVbDu9kgAR1nEA==
+"@dzlzv/hydra-indexer-lib@0.0.22-legacy.1.26.1", "@dzlzv/hydra-indexer-lib@^0.0.22-legacy.1.26.1":
+  version "0.0.22-legacy.1.26.1"
+  resolved "https://registry.yarnpkg.com/@dzlzv/hydra-indexer-lib/-/hydra-indexer-lib-0.0.22-legacy.1.26.1.tgz#4e63994c27b77f39bd9c83e607b5c2adcfe22e78"
+  integrity sha512-NOOJviG/5HTmWpEPYcfE/wvSHoxBbXjTCvB/zHKpNq0xkeedl7x3xSi0gyZDUZsNpxEYQIaUHW9xti3X8MrU7A==
   dependencies:
     "@polkadot/api" "^1.26.1"
     "@types/express" "^4.17.8"