Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sudo commands for hiring Leads #2807

Draft
wants to merge 1 commit into
base: giza_staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions cli/src/base/ApiCommandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
return this.getApi().getUnaugmentedApi()
}

async sendAndFollowNamedSudoTx<
Module extends keyof AugmentedSubmittables<'promise'>,
Method extends keyof AugmentedSubmittables<'promise'>[Module] & string,
Submittable extends AugmentedSubmittables<'promise'>[Module][Method]
>(
sudoAccount: KeyringPair,
module: Module,
method: Method,
params: Submittable extends (...args: any[]) => any ? Parameters<Submittable> : []
): Promise<SubmittableResult> {
this.log(
chalk.magentaBright(
`\nSending ${module}.${method} extrinsic from ${sudoAccount.meta.name ? sudoAccount.meta.name : sudoAccount.address}...`
)
)
const tx = await this.getUnaugmentedApi().tx[module][method](...params)
const sudoTx = await this.getUnaugmentedApi().tx.sudo.sudo(tx)
return await this.sendAndFollowTx(sudoAccount, sudoTx) //, warnOnly)
}

getTypesRegistry(): Registry {
return this.getOriginalApi().registry
}
Expand Down
14 changes: 14 additions & 0 deletions cli/src/base/WorkingGroupsCommandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ export default abstract class WorkingGroupsCommandBase extends RolesCommandBase
return acceptedApplications
}

async getOpening(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
const opening = await this.getApi().groupOpening(this.group, id)

if (requiredStatus && opening.stage.status !== requiredStatus) {
this.error(
`The opening needs to be in "${_.startCase(requiredStatus)}" stage! ` +
`This one is: "${_.startCase(opening.stage.status)}"`,
{ exit: ExitCodes.InvalidInput }
)
}

return opening
}

async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
const opening = await this.getApi().groupOpening(this.group, id)

Expand Down
45 changes: 36 additions & 9 deletions cli/src/commands/working-groups/createOpening.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
'(can be used to generate a "draft" which can be provided as input later)',
dependsOn: ['output'],
}),
sudo: flags.boolean({
char: 's',
required: false,
hidden: true,
description:
'Wrappes the command in sudo',
}),
}

getHRTDefaults(memberHandle: string): HRTJson {
Expand Down Expand Up @@ -111,7 +118,7 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
}

async promptForData(
lead: GroupMember,
lead: GroupMember | string,
rememberedInput?: [WGOpeningJson, HRTJson]
): Promise<[WGOpeningJson, HRTJson]> {
const openingDefaults = rememberedInput?.[0]
Expand All @@ -120,8 +127,12 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
openingDefaults
)
const wgOpeningJson = await openingPrompt.promptAll()
let profile = "Sudo"
if (typeof lead !== "string") {
profile = lead.profile.handle.toString()
}

const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults(lead.profile.handle.toString())
const hrtDefaults = rememberedInput?.[1] || this.getHRTDefaults(profile)
this.log(`Values for ${chalk.greenBright('human_readable_text')} json:`)
const hrtPropmpt = new JsonSchemaPrompter<HRTJson>((HRTSchema as unknown) as JSONSchema, hrtDefaults)
// Prompt only for 'headline', 'job', 'application', 'reward' and 'process', leave the rest default
Expand Down Expand Up @@ -163,12 +174,11 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase

async run() {
const account = await this.getRequiredSelectedAccount()
// lead-only gate
const lead = await this.getRequiredLead()

await this.requestAccountDecoding(account) // Prompt for password

const {
flags: { input, output, edit, dryRun },
flags: { input, output, edit, dryRun, sudo },
} = this.parse(WorkingGroupsCreateOpening)

ensureOutputFileIsWriteable(output)
Expand All @@ -179,6 +189,11 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
if (edit) {
rememberedInput = await this.getInputFromFile(input as string)
}
let lead: string | GroupMember = "Sudo"
if (!sudo) {
// lead-only gate
lead = await this.getRequiredLead()
}
// Either prompt for the data or get it from input file
const [openingJson, hrtJson] =
!input || edit || tryAgain
Expand All @@ -190,6 +205,9 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase

// Generate and ask to confirm tx params
const txParams = this.createTxParams(openingJson, hrtJson)
if (sudo) {
txParams[3] = createTypeFromConstructor(OpeningType, 'Leader')
}
this.jsonPrettyPrint(JSON.stringify(txParams))
const confirmed = await this.simplePrompt({
type: 'confirm',
Expand Down Expand Up @@ -217,10 +235,19 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
// Send the tx
this.log(chalk.magentaBright('Sending the extrinsic...'))
try {
await this.sendAndFollowTx(
account,
this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...txParams)
)
if (!sudo) {
await this.sendAndFollowTx(
account,
this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...txParams)
)
} else {
await this.sendAndFollowNamedSudoTx(
account,
apiModuleByGroup[this.group],
'addOpening',
txParams,
)
}
this.log(chalk.green('Opening successfully created!'))
tryAgain = false
} catch (e) {
Expand Down
79 changes: 66 additions & 13 deletions cli/src/commands/working-groups/fillOpening.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
import { OpeningStatus } from '../../Types'
import { GroupOpening, OpeningStatus } from '../../Types'
import { apiModuleByGroup } from '../../Api'
import chalk from 'chalk'
import { flags } from '@oclif/command'
import { createParamOptions } from '../../helpers/promptOptions'
import { createType } from '@joystream/types'
import { IRewardPolicy } from '@joystream/types/working-group'
import { Codec } from '@polkadot/types/types'

export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
static description = "Allows filling working group opening that's currently in review. Requires lead access."
static args = [
Expand All @@ -16,28 +20,77 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {

static flags = {
...WorkingGroupsCommandBase.flags,
applicantIds: flags.integer({
char: 'a',
required: false,
multiple: true,
description: 'List of applicants to hire, eg. 1 2 3',
}),
rewardPolicy: flags.integer({
char: 'r',
multiple: true,
required: false,
description: 'Set the Recurring Reward Policy, eg. [amount] [nextpayment] <frequency>',
}),
sudo: flags.boolean({
char: 's',
required: false,
hidden: true,
description:
'Wrappes the command in sudo',
}),
}

async run() {
const { args } = this.parse(WorkingGroupsFillOpening)

const account = await this.getRequiredSelectedAccount()
// Lead-only gate
await this.getRequiredLead()
const {
flags: { applicantIds, rewardPolicy, sudo },
} = this.parse(WorkingGroupsFillOpening)

const account = await this.getRequiredSelectedAccount()
const openingId = parseInt(args.wgOpeningId)
const opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview)
let opening: GroupOpening

if (!sudo) {
// Lead-only gate
await this.getRequiredLead()
opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview)
} else {
opening = await this.getOpening(openingId, OpeningStatus.InReview)
}

const applicationIds = await this.promptForApplicationsToAccept(opening)
const rewardPolicyOpt = await this.promptForParam(`Option<RewardPolicy>`, createParamOptions('RewardPolicy'))
let applicationIds: number[] = []
if (!applicantIds) {
applicationIds = await this.promptForApplicationsToAccept(opening)
} else {
applicationIds = applicantIds
}
let rewardPolicyOpt: IRewardPolicy | Codec
if (rewardPolicy.length >= 2) {
rewardPolicyOpt = {
amount_per_payout: createType('u128', rewardPolicy[0]),
next_payment_at_block: createType('u32', rewardPolicy[1]),
payout_interval: createType('Option<u32>',rewardPolicy[2]),
}
} else {
rewardPolicyOpt = await this.promptForParam(`Option<RewardPolicy>`, createParamOptions('RewardPolicy'))
}

await this.requestAccountDecoding(account)

await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'fillOpening', [
openingId,
createType('BTreeSet<ApplicationId>', applicationIds),
rewardPolicyOpt,
])
if (!sudo) {
await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'fillOpening', [
openingId,
createType('BTreeSet<ApplicationId>', applicationIds),
rewardPolicyOpt,
])
} else {
await this.sendAndFollowNamedSudoTx(account, apiModuleByGroup[this.group], 'fillOpening', [
openingId,
createType('BTreeSet<ApplicationId>', applicationIds),
rewardPolicyOpt,
])
}

this.log(chalk.green(`Opening ${chalk.magentaBright(openingId)} successfully filled!`))
this.log(
Expand Down
28 changes: 20 additions & 8 deletions cli/src/commands/working-groups/startReviewPeriod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
import { OpeningStatus } from '../../Types'
import { apiModuleByGroup } from '../../Api'
import chalk from 'chalk'
import { flags } from '@oclif/command'

export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommandBase {
static description = 'Changes the status of active opening to "In review". Requires lead access.'
Expand All @@ -15,21 +16,32 @@ export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommand

static flags = {
...WorkingGroupsCommandBase.flags,
sudo: flags.boolean({
char: 's',
required: false,
hidden: true,
description:
'Wrappes the command in sudo',
}),
}

async run() {
const { args } = this.parse(WorkingGroupsStartReviewPeriod)
const { flags: { sudo } } = this.parse(WorkingGroupsStartReviewPeriod)

const account = await this.getRequiredSelectedAccount()
// Lead-only gate
await this.getRequiredLead()


const openingId = parseInt(args.wgOpeningId)
await this.validateOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications)

await this.requestAccountDecoding(account)

await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId])
if (!sudo) {
// Lead-only gate
await this.getRequiredLead()
await this.requestAccountDecoding(account)
await this.validateOpeningForLeadAction(openingId, OpeningStatus.AcceptingApplications)
await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId])
} else {
await this.requestAccountDecoding(account)
await this.sendAndFollowNamedSudoTx(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId])
}

this.log(
chalk.green(`Opening ${chalk.magentaBright(openingId)} status changed to: ${chalk.magentaBright('In Review')}`)
Expand Down