createOpening.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
  2. import { GroupMember } from '../../Types'
  3. import chalk from 'chalk'
  4. import { apiModuleByGroup } from '../../Api'
  5. import { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
  6. import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
  7. import OpeningParamsSchema from '../../json-schemas/WorkingGroupOpening.schema.json'
  8. import { WorkingGroupOpening as OpeningParamsJson } from '../../json-schemas/typings/WorkingGroupOpening.schema'
  9. import { IOFlags, getInputJson, ensureOutputFileIsWriteable, saveOutputJsonToFile } from '../../helpers/InputOutput'
  10. import ExitCodes from '../../ExitCodes'
  11. import { flags } from '@oclif/command'
  12. import { AugmentedSubmittables } from '@polkadot/api/types'
  13. import { formatBalance } from '@polkadot/util'
  14. import BN from 'bn.js'
  15. const OPENING_STAKE = new BN(2000)
  16. export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase {
  17. static description = 'Create working group opening (requires lead access)'
  18. static flags = {
  19. ...WorkingGroupsCommandBase.flags,
  20. input: IOFlags.input,
  21. output: flags.string({
  22. char: 'o',
  23. required: false,
  24. description: 'Path to the file where the output JSON should be saved (this output can be then reused as input)',
  25. }),
  26. edit: flags.boolean({
  27. char: 'e',
  28. required: false,
  29. description:
  30. 'If provided along with --input - launches in edit mode allowing to modify the input before sending the exstinsic',
  31. dependsOn: ['input'],
  32. }),
  33. dryRun: flags.boolean({
  34. required: false,
  35. description:
  36. 'If provided along with --output - skips sending the actual extrinsic' +
  37. '(can be used to generate a "draft" which can be provided as input later)',
  38. dependsOn: ['output'],
  39. }),
  40. }
  41. createTxParams(
  42. openingParamsJson: OpeningParamsJson
  43. ): Parameters<AugmentedSubmittables<'promise'>['membershipWorkingGroup']['addOpening']> {
  44. return [
  45. openingParamsJson.description,
  46. 'Regular',
  47. {
  48. stake_amount: openingParamsJson.stakingPolicy.amount,
  49. leaving_unstaking_period: openingParamsJson.stakingPolicy.unstakingPeriod,
  50. },
  51. // TODO: Proper bigint handling?
  52. openingParamsJson.rewardPerBlock?.toString() || null,
  53. ]
  54. }
  55. async promptForData(lead: GroupMember, rememberedInput?: OpeningParamsJson): Promise<OpeningParamsJson> {
  56. const openingDefaults = rememberedInput
  57. const openingPrompt = new JsonSchemaPrompter<OpeningParamsJson>(
  58. (OpeningParamsSchema as unknown) as JSONSchema,
  59. openingDefaults
  60. )
  61. const openingParamsJson = await openingPrompt.promptAll()
  62. return openingParamsJson
  63. }
  64. async getInputFromFile(filePath: string): Promise<OpeningParamsJson> {
  65. const inputParams = await getInputJson<OpeningParamsJson>(filePath, (OpeningParamsSchema as unknown) as JSONSchema)
  66. return inputParams as OpeningParamsJson
  67. }
  68. async promptForStakeTopUp(stakingAccount: string): Promise<void> {
  69. this.log(`You need to stake ${chalk.bold(formatBalance(OPENING_STAKE))} in order to create a new opening.`)
  70. const [balances] = await this.getApi().getAccountsBalancesInfo([stakingAccount])
  71. const missingBalance = OPENING_STAKE.sub(balances.availableBalance)
  72. if (missingBalance.gtn(0)) {
  73. await this.requireConfirmation(
  74. `Do you wish to transfer remaining ${chalk.bold(
  75. formatBalance(missingBalance)
  76. )} to your staking account? (${stakingAccount})`
  77. )
  78. const account = await this.promptForAccount('Choose account to transfer the funds from')
  79. await this.sendAndFollowNamedTx(await this.getDecodedPair(account), 'balances', 'transferKeepAlive', [
  80. stakingAccount,
  81. missingBalance,
  82. ])
  83. }
  84. }
  85. async run() {
  86. // lead-only gate
  87. const lead = await this.getRequiredLeadContext()
  88. const {
  89. flags: { input, output, edit, dryRun },
  90. } = this.parse(WorkingGroupsCreateOpening)
  91. ensureOutputFileIsWriteable(output)
  92. let tryAgain = false
  93. let rememberedInput: OpeningParamsJson | undefined
  94. do {
  95. if (edit) {
  96. rememberedInput = await this.getInputFromFile(input as string)
  97. }
  98. // Either prompt for the data or get it from input file
  99. const openingJson =
  100. !input || edit || tryAgain
  101. ? await this.promptForData(lead, rememberedInput)
  102. : await this.getInputFromFile(input)
  103. // Remember the provided/fetched data in a variable
  104. rememberedInput = openingJson
  105. await this.promptForStakeTopUp(lead.stakingAccount.toString())
  106. // Generate and ask to confirm tx params
  107. const txParams = this.createTxParams(openingJson)
  108. this.jsonPrettyPrint(JSON.stringify(txParams))
  109. const confirmed = await this.simplePrompt({
  110. type: 'confirm',
  111. message: 'Do you confirm these extrinsic parameters?',
  112. })
  113. if (!confirmed) {
  114. tryAgain = await this.simplePrompt({ type: 'confirm', message: 'Try again with remembered input?' })
  115. continue
  116. }
  117. // Save output to file
  118. if (output) {
  119. try {
  120. saveOutputJsonToFile(output, rememberedInput)
  121. this.log(chalk.green(`Output succesfully saved in: ${chalk.white(output)}!`))
  122. } catch (e) {
  123. this.warn(`Could not save output to ${output}!`)
  124. }
  125. }
  126. if (dryRun) {
  127. this.exit(ExitCodes.OK)
  128. }
  129. // Send the tx
  130. const txSuccess = await this.sendAndFollowNamedTx(
  131. await this.getDecodedPair(lead.roleAccount.toString()),
  132. apiModuleByGroup[this.group],
  133. 'addOpening',
  134. txParams,
  135. true // warnOnly
  136. )
  137. // Display a success message on success or ask to try again on error
  138. if (txSuccess) {
  139. this.log(chalk.green('Opening succesfully created!'))
  140. tryAgain = false
  141. } else {
  142. tryAgain = await this.simplePrompt({ type: 'confirm', message: 'Try again with remembered input?' })
  143. }
  144. } while (tryAgain)
  145. }
  146. }