Browse Source

createClass, addClassSchema - input and output options

Leszek Wiesner 4 years ago
parent
commit
544cd57993

+ 43 - 35
cli/src/base/ApiCommandBase.ts

@@ -13,6 +13,7 @@ import { InterfaceTypes } from '@polkadot/types/types/registry'
 import ajv from 'ajv'
 import { ApiMethodArg, ApiMethodNamedArgs, ApiParamsOptions, ApiParamOptions } from '../Types'
 import { createParamOptions } from '../helpers/promptOptions'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
 
 class ExtrinsicFailedError extends Error {}
 
@@ -384,32 +385,30 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return values
   }
 
-  sendExtrinsic(account: KeyringPair, module: string, method: string, params: CodecArg[]) {
+  sendExtrinsic(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>) {
     return new Promise((resolve, reject) => {
-      const extrinsicMethod = this.getOriginalApi().tx[module][method]
       let unsubscribe: () => void
-      extrinsicMethod(...params)
-        .signAndSend(account, {}, (result) => {
-          // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
-          if (!result || !result.status) {
-            return
-          }
-
-          if (result.status.isInBlock) {
-            unsubscribe()
-            result.events
-              .filter(({ event: { section } }): boolean => section === 'system')
-              .forEach(({ event: { method } }): void => {
-                if (method === 'ExtrinsicFailed') {
-                  reject(new ExtrinsicFailedError('Extrinsic execution error!'))
-                } else if (method === 'ExtrinsicSuccess') {
-                  resolve()
-                }
-              })
-          } else if (result.isError) {
-            reject(new ExtrinsicFailedError('Extrinsic execution error!'))
-          }
-        })
+      tx.signAndSend(account, {}, (result) => {
+        // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
+        if (!result || !result.status) {
+          return
+        }
+
+        if (result.status.isInBlock) {
+          unsubscribe()
+          result.events
+            .filter(({ event: { section } }): boolean => section === 'system')
+            .forEach(({ event: { method } }): void => {
+              if (method === 'ExtrinsicFailed') {
+                reject(new ExtrinsicFailedError('Extrinsic execution error!'))
+              } else if (method === 'ExtrinsicSuccess') {
+                resolve()
+              }
+            })
+        } else if (result.isError) {
+          reject(new ExtrinsicFailedError('Extrinsic execution error!'))
+        }
+      })
         .then((unsubFunc) => (unsubscribe = unsubFunc))
         .catch((e) =>
           reject(new ExtrinsicFailedError(`Cannot send the extrinsic: ${e.message ? e.message : JSON.stringify(e)}`))
@@ -417,28 +416,37 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     })
   }
 
-  async sendAndFollowExtrinsic(
+  async sendAndFollowTx(
     account: KeyringPair,
-    module: string,
-    method: string,
-    params: CodecArg[],
-    warnOnly = false // If specified - only warning will be displayed (instead of error beeing thrown)
-  ) {
+    tx: SubmittableExtrinsic<'promise'>,
+    warnOnly = true // If specified - only warning will be displayed in case of failure (instead of error beeing thrown)
+  ): Promise<void> {
     try {
-      this.log(chalk.white(`\nSending ${module}.${method} extrinsic...`))
-      await this.sendExtrinsic(account, module, method, params)
+      await this.sendExtrinsic(account, tx)
       this.log(chalk.green(`Extrinsic successful!`))
     } catch (e) {
       if (e instanceof ExtrinsicFailedError && warnOnly) {
-        this.warn(`${module}.${method} extrinsic failed! ${e.message}`)
+        this.warn(`Extrinsic failed! ${e.message}`)
       } else if (e instanceof ExtrinsicFailedError) {
-        throw new CLIError(`${module}.${method} extrinsic failed! ${e.message}`, { exit: ExitCodes.ApiError })
+        throw new CLIError(`Extrinsic failed! ${e.message}`, { exit: ExitCodes.ApiError })
       } else {
         throw e
       }
     }
   }
 
+  async sendAndFollowNamedTx(
+    account: KeyringPair,
+    module: string,
+    method: string,
+    params: CodecArg[],
+    warnOnly = false
+  ): Promise<void> {
+    this.log(chalk.white(`\nSending ${module}.${method} extrinsic...`))
+    const tx = await this.getOriginalApi().tx[module][method](...params)
+    await this.sendAndFollowTx(account, tx, warnOnly)
+  }
+
   async buildAndSendExtrinsic(
     account: KeyringPair,
     module: string,
@@ -447,7 +455,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     warnOnly = false // If specified - only warning will be displayed (instead of error beeing thrown)
   ): Promise<ApiMethodArg[]> {
     const params = await this.promptForExtrinsicParams(module, method, paramsOptions)
-    await this.sendAndFollowExtrinsic(account, module, method, params, warnOnly)
+    await this.sendAndFollowNamedTx(account, module, method, params, warnOnly)
 
     return params
   }

+ 6 - 9
cli/src/base/ContentDirectoryCommandBase.ts

@@ -18,16 +18,13 @@ export default abstract class ContentDirectoryCommandBase extends AccountsComman
     }
   }
 
-  async promptForClass(message = 'Select a class'): Promise<number> {
+  async promptForClassName(message = 'Select a class'): Promise<string> {
     const classes = await this.getApi().availableClasses()
-    const choices = classes.map(([id, aClass]) => ({
-      name: aClass.name.toString(),
-      value: id.toNumber(),
-    }))
+    const choices = classes.map(([, c]) => ({ name: c.name.toString(), value: c.name.toString() }))
 
-    const selectedId = await this.simplePrompt({ message, type: 'list', choices })
+    const selected = await this.simplePrompt({ message, type: 'list', choices })
 
-    return selectedId
+    return selected
   }
 
   async promptForCuratorGroups(message = 'Select a curator group'): Promise<number> {
@@ -43,8 +40,8 @@ export default abstract class ContentDirectoryCommandBase extends AccountsComman
   }
 
   async promptForClassReference(): Promise<ReferenceProperty['Reference']> {
-    const classId = await this.promptForClass()
+    const className = await this.promptForClassName()
     const sameOwner = await this.simplePrompt({ message: 'Same owner required?', ...BOOL_PROMPT_OPTIONS })
-    return [classId, sameOwner]
+    return { className, sameOwner }
   }
 }

+ 32 - 17
cli/src/commands/content-directory/addClassSchema.ts

@@ -1,35 +1,50 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import AddClassSchemaSchema from 'cd-schemas/schemas/extrinsics/AddClassSchema.schema.json'
 import { AddClassSchema } from 'cd-schemas/types/extrinsics/AddClassSchema'
+import { InputParser } from 'cd-schemas/scripts/helpers/InputParser'
 import { JsonSchemaPrompter, JsonSchemaCustomPrompts } from '../../helpers/JsonSchemaPrompt'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 
-export default class CouncilInfo extends ContentDirectoryCommandBase {
+export default class AddClassSchemaCommand extends ContentDirectoryCommandBase {
   static description = 'Add a new schema to a class inside content directory. Requires lead access.'
 
+  static flags = {
+    ...IOFlags,
+  }
+
   async run() {
     const account = await this.getRequiredSelectedAccount()
     await this.requireLead()
 
-    const customPrompts: JsonSchemaCustomPrompts = [
-      ['classId', async () => this.promptForClass('Select a class to add schema to')],
-      [/^newProperties\[\d+\]\.property_type\.Single\.Reference/, async () => this.promptForClassReference()],
-    ]
+    const { input, output } = this.parse(AddClassSchemaCommand).flags
+
+    let inputJson = getInputJson<AddClassSchema>(input)
+    if (!inputJson) {
+      const customPrompts: JsonSchemaCustomPrompts = [
+        ['className', async () => this.promptForClassName('Select a class to add schema to')],
+        [/^newProperties\[\d+\]\.property_type\.Single\.Reference/, async () => this.promptForClassReference()],
+      ]
+
+      const prompter = new JsonSchemaPrompter<AddClassSchema>(
+        AddClassSchemaSchema as JSONSchema,
+        undefined,
+        customPrompts
+      )
 
-    const prompter = new JsonSchemaPrompter<AddClassSchema>(
-      AddClassSchemaSchema as JSONSchema,
-      undefined,
-      customPrompts
-    )
+      inputJson = await prompter.promptAll()
+    }
 
-    const addClassSchemaJson = await prompter.promptAll()
+    this.jsonPrettyPrint(JSON.stringify(inputJson))
+    const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
 
-    this.jsonPrettyPrint(JSON.stringify(addClassSchemaJson))
+    if (confirmed) {
+      await this.requestAccountDecoding(account)
+      const inputParser = new InputParser(this.getOriginalApi())
+      this.log('Sending the extrinsic...')
+      await this.sendAndFollowTx(account, await inputParser.parseAddClassSchemaExtrinsic(inputJson), true)
 
-    await this.sendAndFollowExtrinsic(account, 'contentDirectory', 'addClassSchema', [
-      addClassSchemaJson.classId,
-      addClassSchemaJson.existingProperties,
-      addClassSchemaJson.newProperties as any[],
-    ])
+      saveOutputJson(output, `${inputJson.className}Schema.json`, inputJson)
+    }
   }
 }

+ 26 - 14
cli/src/commands/content-directory/createClass.ts

@@ -1,32 +1,44 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import CreateClassSchema from 'cd-schemas/schemas/extrinsics/CreateClass.schema.json'
 import { CreateClass } from 'cd-schemas/types/extrinsics/CreateClass'
+import { InputParser } from 'cd-schemas/scripts/helpers/InputParser'
 import { JsonSchemaPrompter, JsonSchemaCustomPrompts } from '../../helpers/JsonSchemaPrompt'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
+import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 
-export default class CouncilInfo extends ContentDirectoryCommandBase {
+export default class CreateClassCommand extends ContentDirectoryCommandBase {
   static description = 'Create class inside content directory. Requires lead access.'
+  static flags = {
+    ...IOFlags,
+  }
 
   async run() {
     const account = await this.getRequiredSelectedAccount()
     await this.requireLead()
 
-    const customPrompts: JsonSchemaCustomPrompts = [
-      ['class_permissions.maintainers', () => this.promptForCuratorGroups('Select class maintainers')],
-    ]
+    const { input, output } = this.parse(CreateClassCommand).flags
+
+    let inputJson = getInputJson<CreateClass>(input)
+    if (!inputJson) {
+      const customPrompts: JsonSchemaCustomPrompts = [
+        ['class_permissions.maintainers', () => this.promptForCuratorGroups('Select class maintainers')],
+      ]
+
+      const prompter = new JsonSchemaPrompter<CreateClass>(CreateClassSchema as JSONSchema, undefined, customPrompts)
 
-    const prompter = new JsonSchemaPrompter<CreateClass>(CreateClassSchema as JSONSchema, undefined, customPrompts)
+      inputJson = await prompter.promptAll()
+    }
 
-    const createClassJson = await prompter.promptAll()
+    this.jsonPrettyPrint(JSON.stringify(inputJson))
+    const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
 
-    console.log(this.jsonPrettyPrint(JSON.stringify(createClassJson)))
+    if (confirmed) {
+      await this.requestAccountDecoding(account)
+      this.log('Sending the extrinsic...')
+      const inputParser = new InputParser(this.getOriginalApi())
+      await this.sendAndFollowTx(account, inputParser.parseCreateClassExtrinsic(inputJson))
 
-    await this.sendAndFollowExtrinsic(account, 'contentDirectory', 'createClass', [
-      createClassJson.name,
-      createClassJson.description,
-      createClassJson.class_permissions,
-      createClassJson.maximum_entities_count,
-      createClassJson.default_entity_creation_voucher_upper_bound,
-    ])
+      saveOutputJson(output, `${inputJson.name}Class.json`, inputJson)
+    }
   }
 }

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

@@ -42,7 +42,7 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'decreaseStake', [workerId, balance])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'decreaseStake', [workerId, balance])
 
     this.log(
       chalk.green(

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

@@ -41,7 +41,7 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'terminateRole', [
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'terminateRole', [
       workerId,
       rationale,
       shouldSlash,

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

@@ -33,7 +33,7 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'fillOpening', [
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'fillOpening', [
       openingId,
       applicationIds,
       rewardPolicyOpt,

+ 1 - 4
cli/src/commands/working-groups/increaseStake.ts

@@ -30,10 +30,7 @@ export default class WorkingGroupsIncreaseStake extends WorkingGroupsCommandBase
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'increaseStake', [
-      worker.workerId,
-      balance,
-    ])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'increaseStake', [worker.workerId, balance])
 
     this.log(
       chalk.green(

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

@@ -21,7 +21,7 @@ export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'leaveRole', [worker.workerId, rationale])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'leaveRole', [worker.workerId, rationale])
 
     this.log(chalk.green(`Succesfully left the role! (worker id: ${chalk.white(worker.workerId.toNumber())})`))
   }

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

@@ -39,7 +39,7 @@ export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'slashStake', [workerId, balance])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'slashStake', [workerId, balance])
 
     this.log(
       chalk.green(

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

@@ -29,7 +29,7 @@ export default class WorkingGroupsStartAcceptingApplications extends WorkingGrou
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'acceptApplications', [openingId])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'acceptApplications', [openingId])
 
     this.log(
       chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('Accepting Applications')}`)

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

@@ -29,7 +29,7 @@ export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommand
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId])
 
     this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('In Review')}`))
   }

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

@@ -30,7 +30,7 @@ export default class WorkingGroupsTerminateApplication extends WorkingGroupsComm
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'terminateApplication', [applicationId])
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'terminateApplication', [applicationId])
 
     this.log(chalk.green(`Application ${chalk.white(applicationId)} has been succesfully terminated!`))
   }

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

@@ -38,7 +38,7 @@ export default class WorkingGroupsUpdateRewardAccount extends WorkingGroupsComma
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRewardAccount', [
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'updateRewardAccount', [
       worker.workerId,
       newRewardAccount,
     ])

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

@@ -32,7 +32,7 @@ export default class WorkingGroupsUpdateRoleAccount extends WorkingGroupsCommand
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRoleAccount', [
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'updateRoleAccount', [
       worker.workerId,
       newRoleAccount,
     ])

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

@@ -55,7 +55,7 @@ export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsComman
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRewardAmount', [
+    await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'updateRewardAmount', [
       workerId,
       newRewardValue,
     ])

+ 68 - 0
cli/src/helpers/InputOutput.ts

@@ -0,0 +1,68 @@
+import { flags } from '@oclif/command'
+import { CLIError } from '@oclif/errors'
+import ExitCodes from '../ExitCodes'
+import fs from 'fs'
+import path from 'path'
+import Ajv from 'ajv'
+import { JSONSchema7 } from 'json-schema'
+import chalk from 'chalk'
+
+export const IOFlags = {
+  input: flags.string({
+    char: 'i',
+    required: false,
+    description: `Path to JSON file to use as input (if not specified - the input can be provided interactively)`,
+  }),
+  output: flags.string({
+    char: 'o',
+    required: false,
+    description: 'Path where the output JSON file should be placed (can be then reused as input)',
+  }),
+}
+
+export function getInputJson<T>(inputPath?: string, schema?: JSONSchema7): T | null {
+  if (inputPath) {
+    let content, jsonObj
+    try {
+      content = fs.readFileSync(inputPath).toString()
+    } catch (e) {
+      throw new CLIError(`Cannot access the input file at: ${inputPath}`, { exit: ExitCodes.FsOperationFailed })
+    }
+    try {
+      jsonObj = JSON.parse(content)
+    } catch (e) {
+      throw new CLIError(`JSON parsing failed for file: ${inputPath}`, { exit: ExitCodes.InvalidInput })
+    }
+    if (schema) {
+      const ajv = new Ajv()
+      const valid = ajv.validate(schema, jsonObj)
+      if (!valid) {
+        throw new CLIError(`Input JSON file is not valid: ${ajv.errorsText()}`)
+      }
+    }
+
+    return jsonObj as T
+  }
+
+  return null
+}
+
+export function saveOutputJson(outputPath: string | undefined, fileName: string, data: any): void {
+  if (outputPath) {
+    let outputFilePath = path.join(outputPath, fileName)
+    let postfix = 0
+    while (fs.existsSync(outputFilePath)) {
+      fileName = fileName.replace(/(_[0-9]+)?\.json/, `_${++postfix}.json`)
+      outputFilePath = path.join(outputPath, fileName)
+    }
+    try {
+      fs.writeFileSync(outputFilePath, JSON.stringify(data, null, 4))
+    } catch (e) {
+      throw new CLIError(`Could not save the output to: ${outputFilePath}. Check directory permissions`, {
+        exit: ExitCodes.FsOperationFailed,
+      })
+    }
+
+    console.log(`${chalk.green('Output succesfully saved to:')} ${chalk.white(outputFilePath)}`)
+  }
+}

+ 27 - 23
content-directory-schemas/scripts/helpers/InputParser.ts

@@ -194,32 +194,36 @@ export class InputParser {
     return operations
   }
 
+  public async parseAddClassSchemaExtrinsic(inputData: AddClassSchema) {
+    await this.initializeClassMap() // Initialize if not yet initialized
+    const classId = this.getClassIdByName(inputData.className)
+    const newProperties = inputData.newProperties.map((p) => ({
+      ...p,
+      // Parse different format for Reference (and potentially other propTypes in the future)
+      property_type: this.parsePropertyType(p.property_type).toJSON(),
+    }))
+    return this.api.tx.contentDirectory.addClassSchema(
+      classId,
+      new (JoyBTreeSet(PropertyId))(this.api.registry, inputData.existingProperties),
+      newProperties
+    )
+  }
+
+  public parseCreateClassExtrinsic(inputData: CreateClass) {
+    return this.api.tx.contentDirectory.createClass(
+      inputData.name,
+      inputData.description,
+      inputData.class_permissions || {},
+      inputData.maximum_entities_count,
+      inputData.default_entity_creation_voucher_upper_bound
+    )
+  }
+
   public async getAddSchemaExtrinsics() {
-    await this.initializeClassMap()
-    return this.schemaInputs.map(({ data: schema }) => {
-      const classId = this.getClassIdByName(schema.className)
-      const newProperties = schema.newProperties.map((p) => ({
-        ...p,
-        // Parse different format for Reference (and potentially other propTypes in the future)
-        property_type: this.parsePropertyType(p.property_type).toJSON(),
-      }))
-      return this.api.tx.contentDirectory.addClassSchema(
-        classId,
-        new (JoyBTreeSet(PropertyId))(this.api.registry, schema.existingProperties),
-        newProperties
-      )
-    })
+    return await Promise.all(this.schemaInputs.map(({ data }) => this.parseAddClassSchemaExtrinsic(data)))
   }
 
   public getCreateClassExntrinsics() {
-    return this.classInputs.map(({ data: aClass }) =>
-      this.api.tx.contentDirectory.createClass(
-        aClass.name,
-        aClass.description,
-        aClass.class_permissions || {},
-        aClass.maximum_entities_count,
-        aClass.default_entity_creation_voucher_upper_bound
-      )
-    )
+    return this.classInputs.map(({ data }) => this.parseCreateClassExtrinsic(data))
   }
 }