Browse Source

Stake slash & decrease, worker reward update, worker leaving and termination

Leszek Wiesner 3 năm trước cách đây
mục cha
commit
b0ff95b53b

+ 9 - 0
query-node/manifest.yml

@@ -33,6 +33,7 @@ typegen:
     - storageWorkingGroup.LeaderSet
     - storageWorkingGroup.WorkerRoleAccountUpdated
     - storageWorkingGroup.LeaderUnset
+    - storageWorkingGroup.WorkerStartedLeaving
     - storageWorkingGroup.WorkerExited
     - storageWorkingGroup.TerminatedWorker
     - storageWorkingGroup.TerminatedLeader
@@ -138,6 +139,8 @@ mappings:
       handler: workingGroups_RewardPaid(DatabaseManager, SubstrateEvent)
     - event: storageWorkingGroup.NewMissedRewardLevelReached
       handler: workingGroups_NewMissedRewardLevelReached(DatabaseManager, SubstrateEvent)
+    - event: storageWorkingGroup.WorkerStartedLeaving
+      handler: workingGroups_WorkerStartedLeaving(DatabaseManager, SubstrateEvent)
     # Forum working group
     - event: forumWorkingGroup.OpeningAdded
       handler: workingGroups_OpeningAdded(DatabaseManager, SubstrateEvent)
@@ -181,6 +184,8 @@ mappings:
       handler: workingGroups_RewardPaid(DatabaseManager, SubstrateEvent)
     - event: forumWorkingGroup.NewMissedRewardLevelReached
       handler: workingGroups_NewMissedRewardLevelReached(DatabaseManager, SubstrateEvent)
+    - event: forumWorkingGroup.WorkerStartedLeaving
+      handler: workingGroups_WorkerStartedLeaving(DatabaseManager, SubstrateEvent)
     # Membership working group
     - event: membershipWorkingGroup.OpeningAdded
       handler: workingGroups_OpeningAdded(DatabaseManager, SubstrateEvent)
@@ -224,6 +229,8 @@ mappings:
       handler: workingGroups_RewardPaid(DatabaseManager, SubstrateEvent)
     - event: membershipWorkingGroup.NewMissedRewardLevelReached
       handler: workingGroups_NewMissedRewardLevelReached(DatabaseManager, SubstrateEvent)
+    - event: membershipWorkingGroup.WorkerStartedLeaving
+      handler: workingGroups_WorkerStartedLeaving(DatabaseManager, SubstrateEvent)
     # Content directory working group
     - event: contentDirectoryWorkingGroup.OpeningAdded
       handler: workingGroups_OpeningAdded(DatabaseManager, SubstrateEvent)
@@ -267,6 +274,8 @@ mappings:
       handler: workingGroups_RewardPaid(DatabaseManager, SubstrateEvent)
     - event: contentDirectoryWorkingGroup.NewMissedRewardLevelReached
       handler: workingGroups_NewMissedRewardLevelReached(DatabaseManager, SubstrateEvent)
+    - event: contentDirectoryWorkingGroup.WorkerStartedLeaving
+      handler: workingGroups_WorkerStartedLeaving(DatabaseManager, SubstrateEvent)
   extrinsicHandlers:
     # infer defaults here
     #- extrinsic: Balances.Transfer

+ 4 - 0
query-node/mappings/common.ts

@@ -58,3 +58,7 @@ export async function getOrCreateBlock(
 
   return block
 }
+
+export function bytesToString(b: Bytes): string {
+  return Buffer.from(b.toU8a(true)).toString()
+}

+ 1 - 5
query-node/mappings/membership.ts

@@ -8,7 +8,7 @@ import BN from 'bn.js'
 import { Bytes } from '@polkadot/types'
 import { MemberId, BuyMembershipParameters, InviteMembershipParameters } from '@joystream/types/augment/all'
 import { MembershipMetadata } from '@joystream/metadata-protobuf'
-import { createEvent, getOrCreateBlock } from './common'
+import { bytesToString, createEvent, getOrCreateBlock } from './common'
 import {
   Membership,
   EventType,
@@ -65,10 +65,6 @@ async function getOrCreateMembershipSnapshot(db: DatabaseManager, event_: Substr
       })
 }
 
-function bytesToString(b: Bytes): string {
-  return Buffer.from(b.toU8a(true)).toString()
-}
-
 function deserializeMemberMeta(metadataBytes: Bytes): MembershipMetadata | null {
   try {
     return MembershipMetadata.deserializeBinary(metadataBytes.toU8a(true))

+ 196 - 18
query-node/mappings/workingGroups.ts

@@ -13,7 +13,7 @@ import {
   WorkingGroupMetadataAction,
 } from '@joystream/metadata-protobuf'
 import { Bytes } from '@polkadot/types'
-import { createEvent, deserializeMetadata, getOrCreateBlock } from './common'
+import { createEvent, deserializeMetadata, getOrCreateBlock, bytesToString } from './common'
 import BN from 'bn.js'
 import {
   WorkingGroupOpening,
@@ -55,7 +55,17 @@ import {
   StakeIncreasedEvent,
   RewardPaidEvent,
   RewardPaymentType,
-  NewMissedRewardLevelReached,
+  NewMissedRewardLevelReachedEvent,
+  WorkerExitedEvent,
+  WorkerStatusLeft,
+  WorkerStatusTerminated,
+  TerminatedWorkerEvent,
+  LeaderUnsetEvent,
+  TerminatedLeaderEvent,
+  WorkerRewardAmountUpdatedEvent,
+  StakeSlashedEvent,
+  StakeDecreasedEvent,
+  WorkerStartedLeavingEvent,
 } from 'query-node/dist/model'
 import { createType } from '@joystream/types'
 import _ from 'lodash'
@@ -348,6 +358,38 @@ async function handleWorkingGroupMetadataAction(
   return result
 }
 
+async function handleTerminatedWorker(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId, balance: optPenalty, optBytes: optRationale } = new WorkingGroups.TerminatedWorkerEvent(event_).data
+  const group = await getWorkingGroup(db, event_)
+  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const EventConstructor = worker.isLead ? TerminatedLeaderEvent : TerminatedWorkerEvent
+  const eventType = worker.isLead ? EventType.TerminatedLeader : EventType.TerminatedWorker
+
+  const terminatedEvent = new EventConstructor({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, eventType),
+    worker,
+    penalty: optPenalty.unwrapOr(undefined),
+    rationale: optRationale.isSome ? bytesToString(optRationale.unwrap()) : undefined,
+  })
+
+  await db.save(terminatedEvent)
+
+  const status = new WorkerStatusTerminated()
+  status.terminatedWorkerEventId = terminatedEvent.id
+  worker.status = status
+  worker.stake = new BN(0)
+  worker.rewardPerBlock = new BN(0)
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
+}
+
 // Mapping functions
 export async function workingGroups_OpeningAdded(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
@@ -514,6 +556,7 @@ export async function workingGroups_OpeningFilled(db: DatabaseManager, event_: S
             payouts: [],
             status: new WorkerStatusActive(),
             entry: openingFilledEvent,
+            rewardPerBlock: opening.rewardPerBlock,
           })
           await db.save<Worker>(worker)
           hiredWorkers.push(worker)
@@ -792,7 +835,7 @@ export async function workingGroups_NewMissedRewardLevelReached(
   const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
   const eventTime = new Date(event_.blockTimestamp.toNumber())
 
-  const newMissedRewardLevelReachedEvent = new NewMissedRewardLevelReached({
+  const newMissedRewardLevelReachedEvent = new NewMissedRewardLevelReachedEvent({
     createdAt: eventTime,
     updatedAt: eventTime,
     group,
@@ -801,36 +844,171 @@ export async function workingGroups_NewMissedRewardLevelReached(
     newMissedRewardAmount: newMissedRewardAmountOpt.unwrapOr(new BN(0)),
   })
 
-  await db.save<NewMissedRewardLevelReached>(newMissedRewardLevelReachedEvent)
+  await db.save<NewMissedRewardLevelReachedEvent>(newMissedRewardLevelReachedEvent)
 }
 
-export async function workingGroups_LeaderUnset(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
-}
 export async function workingGroups_WorkerExited(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId } = new WorkingGroups.WorkerExitedEvent(event_).data
+  const group = await getWorkingGroup(db, event_)
+  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const workerExitedEvent = new WorkerExitedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.WorkerExited),
+    worker,
+  })
+
+  await db.save<WorkerExitedEvent>(workerExitedEvent)
+  ;(worker.status as WorkerStatusLeft).workerExitedEventId = workerExitedEvent.id
+  worker.stake = new BN(0)
+  worker.rewardPerBlock = new BN(0)
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
+}
+
+export async function workingGroups_LeaderUnset(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const group = await getWorkingGroup(db, event_)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const leaderUnsetEvent = new LeaderUnsetEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.LeaderUnset),
+  })
+
+  await db.save<LeaderUnsetEvent>(leaderUnsetEvent)
+
+  group.leader = undefined
+  group.updatedAt = eventTime
+
+  await db.save<WorkingGroup>(group)
 }
+
 export async function workingGroups_TerminatedWorker(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
+  await handleTerminatedWorker(db, event_)
 }
 export async function workingGroups_TerminatedLeader(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
+  await handleTerminatedWorker(db, event_)
+}
+
+export async function workingGroups_WorkerRewardAmountUpdated(
+  db: DatabaseManager,
+  event_: SubstrateEvent
+): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId, balance: newRewardPerBlockOpt } = new WorkingGroups.WorkerRewardAmountUpdatedEvent(event_).data
+  const group = await getWorkingGroup(db, event_)
+  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const workerRewardAmountUpdatedEvent = new WorkerRewardAmountUpdatedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.WorkerRewardAmountUpdated),
+    worker,
+    newRewardPerBlock: newRewardPerBlockOpt.unwrapOr(new BN(0)),
+  })
+
+  await db.save<WorkerRewardAmountUpdatedEvent>(workerRewardAmountUpdatedEvent)
+
+  worker.rewardPerBlock = newRewardPerBlockOpt.unwrapOr(new BN(0))
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
 }
+
 export async function workingGroups_StakeSlashed(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const {
+    workerId,
+    balances: { 0: slashedAmount, 1: requestedAmount },
+    optBytes: optRationale,
+  } = new WorkingGroups.StakeSlashedEvent(event_).data
+  const group = await getWorkingGroup(db, event_)
+  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const workerStakeSlashedEvent = new StakeSlashedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.StakeSlashed),
+    worker,
+    requestedAmount,
+    slashedAmount,
+    rationale: optRationale.isSome ? bytesToString(optRationale.unwrap()) : undefined,
+  })
+
+  await db.save<StakeSlashedEvent>(workerStakeSlashedEvent)
+
+  worker.stake = worker.stake.sub(slashedAmount)
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
 }
+
 export async function workingGroups_StakeDecreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId, balance: amount } = new WorkingGroups.StakeDecreasedEvent(event_).data
+  const group = await getWorkingGroup(db, event_)
+  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const workerStakeDecreasedEvent = new StakeDecreasedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.StakeDecreased),
+    worker,
+    amount,
+  })
+
+  await db.save<StakeDecreasedEvent>(workerStakeDecreasedEvent)
+
+  worker.stake = worker.stake.sub(amount)
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
 }
-export async function workingGroups_BudgetSet(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
+
+export async function workingGroups_WorkerStartedLeaving(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId, optBytes: optRationale } = new WorkingGroups.WorkerStartedLeavingEvent(event_).data
+  const group = await getWorkingGroup(db, event_)
+  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+
+  const workerStartedLeavingEvent = new WorkerStartedLeavingEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.WorkerStartedLeaving),
+    worker,
+    rationale: optRationale.isSome ? bytesToString(optRationale.unwrap()) : undefined,
+  })
+
+  await db.save<WorkerStartedLeavingEvent>(workerStartedLeavingEvent)
+
+  const status = new WorkerStatusLeft()
+  status.workerStartedLeavingEventId = workerStartedLeavingEvent.id
+  worker.status = status
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
 }
-export async function workingGroups_WorkerRewardAmountUpdated(
-  db: DatabaseManager,
-  event_: SubstrateEvent
-): Promise<void> {
+
+export async function workingGroups_BudgetSet(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   // TBD
 }
+
 export async function workingGroups_BudgetSpending(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   // TBD
 }

+ 6 - 3
query-node/schemas/workingGroups.graphql

@@ -14,9 +14,6 @@ type WorkerStatusLeft @variant {
 type WorkerStatusTerminated @variant {
   # TODO: Variant relationship
   terminatedWorkerEventId: ID!
-
-  # Set when the unstaking period is finished
-  workerExitedEventId: ID
 }
 
 union WorkerStatus = WorkerStatusActive | WorkerStatusLeft | WorkerStatusTerminated
@@ -53,9 +50,15 @@ type Worker @entity {
   "Current role stake (in JOY)"
   stake: BigInt!
 
+  "Current reward per block"
+  rewardPerBlock: BigInt!
+
   "All related reward payouts"
   payouts: [RewardPaidEvent!] @derivedFrom(field: "worker")
 
+  "All related stake slashes"
+  slashes: [StakeSlashedEvent!] @derivedFrom(field: "worker")
+
   "Block the worker was hired at"
   hiredAtBlock: Block!
 

+ 1 - 1
query-node/schemas/workingGroupsEvents.graphql

@@ -324,7 +324,7 @@ type RewardPaidEvent @entity {
   type: RewardPaymentType!
 }
 
-type NewMissedRewardLevelReached @entity {
+type NewMissedRewardLevelReachedEvent @entity {
   "Generic event data"
   event: Event!
 

+ 4 - 2
tests/integration-tests/.env

@@ -13,11 +13,13 @@ MEMBERS_INVITE_N = 2
 # Amount of staking accounts to add during "add staking accounts" test
 STAKING_ACCOUNTS_ADD_N = 3
 # Amount of applications to create in openingAndApplicationsStatus test
-APPLICATION_STATUS_CREATE_N = 5
+APPLICATION_STATUS_CREATE_N = 6
 # Amount of applications to withdraw in openingAndApplicationsStatus test
 APPLICATION_STATUS_WITHDRAW_N = 3
 # Amount of workers to test in workerActions flow
-WORKER_ACTIONS_WORKERS_N = 5
+WORKER_ACTIONS_WORKERS_N = 6
+# Amount of workers to terminate in workerActions flow
+WORKER_ACTIONS_TERMINATE_N = 3
 
 
 

+ 76 - 0
tests/integration-tests/src/QueryNodeApi.ts

@@ -132,6 +132,30 @@ import {
   GetWorkersByRuntimeIdsQuery,
   GetWorkersByRuntimeIdsQueryVariables,
   GetWorkersByRuntimeIds,
+  GetWorkerStartedLeavingEventsByEventIdsQuery,
+  GetWorkerStartedLeavingEventsByEventIdsQueryVariables,
+  GetWorkerStartedLeavingEventsByEventIds,
+  WorkerStartedLeavingEventFieldsFragment,
+  TerminatedWorkerEventFieldsFragment,
+  GetTerminatedWorkerEventsByEventIdsQuery,
+  GetTerminatedWorkerEventsByEventIdsQueryVariables,
+  GetTerminatedWorkerEventsByEventIds,
+  TerminatedLeaderEventFieldsFragment,
+  GetTerminatedLeaderEventsByEventIdsQuery,
+  GetTerminatedLeaderEventsByEventIdsQueryVariables,
+  GetTerminatedLeaderEventsByEventIds,
+  WorkerRewardAmountUpdatedEventFieldsFragment,
+  GetWorkerRewardAmountUpdatedEventsByEventIdsQuery,
+  GetWorkerRewardAmountUpdatedEventsByEventIdsQueryVariables,
+  GetWorkerRewardAmountUpdatedEventsByEventIds,
+  StakeSlashedEventFieldsFragment,
+  GetStakeSlashedEventsByEventIdsQuery,
+  GetStakeSlashedEventsByEventIdsQueryVariables,
+  GetStakeSlashedEventsByEventIds,
+  StakeDecreasedEventFieldsFragment,
+  GetStakeDecreasedEventsByEventIdsQuery,
+  GetStakeDecreasedEventsByEventIdsQueryVariables,
+  GetStakeDecreasedEventsByEventIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
@@ -536,4 +560,56 @@ export class QueryNodeApi {
       'workers'
     )
   }
+
+  public async getWorkerStartedLeavingEvents(
+    events: EventDetails[]
+  ): Promise<WorkerStartedLeavingEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetWorkerStartedLeavingEventsByEventIdsQuery,
+      GetWorkerStartedLeavingEventsByEventIdsQueryVariables
+    >(GetWorkerStartedLeavingEventsByEventIds, { eventIds }, 'workerStartedLeavingEvents')
+  }
+
+  public async getTerminatedWorkerEvents(events: EventDetails[]): Promise<TerminatedWorkerEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetTerminatedWorkerEventsByEventIdsQuery,
+      GetTerminatedWorkerEventsByEventIdsQueryVariables
+    >(GetTerminatedWorkerEventsByEventIds, { eventIds }, 'terminatedWorkerEvents')
+  }
+
+  public async getTerminatedLeaderEvents(events: EventDetails[]): Promise<TerminatedLeaderEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetTerminatedLeaderEventsByEventIdsQuery,
+      GetTerminatedLeaderEventsByEventIdsQueryVariables
+    >(GetTerminatedLeaderEventsByEventIds, { eventIds }, 'terminatedLeaderEvents')
+  }
+
+  public async getWorkerRewardAmountUpdatedEvents(
+    events: EventDetails[]
+  ): Promise<WorkerRewardAmountUpdatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetWorkerRewardAmountUpdatedEventsByEventIdsQuery,
+      GetWorkerRewardAmountUpdatedEventsByEventIdsQueryVariables
+    >(GetWorkerRewardAmountUpdatedEventsByEventIds, { eventIds }, 'workerRewardAmountUpdatedEvents')
+  }
+
+  public async getStakeSlashedEvents(events: EventDetails[]): Promise<StakeSlashedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetStakeSlashedEventsByEventIdsQuery,
+      GetStakeSlashedEventsByEventIdsQueryVariables
+    >(GetStakeSlashedEventsByEventIds, { eventIds }, 'stakeSlashedEvents')
+  }
+
+  public async getStakeDecreasedEvents(events: EventDetails[]): Promise<StakeDecreasedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetStakeDecreasedEventsByEventIdsQuery,
+      GetStakeDecreasedEventsByEventIdsQueryVariables
+    >(GetStakeDecreasedEventsByEventIds, { eventIds }, 'stakeDecreasedEvents')
+  }
 }

+ 92 - 0
tests/integration-tests/src/fixtures/workingGroups/DecreaseWorkerStakesFixture.ts

@@ -0,0 +1,92 @@
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { Worker, WorkerId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { lockIdByWorkingGroup } from '../../consts'
+import { StakeDecreasedEventFieldsFragment, WorkerFieldsFragment } from '../../graphql/generated/queries'
+
+export class DecreaseWorkerStakesFixture extends BaseWorkingGroupFixture {
+  protected asSudo: boolean
+
+  protected workerIds: WorkerId[]
+  protected amounts: BN[]
+  protected workers: Worker[] = []
+  protected workerStakes: BN[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    workerIds: WorkerId[],
+    amounts: BN[],
+    asSudo = false
+  ) {
+    super(api, query, group)
+    this.workerIds = workerIds
+    this.amounts = amounts
+    this.asSudo = asSudo
+  }
+
+  protected async loadWorkersData(): Promise<void> {
+    this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
+    this.workerStakes = await Promise.all(
+      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, lockIdByWorkingGroup[this.group]))
+    )
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    const extrinsics = this.workerIds.map((workerId, i) =>
+      this.api.tx[this.group].decreaseStake(workerId, this.amounts[i])
+    )
+    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StakeDecreased')
+  }
+
+  public async execute(): Promise<void> {
+    await this.loadWorkersData()
+    await super.execute()
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: StakeDecreasedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.StakeDecreased)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.amount, this.amounts[i].toString())
+  }
+
+  protected assertQueriedWorkersAreValid(qWorkers: WorkerFieldsFragment[]): void {
+    this.workerIds.map((workerId, i) => {
+      const worker = qWorkers.find((w) => w.runtimeId === workerId.toNumber())
+      Utils.assert(worker, 'Query node: Worker not found!')
+      assert.equal(worker.stake, this.workerStakes[i].sub(this.amounts[i]).toString())
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+
+    // Query and check events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getStakeDecreasedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check workers
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qWorkers)
+  }
+}

+ 10 - 1
tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts

@@ -4,7 +4,7 @@ import { Api } from '../../Api'
 import { QueryNodeApi } from '../../QueryNodeApi'
 import { OpeningFilledEventDetails, WorkingGroupModuleName } from '../../types'
 import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
-import { Application, ApplicationId, OpeningId, WorkerId } from '@joystream/types/working-group'
+import { Application, ApplicationId, Opening, OpeningId, WorkerId } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
@@ -22,6 +22,7 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
   protected events: OpeningFilledEventDetails[] = []
   protected asSudo: boolean
 
+  protected openings: Opening[] = []
   protected openingIds: OpeningId[]
   protected acceptedApplicationsIdsArrays: ApplicationId[][]
   protected acceptedApplicationsArrays: Application[][] = []
@@ -83,8 +84,13 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
     )
   }
 
+  protected async loadOpeningsData(): Promise<void> {
+    this.openings = await this.api.query[this.group].openingById.multi(this.openingIds)
+  }
+
   async execute(): Promise<void> {
     await this.loadApplicationsData()
+    await this.loadOpeningsData()
     await super.execute()
     this.events.forEach((e, i) => {
       this.createdWorkerIdsByOpeningId.set(
@@ -116,6 +122,7 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
         this.acceptedApplicationsIdsArrays[i][j],
         this.acceptedApplicationsArrays[i][j],
         this.applicationStakesArrays[i][j],
+        this.openings[i],
         qWorker
       )
     })
@@ -126,6 +133,7 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
     applicationId: ApplicationId,
     application: Application,
     applicationStake: BN,
+    opening: Opening,
     qWorker: WorkerFieldsFragment
   ): void {
     assert.equal(qWorker.group.name, this.group)
@@ -138,6 +146,7 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
     assert.equal(qWorker.stake, applicationStake.toString())
     assert.equal(qWorker.hiredAtBlock.number, eventDetails.blockNumber)
     assert.equal(qWorker.application.runtimeId, applicationId.toNumber())
+    assert.equal(qWorker.rewardPerBlock, opening.reward_per_block.toString())
   }
 
   protected assertOpeningsStatusesAreValid(

+ 80 - 0
tests/integration-tests/src/fixtures/workingGroups/LeaveRoleFixture.ts

@@ -0,0 +1,80 @@
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { WorkerId, Worker } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { WorkerFieldsFragment, WorkerStartedLeavingEventFieldsFragment } from '../../graphql/generated/queries'
+
+export class LeaveRoleFixture extends BaseWorkingGroupFixture {
+  protected workerIds: WorkerId[] = []
+
+  protected workers: Worker[] = []
+
+  public constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, workerIds: WorkerId[]) {
+    super(api, query, group)
+    this.workerIds = workerIds
+  }
+
+  protected async loadWorkersData(): Promise<void> {
+    this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    await this.loadWorkersData()
+    return this.workers.map((w) => w.role_account_id.toString())
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.workerIds.map((workerId) => this.api.tx[this.group].leaveRole(workerId, this.getRationale(workerId)))
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'WorkerStartedLeaving')
+  }
+
+  protected getRationale(workerId: WorkerId): string {
+    return `Worker ${workerId.toString()} leaving rationale`
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: WorkerStartedLeavingEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.WorkerStartedLeaving)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.rationale, this.getRationale(this.workerIds[i]))
+  }
+
+  protected assertQueriedWorkersAreValid(
+    qEvents: WorkerStartedLeavingEventFieldsFragment[],
+    qWorkers: WorkerFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const workerId = this.workerIds[i]
+      const worker = qWorkers.find((w) => w.runtimeId === workerId.toNumber())
+      Utils.assert(worker, 'Query node: Worker not found!')
+      Utils.assert(
+        worker.status.__typename === 'WorkerStatusLeft',
+        `Invalid worker status: ${worker.status.__typename}`
+      )
+      assert.equal(worker.status.workerStartedLeavingEventId, qEvent.id)
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query and check the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getWorkerStartedLeavingEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check the worker
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qEvents, qWorkers)
+  }
+}

+ 107 - 0
tests/integration-tests/src/fixtures/workingGroups/SlashWorkerStakesFixture.ts

@@ -0,0 +1,107 @@
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { Worker, WorkerId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { lockIdByWorkingGroup } from '../../consts'
+import { StakeSlashedEventFieldsFragment, WorkerFieldsFragment } from '../../graphql/generated/queries'
+
+export class SlashWorkerStakesFixture extends BaseWorkingGroupFixture {
+  protected asSudo: boolean
+
+  protected workerIds: WorkerId[]
+  protected penalties: BN[]
+  protected workers: Worker[] = []
+  protected workerStakes: BN[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    workerIds: WorkerId[],
+    penalties: BN[],
+    asSudo = false
+  ) {
+    super(api, query, group)
+    this.workerIds = workerIds
+    this.penalties = penalties
+    this.asSudo = asSudo
+  }
+
+  protected async loadWorkersData(): Promise<void> {
+    this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
+    this.workerStakes = await Promise.all(
+      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, lockIdByWorkingGroup[this.group]))
+    )
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    const extrinsics = this.workerIds.map((workerId, i) =>
+      this.api.tx[this.group].slashStake(workerId, this.penalties[i], this.getRationale(workerId))
+    )
+    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StakeSlashed')
+  }
+
+  public async execute(): Promise<void> {
+    await this.loadWorkersData()
+    await super.execute()
+  }
+
+  protected getRationale(workerId: WorkerId): string {
+    return `Worker ${workerId.toString()} slashing rationale`
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: StakeSlashedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.StakeSlashed)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.rationale, this.getRationale(this.workerIds[i]))
+    assert.equal(qEvent.requestedAmount, this.penalties[i].toString())
+    assert.equal(qEvent.slashedAmount, BN.min(this.penalties[i], this.workerStakes[i]))
+  }
+
+  protected assertQueriedWorkersAreValid(
+    qEvents: StakeSlashedEventFieldsFragment[],
+    qWorkers: WorkerFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const workerId = this.workerIds[i]
+      const worker = qWorkers.find((w) => w.runtimeId === workerId.toNumber())
+      Utils.assert(worker, 'Query node: Worker not found!')
+      assert.equal(worker.stake, BN.max(this.workerStakes[i].sub(this.penalties[i]), new BN(0)).toString())
+      assert.include(
+        worker.slashes.map((e) => e.id),
+        qEvent.id
+      )
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+
+    // Query and check the events
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () => this.query.getStakeSlashedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check workers
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qEvents, qWorkers)
+  }
+}

+ 122 - 0
tests/integration-tests/src/fixtures/workingGroups/TerminateWorkersFixture.ts

@@ -0,0 +1,122 @@
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { Worker, WorkerId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { lockIdByWorkingGroup } from '../../consts'
+import {
+  TerminatedLeaderEventFieldsFragment,
+  TerminatedWorkerEventFieldsFragment,
+  WorkerFieldsFragment,
+} from '../../graphql/generated/queries'
+
+export class TerminateWorkersFixture extends BaseWorkingGroupFixture {
+  protected asSudo: boolean
+
+  protected workerIds: WorkerId[]
+  protected penalties: BN[]
+  protected workers: Worker[] = []
+  protected workerStakes: BN[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    workerIds: WorkerId[],
+    penalties: BN[],
+    asSudo = false
+  ) {
+    super(api, query, group)
+    this.workerIds = workerIds
+    this.penalties = penalties
+    this.asSudo = asSudo
+  }
+
+  protected async loadWorkersData(): Promise<void> {
+    this.workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
+    this.workerStakes = await Promise.all(
+      this.workers.map((w) => this.api.getStakedBalance(w.staking_account_id, lockIdByWorkingGroup[this.group]))
+    )
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    const extrinsics = this.workerIds.map((workerId, i) =>
+      this.api.tx[this.group].terminateRole(workerId, this.penalties[i], this.getRationale(workerId))
+    )
+    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(
+      result,
+      this.group,
+      this.asSudo ? 'TerminatedLeader' : 'TerminatedWorker'
+    )
+  }
+
+  public async execute(): Promise<void> {
+    await this.loadWorkersData()
+    await super.execute()
+  }
+
+  protected getRationale(workerId: WorkerId): string {
+    return `Worker ${workerId.toString()} termination rationale`
+  }
+
+  protected assertQueryNodeEventIsValid(
+    qEvent: TerminatedWorkerEventFieldsFragment | TerminatedLeaderEventFieldsFragment,
+    i: number
+  ): void {
+    assert.equal(qEvent.event.type, this.asSudo ? EventType.TerminatedLeader : EventType.TerminatedWorker)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.rationale, this.getRationale(this.workerIds[i]))
+    assert.equal(qEvent.penalty, this.penalties[i].toString())
+  }
+
+  protected assertQueriedWorkersAreValid(
+    qEvents: (TerminatedWorkerEventFieldsFragment | TerminatedLeaderEventFieldsFragment)[],
+    qWorkers: WorkerFieldsFragment[]
+  ): void {
+    this.events.map((e, i) => {
+      const qEvent = this.findMatchingQueryNodeEvent(e, qEvents)
+      const workerId = this.workerIds[i]
+      const worker = qWorkers.find((w) => w.runtimeId === workerId.toNumber())
+      Utils.assert(worker, 'Query node: Worker not found!')
+      assert.equal(worker.stake, '0')
+      assert.equal(worker.rewardPerBlock, '0')
+      Utils.assert(
+        worker.status.__typename === 'WorkerStatusTerminated',
+        `Invalid worker status: ${worker.status.__typename}`
+      )
+      assert.equal(worker.status.terminatedWorkerEventId, qEvent.id)
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+
+    // Query the event and check event + hiredWorkers
+    const qEvents = await this.query.tryQueryWithTimeout(
+      () =>
+        this.asSudo
+          ? this.query.getTerminatedLeaderEvents(this.events)
+          : this.query.getTerminatedWorkerEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check workers
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qEvents, qWorkers)
+  }
+}

+ 80 - 0
tests/integration-tests/src/fixtures/workingGroups/UpdateWorkerRewardAmountsFixture.ts

@@ -0,0 +1,80 @@
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails, WorkingGroupModuleName } from '../../types'
+import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
+import { Worker, WorkerId } from '@joystream/types/working-group'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import { Utils } from '../../utils'
+import { EventType } from '../../graphql/generated/schema'
+import { WorkerFieldsFragment, WorkerRewardAmountUpdatedEventFieldsFragment } from '../../graphql/generated/queries'
+
+export class UpdateWorkerRewardAmountsFixture extends BaseWorkingGroupFixture {
+  protected asSudo: boolean
+
+  protected workerIds: WorkerId[]
+  protected newRewards: (BN | null)[]
+  protected workers: Worker[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    workerIds: WorkerId[],
+    newRewards: (BN | null)[],
+    asSudo = false
+  ) {
+    super(api, query, group)
+    this.workerIds = workerIds
+    this.newRewards = newRewards
+    this.asSudo = asSudo
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    const extrinsics = this.workerIds.map((workerId, i) =>
+      this.api.tx[this.group].updateRewardAmount(workerId, this.newRewards[i])
+    )
+    return this.asSudo ? extrinsics.map((tx) => this.api.tx.sudo.sudo(tx)) : extrinsics
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'WorkerRewardAmountUpdated')
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: WorkerRewardAmountUpdatedEventFieldsFragment, i: number): void {
+    const newReward = this.newRewards[i]
+    assert.equal(qEvent.event.type, EventType.WorkerRewardAmountUpdated)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.newRewardPerBlock, newReward ? newReward.toString() : '0')
+  }
+
+  protected assertQueriedWorkersAreValid(qWorkers: WorkerFieldsFragment[]): void {
+    this.workerIds.map((workerId, i) => {
+      const newReward = this.newRewards[i]
+      const worker = qWorkers.find((w) => w.runtimeId === workerId.toNumber())
+      Utils.assert(worker, 'Query node: Worker not found!')
+      assert.equal(worker.rewardPerBlock, newReward ? newReward.toString() : '0')
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+
+    // Query the event and check event + hiredWorkers
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getWorkerRewardAmountUpdatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check workers
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qWorkers)
+  }
+}

+ 71 - 12
tests/integration-tests/src/flows/working-groups/workerActions.ts

@@ -2,12 +2,18 @@ import { FlowProps } from '../../Flow'
 import Debugger from 'debug'
 import { FixtureRunner } from '../../Fixture'
 import { workingGroups } from '../../consts'
-import { Utils } from '../../utils'
 import { HireWorkersFixture } from '../../fixtures/workingGroups/HireWorkersFixture'
 import { UpdateWorkerRoleAccountsFixture } from '../../fixtures/workingGroups/UpdateWorkerRoleAccountsFixture'
 import { IncreaseWorkerStakesFixture } from '../../fixtures/workingGroups/IncreaseWorkerStakesFixture'
 import { UpdateWorkerRewardAccountsFixture } from '../../fixtures/workingGroups/UpdateWorkerRewardAccountsFixture'
 import { Worker } from '@joystream/types/working-group'
+import { LeaveRoleFixture } from '../../fixtures/workingGroups/LeaveRoleFixture'
+import { assert } from 'chai'
+import BN from 'bn.js'
+import { UpdateWorkerRewardAmountsFixture } from '../../fixtures/workingGroups/UpdateWorkerRewardAmountsFixture'
+import { DecreaseWorkerStakesFixture } from '../../fixtures/workingGroups/DecreaseWorkerStakesFixture'
+import { SlashWorkerStakesFixture } from '../../fixtures/workingGroups/SlashWorkerStakesFixture'
+import { TerminateWorkersFixture } from '../../fixtures/workingGroups/TerminateWorkersFixture'
 
 export default async function workerActions({ api, query, env }: FlowProps): Promise<void> {
   await Promise.all(
@@ -16,17 +22,21 @@ export default async function workerActions({ api, query, env }: FlowProps): Pro
       debug('Started')
       api.enableDebugTxLogs()
 
-      const N = parseInt(env.WORKER_ACTIONS_WORKERS_N || '')
-      Utils.assert(N > 0, 'Invalid WORKER_ACTIONS_WORKERS_N env value')
+      const WORKERS_N = parseInt(env.WORKER_ACTIONS_WORKERS_N || '')
+      const TERMINATIONS_N = parseInt(env.WORKER_ACTIONS_TERMINATE_N || '')
+      assert.isAtLeast(WORKERS_N, 2)
+      assert.isAtLeast(TERMINATIONS_N, 1)
+      assert.isBelow(TERMINATIONS_N, WORKERS_N)
 
-      const hireWorkersFixture = new HireWorkersFixture(api, query, group, N)
+      const hireWorkersFixture = new HireWorkersFixture(api, query, group, WORKERS_N)
       await new FixtureRunner(hireWorkersFixture).run()
       const workerIds = hireWorkersFixture.getCreatedWorkerIds()
       const workers = await api.query[group].workerById.multi<Worker>(workerIds)
 
-      const runners: FixtureRunner[] = []
+      // Independent updates that don't interfere with each other
+      const workerUpdatesRunners: FixtureRunner[] = []
 
-      const newRoleAccounts = (await api.createKeyPairs(N)).map((kp) => kp.address)
+      const newRoleAccounts = (await api.createKeyPairs(WORKERS_N)).map((kp) => kp.address)
       const updateRoleAccountsFixture = new UpdateWorkerRoleAccountsFixture(
         api,
         query,
@@ -36,9 +46,9 @@ export default async function workerActions({ api, query, env }: FlowProps): Pro
       )
       const updateRoleAccountsRunner = new FixtureRunner(updateRoleAccountsFixture)
       await updateRoleAccountsRunner.run()
-      runners.push(updateRoleAccountsRunner)
+      workerUpdatesRunners.push(updateRoleAccountsRunner)
 
-      const newRewardAccounts = (await api.createKeyPairs(N)).map((kp) => kp.address)
+      const newRewardAccounts = (await api.createKeyPairs(WORKERS_N)).map((kp) => kp.address)
       const updateRewardAccountsFixture = new UpdateWorkerRewardAccountsFixture(
         api,
         query,
@@ -48,9 +58,9 @@ export default async function workerActions({ api, query, env }: FlowProps): Pro
       )
       const updateRewardAccountsRunner = new FixtureRunner(updateRewardAccountsFixture)
       await updateRewardAccountsRunner.run()
-      runners.push(updateRewardAccountsRunner)
+      workerUpdatesRunners.push(updateRewardAccountsRunner)
 
-      const stakeIncreases = workerIds.map((id) => id.muln(1000))
+      const stakeIncreases = workerIds.map((id) => id.addn(1).muln(1000))
       // Transfer balances
       await Promise.all(
         stakeIncreases.map((amount, i) => api.treasuryTransferBalance(workers[i].staking_account_id.toString(), amount))
@@ -58,9 +68,58 @@ export default async function workerActions({ api, query, env }: FlowProps): Pro
       const increaseWorkerStakesFixture = new IncreaseWorkerStakesFixture(api, query, group, workerIds, stakeIncreases)
       const increaseWorkerStakesRunner = new FixtureRunner(increaseWorkerStakesFixture)
       await increaseWorkerStakesRunner.run()
-      runners.push(increaseWorkerStakesRunner)
+      workerUpdatesRunners.push(increaseWorkerStakesRunner)
 
-      await Promise.all(runners.map((r) => r.runQueryNodeChecks()))
+      const newRewards: (BN | null)[] = workerIds.map((id) => id.addn(1).muln(10))
+      // At least one case should be null
+      newRewards[0] = null
+      const updateRewardsFixture = new UpdateWorkerRewardAmountsFixture(api, query, group, workerIds, newRewards)
+      const updateRewardsRunner = new FixtureRunner(updateRewardsFixture)
+      await updateRewardsRunner.run()
+      workerUpdatesRunners.push(updateRewardsRunner)
+
+      // Run query node checks for all above fixtures
+      await Promise.all(workerUpdatesRunners.map((r) => r.runQueryNodeChecks()))
+
+      // Those updates are separated since they affect worker stake and could interfere with each other:
+
+      // Stake decreases
+      const decreaseAmounts = workerIds.map((id) => id.addn(1).muln(100))
+      const decreaseStakesFixture = new DecreaseWorkerStakesFixture(api, query, group, workerIds, decreaseAmounts)
+      const decreaseStakesRunner = new FixtureRunner(decreaseStakesFixture)
+      await decreaseStakesRunner.runWithQueryNodeChecks()
+
+      // Termination / leaving runners
+      const exitRunners: FixtureRunner[] = []
+      const terminatedWorkerIds = workerIds.slice(0, TERMINATIONS_N)
+      const leavingWorkerIds = workerIds.slice(TERMINATIONS_N)
+
+      // Worker terminations
+      const penaltyAmounts = workerIds.map((id) => id.addn(1).muln(200))
+      const terminateWorkersFixture = new TerminateWorkersFixture(
+        api,
+        query,
+        group,
+        terminatedWorkerIds,
+        penaltyAmounts
+      )
+      const terminateWorkersRunner = new FixtureRunner(terminateWorkersFixture)
+      exitRunners.push(terminateWorkersRunner)
+
+      // Workers leaving
+      const leaveRoleFixture = new LeaveRoleFixture(api, query, group, leavingWorkerIds)
+      const leaveRoleRunner = new FixtureRunner(leaveRoleFixture)
+      exitRunners.push(leaveRoleRunner)
+
+      await Promise.all(exitRunners.map((r) => r.runWithQueryNodeChecks()))
+
+      // Slashes (post-leave-role to make sure they still work while worker is unstaking)
+      const slashAmounts = leavingWorkerIds.map((id) => id.addn(1).muln(300))
+      // Add at least 1 case where slashAmount > stake
+      slashAmounts[0] = slashAmounts[0].muln(10)
+      const slashStakesFixture = new SlashWorkerStakesFixture(api, query, group, leavingWorkerIds, slashAmounts)
+      const slashStakesRunner = new FixtureRunner(slashStakesFixture)
+      await slashStakesRunner.runWithQueryNodeChecks()
 
       await debug('Done')
     })

+ 265 - 2
tests/integration-tests/src/graphql/generated/queries.ts

@@ -290,13 +290,15 @@ export type WorkerFieldsFragment = {
   stake: any
   hiredAtTime: any
   storage?: Types.Maybe<string>
+  rewardPerBlock: any
   group: { name: string }
   membership: { id: string }
   status:
     | { __typename: 'WorkerStatusActive' }
-    | { __typename: 'WorkerStatusLeft' }
-    | { __typename: 'WorkerStatusTerminated' }
+    | { __typename: 'WorkerStatusLeft'; workerStartedLeavingEventId: string; workerExitedEventId?: Types.Maybe<string> }
+    | { __typename: 'WorkerStatusTerminated'; terminatedWorkerEventId: string }
   payouts: Array<{ id: string }>
+  slashes: Array<{ id: string }>
   hiredAtBlock: BlockFieldsFragment
   application: ApplicationBasicFieldsFragment
 }
@@ -561,6 +563,102 @@ export type GetStakeIncreasedEventsByEventIdsQueryVariables = Types.Exact<{
 
 export type GetStakeIncreasedEventsByEventIdsQuery = { stakeIncreasedEvents: Array<StakeIncreasedEventFieldsFragment> }
 
+export type WorkerStartedLeavingEventFieldsFragment = {
+  id: string
+  rationale?: Types.Maybe<string>
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetWorkerStartedLeavingEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetWorkerStartedLeavingEventsByEventIdsQuery = {
+  workerStartedLeavingEvents: Array<WorkerStartedLeavingEventFieldsFragment>
+}
+
+export type WorkerRewardAmountUpdatedEventFieldsFragment = {
+  id: string
+  newRewardPerBlock: any
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetWorkerRewardAmountUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetWorkerRewardAmountUpdatedEventsByEventIdsQuery = {
+  workerRewardAmountUpdatedEvents: Array<WorkerRewardAmountUpdatedEventFieldsFragment>
+}
+
+export type StakeSlashedEventFieldsFragment = {
+  id: string
+  requestedAmount: any
+  slashedAmount: any
+  rationale?: Types.Maybe<string>
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetStakeSlashedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetStakeSlashedEventsByEventIdsQuery = { stakeSlashedEvents: Array<StakeSlashedEventFieldsFragment> }
+
+export type StakeDecreasedEventFieldsFragment = {
+  id: string
+  amount: any
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetStakeDecreasedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetStakeDecreasedEventsByEventIdsQuery = { stakeDecreasedEvents: Array<StakeDecreasedEventFieldsFragment> }
+
+export type TerminatedWorkerEventFieldsFragment = {
+  id: string
+  penalty?: Types.Maybe<any>
+  rationale?: Types.Maybe<string>
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetTerminatedWorkerEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetTerminatedWorkerEventsByEventIdsQuery = {
+  terminatedWorkerEvents: Array<TerminatedWorkerEventFieldsFragment>
+}
+
+export type TerminatedLeaderEventFieldsFragment = {
+  id: string
+  penalty?: Types.Maybe<any>
+  rationale?: Types.Maybe<string>
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetTerminatedLeaderEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetTerminatedLeaderEventsByEventIdsQuery = {
+  terminatedLeaderEvents: Array<TerminatedLeaderEventFieldsFragment>
+}
+
 export const MemberMetadataFields = gql`
   fragment MemberMetadataFields on MemberMetadata {
     name
@@ -1014,12 +1112,22 @@ export const WorkerFields = gql`
     stakeAccount
     status {
       __typename
+      ... on WorkerStatusLeft {
+        workerStartedLeavingEventId
+        workerExitedEventId
+      }
+      ... on WorkerStatusTerminated {
+        terminatedWorkerEventId
+      }
     }
     isLead
     stake
     payouts {
       id
     }
+    slashes {
+      id
+    }
     hiredAtBlock {
       ...BlockFields
     }
@@ -1028,6 +1136,7 @@ export const WorkerFields = gql`
       ...ApplicationBasicFields
     }
     storage
+    rewardPerBlock
   }
   ${BlockFields}
   ${ApplicationBasicFields}
@@ -1163,6 +1272,112 @@ export const StakeIncreasedEventFields = gql`
   }
   ${EventFields}
 `
+export const WorkerStartedLeavingEventFields = gql`
+  fragment WorkerStartedLeavingEventFields on WorkerStartedLeavingEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    rationale
+  }
+  ${EventFields}
+`
+export const WorkerRewardAmountUpdatedEventFields = gql`
+  fragment WorkerRewardAmountUpdatedEventFields on WorkerRewardAmountUpdatedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    newRewardPerBlock
+  }
+  ${EventFields}
+`
+export const StakeSlashedEventFields = gql`
+  fragment StakeSlashedEventFields on StakeSlashedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    requestedAmount
+    slashedAmount
+    rationale
+  }
+  ${EventFields}
+`
+export const StakeDecreasedEventFields = gql`
+  fragment StakeDecreasedEventFields on StakeDecreasedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    amount
+  }
+  ${EventFields}
+`
+export const TerminatedWorkerEventFields = gql`
+  fragment TerminatedWorkerEventFields on TerminatedWorkerEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    penalty
+    rationale
+  }
+  ${EventFields}
+`
+export const TerminatedLeaderEventFields = gql`
+  fragment TerminatedLeaderEventFields on TerminatedLeaderEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    penalty
+    rationale
+  }
+  ${EventFields}
+`
 export const GetMemberById = gql`
   query getMemberById($id: ID!) {
     membershipByUniqueInput(where: { id: $id }) {
@@ -1427,3 +1642,51 @@ export const GetStakeIncreasedEventsByEventIds = gql`
   }
   ${StakeIncreasedEventFields}
 `
+export const GetWorkerStartedLeavingEventsByEventIds = gql`
+  query getWorkerStartedLeavingEventsByEventIds($eventIds: [ID!]) {
+    workerStartedLeavingEvents(where: { eventId_in: $eventIds }) {
+      ...WorkerStartedLeavingEventFields
+    }
+  }
+  ${WorkerStartedLeavingEventFields}
+`
+export const GetWorkerRewardAmountUpdatedEventsByEventIds = gql`
+  query getWorkerRewardAmountUpdatedEventsByEventIds($eventIds: [ID!]) {
+    workerRewardAmountUpdatedEvents(where: { eventId_in: $eventIds }) {
+      ...WorkerRewardAmountUpdatedEventFields
+    }
+  }
+  ${WorkerRewardAmountUpdatedEventFields}
+`
+export const GetStakeSlashedEventsByEventIds = gql`
+  query getStakeSlashedEventsByEventIds($eventIds: [ID!]) {
+    stakeSlashedEvents(where: { eventId_in: $eventIds }) {
+      ...StakeSlashedEventFields
+    }
+  }
+  ${StakeSlashedEventFields}
+`
+export const GetStakeDecreasedEventsByEventIds = gql`
+  query getStakeDecreasedEventsByEventIds($eventIds: [ID!]) {
+    stakeDecreasedEvents(where: { eventId_in: $eventIds }) {
+      ...StakeDecreasedEventFields
+    }
+  }
+  ${StakeDecreasedEventFields}
+`
+export const GetTerminatedWorkerEventsByEventIds = gql`
+  query getTerminatedWorkerEventsByEventIds($eventIds: [ID!]) {
+    terminatedWorkerEvents(where: { eventId_in: $eventIds }) {
+      ...TerminatedWorkerEventFields
+    }
+  }
+  ${TerminatedWorkerEventFields}
+`
+export const GetTerminatedLeaderEventsByEventIds = gql`
+  query getTerminatedLeaderEventsByEventIds($eventIds: [ID!]) {
+    terminatedLeaderEvents(where: { eventId_in: $eventIds }) {
+      ...TerminatedLeaderEventFields
+    }
+  }
+  ${TerminatedLeaderEventFields}
+`

+ 156 - 9
tests/integration-tests/src/graphql/generated/schema.ts

@@ -1504,6 +1504,7 @@ export type Event = BaseGraphQlObject & {
   memberverificationstatusupdatedeventevent?: Maybe<Array<MemberVerificationStatusUpdatedEvent>>
   membershipboughteventevent?: Maybe<Array<MembershipBoughtEvent>>
   membershippriceupdatedeventevent?: Maybe<Array<MembershipPriceUpdatedEvent>>
+  newmissedrewardlevelreachedeventevent?: Maybe<Array<NewMissedRewardLevelReachedEvent>>
   openingaddedeventevent?: Maybe<Array<OpeningAddedEvent>>
   openingcanceledeventevent?: Maybe<Array<OpeningCanceledEvent>>
   openingfilledeventevent?: Maybe<Array<OpeningFilledEvent>>
@@ -3375,6 +3376,110 @@ export enum Network {
   Olympia = 'OLYMPIA',
 }
 
+export type NewMissedRewardLevelReachedEvent = BaseGraphQlObject & {
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  event: Event
+  eventId: Scalars['String']
+  group: WorkingGroup
+  groupId: Scalars['String']
+  worker: Worker
+  workerId: Scalars['String']
+  /** New missed reward amount */
+  newMissedRewardAmount: Scalars['BigInt']
+}
+
+export type NewMissedRewardLevelReachedEventConnection = {
+  totalCount: Scalars['Int']
+  edges: Array<NewMissedRewardLevelReachedEventEdge>
+  pageInfo: PageInfo
+}
+
+export type NewMissedRewardLevelReachedEventCreateInput = {
+  eventId: Scalars['ID']
+  groupId: Scalars['ID']
+  workerId: Scalars['ID']
+  newMissedRewardAmount: Scalars['BigInt']
+}
+
+export type NewMissedRewardLevelReachedEventEdge = {
+  node: NewMissedRewardLevelReachedEvent
+  cursor: Scalars['String']
+}
+
+export enum NewMissedRewardLevelReachedEventOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  EventIdAsc = 'eventId_ASC',
+  EventIdDesc = 'eventId_DESC',
+  GroupIdAsc = 'groupId_ASC',
+  GroupIdDesc = 'groupId_DESC',
+  WorkerIdAsc = 'workerId_ASC',
+  WorkerIdDesc = 'workerId_DESC',
+  NewMissedRewardAmountAsc = 'newMissedRewardAmount_ASC',
+  NewMissedRewardAmountDesc = 'newMissedRewardAmount_DESC',
+}
+
+export type NewMissedRewardLevelReachedEventUpdateInput = {
+  eventId?: Maybe<Scalars['ID']>
+  groupId?: Maybe<Scalars['ID']>
+  workerId?: Maybe<Scalars['ID']>
+  newMissedRewardAmount?: Maybe<Scalars['BigInt']>
+}
+
+export type NewMissedRewardLevelReachedEventWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  eventId_eq?: Maybe<Scalars['ID']>
+  eventId_in?: Maybe<Array<Scalars['ID']>>
+  groupId_eq?: Maybe<Scalars['ID']>
+  groupId_in?: Maybe<Array<Scalars['ID']>>
+  workerId_eq?: Maybe<Scalars['ID']>
+  workerId_in?: Maybe<Array<Scalars['ID']>>
+  newMissedRewardAmount_eq?: Maybe<Scalars['BigInt']>
+  newMissedRewardAmount_gt?: Maybe<Scalars['BigInt']>
+  newMissedRewardAmount_gte?: Maybe<Scalars['BigInt']>
+  newMissedRewardAmount_lt?: Maybe<Scalars['BigInt']>
+  newMissedRewardAmount_lte?: Maybe<Scalars['BigInt']>
+  newMissedRewardAmount_in?: Maybe<Array<Scalars['BigInt']>>
+}
+
+export type NewMissedRewardLevelReachedEventWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
 export type OpeningAddedEvent = BaseGraphQlObject & {
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
@@ -3884,6 +3989,9 @@ export type Query = {
   memberships: Array<Membership>
   membershipByUniqueInput?: Maybe<Membership>
   membershipsConnection: MembershipConnection
+  newMissedRewardLevelReachedEvents: Array<NewMissedRewardLevelReachedEvent>
+  newMissedRewardLevelReachedEventByUniqueInput?: Maybe<NewMissedRewardLevelReachedEvent>
+  newMissedRewardLevelReachedEventsConnection: NewMissedRewardLevelReachedEventConnection
   openingAddedEvents: Array<OpeningAddedEvent>
   openingAddedEventByUniqueInput?: Maybe<OpeningAddedEvent>
   openingAddedEventsConnection: OpeningAddedEventConnection
@@ -4445,6 +4553,26 @@ export type QueryMembershipsConnectionArgs = {
   orderBy?: Maybe<MembershipOrderByInput>
 }
 
+export type QueryNewMissedRewardLevelReachedEventsArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<NewMissedRewardLevelReachedEventWhereInput>
+  orderBy?: Maybe<NewMissedRewardLevelReachedEventOrderByInput>
+}
+
+export type QueryNewMissedRewardLevelReachedEventByUniqueInputArgs = {
+  where: NewMissedRewardLevelReachedEventWhereUniqueInput
+}
+
+export type QueryNewMissedRewardLevelReachedEventsConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<NewMissedRewardLevelReachedEventWhereInput>
+  orderBy?: Maybe<NewMissedRewardLevelReachedEventOrderByInput>
+}
+
 export type QueryOpeningAddedEventsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -5079,6 +5207,8 @@ export type RewardPaidEvent = BaseGraphQlObject & {
   rewardAccount: Scalars['String']
   /** Amount recieved */
   amount: Scalars['BigInt']
+  /** Type of the payment (REGULAR/MISSED) */
+  type: RewardPaymentType
 }
 
 export type RewardPaidEventConnection = {
@@ -5093,6 +5223,7 @@ export type RewardPaidEventCreateInput = {
   workerId: Scalars['ID']
   rewardAccount: Scalars['String']
   amount: Scalars['BigInt']
+  type: RewardPaymentType
 }
 
 export type RewardPaidEventEdge = {
@@ -5117,6 +5248,8 @@ export enum RewardPaidEventOrderByInput {
   RewardAccountDesc = 'rewardAccount_DESC',
   AmountAsc = 'amount_ASC',
   AmountDesc = 'amount_DESC',
+  TypeAsc = 'type_ASC',
+  TypeDesc = 'type_DESC',
 }
 
 export type RewardPaidEventUpdateInput = {
@@ -5125,6 +5258,7 @@ export type RewardPaidEventUpdateInput = {
   workerId?: Maybe<Scalars['ID']>
   rewardAccount?: Maybe<Scalars['String']>
   amount?: Maybe<Scalars['BigInt']>
+  type?: Maybe<RewardPaymentType>
 }
 
 export type RewardPaidEventWhereInput = {
@@ -5169,12 +5303,19 @@ export type RewardPaidEventWhereInput = {
   amount_lt?: Maybe<Scalars['BigInt']>
   amount_lte?: Maybe<Scalars['BigInt']>
   amount_in?: Maybe<Array<Scalars['BigInt']>>
+  type_eq?: Maybe<RewardPaymentType>
+  type_in?: Maybe<Array<RewardPaymentType>>
 }
 
 export type RewardPaidEventWhereUniqueInput = {
   id: Scalars['ID']
 }
 
+export enum RewardPaymentType {
+  Regular = 'REGULAR',
+  Missed = 'MISSED',
+}
+
 export type StakeDecreasedEvent = BaseGraphQlObject & {
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
@@ -6393,7 +6534,10 @@ export type Worker = BaseGraphQlObject & {
   isLead: Scalars['Boolean']
   /** Current role stake (in JOY) */
   stake: Scalars['BigInt']
+  /** Current reward per block */
+  rewardPerBlock: Scalars['BigInt']
   payouts: Array<RewardPaidEvent>
+  slashes: Array<StakeSlashedEvent>
   hiredAtBlock: Block
   hiredAtBlockId: Scalars['String']
   /** Time the worker was hired at */
@@ -6406,9 +6550,9 @@ export type Worker = BaseGraphQlObject & {
   storage?: Maybe<Scalars['String']>
   leaderseteventworker?: Maybe<Array<LeaderSetEvent>>
   memberverificationstatusupdatedeventworker?: Maybe<Array<MemberVerificationStatusUpdatedEvent>>
+  newmissedrewardlevelreachedeventworker?: Maybe<Array<NewMissedRewardLevelReachedEvent>>
   stakedecreasedeventworker?: Maybe<Array<StakeDecreasedEvent>>
   stakeincreasedeventworker?: Maybe<Array<StakeIncreasedEvent>>
-  stakeslashedeventworker?: Maybe<Array<StakeSlashedEvent>>
   terminatedleadereventworker?: Maybe<Array<TerminatedLeaderEvent>>
   terminatedworkereventworker?: Maybe<Array<TerminatedWorkerEvent>>
   workerexitedeventworker?: Maybe<Array<WorkerExitedEvent>>
@@ -6435,6 +6579,7 @@ export type WorkerCreateInput = {
   status: Scalars['JSONObject']
   isLead: Scalars['Boolean']
   stake: Scalars['BigInt']
+  rewardPerBlock: Scalars['BigInt']
   hiredAtBlockId: Scalars['ID']
   hiredAtTime: Scalars['DateTime']
   entryId: Scalars['ID']
@@ -6562,6 +6707,8 @@ export enum WorkerOrderByInput {
   IsLeadDesc = 'isLead_DESC',
   StakeAsc = 'stake_ASC',
   StakeDesc = 'stake_DESC',
+  RewardPerBlockAsc = 'rewardPerBlock_ASC',
+  RewardPerBlockDesc = 'rewardPerBlock_DESC',
   HiredAtBlockIdAsc = 'hiredAtBlockId_ASC',
   HiredAtBlockIdDesc = 'hiredAtBlockId_DESC',
   HiredAtTimeAsc = 'hiredAtTime_ASC',
@@ -7096,17 +7243,14 @@ export type WorkerStatusLeftWhereUniqueInput = {
 
 export type WorkerStatusTerminated = {
   terminatedWorkerEventId: Scalars['String']
-  workerExitedEventId?: Maybe<Scalars['String']>
 }
 
 export type WorkerStatusTerminatedCreateInput = {
   terminatedWorkerEventId: Scalars['String']
-  workerExitedEventId?: Maybe<Scalars['String']>
 }
 
 export type WorkerStatusTerminatedUpdateInput = {
   terminatedWorkerEventId?: Maybe<Scalars['String']>
-  workerExitedEventId?: Maybe<Scalars['String']>
 }
 
 export type WorkerStatusTerminatedWhereInput = {
@@ -7139,11 +7283,6 @@ export type WorkerStatusTerminatedWhereInput = {
   terminatedWorkerEventId_startsWith?: Maybe<Scalars['String']>
   terminatedWorkerEventId_endsWith?: Maybe<Scalars['String']>
   terminatedWorkerEventId_in?: Maybe<Array<Scalars['String']>>
-  workerExitedEventId_eq?: Maybe<Scalars['String']>
-  workerExitedEventId_contains?: Maybe<Scalars['String']>
-  workerExitedEventId_startsWith?: Maybe<Scalars['String']>
-  workerExitedEventId_endsWith?: Maybe<Scalars['String']>
-  workerExitedEventId_in?: Maybe<Array<Scalars['String']>>
 }
 
 export type WorkerStatusTerminatedWhereUniqueInput = {
@@ -7160,6 +7299,7 @@ export type WorkerUpdateInput = {
   status?: Maybe<Scalars['JSONObject']>
   isLead?: Maybe<Scalars['Boolean']>
   stake?: Maybe<Scalars['BigInt']>
+  rewardPerBlock?: Maybe<Scalars['BigInt']>
   hiredAtBlockId?: Maybe<Scalars['ID']>
   hiredAtTime?: Maybe<Scalars['DateTime']>
   entryId?: Maybe<Scalars['ID']>
@@ -7226,6 +7366,12 @@ export type WorkerWhereInput = {
   stake_lt?: Maybe<Scalars['BigInt']>
   stake_lte?: Maybe<Scalars['BigInt']>
   stake_in?: Maybe<Array<Scalars['BigInt']>>
+  rewardPerBlock_eq?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_gt?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_gte?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_lt?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_lte?: Maybe<Scalars['BigInt']>
+  rewardPerBlock_in?: Maybe<Array<Scalars['BigInt']>>
   hiredAtBlockId_eq?: Maybe<Scalars['ID']>
   hiredAtBlockId_in?: Maybe<Array<Scalars['ID']>>
   hiredAtTime_eq?: Maybe<Scalars['DateTime']>
@@ -7273,6 +7419,7 @@ export type WorkingGroup = BaseGraphQlObject & {
   budgetspendingeventgroup?: Maybe<Array<BudgetSpendingEvent>>
   leaderseteventgroup?: Maybe<Array<LeaderSetEvent>>
   leaderunseteventgroup?: Maybe<Array<LeaderUnsetEvent>>
+  newmissedrewardlevelreachedeventgroup?: Maybe<Array<NewMissedRewardLevelReachedEvent>>
   openingaddedeventgroup?: Maybe<Array<OpeningAddedEvent>>
   openingcanceledeventgroup?: Maybe<Array<OpeningCanceledEvent>>
   openingfilledeventgroup?: Maybe<Array<OpeningFilledEvent>>

+ 11 - 0
tests/integration-tests/src/graphql/queries/workingGroups.graphql

@@ -59,12 +59,22 @@ fragment WorkerFields on Worker {
   stakeAccount
   status {
     __typename
+    ... on WorkerStatusLeft {
+      workerStartedLeavingEventId
+      workerExitedEventId
+    }
+    ... on WorkerStatusTerminated {
+      terminatedWorkerEventId
+    }
   }
   isLead
   stake
   payouts {
     id
   }
+  slashes {
+    id
+  }
   hiredAtBlock {
     ...BlockFields
   }
@@ -73,6 +83,7 @@ fragment WorkerFields on Worker {
     ...ApplicationBasicFields
   }
   storage
+  rewardPerBlock
 }
 
 fragment WorkingGroupMetadataFields on WorkingGroupMetadata {

+ 132 - 0
tests/integration-tests/src/graphql/queries/workingGroupsEvents.graphql

@@ -199,3 +199,135 @@ query getStakeIncreasedEventsByEventIds($eventIds: [ID!]) {
     ...StakeIncreasedEventFields
   }
 }
+
+fragment WorkerStartedLeavingEventFields on WorkerStartedLeavingEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  rationale
+}
+
+query getWorkerStartedLeavingEventsByEventIds($eventIds: [ID!]) {
+  workerStartedLeavingEvents(where: { eventId_in: $eventIds }) {
+    ...WorkerStartedLeavingEventFields
+  }
+}
+
+fragment WorkerRewardAmountUpdatedEventFields on WorkerRewardAmountUpdatedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  newRewardPerBlock
+}
+
+query getWorkerRewardAmountUpdatedEventsByEventIds($eventIds: [ID!]) {
+  workerRewardAmountUpdatedEvents(where: { eventId_in: $eventIds }) {
+    ...WorkerRewardAmountUpdatedEventFields
+  }
+}
+
+fragment StakeSlashedEventFields on StakeSlashedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  requestedAmount
+  slashedAmount
+  rationale
+}
+
+query getStakeSlashedEventsByEventIds($eventIds: [ID!]) {
+  stakeSlashedEvents(where: { eventId_in: $eventIds }) {
+    ...StakeSlashedEventFields
+  }
+}
+
+fragment StakeDecreasedEventFields on StakeDecreasedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  amount
+}
+
+query getStakeDecreasedEventsByEventIds($eventIds: [ID!]) {
+  stakeDecreasedEvents(where: { eventId_in: $eventIds }) {
+    ...StakeDecreasedEventFields
+  }
+}
+
+fragment TerminatedWorkerEventFields on TerminatedWorkerEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  penalty
+  rationale
+}
+
+query getTerminatedWorkerEventsByEventIds($eventIds: [ID!]) {
+  terminatedWorkerEvents(where: { eventId_in: $eventIds }) {
+    ...TerminatedWorkerEventFields
+  }
+}
+
+fragment TerminatedLeaderEventFields on TerminatedLeaderEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  penalty
+  rationale
+}
+
+query getTerminatedLeaderEventsByEventIds($eventIds: [ID!]) {
+  terminatedLeaderEvents(where: { eventId_in: $eventIds }) {
+    ...TerminatedLeaderEventFields
+  }
+}
+
+# TODO: LeaderUnset?

+ 3 - 0
tests/integration-tests/src/types.ts

@@ -76,6 +76,9 @@ export type WorkingGroupsEventName =
   | 'WorkerRewardAmountUpdated'
   | 'StatusTextChanged'
   | 'BudgetSpending'
+  | 'WorkerStartedLeaving'
+  | 'RewardPaid'
+  | 'NewMissedRewardLevelReached'
 
 export type WorkingGroupModuleName =
   | 'storageWorkingGroup'