Browse Source

Make extrinsic params prompting more customizable

Leszek Wiesner 4 years ago
parent
commit
18558844cc

+ 27 - 8
cli/src/Types.ts

@@ -1,7 +1,7 @@
 import BN from 'bn.js';
 import { ElectionStage, Seat } from '@joystream/types/council';
 import { Option, Text } from '@polkadot/types';
-import { Constructor } from '@polkadot/types/types';
+import { Constructor, Codec } from '@polkadot/types/types';
 import { Struct, Vec } from '@polkadot/types/codec';
 import { u32 } from '@polkadot/types/primitive';
 import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces';
@@ -24,6 +24,7 @@ import {
 } from '@joystream/types/hiring/schemas/role.schema.typings';
 import ajv from 'ajv';
 import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring';
+import { Validator } from 'inquirer';
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -316,10 +317,28 @@ export class HRTStruct extends Struct implements WithJSONable<GenericJoyStreamRo
     }
 };
 
-// A mapping of argName to json struct and schemaValidator
-// It is used to map arguments of type "Bytes" that are in fact a json string
-// (and can be validated against a schema)
-export type JSONArgsMapping = { [argName: string]: {
-    struct: Constructor<Struct>,
-    schemaValidator: ajv.ValidateFunction
-} };
+// Api-related
+
+// Additional options that can be passed to ApiCommandBase.promptForParam in order to override
+// its default behaviour, change param name, add validation etc.
+export type ApiParamOptions<ParamType = Codec> = {
+    forcedName?: string,
+    value?: {
+        default: ParamType;
+        locked?: boolean;
+    }
+    jsonSchema?: {
+        struct: Constructor<Struct>;
+        schemaValidator: ajv.ValidateFunction;
+    }
+    validator?: Validator,
+    nestedOptions?: ApiParamsOptions // For more complex params, like structs
+};
+export type ApiParamsOptions = {
+    [paramName: string]: ApiParamOptions;
+}
+
+export type ApiMethodArg = Codec;
+export type ApiMethodNamedArgs = {
+    [argName: string]: ApiMethodArg;
+}

+ 70 - 52
cli/src/base/ApiCommandBase.ts

@@ -2,7 +2,6 @@ import ExitCodes from '../ExitCodes';
 import { CLIError } from '@oclif/errors';
 import StateAwareCommandBase from './StateAwareCommandBase';
 import Api from '../Api';
-import { JSONArgsMapping } from '../Types';
 import { getTypeDef, createType, Option, Tuple, Bytes } from '@polkadot/types';
 import { Codec, TypeDef, TypeDefInfo, Constructor } from '@polkadot/types/types';
 import { Vec, Struct, Enum } from '@polkadot/types/codec';
@@ -11,8 +10,8 @@ import { KeyringPair } from '@polkadot/keyring/types';
 import chalk from 'chalk';
 import { SubmittableResultImpl } from '@polkadot/api/types';
 import ajv from 'ajv';
-
-export type ApiMethodInputArg = Codec;
+import { ApiMethodArg, ApiMethodNamedArgs, ApiParamsOptions, ApiParamOptions } from '../Types';
+import { createParamOptions } from '../helpers/promptOptions';
 
 class ExtrinsicFailedError extends Error { };
 
@@ -63,21 +62,21 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     // Prompt for simple/plain value (provided as string) of given type
     async promptForSimple(
         typeDef: TypeDef,
-        defaultValue?: Codec,
-        validateFunc?: (input: any) => string | boolean
+        paramOptions?: ApiParamOptions
     ): Promise<Codec> {
         const providedValue = await this.simplePrompt({
             message: `Provide value for ${ this.paramName(typeDef) }`,
             type: 'input',
-            default: defaultValue?.toString(),
-            validate: validateFunc
+            default: paramOptions?.value?.default?.toString(),
+            validate: paramOptions?.validator
         });
         return createType(typeDef.type as any, providedValue);
     }
 
     // Prompt for Option<Codec> value
-    async promptForOption(typeDef: TypeDef, defaultValue?: Option<Codec>): Promise<Option<Codec>> {
+    async promptForOption(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Option<Codec>> {
         const subtype = <TypeDef> typeDef.sub; // We assume that Opion always has a single subtype
+        const defaultValue = paramOptions?.value?.default as Option<Codec> | undefined;
         const confirmed = await this.simplePrompt({
             message: `Do you want to provide the optional ${ this.paramName(typeDef) } parameter?`,
             type: 'confirm',
@@ -86,7 +85,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
         if (confirmed) {
             this.openIndentGroup();
-            const value = await this.promptForParam(subtype.type, subtype.name, defaultValue?.unwrapOr(undefined));
+            const value = await this.promptForParam(subtype.type, createParamOptions(subtype.name, defaultValue?.unwrapOr(undefined)));
             this.closeIndentGroup();
             return new Option(subtype.type as any, value);
         }
@@ -96,16 +95,18 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
     // Prompt for Tuple
     // TODO: Not well tested yet
-    async promptForTuple(typeDef: TypeDef, defaultValue: Tuple): Promise<Tuple> {
+    async promptForTuple(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Tuple> {
         console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } tuple:`));
 
         this.openIndentGroup();
-        const result: ApiMethodInputArg[] = [];
+        const result: ApiMethodArg[] = [];
         // We assume that for Tuple there is always at least 1 subtype (pethaps it's even always an array?)
         const subtypes: TypeDef[] = Array.isArray(typeDef.sub) ? typeDef.sub! : [ typeDef.sub! ];
+        const defaultValue = paramOptions?.value?.default as Tuple | undefined;
 
         for (const [index, subtype] of Object.entries(subtypes)) {
-            const inputParam = await this.promptForParam(subtype.type, subtype.name, defaultValue[parseInt(index)]);
+            const entryDefaultVal = defaultValue && defaultValue[parseInt(index)];
+            const inputParam = await this.promptForParam(subtype.type, createParamOptions(subtype.name, entryDefaultVal));
             result.push(inputParam);
         }
         this.closeIndentGroup();
@@ -114,7 +115,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     }
 
     // Prompt for Struct
-    async promptForStruct(typeDef: TypeDef, defaultValue?: Struct): Promise<ApiMethodInputArg> {
+    async promptForStruct(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<ApiMethodArg> {
         console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } struct:`));
 
         this.openIndentGroup();
@@ -122,11 +123,18 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         const rawTypeDef = this.getRawTypeDef(structType);
         // We assume struct typeDef always has array of typeDefs inside ".sub"
         const structSubtypes = rawTypeDef.sub as TypeDef[];
+        const structDefault = paramOptions?.value?.default as Struct | undefined;
 
-        const structValues: { [key: string]: ApiMethodInputArg } = {};
+        const structValues: { [key: string]: ApiMethodArg } = {};
         for (const subtype of structSubtypes) {
-            structValues[subtype.name!] =
-                await this.promptForParam(subtype.type, subtype.name, defaultValue && defaultValue.get(subtype.name!));
+            const fieldOptions = paramOptions?.nestedOptions && paramOptions.nestedOptions[subtype.name!];
+            const fieldDefaultValue = fieldOptions?.value?.default || (structDefault && structDefault.get(subtype.name!));
+            const finalFieldOptions: ApiParamOptions = {
+                ...fieldOptions,
+                forcedName: subtype.name,
+                value: fieldDefaultValue && { ...fieldOptions?.value, default: fieldDefaultValue }
+            }
+            structValues[subtype.name!] = await this.promptForParam(subtype.type, finalFieldOptions);
         }
         this.closeIndentGroup();
 
@@ -134,12 +142,13 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     }
 
     // Prompt for Vec
-    async promptForVec(typeDef: TypeDef, defaultValue?: Vec<Codec>): Promise<Vec<Codec>> {
+    async promptForVec(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Vec<Codec>> {
         console.log(chalk.grey(`Providing values for ${ this.paramName(typeDef) } vector:`));
 
         this.openIndentGroup();
         // We assume Vec always has one TypeDef as ".sub"
         const subtype = typeDef.sub as TypeDef;
+        const defaultValue = paramOptions?.value?.default as Vec<Codec> | undefined;
         let entries: Codec[] = [];
         let addAnother = false;
         do {
@@ -150,7 +159,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
             });
             const defaultEntryValue = defaultValue && defaultValue[entries.length];
             if (addAnother) {
-                entries.push(await this.promptForParam(subtype.type, subtype.name, defaultEntryValue));
+                entries.push(await this.promptForParam(subtype.type, createParamOptions(subtype.name, defaultEntryValue)));
             }
         } while (addAnother);
         this.closeIndentGroup();
@@ -159,11 +168,12 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     }
 
     // Prompt for Enum
-    async promptForEnum(typeDef: TypeDef, defaultValue?: Enum): Promise<Enum> {
+    async promptForEnum(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Enum> {
         const enumType = typeDef.type;
         const rawTypeDef = this.getRawTypeDef(enumType);
         // We assume enum always has array on TypeDefs inside ".sub"
         const enumSubtypes = rawTypeDef.sub as TypeDef[];
+        const defaultValue = paramOptions?.value?.default as Enum | undefined;
 
         const enumSubtypeName = await this.simplePrompt({
             message: `Choose value for ${this.paramName(typeDef)}:`,
@@ -178,9 +188,10 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         const enumSubtype = enumSubtypes.find(st => st.name === enumSubtypeName)!;
 
         if (enumSubtype.type !== 'Null') {
+            const subtypeOptions = createParamOptions(enumSubtype.name, defaultValue?.value);
             return createType(
                 enumType as any,
-                { [enumSubtype.name!]: await this.promptForParam(enumSubtype.type, enumSubtype.name, defaultValue?.value) }
+                { [enumSubtype.name!]: await this.promptForParam(enumSubtype.type, subtypeOptions) }
             );
         }
 
@@ -191,34 +202,46 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     // TODO: This may not yet work for all possible types
     async promptForParam(
         paramType: string,
-        forcedName?: string,
-        defaultValue?: ApiMethodInputArg,
-        validateFunc?: (input: any) => string | boolean // TODO: Currently only works with "promptForSimple"
-    ): Promise<ApiMethodInputArg> {
+        paramOptions?: ApiParamOptions // TODO: This is not fully implemented for all types yet
+    ): Promise<ApiMethodArg> {
         const typeDef = getTypeDef(paramType);
         const rawTypeDef = this.getRawTypeDef(paramType);
 
-        if (forcedName) {
-            typeDef.name = forcedName;
+        if (paramOptions?.forcedName) {
+            typeDef.name = paramOptions.forcedName;
+        }
+
+        if (paramOptions?.value?.locked) {
+            return paramOptions.value.default;
+        }
+
+        if (paramOptions?.jsonSchema) {
+            const { struct, schemaValidator } = paramOptions.jsonSchema;
+            return await this.promptForJsonBytes(
+                struct,
+                typeDef.name,
+                paramOptions.value?.default as Bytes | undefined,
+                schemaValidator
+            );
         }
 
         if (rawTypeDef.info === TypeDefInfo.Option) {
-            return await this.promptForOption(typeDef, defaultValue as Option<Codec>);
+            return await this.promptForOption(typeDef, paramOptions);
         }
         else if (rawTypeDef.info === TypeDefInfo.Tuple) {
-            return await this.promptForTuple(typeDef, defaultValue as Tuple);
+            return await this.promptForTuple(typeDef, paramOptions);
         }
         else if (rawTypeDef.info === TypeDefInfo.Struct) {
-            return await this.promptForStruct(typeDef, defaultValue as Struct);
+            return await this.promptForStruct(typeDef, paramOptions);
         }
         else if (rawTypeDef.info === TypeDefInfo.Enum) {
-            return await this.promptForEnum(typeDef, defaultValue as Enum);
+            return await this.promptForEnum(typeDef, paramOptions);
         }
         else if (rawTypeDef.info === TypeDefInfo.Vec) {
-            return await this.promptForVec(typeDef, defaultValue as Vec<Codec>);
+            return await this.promptForVec(typeDef, paramOptions);
         }
         else {
-            return await this.promptForSimple(typeDef, defaultValue, validateFunc);
+            return await this.promptForSimple(typeDef, paramOptions);
         }
     }
 
@@ -241,7 +264,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
         let isValid: boolean = true, jsonText: string;
         do {
-            const structVal = await this.promptForStruct(typeDef, defaultStruct);
+            const structVal = await this.promptForStruct(typeDef, createParamOptions(typeDef.name, defaultStruct));
             jsonText = JSON.stringify(structVal.toJSON());
             if (schemaValidator) {
                 isValid = Boolean(schemaValidator(JSON.parse(jsonText)));
@@ -263,24 +286,20 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     async promptForExtrinsicParams(
         module: string,
         method: string,
-        jsonArgs?: JSONArgsMapping,
-        defaultValues?: ApiMethodInputArg[]
-    ): Promise<ApiMethodInputArg[]> {
+        paramsOptions?: ApiParamsOptions
+    ): Promise<ApiMethodArg[]> {
         const extrinsicMethod = this.getOriginalApi().tx[module][method];
-        let values: ApiMethodInputArg[] = [];
+        let values: ApiMethodArg[] = [];
 
         this.openIndentGroup();
-        for (const [index, arg] of Object.entries(extrinsicMethod.meta.args.toArray())) {
+        for (const arg of extrinsicMethod.meta.args.toArray()) {
             const argName = arg.name.toString();
             const argType = arg.type.toString();
-            const defaultValue = defaultValues && defaultValues[parseInt(index)];
-            if (jsonArgs && jsonArgs[argName]) {
-                const { struct, schemaValidator } = jsonArgs[argName];
-                values.push(await this.promptForJsonBytes(struct, argName, defaultValue as Bytes, schemaValidator));
-            }
-            else {
-                values.push(await this.promptForParam(argType, argName, defaultValue));
+            let argOptions = paramsOptions && paramsOptions[argName];
+            if (!argOptions?.forcedName) {
+                argOptions = { ...argOptions, forcedName: argName };
             }
+            values.push(await this.promptForParam(argType, argOptions));
         };
         this.closeIndentGroup();
 
@@ -346,18 +365,17 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         account: KeyringPair,
         module: string,
         method: string,
-        jsonArgs?: JSONArgsMapping, // Special JSON arguments (ie. human_readable_text of working group opening)
-        defaultValues?: ApiMethodInputArg[],
+        paramsOptions: ApiParamsOptions,
         warnOnly: boolean = false // If specified - only warning will be displayed (instead of error beeing thrown)
-    ): Promise<ApiMethodInputArg[]> {
-        const params = await this.promptForExtrinsicParams(module, method, jsonArgs, defaultValues);
+    ): Promise<ApiMethodArg[]> {
+        const params = await this.promptForExtrinsicParams(module, method, paramsOptions);
         await this.sendAndFollowExtrinsic(account, module, method, params, warnOnly);
 
         return params;
     }
 
-    extrinsicArgsFromDraft(module: string, method: string, draftFilePath: string): ApiMethodInputArg[] {
-        let draftJSONObj, parsedArgs: ApiMethodInputArg[] = [];
+    extrinsicArgsFromDraft(module: string, method: string, draftFilePath: string): ApiMethodNamedArgs {
+        let draftJSONObj, parsedArgs: ApiMethodNamedArgs = {};
         const extrinsicMethod = this.getOriginalApi().tx[module][method];
         try {
             draftJSONObj = require(draftFilePath);
@@ -375,7 +393,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
             const argName = arg.name.toString();
             const argType = arg.type.toString();
             try {
-                parsedArgs.push(createType(argType as any, draftJSONObj[parseInt(index)]));
+                parsedArgs[argName] = createType(argType as any, draftJSONObj[parseInt(index)]);
             } catch (e) {
                 throw new CLIError(`Couldn't parse ${argName} value from draft at ${draftFilePath}!`, { exit: ExitCodes.InvalidFile });
             }

+ 3 - 4
cli/src/base/WorkingGroupsCommandBase.ts

@@ -1,10 +1,9 @@
 import ExitCodes from '../ExitCodes';
 import AccountsCommandBase from './AccountsCommandBase';
 import { flags } from '@oclif/command';
-import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening } from '../Types';
+import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening, ApiMethodArg, ApiMethodNamedArgs } from '../Types';
 import { apiModuleByGroup } from '../Api';
 import { CLIError } from '@oclif/errors';
-import { ApiMethodInputArg } from './ApiCommandBase';
 import fs from 'fs';
 import path from 'path';
 import _ from 'lodash';
@@ -154,7 +153,7 @@ export default abstract class WorkingGroupsCommandBase extends AccountsCommandBa
         return selectedDraftName;
     }
 
-    loadOpeningDraftParams(draftName: string) {
+    loadOpeningDraftParams(draftName: string): ApiMethodNamedArgs {
         const draftFilePath = this.getOpeningDraftPath(draftName);
         const params = this.extrinsicArgsFromDraft(
             apiModuleByGroup[this.group],
@@ -173,7 +172,7 @@ export default abstract class WorkingGroupsCommandBase extends AccountsCommandBa
         return path.join(this.getOpeingDraftsPath(), _.snakeCase(draftName)+'.json');
     }
 
-    saveOpeningDraft(draftName: string, params: ApiMethodInputArg[]) {
+    saveOpeningDraft(draftName: string, params: ApiMethodArg[]) {
         const paramsJson = JSON.stringify(
             params.map(p => p.toJSON()),
             null,

+ 5 - 5
cli/src/commands/api/inspect.ts

@@ -7,8 +7,8 @@ import { Codec } from '@polkadot/types/types';
 import { ConstantCodec } from '@polkadot/api-metadata/consts/types';
 import ExitCodes from '../../ExitCodes';
 import chalk from 'chalk';
-import { NameValueObj } from '../../Types';
-import ApiCommandBase, { ApiMethodInputArg } from '../../base/ApiCommandBase';
+import { NameValueObj, ApiMethodArg } from '../../Types';
+import ApiCommandBase from '../../base/ApiCommandBase';
 
 // Command flags type
 type ApiInspectFlags = {
@@ -148,8 +148,8 @@ export default class ApiInspect extends ApiCommandBase {
     }
 
     // Request values for params using array of param types (strings)
-    async requestParamsValues(paramTypes: string[]): Promise<ApiMethodInputArg[]> {
-        let result: ApiMethodInputArg[] = [];
+    async requestParamsValues(paramTypes: string[]): Promise<ApiMethodArg[]> {
+        let result: ApiMethodArg[] = [];
         for (let [key, paramType] of Object.entries(paramTypes)) {
             this.log(chalk.bold.white(`Parameter no. ${ parseInt(key)+1 } (${ paramType }):`));
             let paramValue = await this.promptForParam(paramType);
@@ -177,7 +177,7 @@ export default class ApiInspect extends ApiCommandBase {
 
             if (apiType === 'query') {
                 // Api query - call with (or without) arguments
-                let args: (string | ApiMethodInputArg)[] = flags.callArgs ? flags.callArgs.split(',') : [];
+                let args: (string | ApiMethodArg)[] = flags.callArgs ? flags.callArgs.split(',') : [];
                 const paramsTypes: string[] = this.getQueryMethodParamsTypes(apiModule, apiMethod);
                 if (args.length < paramsTypes.length) {
                     this.warn('Some parameters are missing! Please, provide the missing parameters:');

+ 10 - 20
cli/src/commands/working-groups/createOpening.ts

@@ -1,10 +1,10 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase';
-import { HRTStruct } from '../../Types';
+import { ApiMethodArg, ApiMethodNamedArgs } from '../../Types';
 import chalk from 'chalk';
 import { flags } from '@oclif/command';
-import { ApiMethodInputArg } from '../../base/ApiCommandBase';
-import { schemaValidator } from '@joystream/types/hiring';
 import { apiModuleByGroup } from '../../Api';
+import WorkerOpeningOptions from '../../promptOptions/addWorkerOpening';
+import { setDefaults } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase {
     static description = 'Create working group opening (requires lead access)';
@@ -43,35 +43,25 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
 
         const { flags } = this.parse(WorkingGroupsCreateOpening);
 
-        let defaultValues: ApiMethodInputArg[] | undefined = undefined;
+        let promptOptions = new WorkerOpeningOptions(), defaultValues: ApiMethodNamedArgs | undefined;
         if (flags.useDraft) {
             const draftName = flags.draftName || await this.promptForOpeningDraft();
-            defaultValues =  await this.loadOpeningDraftParams(draftName);
+            defaultValues = await this.loadOpeningDraftParams(draftName);
+            setDefaults(promptOptions, defaultValues);
         }
 
         if (!flags.skipPrompts) {
             const module = apiModuleByGroup[this.group];
             const method = 'addOpening';
-            const jsonArgsMapping = { 'human_readable_text': { struct: HRTStruct, schemaValidator } };
 
-            let saveDraft = false, params: ApiMethodInputArg[];
+            let saveDraft = false, params: ApiMethodArg[];
             if (flags.createDraftOnly) {
-                params = await this.promptForExtrinsicParams(module, method, jsonArgsMapping, defaultValues);
+                params = await this.promptForExtrinsicParams(module, method, promptOptions);
                 saveDraft = true;
             }
             else {
                 await this.requestAccountDecoding(account); // Prompt for password
-
-                params = await this.buildAndSendExtrinsic(
-                    account,
-                    module,
-                    method,
-                    jsonArgsMapping,
-                    defaultValues,
-                    true
-                );
-
-                this.log(chalk.green('Opening succesfully created!'));
+                params = await this.buildAndSendExtrinsic(account, module, method, promptOptions, true);
 
                 saveDraft = await this.simplePrompt({
                     message: 'Do you wish to save this opening as draft?',
@@ -89,7 +79,7 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
         else {
             await this.requestAccountDecoding(account); // Prompt for password
             this.log(chalk.white('Sending the extrinsic...'));
-            await this.sendExtrinsic(account, apiModuleByGroup[this.group], 'addOpening', defaultValues!);
+            await this.sendExtrinsic(account, apiModuleByGroup[this.group], 'addOpening', Object.values(defaultValues!));
             this.log(chalk.green('Opening succesfully created!'));
         }
     }

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

@@ -7,6 +7,7 @@ import { formatBalance } from '@polkadot/util';
 import { minMaxInt } from '../../validators/common';
 import chalk from 'chalk';
 import ExitCodes from '../../ExitCodes';
+import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsCommandBase {
     static description =
@@ -40,7 +41,7 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
 
         this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)));
         const balanceValidator = minMaxInt(1, groupMember.stake.toNumber());
-        const balance = await this.promptForParam('Balance', 'amount', undefined, balanceValidator) as Balance;
+        const balance = await this.promptForParam('Balance', createParamOptions('amount', undefined, balanceValidator)) as Balance;
 
         await this.requestAccountDecoding(account);
 

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

@@ -5,6 +5,7 @@ import { WorkerId } from '@joystream/types/lib/working-group';
 import { bool } from '@polkadot/types/primitive';
 import { formatBalance } from '@polkadot/util';
 import chalk from 'chalk';
+import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
     static description = 'Evicts given worker. Requires lead access.';
@@ -30,7 +31,7 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
         // This will also make sure the worker is valid
         const groupMember = await this.getApi().groupMember(this.group, workerId);
 
-        const rationale = await this.promptForParam('Bytes', 'rationale');  // TODO: Terminate worker text limits? (minMaxStr)
+        const rationale = await this.promptForParam('Bytes', createParamOptions('rationale'));  // TODO: Terminate worker text limits? (minMaxStr)
         const shouldSlash = groupMember.stake
             ?
                 await this.simplePrompt({

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

@@ -7,6 +7,7 @@ import { OpeningId } from '@joystream/types/hiring';
 import { ApplicationIdSet } from '@joystream/types/working-group';
 import { RewardPolicy } from '@joystream/types/content-working-group';
 import chalk from 'chalk';
+import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
     static description = 'Allows filling working group opening that\'s currently in review. Requires lead access.';
@@ -35,7 +36,7 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
         }
 
         const applicationIds = await this.promptForApplicationsToAccept(opening);
-        const rewardPolicyOpt = await this.promptForParam(`Option<${RewardPolicy.name}>`, 'RewardPolicy');
+        const rewardPolicyOpt = await this.promptForParam(`Option<${RewardPolicy.name}>`, createParamOptions('RewardPolicy'));
 
         await this.requestAccountDecoding(account);
 

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

@@ -6,6 +6,7 @@ import { formatBalance } from '@polkadot/util';
 import { positiveInt } from '../../validators/common';
 import chalk from 'chalk';
 import ExitCodes from '../../ExitCodes';
+import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsIncreaseStake extends WorkingGroupsCommandBase {
     static description =
@@ -24,7 +25,7 @@ export default class WorkingGroupsIncreaseStake extends WorkingGroupsCommandBase
         }
 
         this.log(chalk.white('Current stake: ', formatBalance(worker.stake)));
-        const balance = await this.promptForParam('Balance', 'amount', undefined, positiveInt()) as Balance;
+        const balance = await this.promptForParam('Balance', createParamOptions('amount', undefined, positiveInt())) as Balance;
 
         await this.requestAccountDecoding(account);
 

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

@@ -3,6 +3,7 @@ import _ from 'lodash';
 import { apiModuleByGroup } from '../../Api';
 import { minMaxStr } from '../../validators/common';
 import chalk from 'chalk';
+import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
     static description = 'Leave the role associated with currently selected account.';
@@ -17,7 +18,7 @@ export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
 
         const constraint = await this.getApi().workerExitRationaleConstraint(this.group);
         const rationaleValidator = minMaxStr(constraint.min.toNumber(), constraint.max.toNumber());
-        const rationale = await this.promptForParam('Bytes', 'rationale', undefined, rationaleValidator);
+        const rationale = await this.promptForParam('Bytes', createParamOptions('rationale', undefined, rationaleValidator));
 
         await this.requestAccountDecoding(account);
 

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

@@ -7,6 +7,7 @@ import { formatBalance } from '@polkadot/util';
 import { minMaxInt } from '../../validators/common';
 import chalk from 'chalk';
 import ExitCodes from '../../ExitCodes';
+import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
     static description = 'Slashes given worker stake. Requires lead access.';
@@ -38,7 +39,7 @@ export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
 
         this.log(chalk.white('Current worker stake: ', formatBalance(groupMember.stake)));
         const balanceValidator = minMaxInt(1, groupMember.stake.toNumber());
-        const balance = await this.promptForParam('Balance', 'amount', undefined, balanceValidator) as Balance;
+        const balance = await this.promptForParam('Balance', createParamOptions('amount', undefined, balanceValidator)) as Balance;
 
         await this.requestAccountDecoding(account);
 

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

@@ -6,6 +6,7 @@ import { formatBalance } from '@polkadot/util';
 import chalk from 'chalk';
 import { Reward } from '../../Types';
 import { positiveInt } from '../../validators/common';
+import { createParamOptions } from '../../helpers/promptOptions';
 
 export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsCommandBase {
     static description = 'Change given worker\'s reward (amount only). Requires lead access.';
@@ -44,7 +45,7 @@ export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsComman
         const { reward } = groupMember;
         console.log(chalk.white(`Current worker reward: ${this.formatReward(reward)}`));
 
-        const newRewardValue = await this.promptForParam('BalanceOfMint', 'new_amount', undefined, positiveInt());
+        const newRewardValue = await this.promptForParam('BalanceOfMint', createParamOptions('new_amount', undefined, positiveInt()));
 
         await this.requestAccountDecoding(account);
 

+ 27 - 0
cli/src/helpers/promptOptions.ts

@@ -0,0 +1,27 @@
+import { ApiParamsOptions, ApiMethodNamedArgs, ApiParamOptions, ApiMethodArg } from '../Types'
+import { Validator } from 'inquirer';
+
+export function setDefaults(promptOptions: ApiParamsOptions, defaultValues: ApiMethodNamedArgs) {
+    for (const [paramName, defaultValue] of Object.entries(defaultValues)) {
+        const paramOptions = promptOptions[paramName];
+        if (paramOptions && paramOptions.value) {
+            paramOptions.value.default = defaultValue;
+        }
+        else if (paramOptions) {
+            promptOptions[paramName].value = { default: defaultValue };
+        }
+        else {
+            promptOptions[paramName] = { value: { default: defaultValue } };
+        }
+    }
+}
+
+// Temporary(?) helper for easier creation of common ApiParamOptions
+export function createParamOptions(forcedName?: string, defaultValue?: ApiMethodArg | undefined, validator?: Validator): ApiParamOptions {
+    const paramOptions: ApiParamOptions = { forcedName, validator };
+    if (defaultValue) {
+        paramOptions.value = { default: defaultValue };
+    }
+
+    return paramOptions;
+}

+ 39 - 0
cli/src/promptOptions/addWorkerOpening.ts

@@ -0,0 +1,39 @@
+import { ApiParamsOptions, ApiParamOptions, HRTStruct } from '../Types';
+import { OpeningType, OpeningTypeKeys, SlashingTerms, UnslashableTerms } from '@joystream/types/working-group';
+import { Bytes } from '@polkadot/types';
+import { schemaValidator } from '@joystream/types/hiring';
+import { WorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group';
+
+class OpeningPolicyCommitmentOptions implements ApiParamsOptions {
+    [paramName: string]: ApiParamOptions;
+    public role_slashing_terms: ApiParamOptions<SlashingTerms> = {
+        value: {
+            default: SlashingTerms.create('Unslashable', new UnslashableTerms()),
+            locked: true
+        }
+    }
+}
+
+class AddWrokerOpeningOptions implements ApiParamsOptions {
+    [paramName: string]: ApiParamOptions;
+    // Lock value for opening_type
+    public opening_type: ApiParamOptions<OpeningType> = {
+        value: {
+            default: new OpeningType(OpeningTypeKeys.Worker), // TODO: Use JoyEnum
+            locked: true
+        }
+    };
+    // Json schema for human_readable_text
+    public human_readable_text: ApiParamOptions<Bytes> = {
+        jsonSchema: {
+            schemaValidator,
+            struct: HRTStruct
+        }
+    }
+    // Lock value for role_slashing_terms
+    public commitment: ApiParamOptions<WorkingGroupOpeningPolicyCommitment> = {
+        nestedOptions: new OpeningPolicyCommitmentOptions()
+    }
+};
+
+export default AddWrokerOpeningOptions;

+ 9 - 2
types/src/JoyEnum.ts

@@ -7,15 +7,22 @@ export interface ExtendedEnum<Types extends Record<string, Constructor>> extends
   asType<TypeKey extends keyof Types>(type: TypeKey): InstanceType<Types[TypeKey]>;
 };
 
+export interface ExtendedEnumConstructor<Types extends Record<string, Constructor>> extends EnumConstructor<ExtendedEnum<Types>> {
+  create<TypeKey extends keyof Types>(typeKey: keyof Types, value: InstanceType<Types[TypeKey]>): ExtendedEnum<Types>;
+}
+
 // Helper for creating extended Enum type with TS-compatible isOfType and asType helpers
-export function JoyEnum<Types extends Record<string, Constructor>>(types: Types): EnumConstructor<ExtendedEnum<Types>>
+export function JoyEnum<Types extends Record<string, Constructor>>(types: Types): ExtendedEnumConstructor<Types>
 {
   // Unique values check
   if (Object.values(types).some((val, i) => Object.values(types).indexOf(val, i + 1) !== -1)) {
     throw new Error('Values passed to JoyEnum are not unique. Create an individual class for each value.');
   }
 
-  return class extends Enum {
+  return class JoyEnumObject extends Enum {
+    public static create<TypeKey extends keyof Types>(typeKey: keyof Types, value: InstanceType<Types[TypeKey]>) {
+      return new JoyEnumObject({ [typeKey]: value });
+    }
     constructor(value?: any, index?: number) {
       super(types, value, index);
     }

+ 7 - 10
types/src/working-group/index.ts

@@ -6,6 +6,7 @@ import { MemberId, ActorId } from '../members';
 import { RewardRelationshipId } from '../recurring-rewards';
 import { StakeId } from '../stake';
 import { ApplicationId, OpeningId, ApplicationRationingPolicy, StakingPolicy } from '../hiring';
+import { JoyEnum } from '../JoyEnum';
 
 export class RationaleText extends Bytes { };
 
@@ -138,17 +139,13 @@ export class SlashableTerms extends JoyStruct<ISlashableTerms> {
   }
 };
 
+export class UnslashableTerms extends Null { };
+
 // This type is also defined in /content-working-group (as above)
-export class SlashingTerms extends Enum {
-  constructor (value?: any, index?: number) {
-    super(
-      {
-        Unslashable: Null,
-        Slashable: SlashableTerms,
-      },
-      value, index);
-  }
-};
+export class SlashingTerms extends JoyEnum({
+  Unslashable: UnslashableTerms,
+  Slashable: SlashableTerms
+} as const) { };
 
 export type IWorkingGroupOpeningPolicyCommitment = {
   application_rationing_policy: Option<ApplicationRationingPolicy>,