Prechádzať zdrojové kódy

Asset uploads + refactorization

Leszek Wiesner 3 rokov pred
rodič
commit
88fa783fb8

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

@@ -1,5 +1,3 @@
 {
-  "meta": {
-    "name": "Nature"
-  }
+  "name": "Nature"
 }

+ 8 - 14
cli/examples/content/CreateChannel.json

@@ -1,16 +1,10 @@
 {
-  "assets": [
-    {
-      "Urls": ["https://joystream.org/WorkingGroupOpening.schema.json"]
-    }
-  ],
-  "meta": {
-    "title": "Title",
-    "description": "Description",
-    "isPublic": true,
-    "language": "eng",
-    "coverPhoto": 1,
-    "avatarPhoto": 1,
-    "category": 1
-  }
+  "title": "Example Joystream Channel",
+  "description": "This is an awesome example channel!",
+  "isPublic": true,
+  "language": "en",
+  "category": 1,
+  "avatarPhotoPath": "./avatar-photo-1.png",
+  "coverPhotoPath": "./cover-photo-1.png",
+  "rewardAccount": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
 }

+ 20 - 0
cli/examples/content/CreateVideo.json

@@ -0,0 +1,20 @@
+{
+  "title": "Example Joystream Video",
+  "description": "This is an awesome example video!",
+  "videoPath": "./video.mp4",
+  "thumbnailPhotoPath": "./avatar-photo-1.png",
+  "language": "en",
+  "hasMarketing": false,
+  "isPublic": true,
+  "isExplicit": false,
+  "personsList": [],
+  "category": 1,
+  "license": {
+    "code": 1001,
+    "attribution": "by Joystream Contributors"
+  },
+  "publishedBeforeJoystream": {
+    "isPublished": true,
+    "date": "2020-01-01"
+  }
+}

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

@@ -1,5 +1,3 @@
 {
-  "meta": {
-    "name": "Science"
-  }
+  "name": "Science"
 }

+ 3 - 14
cli/examples/content/UpdateChannel.json

@@ -1,16 +1,5 @@
 {
-  "assets": [
-    {
-      "Urls": ["https://joystream.org/WorkingGroupOpening.schema.json"]
-    }
-  ],
-  "meta": {
-    "title": "Channel Title",
-    "description": "Cool Description",
-    "isPublic": true,
-    "language": "eng",
-    "coverPhoto": 1,
-    "avatarPhoto": 1,
-    "category": 1
-  }
+  "title": "Example Joystream Channel [UPDATED!]",
+  "avatarPhotoPath": "./avatar-photo-2.png",
+  "rewardAccount": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
 }

+ 7 - 0
cli/examples/content/UpdateVideo.json

@@ -0,0 +1,7 @@
+{
+  "title": "Example Joystream Video [UPDATED!]",
+  "thumbnailPhotoPath": "./avatar-photo-2.png",
+  "publishedBeforeJoystream": {
+    "isPublished": false
+  }
+}

BIN
cli/examples/content/avatar-photo-1.png


BIN
cli/examples/content/avatar-photo-2.png


BIN
cli/examples/content/cover-photo-1.png


BIN
cli/examples/content/cover-photo-2.png


+ 0 - 36
cli/examples/content/createVideo.json

@@ -1,36 +0,0 @@
-{
-  "assets": [
-    {
-      "Urls": ["https://joystream.org/WorkingGroupOpening.schema.json"]
-    }
-  ],
-  "meta": {
-    "title": "Title",
-    "description": "Description",
-    "video": 1,
-    "thumbnailPhoto": 1,
-    "duration": 10,
-    "mediaPixelHeight": 20,
-    "mediaPixelWidth": 50,
-    "language": "en",
-    "hasMarketing": true,
-    "isPublic": true,
-    "isExplicit": true,
-    "personsList": [1, 2, 5],
-    "category": 2,
-    "mediaType": {
-      "codecName": "mpeg4",
-      "container": "avi",
-      "mimeMediaType": "videp/mp4"
-    },
-    "license": {
-      "code": 1001,
-      "attribution": "first",
-      "customText": "text"
-    },
-    "publishedBeforeJoystream": {
-      "isPublished": true,
-      "date": "2012-09-27"
-    }
-  }
-}

+ 0 - 36
cli/examples/content/updateVideo.json

@@ -1,36 +0,0 @@
-{
-  "assets": [
-    {
-      "Urls": ["https://joystream.org/WorkingGroupOpening.schema.json"]
-    }
-  ],
-  "meta": {
-    "title": "Title",
-    "description": "Description",
-    "video": 1,
-    "thumbnailPhoto": "1",
-    "duration": 10,
-    "mediaPixelHeight": 20,
-    "mediaPixelWidth": 50,
-    "language": "en",
-    "hasMarketing": true,
-    "isPublic": true,
-    "isExplicit": true,
-    "personsList": [1, 2, 5],
-    "category": 2,
-    "mediaType": {
-      "codecName": "mpeg4",
-      "container": "avi",
-      "mimeMediaType": "videp/mp4"
-    },
-    "license": {
-      "code": 1001,
-      "attribution": "first",
-      "customText": "text"
-    },
-    "publishedBeforeJoystream": {
-      "isPublished": true,
-      "date": "2012-09-27"
-    }
-  }
-}

BIN
cli/examples/content/video.mp4


+ 6 - 4
cli/package.json

@@ -18,11 +18,14 @@
     "@oclif/plugin-not-found": "^1.2.4",
     "@oclif/plugin-warn-if-update-available": "^1.7.0",
     "@polkadot/api": "1.26.1",
+    "cli-progress": "^3.9.0",
+    "@types/cli-progress": "^3.9.1",
     "@types/fluent-ffmpeg": "^2.1.16",
     "@types/inquirer": "^6.5.0",
     "@types/proper-lockfile": "^4.1.1",
     "@types/slug": "^0.9.1",
     "ajv": "^6.11.0",
+    "axios": "^0.21.1",
     "cli-ux": "^5.4.5",
     "fluent-ffmpeg": "^2.1.2",
     "inquirer": "^7.1.0",
@@ -37,8 +40,7 @@
     "moment": "^2.24.0",
     "proper-lockfile": "^4.1.1",
     "slug": "^2.1.1",
-    "tslib": "^1.11.1",
-    "axios": "^0.21.1"
+    "tslib": "^1.11.1"
   },
   "devDependencies": {
     "@oclif/dev-cli": "^1.22.2",
@@ -52,11 +54,11 @@
     "eslint-config-oclif": "^3.1.0",
     "eslint-config-oclif-typescript": "^0.1.0",
     "globby": "^10.0.2",
+    "json-schema-to-typescript": "^9.1.1",
     "mocha": "^5.2.0",
     "nyc": "^14.1.1",
     "ts-node": "^8.8.2",
-    "typescript": "^3.8.3",
-    "json-schema-to-typescript": "^9.1.1"
+    "typescript": "^3.8.3"
   },
   "engines": {
     "node": ">=12.18.0",

+ 21 - 8
cli/src/Api.ts

@@ -6,7 +6,7 @@ import { formatBalance } from '@polkadot/util'
 import { Balance, Moment, BlockNumber } from '@polkadot/types/interfaces'
 import { KeyringPair } from '@polkadot/keyring/types'
 import { Codec, CodecArg } from '@polkadot/types/types'
-import { Option, Vec, UInt } from '@polkadot/types'
+import { Option, Vec, UInt, Bytes } from '@polkadot/types'
 import {
   AccountSummary,
   CouncilInfoObj,
@@ -32,6 +32,7 @@ import {
   RoleStakeProfile,
   Opening as WGOpening,
   Application as WGApplication,
+  StorageProviderId,
 } from '@joystream/types/working-group'
 import {
   Opening,
@@ -524,10 +525,12 @@ export default class Api {
   }
 
   async channelById(channelId: ChannelId | number | string): Promise<Channel> {
-    const channel = await this._api.query.content.channelById<Channel>(channelId)
-    if (channel.isEmpty) {
+    // isEmpty will not work for { MemmberId: 0 } ownership
+    const exists = !!(await this._api.query.content.channelById.size(channelId)).toNumber()
+    if (!exists) {
       throw new CLIError(`Channel by id ${channelId.toString()} not found!`)
     }
+    const channel = await this._api.query.content.channelById<Channel>(channelId)
 
     return channel
   }
@@ -570,9 +573,13 @@ export default class Api {
     )
   }
 
-  async dataByContentId(contentId: ContentId): Promise<DataObject | null> {
-    const dataObject = await this._api.query.dataDirectory.dataByContentId<DataObject>(contentId)
-    return dataObject.isEmpty ? null : dataObject
+  async dataObjectsByContentIds(contentIds: ContentId[]): Promise<DataObject[]> {
+    const dataObjects = await this._api.query.dataDirectory.dataByContentId.multi<DataObject>(contentIds)
+    const notFoundIndex = dataObjects.findIndex((o) => o.isEmpty)
+    if (notFoundIndex !== -1) {
+      throw new CLIError(`DataObject not found by id ${contentIds[notFoundIndex].toString()}`)
+    }
+    return dataObjects
   }
 
   async getRandomBootstrapEndpoint(): Promise<string | null> {
@@ -581,8 +588,14 @@ export default class Api {
     return randomEndpoint ? randomEndpoint.toString() : null
   }
 
-  async storageProviderEndpoint(storageProviderId: number): Promise<string> {
-    const value = await this._api.query.storageWorkingGroup.workerStorage(storageProviderId)
+  async storageProviderEndpoint(storageProviderId: StorageProviderId | number): Promise<string> {
+    const value = await this._api.query.storageWorkingGroup.workerStorage<Bytes>(storageProviderId)
     return this._api.createType('Text', value).toString()
   }
+
+  async allStorageProviderEndpoints(): Promise<string[]> {
+    const workerIds = (await this.groupWorkers(WorkingGroups.StorageProviders)).map(([id]) => id)
+    const workerStorages = await this._api.query.storageWorkingGroup.workerStorage.multi<Bytes>(workerIds)
+    return workerStorages.map((storage) => this._api.createType('Text', storage).toString())
+  }
 }

+ 27 - 65
cli/src/Types.ts

@@ -1,6 +1,6 @@
 import BN from 'bn.js'
 import { ElectionStage, Seat } from '@joystream/types/council'
-import { Vec, Option } from '@polkadot/types'
+import { Option } from '@polkadot/types'
 import { Codec } from '@polkadot/types/types'
 import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
@@ -9,14 +9,13 @@ import { WorkerId, OpeningType } from '@joystream/types/working-group'
 import { Membership, MemberId } from '@joystream/types/members'
 import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring'
 import { Validator } from 'inquirer'
-import { NewAsset } from '@joystream/types/content'
-import { Bytes } from '@polkadot/types/primitive'
 import {
   VideoMetadata,
   ChannelMetadata,
   ChannelCategoryMetadata,
   VideoCategoryMetadata,
 } from '@joystream/content-metadata-protobuf'
+import { ContentParameters } from '@joystream/types/storage'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -206,78 +205,41 @@ export type ApiMethodNamedArg = {
 }
 export type ApiMethodNamedArgs = ApiMethodNamedArg[]
 
-export type VideoUpdateParametersInput = {
-  assets: Option<Vec<NewAsset>>
-  meta: VideoMetadata.AsObject
+// Content-related
+export enum AssetType {
+  AnyAsset = 1,
 }
 
-export type VideoUpdateParameters = {
-  assets: Option<Vec<NewAsset>>
-  meta: Bytes
+export type InputAssetDetails = {
+  parameters: ContentParameters
+  path: string
 }
 
-export type VideoCreationParametersInput = {
-  assets: Vec<NewAsset>
-  meta: VideoMetadata.AsObject
+export type VideoFFProbeMetadata = {
+  width?: number
+  height?: number
+  codecName?: string
+  codecFullName?: string
+  duration?: number
 }
 
-export type VideoCreationParameters = {
-  assets: Vec<NewAsset>
-  meta: Bytes
+export type VideoFileMetadata = VideoFFProbeMetadata & {
+  size: number
+  container: string
+  mimeType: string
 }
 
-export type ChannelCreationParametersInput = {
-  assets: Vec<NewAsset>
-  meta: ChannelMetadata.AsObject
-  reward_account: Option<AccountId>
+export type VideoInputParameters = Omit<VideoMetadata.AsObject, 'video' | 'thumbnailPhoto'> & {
+  videoPath?: string
+  thumbnailPhotoPath?: string
 }
 
-export type ChannelCreationParameters = {
-  assets: Vec<NewAsset>
-  meta: Bytes
-  reward_account: Option<AccountId>
+export type ChannelInputParameters = Omit<ChannelMetadata.AsObject, 'coverPhoto' | 'avatarPhoto'> & {
+  coverPhotoPath?: string
+  avatarPhotoPath?: string
+  rewardAccount?: string
 }
 
-export type ChannelUpdateParametersInput = {
-  assets: Option<Vec<NewAsset>>
-  meta: ChannelMetadata.AsObject
-  reward_account: Option<AccountId>
-}
-
-export type ChannelUpdateParameters = {
-  assets: Option<Vec<NewAsset>>
-  new_meta: Bytes
-  reward_account: Option<AccountId>
-}
-
-export type ChannelCategoryCreationParametersInput = {
-  meta: ChannelCategoryMetadata.AsObject
-}
-
-export type ChannelCategoryCreationParameters = {
-  meta: Bytes
-}
-
-export type ChannelCategoryUpdateParametersInput = {
-  meta: ChannelCategoryMetadata.AsObject
-}
+export type ChannelCategoryInputParameters = ChannelCategoryMetadata.AsObject
 
-export type ChannelCategoryUpdateParameters = {
-  new_meta: Bytes
-}
-
-export type VideoCategoryCreationParametersInput = {
-  meta: VideoCategoryMetadata.AsObject
-}
-
-export type VideoCategoryCreationParameters = {
-  meta: Bytes
-}
-
-export type VideoCategoryUpdateParametersInput = {
-  meta: VideoCategoryMetadata.AsObject
-}
-
-export type VideoCategoryUpdateParameters = {
-  new_meta: Bytes
-}
+export type VideoCategoryInputParameters = VideoCategoryMetadata.AsObject

+ 212 - 0
cli/src/base/UploadCommandBase.ts

@@ -0,0 +1,212 @@
+import ContentDirectoryCommandBase from './ContentDirectoryCommandBase'
+import { VideoFFProbeMetadata, VideoFileMetadata, AssetType, InputAssetDetails } from '../Types'
+import { ContentId, ContentParameters } from '@joystream/types/storage'
+import { MultiBar, Options, SingleBar } from 'cli-progress'
+import ExitCodes from '../ExitCodes'
+import ipfsHash from 'ipfs-only-hash'
+import fs from 'fs'
+import _ from 'lodash'
+import axios, { AxiosRequestConfig } from 'axios'
+import ffprobeInstaller from '@ffprobe-installer/ffprobe'
+import ffmpeg from 'fluent-ffmpeg'
+import path from 'path'
+
+ffmpeg.setFfprobePath(ffprobeInstaller.path)
+
+/**
+ * Abstract base class for commands that require uploading functionality
+ */
+export default abstract class UploadCommandBase extends ContentDirectoryCommandBase {
+  private fileSizeCache: Map<string, number> = new Map<string, number>()
+  private progressBarOptions: Options = {
+    format: `{barTitle} | {bar} | {value}/{total} KB processed`,
+  }
+
+  getFileSize(path: string): number {
+    const cachedSize = this.fileSizeCache.get(path)
+    return cachedSize !== undefined ? cachedSize : fs.statSync(path).size
+  }
+
+  createReadStreamWithProgressBar(
+    filePath: string,
+    barTitle: string,
+    multiBar?: MultiBar
+  ): {
+    fileStream: fs.ReadStream
+    progressBar: SingleBar
+  } {
+    // Progress CLI UX:
+    // https://github.com/oclif/cli-ux#cliprogress
+    // https://www.npmjs.com/package/cli-progress
+    const fileSize = this.getFileSize(filePath)
+    let processedKB = 0
+    const fileSizeKB = Math.ceil(fileSize / 1024)
+    const progress = multiBar
+      ? multiBar.create(fileSizeKB, processedKB, { barTitle })
+      : new SingleBar(this.progressBarOptions)
+
+    progress.start(fileSizeKB, processedKB, { barTitle })
+    return {
+      fileStream: fs
+        .createReadStream(filePath)
+        .pause() // Explicitly pause to prevent switching to flowing mode (https://nodejs.org/api/stream.html#stream_event_data)
+        .on('error', () => {
+          progress.stop()
+          this.error(`Error while trying to read data from: ${filePath}!`, {
+            exit: ExitCodes.FsOperationFailed,
+          })
+        })
+        .on('data', (data) => {
+          processedKB += data.length / 1024
+          progress.update(processedKB)
+        })
+        .on('end', () => {
+          progress.update(fileSizeKB)
+          progress.stop()
+        }),
+      progressBar: progress,
+    }
+  }
+
+  async getVideoFFProbeMetadata(filePath: string): Promise<VideoFFProbeMetadata> {
+    return new Promise<VideoFFProbeMetadata>((resolve, reject) => {
+      ffmpeg.ffprobe(filePath, (err, data) => {
+        if (err) {
+          reject(err)
+          return
+        }
+        const videoStream = data.streams.find((s) => s.codec_type === 'video')
+        if (videoStream) {
+          resolve({
+            width: videoStream.width,
+            height: videoStream.height,
+            codecName: videoStream.codec_name,
+            codecFullName: videoStream.codec_long_name,
+            duration: videoStream.duration !== undefined ? Math.ceil(Number(videoStream.duration)) || 0 : undefined,
+          })
+        } else {
+          reject(new Error('No video stream found in file'))
+        }
+      })
+    })
+  }
+
+  async getVideoFileMetadata(filePath: string): Promise<VideoFileMetadata> {
+    let ffProbeMetadata: VideoFFProbeMetadata = {}
+    try {
+      ffProbeMetadata = await this.getVideoFFProbeMetadata(filePath)
+    } catch (e) {
+      const message = e.message || e
+      this.warn(`Failed to get video metadata via ffprobe (${message})`)
+    }
+
+    const size = this.getFileSize(filePath)
+    const container = path.extname(filePath).slice(1)
+    const mimeType = `video/${container}` // TODO: Is this enough?
+    return {
+      size,
+      container,
+      mimeType,
+      ...ffProbeMetadata,
+    }
+  }
+
+  async calculateFileIpfsHash(filePath: string): Promise<string> {
+    const { fileStream } = this.createReadStreamWithProgressBar(filePath, 'Calculating file hash')
+    const hash: string = await ipfsHash.of(fileStream)
+
+    return hash
+  }
+
+  validateFile(filePath: string): void {
+    // Basic file validation
+    if (!fs.existsSync(filePath)) {
+      this.error(`${filePath} - file does not exist under provided path!`, { exit: ExitCodes.FileNotFound })
+    }
+  }
+
+  assetUrl(endpointRoot: string, contentId: ContentId): string {
+    // This will also make sure the resulting url is a valid url
+    return new URL(`asset/v0/${contentId.encode()}`, endpointRoot).toString()
+  }
+
+  async getRandomProviderEndpoint(): Promise<string> {
+    const endpoints = _.shuffle(await this.getApi().allStorageProviderEndpoints())
+    for (const endpoint of endpoints) {
+      try {
+        const url = new URL(endpoint).toString()
+        // TODO: Some better way to test if provider is online?
+        await axios.get(url, { validateStatus: (s) => s === 404 /* 404 is expected */ })
+        return endpoint
+      } catch (e) {
+        continue
+      }
+    }
+
+    this.error('No active storage provider found', { exit: ExitCodes.ActionCurrentlyUnavailable })
+  }
+
+  async generateContentParameters(filePath: string, type: AssetType): Promise<ContentParameters> {
+    return this.createType('ContentParameters', {
+      content_id: ContentId.generate(this.getTypesRegistry()),
+      type_id: type,
+      size: this.getFileSize(filePath),
+      ipfs_content_id: await this.calculateFileIpfsHash(filePath),
+    })
+  }
+
+  async prepareInputAssets(paths: string[], basePath?: string): Promise<InputAssetDetails[]> {
+    // Resolve assets
+    if (basePath) {
+      paths = paths.map((p) => basePath && path.resolve(path.dirname(basePath), p))
+    }
+    // Validate assets
+    paths.forEach((p) => this.validateFile(p))
+
+    // Return data
+    return await Promise.all(
+      paths.map(async (path) => ({
+        path,
+        parameters: await this.generateContentParameters(path, AssetType.AnyAsset),
+      }))
+    )
+  }
+
+  async uploadAsset(contentId: ContentId, filePath: string, endpoint?: string, multiBar?: MultiBar): Promise<void> {
+    const providerEndpoint = endpoint || (await this.getRandomProviderEndpoint())
+    const uploadUrl = this.assetUrl(providerEndpoint, contentId)
+    const fileSize = this.getFileSize(filePath)
+    const { fileStream, progressBar } = this.createReadStreamWithProgressBar(
+      filePath,
+      `Uploading ${contentId.encode()}`,
+      multiBar
+    )
+    fileStream.on('end', () => {
+      // Temporarly disable because with Promise.all it breaks the UI
+      // cli.action.start('Waiting for the file to be processed...')
+    })
+
+    try {
+      const config: AxiosRequestConfig = {
+        headers: {
+          'Content-Type': '', // https://github.com/Joystream/storage-node-joystream/issues/16
+          'Content-Length': fileSize.toString(),
+        },
+      }
+      await axios.put(uploadUrl, fileStream, config)
+    } catch (e) {
+      multiBar ? multiBar.stop() : progressBar.stop()
+      const msg = (e.response && e.response.data && e.response.data.message) || e.message || e
+      this.error(`Unexpected error when trying to upload a file: ${msg}`, {
+        exit: ExitCodes.ExternalInfrastructureError,
+      })
+    }
+  }
+
+  async uploadAssets(assets: InputAssetDetails[]): Promise<void> {
+    const endpoint = await this.getRandomProviderEndpoint()
+    const multiBar = new MultiBar(this.progressBarOptions)
+    await Promise.all(assets.map((a) => this.uploadAsset(a.parameters.content_id, a.path, endpoint, multiBar)))
+    multiBar.stop()
+  }
+}

+ 39 - 31
cli/src/commands/content/createChannel.ts

@@ -1,53 +1,61 @@
+import { getInputJson } from '../../helpers/InputOutput'
+import { ChannelInputParameters } from '../../Types'
+import { metadataToBytes, channelMetadataFromInput } from '../../helpers/serialization'
+import { flags } from '@oclif/command'
+import { CreateInterface } from '@joystream/types'
+import { ChannelCreationParameters } from '@joystream/types/content'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { ChannelCreationParameters, ChannelCreationParametersInput } from '../../Types'
-import { channelMetadataFromInput } from '../../helpers/serialization'
+import UploadCommandBase from '../../base/UploadCommandBase'
+import ExitCodes from '../../ExitCodes'
 
-export default class CreateChannelCommand extends ContentDirectoryCommandBase {
+export default class CreateChannelCommand extends UploadCommandBase {
   static description = 'Create channel inside content directory.'
   static flags = {
     context: ContentDirectoryCommandBase.ownerContextFlag,
-    input: IOFlags.input,
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
   }
 
   async run() {
     let { context, input } = this.parse(CreateChannelCommand).flags
 
+    // Context
     if (!context) {
       context = await this.promptForOwnerContext()
     }
-
-    const currentAccount = await this.getRequiredSelectedAccount()
-    await this.requestAccountDecoding(currentAccount)
-
+    const account = await this.getRequiredSelectedAccount()
     const actor = await this.getActor(context)
+    await this.requestAccountDecoding(account)
 
-    if (input) {
-      const channelCreationParametersInput = await getInputJson<ChannelCreationParametersInput>(input)
-
-      const api = await this.getOriginalApi()
-
-      const meta = channelMetadataFromInput(api, channelCreationParametersInput)
+    const channelInput = await getInputJson<ChannelInputParameters>(input)
 
-      const channelCreationParameters: ChannelCreationParameters = {
-        assets: channelCreationParametersInput.assets,
-        meta,
-        reward_account: channelCreationParametersInput.reward_account,
-      }
-
-      this.jsonPrettyPrint(JSON.stringify(channelCreationParametersInput))
+    const meta = channelMetadataFromInput(channelInput)
+    const { coverPhotoPath, avatarPhotoPath } = channelInput
+    if (!coverPhotoPath || !avatarPhotoPath) {
+      // TODO: Handle with json schema validation?
+      this.error('Invalid input! coverPhotoPath and avatarPhotoPath are required!', { exit: ExitCodes.InvalidInput })
+    }
+    const inputAssets = await this.prepareInputAssets([coverPhotoPath, avatarPhotoPath], input)
+    const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
+    // Set assets indexes in the metadata
+    meta.setCoverPhoto(0)
+    meta.setAvatarPhoto(1)
+
+    const channelCreationParameters: CreateInterface<ChannelCreationParameters> = {
+      assets,
+      meta: metadataToBytes(meta),
+      reward_account: channelInput.rewardAccount,
+    }
 
-      this.log('Meta: ' + meta)
+    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta.toObject() }))
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.sendAndFollowNamedTx(account, 'content', 'createChannel', [actor, channelCreationParameters])
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'createChannel', [actor, channelCreationParameters])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    await this.uploadAssets(inputAssets)
   }
 }

+ 22 - 27
cli/src/commands/content/createChannelCategory.ts

@@ -1,13 +1,20 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { ChannelCategoryCreationParameters, ChannelCategoryCreationParametersInput } from '../../Types'
-import { channelCategoryMetadataFromInput } from '../../helpers/serialization'
+import { getInputJson } from '../../helpers/InputOutput'
+import { ChannelCategoryInputParameters } from '../../Types'
+import { channelCategoryMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { flags } from '@oclif/command'
+import { CreateInterface } from '@joystream/types'
+import { ChannelCategoryCreationParameters } from '@joystream/types/content'
 
 export default class CreateChannelCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create channel category inside content directory.'
   static flags = {
     context: ContentDirectoryCommandBase.categoriesContextFlag,
-    input: IOFlags.input,
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
   }
 
   async run() {
@@ -18,33 +25,21 @@ export default class CreateChannelCategoryCommand extends ContentDirectoryComman
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    if (input) {
-      const channelCategoryCreationParametersInput = await getInputJson<ChannelCategoryCreationParametersInput>(input)
+    const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input)
 
-      const api = await this.getOriginalApi()
+    const meta = channelCategoryMetadataFromInput(channelCategoryInput)
 
-      const meta = channelCategoryMetadataFromInput(api, channelCategoryCreationParametersInput)
-
-      const channelCategoryCreationParameters: ChannelCategoryCreationParameters = {
-        meta,
-      }
-
-      this.jsonPrettyPrint(JSON.stringify(channelCategoryCreationParametersInput))
-
-      this.log('Meta: ' + meta)
+    const channelCategoryCreationParameters: CreateInterface<ChannelCategoryCreationParameters> = {
+      meta: metadataToBytes(meta),
+    }
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    this.jsonPrettyPrint(JSON.stringify(channelCategoryInput))
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'createChannelCategory', [
-          actor,
-          channelCategoryCreationParameters,
-        ])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    await this.sendAndFollowNamedTx(currentAccount, 'content', 'createChannelCategory', [
+      actor,
+      channelCategoryCreationParameters,
+    ])
   }
 }

+ 64 - 40
cli/src/commands/content/createVideo.ts

@@ -1,62 +1,86 @@
-import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { videoMetadataFromInput } from '../../helpers/serialization'
-import { VideoCreationParameters, VideoCreationParametersInput } from '../../Types'
+import UploadCommandBase from '../../base/UploadCommandBase'
+import { getInputJson } from '../../helpers/InputOutput'
+import { videoMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { VideoInputParameters, VideoFileMetadata } from '../../Types'
+import { CreateInterface } from '@joystream/types'
+import { flags } from '@oclif/command'
+import { VideoCreationParameters } from '@joystream/types/content'
+import { MediaType, VideoMetadata } from '@joystream/content-metadata-protobuf'
+import ExitCodes from '../../ExitCodes'
 
-export default class CreateVideoCommand extends ContentDirectoryCommandBase {
+export default class CreateVideoCommand extends UploadCommandBase {
   static description = 'Create video under specific channel inside content directory.'
   static flags = {
-    input: IOFlags.input,
-  }
-
-  static args = [
-    {
-      name: 'channelId',
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
+    channelId: flags.integer({
+      char: 'c',
       required: true,
       description: 'ID of the Channel',
-    },
-  ]
+    }),
+  }
 
-  async run() {
-    const { input } = this.parse(CreateVideoCommand).flags
+  setVideoMetadataDefaults(metadata: VideoMetadata, videoFileMetadata: VideoFileMetadata) {
+    const metaObj = metadata.toObject()
+    metadata.setDuration((metaObj.duration || videoFileMetadata.duration) as number)
+    metadata.setMediaPixelWidth((metaObj.mediaPixelWidth || videoFileMetadata.width) as number)
+    metadata.setMediaPixelHeight((metaObj.mediaPixelHeight || videoFileMetadata.height) as number)
 
-    const { channelId } = this.parse(CreateVideoCommand).args
+    const fileMediaType = new MediaType()
+    fileMediaType.setCodecName(videoFileMetadata.codecName as string)
+    fileMediaType.setContainer(videoFileMetadata.container)
+    fileMediaType.setMimeMediaType(videoFileMetadata.mimeType)
+    metadata.setMediaType(metadata.getMediaType() || fileMediaType)
+  }
 
-    const currentAccount = await this.getRequiredSelectedAccount()
+  async run() {
+    const { input, channelId } = this.parse(CreateVideoCommand).flags
 
+    // Get context
+    const account = await this.getRequiredSelectedAccount()
     const channel = await this.getApi().channelById(channelId)
     const actor = await this.getChannelOwnerActor(channel)
+    await this.requestAccountDecoding(account)
 
-    await this.requestAccountDecoding(currentAccount)
+    // Get input from file
+    const videoCreationParametersInput = await getInputJson<VideoInputParameters>(input)
 
-    if (input) {
-      const videoCreationParametersInput = await getInputJson<VideoCreationParametersInput>(input)
+    const meta = videoMetadataFromInput(videoCreationParametersInput)
 
-      const api = await this.getOriginalApi()
+    // Assets
+    const { videoPath, thumbnailPhotoPath } = videoCreationParametersInput
+    if (!videoPath || !thumbnailPhotoPath) {
+      // TODO: Handle with json schema validation?
+      this.error('Invalid input! videoPath and thumbnailVideoPath are required!', { exit: ExitCodes.InvalidInput })
+    }
+    const inputAssets = await this.prepareInputAssets([videoPath, thumbnailPhotoPath], input)
+    const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
+    // Set assets indexes in the metadata
+    meta.setVideo(0)
+    meta.setThumbnailPhoto(1)
 
-      const meta = videoMetadataFromInput(api, videoCreationParametersInput)
+    // Try to get video file metadata
+    const videoFileMetadata = await this.getVideoFileMetadata(inputAssets[0].path)
+    this.log('Video media file parameters established:', videoFileMetadata)
+    this.setVideoMetadataDefaults(meta, videoFileMetadata)
 
-      const videoCreationParameters: VideoCreationParameters = {
-        assets: videoCreationParametersInput.assets,
-        meta,
-      }
+    // Create final extrinsic params and send the extrinsic
+    const videoCreationParameters: CreateInterface<VideoCreationParameters> = {
+      assets,
+      meta: metadataToBytes(meta),
+    }
 
-      this.jsonPrettyPrint(JSON.stringify(videoCreationParametersInput))
-      this.log('Meta: ' + meta)
+    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta.toObject() }))
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.sendAndFollowNamedTx(account, 'content', 'createVideo', [actor, channelId, videoCreationParameters])
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'createVideo', [
-          actor,
-          channelId,
-          videoCreationParameters,
-        ])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    // Upload assets
+    await this.uploadAssets(inputAssets)
+    // TODO: Reupload option if failed?
   }
 }

+ 22 - 27
cli/src/commands/content/createVideoCategory.ts

@@ -1,13 +1,20 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { VideoCategoryCreationParameters, VideoCategoryCreationParametersInput } from '../../Types'
-import { videoCategoryMetadataFromInput } from '../../helpers/serialization'
+import { getInputJson } from '../../helpers/InputOutput'
+import { VideoCategoryInputParameters } from '../../Types'
+import { metadataToBytes, videoCategoryMetadataFromInput } from '../../helpers/serialization'
+import { flags } from '@oclif/command'
+import { CreateInterface } from '@joystream/types'
+import { VideoCategoryCreationParameters } from '@joystream/types/content'
 
 export default class CreateVideoCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create video category inside content directory.'
   static flags = {
     context: ContentDirectoryCommandBase.categoriesContextFlag,
-    input: IOFlags.input, // TODO: Fix that
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
   }
 
   async run() {
@@ -18,33 +25,21 @@ export default class CreateVideoCategoryCommand extends ContentDirectoryCommandB
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    if (input) {
-      const videoCategoryCreationParametersInput = await getInputJson<VideoCategoryCreationParametersInput>(input)
+    const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input)
 
-      const api = await this.getOriginalApi()
+    const meta = videoCategoryMetadataFromInput(videoCategoryInput)
 
-      const meta = videoCategoryMetadataFromInput(api, videoCategoryCreationParametersInput)
-
-      const videoCategoryCreationParameters: VideoCategoryCreationParameters = {
-        meta,
-      }
-
-      this.jsonPrettyPrint(JSON.stringify(videoCategoryCreationParametersInput))
-
-      this.log('Meta: ' + meta)
+    const videoCategoryCreationParameters: CreateInterface<VideoCategoryCreationParameters> = {
+      meta: metadataToBytes(meta),
+    }
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    this.jsonPrettyPrint(JSON.stringify(videoCategoryInput))
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'createVideoCategory', [
-          actor,
-          videoCategoryCreationParameters,
-        ])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    await this.sendAndFollowNamedTx(currentAccount, 'content', 'createVideoCategory', [
+      actor,
+      videoCategoryCreationParameters,
+    ])
   }
 }

+ 45 - 36
cli/src/commands/content/updateChannel.ts

@@ -1,12 +1,19 @@
-import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { channelMetadataFromInput } from '../../helpers/serialization'
-import { ChannelUpdateParameters, ChannelUpdateParametersInput } from '../../Types'
-
-export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
+import { getInputJson } from '../../helpers/InputOutput'
+import { channelMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { ChannelInputParameters } from '../../Types'
+import { flags } from '@oclif/command'
+import UploadCommandBase from '../../base/UploadCommandBase'
+import { CreateInterface } from '@joystream/types'
+import { ChannelUpdateParameters } from '@joystream/types/content'
+
+export default class UpdateChannelCommand extends UploadCommandBase {
   static description = 'Update existing content directory channel.'
   static flags = {
-    input: IOFlags.input,
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
   }
 
   static args = [
@@ -18,47 +25,49 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
   ]
 
   async run() {
-    const { input } = this.parse(UpdateChannelCommand).flags
-
-    const { channelId } = this.parse(UpdateChannelCommand).args
+    const {
+      flags: { input },
+      args: { channelId },
+    } = this.parse(UpdateChannelCommand)
 
+    // Context
     const currentAccount = await this.getRequiredSelectedAccount()
-
     const channel = await this.getApi().channelById(channelId)
     const actor = await this.getChannelOwnerActor(channel)
-
     await this.requestAccountDecoding(currentAccount)
 
-    if (input) {
-      const channelUpdateParametersInput = await getInputJson<ChannelUpdateParametersInput>(input)
+    const channelInput = await getInputJson<ChannelInputParameters>(input)
 
-      const api = await this.getOriginalApi()
+    const meta = channelMetadataFromInput(channelInput)
 
-      const meta = channelMetadataFromInput(api, channelUpdateParametersInput)
-
-      const channelUpdateParameters: ChannelUpdateParameters = {
-        assets: channelUpdateParametersInput.assets,
-        new_meta: meta,
-        reward_account: channelUpdateParametersInput.reward_account,
-      }
+    const { coverPhotoPath, avatarPhotoPath } = channelInput
+    const inputPaths = [coverPhotoPath, avatarPhotoPath].filter((p) => p !== undefined) as string[]
+    const inputAssets = await this.prepareInputAssets(inputPaths, input)
+    const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
+    // Set assets indexes in the metadata
+    if (coverPhotoPath) {
+      meta.setCoverPhoto(0)
+    }
+    if (avatarPhotoPath) {
+      meta.setAvatarPhoto(coverPhotoPath ? 1 : 0)
+    }
 
-      this.jsonPrettyPrint(JSON.stringify(channelUpdateParametersInput))
+    const channelUpdateParameters: CreateInterface<ChannelUpdateParameters> = {
+      assets,
+      new_meta: metadataToBytes(meta),
+      reward_account: channelInput.rewardAccount,
+    }
 
-      this.log('Meta: ' + meta)
+    this.jsonPrettyPrint(JSON.stringify({ assets, metadata: meta.toObject() }))
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateChannel', [
+      actor,
+      channelId,
+      channelUpdateParameters,
+    ])
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateChannel', [
-          actor,
-          channelId,
-          channelUpdateParameters,
-        ])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    await this.uploadAssets(inputAssets)
   }
 }

+ 23 - 28
cli/src/commands/content/updateChannelCategory.ts

@@ -1,13 +1,20 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { ChannelCategoryUpdateParameters, ChannelCategoryUpdateParametersInput } from '../../Types'
-import { channelCategoryMetadataFromInput } from '../../helpers/serialization'
+import { getInputJson } from '../../helpers/InputOutput'
+import { ChannelCategoryInputParameters } from '../../Types'
+import { channelCategoryMetadataFromInput, metadataToBytes } from '../../helpers/serialization'
+import { CreateInterface } from '@joystream/types'
+import { ChannelCategoryUpdateParameters } from '@joystream/types/content'
+import { flags } from '@oclif/command'
 
 export default class UpdateChannelCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Update channel category inside content directory.'
   static flags = {
     context: ContentDirectoryCommandBase.categoriesContextFlag,
-    input: IOFlags.input,
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
   }
 
   static args = [
@@ -28,34 +35,22 @@ export default class UpdateChannelCategoryCommand extends ContentDirectoryComman
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    if (input) {
-      const channelCategoryUpdateParametersInput = await getInputJson<ChannelCategoryUpdateParametersInput>(input)
-
-      const api = await this.getOriginalApi()
-
-      const meta = channelCategoryMetadataFromInput(api, channelCategoryUpdateParametersInput)
+    const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input)
 
-      const channelCategoryUpdateParameters: ChannelCategoryUpdateParameters = {
-        new_meta: meta,
-      }
+    const meta = channelCategoryMetadataFromInput(channelCategoryInput)
 
-      this.jsonPrettyPrint(JSON.stringify(channelCategoryUpdateParametersInput))
-
-      this.log('Meta: ' + meta)
+    const channelCategoryUpdateParameters: CreateInterface<ChannelCategoryUpdateParameters> = {
+      new_meta: metadataToBytes(meta),
+    }
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    this.jsonPrettyPrint(JSON.stringify(channelCategoryInput))
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateChannelCategory', [
-          actor,
-          channelCategoryId,
-          channelCategoryUpdateParameters,
-        ])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateChannelCategory', [
+      actor,
+      channelCategoryId,
+      channelCategoryUpdateParameters,
+    ])
   }
 }

+ 39 - 34
cli/src/commands/content/updateVideo.ts

@@ -1,12 +1,19 @@
-import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { VideoUpdateParameters, VideoUpdateParametersInput } from '../../Types'
-import { videoMetadataFromInput } from '../../helpers/serialization'
+import { getInputJson } from '../../helpers/InputOutput'
+import { VideoInputParameters } from '../../Types'
+import { metadataToBytes, videoMetadataFromInput } from '../../helpers/serialization'
+import UploadCommandBase from '../../base/UploadCommandBase'
+import { flags } from '@oclif/command'
+import { CreateInterface } from '@joystream/types'
+import { VideoUpdateParameters } from '@joystream/types/content'
 
-export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
+export default class UpdateVideoCommand extends UploadCommandBase {
   static description = 'Update video under specific id.'
   static flags = {
-    input: IOFlags.input,
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
   }
 
   static args = [
@@ -18,46 +25,44 @@ export default class UpdateVideoCommand extends ContentDirectoryCommandBase {
   ]
 
   async run() {
-    const { input } = this.parse(UpdateVideoCommand).flags
-
-    const { videoId } = this.parse(UpdateVideoCommand).args
+    const {
+      flags: { input },
+      args: { videoId },
+    } = this.parse(UpdateVideoCommand)
 
+    // Context
     const currentAccount = await this.getRequiredSelectedAccount()
-
     const video = await this.getApi().videoById(videoId)
     const channel = await this.getApi().channelById(video.in_channel.toNumber())
     const actor = await this.getChannelOwnerActor(channel)
-
     await this.requestAccountDecoding(currentAccount)
 
-    if (input) {
-      const videoUpdateParametersInput = await getInputJson<VideoUpdateParametersInput>(input)
-
-      const api = await this.getOriginalApi()
+    const videoInput = await getInputJson<VideoInputParameters>(input)
 
-      const meta = videoMetadataFromInput(api, videoUpdateParametersInput)
+    const meta = videoMetadataFromInput(videoInput)
+    const { videoPath, thumbnailPhotoPath } = videoInput
+    const inputPaths = [videoPath, thumbnailPhotoPath].filter((p) => p !== undefined) as string[]
+    const inputAssets = await this.prepareInputAssets(inputPaths, input)
+    const assets = inputAssets.map(({ parameters }) => ({ Upload: parameters }))
+    // Set assets indexes in the metadata
+    if (videoPath) {
+      meta.setVideo(0)
+    }
+    if (thumbnailPhotoPath) {
+      meta.setThumbnailPhoto(videoPath ? 1 : 0)
+    }
 
-      const videoUpdateParameters: VideoUpdateParameters = {
-        assets: videoUpdateParametersInput.assets,
-        meta,
-      }
+    const videoUpdateParameters: CreateInterface<VideoUpdateParameters> = {
+      assets,
+      new_meta: metadataToBytes(meta),
+    }
 
-      this.jsonPrettyPrint(JSON.stringify(videoUpdateParametersInput))
-      this.log('Meta: ' + meta)
+    this.jsonPrettyPrint(JSON.stringify({ assets, newMetadata: meta.toObject() }))
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateVideo', [actor, videoId, videoUpdateParameters])
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateVideo', [
-          actor,
-          videoId,
-          videoUpdateParameters,
-        ])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    await this.uploadAssets(inputAssets)
   }
 }

+ 23 - 28
cli/src/commands/content/updateVideoCategory.ts

@@ -1,13 +1,20 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { IOFlags, getInputJson } from '../../helpers/InputOutput'
-import { VideoCategoryUpdateParameters, VideoCategoryUpdateParametersInput } from '../../Types'
-import { videoCategoryMetadataFromInput } from '../../helpers/serialization'
+import { getInputJson } from '../../helpers/InputOutput'
+import { VideoCategoryInputParameters } from '../../Types'
+import { metadataToBytes, videoCategoryMetadataFromInput } from '../../helpers/serialization'
+import { flags } from '@oclif/command'
+import { CreateInterface } from '@joystream/types'
+import { VideoCategoryUpdateParameters } from '@joystream/types/content'
 
 export default class UpdateVideoCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Update video category inside content directory.'
   static flags = {
     context: ContentDirectoryCommandBase.categoriesContextFlag,
-    input: IOFlags.input,
+    input: flags.string({
+      char: 'i',
+      required: true,
+      description: `Path to JSON file to use as input`,
+    }),
   }
 
   static args = [
@@ -28,34 +35,22 @@ export default class UpdateVideoCategoryCommand extends ContentDirectoryCommandB
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    if (input) {
-      const videoCategoryUpdateParametersInput = await getInputJson<VideoCategoryUpdateParametersInput>(input)
-
-      const api = await this.getOriginalApi()
-
-      const meta = videoCategoryMetadataFromInput(api, videoCategoryUpdateParametersInput)
+    const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input)
 
-      const videoCategoryUpdateParameters: VideoCategoryUpdateParameters = {
-        new_meta: meta,
-      }
+    const meta = videoCategoryMetadataFromInput(videoCategoryInput)
 
-      this.jsonPrettyPrint(JSON.stringify(videoCategoryUpdateParameters))
-
-      this.log('Meta: ' + meta)
+    const videoCategoryUpdateParameters: CreateInterface<VideoCategoryUpdateParameters> = {
+      new_meta: metadataToBytes(meta),
+    }
 
-      const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    this.jsonPrettyPrint(JSON.stringify(videoCategoryInput))
 
-      if (confirmed) {
-        this.log('Sending the extrinsic...')
+    await this.requireConfirmation('Do you confirm the provided input?', true)
 
-        await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateVideoCategory', [
-          actor,
-          videoCategoryId,
-          videoCategoryUpdateParameters,
-        ])
-      }
-    } else {
-      this.error('Input invalid or was not provided...')
-    }
+    await this.sendAndFollowNamedTx(currentAccount, 'content', 'updateVideoCategory', [
+      actor,
+      videoCategoryId,
+      videoCategoryUpdateParameters,
+    ])
   }
 }

+ 63 - 70
cli/src/helpers/serialization.ts

@@ -8,98 +8,91 @@ import {
   VideoCategoryMetadata,
 } from '@joystream/content-metadata-protobuf'
 import {
-  VideoUpdateParametersInput,
-  VideoCreationParametersInput,
-  ChannelUpdateParametersInput,
-  ChannelCreationParametersInput,
-  ChannelCategoryCreationParametersInput,
-  ChannelCategoryUpdateParametersInput,
-  VideoCategoryCreationParametersInput,
-  VideoCategoryUpdateParametersInput,
+  ChannelCategoryInputParameters,
+  ChannelInputParameters,
+  VideoCategoryInputParameters,
+  VideoInputParameters,
 } from '../Types'
-import { ApiPromise } from '@polkadot/api'
 import { Bytes } from '@polkadot/types/primitive'
+import { createType } from '@joystream/types'
 
-export function binaryToMeta(api: ApiPromise, serialized: Uint8Array): Bytes {
-  return api.createType('Bytes', '0x' + Buffer.from(serialized).toString('hex'))
+type AnyMetadata = {
+  serializeBinary(): Uint8Array
 }
 
-export function videoMetadataFromInput(
-  api: ApiPromise,
-  videoParametersInput: VideoCreationParametersInput | VideoUpdateParametersInput
-): Bytes {
-  const mediaType = new MediaType()
-  mediaType.setCodecName(videoParametersInput.meta.mediaType!.codecName!)
-  mediaType.setContainer(videoParametersInput.meta.mediaType!.container!)
-  mediaType.setMimeMediaType(videoParametersInput.meta.mediaType!.mimeMediaType!)
-
-  const license = new License()
-  license.setCode(videoParametersInput.meta.license!.code!)
-  license.setAttribution(videoParametersInput.meta.license!.attribution!)
-  license.setCustomText(videoParametersInput.meta.license!.customText!)
+export function metadataToBytes(metadata: AnyMetadata): Bytes {
+  const bytes = createType('Bytes', '0x' + Buffer.from(metadata.serializeBinary()).toString('hex'))
+  console.log('Metadata as Bytes:', bytes.toString())
+  return bytes
+}
 
-  const publishedBeforeJoystream = new PublishedBeforeJoystream()
-  publishedBeforeJoystream.setIsPublished(videoParametersInput.meta.publishedBeforeJoystream!.isPublished!)
-  publishedBeforeJoystream.setDate(videoParametersInput.meta.publishedBeforeJoystream!.date!)
+// TODO: If "fromObject()" was generated for the protobuffs we could avoid having to create separate converters for each metadata
 
+export function videoMetadataFromInput(videoParametersInput: VideoInputParameters): VideoMetadata {
   const videoMetadata = new VideoMetadata()
-  videoMetadata.setTitle(videoParametersInput.meta.title!)
-  videoMetadata.setDescription(videoParametersInput.meta.description!)
-  videoMetadata.setVideo(videoParametersInput.meta.video!)
-  videoMetadata.setThumbnailPhoto(videoParametersInput.meta.thumbnailPhoto!)
-  videoMetadata.setDuration(videoParametersInput.meta.duration!)
-  videoMetadata.setMediaPixelHeight(videoParametersInput.meta.mediaPixelHeight!)
-  videoMetadata.setMediaPixelWidth(videoParametersInput.meta.mediaPixelWidth!)
-  videoMetadata.setLanguage(videoParametersInput.meta.language!)
-  videoMetadata.setHasMarketing(videoParametersInput.meta.hasMarketing!)
-  videoMetadata.setIsPublic(videoParametersInput.meta.isPublic!)
-  videoMetadata.setIsExplicit(videoParametersInput.meta.isExplicit!)
-  videoMetadata.setPersonsList(videoParametersInput.meta.personsList!)
-  videoMetadata.setCategory(videoParametersInput.meta.category!)
+  videoMetadata.setTitle(videoParametersInput.title as string)
+  videoMetadata.setDescription(videoParametersInput.description as string)
+  videoMetadata.setDuration(videoParametersInput.duration as number)
+  videoMetadata.setMediaPixelHeight(videoParametersInput.mediaPixelHeight as number)
+  videoMetadata.setMediaPixelWidth(videoParametersInput.mediaPixelWidth as number)
+  videoMetadata.setLanguage(videoParametersInput.language as string)
+  videoMetadata.setHasMarketing(videoParametersInput.hasMarketing as boolean)
+  videoMetadata.setIsPublic(videoParametersInput.isPublic as boolean)
+  videoMetadata.setIsExplicit(videoParametersInput.isExplicit as boolean)
+  videoMetadata.setPersonsList(videoParametersInput.personsList as number[])
+  videoMetadata.setCategory(videoParametersInput.category as number)
+
+  if (videoParametersInput.mediaType) {
+    const mediaType = new MediaType()
+    mediaType.setCodecName(videoParametersInput.mediaType.codecName as string)
+    mediaType.setContainer(videoParametersInput.mediaType.container as string)
+    mediaType.setMimeMediaType(videoParametersInput.mediaType.mimeMediaType as string)
+    videoMetadata.setMediaType(mediaType)
+  }
+
+  if (videoParametersInput.publishedBeforeJoystream) {
+    const publishedBeforeJoystream = new PublishedBeforeJoystream()
+    publishedBeforeJoystream.setIsPublished(videoParametersInput.publishedBeforeJoystream.isPublished as boolean)
+    publishedBeforeJoystream.setDate(videoParametersInput.publishedBeforeJoystream.date as string)
+    videoMetadata.setPublishedBeforeJoystream(publishedBeforeJoystream)
+  }
 
-  videoMetadata.setMediaType(mediaType)
-  videoMetadata.setLicense(license)
-  videoMetadata.setPublishedBeforeJoystream(publishedBeforeJoystream)
+  if (videoParametersInput.license) {
+    const license = new License()
+    license.setCode(videoParametersInput.license.code as number)
+    license.setAttribution(videoParametersInput.license.attribution as string)
+    license.setCustomText(videoParametersInput.license.customText as string)
+    videoMetadata.setLicense(license)
+  }
 
-  const serialized = videoMetadata.serializeBinary()
-  return binaryToMeta(api, serialized)
+  return videoMetadata
 }
 
-export function channelMetadataFromInput(
-  api: ApiPromise,
-  channelParametersInput: ChannelCreationParametersInput | ChannelUpdateParametersInput
-): Bytes {
+export function channelMetadataFromInput(channelParametersInput: ChannelInputParameters): ChannelMetadata {
   const channelMetadata = new ChannelMetadata()
-  channelMetadata.setTitle(channelParametersInput.meta.title!)
-  channelMetadata.setDescription(channelParametersInput.meta.description!)
-  channelMetadata.setIsPublic(channelParametersInput.meta.isPublic!)
-  channelMetadata.setLanguage(channelParametersInput.meta.language!)
-  channelMetadata.setCoverPhoto(channelParametersInput.meta.coverPhoto!)
-  channelMetadata.setAvatarPhoto(channelParametersInput.meta.avatarPhoto!)
-  channelMetadata.setCategory(channelParametersInput.meta.category!)
+  channelMetadata.setTitle(channelParametersInput.title as string)
+  channelMetadata.setDescription(channelParametersInput.description as string)
+  channelMetadata.setIsPublic(channelParametersInput.isPublic as boolean)
+  channelMetadata.setLanguage(channelParametersInput.language as string)
+  channelMetadata.setCategory(channelParametersInput.category as number)
 
-  const serialized = channelMetadata.serializeBinary()
-  return binaryToMeta(api, serialized)
+  return channelMetadata
 }
 
 export function channelCategoryMetadataFromInput(
-  api: ApiPromise,
-  channelCategoryParametersInput: ChannelCategoryCreationParametersInput | ChannelCategoryUpdateParametersInput
-): Bytes {
+  channelCategoryParametersInput: ChannelCategoryInputParameters
+): ChannelCategoryMetadata {
   const channelCategoryMetadata = new ChannelCategoryMetadata()
-  channelCategoryMetadata.setName(channelCategoryParametersInput.meta.name!)
+  channelCategoryMetadata.setName(channelCategoryParametersInput.name as string)
 
-  const serialized = channelCategoryMetadata.serializeBinary()
-  return binaryToMeta(api, serialized)
+  return channelCategoryMetadata
 }
 
 export function videoCategoryMetadataFromInput(
-  api: ApiPromise,
-  videoCategoryParametersInput: VideoCategoryCreationParametersInput | VideoCategoryUpdateParametersInput
-): Bytes {
+  videoCategoryParametersInput: VideoCategoryInputParameters
+): VideoCategoryMetadata {
   const videoCategoryMetadata = new VideoCategoryMetadata()
-  videoCategoryMetadata.setName(videoCategoryParametersInput.meta.name!)
+  videoCategoryMetadata.setName(videoCategoryParametersInput.name as string)
 
-  const serialized = videoCategoryMetadata.serializeBinary()
-  return binaryToMeta(api, serialized)
+  return videoCategoryMetadata
 }

+ 1 - 1
types/src/index.ts

@@ -90,7 +90,7 @@ type CreateInterface_NoOption<T extends Codec> =
 
 // Wrapper for CreateInterface_NoOption that includes resolving an Option
 // (nested Options like Option<Option<Codec>> will resolve to Option<any>, but there are very edge case)
-type CreateInterface<T extends Codec> =
+export type CreateInterface<T extends Codec> =
   | T
   | (T extends Option<infer S> ? undefined | null | S | CreateInterface_NoOption<S> : CreateInterface_NoOption<T>)
 

+ 451 - 9
yarn.lock

@@ -1242,6 +1242,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.7", "@babel/runtime@^7.13.8":
+  version "7.13.10"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
+  integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/runtime@^7.6.3":
   version "7.7.4"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b"
@@ -3483,7 +3490,7 @@
     is-ipfs "^0.6.0"
     recursive-fs "^1.1.2"
 
-"@polkadot/api-contract@1.26.1", "@polkadot/api-contract@^1.26.1":
+"@polkadot/api-contract@^1.26.1":
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-1.26.1.tgz#a8b52ef469ab8bbddb83191f8d451e31ffd76142"
   integrity sha512-zLGA/MHUJf12vanUEUBBRqpHVAONHWztoHS0JTIWUUS2+3GEXk6hGw+7PPtBDfDsLj0LgU/Qna1bLalC/zyl5w==
@@ -3512,7 +3519,22 @@
     memoizee "^0.4.14"
     rxjs "^6.6.0"
 
-"@polkadot/api@1.26.1", "@polkadot/api@^1.26.1", "@polkadot/api@^2.10.1":
+"@polkadot/api-derive@2.10.1":
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-2.10.1.tgz#6dc6c0030e036e8a38d44b7e06fd884e9c1b32fb"
+  integrity sha512-cMbXrOyHWJ/uLxNiAjmRa6a8WM/FEDMansWbQGJtN7ebHrJD3t1SE53aM4zgD+AgaEJgPAUfI5RuOrEzxDDTdw==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/api" "2.10.1"
+    "@polkadot/rpc-core" "2.10.1"
+    "@polkadot/types" "2.10.1"
+    "@polkadot/util" "^4.2.1"
+    "@polkadot/util-crypto" "^4.2.1"
+    bn.js "^4.11.9"
+    memoizee "^0.4.14"
+    rxjs "^6.6.3"
+
+"@polkadot/api@1.26.1", "@polkadot/api@^1.26.1":
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-1.26.1.tgz#215268489c10b1a65429c6ce451c8d65bd3ad843"
   integrity sha512-al8nmLgIU1EKo0oROEgw1mqUvrHJu4gKYBwnFONaEOxHSxBgBSSgNy1MWKNntAQYDKA4ETCj4pz7ZpMXTx2SDA==
@@ -3531,6 +3553,25 @@
     eventemitter3 "^4.0.4"
     rxjs "^6.6.0"
 
+"@polkadot/api@2.10.1", "@polkadot/api@^2.10.1":
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-2.10.1.tgz#750987bccbf8e607c3690a7bdfed818bfc2c7571"
+  integrity sha512-C/vd5eGK3SDpPBWfs6tbNJM6uKpThE9GiTs5Lb5yR83J2ssvnZnn4qGOoEZnpPH+2iW7hVS4GR5sE9YcZxUXTg==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/api-derive" "2.10.1"
+    "@polkadot/keyring" "^4.2.1"
+    "@polkadot/metadata" "2.10.1"
+    "@polkadot/rpc-core" "2.10.1"
+    "@polkadot/rpc-provider" "2.10.1"
+    "@polkadot/types" "2.10.1"
+    "@polkadot/types-known" "2.10.1"
+    "@polkadot/util" "^4.2.1"
+    "@polkadot/util-crypto" "^4.2.1"
+    bn.js "^4.11.9"
+    eventemitter3 "^4.0.7"
+    rxjs "^6.6.3"
+
 "@polkadot/dev@^0.55.28":
   version "0.55.33"
   resolved "https://registry.yarnpkg.com/@polkadot/dev/-/dev-0.55.33.tgz#52ef3a941e5b68ea669dca01e3df55e220e27cfb"
@@ -3624,6 +3665,15 @@
     "@polkadot/util" "3.0.1"
     "@polkadot/util-crypto" "3.0.1"
 
+"@polkadot/keyring@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-4.2.1.tgz#34bf18ae8cb5822f2ea522c8db62dd0086725ffa"
+  integrity sha512-8kH8jXSIA3I2Gn96o7KjGoLBa7fmc2iB/VKOmEEcMCgJR32HyE8YbeXwc/85OQCheQjG4rJA3RxPQ4CsTsjO7w==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/util" "4.2.1"
+    "@polkadot/util-crypto" "4.2.1"
+
 "@polkadot/metadata@1.26.1":
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/metadata/-/metadata-1.26.1.tgz#64b959415dab6f61ba415b0a337a3ec06e3cad3e"
@@ -3636,6 +3686,44 @@
     "@polkadot/util-crypto" "^3.0.1"
     bn.js "^5.1.2"
 
+"@polkadot/metadata@2.10.1":
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/metadata/-/metadata-2.10.1.tgz#bea4696c8773af4214c071ab5017bef215d978c1"
+  integrity sha512-ilB81k4ZDFVLHYo8mhxs9VFpL7Vi/Q0tqTSuQ+ziD3U7fYh0QV5si+1nqo5EBzvIKws6hsC7B4bTPQLJHHTC9w==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/types" "2.10.1"
+    "@polkadot/types-known" "2.10.1"
+    "@polkadot/util" "^4.2.1"
+    "@polkadot/util-crypto" "^4.2.1"
+    bn.js "^4.11.9"
+
+"@polkadot/metadata@2.10.2-18":
+  version "2.10.2-18"
+  resolved "https://registry.yarnpkg.com/@polkadot/metadata/-/metadata-2.10.2-18.tgz#cba0cb31aa7335fb54478b4cc20013b4897d6079"
+  integrity sha512-bJ9cxMInspOKusMgL1HLdvwycYtKueYE43CY9u7pI+tcUVge+mC2/aL80dIvJPZ04UxVLN0WSam8HiMGptHuOA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/types" "2.10.2-18"
+    "@polkadot/types-known" "2.10.2-18"
+    "@polkadot/util" "^5.0.1"
+    "@polkadot/util-crypto" "^5.0.1"
+    bn.js "^4.11.9"
+
+"@polkadot/networks@4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-4.2.1.tgz#b0ca69807ed60189f1c958bb27cfeb3cb1c6b12b"
+  integrity sha512-T1tg0V0uG09Vdce2O4KfEcWO3/fZh4VYt0bmJ6iPwC+x6yv939X2BKvuFTDDVNT3fqBpGzWQlwiTXYQ15o9bGA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+
+"@polkadot/networks@5.9.2":
+  version "5.9.2"
+  resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-5.9.2.tgz#c687525b5886c9418f75240afe22b562ed88e2dd"
+  integrity sha512-JQyXJDJTZKQtn8y3HBHWDhiBfijhpiXjVEhY+fKvFcQ82TaKmzhnipYX0EdBoopZbuxpn/BJy6Y1Y/3y85EC+g==
+  dependencies:
+    "@babel/runtime" "^7.13.8"
+
 "@polkadot/react-identicon@^0.57.3":
   version "0.57.3"
   resolved "https://registry.yarnpkg.com/@polkadot/react-identicon/-/react-identicon-0.57.3.tgz#f2f1a9b57faa66e1df47a0238daa9607f76d946c"
@@ -3670,6 +3758,19 @@
     memoizee "^0.4.14"
     rxjs "^6.6.0"
 
+"@polkadot/rpc-core@2.10.1":
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-2.10.1.tgz#6d9cca349dc03324dbf9c3bfe2a9db555808a664"
+  integrity sha512-oyEEhSwlKW3FNO5v7MJYSoiF5kIxcJKMKVJSIpLHp6G2oHhgKRZtsGlX4n6QJYxIBWb0EueewpkuEMCGAv3R7g==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/metadata" "2.10.1"
+    "@polkadot/rpc-provider" "2.10.1"
+    "@polkadot/types" "2.10.1"
+    "@polkadot/util" "^4.2.1"
+    memoizee "^0.4.14"
+    rxjs "^6.6.3"
+
 "@polkadot/rpc-provider@1.26.1":
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-1.26.1.tgz#85adef601ab7e65925761ac6e7081019de4de1c7"
@@ -3685,6 +3786,20 @@
     isomorphic-fetch "^2.2.1"
     websocket "^1.0.31"
 
+"@polkadot/rpc-provider@2.10.1":
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-2.10.1.tgz#7929b5aa8899033ba127984b4411baef92a1232d"
+  integrity sha512-VvrFedxIbPrcm3CadZLdVwm3eWyyaZV1Sh0BSGZ2u9Pi2JkONshWrg7mf32SbKhckXWt/BNwUnpCQfIUjnKaDw==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/types" "2.10.1"
+    "@polkadot/util" "^4.2.1"
+    "@polkadot/util-crypto" "^4.2.1"
+    "@polkadot/x-fetch" "^4.2.1"
+    "@polkadot/x-ws" "^4.2.1"
+    bn.js "^4.11.9"
+    eventemitter3 "^4.0.7"
+
 "@polkadot/ts@^0.1.56":
   version "0.1.91"
   resolved "https://registry.yarnpkg.com/@polkadot/ts/-/ts-0.1.91.tgz#e3cc05cea480cc3d15b213110aec082fb0af5e79"
@@ -3733,7 +3848,27 @@
     "@polkadot/util" "^3.0.1"
     bn.js "^5.1.2"
 
-"@polkadot/types@1.26.1", "@polkadot/types@^1.26.1", "@polkadot/types@~2.10.2-7":
+"@polkadot/types-known@2.10.1":
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-2.10.1.tgz#37bc032aae7db12e9a4480caf5aa65f619cffac9"
+  integrity sha512-RmnRPMoypxodfXRRqO+t4ogeaHTEC1S968+Djo8SYeSSmeUrlo9LdoJ5DZBXd0dTOUJbo0wXl9DOjL5qVnRy6A==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/types" "2.10.1"
+    "@polkadot/util" "^4.2.1"
+    bn.js "^4.11.9"
+
+"@polkadot/types-known@2.10.2-18":
+  version "2.10.2-18"
+  resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-2.10.2-18.tgz#eeb00deadf36bf8762a93994e2512929f1524a01"
+  integrity sha512-0YKYF/bxjFlK/JeLjNyi7qjvZLxBkSYI89Wt2+qYnLwXvvcRzxTBdq8E1BUHOqlh2FZoVpxUGs99J0Kbblwbmw==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/types" "2.10.2-18"
+    "@polkadot/util" "^5.0.1"
+    bn.js "^4.11.9"
+
+"@polkadot/types@1.26.1", "@polkadot/types@^1.26.1":
   version "1.26.1"
   resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-1.26.1.tgz#e58a823da22bd526b298f7d42384bf59b8994fad"
   integrity sha512-mrA3+qYyDvfOIOMkY8lg2ziCYpwOl3N1LUxKdiyBDtKM7Dl8ZWQ0nLUCDW5MhbzDlThmYjE4feBRA+2eBShfyA==
@@ -3747,6 +3882,33 @@
     memoizee "^0.4.14"
     rxjs "^6.6.0"
 
+"@polkadot/types@2.10.1":
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-2.10.1.tgz#84189d508c28d375ec562a049aaf58aa34256a74"
+  integrity sha512-wRs9X7uiSRNQBFxcuCDv++FU+HgFml55U73zsqxDgBb7+bor4QGLPpki8rV+xQOpqhfPjKHN1gosK99sFcC3Aw==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/metadata" "2.10.1"
+    "@polkadot/util" "^4.2.1"
+    "@polkadot/util-crypto" "^4.2.1"
+    "@types/bn.js" "^4.11.6"
+    bn.js "^4.11.9"
+    memoizee "^0.4.14"
+    rxjs "^6.6.3"
+
+"@polkadot/types@2.10.2-18", "@polkadot/types@~2.10.2-7":
+  version "2.10.2-18"
+  resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-2.10.2-18.tgz#01c4c23ea372707311b2b9c06f47e9ddc9793ad6"
+  integrity sha512-bNMXnG+3jQ70JksCErAhYH3ZvkcLuVmZjzrxUE6YJM7gdCXZ3R6qJdLunYDwnXX/RXoGAQUq/y3rrpOdCvJ4Dg==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/metadata" "2.10.2-18"
+    "@polkadot/util" "^5.0.1"
+    "@polkadot/util-crypto" "^5.0.1"
+    "@polkadot/x-rxjs" "2.10.2-18"
+    "@types/bn.js" "^4.11.6"
+    bn.js "^4.11.9"
+
 "@polkadot/ui-keyring@^0.57.3":
   version "0.57.3"
   resolved "https://registry.yarnpkg.com/@polkadot/ui-keyring/-/ui-keyring-0.57.3.tgz#f66cdc2943a6f76734df56d2a8520ba60b7c3709"
@@ -3797,6 +3959,49 @@
     tweetnacl "^1.0.3"
     xxhashjs "^0.2.2"
 
+"@polkadot/util-crypto@4.2.1", "@polkadot/util-crypto@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-4.2.1.tgz#a342cd6b400c69ed61cd929917030ed2f43c59d1"
+  integrity sha512-U1rCdzBQxVTA854HRpt2d4InDnPCfHD15JiWAwIzjBvq7i59EcTbVSqV02fcwet/KpmT3XYa25xoiff+alzCBA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/networks" "4.2.1"
+    "@polkadot/util" "4.2.1"
+    "@polkadot/wasm-crypto" "^2.0.1"
+    "@polkadot/x-randomvalues" "4.2.1"
+    base-x "^3.0.8"
+    blakejs "^1.1.0"
+    bn.js "^4.11.9"
+    create-hash "^1.2.0"
+    elliptic "^6.5.3"
+    hash.js "^1.1.7"
+    js-sha3 "^0.8.0"
+    scryptsy "^2.1.0"
+    tweetnacl "^1.0.3"
+    xxhashjs "^0.2.2"
+
+"@polkadot/util-crypto@^5.0.1":
+  version "5.9.2"
+  resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-5.9.2.tgz#3858cfffe7732458b4a2b38ece01eaf52a3746c2"
+  integrity sha512-d8CW2grI3gWi6d/brmcZQWaMPHqQq5z7VcM74/v8D2KZ+hPYL3B0Jn8zGL1vtgMz2qdpWrZdAe89LBC8BvM9bw==
+  dependencies:
+    "@babel/runtime" "^7.13.8"
+    "@polkadot/networks" "5.9.2"
+    "@polkadot/util" "5.9.2"
+    "@polkadot/wasm-crypto" "^3.2.4"
+    "@polkadot/x-randomvalues" "5.9.2"
+    base-x "^3.0.8"
+    base64-js "^1.5.1"
+    blakejs "^1.1.0"
+    bn.js "^4.11.9"
+    create-hash "^1.2.0"
+    elliptic "^6.5.4"
+    hash.js "^1.1.7"
+    js-sha3 "^0.8.0"
+    scryptsy "^2.1.0"
+    tweetnacl "^1.0.3"
+    xxhashjs "^0.2.2"
+
 "@polkadot/util@3.0.1", "@polkadot/util@^3.0.1":
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-3.0.1.tgz#f7ed9d81d745136aa6d6ad57277ee05c88f32784"
@@ -3809,6 +4014,32 @@
     chalk "^4.1.0"
     ip-regex "^4.1.0"
 
+"@polkadot/util@4.2.1", "@polkadot/util@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-4.2.1.tgz#1845d03be7e418a14ec2ef929d6288f326f2145d"
+  integrity sha512-eO/IFbSDjqVPPWPnARDFydy2Kt992Th+8ByleTkCRqWk0aNYaseO1pGKNdwrYbLfUR3JlyWqvJ60lITeS+qAfQ==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@polkadot/x-textdecoder" "4.2.1"
+    "@polkadot/x-textencoder" "4.2.1"
+    "@types/bn.js" "^4.11.6"
+    bn.js "^4.11.9"
+    camelcase "^5.3.1"
+    ip-regex "^4.2.0"
+
+"@polkadot/util@5.9.2", "@polkadot/util@^5.0.1":
+  version "5.9.2"
+  resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-5.9.2.tgz#ad2494e78ca6c3aadd6fb394a6be55020dc9b2a8"
+  integrity sha512-p225NJusnXeu7i2iAb8HAGWiMOUAnRaIyblIjJ4F89ZFZZ4amyliGxe5gKcyjRgxAJ44WdKyBLl/8L3rNv8hmQ==
+  dependencies:
+    "@babel/runtime" "^7.13.8"
+    "@polkadot/x-textdecoder" "5.9.2"
+    "@polkadot/x-textencoder" "5.9.2"
+    "@types/bn.js" "^4.11.6"
+    bn.js "^4.11.9"
+    camelcase "^5.3.1"
+    ip-regex "^4.3.0"
+
 "@polkadot/vanitygen@^0.18.1":
   version "0.18.1"
   resolved "https://registry.yarnpkg.com/@polkadot/vanitygen/-/vanitygen-0.18.1.tgz#44839473e3cd1490289cef57c05f0466a4e1db80"
@@ -3821,11 +4052,119 @@
     chalk "^4.1.0"
     yargs "^15.4.1"
 
+"@polkadot/wasm-crypto-asmjs@^3.2.4":
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-3.2.4.tgz#837f5b723161b21670d13779eff4c061f7947577"
+  integrity sha512-fgN26iL+Pbb35OYsDIRHC74Xnwde+A5u3OjEcQ9zJhM391eOTuKsQ2gyC9TLNAKqeYH8pxsa27yjRO71We7FUA==
+  dependencies:
+    "@babel/runtime" "^7.13.7"
+
+"@polkadot/wasm-crypto-wasm@^3.2.4":
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-3.2.4.tgz#70885e06a813af91d81cf7e8ff826976fa99a38b"
+  integrity sha512-Q/3IEpoo7vkTzg40GxehRK000A9oBgjbh/uWCNQ8cMqWLYYCfzZy4NIzw8szpxNiSiGfGL0iZlP4ZSx2ZqEe2g==
+  dependencies:
+    "@babel/runtime" "^7.13.7"
+
 "@polkadot/wasm-crypto@^1.2.1":
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-1.2.1.tgz#2189702447acd28d763886359576c87562241767"
   integrity sha512-nckIoZBV4nBZdeKwFwH5t7skS7L7GO5EFUl5B1F6uCjUfdNpDz3DtqbYQHcLdCZNmG4TDLg6w/1J+rkl2SiUZw==
 
+"@polkadot/wasm-crypto@^2.0.1":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-2.0.1.tgz#cf7384385f832f6389520cc00e52a87fda6f29b6"
+  integrity sha512-Vb0q4NToCRHXYJwhLWc4NTy77+n1dtJmkiE1tt8j1pmY4IJ4UL25yBxaS8NCS1LGqofdUYK1wwgrHiq5A78PFA==
+
+"@polkadot/wasm-crypto@^3.2.4":
+  version "3.2.4"
+  resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-3.2.4.tgz#c3e23ff728c1d5701215ae15ecdc605e96901989"
+  integrity sha512-poeRU91zzZza0ZectT63vBiAqh6DsHCyd3Ogx1U6jsYiRa0yuECMWJx1onvnseDW4tIqsC8vZ/9xHXWwhjTAVg==
+  dependencies:
+    "@babel/runtime" "^7.13.7"
+    "@polkadot/wasm-crypto-asmjs" "^3.2.4"
+    "@polkadot/wasm-crypto-wasm" "^3.2.4"
+
+"@polkadot/x-fetch@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-4.2.1.tgz#6cd157da6f98f97395c3f01849ccdd3de23ee44f"
+  integrity sha512-dfVYvCQQXo2AgoWPi4jQp47eIMjAi6glQQ8Y1OsK4sCqmX7BSkNl9ONUKQuH27oi0BkJ/BL7fwDg55JeB5QrKg==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@types/node-fetch" "^2.5.7"
+    node-fetch "^2.6.1"
+
+"@polkadot/x-global@5.9.2":
+  version "5.9.2"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-5.9.2.tgz#e223d59536d168c7cbc49fc3a2052cbd71bd7256"
+  integrity sha512-wpY6IAOZMGiJQa8YMm7NeTLi9bwnqqVauR+v7HwyrssnGPuYX8heb6BQLOnnnPh/EK0+M8zNtwRBU48ez0/HOg==
+  dependencies:
+    "@babel/runtime" "^7.13.8"
+    "@types/node-fetch" "^2.5.8"
+    node-fetch "^2.6.1"
+
+"@polkadot/x-randomvalues@4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-4.2.1.tgz#91fd272f8bb79a59b20055a4514f944888a6ee76"
+  integrity sha512-eOfz/KnHYFVl9l0zlhlwomKMzFASgolaQV6uXSN38np+99/+F38wlbOSXFbfZ5H3vmMCt4y/UUTLtoGV/44yLg==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+
+"@polkadot/x-randomvalues@5.9.2":
+  version "5.9.2"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-5.9.2.tgz#563a76550f94107ce5a37c462ed067dc040626b1"
+  integrity sha512-Zv+eXSP3oBImMnB82y05Doo0A96WUFsQDbnLHI3jFHioIg848cL0nndB9TgBwPaFkZ2oiwoHEC8yxqNI6/jkzQ==
+  dependencies:
+    "@babel/runtime" "^7.13.8"
+    "@polkadot/x-global" "5.9.2"
+
+"@polkadot/x-rxjs@2.10.2-18":
+  version "2.10.2-18"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-rxjs/-/x-rxjs-2.10.2-18.tgz#d849066588d4183137a455cc337db7346c4f9b01"
+  integrity sha512-fH9f237F8pevPESf1AgH94+huxWlU1D/b2EyYrQcrS/fFlBy5MIDQy7X12QHbwMR5xElCVroJ0mq7cDrlchokQ==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    rxjs "^6.6.3"
+
+"@polkadot/x-textdecoder@4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-4.2.1.tgz#c2fe9f5da9498d982f8fd9244a52e039c0f0dacc"
+  integrity sha512-B5t20PryMKr7kdd7q+kmzJPU01l28ZDD06cQ/ZFkybI7avI6PIz/U33ctXxiHOatbBRO6Ez8uzrWd3JmaQ2bGQ==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+
+"@polkadot/x-textdecoder@5.9.2":
+  version "5.9.2"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-5.9.2.tgz#2e69922acc426f91adc2629fea362e41c9035f25"
+  integrity sha512-MCkgITwGY3tG0UleDkBJEoiKGk/YWYwMM5OR6fNo07RymHRtJ8OLJC+Sej9QD05yz6TIhFaaRRYzmtungIcwTw==
+  dependencies:
+    "@babel/runtime" "^7.13.8"
+    "@polkadot/x-global" "5.9.2"
+
+"@polkadot/x-textencoder@4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-4.2.1.tgz#cf6b92d7de0fb2dde8314e0f359dd83dc9f25036"
+  integrity sha512-EHc6RS9kjdP28q6EYlSgHF2MrJCdOTc5EVlqHL7V1UKLh3vD6QaWGYBwbzXNFPXO3RYPO/DKYCu4RxAVSM1OOg==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+
+"@polkadot/x-textencoder@5.9.2":
+  version "5.9.2"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-5.9.2.tgz#67362e64bacfe6ff4eec73bf596873a2f9d9f36d"
+  integrity sha512-IjdLY3xy0nUfps1Bdi0tRxAX7X081YyoiSWExwqUkChdcYGMqMe3T2wqrrt9qBr2IkW8O/tlfYBiZXdII0YCcw==
+  dependencies:
+    "@babel/runtime" "^7.13.8"
+    "@polkadot/x-global" "5.9.2"
+
+"@polkadot/x-ws@^4.2.1":
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-4.2.1.tgz#f160a0c61227419b1d7da623a72ce21063ef69ee"
+  integrity sha512-7L1ve2rshBFI/00/0zkX1k0OP/rSD6Tp0Mj/GSg2UvnsmUb2Bb3OpwUJ4aTDr1En6OVGWj9c0fNO0tZR7rtoYA==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@types/websocket" "^1.0.1"
+    websocket "^1.0.32"
+
 "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@@ -4642,6 +4981,13 @@
   dependencies:
     "@types/filesystem" "*"
 
+"@types/cli-progress@^3.9.1":
+  version "3.9.1"
+  resolved "https://registry.yarnpkg.com/@types/cli-progress/-/cli-progress-3.9.1.tgz#285e7fbdad6e7baf072d163ae1c3b23b7b219130"
+  integrity sha512-X/tKJv/GoYlCBS9wwJTLrVSxzIOI/Cj1cCatYOAAoQne3aT1QbHBptBS5+zLe2ToSljAijHU1N/ouBNFvZ2H/g==
+  dependencies:
+    "@types/node" "*"
+
 "@types/codeflask@^1.4.1":
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/@types/codeflask/-/codeflask-1.4.1.tgz#05d6b919162b754e6014249ab02841a0db0fd60d"
@@ -5110,6 +5456,14 @@
     "@types/node" "*"
     form-data "^3.0.0"
 
+"@types/node-fetch@^2.5.7", "@types/node-fetch@^2.5.8":
+  version "2.5.10"
+  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132"
+  integrity sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ==
+  dependencies:
+    "@types/node" "*"
+    form-data "^3.0.0"
+
 "@types/node@*", "@types/node@>= 8":
   version "12.12.14"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.14.tgz#1c1d6e3c75dba466e0326948d56e8bd72a1903d2"
@@ -5519,6 +5873,13 @@
     "@types/webpack-sources" "*"
     source-map "^0.6.0"
 
+"@types/websocket@^1.0.1":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a"
+  integrity sha512-B5m9aq7cbbD/5/jThEr33nUY8WEfVi6A2YKCTOvw5Ldy7mtsOkqRvGjnzy6g7iMMDsgu7xREuCzqATLDLQVKcQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/ws@^6.0.3":
   version "6.0.4"
   resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
@@ -7781,6 +8142,11 @@ base64-js@^1.0.2:
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
   integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
 
+base64-js@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 base@^0.11.1:
   version "0.11.2"
   resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@@ -7946,11 +8312,21 @@ bluebird@^3.1.1, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
   integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0, bn.js@^5.1.2, bn.js@^5.1.3:
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.4.0:
+  version "4.12.0"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
+  integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
+
+bn.js@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0"
   integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==
 
+bn.js@^5.1.3:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
+  integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
+
 body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -8080,7 +8456,7 @@ braces@^3.0.1, braces@~3.0.2:
   dependencies:
     fill-range "^7.0.1"
 
-brorand@^1.0.1:
+brorand@^1.0.1, brorand@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
   integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
@@ -8308,6 +8684,13 @@ buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
     base64-js "^1.0.2"
     ieee754 "^1.1.4"
 
+bufferutil@^4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.3.tgz#66724b756bed23cd7c28c4d306d7994f9943cc6b"
+  integrity sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==
+  dependencies:
+    node-gyp-build "^4.2.0"
+
 builder-util-runtime@8.7.2:
   version "8.7.2"
   resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.7.2.tgz#d93afc71428a12789b437e13850e1fa7da956d72"
@@ -11606,6 +11989,19 @@ elliptic@^6.5.2, elliptic@^6.5.3:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.0"
 
+elliptic@^6.5.4:
+  version "6.5.4"
+  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
+  integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
+  dependencies:
+    bn.js "^4.11.9"
+    brorand "^1.1.0"
+    hash.js "^1.0.0"
+    hmac-drbg "^1.0.1"
+    inherits "^2.0.4"
+    minimalistic-assert "^1.0.1"
+    minimalistic-crypto-utils "^1.0.1"
+
 email-addresses@^3.0.1:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.1.0.tgz#cabf7e085cbdb63008a70319a74e6136188812fb"
@@ -12463,6 +12859,11 @@ eventemitter3@^4.0.4:
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384"
   integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==
 
+eventemitter3@^4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
+
 events@1.1.1, events@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@@ -14725,7 +15126,7 @@ hash-sum@^1.0.2:
   resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
   integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=
 
-hash.js@^1.0.0, hash.js@^1.0.3:
+hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
   integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
@@ -14802,7 +15203,7 @@ history@^4.9.0:
     tiny-warning "^1.0.0"
     value-equal "^1.0.1"
 
-hmac-drbg@^1.0.0:
+hmac-drbg@^1.0.0, hmac-drbg@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
   integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
@@ -15729,6 +16130,11 @@ ip-regex@^4.0.0, ip-regex@^4.1.0:
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.1.0.tgz#5ad62f685a14edb421abebc2fff8db94df67b455"
   integrity sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==
 
+ip-regex@^4.2.0, ip-regex@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
+  integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
+
 ip@1.1.5, ip@^1.1.0, ip@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
@@ -20867,7 +21273,7 @@ node-fetch-npm@^2.0.2:
     json-parse-better-errors "^1.0.0"
     safe-buffer "^5.1.1"
 
-node-fetch@2.6.1, node-fetch@^2.1.2, node-fetch@^2.2.0:
+node-fetch@2.6.1, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
   integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@@ -20895,6 +21301,11 @@ node-forge@~0.9.1:
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
   integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
 
+node-gyp-build@^4.2.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739"
+  integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==
+
 node-gyp-build@~4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
@@ -25357,6 +25768,13 @@ rxjs@^6.5.2, rxjs@^6.6.0:
   dependencies:
     tslib "^1.9.0"
 
+rxjs@^6.6.3:
+  version "6.6.7"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+  integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+  dependencies:
+    tslib "^1.9.0"
+
 safe-buffer@5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -28155,11 +28573,16 @@ typescript-formatter@^7.2.2:
     commandpost "^1.0.0"
     editorconfig "^0.15.0"
 
-typescript@^3.0.3, typescript@^3.7.2, typescript@^3.7.5, typescript@^3.8, typescript@^3.8.3, typescript@^3.9.5, typescript@^3.9.6, typescript@^3.9.7, typescript@^4.1.3:
+typescript@^3.0.3, typescript@^3.7.2, typescript@^3.7.5, typescript@^3.8, typescript@^3.8.3, typescript@^3.9.5, typescript@^3.9.6, typescript@^3.9.7:
   version "3.9.7"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
   integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
 
+typescript@^4.1.3:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
+  integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
+
 ua-parser-js@^0.7.18:
   version "0.7.21"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
@@ -28665,6 +29088,13 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
+utf-8-validate@^5.0.2:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.4.tgz#72a1735983ddf7a05a43a9c6b67c5ce1c910f9b8"
+  integrity sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==
+  dependencies:
+    node-gyp-build "^4.2.0"
+
 utf8-byte-length@^1.0.1:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
@@ -29478,6 +29908,18 @@ websocket@^1.0.31:
     typedarray-to-buffer "^3.1.5"
     yaeti "^0.0.6"
 
+websocket@^1.0.32:
+  version "1.0.33"
+  resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.33.tgz#407f763fc58e74a3fa41ca3ae5d78d3f5e3b82a5"
+  integrity sha512-XwNqM2rN5eh3G2CUQE3OHZj+0xfdH42+OFK6LdC2yqiC0YU8e5UK0nYre220T0IyyN031V/XOvtHvXozvJYFWA==
+  dependencies:
+    bufferutil "^4.0.1"
+    debug "^2.2.0"
+    es5-ext "^0.10.50"
+    typedarray-to-buffer "^3.1.5"
+    utf-8-validate "^5.0.2"
+    yaeti "^0.0.6"
+
 whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"