Browse Source

Worker actions - update accounts, increase stake

Leszek Wiesner 3 years ago
parent
commit
f381faa6cc

+ 1 - 0
query-node/mappings/initializeDb.ts

@@ -34,6 +34,7 @@ async function initWorkingGroups(api: ApiPromise, db: DatabaseManager) {
     groupNames.map(async (groupName) => {
       const budget = await api.query[groupName].budget.at<BalanceOf>(api.genesisHash)
       return new WorkingGroup({
+        id: groupName,
         name: groupName,
         workers: [],
         openings: [],

+ 85 - 10
query-node/mappings/workingGroups.ts

@@ -50,6 +50,9 @@ import {
   InvalidActionMetadata,
   WorkingGroupMetadataActionResult,
   UpcomingOpeningAdded,
+  WorkerRoleAccountUpdatedEvent,
+  WorkerRewardAccountUpdatedEvent,
+  StakeIncreasedEvent,
 } from 'query-node/dist/model'
 import { createType } from '@joystream/types'
 import _ from 'lodash'
@@ -95,6 +98,15 @@ async function getApplication(db: DatabaseManager, applicationDbId: string): Pro
   return application
 }
 
+async function getWorker(db: DatabaseManager, workerDbId: string): Promise<Worker> {
+  const worker = await db.get(Worker, { where: { id: workerDbId } })
+  if (!worker) {
+    throw new Error(`Worker not found by id ${workerDbId}`)
+  }
+
+  return worker
+}
+
 async function getApplicationFormQuestions(
   db: DatabaseManager,
   openingDbId: string
@@ -665,8 +677,80 @@ export async function workingGroups_WorkerRoleAccountUpdated(
   db: DatabaseManager,
   event_: SubstrateEvent
 ): Promise<void> {
-  // TBD
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId, accountId } = new WorkingGroups.WorkerRoleAccountUpdatedEvent(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 workerRoleAccountUpdatedEvent = new WorkerRoleAccountUpdatedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.WorkerRoleAccountUpdated),
+    worker,
+    newRoleAccount: accountId.toString(),
+  })
+
+  await db.save<WorkerRoleAccountUpdatedEvent>(workerRoleAccountUpdatedEvent)
+
+  worker.roleAccount = accountId.toString()
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
 }
+
+export async function workingGroups_WorkerRewardAccountUpdated(
+  db: DatabaseManager,
+  event_: SubstrateEvent
+): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId, accountId } = new WorkingGroups.WorkerRewardAccountUpdatedEvent(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 workerRewardAccountUpdatedEvent = new WorkerRewardAccountUpdatedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.WorkerRewardAccountUpdated),
+    worker,
+    newRewardAccount: accountId.toString(),
+  })
+
+  await db.save<WorkerRoleAccountUpdatedEvent>(workerRewardAccountUpdatedEvent)
+
+  worker.rewardAccount = accountId.toString()
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
+}
+
+export async function workingGroups_StakeIncreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const { workerId, balance: increaseAmount } = new WorkingGroups.StakeIncreasedEvent(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 stakeIncreasedEvent = new StakeIncreasedEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    group,
+    event: await createEvent(db, event_, EventType.StakeIncreased),
+    worker,
+    amount: increaseAmount,
+  })
+
+  await db.save<StakeIncreasedEvent>(stakeIncreasedEvent)
+
+  worker.stake = worker.stake.add(increaseAmount)
+  worker.updatedAt = eventTime
+
+  await db.save<Worker>(worker)
+}
+
 export async function workingGroups_LeaderUnset(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   // TBD
 }
@@ -685,18 +769,9 @@ export async function workingGroups_StakeSlashed(db: DatabaseManager, event_: Su
 export async function workingGroups_StakeDecreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   // TBD
 }
-export async function workingGroups_StakeIncreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TBD
-}
 export async function workingGroups_BudgetSet(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   // TBD
 }
-export async function workingGroups_WorkerRewardAccountUpdated(
-  db: DatabaseManager,
-  event_: SubstrateEvent
-): Promise<void> {
-  // TBD
-}
 export async function workingGroups_WorkerRewardAmountUpdated(
   db: DatabaseManager,
   event_: SubstrateEvent

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

@@ -96,6 +96,9 @@ type WorkingGroupMetadata @entity {
 }
 
 type WorkingGroup @entity {
+  "Working group id (currently === name)"
+  id: ID!
+
   "Working group name"
   name: String! @unique
 

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

@@ -16,6 +16,8 @@ STAKING_ACCOUNTS_ADD_N = 3
 APPLICATION_STATUS_CREATE_N = 5
 # 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
 
 
 

+ 53 - 1
tests/integration-tests/src/QueryNodeApi.ts

@@ -1,7 +1,7 @@
 import { ApolloClient, DocumentNode, NormalizedCacheObject } from '@apollo/client'
 import { MemberId } from '@joystream/types/common'
 import Debugger from 'debug'
-import { ApplicationId, OpeningId } from '@joystream/types/working-group'
+import { ApplicationId, OpeningId, WorkerId } from '@joystream/types/working-group'
 import { EventDetails, WorkingGroupModuleName } from './types'
 import {
   GetMemberByIdQuery,
@@ -116,6 +116,22 @@ import {
   GetApplicationsByIdsQuery,
   GetApplicationsByIdsQueryVariables,
   GetApplicationsByIds,
+  GetWorkerRoleAccountUpdatedEventsByEventIdsQuery,
+  GetWorkerRoleAccountUpdatedEventsByEventIdsQueryVariables,
+  WorkerRoleAccountUpdatedEventFieldsFragment,
+  GetWorkerRoleAccountUpdatedEventsByEventIds,
+  GetWorkerRewardAccountUpdatedEventsByEventIdsQuery,
+  GetWorkerRewardAccountUpdatedEventsByEventIdsQueryVariables,
+  WorkerRewardAccountUpdatedEventFieldsFragment,
+  GetWorkerRewardAccountUpdatedEventsByEventIds,
+  StakeIncreasedEventFieldsFragment,
+  GetStakeIncreasedEventsByEventIdsQuery,
+  GetStakeIncreasedEventsByEventIdsQueryVariables,
+  GetStakeIncreasedEventsByEventIds,
+  WorkerFieldsFragment,
+  GetWorkersByRuntimeIdsQuery,
+  GetWorkersByRuntimeIdsQueryVariables,
+  GetWorkersByRuntimeIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
@@ -484,4 +500,40 @@ export class QueryNodeApi {
       GetWorkingGroupMetadataSnapshotsByTimeAscQueryVariables
     >(GetWorkingGroupMetadataSnapshotsByTimeAsc, { groupId }, 'workingGroupMetadata')
   }
+
+  public async getWorkerRoleAccountUpdatedEvents(
+    events: EventDetails[]
+  ): Promise<WorkerRoleAccountUpdatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetWorkerRoleAccountUpdatedEventsByEventIdsQuery,
+      GetWorkerRoleAccountUpdatedEventsByEventIdsQueryVariables
+    >(GetWorkerRoleAccountUpdatedEventsByEventIds, { eventIds }, 'workerRoleAccountUpdatedEvents')
+  }
+
+  public async getWorkerRewardAccountUpdatedEvents(
+    events: EventDetails[]
+  ): Promise<WorkerRewardAccountUpdatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetWorkerRewardAccountUpdatedEventsByEventIdsQuery,
+      GetWorkerRewardAccountUpdatedEventsByEventIdsQueryVariables
+    >(GetWorkerRewardAccountUpdatedEventsByEventIds, { eventIds }, 'workerRewardAccountUpdatedEvents')
+  }
+
+  public async getStakeIncreasedEvents(events: EventDetails[]): Promise<StakeIncreasedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetStakeIncreasedEventsByEventIdsQuery,
+      GetStakeIncreasedEventsByEventIdsQueryVariables
+    >(GetStakeIncreasedEventsByEventIds, { eventIds }, 'stakeIncreasedEvents')
+  }
+
+  public async getWorkersByIds(ids: WorkerId[], group: WorkingGroupModuleName): Promise<WorkerFieldsFragment[]> {
+    return this.multipleEntitiesQuery<GetWorkersByRuntimeIdsQuery, GetWorkersByRuntimeIdsQueryVariables>(
+      GetWorkersByRuntimeIds,
+      { workerIds: ids.map((id) => id.toNumber()), groupId: group },
+      'workers'
+    )
+  }
 }

+ 14 - 2
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 } from '@joystream/types/working-group'
+import { Application, ApplicationId, OpeningId, WorkerId } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { ISubmittableResult } from '@polkadot/types/types/'
 import { Utils } from '../../utils'
@@ -24,9 +24,9 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
 
   protected openingIds: OpeningId[]
   protected acceptedApplicationsIdsArrays: ApplicationId[][]
-
   protected acceptedApplicationsArrays: Application[][] = []
   protected applicationStakesArrays: BN[][] = []
+  protected createdWorkerIdsByOpeningId: Map<number, WorkerId[]> = new Map<number, WorkerId[]>()
 
   public constructor(
     api: Api,
@@ -42,6 +42,12 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
     this.asSudo = asSudo
   }
 
+  public getCreatedWorkerIdsByOpeningId(openingId: OpeningId): WorkerId[] {
+    const workerIds = this.createdWorkerIdsByOpeningId.get(openingId.toNumber())
+    Utils.assert(workerIds, `No created worker ids for opening id ${openingId.toString()} were found!`)
+    return workerIds
+  }
+
   protected async getSignerAccountOrAccounts(): Promise<string> {
     return this.asSudo ? (await this.api.query.sudo.key()).toString() : await this.api.getLeadRoleKey(this.group)
   }
@@ -80,6 +86,12 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
   async execute(): Promise<void> {
     await this.loadApplicationsData()
     await super.execute()
+    this.events.forEach((e, i) => {
+      this.createdWorkerIdsByOpeningId.set(
+        this.openingIds[i].toNumber(),
+        Array.from(e.applicationIdToWorkerIdMap.values())
+      )
+    })
   }
 
   protected assertQueryNodeEventIsValid(qEvent: OpeningFilledEventFieldsFragment, i: number): void {

+ 96 - 0
tests/integration-tests/src/fixtures/workingGroups/HireWorkersFixture.ts

@@ -0,0 +1,96 @@
+import { WorkerId } from '@joystream/types/working-group'
+import { Api } from '../../Api'
+import { LEADER_OPENING_STAKE } from '../../consts'
+import { BaseQueryNodeFixture, FixtureRunner } from '../../Fixture'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { WorkingGroupModuleName } from '../../types'
+import { Utils } from '../../utils'
+import { AddStakingAccountsHappyCaseFixture, BuyMembershipHappyCaseFixture } from '../membership'
+import { ApplicantDetails, ApplyOnOpeningsHappyCaseFixture } from './ApplyOnOpeningsHappyCaseFixture'
+import { CreateOpeningsFixture } from './CreateOpeningsFixture'
+import { FillOpeningsFixture } from './FillOpeningsFixture'
+
+export class HireWorkersFixture extends BaseQueryNodeFixture {
+  protected group: WorkingGroupModuleName
+  protected workersN: number
+  protected createdWorkerIds: WorkerId[] = []
+
+  protected fillOpeningRunner?: FixtureRunner
+
+  constructor(api: Api, query: QueryNodeApi, group: WorkingGroupModuleName, workersN: number) {
+    super(api, query)
+    this.group = group
+    this.workersN = workersN
+  }
+
+  public getCreatedWorkerIds(): WorkerId[] {
+    Utils.assert(this.createdWorkerIds.length, 'Trying to get created workers before they were created!')
+    return this.createdWorkerIds
+  }
+
+  public async execute(): Promise<void> {
+    // Transfer funds to leader staking account to cover opening stake
+    const leaderStakingAcc = await this.api.getLeaderStakingKey(this.group)
+    await this.api.treasuryTransferBalance(leaderStakingAcc, LEADER_OPENING_STAKE)
+
+    // Create an opening
+    const createOpeningFixture = new CreateOpeningsFixture(this.api, this.query, this.group)
+    const openingRunner = new FixtureRunner(createOpeningFixture)
+    await openingRunner.run()
+    const [openingId] = createOpeningFixture.getCreatedOpeningIds()
+    const { stake: openingStake, metadata: openingMetadata } = createOpeningFixture.getDefaultOpeningParams()
+
+    // Create the applications
+    const roleAccounts = (await this.api.createKeyPairs(this.workersN)).map((kp) => kp.address)
+    const stakingAccounts = (await this.api.createKeyPairs(this.workersN)).map((kp) => kp.address)
+    const rewardAccounts = (await this.api.createKeyPairs(this.workersN)).map((kp) => kp.address)
+
+    const buyMembershipFixture = new BuyMembershipHappyCaseFixture(this.api, this.query, roleAccounts)
+    await new FixtureRunner(buyMembershipFixture).run()
+    const memberIds = buyMembershipFixture.getCreatedMembers()
+
+    const applicantContexts = roleAccounts.map((account, i) => ({
+      account,
+      memberId: memberIds[i],
+    }))
+
+    await Promise.all(
+      applicantContexts.map((applicantContext, i) => {
+        const addStakingAccFixture = new AddStakingAccountsHappyCaseFixture(this.api, this.query, applicantContext, [
+          stakingAccounts[i],
+        ])
+        return new FixtureRunner(addStakingAccFixture).run()
+      })
+    )
+    await Promise.all(stakingAccounts.map((a) => this.api.treasuryTransferBalance(a, openingStake)))
+
+    const applicants: ApplicantDetails[] = memberIds.map((memberId, i) => ({
+      memberId,
+      roleAccount: roleAccounts[i],
+      stakingAccount: stakingAccounts[i],
+      rewardAccount: rewardAccounts[i],
+    }))
+    const applyOnOpeningFixture = new ApplyOnOpeningsHappyCaseFixture(this.api, this.query, this.group, [
+      {
+        openingId,
+        openingMetadata,
+        applicants,
+      },
+    ])
+    const applyRunner = new FixtureRunner(applyOnOpeningFixture)
+    await applyRunner.run()
+    const applicationIds = await applyOnOpeningFixture.getCreatedApplicationsByOpeningId(openingId)
+
+    // Fill the opening
+    const fillOpeningFixture = new FillOpeningsFixture(this.api, this.query, this.group, [openingId], [applicationIds])
+    const fillOpeningRunner = new FixtureRunner(fillOpeningFixture)
+    await fillOpeningRunner.run()
+
+    this.createdWorkerIds = fillOpeningFixture.getCreatedWorkerIdsByOpeningId(openingId)
+  }
+
+  public async runQueryNodeChecks(): Promise<void> {
+    Utils.assert(this.fillOpeningRunner)
+    await this.fillOpeningRunner.runQueryNodeChecks()
+  }
+}

+ 81 - 0
tests/integration-tests/src/fixtures/workingGroups/IncreaseWorkerStakesFixture.ts

@@ -0,0 +1,81 @@
+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 { 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 { lockIdByWorkingGroup } from '../../consts'
+import { StakeIncreasedEventFieldsFragment, WorkerFieldsFragment } from '../../graphql/generated/queries'
+
+export class IncreaseWorkerStakesFixture extends BaseWorkingGroupFixture {
+  protected workerIds: WorkerId[]
+  protected stakeIncreases: BN[]
+
+  protected workers: Worker[] = []
+  protected workerStakes: BN[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    workerIds: WorkerId[],
+    stakeIncreases: BN[]
+  ) {
+    super(api, query, group)
+    this.workerIds = workerIds
+    this.stakeIncreases = stakeIncreases
+  }
+
+  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[]> {
+    await this.loadWorkersData()
+    return this.workers.map((w) => w.role_account_id.toString())
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.workerIds.map((workerId, i) => this.api.tx[this.group].increaseStake(workerId, this.stakeIncreases[i]))
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'StakeIncreased')
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: StakeIncreasedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.StakeIncreased)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.amount, this.stakeIncreases[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].add(this.stakeIncreases[i]).toString())
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query and check the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getStakeIncreasedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check the workers
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qWorkers)
+  }
+}

+ 78 - 0
tests/integration-tests/src/fixtures/workingGroups/UpdateWorkerRewardAccountsFixture.ts

@@ -0,0 +1,78 @@
+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, WorkerRewardAccountUpdatedEventFieldsFragment } from '../../graphql/generated/queries'
+import { AccountId } from '@joystream/types/common'
+
+export class UpdateWorkerRewardAccountsFixture extends BaseWorkingGroupFixture {
+  protected workerIds: WorkerId[] = []
+  protected rewardAccounts: (string | AccountId)[] = []
+
+  protected workers: Worker[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    workerIds: WorkerId[],
+    rewardAccounts: (string | AccountId)[]
+  ) {
+    super(api, query, group)
+    this.workerIds = workerIds
+    this.rewardAccounts = rewardAccounts
+  }
+
+  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, i) =>
+      this.api.tx[this.group].updateRewardAccount(workerId, this.rewardAccounts[i])
+    )
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'WorkerRewardAccountUpdated')
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: WorkerRewardAccountUpdatedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.WorkerRewardAccountUpdated)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.newRewardAccount, this.rewardAccounts[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.rewardAccount, this.rewardAccounts[i].toString())
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query and check the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getWorkerRewardAccountUpdatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check the worker
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qWorkers)
+  }
+}

+ 80 - 0
tests/integration-tests/src/fixtures/workingGroups/UpdateWorkerRoleAccountsFixture.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, WorkerRoleAccountUpdatedEventFieldsFragment } from '../../graphql/generated/queries'
+import { AccountId } from '@joystream/types/common'
+import { Membership } from '@joystream/types/members'
+
+export class UpdateWorkerRoleAccountsFixture extends BaseWorkingGroupFixture {
+  protected workerIds: WorkerId[] = []
+  protected roleAccounts: (string | AccountId)[] = []
+
+  protected workerMembers: Membership[] = []
+
+  public constructor(
+    api: Api,
+    query: QueryNodeApi,
+    group: WorkingGroupModuleName,
+    workerIds: WorkerId[],
+    roleAccounts: (string | AccountId)[]
+  ) {
+    super(api, query, group)
+    this.workerIds = workerIds
+    this.roleAccounts = roleAccounts
+  }
+
+  protected async loadWorkersData(): Promise<void> {
+    const workers = await this.api.query[this.group].workerById.multi<Worker>(this.workerIds)
+    this.workerMembers = await this.api.query.members.membershipById.multi(workers.map((w) => w.member_id))
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string[]> {
+    await this.loadWorkersData()
+    return this.workerMembers.map((m) => m.controller_account.toString())
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.workerIds.map((workerId, i) =>
+      this.api.tx[this.group].updateRoleAccount(workerId, this.roleAccounts[i])
+    )
+  }
+
+  protected getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveWorkingGroupsEventDetails(result, this.group, 'WorkerRoleAccountUpdated')
+  }
+
+  protected assertQueryNodeEventIsValid(qEvent: WorkerRoleAccountUpdatedEventFieldsFragment, i: number): void {
+    assert.equal(qEvent.event.type, EventType.WorkerRoleAccountUpdated)
+    assert.equal(qEvent.worker.runtimeId, this.workerIds[i].toNumber())
+    assert.equal(qEvent.group.name, this.group)
+    assert.equal(qEvent.newRoleAccount, this.roleAccounts[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.roleAccount, this.roleAccounts[i].toString())
+    })
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query and check the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getWorkerRoleAccountUpdatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Check the worker
+    const qWorkers = await this.query.getWorkersByIds(this.workerIds, this.group)
+    this.assertQueriedWorkersAreValid(qWorkers)
+  }
+}

+ 68 - 0
tests/integration-tests/src/flows/working-groups/workerActions.ts

@@ -0,0 +1,68 @@
+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'
+
+export default async function workerActions({ api, query, env }: FlowProps): Promise<void> {
+  await Promise.all(
+    workingGroups.map(async (group) => {
+      const debug = Debugger(`flow:worker-actions:${group}`)
+      debug('Started')
+      api.enableDebugTxLogs()
+
+      const N = parseInt(env.WORKER_ACTIONS_WORKERS_N || '')
+      Utils.assert(N > 0, 'Invalid WORKER_ACTIONS_WORKERS_N env value')
+
+      const hireWorkersFixture = new HireWorkersFixture(api, query, group, N)
+      await new FixtureRunner(hireWorkersFixture).run()
+      const workerIds = hireWorkersFixture.getCreatedWorkerIds()
+      const workers = await api.query[group].workerById.multi<Worker>(workerIds)
+
+      const runners: FixtureRunner[] = []
+
+      const newRoleAccounts = (await api.createKeyPairs(N)).map((kp) => kp.address)
+      const updateRoleAccountsFixture = new UpdateWorkerRoleAccountsFixture(
+        api,
+        query,
+        group,
+        workerIds,
+        newRoleAccounts
+      )
+      const updateRoleAccountsRunner = new FixtureRunner(updateRoleAccountsFixture)
+      await updateRoleAccountsRunner.run()
+      runners.push(updateRoleAccountsRunner)
+
+      const newRewardAccounts = (await api.createKeyPairs(N)).map((kp) => kp.address)
+      const updateRewardAccountsFixture = new UpdateWorkerRewardAccountsFixture(
+        api,
+        query,
+        group,
+        workerIds,
+        newRewardAccounts
+      )
+      const updateRewardAccountsRunner = new FixtureRunner(updateRewardAccountsFixture)
+      await updateRewardAccountsRunner.run()
+      runners.push(updateRewardAccountsRunner)
+
+      const stakeIncreases = workerIds.map((id) => id.muln(1000))
+      // Transfer balances
+      await Promise.all(
+        stakeIncreases.map((amount, i) => api.treasuryTransferBalance(workers[i].staking_account_id.toString(), amount))
+      )
+      const increaseWorkerStakesFixture = new IncreaseWorkerStakesFixture(api, query, group, workerIds, stakeIncreases)
+      const increaseWorkerStakesRunner = new FixtureRunner(increaseWorkerStakesFixture)
+      await increaseWorkerStakesRunner.run()
+      runners.push(increaseWorkerStakesRunner)
+
+      await Promise.all(runners.map((r) => r.runQueryNodeChecks()))
+
+      await debug('Done')
+    })
+  )
+}

+ 136 - 0
tests/integration-tests/src/graphql/generated/queries.ts

@@ -415,6 +415,13 @@ export type GetWorkingGroupMetadataSnapshotsByTimeAscQuery = {
   workingGroupMetadata: Array<WorkingGroupMetadataFieldsFragment>
 }
 
+export type GetWorkersByRuntimeIdsQueryVariables = Types.Exact<{
+  workerIds?: Types.Maybe<Array<Types.Scalars['Int']> | Types.Scalars['Int']>
+  groupId: Types.Scalars['ID']
+}>
+
+export type GetWorkersByRuntimeIdsQuery = { workers: Array<WorkerFieldsFragment> }
+
 export type AppliedOnOpeningEventFieldsFragment = {
   id: string
   event: EventFieldsFragment
@@ -508,6 +515,52 @@ export type GetStatusTextChangedEventsByEventIdsQuery = {
   statusTextChangedEvents: Array<StatusTextChangedEventFieldsFragment>
 }
 
+export type WorkerRoleAccountUpdatedEventFieldsFragment = {
+  id: string
+  newRoleAccount: string
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetWorkerRoleAccountUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetWorkerRoleAccountUpdatedEventsByEventIdsQuery = {
+  workerRoleAccountUpdatedEvents: Array<WorkerRoleAccountUpdatedEventFieldsFragment>
+}
+
+export type WorkerRewardAccountUpdatedEventFieldsFragment = {
+  id: string
+  newRewardAccount: string
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetWorkerRewardAccountUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetWorkerRewardAccountUpdatedEventsByEventIdsQuery = {
+  workerRewardAccountUpdatedEvents: Array<WorkerRewardAccountUpdatedEventFieldsFragment>
+}
+
+export type StakeIncreasedEventFieldsFragment = {
+  id: string
+  amount: any
+  event: EventFieldsFragment
+  group: { name: string }
+  worker: { id: string; runtimeId: number }
+}
+
+export type GetStakeIncreasedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetStakeIncreasedEventsByEventIdsQuery = { stakeIncreasedEvents: Array<StakeIncreasedEventFieldsFragment> }
+
 export const MemberMetadataFields = gql`
   fragment MemberMetadataFields on MemberMetadata {
     name
@@ -1059,6 +1112,57 @@ export const StatusTextChangedEventFields = gql`
   }
   ${EventFields}
 `
+export const WorkerRoleAccountUpdatedEventFields = gql`
+  fragment WorkerRoleAccountUpdatedEventFields on WorkerRoleAccountUpdatedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    newRoleAccount
+  }
+  ${EventFields}
+`
+export const WorkerRewardAccountUpdatedEventFields = gql`
+  fragment WorkerRewardAccountUpdatedEventFields on WorkerRewardAccountUpdatedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    newRewardAccount
+  }
+  ${EventFields}
+`
+export const StakeIncreasedEventFields = gql`
+  fragment StakeIncreasedEventFields on StakeIncreasedEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+    amount
+  }
+  ${EventFields}
+`
 export const GetMemberById = gql`
   query getMemberById($id: ID!) {
     membershipByUniqueInput(where: { id: $id }) {
@@ -1243,6 +1347,14 @@ export const GetWorkingGroupMetadataSnapshotsByTimeAsc = gql`
   }
   ${WorkingGroupMetadataFields}
 `
+export const GetWorkersByRuntimeIds = gql`
+  query getWorkersByRuntimeIds($workerIds: [Int!], $groupId: ID!) {
+    workers(where: { runtimeId_in: $workerIds, groupId_eq: $groupId }) {
+      ...WorkerFields
+    }
+  }
+  ${WorkerFields}
+`
 export const GetAppliedOnOpeningEventsByEventIds = gql`
   query getAppliedOnOpeningEventsByEventIds($eventIds: [ID!]) {
     appliedOnOpeningEvents(where: { eventId_in: $eventIds }) {
@@ -1291,3 +1403,27 @@ export const GetStatusTextChangedEventsByEventIds = gql`
   }
   ${StatusTextChangedEventFields}
 `
+export const GetWorkerRoleAccountUpdatedEventsByEventIds = gql`
+  query getWorkerRoleAccountUpdatedEventsByEventIds($eventIds: [ID!]) {
+    workerRoleAccountUpdatedEvents(where: { eventId_in: $eventIds }) {
+      ...WorkerRoleAccountUpdatedEventFields
+    }
+  }
+  ${WorkerRoleAccountUpdatedEventFields}
+`
+export const GetWorkerRewardAccountUpdatedEventsByEventIds = gql`
+  query getWorkerRewardAccountUpdatedEventsByEventIds($eventIds: [ID!]) {
+    workerRewardAccountUpdatedEvents(where: { eventId_in: $eventIds }) {
+      ...WorkerRewardAccountUpdatedEventFields
+    }
+  }
+  ${WorkerRewardAccountUpdatedEventFields}
+`
+export const GetStakeIncreasedEventsByEventIds = gql`
+  query getStakeIncreasedEventsByEventIds($eventIds: [ID!]) {
+    stakeIncreasedEvents(where: { eventId_in: $eventIds }) {
+      ...StakeIncreasedEventFields
+    }
+  }
+  ${StakeIncreasedEventFields}
+`

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

@@ -221,3 +221,9 @@ query getWorkingGroupMetadataSnapshotsByTimeAsc($groupId: ID!) {
     ...WorkingGroupMetadataFields
   }
 }
+
+query getWorkersByRuntimeIds($workerIds: [Int!], $groupId: ID!) {
+  workers(where: { runtimeId_in: $workerIds, groupId_eq: $groupId }) {
+    ...WorkerFields
+  }
+}

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

@@ -136,3 +136,66 @@ query getStatusTextChangedEventsByEventIds($eventIds: [ID!]) {
     ...StatusTextChangedEventFields
   }
 }
+
+fragment WorkerRoleAccountUpdatedEventFields on WorkerRoleAccountUpdatedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  newRoleAccount
+}
+
+query getWorkerRoleAccountUpdatedEventsByEventIds($eventIds: [ID!]) {
+  workerRoleAccountUpdatedEvents(where: { eventId_in: $eventIds }) {
+    ...WorkerRoleAccountUpdatedEventFields
+  }
+}
+
+fragment WorkerRewardAccountUpdatedEventFields on WorkerRewardAccountUpdatedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  newRewardAccount
+}
+
+query getWorkerRewardAccountUpdatedEventsByEventIds($eventIds: [ID!]) {
+  workerRewardAccountUpdatedEvents(where: { eventId_in: $eventIds }) {
+    ...WorkerRewardAccountUpdatedEventFields
+  }
+}
+
+fragment StakeIncreasedEventFields on StakeIncreasedEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+  amount
+}
+
+query getStakeIncreasedEventsByEventIds($eventIds: [ID!]) {
+  stakeIncreasedEvents(where: { eventId_in: $eventIds }) {
+    ...StakeIncreasedEventFields
+  }
+}

+ 2 - 0
tests/integration-tests/src/scenarios/workingGroups.ts

@@ -2,6 +2,7 @@ import leadOpening from '../flows/working-groups/leadOpening'
 import openingAndApplicationStatus from '../flows/working-groups/openingAndApplicationStatus'
 import upcomingOpenings from '../flows/working-groups/upcomingOpenings'
 import groupStatus from '../flows/working-groups/groupStatus'
+import workerActions from '../flows/working-groups/workerActions'
 import { scenario } from '../Scenario'
 
 scenario(async ({ job }) => {
@@ -9,4 +10,5 @@ scenario(async ({ job }) => {
   job('opening and application status', openingAndApplicationStatus).requires(sudoHireLead)
   job('upcoming openings', upcomingOpenings).requires(sudoHireLead)
   job('group status', groupStatus).requires(sudoHireLead)
+  job('worker actions', workerActions).requires(sudoHireLead)
 })