Browse Source

CLI: Creating / Removing upcoming openings

Leszek Wiesner 3 years ago
parent
commit
5bb657aafc

+ 10 - 0
cli/scripts/working-groups-test.sh

@@ -23,6 +23,16 @@ GROUP="storageProviders"
 # Add integration tests lead key (in case the script is executed after ./start.sh)
 ${CLI} account:forget --name "Test wg lead key" || true
 ${CLI} account:import --suri ${TEST_LEAD_SURI} --name "Test wg lead key" --password "" || true
+# Set/update working group metadata
+${CLI} working-groups:updateGroupMetadata --group ${GROUP} -i ../examples/working-groups/UpdateMetadata.json
+# Create upcoming opening
+UPCOMING_OPENING_ID=`${CLI} working-groups:createOpening \
+  --group ${GROUP} \
+  --input ../examples/working-groups/CreateOpening.json \
+  --upcoming \
+  --startsAt 2030-01-01`
+# Delete upcoming opening
+${CLI} working-groups:removeUpcomingOpening --group ${GROUP} --id ${UPCOMING_OPENING_ID}
 # Create opening
 OPENING_ID=`${CLI} working-groups:createOpening \
   --group ${GROUP} \

+ 26 - 1
cli/src/QueryNodeApi.ts

@@ -36,6 +36,13 @@ import {
   ApplicationDetailsByIdQuery,
   ApplicationDetailsByIdQueryVariables,
   ApplicationDetailsById,
+  UpcomingWorkingGroupOpeningByEventQuery,
+  UpcomingWorkingGroupOpeningByEventQueryVariables,
+  UpcomingWorkingGroupOpeningByEvent,
+  UpcomingWorkingGroupOpeningDetailsFragment,
+  UpcomingWorkingGroupOpeningByIdQuery,
+  UpcomingWorkingGroupOpeningByIdQueryVariables,
+  UpcomingWorkingGroupOpeningById,
 } from './graphql/generated/queries'
 import { URL } from 'url'
 import fetch from 'cross-fetch'
@@ -54,7 +61,7 @@ export default class QueryNodeApi {
     links.push(new HttpLink({ uri, fetch }))
     this._qnClient = new ApolloClient({
       link: from(links),
-      cache: new InMemoryCache(),
+      cache: new InMemoryCache({ addTypename: false }),
       defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
     })
   }
@@ -169,4 +176,22 @@ export default class QueryNodeApi {
       'workingGroupApplicationByUniqueInput'
     )
   }
+
+  async upcomingWorkingGroupOpeningByEvent(
+    blockNumber: number,
+    indexInBlock: number
+  ): Promise<UpcomingWorkingGroupOpeningDetailsFragment | null> {
+    return this.firstEntityQuery<
+      UpcomingWorkingGroupOpeningByEventQuery,
+      UpcomingWorkingGroupOpeningByEventQueryVariables
+    >(UpcomingWorkingGroupOpeningByEvent, { blockNumber, indexInBlock }, 'upcomingWorkingGroupOpenings')
+  }
+
+  async upcomingWorkingGroupOpeningById(id: string): Promise<UpcomingWorkingGroupOpeningDetailsFragment | null> {
+    return this.uniqueEntityQuery<UpcomingWorkingGroupOpeningByIdQuery, UpcomingWorkingGroupOpeningByIdQueryVariables>(
+      UpcomingWorkingGroupOpeningById,
+      { id },
+      'upcomingWorkingGroupOpeningByUniqueInput'
+    )
+  }
 }

+ 24 - 1
cli/src/Types.ts

@@ -8,7 +8,12 @@ import { Membership } from '@joystream/types/members'
 import { MemberId } from '@joystream/types/common'
 import { Validator } from 'inquirer'
 import { ApiPromise } from '@polkadot/api'
-import { SubmittableModuleExtrinsics, QueryableModuleStorage, QueryableModuleConsts } from '@polkadot/api/types'
+import {
+  SubmittableModuleExtrinsics,
+  QueryableModuleStorage,
+  QueryableModuleConsts,
+  AugmentedEvent,
+} from '@polkadot/api/types'
 import { JSONSchema4 } from 'json-schema'
 import {
   IChannelMetadata,
@@ -24,6 +29,7 @@ import {
   WorkingGroupApplicationDetailsFragment,
   WorkingGroupOpeningDetailsFragment,
 } from './graphql/generated/queries'
+import { IEvent } from '@polkadot/types/types'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -149,6 +155,23 @@ export type UnaugmentedApiPromise = Omit<ApiPromise, 'query' | 'tx' | 'consts'>
   consts: { [key: string]: QueryableModuleConsts }
 }
 
+// Event-related types
+export type EventSection = keyof ApiPromise['events'] & string
+export type EventMethod<Section extends EventSection> = keyof ApiPromise['events'][Section] & string
+export type EventType<
+  Section extends EventSection,
+  Method extends EventMethod<Section>
+> = ApiPromise['events'][Section][Method] extends AugmentedEvent<'promise', infer T> ? IEvent<T> & Codec : never
+
+export type EventDetails<E> = {
+  event: E
+  blockNumber: number
+  blockHash: string
+  blockTimestamp: number
+  indexInBlock: number
+}
+
+// Storage
 export type AssetToUpload = {
   dataObjectId: BN
   path: string

+ 39 - 14
cli/src/base/ApiCommandBase.ts

@@ -2,8 +2,9 @@ import ExitCodes from '../ExitCodes'
 import { CLIError } from '@oclif/errors'
 import StateAwareCommandBase from './StateAwareCommandBase'
 import Api from '../Api'
+import { EventSection, EventMethod, EventType, EventDetails } from '../Types'
 import { getTypeDef, Option, Tuple } from '@polkadot/types'
-import { Registry, Codec, TypeDef, TypeDefInfo, IEvent, DetectCodec } from '@polkadot/types/types'
+import { Registry, Codec, TypeDef, TypeDefInfo, DetectCodec, ISubmittableResult } from '@polkadot/types/types'
 import { Vec, Struct, Enum } from '@polkadot/types/codec'
 import { SubmittableResult, WsProvider, ApiPromise } from '@polkadot/api'
 import { KeyringPair } from '@polkadot/keyring/types'
@@ -11,7 +12,7 @@ import chalk from 'chalk'
 import { InterfaceTypes } from '@polkadot/types/types/registry'
 import { ApiMethodArg, ApiMethodNamedArgs, ApiParamsOptions, ApiParamOptions, UnaugmentedApiPromise } from '../Types'
 import { createParamOptions } from '../helpers/promptOptions'
-import { AugmentedSubmittables, SubmittableExtrinsic, AugmentedEvents, AugmentedEvent } from '@polkadot/api/types'
+import { AugmentedSubmittables, SubmittableExtrinsic } from '@polkadot/api/types'
 import { DistinctQuestion } from 'inquirer'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
 import { DispatchError } from '@polkadot/types/interfaces/system'
@@ -570,26 +571,50 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return this.sendAndFollowTx(account, tx)
   }
 
-  public findEvent<
-    S extends keyof AugmentedEvents<'promise'> & string,
-    M extends keyof AugmentedEvents<'promise'>[S] & string,
-    EventType = AugmentedEvents<'promise'>[S][M] extends AugmentedEvent<'promise', infer T> ? IEvent<T> : never
-  >(result: SubmittableResult, section: S, method: M): EventType | undefined {
-    return result.findRecord(section, method)?.event as EventType | undefined
+  public findEvent<S extends EventSection, M extends EventMethod<S>, E = EventType<S, M>>(
+    result: SubmittableResult,
+    section: S,
+    method: M
+  ): E | undefined {
+    return result.findRecord(section, method)?.event as E | undefined
   }
 
-  public getEvent<
-    S extends keyof AugmentedEvents<'promise'> & string,
-    M extends keyof AugmentedEvents<'promise'>[S] & string,
-    EventType = AugmentedEvents<'promise'>[S][M] extends AugmentedEvent<'promise', infer T> ? IEvent<T> : never
-  >(result: SubmittableResult, section: S, method: M): EventType {
-    const event = this.findEvent<S, M, EventType>(result, section, method)
+  public getEvent<S extends EventSection, M extends EventMethod<S>, E = EventType<S, M>>(
+    result: SubmittableResult,
+    section: S,
+    method: M
+  ): E {
+    const event = this.findEvent<S, M, E>(result, section, method)
     if (!event) {
       throw new Error(`Event ${section}.${method} not found in tx result: ${JSON.stringify(result.toHuman())}`)
     }
     return event
   }
 
+  async getEventDetails<S extends EventSection, M extends EventMethod<S>>(
+    result: ISubmittableResult,
+    section: S,
+    method: M
+  ): Promise<EventDetails<EventType<S, M>>> {
+    const api = this.getOriginalApi()
+    const { status } = result
+    const event = this.getEvent(result, section, method)
+
+    const blockHash = (status.isInBlock ? status.asInBlock : status.asFinalized).toString()
+    const blockNumber = (await api.rpc.chain.getHeader(blockHash)).number.toNumber()
+    const blockTimestamp = (await api.query.timestamp.now.at(blockHash)).toNumber()
+    const blockEvents = await api.query.system.events.at(blockHash)
+    const indexInBlock = blockEvents.findIndex(({ event: blockEvent }) => blockEvent.hash.eq(event.hash))
+
+    return {
+      event,
+      blockNumber,
+      blockHash,
+      blockTimestamp,
+      indexInBlock,
+    }
+  }
+
   async buildAndSendExtrinsic<
     Module extends keyof AugmentedSubmittables<'promise'>,
     Method extends keyof AugmentedSubmittables<'promise'>[Module] & string

+ 135 - 13
cli/src/commands/working-groups/createOpening.ts

@@ -1,5 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
-import { WorkingGroupOpeningInputParameters } from '../../Types'
+import { GroupMember, WorkingGroupOpeningInputParameters } from '../../Types'
 import { WorkingGroupOpeningInputSchema } from '../../schemas/WorkingGroups'
 import chalk from 'chalk'
 import { apiModuleByGroup } from '../../Api'
@@ -11,14 +11,23 @@ import { AugmentedSubmittables } from '@polkadot/api/types'
 import { formatBalance } from '@polkadot/util'
 import BN from 'bn.js'
 import { CLIError } from '@oclif/errors'
-import { IOpeningMetadata, OpeningMetadata } from '@joystream/metadata-protobuf'
+import {
+  IOpeningMetadata,
+  IWorkingGroupMetadataAction,
+  OpeningMetadata,
+  WorkingGroupMetadataAction,
+} from '@joystream/metadata-protobuf'
 import { metadataToBytes } from '../../helpers/serialization'
 import { OpeningId } from '@joystream/types/working-group'
+import Long from 'long'
+import moment from 'moment'
+import { UpcomingWorkingGroupOpeningDetailsFragment } from '../../graphql/generated/queries'
 
 const OPENING_STAKE = new BN(2000)
+const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'
 
 export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase {
-  static description = 'Create working group opening (requires lead access)'
+  static description = 'Create working group opening / upcoming opening (requires lead access)'
   static flags = {
     input: IOFlags.input,
     output: flags.string({
@@ -45,6 +54,14 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
       description:
         "If provided - this account (key) will be used as default funds source for lead stake top up (in case it's needed)",
     }),
+    upcoming: flags.boolean({
+      description: 'Whether the opening should be an upcoming opening',
+    }),
+    startsAt: flags.string({
+      required: false,
+      description: `If upcoming opening - the expected opening start date (${DATE_FORMAT})`,
+      dependsOn: ['upcoming'],
+    }),
     ...WorkingGroupsCommandBase.flags,
   }
 
@@ -111,14 +128,114 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
     }
   }
 
+  async createOpening(lead: GroupMember, inputParameters: WorkingGroupOpeningInputParameters): Promise<OpeningId> {
+    const result = await this.sendAndFollowTx(
+      await this.getDecodedPair(lead.roleAccount),
+      this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...this.createTxParams(inputParameters))
+    )
+    const openingId: OpeningId = this.getEvent(result, apiModuleByGroup[this.group], 'OpeningAdded').data[0]
+    this.log(chalk.green(`Opening with id ${chalk.magentaBright(openingId)} successfully created!`))
+    this.output(openingId.toString())
+    return openingId
+  }
+
+  async createUpcomingOpening(
+    lead: GroupMember,
+    actionMetadata: IWorkingGroupMetadataAction
+  ): Promise<UpcomingWorkingGroupOpeningDetailsFragment | undefined> {
+    const result = await this.sendAndFollowTx(
+      await this.getDecodedPair(lead.roleAccount),
+      this.getOriginalApi().tx[apiModuleByGroup[this.group]].setStatusText(
+        metadataToBytes(WorkingGroupMetadataAction, actionMetadata)
+      )
+    )
+    const { indexInBlock, blockNumber } = await this.getEventDetails(
+      result,
+      apiModuleByGroup[this.group],
+      'StatusTextChanged'
+    )
+    if (this.isQueryNodeUriSet()) {
+      let createdUpcomingOpening: UpcomingWorkingGroupOpeningDetailsFragment | null = null
+      let currentAttempt = 0
+      const maxRetryAttempts = 5
+      while (!createdUpcomingOpening && currentAttempt <= maxRetryAttempts) {
+        ++currentAttempt
+        createdUpcomingOpening = await this.getQNApi().upcomingWorkingGroupOpeningByEvent(blockNumber, indexInBlock)
+        if (!createdUpcomingOpening && currentAttempt <= maxRetryAttempts) {
+          this.log(
+            `Waiting for the upcoming opening to be processed by the query node (${currentAttempt}/${maxRetryAttempts})...`
+          )
+          await new Promise((resolve) => setTimeout(resolve, 6000))
+        }
+      }
+      if (!createdUpcomingOpening) {
+        this.error('Could not fetch the upcoming opening from the query node', { exit: ExitCodes.QueryNodeError })
+      }
+      this.log(
+        chalk.green(`Upcoming opening with id ${chalk.magentaBright(createdUpcomingOpening.id)} successfully created!`)
+      )
+      this.output(createdUpcomingOpening.id)
+      return createdUpcomingOpening
+    } else {
+      this.log(`StatusTextChanged event emitted in block ${blockNumber}, index: ${indexInBlock}`)
+      this.warn('Query node uri not set, cannot confirm whether the upcoming opening was succesfully created')
+    }
+  }
+
+  validateUpcomingOpeningStartDate(dateStr: string): string | true {
+    const momentObj = moment(dateStr, DATE_FORMAT)
+    if (!momentObj.isValid()) {
+      return `Unrecognized date format: ${dateStr}`
+    }
+    const ts = momentObj.unix()
+    if (ts <= moment().unix()) {
+      return 'Upcoming opening start date should be in the future!'
+    }
+    return true
+  }
+
+  async getUpcomingOpeningExpectedStartTimestamp(dateStr: string | undefined): Promise<number> {
+    if (dateStr) {
+      const validationResult = this.validateUpcomingOpeningStartDate(dateStr)
+      if (validationResult === true) {
+        return moment(dateStr).unix()
+      } else {
+        this.warn(`Invalid opening start date provided: ${validationResult}`)
+      }
+    }
+    dateStr = await this.simplePrompt<string>({
+      message: `Expected upcoming opening start date (${DATE_FORMAT}):`,
+      validate: (dateStr) => this.validateUpcomingOpeningStartDate(dateStr),
+    })
+    return moment(dateStr).unix()
+  }
+
+  prepareCreateUpcomingOpeningMetadata(
+    inputParameters: WorkingGroupOpeningInputParameters,
+    expectedStartTs: number
+  ): IWorkingGroupMetadataAction {
+    return {
+      addUpcomingOpening: {
+        metadata: {
+          rewardPerBlock: inputParameters.rewardPerBlock ? Long.fromNumber(inputParameters.rewardPerBlock) : undefined,
+          expectedStart: expectedStartTs,
+          minApplicationStake: Long.fromNumber(inputParameters.stakingPolicy.amount),
+          metadata: this.prepareMetadata(inputParameters),
+        },
+      },
+    }
+  }
+
   async run(): Promise<void> {
     // lead-only gate
     const lead = await this.getRequiredLeadContext()
 
     const {
-      flags: { input, output, edit, dryRun, stakeTopUpSource },
+      flags: { input, output, edit, dryRun, stakeTopUpSource, upcoming, startsAt },
     } = this.parse(WorkingGroupsCreateOpening)
 
+    const expectedStartTs = upcoming ? await this.getUpcomingOpeningExpectedStartTimestamp(startsAt) : 0
+
     ensureOutputFileIsWriteable(output)
 
     let tryAgain = false
@@ -134,9 +251,18 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
       // Remember the provided/fetched data in a variable
       rememberedInput = openingJson
 
-      await this.promptForStakeTopUp(lead.stakingAccount.toString(), stakeTopUpSource)
+      if (!upcoming) {
+        await this.promptForStakeTopUp(lead.stakingAccount.toString(), stakeTopUpSource)
+      }
+
+      const createUpcomingOpeningActionMeta = this.prepareCreateUpcomingOpeningMetadata(
+        rememberedInput,
+        expectedStartTs
+      )
 
-      this.jsonPrettyPrint(JSON.stringify(rememberedInput))
+      this.jsonPrettyPrint(
+        JSON.stringify(upcoming ? { WorkingGroupMetadataAction: createUpcomingOpeningActionMeta } : rememberedInput)
+      )
       const confirmed = await this.requestConfirmation('Do you confirm the provided input?')
       if (!confirmed) {
         tryAgain = await this.requestConfirmation('Try again with remembered input?')
@@ -159,13 +285,9 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
 
       // Send the tx
       try {
-        const result = await this.sendAndFollowTx(
-          await this.getDecodedPair(lead.roleAccount),
-          this.getOriginalApi().tx[apiModuleByGroup[this.group]].addOpening(...this.createTxParams(rememberedInput))
-        )
-        const openingId: OpeningId = this.getEvent(result, apiModuleByGroup[this.group], 'OpeningAdded').data[0]
-        this.log(chalk.green(`Opening with id ${chalk.magentaBright(openingId)} successfully created!`))
-        this.output(openingId.toString())
+        upcoming
+          ? await this.createUpcomingOpening(lead, createUpcomingOpeningActionMeta)
+          : await this.createOpening(lead, rememberedInput)
         tryAgain = false
       } catch (e) {
         if (e instanceof CLIError) {

+ 85 - 0
cli/src/commands/working-groups/removeUpcomingOpening.ts

@@ -0,0 +1,85 @@
+import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
+import chalk from 'chalk'
+import { apiModuleByGroup } from '../../Api'
+import { flags } from '@oclif/command'
+import { IWorkingGroupMetadataAction, WorkingGroupMetadataAction } from '@joystream/metadata-protobuf'
+import { metadataToBytes } from '../../helpers/serialization'
+import ExitCodes from '../../ExitCodes'
+
+export default class WorkingGroupsRemoveUpcomingOpening extends WorkingGroupsCommandBase {
+  static description =
+    'Remove an existing upcoming opening by sending RemoveUpcomingOpening metadata signal (requires lead access)'
+
+  static flags = {
+    id: flags.string({
+      char: 'i',
+      required: true,
+      description: `Id of the upcoming opening to remove`,
+    }),
+    ...WorkingGroupsCommandBase.flags,
+  }
+
+  async checkIfUpcomingOpeningExists(id: string): Promise<void> {
+    if (this.isQueryNodeUriSet()) {
+      const upcomingOpening = await this.getQNApi().upcomingWorkingGroupOpeningById(id)
+      this.log(`Upcoming opening by id ${id} found:`)
+      this.jsonPrettyPrint(JSON.stringify(upcomingOpening))
+      this.log('\n')
+    } else {
+      this.warn('Query node uri not set, cannot verify if upcoming opening exists!')
+    }
+  }
+
+  async checkIfUpcomingOpeningRemoved(id: string): Promise<void> {
+    if (this.isQueryNodeUriSet()) {
+      let removed = false
+      let currentAttempt = 0
+      const maxRetryAttempts = 5
+      while (!removed && currentAttempt <= maxRetryAttempts) {
+        ++currentAttempt
+        removed = !(await this.getQNApi().upcomingWorkingGroupOpeningById(id))
+        if (!removed && currentAttempt <= maxRetryAttempts) {
+          this.log(
+            `Waiting for the upcoming opening removal to be processed by the query node (${currentAttempt}/${maxRetryAttempts})...`
+          )
+          await new Promise((resolve) => setTimeout(resolve, 6000))
+        }
+      }
+      if (!removed) {
+        this.error('Could not confirm upcoming opening removal against the query node', {
+          exit: ExitCodes.QueryNodeError,
+        })
+      }
+      this.log(chalk.green(`Upcoming opening with id ${chalk.magentaBright(id)} successfully removed!`))
+    } else {
+      this.warn('Query node uri not set, cannot verify if upcoming opening was removed!')
+    }
+  }
+
+  async run(): Promise<void> {
+    const { id } = this.parse(WorkingGroupsRemoveUpcomingOpening).flags
+    // lead-only gate
+    const lead = await this.getRequiredLeadContext()
+
+    await this.checkIfUpcomingOpeningExists(id)
+
+    const actionMetadata: IWorkingGroupMetadataAction = {
+      'removeUpcomingOpening': {
+        id,
+      },
+    }
+
+    this.jsonPrettyPrint(JSON.stringify({ WorkingGroupMetadataAction: actionMetadata }))
+
+    await this.requireConfirmation('Do you confirm the provided input?')
+
+    await this.sendAndFollowTx(
+      await this.getDecodedPair(lead.roleAccount),
+      this.getOriginalApi().tx[apiModuleByGroup[this.group]].setStatusText(
+        metadataToBytes(WorkingGroupMetadataAction, actionMetadata)
+      )
+    )
+
+    await this.checkIfUpcomingOpeningRemoved(id)
+  }
+}

+ 86 - 0
cli/src/graphql/generated/queries.ts

@@ -67,6 +67,22 @@ export type WorkingGroupApplicationDetailsFragment = {
   answers: Array<{ answer: string; question: { question?: Types.Maybe<string> } }>
 }
 
+export type UpcomingWorkingGroupOpeningDetailsFragment = {
+  id: string
+  groupId: string
+  expectedStart?: Types.Maybe<any>
+  stakeAmount?: Types.Maybe<any>
+  rewardPerBlock?: Types.Maybe<any>
+  metadata: {
+    shortDescription?: Types.Maybe<string>
+    description?: Types.Maybe<string>
+    hiringLimit?: Types.Maybe<number>
+    expectedEnding?: Types.Maybe<any>
+    applicationDetails?: Types.Maybe<string>
+    applicationFormQuestions: Array<{ question?: Types.Maybe<string> }>
+  }
+}
+
 export type OpeningDetailsByIdQueryVariables = Types.Exact<{
   id: Types.Scalars['ID']
 }>
@@ -83,6 +99,31 @@ export type ApplicationDetailsByIdQuery = {
   workingGroupApplicationByUniqueInput?: Types.Maybe<WorkingGroupApplicationDetailsFragment>
 }
 
+export type UpcomingWorkingGroupOpeningByEventQueryVariables = Types.Exact<{
+  blockNumber: Types.Scalars['Int']
+  indexInBlock: Types.Scalars['Int']
+}>
+
+export type UpcomingWorkingGroupOpeningByEventQuery = {
+  upcomingWorkingGroupOpenings: Array<UpcomingWorkingGroupOpeningDetailsFragment>
+}
+
+export type UpcomingWorkingGroupOpeningsByGroupQueryVariables = Types.Exact<{
+  workingGroupId: Types.Scalars['ID']
+}>
+
+export type UpcomingWorkingGroupOpeningsByGroupQuery = {
+  upcomingWorkingGroupOpenings: Array<UpcomingWorkingGroupOpeningDetailsFragment>
+}
+
+export type UpcomingWorkingGroupOpeningByIdQueryVariables = Types.Exact<{
+  id: Types.Scalars['ID']
+}>
+
+export type UpcomingWorkingGroupOpeningByIdQuery = {
+  upcomingWorkingGroupOpeningByUniqueInput?: Types.Maybe<UpcomingWorkingGroupOpeningDetailsFragment>
+}
+
 export const MemberMetadataFields = gql`
   fragment MemberMetadataFields on MemberMetadata {
     name
@@ -162,6 +203,25 @@ export const WorkingGroupApplicationDetails = gql`
     }
   }
 `
+export const UpcomingWorkingGroupOpeningDetails = gql`
+  fragment UpcomingWorkingGroupOpeningDetails on UpcomingWorkingGroupOpening {
+    id
+    groupId
+    expectedStart
+    stakeAmount
+    rewardPerBlock
+    metadata {
+      shortDescription
+      description
+      hiringLimit
+      expectedEnding
+      applicationDetails
+      applicationFormQuestions {
+        question
+      }
+    }
+  }
+`
 export const GetMembersByIds = gql`
   query getMembersByIds($ids: [ID!]) {
     memberships(where: { id_in: $ids }) {
@@ -224,3 +284,29 @@ export const ApplicationDetailsById = gql`
   }
   ${WorkingGroupApplicationDetails}
 `
+export const UpcomingWorkingGroupOpeningByEvent = gql`
+  query upcomingWorkingGroupOpeningByEvent($blockNumber: Int!, $indexInBlock: Int!) {
+    upcomingWorkingGroupOpenings(
+      where: { createdInEvent: { inBlock_eq: $blockNumber, indexInBlock_eq: $indexInBlock } }
+    ) {
+      ...UpcomingWorkingGroupOpeningDetails
+    }
+  }
+  ${UpcomingWorkingGroupOpeningDetails}
+`
+export const UpcomingWorkingGroupOpeningsByGroup = gql`
+  query upcomingWorkingGroupOpeningsByGroup($workingGroupId: ID!) {
+    upcomingWorkingGroupOpenings(where: { group: { id_eq: $workingGroupId } }) {
+      ...UpcomingWorkingGroupOpeningDetails
+    }
+  }
+  ${UpcomingWorkingGroupOpeningDetails}
+`
+export const UpcomingWorkingGroupOpeningById = gql`
+  query upcomingWorkingGroupOpeningById($id: ID!) {
+    upcomingWorkingGroupOpeningByUniqueInput(where: { id: $id }) {
+      ...UpcomingWorkingGroupOpeningDetails
+    }
+  }
+  ${UpcomingWorkingGroupOpeningDetails}
+`

+ 38 - 0
cli/src/graphql/queries/workingGroups.graphql

@@ -21,6 +21,24 @@ fragment WorkingGroupApplicationDetails on WorkingGroupApplication {
   }
 }
 
+fragment UpcomingWorkingGroupOpeningDetails on UpcomingWorkingGroupOpening {
+  id
+  groupId
+  expectedStart
+  stakeAmount
+  rewardPerBlock
+  metadata {
+    shortDescription
+    description
+    hiringLimit
+    expectedEnding
+    applicationDetails
+    applicationFormQuestions {
+      question
+    }
+  }
+}
+
 query openingDetailsById($id: ID!) {
   workingGroupOpeningByUniqueInput(where: { id: $id }) {
     ...WorkingGroupOpeningDetails
@@ -32,3 +50,23 @@ query applicationDetailsById($id: ID!) {
     ...WorkingGroupApplicationDetails
   }
 }
+
+query upcomingWorkingGroupOpeningByEvent($blockNumber: Int!, $indexInBlock: Int!) {
+  upcomingWorkingGroupOpenings(
+    where: { createdInEvent: { inBlock_eq: $blockNumber, indexInBlock_eq: $indexInBlock } }
+  ) {
+    ...UpcomingWorkingGroupOpeningDetails
+  }
+}
+
+query upcomingWorkingGroupOpeningsByGroup($workingGroupId: ID!) {
+  upcomingWorkingGroupOpenings(where: { group: { id_eq: $workingGroupId } }) {
+    ...UpcomingWorkingGroupOpeningDetails
+  }
+}
+
+query upcomingWorkingGroupOpeningById($id: ID!) {
+  upcomingWorkingGroupOpeningByUniqueInput(where: { id: $id }) {
+    ...UpcomingWorkingGroupOpeningDetails
+  }
+}