Selaa lähdekoodia

Merge branch 'iznik' into memberships-refactor-adjustments

Leszek Wiesner 4 vuotta sitten
vanhempi
commit
be89d1ad2e
31 muutettua tiedostoa jossa 6528 lisäystä ja 3 poistoa
  1. 2 2
      tests/network-tests/package.json
  2. 0 0
      tests/network-tests/src/iznik/tap-parallel-not-ok
  3. 65 0
      tests/network-tests/src/iznik/tests/council/electingCouncilTest.ts
  4. 140 0
      tests/network-tests/src/iznik/tests/fixtures/councilElectionModule.ts
  5. 3 0
      tests/network-tests/src/iznik/tests/fixtures/interfaces/fixture.ts
  6. 106 0
      tests/network-tests/src/iznik/tests/fixtures/membershipModule.ts
  7. 1037 0
      tests/network-tests/src/iznik/tests/fixtures/proposalsModule.ts
  8. 1195 0
      tests/network-tests/src/iznik/tests/fixtures/workingGroupModule.ts
  9. 56 0
      tests/network-tests/src/iznik/tests/membership/membershipCreationTest.ts
  10. 79 0
      tests/network-tests/src/iznik/tests/proposals/contentWorkingGroupMintCapacityProposalTest.ts
  11. 75 0
      tests/network-tests/src/iznik/tests/proposals/electionParametersProposalTest.ts
  12. 304 0
      tests/network-tests/src/iznik/tests/proposals/manageLeaderRoleTest.ts
  13. 75 0
      tests/network-tests/src/iznik/tests/proposals/setLeadProposalTest.ts
  14. 78 0
      tests/network-tests/src/iznik/tests/proposals/spendingProposalTest.ts
  15. 69 0
      tests/network-tests/src/iznik/tests/proposals/textProposalTest.ts
  16. 77 0
      tests/network-tests/src/iznik/tests/proposals/updateRuntime.ts
  17. 76 0
      tests/network-tests/src/iznik/tests/proposals/validatorCountProposalTest.ts
  18. 96 0
      tests/network-tests/src/iznik/tests/proposals/workingGroupMintCapacityProposalTest.ts
  19. 154 0
      tests/network-tests/src/iznik/tests/workingGroup/atLeastValueBugTest.ts
  20. 280 0
      tests/network-tests/src/iznik/tests/workingGroup/manageWorkerAsLeadTest.ts
  21. 209 0
      tests/network-tests/src/iznik/tests/workingGroup/manageWorkerAsWorkerTest.ts
  22. 204 0
      tests/network-tests/src/iznik/tests/workingGroup/workerApplicationHappyCaseTest.ts
  23. 204 0
      tests/network-tests/src/iznik/tests/workingGroup/workerApplicationRejectionCaseTest.ts
  24. 243 0
      tests/network-tests/src/iznik/tests/workingGroup/workerPayoutTest.ts
  25. 1551 0
      tests/network-tests/src/iznik/utils/apiWrapper.ts
  26. 8 0
      tests/network-tests/src/iznik/utils/closeApi.ts
  27. 5 0
      tests/network-tests/src/iznik/utils/config.ts
  28. 66 0
      tests/network-tests/src/iznik/utils/sender.ts
  29. 7 0
      tests/network-tests/src/iznik/utils/setTestTimeout.ts
  30. 63 0
      tests/network-tests/src/iznik/utils/utils.ts
  31. 1 1
      types/src/working-group/index.ts

+ 2 - 2
tests/network-tests/package.json

@@ -4,10 +4,10 @@
   "license": "GPL-3.0-only",
   "scripts": {
     "build": "tsc --build tsconfig.json",
-    "test": "tap --files src/nicaea/tests/proposals/*Test.ts --files src/nicaea/tests/workingGroup/*Test.ts -T",
+    "test": "tap --files src/iznik/tests/proposals/*Test.ts --files src/iznik/tests/workingGroup/*Test.ts -T",
     "test-migration-constantinople": "tap --files src/rome/tests/romeRuntimeUpgradeTest.ts --files src/constantinople/tests/electingCouncilTest.ts -T",
     "test-migration-nicaea": "tap --files src/constantinople/tests/proposals/updateRuntimeTest.ts --files src/nicaea/tests/electingCouncilTest.ts -T",
-    "debug": "tap --files src/nicaea/tests/workingGroup/*Test.ts -T",
+    "debug": "tap --files src/iznik/tests/proposals/manageLeaderRoleTest.ts -T",
     "lint": "eslint . --quiet --ext .ts",
     "checks": "yarn lint && tsc --noEmit --pretty && prettier ./ --check",
     "format": "prettier ./ --write "

+ 0 - 0
tests/network-tests/src/iznik/tap-parallel-not-ok


+ 65 - 0
tests/network-tests/src/iznik/tests/council/electingCouncilTest.ts

@@ -0,0 +1,65 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import BN from 'bn.js'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { closeApi } from '../../utils/closeApi'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { Utils } from '../../utils/utils'
+
+tap.mocha.describe('Electing council scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+
+  const durationInBlocks = 25
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 140 - 0
tests/network-tests/src/iznik/tests/fixtures/councilElectionModule.ts

@@ -0,0 +1,140 @@
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { KeyringPair } from '@polkadot/keyring/types'
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Seat } from '@nicaea/types/council'
+import { v4 as uuid } from 'uuid'
+import { Utils } from '../../utils/utils'
+import { Fixture } from './interfaces/fixture'
+
+export class ElectCouncilFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private k: number
+  private sudo: KeyringPair
+  private greaterStake: BN
+  private lesserStake: BN
+
+  public constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    m2KeyPairs: KeyringPair[],
+    k: number,
+    sudo: KeyringPair,
+    greaterStake: BN,
+    lesserStake: BN
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.k = k
+    this.sudo = sudo
+    this.greaterStake = greaterStake
+    this.lesserStake = lesserStake
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    let now = await this.apiWrapper.getBestBlock()
+    const applyForCouncilFee: BN = this.apiWrapper.estimateApplyForCouncilFee(this.greaterStake)
+    const voteForCouncilFee: BN = this.apiWrapper.estimateVoteForCouncilFee(
+      this.sudo.address,
+      this.sudo.address,
+      this.greaterStake
+    )
+    const salt: string[] = []
+    this.m1KeyPairs.forEach(() => {
+      salt.push(''.concat(uuid().replace(/-/g, '')))
+    })
+    const revealVoteFee: BN = this.apiWrapper.estimateRevealVoteFee(this.sudo.address, salt[0])
+
+    // Topping the balances
+    await this.apiWrapper.transferBalanceToAccounts(
+      this.sudo,
+      this.m2KeyPairs,
+      applyForCouncilFee.add(this.greaterStake)
+    )
+    await this.apiWrapper.transferBalanceToAccounts(
+      this.sudo,
+      this.m1KeyPairs,
+      voteForCouncilFee.add(revealVoteFee).add(this.greaterStake)
+    )
+
+    // First K members stake more
+    await this.apiWrapper.sudoStartAnnouncingPerion(this.sudo, now.addn(100))
+    await this.apiWrapper.batchApplyForCouncilElection(this.m2KeyPairs.slice(0, this.k), this.greaterStake)
+    this.m2KeyPairs.slice(0, this.k).forEach((keyPair) =>
+      this.apiWrapper.getCouncilElectionStake(keyPair.address).then((stake) => {
+        assert(
+          stake.eq(this.greaterStake),
+          `${keyPair.address} not applied correctly for council election with stake ${stake} versus expected ${this.greaterStake}`
+        )
+      })
+    )
+
+    // Last members stake less
+    await this.apiWrapper.batchApplyForCouncilElection(this.m2KeyPairs.slice(this.k), this.lesserStake)
+    this.m2KeyPairs.slice(this.k).forEach((keyPair) =>
+      this.apiWrapper.getCouncilElectionStake(keyPair.address).then((stake) => {
+        assert(
+          stake.eq(this.lesserStake),
+          `${keyPair.address} not applied correctrly for council election with stake ${stake} versus expected ${this.lesserStake}`
+        )
+      })
+    )
+
+    // Voting
+    await this.apiWrapper.sudoStartVotingPerion(this.sudo, now.addn(100))
+    await this.apiWrapper.batchVoteForCouncilMember(
+      this.m1KeyPairs.slice(0, this.k),
+      this.m2KeyPairs.slice(0, this.k),
+      salt.slice(0, this.k),
+      this.lesserStake
+    )
+    await this.apiWrapper.batchVoteForCouncilMember(
+      this.m1KeyPairs.slice(this.k),
+      this.m2KeyPairs.slice(this.k),
+      salt.slice(this.k),
+      this.greaterStake
+    )
+
+    // Revealing
+    await this.apiWrapper.sudoStartRevealingPerion(this.sudo, now.addn(100))
+    await this.apiWrapper.batchRevealVote(
+      this.m1KeyPairs.slice(0, this.k),
+      this.m2KeyPairs.slice(0, this.k),
+      salt.slice(0, this.k)
+    )
+    await this.apiWrapper.batchRevealVote(
+      this.m1KeyPairs.slice(this.k),
+      this.m2KeyPairs.slice(this.k),
+      salt.slice(this.k)
+    )
+    now = await this.apiWrapper.getBestBlock()
+
+    // Resolving election
+    // 3 is to ensure the revealing block is in future
+    await this.apiWrapper.sudoStartRevealingPerion(this.sudo, now.addn(3))
+    await Utils.wait(this.apiWrapper.getBlockDuration().muln(2.5).toNumber())
+    const seats: Seat[] = await this.apiWrapper.getCouncil()
+
+    // Preparing collections to increase assertion readability
+    const m2addresses: string[] = this.m2KeyPairs.map((keyPair) => keyPair.address)
+    const m1addresses: string[] = this.m1KeyPairs.map((keyPair) => keyPair.address)
+    const members: string[] = seats.map((seat) => seat.member.toString())
+    const bakers: string[] = seats.map((seat) => seat.backers.map((baker) => baker.member.toString())).flat()
+
+    // Assertions
+    m2addresses.forEach((address) => assert(members.includes(address), `Account ${address} is not in the council`))
+    m1addresses.forEach((address) => assert(bakers.includes(address), `Account ${address} is not in the voters`))
+    seats.forEach((seat) =>
+      assert(
+        Utils.getTotalStake(seat).eq(this.greaterStake.add(this.lesserStake)),
+        `Member ${seat.member} has unexpected stake ${Utils.getTotalStake(seat)}`
+      )
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}

+ 3 - 0
tests/network-tests/src/iznik/tests/fixtures/interfaces/fixture.ts

@@ -0,0 +1,3 @@
+export interface Fixture {
+  runner(expectFailure: boolean): Promise<void>
+}

+ 106 - 0
tests/network-tests/src/iznik/tests/fixtures/membershipModule.ts

@@ -0,0 +1,106 @@
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { KeyringPair } from '@polkadot/keyring/types'
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { Fixture } from './interfaces/fixture'
+
+export class BuyMembershipHappyCaseFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private sudo: KeyringPair
+  private keyPairs: KeyringPair[]
+  private paidTerms: number
+
+  public constructor(apiWrapper: ApiWrapper, sudo: KeyringPair, keyPairs: KeyringPair[], paidTerms: number) {
+    this.apiWrapper = apiWrapper
+    this.sudo = sudo
+    this.keyPairs = keyPairs
+    this.paidTerms = paidTerms
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const membershipFee: BN = await this.apiWrapper.getMembershipFee(this.paidTerms)
+    const membershipTransactionFee: BN = this.apiWrapper.estimateBuyMembershipFee(
+      this.sudo,
+      this.paidTerms,
+      'member_name_which_is_longer_than_expected'
+    )
+    await this.apiWrapper.transferBalanceToAccounts(
+      this.sudo,
+      this.keyPairs,
+      membershipTransactionFee.add(new BN(membershipFee))
+    )
+
+    // Buying membership
+    await Promise.all(
+      this.keyPairs.map(async (keyPair, index) => {
+        await this.apiWrapper.buyMembership(
+          keyPair,
+          this.paidTerms,
+          `new_member_${index}${keyPair.address.substring(0, 8)}`
+        )
+      })
+    )
+
+    // Assertions
+    this.keyPairs.forEach((keyPair) =>
+      this.apiWrapper
+        .getMemberIds(keyPair.address)
+        .then((membership) => assert(membership.length > 0, `Account ${keyPair.address} is not a member`))
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class BuyMembershipWithInsufficienFundsFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private sudo: KeyringPair
+  private aKeyPair: KeyringPair
+  private paidTerms: number
+
+  public constructor(apiWrapper: ApiWrapper, sudo: KeyringPair, aKeyPair: KeyringPair, paidTerms: number) {
+    this.apiWrapper = apiWrapper
+    this.sudo = sudo
+    this.aKeyPair = aKeyPair
+    this.paidTerms = paidTerms
+  }
+
+  public async runner(expectFailure: boolean) {
+    // Fee estimation and transfer
+    const membershipFee: BN = await this.apiWrapper.getMembershipFee(this.paidTerms)
+    const membershipTransactionFee: BN = this.apiWrapper.estimateBuyMembershipFee(
+      this.sudo,
+      this.paidTerms,
+      'member_name_which_is_longer_than_expected'
+    )
+    await this.apiWrapper.transferBalance(this.sudo, this.aKeyPair.address, membershipTransactionFee)
+
+    // Balance assertion
+    await this.apiWrapper
+      .getBalance(this.aKeyPair.address)
+      .then((balance) =>
+        assert(
+          balance.toBn() < membershipFee.add(membershipTransactionFee),
+          'Account A already have sufficient balance to purchase membership'
+        )
+      )
+
+    // Buying memebership
+    await this.apiWrapper.buyMembership(
+      this.aKeyPair,
+      this.paidTerms,
+      `late_member_${this.aKeyPair.address.substring(0, 8)}`,
+      true
+    )
+
+    // Assertions
+    this.apiWrapper
+      .getMemberIds(this.aKeyPair.address)
+      .then((membership) => assert(membership.length === 0, 'Account A is a member'))
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}

+ 1037 - 0
tests/network-tests/src/iznik/tests/fixtures/proposalsModule.ts

@@ -0,0 +1,1037 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { v4 as uuid } from 'uuid'
+import BN from 'bn.js'
+import { FillOpeningParameters } from '@nicaea/types/proposals'
+import { Fixture } from './interfaces/fixture'
+import { Bytes, Option, u32 } from '@polkadot/types'
+import { Balance, BlockNumber } from '@polkadot/types/interfaces'
+import { assert } from 'chai'
+import {
+  ActivateOpeningAt,
+  ApplicationId,
+  ApplicationRationingPolicy,
+  OpeningId,
+  StakingPolicy,
+} from '@nicaea/types/hiring'
+import { RewardPolicy, SlashingTerms, WorkingGroupOpeningPolicyCommitment } from '@nicaea/types/working-group'
+import { WorkingGroup } from '@nicaea/types/common'
+
+export class CreateWorkingGroupLeaderOpeningFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private applicationStake: BN
+  private roleStake: BN
+  private workingGroup: string
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    applicationStake: BN,
+    roleStake: BN,
+    workingGroup: string
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.sudo = sudo
+    this.applicationStake = applicationStake
+    this.roleStake = roleStake
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing working group lead opening proposal ' + uuid().substring(0, 8)
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(100000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeCreateWorkingGroupLeaderOpeningFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Opening construction
+    const activateAtBlock: ActivateOpeningAt = new ActivateOpeningAt('CurrentBlock')
+    const commitment: WorkingGroupOpeningPolicyCommitment = new WorkingGroupOpeningPolicyCommitment({
+      application_rationing_policy: new Option(ApplicationRationingPolicy, {
+        max_active_applicants: new BN(this.m1KeyPairs.length) as u32,
+      }),
+      max_review_period_length: new BN(32) as u32,
+      application_staking_policy: new Option(StakingPolicy, {
+        amount: this.applicationStake,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_staking_policy: new Option(StakingPolicy, {
+        amount: this.roleStake,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_slashing_terms: new SlashingTerms({
+        Slashable: {
+          max_count: new BN(1),
+          max_percent_pts_per_time: new BN(100),
+        },
+      }),
+      fill_opening_successful_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+    })
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeCreateWorkingGroupLeaderOpening(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      activateAtBlock,
+      commitment,
+      uuid().substring(0, 8),
+      this.workingGroup
+    )
+    const proposalNumber: BN = await proposalPromise
+    this.result = proposalNumber
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class BeginWorkingGroupLeaderApplicationReviewFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private openingId: BN
+  private workingGroup: string
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    openingId: BN,
+    workingGroup: string
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.sudo = sudo
+    this.openingId = openingId
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing begin working group lead application review proposal ' + uuid().substring(0, 8)
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(25000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeBeginWorkingGroupLeaderApplicationReviewFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeBeginWorkingGroupLeaderApplicationReview(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      this.openingId,
+      this.workingGroup
+    )
+    this.result = await proposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class FillLeaderOpeningProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private applicantRoleAccountAddress: string
+  private sudo: KeyringPair
+  private firstRewardInterval: BN
+  private rewardInterval: BN
+  private payoutAmount: BN
+  private openingId: BN
+  private workingGroup: WorkingGroups
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    applicantRoleAccountAddress: string,
+    sudo: KeyringPair,
+    firstRewardInterval: BN,
+    rewardInterval: BN,
+    payoutAmount: BN,
+    openingId: BN,
+    workingGroup: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.applicantRoleAccountAddress = applicantRoleAccountAddress
+    this.sudo = sudo
+    this.firstRewardInterval = firstRewardInterval
+    this.rewardInterval = rewardInterval
+    this.payoutAmount = payoutAmount
+    this.openingId = openingId
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing fill opening proposal ' + uuid().substring(0, 8)
+    const workingGroupString: string = this.apiWrapper.getWorkingGroupString(this.workingGroup)
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(50000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeFillLeaderOpeningFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const applicationId: BN = (
+      await this.apiWrapper.getActiveApplicationsIdsByRoleAccount(this.applicantRoleAccountAddress, this.workingGroup)
+    )[0]
+    const now = await this.apiWrapper.getBestBlock()
+
+    const fillOpeningParameters: FillOpeningParameters = new FillOpeningParameters({
+      opening_id: this.openingId as OpeningId,
+      successful_application_id: applicationId as ApplicationId,
+      reward_policy: new Option(
+        RewardPolicy,
+        new RewardPolicy({
+          amount_per_payout: this.payoutAmount as Balance,
+          next_payment_at_block: now.add(this.firstRewardInterval) as BlockNumber,
+          payout_interval: new Option(u32, this.rewardInterval as u32),
+        })
+      ),
+      working_group: new WorkingGroup(workingGroupString),
+    })
+
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeFillLeaderOpening(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      fillOpeningParameters
+    )
+    this.result = await proposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class TerminateLeaderRoleProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private leaderRoleAccountAddress: string
+  private sudo: KeyringPair
+  private slash: boolean
+  private workingGroup: WorkingGroups
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    leaderRoleAccountAddress: string,
+    sudo: KeyringPair,
+    slash: boolean,
+    workingGroup: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.leaderRoleAccountAddress = leaderRoleAccountAddress
+    this.sudo = sudo
+    this.slash = slash
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing begin working group lead application review proposal ' + uuid().substring(0, 8)
+    const rationale: string = 'Testing leader termination ' + uuid().substring(0, 8)
+    const workingGroupString: string = this.apiWrapper.getWorkingGroupString(this.workingGroup)
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(
+      this.leaderRoleAccountAddress,
+      this.workingGroup
+    )
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(100000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeTerminateLeaderRoleFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeTerminateLeaderRole(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      workerId,
+      rationale,
+      this.slash,
+      workingGroupString
+    )
+    this.result = await proposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class SetLeaderRewardProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private payoutAmount: BN
+  private workingGroup: WorkingGroups
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    payoutAmount: BN,
+    workingGroup: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.sudo = sudo
+    this.payoutAmount = payoutAmount
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing set leader reward proposal ' + uuid().substring(0, 8)
+    const workingGroupString: string = this.apiWrapper.getWorkingGroupString(this.workingGroup)
+    const workerId: BN = (await this.apiWrapper.getLeadWorkerId(this.workingGroup))!
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(50000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeLeaderRewardFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeLeaderReward(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      workerId,
+      this.payoutAmount,
+      workingGroupString
+    )
+    this.result = await proposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class DecreaseLeaderStakeProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private stakeDecrement: BN
+  private workingGroup: WorkingGroups
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    stakeDecrement: BN,
+    workingGroup: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.sudo = sudo
+    this.stakeDecrement = stakeDecrement
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing decrease leader stake proposal ' + uuid().substring(0, 8)
+    const workingGroupString: string = this.apiWrapper.getWorkingGroupString(this.workingGroup)
+    const workerId: BN = (await this.apiWrapper.getLeadWorkerId(this.workingGroup))!
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(50000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeDecreaseLeaderStakeFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeDecreaseLeaderStake(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      workerId,
+      this.stakeDecrement,
+      workingGroupString
+    )
+    this.result = await proposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class SlashLeaderProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private slashAmount: BN
+  private workingGroup: WorkingGroups
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    slashAmount: BN,
+    workingGroup: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.sudo = sudo
+    this.slashAmount = slashAmount
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing slash leader stake proposal ' + uuid().substring(0, 8)
+    const workingGroupString: string = this.apiWrapper.getWorkingGroupString(this.workingGroup)
+    const workerId: BN = (await this.apiWrapper.getLeadWorkerId(this.workingGroup))!
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(50000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeSlashLeaderStakeFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeSlashLeaderStake(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      workerId,
+      this.slashAmount,
+      workingGroupString
+    )
+    this.result = await proposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class WorkingGroupMintCapacityProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private mintCapacity: BN
+  private workingGroup: WorkingGroups
+
+  private result: BN | undefined
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    mintCapacity: BN,
+    workingGroup: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.sudo = sudo
+    this.mintCapacity = mintCapacity
+    this.workingGroup = workingGroup
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing working group mint capacity proposal ' + uuid().substring(0, 8)
+    const workingGroupString: string = this.apiWrapper.getWorkingGroupString(this.workingGroup)
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(50000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeWorkingGroupMintCapacityFee()
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeWorkingGroupMintCapacity(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      this.mintCapacity,
+      workingGroupString
+    )
+    this.result = await proposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ElectionParametersProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+
+  constructor(apiWrapper: ApiWrapper, m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[], sudo: KeyringPair) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8)
+    const runtimeVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, runtimeVoteFee)
+    const announcingPeriod: BN = await this.apiWrapper.getAnnouncingPeriod()
+    const votingPeriod: BN = await this.apiWrapper.getVotingPeriod()
+    const revealingPeriod: BN = await this.apiWrapper.getRevealingPeriod()
+    const councilSize: BN = await this.apiWrapper.getCouncilSize()
+    const candidacyLimit: BN = await this.apiWrapper.getCandidacyLimit()
+    const newTermDuration: BN = await this.apiWrapper.getNewTermDuration()
+    const minCouncilStake: BN = await this.apiWrapper.getMinCouncilStake()
+    const minVotingStake: BN = await this.apiWrapper.getMinVotingStake()
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(200000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeElectionParametersFee(
+      description,
+      description,
+      proposalStake,
+      announcingPeriod,
+      votingPeriod,
+      revealingPeriod,
+      councilSize,
+      candidacyLimit,
+      newTermDuration,
+      minCouncilStake,
+      minVotingStake
+    )
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposedAnnouncingPeriod: BN = announcingPeriod.subn(1)
+    const proposedVotingPeriod: BN = votingPeriod.addn(1)
+    const proposedRevealingPeriod: BN = revealingPeriod.addn(1)
+    const proposedCouncilSize: BN = councilSize.addn(1)
+    const proposedCandidacyLimit: BN = candidacyLimit.addn(1)
+    const proposedNewTermDuration: BN = newTermDuration.addn(1)
+    const proposedMinCouncilStake: BN = minCouncilStake.addn(1)
+    const proposedMinVotingStake: BN = minVotingStake.addn(1)
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeElectionParameters(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      proposedAnnouncingPeriod,
+      proposedVotingPeriod,
+      proposedRevealingPeriod,
+      proposedCouncilSize,
+      proposedCandidacyLimit,
+      proposedNewTermDuration,
+      proposedMinCouncilStake,
+      proposedMinVotingStake
+    )
+    const proposalNumber = await proposalPromise
+
+    // Approving the proposal
+    const proposalExecutionPromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, proposalNumber)
+    await proposalExecutionPromise
+
+    // Assertions
+    const newAnnouncingPeriod: BN = await this.apiWrapper.getAnnouncingPeriod()
+    const newVotingPeriod: BN = await this.apiWrapper.getVotingPeriod()
+    const newRevealingPeriod: BN = await this.apiWrapper.getRevealingPeriod()
+    const newCouncilSize: BN = await this.apiWrapper.getCouncilSize()
+    const newCandidacyLimit: BN = await this.apiWrapper.getCandidacyLimit()
+    const newNewTermDuration: BN = await this.apiWrapper.getNewTermDuration()
+    const newMinCouncilStake: BN = await this.apiWrapper.getMinCouncilStake()
+    const newMinVotingStake: BN = await this.apiWrapper.getMinVotingStake()
+    assert(
+      proposedAnnouncingPeriod.eq(newAnnouncingPeriod),
+      `Announcing period has unexpected value ${newAnnouncingPeriod}, expected ${proposedAnnouncingPeriod}`
+    )
+    assert(
+      proposedVotingPeriod.eq(newVotingPeriod),
+      `Voting period has unexpected value ${newVotingPeriod}, expected ${proposedVotingPeriod}`
+    )
+    assert(
+      proposedRevealingPeriod.eq(newRevealingPeriod),
+      `Revealing has unexpected value ${newRevealingPeriod}, expected ${proposedRevealingPeriod}`
+    )
+    assert(
+      proposedCouncilSize.eq(newCouncilSize),
+      `Council size has unexpected value ${newCouncilSize}, expected ${proposedCouncilSize}`
+    )
+    assert(
+      proposedCandidacyLimit.eq(newCandidacyLimit),
+      `Candidacy limit has unexpected value ${newCandidacyLimit}, expected ${proposedCandidacyLimit}`
+    )
+    assert(
+      proposedNewTermDuration.eq(newNewTermDuration),
+      `New term duration has unexpected value ${newNewTermDuration}, expected ${proposedNewTermDuration}`
+    )
+    assert(
+      proposedMinCouncilStake.eq(newMinCouncilStake),
+      `Min council stake has unexpected value ${newMinCouncilStake}, expected ${proposedMinCouncilStake}`
+    )
+    assert(
+      proposedMinVotingStake.eq(newMinVotingStake),
+      `Min voting stake has unexpected value ${newMinVotingStake}, expected ${proposedMinVotingStake}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class SetLeadProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+
+  constructor(apiWrapper: ApiWrapper, m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[], sudo: KeyringPair) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8)
+    const runtimeVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, runtimeVoteFee)
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(50000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeLeadFee(
+      description,
+      description,
+      proposalStake,
+      this.sudo.address
+    )
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeLead(this.m1KeyPairs[0], proposalTitle, description, proposalStake, this.m1KeyPairs[1])
+    const proposalNumber = await proposalPromise
+
+    // Approving the proposal
+    const proposalExecutionPromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, proposalNumber)
+    await proposalExecutionPromise
+    const newLead: string = await this.apiWrapper.getCurrentLeadAddress()
+    assert(
+      newLead === this.m1KeyPairs[1].address,
+      `New lead has unexpected value ${newLead}, expected ${this.m1KeyPairs[1].address}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class SpendingProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private spendingBalance: BN
+  private mintCapacity: BN
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    m2KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    spendingBalance: BN,
+    mintCapacity: BN
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+    this.spendingBalance = spendingBalance
+    this.mintCapacity = mintCapacity
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const description = 'spending proposal which is used for API network testing with some mock data'
+    const runtimeVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+
+    // Topping the balances
+    const proposalStake: BN = new BN(25000)
+    const runtimeProposalFee: BN = this.apiWrapper.estimateProposeSpendingFee(
+      description,
+      description,
+      proposalStake,
+      this.spendingBalance,
+      this.sudo.address
+    )
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake))
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, runtimeVoteFee)
+    await this.apiWrapper.sudoSetCouncilMintCapacity(this.sudo, this.mintCapacity)
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeSpending(
+      this.m1KeyPairs[0],
+      'testing spending' + uuid().substring(0, 8),
+      'spending to test proposal functionality' + uuid().substring(0, 8),
+      proposalStake,
+      this.spendingBalance,
+      this.sudo.address
+    )
+    const proposalNumber = await proposalPromise
+
+    // Approving spending proposal
+    const balanceBeforeMinting: BN = await this.apiWrapper.getBalance(this.sudo.address)
+    const spendingPromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, proposalNumber)
+    await spendingPromise
+    const balanceAfterMinting: BN = await this.apiWrapper.getBalance(this.sudo.address)
+    assert(
+      balanceAfterMinting.sub(balanceBeforeMinting).eq(this.spendingBalance),
+      `member ${
+        this.m1KeyPairs[0].address
+      } has unexpected balance ${balanceAfterMinting}, expected ${balanceBeforeMinting.add(this.spendingBalance)}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class TextProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+
+  constructor(apiWrapper: ApiWrapper, m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[], sudo: KeyringPair) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing text proposal ' + uuid().substring(0, 8)
+    const proposalText: string = 'Text of the testing proposal ' + uuid().substring(0, 8)
+    const runtimeVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, runtimeVoteFee)
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(25000)
+    const runtimeProposalFee: BN = this.apiWrapper.estimateProposeTextFee(
+      proposalStake,
+      description,
+      description,
+      proposalText
+    )
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake))
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeText(this.m1KeyPairs[0], proposalStake, proposalTitle, description, proposalText)
+    const proposalNumber = await proposalPromise
+
+    // Approving text proposal
+    const textProposalPromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, proposalNumber)
+    await textProposalPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ValidatorCountProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private validatorCountIncrement: BN
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    m2KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    validatorCountIncrement: BN
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+    this.validatorCountIncrement = validatorCountIncrement
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8)
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8)
+    const runtimeVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, runtimeVoteFee)
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(100000)
+    const proposalFee: BN = this.apiWrapper.estimateProposeValidatorCountFee(description, description, proposalStake)
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, proposalFee.add(proposalStake))
+    const validatorCount: BN = await this.apiWrapper.getValidatorCount()
+
+    // Proposal creation
+    const proposedValidatorCount: BN = validatorCount.add(this.validatorCountIncrement)
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeValidatorCount(
+      this.m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      proposedValidatorCount
+    )
+    const proposalNumber = await proposalPromise
+
+    // Approving the proposal
+    const proposalExecutionPromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, proposalNumber)
+    await proposalExecutionPromise
+    const newValidatorCount: BN = await this.apiWrapper.getValidatorCount()
+    assert(
+      proposedValidatorCount.eq(newValidatorCount),
+      `Validator count has unexpeccted value ${newValidatorCount}, expected ${proposedValidatorCount}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ContentWorkingGroupMintCapacityProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private mintingCapacityIncrement: BN
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    m1KeyPairs: KeyringPair[],
+    m2KeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    mintingCapacityIncrement: BN
+  ) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+    this.mintingCapacityIncrement = mintingCapacityIncrement
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const description = 'Mint capacity proposal which is used for API network testing'
+    const runtimeVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+    const initialMintingCapacity: BN = await this.apiWrapper.getContentWorkingGroupMintCapacity()
+
+    // Topping the balances
+    const proposalStake: BN = new BN(50000)
+    const runtimeProposalFee: BN = this.apiWrapper.estimateProposeContentWorkingGroupMintCapacityFee(
+      description,
+      description,
+      proposalStake,
+      initialMintingCapacity.add(this.mintingCapacityIncrement)
+    )
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake))
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, runtimeVoteFee)
+
+    // Proposal creation
+    const proposedMintingCapacity: BN = initialMintingCapacity.add(this.mintingCapacityIncrement)
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeContentWorkingGroupMintCapacity(
+      this.m1KeyPairs[0],
+      'testing mint capacity' + uuid().substring(0, 8),
+      'mint capacity to test proposal functionality' + uuid().substring(0, 8),
+      proposalStake,
+      proposedMintingCapacity
+    )
+    const proposalNumber = await proposalPromise
+
+    // Approving mint capacity proposal
+    const mintCapacityPromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, proposalNumber)
+    await mintCapacityPromise
+    const newMintingCapacity: BN = await this.apiWrapper.getContentWorkingGroupMintCapacity()
+    assert(
+      proposedMintingCapacity.eq(newMintingCapacity),
+      `Content working group has unexpected minting capacity ${newMintingCapacity}, expected ${proposedMintingCapacity}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class UpdateRuntimeFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m1KeyPairs: KeyringPair[]
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+
+  constructor(apiWrapper: ApiWrapper, m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[], sudo: KeyringPair) {
+    this.apiWrapper = apiWrapper
+    this.m1KeyPairs = m1KeyPairs
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Setup
+    const runtime: Bytes = await this.apiWrapper.getRuntime()
+    const description = 'runtime upgrade proposal which is used for API network testing'
+    const runtimeVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+
+    // Topping the balances
+    const proposalStake: BN = new BN(1000000)
+    const runtimeProposalFee: BN = this.apiWrapper.estimateProposeRuntimeUpgradeFee(
+      proposalStake,
+      description,
+      description,
+      runtime
+    )
+    await this.apiWrapper.transferBalance(this.sudo, this.m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake))
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, runtimeVoteFee)
+
+    // Proposal creation
+    const proposalPromise = this.apiWrapper.expectProposalCreated()
+    await this.apiWrapper.proposeRuntime(
+      this.m1KeyPairs[0],
+      proposalStake,
+      'testing runtime' + uuid().substring(0, 8),
+      'runtime to test proposal functionality' + uuid().substring(0, 8),
+      runtime
+    )
+    const proposalNumber = await proposalPromise
+
+    // Approving runtime update proposal
+    const runtimePromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, proposalNumber)
+    await runtimePromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class VoteForProposalFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private m2KeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private proposalNumber: BN
+
+  constructor(apiWrapper: ApiWrapper, m2KeyPairs: KeyringPair[], sudo: KeyringPair, proposalNumber: BN) {
+    this.apiWrapper = apiWrapper
+    this.m2KeyPairs = m2KeyPairs
+    this.sudo = sudo
+    this.proposalNumber = proposalNumber
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const proposalVoteFee: BN = this.apiWrapper.estimateVoteForProposalFee()
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.m2KeyPairs, proposalVoteFee)
+
+    // Approving the proposal
+    const proposalExecutionPromise = this.apiWrapper.expectProposalFinalized()
+    await this.apiWrapper.batchApproveProposal(this.m2KeyPairs, this.proposalNumber)
+    await proposalExecutionPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}

+ 1195 - 0
tests/network-tests/src/iznik/tests/fixtures/workingGroupModule.ts

@@ -0,0 +1,1195 @@
+import BN from 'bn.js'
+import { assert } from 'chai'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { Balance, BlockNumber, Event } from '@polkadot/types/interfaces'
+import { Keyring } from '@polkadot/api'
+import { Option, u32 } from '@polkadot/types'
+import { v4 as uuid } from 'uuid'
+import { RewardRelationship } from '@nicaea/types/recurring-rewards'
+import {
+  Application,
+  ApplicationIdToWorkerIdMap,
+  SlashingTerms,
+  Worker,
+  WorkingGroupOpeningPolicyCommitment,
+} from '@nicaea/types/working-group'
+import { Utils } from '../../utils/utils'
+import {
+  ActivateOpeningAt,
+  ApplicationRationingPolicy,
+  Opening as HiringOpening,
+  StakingPolicy,
+} from '@nicaea/types/hiring'
+import { Fixture } from './interfaces/fixture'
+
+export class AddWorkerOpeningFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private applicationStake: BN
+  private roleStake: BN
+  private activationDelay: BN
+  private unstakingPeriod: BN
+  private module: WorkingGroups
+
+  private result: BN | undefined
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    lead: KeyringPair,
+    sudo: KeyringPair,
+    applicationStake: BN,
+    roleStake: BN,
+    activationDelay: BN,
+    unstakingPeriod: BN,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.lead = lead
+    this.sudo = sudo
+    this.applicationStake = applicationStake
+    this.roleStake = roleStake
+    this.activationDelay = activationDelay
+    this.unstakingPeriod = unstakingPeriod
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Worker opening construction
+    const activateAtBlock: ActivateOpeningAt = new ActivateOpeningAt(
+      this.activationDelay.eqn(0)
+        ? 'CurrentBlock'
+        : { ExactBlock: (await this.apiWrapper.getBestBlock()).add(this.activationDelay) }
+    )
+    const commitment: WorkingGroupOpeningPolicyCommitment = new WorkingGroupOpeningPolicyCommitment({
+      application_rationing_policy: new Option(ApplicationRationingPolicy, {
+        max_active_applicants: new BN(this.membersKeyPairs.length) as u32,
+      }),
+      max_review_period_length: new BN(32) as u32,
+      application_staking_policy: new Option(StakingPolicy, {
+        amount: this.applicationStake,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_staking_policy: new Option(StakingPolicy, {
+        amount: this.roleStake,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_slashing_terms: new SlashingTerms({
+        Slashable: {
+          max_count: new BN(1),
+          max_percent_pts_per_time: new BN(100),
+        },
+      }),
+      fill_opening_successful_applicant_application_stake_unstaking_period: new Option(
+        u32,
+        this.unstakingPeriod as BlockNumber
+      ),
+      fill_opening_failed_applicant_application_stake_unstaking_period: new Option(
+        u32,
+        this.unstakingPeriod as BlockNumber
+      ),
+      fill_opening_failed_applicant_role_stake_unstaking_period: new Option(u32, this.unstakingPeriod as BlockNumber),
+      terminate_application_stake_unstaking_period: new Option(u32, this.unstakingPeriod as BlockNumber),
+      terminate_role_stake_unstaking_period: new Option(u32, this.unstakingPeriod as BlockNumber),
+      exit_role_application_stake_unstaking_period: new Option(u32, this.unstakingPeriod as BlockNumber),
+      exit_role_stake_unstaking_period: new Option(u32, this.unstakingPeriod as BlockNumber),
+    })
+
+    // Fee estimation and transfer
+    const addOpeningFee: BN = this.apiWrapper.estimateAddOpeningFee(this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.lead.address, addOpeningFee)
+
+    // Worker opening creation
+    const addOpeningPromise: Promise<Event> = this.apiWrapper.expectEvent('OpeningAdded')
+    await this.apiWrapper.addOpening(
+      this.lead,
+      activateAtBlock,
+      commitment,
+      uuid().substring(0, 8),
+      'Worker',
+      this.module,
+      expectFailure
+    )
+    if (!expectFailure) {
+      const openingId: BN = ((await addOpeningPromise).data[0] as unknown) as BN
+      console.log('received opening id ' + openingId)
+      this.result = openingId
+    }
+  }
+}
+
+export class AddLeaderOpeningFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private applicationStake: BN
+  private roleStake: BN
+  private activationDelay: BN
+  private module: WorkingGroups
+
+  private result: BN | undefined
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    applicationStake: BN,
+    roleStake: BN,
+    activationDelay: BN,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.sudo = sudo
+    this.applicationStake = applicationStake
+    this.roleStake = roleStake
+    this.activationDelay = activationDelay
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Leader opening creation
+    const activateAtBlock: ActivateOpeningAt = new ActivateOpeningAt(
+      this.activationDelay.eqn(0)
+        ? 'CurrentBlock'
+        : { ExactBlock: (await this.apiWrapper.getBestBlock()).add(this.activationDelay) }
+    )
+    const commitment: WorkingGroupOpeningPolicyCommitment = new WorkingGroupOpeningPolicyCommitment({
+      application_rationing_policy: new Option(ApplicationRationingPolicy, {
+        max_active_applicants: new BN(this.membersKeyPairs.length) as u32,
+      }),
+      max_review_period_length: new BN(32) as u32,
+      application_staking_policy: new Option(StakingPolicy, {
+        amount: this.applicationStake,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_staking_policy: new Option(StakingPolicy, {
+        amount: this.roleStake,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_slashing_terms: new SlashingTerms({
+        Slashable: {
+          max_count: new BN(1),
+          max_percent_pts_per_time: new BN(100),
+        },
+      }),
+      fill_opening_successful_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+    })
+
+    const addOpeningPromise: Promise<Event> = this.apiWrapper.expectEvent('OpeningAdded')
+    await this.apiWrapper.sudoAddOpening(
+      this.sudo,
+      activateAtBlock,
+      commitment,
+      uuid().substring(0, 8),
+      'Leader',
+      this.module
+    )
+    const openingId: BN = ((await addOpeningPromise).data[0] as unknown) as BN
+    this.result = openingId
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class AcceptApplicationsFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private openingId: BN
+  private module: WorkingGroups
+
+  public constructor(
+    apiWrapper: ApiWrapper,
+    lead: KeyringPair,
+    sudo: KeyringPair,
+    openingId: BN,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.lead = lead
+    this.sudo = sudo
+    this.openingId = openingId
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const acceptApplicationsFee = this.apiWrapper.estimateAcceptApplicationsFee(this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.lead.address, acceptApplicationsFee)
+
+    // Begin accepting applications
+    await this.apiWrapper.acceptApplications(this.lead, this.openingId, this.module)
+
+    const opening: HiringOpening = await this.apiWrapper.getHiringOpening(this.openingId)
+    assert(opening.is_active, `Opening ${this.openingId} is not active`)
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ApplyForOpeningFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private applicationStake: BN
+  private roleStake: BN
+  private openingId: BN
+  private module: WorkingGroups
+
+  public constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    applicationStake: BN,
+    roleStake: BN,
+    openingId: BN,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.sudo = sudo
+    this.applicationStake = applicationStake
+    this.roleStake = roleStake
+    this.openingId = openingId
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const applyOnOpeningFee: BN = this.apiWrapper
+      .estimateApplyOnOpeningFee(this.sudo, this.module)
+      .add(this.applicationStake)
+      .add(this.roleStake)
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.membersKeyPairs, applyOnOpeningFee)
+
+    // Applying for created worker opening
+    await this.apiWrapper.batchApplyOnOpening(
+      this.membersKeyPairs,
+      this.openingId,
+      this.roleStake,
+      this.applicationStake,
+      uuid().substring(0, 8),
+      this.module,
+      expectFailure
+    )
+  }
+}
+
+export class WithdrawApplicationFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(apiWrapper: ApiWrapper, membersKeyPairs: KeyringPair[], sudo: KeyringPair, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const withdrawApplicaitonFee: BN = this.apiWrapper.estimateWithdrawApplicationFee(this.module)
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.membersKeyPairs, withdrawApplicaitonFee)
+
+    // Application withdrawal
+    await this.apiWrapper.batchWithdrawApplication(this.membersKeyPairs, this.module)
+
+    // Assertions
+    this.membersKeyPairs.forEach(async (keyPair) => {
+      const activeApplications: BN[] = await this.apiWrapper.getActiveApplicationsIdsByRoleAccount(
+        keyPair.address,
+        this.module
+      )
+      assert(activeApplications.length === 0, `Unexpected active application found for ${keyPair.address}`)
+    })
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class BeginApplicationReviewFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private openingId: BN
+  private module: WorkingGroups
+
+  constructor(apiWrapper: ApiWrapper, lead: KeyringPair, sudo: KeyringPair, openingId: BN, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.lead = lead
+    this.sudo = sudo
+    this.openingId = openingId
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const beginReviewFee: BN = this.apiWrapper.estimateBeginApplicantReviewFee(this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.lead.address, beginReviewFee)
+
+    // Begin application review
+    const beginApplicantReviewPromise: Promise<BN> = this.apiWrapper.expectApplicationReviewBegan()
+    await this.apiWrapper.beginApplicantReview(this.lead, this.openingId, this.module)
+    await beginApplicantReviewPromise
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class BeginLeaderApplicationReviewFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private sudo: KeyringPair
+  private openingId: BN
+  private module: WorkingGroups
+
+  constructor(apiWrapper: ApiWrapper, sudo: KeyringPair, openingId: BN, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.sudo = sudo
+    this.openingId = openingId
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Begin application review
+    await this.apiWrapper.sudoBeginApplicantReview(this.sudo, this.openingId, this.module)
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class FillOpeningFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private openingId: BN
+  private firstPayoutInterval: BN
+  private payoutInterval: BN
+  private amountPerPayout: BN
+  private module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    lead: KeyringPair,
+    sudo: KeyringPair,
+    openingId: BN,
+    firstPayoutInterval: BN,
+    payoutInterval: BN,
+    amountPerPayout: BN,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.lead = lead
+    this.sudo = sudo
+    this.openingId = openingId
+    this.firstPayoutInterval = firstPayoutInterval
+    this.payoutInterval = payoutInterval
+    this.amountPerPayout = amountPerPayout
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const beginReviewFee: BN = this.apiWrapper.estimateBeginApplicantReviewFee(this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.lead.address, beginReviewFee)
+    const applicationIds: BN[] = (
+      await Promise.all(
+        this.membersKeyPairs.map(async (keypair) =>
+          this.apiWrapper.getActiveApplicationsIdsByRoleAccount(keypair.address, this.module)
+        )
+      )
+    ).flat()
+
+    // Fill worker opening
+    const now: BN = await this.apiWrapper.getBestBlock()
+    const fillOpeningPromise: Promise<ApplicationIdToWorkerIdMap> = this.apiWrapper.expectOpeningFilled()
+    await this.apiWrapper.fillOpening(
+      this.lead,
+      this.openingId,
+      applicationIds,
+      this.amountPerPayout,
+      now.add(this.firstPayoutInterval),
+      this.payoutInterval,
+      this.module
+    )
+    const applicationIdToWorkerIdMap: ApplicationIdToWorkerIdMap = await fillOpeningPromise
+
+    // Assertions
+    applicationIdToWorkerIdMap.forEach(async (workerId, applicationId) => {
+      const worker: Worker = await this.apiWrapper.getWorkerById(workerId, this.module)
+      const application: Application = await this.apiWrapper.getApplicationById(applicationId, this.module)
+      assert(
+        worker.role_account_id.toString() === application.role_account_id.toString(),
+        `Role account ids does not match, worker account: ${worker.role_account_id}, application account ${application.role_account_id}`
+      )
+    })
+    const openingWorkersAccounts: string[] = (await this.apiWrapper.getWorkers(this.module)).map((worker) =>
+      worker.role_account_id.toString()
+    )
+    this.membersKeyPairs.forEach((keyPair) =>
+      assert(openingWorkersAccounts.includes(keyPair.address), `Account ${keyPair.address} is not worker`)
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class FillLeaderOpeningFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private openingId: BN
+  private firstPayoutInterval: BN
+  private payoutInterval: BN
+  private amountPerPayout: BN
+  private module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    sudo: KeyringPair,
+    openingId: BN,
+    firstPayoutInterval: BN,
+    payoutInterval: BN,
+    amountPerPayout: BN,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.sudo = sudo
+    this.openingId = openingId
+    this.firstPayoutInterval = firstPayoutInterval
+    this.payoutInterval = payoutInterval
+    this.amountPerPayout = amountPerPayout
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const applicationIds: BN[] = (
+      await Promise.all(
+        this.membersKeyPairs.map(async (keypair) =>
+          this.apiWrapper.getActiveApplicationsIdsByRoleAccount(keypair.address, this.module)
+        )
+      )
+    ).flat()
+
+    // Fill leader opening
+    const now: BN = await this.apiWrapper.getBestBlock()
+    const fillOpeningPromise: Promise<ApplicationIdToWorkerIdMap> = this.apiWrapper.expectOpeningFilled()
+    await this.apiWrapper.sudoFillOpening(
+      this.sudo,
+      this.openingId,
+      applicationIds,
+      this.amountPerPayout,
+      now.add(this.firstPayoutInterval),
+      this.payoutInterval,
+      this.module
+    )
+
+    // Assertions
+    const applicationIdToWorkerIdMap: ApplicationIdToWorkerIdMap = await fillOpeningPromise
+    applicationIdToWorkerIdMap.forEach(async (workerId, applicationId) => {
+      const worker: Worker = await this.apiWrapper.getWorkerById(workerId, this.module)
+      const application: Application = await this.apiWrapper.getApplicationById(applicationId, this.module)
+      assert(
+        worker.role_account_id.toString() === application.role_account_id.toString(),
+        `Role account ids does not match, leader account: ${worker.role_account_id}, application account ${application.role_account_id}`
+      )
+    })
+    const leadWorkerId: BN = (await this.apiWrapper.getLeadWorkerId(this.module))!
+    const openingLeaderAccount: string = (
+      await this.apiWrapper.getWorkerById(leadWorkerId, this.module)
+    ).role_account_id.toString()
+    assert(
+      openingLeaderAccount === this.membersKeyPairs[0].address,
+      `Unexpected leader account ${openingLeaderAccount}, expected ${this.membersKeyPairs[0].address}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class IncreaseStakeFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(apiWrapper: ApiWrapper, membersKeyPairs: KeyringPair[], sudo: KeyringPair, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const increaseStakeFee: BN = this.apiWrapper.estimateIncreaseStakeFee(this.module)
+    const stakeIncrement: BN = new BN(1)
+    await this.apiWrapper.transferBalance(
+      this.sudo,
+      this.membersKeyPairs[0].address,
+      increaseStakeFee.add(stakeIncrement)
+    )
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+
+    // Increase worker stake
+    const increasedWorkerStake: BN = (await this.apiWrapper.getWorkerStakeAmount(workerId, this.module)).add(
+      stakeIncrement
+    )
+    await this.apiWrapper.increaseStake(this.membersKeyPairs[0], workerId, stakeIncrement, this.module)
+    const newWorkerStake: BN = await this.apiWrapper.getWorkerStakeAmount(workerId, this.module)
+    assert(
+      increasedWorkerStake.eq(newWorkerStake),
+      `Unexpected worker stake ${newWorkerStake}, expected ${increasedWorkerStake}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class UpdateRewardAccountFixture implements Fixture {
+  public apiWrapper: ApiWrapper
+  public membersKeyPairs: KeyringPair[]
+  public keyring: Keyring
+  public sudo: KeyringPair
+  public module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    keyring: Keyring,
+    sudo: KeyringPair,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.keyring = keyring
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const updateRewardAccountFee: BN = this.apiWrapper.estimateUpdateRewardAccountFee(this.sudo.address, this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.membersKeyPairs[0].address, updateRewardAccountFee)
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+
+    // Update reward account
+    const createdAccount: KeyringPair = this.keyring.addFromUri(uuid().substring(0, 8))
+    await this.apiWrapper.updateRewardAccount(this.membersKeyPairs[0], workerId, createdAccount.address, this.module)
+    const newRewardAccount: string = await this.apiWrapper.getWorkerRewardAccount(workerId, this.module)
+    assert(
+      newRewardAccount === createdAccount.address,
+      `Unexpected role account ${newRewardAccount}, expected ${createdAccount.address}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class UpdateRoleAccountFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private keyring: Keyring
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    keyring: Keyring,
+    sudo: KeyringPair,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.keyring = keyring
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const updateRoleAccountFee: BN = this.apiWrapper.estimateUpdateRoleAccountFee(this.sudo.address, this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.membersKeyPairs[0].address, updateRoleAccountFee)
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+
+    // Update role account
+    const createdAccount: KeyringPair = this.keyring.addFromUri(uuid().substring(0, 8))
+    await this.apiWrapper.updateRoleAccount(this.membersKeyPairs[0], workerId, createdAccount.address, this.module)
+    const newRoleAccount: string = (
+      await this.apiWrapper.getWorkerById(workerId, this.module)
+    ).role_account_id.toString()
+    assert(
+      newRoleAccount === createdAccount.address,
+      `Unexpected role account ${newRoleAccount}, expected ${createdAccount.address}`
+    )
+
+    this.membersKeyPairs[0] = createdAccount
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class TerminateApplicationsFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    lead: KeyringPair,
+    sudo: KeyringPair,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.lead = lead
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const terminateApplicationFee = this.apiWrapper.estimateTerminateApplicationFee(this.module)
+    await this.apiWrapper.transferBalance(
+      this.sudo,
+      this.lead.address,
+      terminateApplicationFee.muln(this.membersKeyPairs.length)
+    )
+
+    // Terminate worker applications
+    await this.apiWrapper.batchTerminateApplication(this.lead, this.membersKeyPairs, this.module)
+    this.membersKeyPairs.forEach(async (keyPair) => {
+      const activeApplications = await this.apiWrapper.getActiveApplicationsIdsByRoleAccount(
+        keyPair.address,
+        this.module
+      )
+      assert(activeApplications.length === 0, `Account ${keyPair.address} has unexpected active applications`)
+    })
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class DecreaseStakeFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    lead: KeyringPair,
+    sudo: KeyringPair,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.lead = lead
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const decreaseStakeFee = this.apiWrapper.estimateDecreaseStakeFee(this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.lead.address, decreaseStakeFee)
+    const workerStakeDecrement = new BN(1)
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+
+    // Worker stake decrement
+    const decreasedWorkerStake: BN = (await this.apiWrapper.getWorkerStakeAmount(workerId, this.module)).sub(
+      workerStakeDecrement
+    )
+    await this.apiWrapper.decreaseStake(this.lead, workerId, workerStakeDecrement, this.module, expectFailure)
+    const newWorkerStake: BN = await this.apiWrapper.getWorkerStakeAmount(workerId, this.module)
+
+    // Assertions
+    if (!expectFailure) {
+      assert(
+        decreasedWorkerStake.eq(newWorkerStake),
+        `Unexpected worker stake ${newWorkerStake}, expected ${decreasedWorkerStake}`
+      )
+    }
+  }
+}
+
+export class SlashFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    lead: KeyringPair,
+    sudo: KeyringPair,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.lead = lead
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const slashStakeFee = this.apiWrapper.estimateSlashStakeFee(this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.lead.address, slashStakeFee)
+    const slashAmount = new BN(1)
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+
+    // Slash worker
+    const slashedStake: BN = (await this.apiWrapper.getWorkerStakeAmount(workerId, this.module)).sub(slashAmount)
+    await this.apiWrapper.slashStake(this.lead, workerId, slashAmount, this.module, expectFailure)
+    const newStake: BN = await this.apiWrapper.getWorkerStakeAmount(workerId, this.module)
+
+    // Assertions
+    assert(slashedStake.eq(newStake), `Unexpected worker stake ${newStake}, expected ${slashedStake}`)
+  }
+}
+
+export class TerminateRoleFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private lead: KeyringPair
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(
+    apiWrapper: ApiWrapper,
+    membersKeyPairs: KeyringPair[],
+    lead: KeyringPair,
+    sudo: KeyringPair,
+    module: WorkingGroups
+  ) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.lead = lead
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const terminateRoleFee = this.apiWrapper.estimateTerminateRoleFee(this.module)
+    await this.apiWrapper.transferBalance(this.sudo, this.lead.address, terminateRoleFee)
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+
+    // Slash worker
+    await this.apiWrapper.terminateRole(this.lead, workerId, uuid().substring(0, 8), this.module, expectFailure)
+
+    // Assertions
+    this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+    const newWorkerId = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+    assert(newWorkerId === undefined, `Worker with account ${this.membersKeyPairs[0].address} is not terminated`)
+  }
+}
+
+export class LeaveRoleFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private sudo: KeyringPair
+  private module: WorkingGroups
+
+  constructor(apiWrapper: ApiWrapper, membersKeyPairs: KeyringPair[], sudo: KeyringPair, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.sudo = sudo
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    // Fee estimation and transfer
+    const leaveRoleFee = this.apiWrapper.estimateLeaveRoleFee(this.module)
+    await this.apiWrapper.transferBalanceToAccounts(this.sudo, this.membersKeyPairs, leaveRoleFee)
+
+    await this.apiWrapper.batchLeaveRole(this.membersKeyPairs, uuid().substring(0, 8), expectFailure, this.module)
+
+    // Assertions
+    this.membersKeyPairs.forEach(async (keyPair) => {
+      this.apiWrapper.getWorkerIdByRoleAccount(keyPair.address, this.module)
+      const newWorkerId = await this.apiWrapper.getWorkerIdByRoleAccount(keyPair.address, this.module)
+      assert(newWorkerId === undefined, `Worker with account ${keyPair.address} is not terminated`)
+    })
+  }
+}
+
+export class AwaitPayoutFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private membersKeyPairs: KeyringPair[]
+  private module: WorkingGroups
+
+  constructor(apiWrapper: ApiWrapper, membersKeyPairs: KeyringPair[], module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.membersKeyPairs = membersKeyPairs
+    this.module = module
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const workerId: BN = await this.apiWrapper.getWorkerIdByRoleAccount(this.membersKeyPairs[0].address, this.module)
+    const worker: Worker = await this.apiWrapper.getWorkerById(workerId, this.module)
+    const reward: RewardRelationship = await this.apiWrapper.getRewardRelationship(worker.reward_relationship.unwrap())
+    const now: BN = await this.apiWrapper.getBestBlock()
+    const nextPaymentBlock: BN = new BN(reward.getField('next_payment_at_block').toString())
+    const payoutInterval: BN = new BN(reward.getField('payout_interval').toString())
+    const amountPerPayout: BN = new BN(reward.getField('amount_per_payout').toString())
+
+    assert(now.lt(nextPaymentBlock), `Payout already happened in block ${nextPaymentBlock} now ${now}`)
+    const balance = await this.apiWrapper.getBalance(this.membersKeyPairs[0].address)
+
+    const firstPayoutWaitingPeriod = nextPaymentBlock.sub(now).addn(1)
+    await Utils.wait(this.apiWrapper.getBlockDuration().mul(firstPayoutWaitingPeriod).toNumber())
+
+    const balanceAfterFirstPayout = await this.apiWrapper.getBalance(this.membersKeyPairs[0].address)
+    const expectedBalanceFirst = balance.add(amountPerPayout)
+    assert(
+      balanceAfterFirstPayout.eq(expectedBalanceFirst),
+      `Unexpected balance, expected ${expectedBalanceFirst} got ${balanceAfterFirstPayout}`
+    )
+
+    const secondPayoutWaitingPeriod = payoutInterval.addn(1)
+    await Utils.wait(this.apiWrapper.getBlockDuration().mul(secondPayoutWaitingPeriod).toNumber())
+
+    const balanceAfterSecondPayout = await this.apiWrapper.getBalance(this.membersKeyPairs[0].address)
+    const expectedBalanceSecond = expectedBalanceFirst.add(amountPerPayout)
+    assert(
+      balanceAfterSecondPayout.eq(expectedBalanceSecond),
+      `Unexpected balance, expected ${expectedBalanceSecond} got ${balanceAfterSecondPayout}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectLeadOpeningAddedFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+
+  // Opening id
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper) {
+    this.apiWrapper = apiWrapper
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('OpeningAdded')
+    this.events.push(event)
+    this.result = (event.data as unknown) as BN
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectLeaderSetFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private leaderAddress: string
+  private module: WorkingGroups
+
+  // Lead worker id
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper, leaderAddress: string, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.leaderAddress = leaderAddress
+    this.module = module
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('LeaderSet')
+    this.events.push(event)
+    const leadWorkerId: BN = (event.data as unknown) as BN
+    const worker: Worker = await this.apiWrapper.getWorkerById(leadWorkerId, this.module)
+    const leaderApplicationId = (
+      await this.apiWrapper.getApplicationsIdsByRoleAccount(this.leaderAddress, this.module)
+    )[0]
+    const application: Application = await this.apiWrapper.getApplicationById(leaderApplicationId, this.module)
+    assert(
+      worker.role_account_id.eq(application.role_account_id),
+      `Role account ids does not match, leader account: ${worker.role_account_id}, application account ${application.role_account_id}`
+    )
+    this.result = leadWorkerId
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectBeganApplicationReviewFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper) {
+    this.apiWrapper = apiWrapper
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('BeganApplicationReview')
+    this.events.push(event)
+    this.result = (event.data as unknown) as BN
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectLeaderRoleTerminatedFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private module: WorkingGroups
+
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.module = module
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('TerminatedLeader')
+    this.events.push(event)
+    const leadWorkerId: BN | undefined = await this.apiWrapper.getLeadWorkerId(this.module)
+    assert(leadWorkerId === undefined, `Unexpected lead worker id: ${leadWorkerId}, expected none`)
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectLeaderRewardAmountUpdatedFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private expectedReward: BN
+  private module: WorkingGroups
+
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper, expectedReward: BN, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.expectedReward = expectedReward
+    this.module = module
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('WorkerRewardAmountUpdated')
+    this.events.push(event)
+    const leadWorkerId: BN = (await this.apiWrapper.getLeadWorkerId(this.module))!
+    const receivedReward: BN = (await this.apiWrapper.getRewardRelationship(leadWorkerId)).getField<Balance>(
+      'amount_per_payout'
+    )
+    assert(
+      receivedReward.eq(this.expectedReward),
+      `Unexpected reward amount for worker with id ${leadWorkerId}: ${receivedReward}, expected ${this.expectedReward}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectLeaderStakeDecreasedFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private expectedStake: BN
+  private module: WorkingGroups
+
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper, expectedStake: BN, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.expectedStake = expectedStake
+    this.module = module
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('StakeDecreased')
+    this.events.push(event)
+    const leadWorkerId: BN = (await this.apiWrapper.getLeadWorkerId(this.module))!
+    const receivedStake: BN = await this.apiWrapper.getWorkerStakeAmount(leadWorkerId, this.module)
+    assert(
+      receivedStake.eq(this.expectedStake),
+      `Unexpected stake amount for worker with id ${leadWorkerId}: ${receivedStake}, expected ${this.expectedStake}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectLeaderSlashedFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private expectedStake: BN
+  private module: WorkingGroups
+
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper, expectedStake: BN, module: WorkingGroups) {
+    this.apiWrapper = apiWrapper
+    this.expectedStake = expectedStake
+    this.module = module
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('StakeSlashed')
+    this.events.push(event)
+    const leadWorkerId: BN = (await this.apiWrapper.getLeadWorkerId(this.module))!
+    const receivedStake: BN = await this.apiWrapper.getWorkerStakeAmount(leadWorkerId, this.module)
+    assert(
+      receivedStake.eq(this.expectedStake),
+      `Unexpected stake amount for worker with id after slash ${leadWorkerId}: ${receivedStake}, expected ${this.expectedStake}`
+    )
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}
+
+export class ExpectMintCapacityChangedFixture implements Fixture {
+  private apiWrapper: ApiWrapper
+  private expectedMintCapacity: BN
+
+  private result: BN | undefined
+  private events: Event[] = []
+
+  constructor(apiWrapper: ApiWrapper, expectedMintCapacity: BN) {
+    this.apiWrapper = apiWrapper
+    this.expectedMintCapacity = expectedMintCapacity
+  }
+
+  public getResult(): BN | undefined {
+    return this.result
+  }
+
+  public getEvents(): Event[] {
+    return this.events
+  }
+
+  public async runner(expectFailure: boolean): Promise<void> {
+    const event: Event = await this.apiWrapper.expectEvent('MintCapacityChanged')
+    this.events.push(event)
+    const receivedMintCapacity = (event.data[1] as unknown) as BN
+    assert(
+      receivedMintCapacity.eq(this.expectedMintCapacity),
+      `Unexpected mint capacity: ${receivedMintCapacity}, expected ${this.expectedMintCapacity}`
+    )
+    this.result = receivedMintCapacity
+    if (expectFailure) {
+      throw new Error('Successful fixture run while expecting failure')
+    }
+  }
+}

+ 56 - 0
tests/network-tests/src/iznik/tests/membership/membershipCreationTest.ts

@@ -0,0 +1,56 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { Keyring, WsProvider } from '@polkadot/api'
+import { initConfig } from '../../utils/config'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { closeApi } from '../../utils/closeApi'
+import { BuyMembershipHappyCaseFixture, BuyMembershipWithInsufficienFundsFixture } from '../fixtures/membershipModule'
+import { Utils } from '../../utils/utils'
+
+tap.mocha.describe('Membership creation scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const nKeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const aKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+
+  const durationInBlocks = 7
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const happyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    nKeyPairs,
+    paidTerms
+  )
+  tap.test('Buy membeship is accepted with sufficient funds', async () => happyCaseFixture.runner(false))
+
+  const insufficientFundsFixture: BuyMembershipWithInsufficienFundsFixture = new BuyMembershipWithInsufficienFundsFixture(
+    apiWrapper,
+    sudo,
+    aKeyPair[0],
+    paidTerms
+  )
+  tap.test('Account A can not buy the membership with insufficient funds', async () =>
+    insufficientFundsFixture.runner(false)
+  )
+
+  const buyMembershipAfterAccountTopUp = new BuyMembershipHappyCaseFixture(apiWrapper, sudo, aKeyPair, paidTerms)
+  tap.test('Account A was able to buy the membership with sufficient funds', async () =>
+    buyMembershipAfterAccountTopUp.runner(false)
+  )
+
+  closeApi(apiWrapper)
+})

+ 79 - 0
tests/network-tests/src/iznik/tests/proposals/contentWorkingGroupMintCapacityProposalTest.ts

@@ -0,0 +1,79 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { ContentWorkingGroupMintCapacityProposalFixture } from '../fixtures/proposalsModule'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { Utils } from '../../utils/utils'
+
+tap.mocha.describe('Validator count proposal scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const mintingCapacityIncrement: BN = new BN(+process.env.MINTING_CAPACITY_INCREMENT!)
+
+  const durationInBlocks = 29
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const contentWorkingGroupMintCapacityProposalFixture: ContentWorkingGroupMintCapacityProposalFixture = new ContentWorkingGroupMintCapacityProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    sudo,
+    mintingCapacityIncrement
+  )
+  tap.test('Content working group mint capacity test', async () =>
+    contentWorkingGroupMintCapacityProposalFixture.runner(false)
+  )
+
+  closeApi(apiWrapper)
+})

+ 75 - 0
tests/network-tests/src/iznik/tests/proposals/electionParametersProposalTest.ts

@@ -0,0 +1,75 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { Utils } from '../../utils/utils'
+import { ElectionParametersProposalFixture } from '../fixtures/proposalsModule'
+
+tap.mocha.describe('Election parameters proposal scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+
+  const durationInBlocks = 29
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const electionParametersProposalFixture: ElectionParametersProposalFixture = new ElectionParametersProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    sudo
+  )
+  tap.test('Election parameters proposal test', async () => electionParametersProposalFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 304 - 0
tests/network-tests/src/iznik/tests/proposals/manageLeaderRoleTest.ts

@@ -0,0 +1,304 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import {
+  BeginWorkingGroupLeaderApplicationReviewFixture,
+  CreateWorkingGroupLeaderOpeningFixture,
+  DecreaseLeaderStakeProposalFixture,
+  FillLeaderOpeningProposalFixture,
+  SetLeaderRewardProposalFixture,
+  SlashLeaderProposalFixture,
+  TerminateLeaderRoleProposalFixture,
+  VoteForProposalFixture,
+} from '../fixtures/proposalsModule'
+import {
+  ApplyForOpeningFixture,
+  ExpectBeganApplicationReviewFixture,
+  ExpectLeaderRewardAmountUpdatedFixture,
+  ExpectLeaderRoleTerminatedFixture,
+  ExpectLeaderSetFixture,
+  ExpectLeaderSlashedFixture,
+  ExpectLeaderStakeDecreasedFixture,
+  ExpectLeadOpeningAddedFixture,
+} from '../fixtures/workingGroupModule'
+import { Utils } from '../../utils/utils'
+
+tap.mocha.describe('Set lead proposal scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const leadKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const applicationStake: BN = new BN(process.env.WORKING_GROUP_APPLICATION_STAKE!)
+  const roleStake: BN = new BN(process.env.WORKING_GROUP_ROLE_STAKE!)
+  const firstRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const rewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const payoutAmount: BN = new BN(process.env.PAYOUT_AMOUNT!)
+  const alteredPayoutAmount: BN = new BN(process.env.ALTERED_PAYOUT_AMOUNT!)
+  const stakeDecrement: BN = new BN(process.env.STAKE_DECREMENT!)
+  const slashAmount: BN = new BN(process.env.SLASH_AMOUNT!)
+  const durationInBlocks = 70
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const leaderMembershipFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    leadKeyPair,
+    paidTerms
+  )
+  tap.test('Buy membership for lead', async () => leaderMembershipFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const createWorkingGroupLeaderOpeningFixture: CreateWorkingGroupLeaderOpeningFixture = new CreateWorkingGroupLeaderOpeningFixture(
+    apiWrapper,
+    m1KeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    'Storage'
+  )
+  tap.test('Propose create leader opening', async () => createWorkingGroupLeaderOpeningFixture.runner(false))
+
+  let voteForCreateOpeningProposalFixture: VoteForProposalFixture
+  const expectLeadOpeningAddedFixture: ExpectLeadOpeningAddedFixture = new ExpectLeadOpeningAddedFixture(apiWrapper)
+  tap.test('Approve add opening proposal', async () => {
+    voteForCreateOpeningProposalFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      createWorkingGroupLeaderOpeningFixture.getResult()!
+    )
+    voteForCreateOpeningProposalFixture.runner(false)
+    await expectLeadOpeningAddedFixture.runner(false)
+  })
+
+  let applyForLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      new BN(expectLeadOpeningAddedFixture.getResult()!),
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForLeaderOpeningFixture.runner(false)
+  })
+
+  let beginWorkingGroupLeaderApplicationReviewFixture: BeginWorkingGroupLeaderApplicationReviewFixture
+  tap.test('Propose begin leader application review', async () => {
+    beginWorkingGroupLeaderApplicationReviewFixture = new BeginWorkingGroupLeaderApplicationReviewFixture(
+      apiWrapper,
+      m1KeyPairs,
+      sudo,
+      new BN(expectLeadOpeningAddedFixture.getResult()!),
+      'Storage'
+    )
+    await beginWorkingGroupLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let voteForBeginReviewProposal: VoteForProposalFixture
+  const expectBeganApplicationReviewFixture: ExpectBeganApplicationReviewFixture = new ExpectBeganApplicationReviewFixture(
+    apiWrapper
+  )
+  tap.test('Approve begin application review', async () => {
+    voteForBeginReviewProposal = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      beginWorkingGroupLeaderApplicationReviewFixture.getResult()!
+    )
+    voteForBeginReviewProposal.runner(false)
+    await expectBeganApplicationReviewFixture.runner(false)
+  })
+
+  let fillLeaderOpeningProposalFixture: FillLeaderOpeningProposalFixture
+  tap.test('Propose fill leader opening', async () => {
+    fillLeaderOpeningProposalFixture = new FillLeaderOpeningProposalFixture(
+      apiWrapper,
+      m1KeyPairs,
+      leadKeyPair[0].address,
+      sudo,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      new BN(expectLeadOpeningAddedFixture.getResult()!),
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillLeaderOpeningProposalFixture.runner(false)
+  })
+
+  let voteForFillLeaderProposalFixture: VoteForProposalFixture
+  const expectLeaderSetFixture: ExpectLeaderSetFixture = new ExpectLeaderSetFixture(
+    apiWrapper,
+    leadKeyPair[0].address,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Approve fill leader opening', async () => {
+    voteForFillLeaderProposalFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      fillLeaderOpeningProposalFixture.getResult()!
+    )
+    voteForFillLeaderProposalFixture.runner(false)
+    await expectLeaderSetFixture.runner(false)
+  })
+
+  const setLeaderRewardProposalFixture: SetLeaderRewardProposalFixture = new SetLeaderRewardProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    sudo,
+    alteredPayoutAmount,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Propose leader reward', async () => setLeaderRewardProposalFixture.runner(false))
+
+  let voteForeLeaderRewardFixture: VoteForProposalFixture
+  const expectLeaderRewardAmountUpdatedFixture: ExpectLeaderRewardAmountUpdatedFixture = new ExpectLeaderRewardAmountUpdatedFixture(
+    apiWrapper,
+    alteredPayoutAmount,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Approve new leader reward', async () => {
+    voteForeLeaderRewardFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      setLeaderRewardProposalFixture.getResult()!
+    )
+    voteForeLeaderRewardFixture.runner(false)
+    await expectLeaderRewardAmountUpdatedFixture.runner(false)
+  })
+
+  const decreaseLeaderStakeProposalFixture: DecreaseLeaderStakeProposalFixture = new DecreaseLeaderStakeProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    sudo,
+    stakeDecrement,
+    WorkingGroups.StorageWorkingGroup
+  )
+  let newStake: BN
+  tap.test('Propose decrease stake', async () => decreaseLeaderStakeProposalFixture.runner(false))
+
+  let voteForDecreaseStakeProposal: VoteForProposalFixture
+  let expectLeaderStakeDecreasedFixture: ExpectLeaderStakeDecreasedFixture
+  tap.test('Approve decreased leader stake', async () => {
+    newStake = applicationStake.sub(stakeDecrement)
+    voteForFillLeaderProposalFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      decreaseLeaderStakeProposalFixture.getResult()!
+    )
+    voteForFillLeaderProposalFixture.runner(false)
+    expectLeaderStakeDecreasedFixture = new ExpectLeaderStakeDecreasedFixture(
+      apiWrapper,
+      newStake,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await expectLeaderStakeDecreasedFixture.runner(false)
+  })
+
+  const slashLeaderProposalFixture: SlashLeaderProposalFixture = new SlashLeaderProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    sudo,
+    slashAmount,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Propose leader slash', async () => slashLeaderProposalFixture.runner(false))
+
+  let voteForSlashProposalFixture: VoteForProposalFixture
+  let expectLeaderSlashedFixture: ExpectLeaderSlashedFixture
+  tap.test('Approve leader slash', async () => {
+    newStake = newStake.sub(slashAmount)
+    voteForSlashProposalFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      slashLeaderProposalFixture.getResult()!
+    )
+    voteForSlashProposalFixture.runner(false)
+    expectLeaderSlashedFixture = new ExpectLeaderSlashedFixture(apiWrapper, newStake, WorkingGroups.StorageWorkingGroup)
+    await expectLeaderSlashedFixture.runner(false)
+  })
+
+  const terminateLeaderRoleProposalFixture: TerminateLeaderRoleProposalFixture = new TerminateLeaderRoleProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    leadKeyPair[0].address,
+    sudo,
+    false,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Propose terminate leader role', async () => terminateLeaderRoleProposalFixture.runner(false))
+
+  let voteForLeaderRoleTerminationFixture: VoteForProposalFixture
+  const expectLeaderRoleTerminatedFixture: ExpectLeaderRoleTerminatedFixture = new ExpectLeaderRoleTerminatedFixture(
+    apiWrapper,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Approve leader role termination', async () => {
+    voteForLeaderRoleTerminationFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      terminateLeaderRoleProposalFixture.getResult()!
+    )
+    voteForLeaderRoleTerminationFixture.runner(false)
+    await expectLeaderRoleTerminatedFixture.runner(false)
+  })
+
+  closeApi(apiWrapper)
+})

+ 75 - 0
tests/network-tests/src/iznik/tests/proposals/setLeadProposalTest.ts

@@ -0,0 +1,75 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { Utils } from '../../utils/utils'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { SetLeadProposalFixture } from '../fixtures/proposalsModule'
+
+tap.mocha.describe('Set lead proposal scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+
+  const durationInBlocks = 29
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const setLeadProposalFixture: SetLeadProposalFixture = new SetLeadProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    sudo
+  )
+  tap.test('Set lead proposal test', async () => setLeadProposalFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 78 - 0
tests/network-tests/src/iznik/tests/proposals/spendingProposalTest.ts

@@ -0,0 +1,78 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { Utils } from '../../utils/utils'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { SpendingProposalFixture } from '../fixtures/proposalsModule'
+
+tap.mocha.describe('Spending proposal scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const spendingBalance: BN = new BN(+process.env.SPENDING_BALANCE!)
+  const mintCapacity: BN = new BN(+process.env.COUNCIL_MINTING_CAPACITY!)
+  const durationInBlocks = 29
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const spendingProposalFixture: SpendingProposalFixture = new SpendingProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    sudo,
+    spendingBalance,
+    mintCapacity
+  )
+  tap.test('Spending proposal test', async () => spendingProposalFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 69 - 0
tests/network-tests/src/iznik/tests/proposals/textProposalTest.ts

@@ -0,0 +1,69 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { Utils } from '../../utils/utils'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { TextProposalFixture } from '../fixtures/proposalsModule'
+
+tap.mocha.describe('Text proposal scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const durationInBlocks = 28
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const textProposalFixture: TextProposalFixture = new TextProposalFixture(apiWrapper, m1KeyPairs, m2KeyPairs, sudo)
+  tap.test('Text proposal test', async () => textProposalFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 77 - 0
tests/network-tests/src/iznik/tests/proposals/updateRuntime.ts

@@ -0,0 +1,77 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { Utils } from '../../utils/utils'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { UpdateRuntimeFixture } from '../fixtures/proposalsModule'
+
+tap.mocha.describe('Update runtime scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const durationInBlocks = 54
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const updateRuntimeFixture: UpdateRuntimeFixture = new UpdateRuntimeFixture(apiWrapper, m1KeyPairs, m2KeyPairs, sudo)
+  tap.test('Upgrade runtime', async () => updateRuntimeFixture.runner(false))
+
+  const thirdMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    Utils.createKeyPairs(keyring, N),
+    paidTerms
+  )
+  tap.test('Creating third set of members', async () => thirdMemberSetFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 76 - 0
tests/network-tests/src/iznik/tests/proposals/validatorCountProposalTest.ts

@@ -0,0 +1,76 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper } from '../../utils/apiWrapper'
+import { Utils } from '../../utils/utils'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { ValidatorCountProposalFixture } from '../fixtures/proposalsModule'
+
+tap.mocha.describe('Validator count proposal scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const validatorCountIncrement: BN = new BN(+process.env.VALIDATOR_COUNT_INCREMENT!)
+  const durationInBlocks = 29
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const validatorCountProposalFixture: ValidatorCountProposalFixture = new ValidatorCountProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    sudo,
+    validatorCountIncrement
+  )
+  tap.test('Validator count proposal', async () => validatorCountProposalFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 96 - 0
tests/network-tests/src/iznik/tests/proposals/workingGroupMintCapacityProposalTest.ts

@@ -0,0 +1,96 @@
+import { KeyringPair } from '@polkadot/keyring/types'
+import { initConfig } from '../../utils/config'
+import { Keyring, WsProvider } from '@polkadot/api'
+import BN from 'bn.js'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import tap from 'tap'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { Utils } from '../../utils/utils'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { VoteForProposalFixture, WorkingGroupMintCapacityProposalFixture } from '../fixtures/proposalsModule'
+import { ExpectMintCapacityChangedFixture } from '../fixtures/workingGroupModule'
+
+tap.mocha.describe('Set storage working group mint capacity scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const mintCapacityIncrement: BN = new BN(process.env.MINT_CAPACITY_INCREMENT!)
+  const durationInBlocks = 30
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const newMintCapacity: BN = (await apiWrapper.getWorkingGroupMintCapacity(WorkingGroups.StorageWorkingGroup)).add(
+    mintCapacityIncrement
+  )
+  const workingGroupMintCapacityProposalFixture: WorkingGroupMintCapacityProposalFixture = new WorkingGroupMintCapacityProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    sudo,
+    newMintCapacity,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Propose mint capacity', async () => workingGroupMintCapacityProposalFixture.runner(false))
+
+  let voteForProposalFixture: VoteForProposalFixture
+  const expectMintCapacityChanged: ExpectMintCapacityChangedFixture = new ExpectMintCapacityChangedFixture(
+    apiWrapper,
+    newMintCapacity
+  )
+  tap.test('Approve mint capacity', async () => {
+    voteForProposalFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      workingGroupMintCapacityProposalFixture.getResult()!
+    )
+    voteForProposalFixture.runner(false)
+    await expectMintCapacityChanged.runner(false)
+  })
+
+  closeApi(apiWrapper)
+})

+ 154 - 0
tests/network-tests/src/iznik/tests/workingGroup/atLeastValueBugTest.ts

@@ -0,0 +1,154 @@
+import { initConfig } from '../../utils/config'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { WsProvider, Keyring } from '@polkadot/api'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import {
+  AddLeaderOpeningFixture,
+  AddWorkerOpeningFixture,
+  ApplyForOpeningFixture,
+  BeginLeaderApplicationReviewFixture,
+  FillLeaderOpeningFixture,
+  LeaveRoleFixture,
+} from '../fixtures/workingGroupModule'
+import BN from 'bn.js'
+import tap from 'tap'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { Utils } from '../../utils/utils'
+
+tap.mocha.describe('Worker application happy case scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.WORKING_GROUP_N!
+  const nKeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const leadKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const applicationStake: BN = new BN(process.env.WORKING_GROUP_APPLICATION_STAKE!)
+  const roleStake: BN = new BN(process.env.WORKING_GROUP_ROLE_STAKE!)
+  const firstRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const rewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const payoutAmount: BN = new BN(process.env.PAYOUT_AMOUNT!)
+  const unstakingPeriod: BN = new BN(process.env.STORAGE_WORKING_GROUP_UNSTAKING_PERIOD!)
+  const durationInBlocks = 48
+  const openingActivationDelay: BN = new BN(0)
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const happyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    nKeyPairs,
+    paidTerms
+  )
+  tap.test('Creating a set of members', async () => happyCaseFixture.runner(false))
+
+  const leaderHappyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    leadKeyPair,
+    paidTerms
+  )
+  tap.test('Buying membership for leader account', async () => leaderHappyCaseFixture.runner(false))
+
+  const addLeaderOpeningFixture: AddLeaderOpeningFixture = new AddLeaderOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add lead opening', async () => await addLeaderOpeningFixture.runner(false))
+
+  let applyForLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForLeaderOpeningFixture.runner(false)
+  })
+
+  let beginLeaderApplicationReviewFixture: BeginLeaderApplicationReviewFixture
+  tap.test('Begin lead application review', async () => {
+    beginLeaderApplicationReviewFixture = new BeginLeaderApplicationReviewFixture(
+      apiWrapper,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let fillLeaderOpeningFixture: FillLeaderOpeningFixture
+  tap.test('Fill lead opening', async () => {
+    fillLeaderOpeningFixture = new FillLeaderOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillLeaderOpeningFixture.runner(false)
+  })
+
+  const addWorkerOpeningWithoutStakeFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    new BN(0),
+    new BN(0),
+    openingActivationDelay,
+    unstakingPeriod,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add worker opening with 0 stake, expect failure', async () =>
+    addWorkerOpeningWithoutStakeFixture.runner(true)
+  )
+
+  const addWorkerOpeningWithoutUnstakingPeriodFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    new BN(0),
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add worker opening with 0 unstaking period, expect failure', async () =>
+    addWorkerOpeningWithoutUnstakingPeriodFixture.runner(true)
+  )
+
+  const leaveRoleFixture: LeaveRoleFixture = new LeaveRoleFixture(
+    apiWrapper,
+    leadKeyPair,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Leaving lead role', async () => leaveRoleFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 280 - 0
tests/network-tests/src/iznik/tests/workingGroup/manageWorkerAsLeadTest.ts

@@ -0,0 +1,280 @@
+import { initConfig } from '../../utils/config'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { WsProvider, Keyring } from '@polkadot/api'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import {
+  AddLeaderOpeningFixture,
+  ApplyForOpeningFixture,
+  BeginLeaderApplicationReviewFixture,
+  FillLeaderOpeningFixture,
+  AddWorkerOpeningFixture,
+  WithdrawApplicationFixture,
+  BeginApplicationReviewFixture,
+  FillOpeningFixture,
+  LeaveRoleFixture,
+  DecreaseStakeFixture,
+  SlashFixture,
+  TerminateRoleFixture,
+} from '../fixtures/workingGroupModule'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { Utils } from '../../utils/utils'
+import BN from 'bn.js'
+import tap from 'tap'
+
+tap.mocha.describe('Manage worker as worker scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.WORKING_GROUP_N!
+  const nKeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const leadKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const applicationStake: BN = new BN(process.env.WORKING_GROUP_APPLICATION_STAKE!)
+  const roleStake: BN = new BN(process.env.WORKING_GROUP_ROLE_STAKE!)
+  const firstRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const rewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const payoutAmount: BN = new BN(process.env.PAYOUT_AMOUNT!)
+  const unstakingPeriod: BN = new BN(process.env.STORAGE_WORKING_GROUP_UNSTAKING_PERIOD!)
+  const durationInBlocks = 60
+  const openingActivationDelay: BN = new BN(0)
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const happyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    nKeyPairs,
+    paidTerms
+  )
+  tap.test('Creating a set of members', async () => happyCaseFixture.runner(false))
+
+  const leaderHappyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    leadKeyPair,
+    paidTerms
+  )
+  tap.test('Buying membership for leader account', async () => leaderHappyCaseFixture.runner(false))
+
+  const addLeaderOpeningFixture: AddLeaderOpeningFixture = new AddLeaderOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add lead opening', async () => await addLeaderOpeningFixture.runner(false))
+
+  let applyForLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForLeaderOpeningFixture.runner(false)
+  })
+
+  let beginLeaderApplicationReviewFixture: BeginLeaderApplicationReviewFixture
+  tap.test('Begin lead application review', async () => {
+    beginLeaderApplicationReviewFixture = new BeginLeaderApplicationReviewFixture(
+      apiWrapper,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let fillLeaderOpeningFixture: FillLeaderOpeningFixture
+  tap.test('Fill lead opening', async () => {
+    fillLeaderOpeningFixture = new FillLeaderOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillLeaderOpeningFixture.runner(false)
+  })
+
+  const addWorkerOpeningFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    unstakingPeriod,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add worker opening', async () => addWorkerOpeningFixture.runner(false))
+
+  let applyForWorkerOpeningFixture: ApplyForOpeningFixture
+  tap.test('First apply for worker opening', async () => {
+    applyForWorkerOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForWorkerOpeningFixture.runner(false)
+  })
+
+  let beginApplicationReviewFixture: BeginApplicationReviewFixture
+  tap.test('Begin application review', async () => {
+    beginApplicationReviewFixture = new BeginApplicationReviewFixture(
+      apiWrapper,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginApplicationReviewFixture.runner(false)
+  })
+
+  let fillOpeningFixture: FillOpeningFixture
+  tap.test('Fill worker opening', async () => {
+    fillOpeningFixture = new FillOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillOpeningFixture.runner(false)
+  })
+
+  const leaveRoleFixture: LeaveRoleFixture = new LeaveRoleFixture(
+    apiWrapper,
+    leadKeyPair,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Leaving lead role', async () => leaveRoleFixture.runner(false))
+
+  const decreaseStakeFailureFixture: DecreaseStakeFixture = new DecreaseStakeFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Decrease worker stake, expect failure', async () => decreaseStakeFailureFixture.runner(true))
+
+  const addNewLeaderOpeningFixture: AddLeaderOpeningFixture = new AddLeaderOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add lead opening', async () => await addNewLeaderOpeningFixture.runner(false))
+
+  let applyForNewLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForNewLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      addNewLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForNewLeaderOpeningFixture.runner(false)
+  })
+
+  let beginNewLeaderApplicationReviewFixture: BeginLeaderApplicationReviewFixture
+  tap.test('Begin lead application review', async () => {
+    beginNewLeaderApplicationReviewFixture = new BeginLeaderApplicationReviewFixture(
+      apiWrapper,
+      sudo,
+      addNewLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginNewLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let fillNewLeaderOpeningFixture: FillLeaderOpeningFixture
+  tap.test('Fill lead opening', async () => {
+    fillNewLeaderOpeningFixture = new FillLeaderOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      addNewLeaderOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillNewLeaderOpeningFixture.runner(false)
+  })
+
+  const decreaseStakeFixture: DecreaseStakeFixture = new DecreaseStakeFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Decrease worker stake', async () => decreaseStakeFixture.runner(false))
+
+  const slashFixture: SlashFixture = new SlashFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Slash worker', async () => slashFixture.runner(false))
+
+  const terminateRoleFixture: TerminateRoleFixture = new TerminateRoleFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Terminate worker role', async () => terminateRoleFixture.runner(false))
+
+  const newLeaveRoleFixture: LeaveRoleFixture = new LeaveRoleFixture(
+    apiWrapper,
+    leadKeyPair,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Leaving lead role', async () => newLeaveRoleFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 209 - 0
tests/network-tests/src/iznik/tests/workingGroup/manageWorkerAsWorkerTest.ts

@@ -0,0 +1,209 @@
+import { initConfig } from '../../utils/config'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { WsProvider, Keyring } from '@polkadot/api'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import {
+  AddLeaderOpeningFixture,
+  AddWorkerOpeningFixture,
+  ApplyForOpeningFixture,
+  BeginApplicationReviewFixture,
+  BeginLeaderApplicationReviewFixture,
+  FillLeaderOpeningFixture,
+  FillOpeningFixture,
+  IncreaseStakeFixture,
+  LeaveRoleFixture,
+  UpdateRewardAccountFixture,
+} from '../fixtures/workingGroupModule'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { Utils } from '../../utils/utils'
+import BN from 'bn.js'
+import tap from 'tap'
+
+tap.mocha.describe('Manage worker as worker scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.WORKING_GROUP_N!
+  const nKeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const leadKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const applicationStake: BN = new BN(process.env.WORKING_GROUP_APPLICATION_STAKE!)
+  const roleStake: BN = new BN(process.env.WORKING_GROUP_ROLE_STAKE!)
+  const firstRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const rewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const payoutAmount: BN = new BN(process.env.PAYOUT_AMOUNT!)
+  const unstakingPeriod: BN = new BN(process.env.STORAGE_WORKING_GROUP_UNSTAKING_PERIOD!)
+  const durationInBlocks = 38
+  const openingActivationDelay: BN = new BN(0)
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const happyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    nKeyPairs,
+    paidTerms
+  )
+  tap.test('Creating a set of members', async () => happyCaseFixture.runner(false))
+
+  const leaderHappyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    leadKeyPair,
+    paidTerms
+  )
+  tap.test('Buying membership for leader account', async () => leaderHappyCaseFixture.runner(false))
+
+  const addLeaderOpeningFixture: AddLeaderOpeningFixture = new AddLeaderOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add lead opening', async () => await addLeaderOpeningFixture.runner(false))
+
+  let applyForLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForLeaderOpeningFixture.runner(false)
+  })
+
+  let beginLeaderApplicationReviewFixture: BeginLeaderApplicationReviewFixture
+  tap.test('Begin lead application review', async () => {
+    beginLeaderApplicationReviewFixture = new BeginLeaderApplicationReviewFixture(
+      apiWrapper,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let fillLeaderOpeningFixture: FillLeaderOpeningFixture
+  tap.test('Fill lead opening', async () => {
+    fillLeaderOpeningFixture = new FillLeaderOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillLeaderOpeningFixture.runner(false)
+  })
+
+  const addWorkerOpeningFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    unstakingPeriod,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add worker opening', async () => addWorkerOpeningFixture.runner(false))
+
+  let applyForWorkerOpeningFixture: ApplyForOpeningFixture
+  tap.test('First apply for worker opening', async () => {
+    applyForWorkerOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForWorkerOpeningFixture.runner(false)
+  })
+
+  let beginApplicationReviewFixture: BeginApplicationReviewFixture
+  tap.test('Begin application review', async () => {
+    beginApplicationReviewFixture = new BeginApplicationReviewFixture(
+      apiWrapper,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginApplicationReviewFixture.runner(false)
+  })
+
+  let fillOpeningFixture: FillOpeningFixture
+  tap.test('Fill worker opening', async () => {
+    fillOpeningFixture = new FillOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillOpeningFixture.runner(false)
+  })
+
+  const increaseStakeFixture: IncreaseStakeFixture = new IncreaseStakeFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Increase worker stake', async () => increaseStakeFixture.runner(false))
+
+  const updateRewardAccountFixture: UpdateRewardAccountFixture = new UpdateRewardAccountFixture(
+    apiWrapper,
+    nKeyPairs,
+    keyring,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Update reward account', async () => updateRewardAccountFixture.runner(false))
+
+  const updateRoleAccountFixture: UpdateRewardAccountFixture = new UpdateRewardAccountFixture(
+    apiWrapper,
+    nKeyPairs,
+    keyring,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Update role account', async () => updateRoleAccountFixture.runner(false))
+
+  const newLeaveRoleFixture: LeaveRoleFixture = new LeaveRoleFixture(
+    apiWrapper,
+    leadKeyPair,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Leaving lead role', async () => newLeaveRoleFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 204 - 0
tests/network-tests/src/iznik/tests/workingGroup/workerApplicationHappyCaseTest.ts

@@ -0,0 +1,204 @@
+import { initConfig } from '../../utils/config'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { WsProvider, Keyring } from '@polkadot/api'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import BN from 'bn.js'
+import tap from 'tap'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { Utils } from '../../utils/utils'
+import {
+  AddLeaderOpeningFixture,
+  AddWorkerOpeningFixture,
+  ApplyForOpeningFixture,
+  BeginApplicationReviewFixture,
+  BeginLeaderApplicationReviewFixture,
+  FillLeaderOpeningFixture,
+  FillOpeningFixture,
+  LeaveRoleFixture,
+  WithdrawApplicationFixture,
+} from '../fixtures/workingGroupModule'
+
+tap.mocha.describe('Worker application happy case scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.WORKING_GROUP_N!
+  const nKeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const leadKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const applicationStake: BN = new BN(process.env.WORKING_GROUP_APPLICATION_STAKE!)
+  const roleStake: BN = new BN(process.env.WORKING_GROUP_ROLE_STAKE!)
+  const firstRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const rewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const payoutAmount: BN = new BN(process.env.PAYOUT_AMOUNT!)
+  const unstakingPeriod: BN = new BN(process.env.STORAGE_WORKING_GROUP_UNSTAKING_PERIOD!)
+  const durationInBlocks = 48
+  const openingActivationDelay: BN = new BN(0)
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const happyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    nKeyPairs,
+    paidTerms
+  )
+  tap.test('Creating a set of members', async () => happyCaseFixture.runner(false))
+
+  const leaderHappyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    leadKeyPair,
+    paidTerms
+  )
+  tap.test('Buying membership for leader account', async () => leaderHappyCaseFixture.runner(false))
+
+  const addLeaderOpeningFixture: AddLeaderOpeningFixture = new AddLeaderOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add lead opening', async () => await addLeaderOpeningFixture.runner(false))
+
+  let applyForLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForLeaderOpeningFixture.runner(false)
+  })
+
+  let beginLeaderApplicationReviewFixture: BeginLeaderApplicationReviewFixture
+  tap.test('Begin lead application review', async () => {
+    beginLeaderApplicationReviewFixture = new BeginLeaderApplicationReviewFixture(
+      apiWrapper,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let fillLeaderOpeningFixture: FillLeaderOpeningFixture
+  tap.test('Fill lead opening', async () => {
+    fillLeaderOpeningFixture = new FillLeaderOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillLeaderOpeningFixture.runner(false)
+  })
+
+  const addWorkerOpeningFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    unstakingPeriod,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add worker opening', async () => addWorkerOpeningFixture.runner(false))
+
+  let firstApplyForWorkerOpeningFixture: ApplyForOpeningFixture
+  tap.test('First apply for worker opening', async () => {
+    firstApplyForWorkerOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await firstApplyForWorkerOpeningFixture.runner(false)
+  })
+
+  const withdrawApplicationFixture: WithdrawApplicationFixture = new WithdrawApplicationFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Withdraw worker application', async () => withdrawApplicationFixture.runner(false))
+
+  let secondApplyForWorkerOpeningFixture: ApplyForOpeningFixture
+  tap.test('Second apply for worker opening', async () => {
+    secondApplyForWorkerOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await secondApplyForWorkerOpeningFixture.runner(false)
+  })
+
+  let beginApplicationReviewFixture: BeginApplicationReviewFixture
+  tap.test('Begin application review', async () => {
+    beginApplicationReviewFixture = new BeginApplicationReviewFixture(
+      apiWrapper,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginApplicationReviewFixture.runner(false)
+  })
+
+  let fillOpeningFixture: FillOpeningFixture
+  tap.test('Fill worker opening', async () => {
+    fillOpeningFixture = new FillOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillOpeningFixture.runner(false)
+  })
+
+  const leaveRoleFixture: LeaveRoleFixture = new LeaveRoleFixture(
+    apiWrapper,
+    leadKeyPair,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Leaving lead role', async () => leaveRoleFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 204 - 0
tests/network-tests/src/iznik/tests/workingGroup/workerApplicationRejectionCaseTest.ts

@@ -0,0 +1,204 @@
+import { initConfig } from '../../utils/config'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { WsProvider, Keyring } from '@polkadot/api'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import BN from 'bn.js'
+import tap from 'tap'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { Utils } from '../../utils/utils'
+import {
+  AcceptApplicationsFixture,
+  AddLeaderOpeningFixture,
+  AddWorkerOpeningFixture,
+  ApplyForOpeningFixture,
+  BeginLeaderApplicationReviewFixture,
+  FillLeaderOpeningFixture,
+  LeaveRoleFixture,
+  TerminateApplicationsFixture,
+} from '../fixtures/workingGroupModule'
+
+tap.mocha.describe('Worker application happy case scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.WORKING_GROUP_N!
+  const nKeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const leadKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+  const nonMemberKeyPairs = Utils.createKeyPairs(keyring, N)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const applicationStake: BN = new BN(process.env.WORKING_GROUP_APPLICATION_STAKE!)
+  const roleStake: BN = new BN(process.env.WORKING_GROUP_ROLE_STAKE!)
+  const firstRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const rewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const payoutAmount: BN = new BN(process.env.PAYOUT_AMOUNT!)
+  const unstakingPeriod: BN = new BN(process.env.STORAGE_WORKING_GROUP_UNSTAKING_PERIOD!)
+  const durationInBlocks = 38
+  const openingActivationDelay: BN = new BN(100)
+  const leadOpeningActivationDelay: BN = new BN(0)
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const happyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    nKeyPairs,
+    paidTerms
+  )
+  tap.test('Creating a set of members', async () => happyCaseFixture.runner(false))
+
+  const leaderHappyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    leadKeyPair,
+    paidTerms
+  )
+  tap.test('Buying membership for leader account', async () => leaderHappyCaseFixture.runner(false))
+
+  const addLeaderOpeningFixture: AddLeaderOpeningFixture = new AddLeaderOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    leadOpeningActivationDelay,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add lead opening', async () => await addLeaderOpeningFixture.runner(false))
+
+  let applyForLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForLeaderOpeningFixture.runner(false)
+  })
+
+  let beginLeaderApplicationReviewFixture: BeginLeaderApplicationReviewFixture
+  tap.test('Begin lead application review', async () => {
+    beginLeaderApplicationReviewFixture = new BeginLeaderApplicationReviewFixture(
+      apiWrapper,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let fillLeaderOpeningFixture: FillLeaderOpeningFixture
+  tap.test('Fill lead opening', async () => {
+    fillLeaderOpeningFixture = new FillLeaderOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillLeaderOpeningFixture.runner(false)
+  })
+
+  const addWorkerOpeningFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    unstakingPeriod,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add worker opening', async () => addWorkerOpeningFixture.runner(false))
+
+  let applyForWorkerOpeningBeforeAcceptanceFixture: ApplyForOpeningFixture
+  tap.test('Apply for worker opening, expect failure', async () => {
+    applyForWorkerOpeningBeforeAcceptanceFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForWorkerOpeningBeforeAcceptanceFixture.runner(true)
+  })
+
+  let acceptApplicationsFixture: AcceptApplicationsFixture
+  tap.test('Begin accepting worker applications', async () => {
+    acceptApplicationsFixture = new AcceptApplicationsFixture(
+      apiWrapper,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    acceptApplicationsFixture.runner(false)
+  })
+
+  let applyForWorkerOpeningAsNonMemberFixture: ApplyForOpeningFixture
+  tap.test('Apply for worker opening as non-member, expect failure', async () => {
+    applyForWorkerOpeningAsNonMemberFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      nonMemberKeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForWorkerOpeningAsNonMemberFixture.runner(true)
+  })
+
+  let applyForWorkerOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for worker opening', async () => {
+    applyForWorkerOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      nKeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForWorkerOpeningFixture.runner(false)
+  })
+
+  const terminateApplicationsFixture: TerminateApplicationsFixture = new TerminateApplicationsFixture(
+    apiWrapper,
+    nKeyPairs,
+    leadKeyPair[0],
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Terminate worker applicaitons', async () => terminateApplicationsFixture.runner(false))
+
+  const leaveRoleFixture: LeaveRoleFixture = new LeaveRoleFixture(
+    apiWrapper,
+    leadKeyPair,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Leaving lead role', async () => leaveRoleFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 243 - 0
tests/network-tests/src/iznik/tests/workingGroup/workerPayoutTest.ts

@@ -0,0 +1,243 @@
+import { initConfig } from '../../utils/config'
+import { registerJoystreamTypes } from '@nicaea/types'
+import { closeApi } from '../../utils/closeApi'
+import { ApiWrapper, WorkingGroups } from '../../utils/apiWrapper'
+import { WsProvider, Keyring } from '@polkadot/api'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { setTestTimeout } from '../../utils/setTestTimeout'
+import {
+  AddLeaderOpeningFixture,
+  AddWorkerOpeningFixture,
+  ApplyForOpeningFixture,
+  AwaitPayoutFixture,
+  BeginApplicationReviewFixture,
+  BeginLeaderApplicationReviewFixture,
+  ExpectMintCapacityChangedFixture,
+  FillLeaderOpeningFixture,
+  FillOpeningFixture,
+  LeaveRoleFixture,
+} from '../fixtures/workingGroupModule'
+import BN from 'bn.js'
+import tap from 'tap'
+import { BuyMembershipHappyCaseFixture } from '../fixtures/membershipModule'
+import { Utils } from '../../utils/utils'
+import { ElectCouncilFixture } from '../fixtures/councilElectionModule'
+import { VoteForProposalFixture, WorkingGroupMintCapacityProposalFixture } from '../fixtures/proposalsModule'
+
+tap.mocha.describe('Worker application happy case scenario', async () => {
+  initConfig()
+  registerJoystreamTypes()
+
+  const nodeUrl: string = process.env.NODE_URL!
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!
+  const keyring = new Keyring({ type: 'sr25519' })
+  const provider = new WsProvider(nodeUrl)
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider)
+  const sudo: KeyringPair = keyring.addFromUri(sudoUri)
+
+  const N: number = +process.env.WORKING_GROUP_N!
+  const m1KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const m2KeyPairs: KeyringPair[] = Utils.createKeyPairs(keyring, N)
+  const leadKeyPair: KeyringPair[] = Utils.createKeyPairs(keyring, 1)
+
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!
+  const K: number = +process.env.COUNCIL_ELECTION_K!
+  const greaterStake: BN = new BN(+process.env.COUNCIL_STAKE_GREATER_AMOUNT!)
+  const lesserStake: BN = new BN(+process.env.COUNCIL_STAKE_LESSER_AMOUNT!)
+  const applicationStake: BN = new BN(process.env.WORKING_GROUP_APPLICATION_STAKE!)
+  const roleStake: BN = new BN(process.env.WORKING_GROUP_ROLE_STAKE!)
+  const leaderFirstRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const leaderRewardInterval: BN = new BN(process.env.LONG_REWARD_INTERVAL!)
+  const firstRewardInterval: BN = new BN(process.env.SHORT_FIRST_REWARD_INTERVAL!)
+  const rewardInterval: BN = new BN(process.env.SHORT_REWARD_INTERVAL!)
+  const payoutAmount: BN = new BN(process.env.PAYOUT_AMOUNT!)
+  const unstakingPeriod: BN = new BN(process.env.STORAGE_WORKING_GROUP_UNSTAKING_PERIOD!)
+  const mintCapacity: BN = new BN(process.env.STORAGE_WORKING_GROUP_MINTING_CAPACITY!)
+  const durationInBlocks = 48
+  const openingActivationDelay: BN = new BN(0)
+
+  setTestTimeout(apiWrapper, durationInBlocks)
+
+  const firstMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m1KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating first set of members', async () => firstMemberSetFixture.runner(false))
+
+  const secondMemberSetFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    m2KeyPairs,
+    paidTerms
+  )
+  tap.test('Creating second set of members', async () => secondMemberSetFixture.runner(false))
+
+  const electCouncilFixture: ElectCouncilFixture = new ElectCouncilFixture(
+    apiWrapper,
+    m1KeyPairs,
+    m2KeyPairs,
+    K,
+    sudo,
+    greaterStake,
+    lesserStake
+  )
+  tap.test('Elect council', async () => electCouncilFixture.runner(false))
+
+  const leaderHappyCaseFixture: BuyMembershipHappyCaseFixture = new BuyMembershipHappyCaseFixture(
+    apiWrapper,
+    sudo,
+    leadKeyPair,
+    paidTerms
+  )
+  tap.test('Buying membership for leader account', async () => leaderHappyCaseFixture.runner(false))
+
+  const addLeaderOpeningFixture: AddLeaderOpeningFixture = new AddLeaderOpeningFixture(
+    apiWrapper,
+    m1KeyPairs,
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add lead opening', async () => await addLeaderOpeningFixture.runner(false))
+
+  let applyForLeaderOpeningFixture: ApplyForOpeningFixture
+  tap.test('Apply for lead opening', async () => {
+    applyForLeaderOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      applicationStake,
+      roleStake,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForLeaderOpeningFixture.runner(false)
+  })
+
+  let beginLeaderApplicationReviewFixture: BeginLeaderApplicationReviewFixture
+  tap.test('Begin lead application review', async () => {
+    beginLeaderApplicationReviewFixture = new BeginLeaderApplicationReviewFixture(
+      apiWrapper,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginLeaderApplicationReviewFixture.runner(false)
+  })
+
+  let fillLeaderOpeningFixture: FillLeaderOpeningFixture
+  tap.test('Fill lead opening', async () => {
+    fillLeaderOpeningFixture = new FillLeaderOpeningFixture(
+      apiWrapper,
+      leadKeyPair,
+      sudo,
+      addLeaderOpeningFixture.getResult()!,
+      leaderFirstRewardInterval,
+      leaderRewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillLeaderOpeningFixture.runner(false)
+  })
+
+  const workingGroupMintCapacityProposalFixture: WorkingGroupMintCapacityProposalFixture = new WorkingGroupMintCapacityProposalFixture(
+    apiWrapper,
+    m1KeyPairs,
+    sudo,
+    mintCapacity,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Propose mint capacity', async () => workingGroupMintCapacityProposalFixture.runner(false))
+
+  let voteForProposalFixture: VoteForProposalFixture
+  const expectMintCapacityChanged: ExpectMintCapacityChangedFixture = new ExpectMintCapacityChangedFixture(
+    apiWrapper,
+    mintCapacity
+  )
+  tap.test('Approve mint capacity', async () => {
+    voteForProposalFixture = new VoteForProposalFixture(
+      apiWrapper,
+      m2KeyPairs,
+      sudo,
+      workingGroupMintCapacityProposalFixture.getResult()!
+    )
+    voteForProposalFixture.runner(false)
+    await expectMintCapacityChanged.runner(false)
+  })
+
+  const addWorkerOpeningFixture: AddWorkerOpeningFixture = new AddWorkerOpeningFixture(
+    apiWrapper,
+    m1KeyPairs,
+    leadKeyPair[0],
+    sudo,
+    applicationStake,
+    roleStake,
+    openingActivationDelay,
+    unstakingPeriod,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Add worker opening', async () => addWorkerOpeningFixture.runner(false))
+
+  let applyForWorkerOpeningFixture: ApplyForOpeningFixture
+  tap.test('First apply for worker opening', async () => {
+    applyForWorkerOpeningFixture = new ApplyForOpeningFixture(
+      apiWrapper,
+      m1KeyPairs,
+      sudo,
+      applicationStake,
+      roleStake,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await applyForWorkerOpeningFixture.runner(false)
+  })
+
+  let beginApplicationReviewFixture: BeginApplicationReviewFixture
+  tap.test('Begin application review', async () => {
+    beginApplicationReviewFixture = new BeginApplicationReviewFixture(
+      apiWrapper,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await beginApplicationReviewFixture.runner(false)
+  })
+
+  let fillOpeningFixture: FillOpeningFixture
+  tap.test('Fill worker opening', async () => {
+    fillOpeningFixture = new FillOpeningFixture(
+      apiWrapper,
+      m1KeyPairs,
+      leadKeyPair[0],
+      sudo,
+      addWorkerOpeningFixture.getResult()!,
+      firstRewardInterval,
+      rewardInterval,
+      payoutAmount,
+      WorkingGroups.StorageWorkingGroup
+    )
+    await fillOpeningFixture.runner(false)
+  })
+
+  const awaitPayoutFixture: AwaitPayoutFixture = new AwaitPayoutFixture(
+    apiWrapper,
+    m1KeyPairs,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Await worker payout', async () => awaitPayoutFixture.runner(false))
+
+  const leaveRoleFixture: LeaveRoleFixture = new LeaveRoleFixture(
+    apiWrapper,
+    leadKeyPair,
+    sudo,
+    WorkingGroups.StorageWorkingGroup
+  )
+  tap.test('Leaving lead role', async () => leaveRoleFixture.runner(false))
+
+  closeApi(apiWrapper)
+})

+ 1551 - 0
tests/network-tests/src/iznik/utils/apiWrapper.ts

@@ -0,0 +1,1551 @@
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { Bytes, Option, u32, Vec } from '@polkadot/types'
+import { Codec } from '@polkadot/types/types'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { MemberId, PaidMembershipTerms, UserInfo } from '@nicaea/types/members'
+import { Mint, MintId } from '@nicaea/types/mint'
+import { Lead, LeadId } from '@nicaea/types/content-working-group'
+import {
+  Application,
+  ApplicationIdToWorkerIdMap,
+  Opening,
+  RewardPolicy,
+  SlashingTerms,
+  Worker,
+  WorkerId,
+  WorkingGroupOpeningPolicyCommitment,
+} from '@nicaea/types/working-group'
+import { RoleParameters } from '@nicaea/types/roles'
+import { Seat } from '@nicaea/types/council'
+import { AccountId, Balance, BalanceOf, BlockNumber, Event, EventRecord } from '@polkadot/types/interfaces'
+import BN from 'bn.js'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Sender } from './sender'
+import { Utils } from './utils'
+import { Stake, StakedState } from '@nicaea/types/stake'
+import { RewardRelationship } from '@nicaea/types/recurring-rewards'
+import {
+  ActivateOpeningAt,
+  Application as HiringApplication,
+  ApplicationId,
+  ApplicationRationingPolicy,
+  Opening as HiringOpening,
+  OpeningId,
+  StakingPolicy,
+} from '@nicaea/types/hiring'
+import { FillOpeningParameters } from '@nicaea/types/proposals'
+import { WorkingGroup } from '@nicaea/types/common'
+
+export enum WorkingGroups {
+  StorageWorkingGroup = 'storageWorkingGroup',
+}
+
+export class ApiWrapper {
+  private readonly api: ApiPromise
+  private readonly sender: Sender
+
+  public static async create(provider: WsProvider): Promise<ApiWrapper> {
+    const api = await ApiPromise.create({ provider })
+    return new ApiWrapper(api)
+  }
+
+  constructor(api: ApiPromise) {
+    this.api = api
+    this.sender = new Sender(api)
+  }
+
+  public close() {
+    this.api.disconnect()
+  }
+
+  public getWorkingGroupString(workingGroup: WorkingGroups): string {
+    switch (workingGroup) {
+      case WorkingGroups.StorageWorkingGroup:
+        return 'Storage'
+      default:
+        throw new Error(`Invalid working group string representation: ${workingGroup}`)
+    }
+  }
+
+  public async buyMembership(
+    account: KeyringPair,
+    paidTermsId: number,
+    name: string,
+    expectFailure = false
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.members.buyMembership(paidTermsId, new UserInfo({ 'handle': name, 'avatar_uri': '', 'about': '' })),
+      account,
+      expectFailure
+    )
+  }
+
+  public getMemberIds(address: string): Promise<MemberId[]> {
+    return this.api.query.members.memberIdsByControllerAccountId<Vec<MemberId>>(address)
+  }
+
+  public getBalance(address: string): Promise<Balance> {
+    return this.api.query.balances.freeBalance<Balance>(address)
+  }
+
+  public async transferBalance(from: KeyringPair, to: string, amount: BN): Promise<void> {
+    return this.sender.signAndSend(this.api.tx.balances.transfer(to, amount), from)
+  }
+
+  public getPaidMembershipTerms(paidTermsId: number): Promise<Option<PaidMembershipTerms>> {
+    return this.api.query.members.paidMembershipTermsById<Option<PaidMembershipTerms>>(paidTermsId)
+  }
+
+  public getMembershipFee(paidTermsId: number): Promise<BN> {
+    return this.getPaidMembershipTerms(paidTermsId).then((terms) => terms.unwrap().fee.toBn())
+  }
+
+  public async transferBalanceToAccounts(from: KeyringPair, to: KeyringPair[], amount: BN): Promise<void[]> {
+    return Promise.all(
+      to.map(async (keyPair) => {
+        await this.transferBalance(from, keyPair.address, amount)
+      })
+    )
+  }
+
+  private getBaseTxFee(): BN {
+    return this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionBaseFee).toBn()
+  }
+
+  private estimateTxFee(tx: SubmittableExtrinsic<'promise'>): BN {
+    const baseFee: BN = this.getBaseTxFee()
+    const byteFee: BN = this.api.createType('BalanceOf', this.api.consts.transactionPayment.transactionByteFee).toBn()
+    return Utils.calcTxLength(tx).mul(byteFee).add(baseFee)
+  }
+
+  public estimateBuyMembershipFee(account: KeyringPair, paidTermsId: number, name: string): BN {
+    return this.estimateTxFee(
+      this.api.tx.members.buyMembership(paidTermsId, new UserInfo({ 'handle': name, 'avatar_uri': '', 'about': '' }))
+    )
+  }
+
+  public estimateApplyForCouncilFee(amount: BN): BN {
+    return this.estimateTxFee(this.api.tx.councilElection.apply(amount))
+  }
+
+  public estimateVoteForCouncilFee(nominee: string, salt: string, stake: BN): BN {
+    const hashedVote: string = Utils.hashVote(nominee, salt)
+    return this.estimateTxFee(this.api.tx.councilElection.vote(hashedVote, stake))
+  }
+
+  public estimateRevealVoteFee(nominee: string, salt: string): BN {
+    const hashedVote: string = Utils.hashVote(nominee, salt)
+    return this.estimateTxFee(this.api.tx.councilElection.reveal(hashedVote, nominee, salt))
+  }
+
+  public estimateProposeRuntimeUpgradeFee(stake: BN, name: string, description: string, runtime: Bytes | string): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(stake, name, description, stake, runtime)
+    )
+  }
+
+  public estimateProposeTextFee(stake: BN, name: string, description: string, text: string): BN {
+    return this.estimateTxFee(this.api.tx.proposalsCodex.createTextProposal(stake, name, description, stake, text))
+  }
+
+  public estimateProposeSpendingFee(
+    title: string,
+    description: string,
+    stake: BN,
+    balance: BN,
+    destination: string
+  ): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSpendingProposal(stake, title, description, stake, balance, destination)
+    )
+  }
+
+  public estimateProposeContentWorkingGroupMintCapacityFee(
+    title: string,
+    description: string,
+    stake: BN,
+    balance: BN
+  ): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal(
+        stake,
+        title,
+        description,
+        stake,
+        balance
+      )
+    )
+  }
+
+  public estimateProposeValidatorCountFee(title: string, description: string, stake: BN): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSetValidatorCountProposal(stake, title, description, stake, stake)
+    )
+  }
+
+  public estimateProposeLeadFee(title: string, description: string, stake: BN, address: string): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSetLeadProposal(stake, title, description, stake, { stake, address })
+    )
+  }
+
+  public estimateProposeEvictStorageProviderFee(title: string, description: string, stake: BN, address: string): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createEvictStorageProviderProposal(stake, title, description, stake, address)
+    )
+  }
+
+  public estimateProposeStorageRoleParametersFee(
+    title: string,
+    description: string,
+    stake: BN,
+    minStake: BN,
+    minActors: BN,
+    maxActors: BN,
+    reward: BN,
+    rewardPeriod: BN,
+    bondingPeriod: BN,
+    unbondingPeriod: BN,
+    minServicePeriod: BN,
+    startupGracePeriod: BN,
+    entryRequestFee: BN
+  ): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSetStorageRoleParametersProposal(stake, title, description, stake, [
+        minStake,
+        minActors,
+        maxActors,
+        reward,
+        rewardPeriod,
+        bondingPeriod,
+        unbondingPeriod,
+        minServicePeriod,
+        startupGracePeriod,
+        entryRequestFee,
+      ])
+    )
+  }
+
+  public estimateProposeElectionParametersFee(
+    title: string,
+    description: string,
+    stake: BN,
+    announcingPeriod: BN,
+    votingPeriod: BN,
+    revealingPeriod: BN,
+    councilSize: BN,
+    candidacyLimit: BN,
+    newTermDuration: BN,
+    minCouncilStake: BN,
+    minVotingStake: BN
+  ): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSetElectionParametersProposal(stake, title, description, stake, [
+        announcingPeriod,
+        votingPeriod,
+        revealingPeriod,
+        councilSize,
+        candidacyLimit,
+        newTermDuration,
+        minCouncilStake,
+        minVotingStake,
+      ])
+    )
+  }
+
+  public estimateVoteForProposalFee(): BN {
+    return this.estimateTxFee(this.api.tx.proposalsEngine.vote(0, 0, 'Approve'))
+  }
+
+  public estimateAddOpeningFee(module: WorkingGroups): BN {
+    const commitment: WorkingGroupOpeningPolicyCommitment = new WorkingGroupOpeningPolicyCommitment({
+      application_rationing_policy: new Option(ApplicationRationingPolicy, {
+        max_active_applicants: new BN(32) as u32,
+      }),
+      max_review_period_length: new BN(32) as u32,
+      application_staking_policy: new Option(StakingPolicy, {
+        amount: new BN(1),
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_staking_policy: new Option(StakingPolicy, {
+        amount: new BN(1),
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_slashing_terms: new SlashingTerms({
+        Slashable: {
+          max_count: new BN(0),
+          max_percent_pts_per_time: new BN(0),
+        },
+      }),
+      fill_opening_successful_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+    })
+
+    return this.estimateTxFee(
+      this.api.tx[module].addOpening('CurrentBlock', commitment, 'Human readable text', 'Worker')
+    )
+  }
+
+  public estimateAcceptApplicationsFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].acceptApplications(0))
+  }
+
+  public estimateApplyOnOpeningFee(account: KeyringPair, module: WorkingGroups): BN {
+    return this.estimateTxFee(
+      this.api.tx[module].applyOnOpening(
+        0,
+        0,
+        account.address,
+        0,
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test'
+      )
+    )
+  }
+
+  public estimateBeginApplicantReviewFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].beginApplicantReview(0))
+  }
+
+  public estimateFillOpeningFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(
+      this.api.tx[module].fillOpening(0, [0], {
+        'amount_per_payout': 0,
+        'next_payment_at_block': 0,
+        'payout_interval': 0,
+      })
+    )
+  }
+
+  public estimateIncreaseStakeFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].increaseStake(0, 0))
+  }
+
+  public estimateDecreaseStakeFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].decreaseStake(0, 0))
+  }
+
+  public estimateUpdateRoleAccountFee(address: string, module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].updateRoleAccount(0, address))
+  }
+
+  public estimateUpdateRewardAccountFee(address: string, module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].updateRewardAccount(0, address))
+  }
+
+  public estimateLeaveRoleFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].leaveRole(0, 'Long justification text'))
+  }
+
+  public estimateWithdrawApplicationFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].withdrawApplication(0))
+  }
+
+  public estimateTerminateApplicationFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].terminateApplication(0))
+  }
+
+  public estimateSlashStakeFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(this.api.tx[module].slashStake(0, 0))
+  }
+
+  public estimateTerminateRoleFee(module: WorkingGroups): BN {
+    return this.estimateTxFee(
+      this.api.tx[module].terminateRole(
+        0,
+        'Long justification text explaining why the worker role will be terminated',
+        false
+      )
+    )
+  }
+
+  public estimateProposeCreateWorkingGroupLeaderOpeningFee(): BN {
+    const commitment: WorkingGroupOpeningPolicyCommitment = new WorkingGroupOpeningPolicyCommitment({
+      application_rationing_policy: new Option(ApplicationRationingPolicy, {
+        max_active_applicants: new BN(32) as u32,
+      }),
+      max_review_period_length: new BN(32) as u32,
+      application_staking_policy: new Option(StakingPolicy, {
+        amount: new BN(1),
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_staking_policy: new Option(StakingPolicy, {
+        amount: new BN(1),
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: new BN(1),
+        review_period_expired_unstaking_period_length: new BN(1),
+      }),
+      role_slashing_terms: new SlashingTerms({
+        Slashable: {
+          max_count: new BN(0),
+          max_percent_pts_per_time: new BN(0),
+        },
+      }),
+      fill_opening_successful_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      fill_opening_failed_applicant_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      terminate_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_application_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+      exit_role_stake_unstaking_period: new Option(u32, new BN(1) as BlockNumber),
+    })
+
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createAddWorkingGroupLeaderOpeningProposal(
+        0,
+        'some long title for the purpose of testing',
+        'some long description for the purpose of testing',
+        0,
+        {
+          'activate_at': 'CurrentBlock',
+          'commitment': commitment,
+          'human_readable_text': 'Opening readable text',
+          'working_group': 'Storage',
+        }
+      )
+    )
+  }
+
+  public estimateProposeBeginWorkingGroupLeaderApplicationReviewFee(): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createBeginReviewWorkingGroupLeaderApplicationsProposal(
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        0,
+        0,
+        'Storage'
+      )
+    )
+  }
+
+  public estimateProposeFillLeaderOpeningFee(): BN {
+    const fillOpeningParameters: FillOpeningParameters = new FillOpeningParameters({
+      opening_id: new BN(0) as OpeningId,
+      successful_application_id: new BN(0) as ApplicationId,
+      reward_policy: new Option(
+        RewardPolicy,
+        new RewardPolicy({
+          amount_per_payout: new BN(1) as Balance,
+          next_payment_at_block: new BN(99999) as BlockNumber,
+          payout_interval: new Option(u32, new BN(99999) as u32),
+        })
+      ),
+      working_group: new WorkingGroup('Storage'),
+    })
+
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createFillWorkingGroupLeaderOpeningProposal(
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        0,
+        fillOpeningParameters
+      )
+    )
+  }
+
+  public estimateProposeTerminateLeaderRoleFee(): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createTerminateWorkingGroupLeaderRoleProposal(
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        0,
+        {
+          'worker_id': 0,
+          'rationale': 'Exceptionaly long and extraordinary descriptive rationale',
+          'slash': true,
+          'working_group': 'Storage',
+        }
+      )
+    )
+  }
+
+  public estimateProposeLeaderRewardFee(): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSetWorkingGroupLeaderRewardProposal(
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        0,
+        0,
+        0,
+        'Storage'
+      )
+    )
+  }
+
+  public estimateProposeDecreaseLeaderStakeFee(): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createDecreaseWorkingGroupLeaderStakeProposal(
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        0,
+        0,
+        0,
+        'Storage'
+      )
+    )
+  }
+
+  public estimateProposeSlashLeaderStakeFee(): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSlashWorkingGroupLeaderStakeProposal(
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        0,
+        0,
+        0,
+        'Storage'
+      )
+    )
+  }
+
+  public estimateProposeWorkingGroupMintCapacityFee(): BN {
+    return this.estimateTxFee(
+      this.api.tx.proposalsCodex.createSetWorkingGroupMintCapacityProposal(
+        0,
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        'Some testing text used for estimation purposes which is longer than text expected during the test',
+        0,
+        0,
+        'Storage'
+      )
+    )
+  }
+
+  private applyForCouncilElection(account: KeyringPair, amount: BN): Promise<void> {
+    return this.sender.signAndSend(this.api.tx.councilElection.apply(amount), account, false)
+  }
+
+  public batchApplyForCouncilElection(accounts: KeyringPair[], amount: BN): Promise<void[]> {
+    return Promise.all(
+      accounts.map(async (keyPair) => {
+        await this.applyForCouncilElection(keyPair, amount)
+      })
+    )
+  }
+
+  public async getCouncilElectionStake(address: string): Promise<BN> {
+    // TODO alter then `applicantStake` type will be introduced
+    return this.api.query.councilElection.applicantStakes(address).then((stake) => {
+      const parsed = JSON.parse(stake.toString())
+      return new BN(parsed.new)
+    })
+  }
+
+  private voteForCouncilMember(account: KeyringPair, nominee: string, salt: string, stake: BN): Promise<void> {
+    const hashedVote: string = Utils.hashVote(nominee, salt)
+    return this.sender.signAndSend(this.api.tx.councilElection.vote(hashedVote, stake), account, false)
+  }
+
+  public batchVoteForCouncilMember(
+    accounts: KeyringPair[],
+    nominees: KeyringPair[],
+    salt: string[],
+    stake: BN
+  ): Promise<void[]> {
+    return Promise.all(
+      accounts.map(async (keyPair, index) => {
+        await this.voteForCouncilMember(keyPair, nominees[index].address, salt[index], stake)
+      })
+    )
+  }
+
+  private revealVote(account: KeyringPair, commitment: string, nominee: string, salt: string): Promise<void> {
+    return this.sender.signAndSend(this.api.tx.councilElection.reveal(commitment, nominee, salt), account, false)
+  }
+
+  public batchRevealVote(accounts: KeyringPair[], nominees: KeyringPair[], salt: string[]): Promise<void[]> {
+    return Promise.all(
+      accounts.map(async (keyPair, index) => {
+        const commitment = Utils.hashVote(nominees[index].address, salt[index])
+        await this.revealVote(keyPair, commitment, nominees[index].address, salt[index])
+      })
+    )
+  }
+
+  // TODO consider using configurable genesis instead
+  public sudoStartAnnouncingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageAnnouncing(endsAtBlock)),
+      sudo,
+      false
+    )
+  }
+
+  public sudoStartVotingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageVoting(endsAtBlock)),
+      sudo,
+      false
+    )
+  }
+
+  public sudoStartRevealingPerion(sudo: KeyringPair, endsAtBlock: BN): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(this.api.tx.councilElection.setStageRevealing(endsAtBlock)),
+      sudo,
+      false
+    )
+  }
+
+  public sudoSetCouncilMintCapacity(sudo: KeyringPair, capacity: BN): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(this.api.tx.council.setCouncilMintCapacity(capacity)),
+      sudo,
+      false
+    )
+  }
+
+  public sudoSetWorkingGroupMintCapacity(sudo: KeyringPair, capacity: BN, module: WorkingGroups): Promise<void> {
+    return this.sender.signAndSend(this.api.tx.sudo.sudo(this.api.tx[module].setMintCapacity(capacity)), sudo, false)
+  }
+
+  public getBestBlock(): Promise<BN> {
+    return this.api.derive.chain.bestNumber()
+  }
+
+  public getCouncil(): Promise<Seat[]> {
+    return this.api.query.council.activeCouncil<Vec<Codec>>().then((seats) => {
+      return (seats as unknown) as Seat[]
+    })
+  }
+
+  public getRuntime(): Promise<Bytes> {
+    return this.api.query.substrate.code<Bytes>()
+  }
+
+  public async proposeRuntime(
+    account: KeyringPair,
+    stake: BN,
+    name: string,
+    description: string,
+    runtime: Bytes | string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(memberId, name, description, stake, runtime),
+      account,
+      false
+    )
+  }
+
+  public async proposeText(
+    account: KeyringPair,
+    stake: BN,
+    name: string,
+    description: string,
+    text: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createTextProposal(memberId, name, description, stake, text),
+      account,
+      false
+    )
+  }
+
+  public async proposeSpending(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    balance: BN,
+    destination: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSpendingProposal(memberId, title, description, stake, balance, destination),
+      account,
+      false
+    )
+  }
+
+  public async proposeContentWorkingGroupMintCapacity(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    balance: BN
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal(
+        memberId,
+        title,
+        description,
+        stake,
+        balance
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeValidatorCount(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    validatorCount: BN
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSetValidatorCountProposal(memberId, title, description, stake, validatorCount),
+      account,
+      false
+    )
+  }
+
+  public async proposeLead(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    leadAccount: KeyringPair
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    const leadMemberId: BN = (await this.getMemberIds(leadAccount.address))[0].toBn()
+    const addressString: string = leadAccount.address
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSetLeadProposal(memberId, title, description, stake, [
+        leadMemberId,
+        addressString,
+      ]),
+      account,
+      false
+    )
+  }
+
+  public async proposeEvictStorageProvider(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    storageProvider: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createEvictStorageProviderProposal(
+        memberId,
+        title,
+        description,
+        stake,
+        storageProvider
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeStorageRoleParameters(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    minStake: BN,
+    minActors: BN,
+    maxActors: BN,
+    reward: BN,
+    rewardPeriod: BN,
+    bondingPeriod: BN,
+    unbondingPeriod: BN,
+    minServicePeriod: BN,
+    startupGracePeriod: BN,
+    entryRequestFee: BN
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSetStorageRoleParametersProposal(memberId, title, description, stake, [
+        minStake,
+        minActors,
+        maxActors,
+        reward,
+        rewardPeriod,
+        bondingPeriod,
+        unbondingPeriod,
+        minServicePeriod,
+        startupGracePeriod,
+        entryRequestFee,
+      ]),
+      account,
+      false
+    )
+  }
+
+  public async proposeElectionParameters(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    announcingPeriod: BN,
+    votingPeriod: BN,
+    revealingPeriod: BN,
+    councilSize: BN,
+    candidacyLimit: BN,
+    newTermDuration: BN,
+    minCouncilStake: BN,
+    minVotingStake: BN
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSetElectionParametersProposal(memberId, title, description, stake, [
+        announcingPeriod,
+        votingPeriod,
+        revealingPeriod,
+        councilSize,
+        candidacyLimit,
+        newTermDuration,
+        minCouncilStake,
+        minVotingStake,
+      ]),
+      account,
+      false
+    )
+  }
+
+  public async proposeBeginWorkingGroupLeaderApplicationReview(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    stake: BN,
+    openingId: BN,
+    workingGroup: string
+  ) {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createBeginReviewWorkingGroupLeaderApplicationsProposal(
+        memberId,
+        title,
+        description,
+        stake,
+        openingId,
+        workingGroup
+      ),
+      account,
+      false
+    )
+  }
+
+  public approveProposal(account: KeyringPair, memberId: BN, proposal: BN): Promise<void> {
+    return this.sender.signAndSend(this.api.tx.proposalsEngine.vote(memberId, proposal, 'Approve'), account, false)
+  }
+
+  public batchApproveProposal(council: KeyringPair[], proposal: BN): Promise<void[]> {
+    return Promise.all(
+      council.map(async (keyPair) => {
+        const memberId: BN = (await this.getMemberIds(keyPair.address))[0].toBn()
+        await this.approveProposal(keyPair, memberId, proposal)
+      })
+    )
+  }
+
+  public getBlockDuration(): BN {
+    return this.api.createType('Moment', this.api.consts.babe.expectedBlockTime).toBn()
+  }
+
+  public expectProposalCreated(): Promise<BN> {
+    return new Promise(async (resolve) => {
+      const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
+        events.forEach((record) => {
+          if (record.event.method && record.event.method.toString() === 'ProposalCreated') {
+            unsubscribe()
+            resolve(new BN(record.event.data[1].toString()))
+          }
+        })
+      })
+    })
+  }
+
+  public expectRuntimeUpgraded(): Promise<void> {
+    return new Promise(async (resolve) => {
+      const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
+        events.forEach((record) => {
+          if (record.event.method.toString() === 'RuntimeUpdated') {
+            unsubscribe()
+            resolve()
+          }
+        })
+      })
+    })
+  }
+
+  public expectProposalFinalized(): Promise<void> {
+    return new Promise(async (resolve) => {
+      const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
+        events.forEach((record) => {
+          if (
+            record.event.method &&
+            record.event.method.toString() === 'ProposalStatusUpdated' &&
+            record.event.data[1].toString().includes('Executed')
+          ) {
+            unsubscribe()
+            resolve()
+          }
+        })
+      })
+    })
+  }
+
+  public expectOpeningFilled(): Promise<ApplicationIdToWorkerIdMap> {
+    return new Promise(async (resolve) => {
+      const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
+        events.forEach((record) => {
+          if (record.event.method && record.event.method.toString() === 'OpeningFilled') {
+            unsubscribe()
+            resolve((record.event.data[1] as unknown) as ApplicationIdToWorkerIdMap)
+          }
+        })
+      })
+    })
+  }
+
+  public expectEvent(eventName: string): Promise<Event> {
+    return new Promise(async (resolve) => {
+      const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
+        events.forEach((record) => {
+          if (record.event.method && record.event.method.toString() === eventName) {
+            unsubscribe()
+            resolve(record.event)
+          }
+        })
+      })
+    })
+  }
+
+  public expectApplicationReviewBegan(): Promise<BN> {
+    return new Promise(async (resolve) => {
+      const unsubscribe = await this.api.query.system.events<Vec<EventRecord>>((events) => {
+        events.forEach((record) => {
+          if (record.event.method && record.event.method.toString() === 'BeganApplicationReview') {
+            unsubscribe()
+            resolve((record.event.data as unknown) as BN)
+          }
+        })
+      })
+    })
+  }
+
+  public getTotalIssuance(): Promise<BN> {
+    return this.api.query.balances.totalIssuance<Balance>()
+  }
+
+  public async getRequiredProposalStake(numerator: number, denominator: number): Promise<BN> {
+    const issuance: number = await (await this.getTotalIssuance()).toNumber()
+    const stake = (issuance * numerator) / denominator
+    return new BN(stake.toFixed(0))
+  }
+
+  public getProposalCount(): Promise<BN> {
+    return this.api.query.proposalsEngine.proposalCount<u32>()
+  }
+
+  public async getContentWorkingGroupMintCapacity(): Promise<BN> {
+    const mintId: MintId = await this.api.query.contentWorkingGroup.mint<MintId>()
+    const mintCodec = await this.api.query.minting.mints<Codec[]>(mintId)
+    const mint: Mint = (mintCodec[0] as unknown) as Mint
+    return mint.getField<Balance>('capacity')
+  }
+
+  public async getWorkingGroupMintCapacity(module: WorkingGroups): Promise<BN> {
+    const mintId: MintId = await this.api.query[module].mint<MintId>()
+    const mintCodec = await this.api.query.minting.mints<Codec[]>(mintId)
+    const mint: Mint = (mintCodec[0] as unknown) as Mint
+    return mint.getField<Balance>('capacity')
+  }
+
+  public getValidatorCount(): Promise<BN> {
+    return this.api.query.staking.validatorCount<u32>()
+  }
+
+  public async getCurrentLeadAddress(): Promise<string> {
+    const leadId: Option<LeadId> = await this.api.query.contentWorkingGroup.currentLeadId<Option<LeadId>>()
+    const leadCodec = await this.api.query.contentWorkingGroup.leadById<Codec[]>(leadId.unwrap())
+    const lead = (leadCodec[0] as unknown) as Lead
+    return lead.role_account.toString()
+  }
+
+  public async createStorageProvider(account: KeyringPair): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0].toBn()
+    await this.sender.signAndSend(this.api.tx.actors.roleEntryRequest('StorageProvider', memberId), account, false)
+    await this.sender.signAndSend(this.api.tx.actors.stake('StorageProvider', account.address), account, false)
+    return
+  }
+
+  public async isStorageProvider(address: string): Promise<boolean> {
+    const storageProviders: Vec<AccountId> = await this.api.query.actors.accountIdsByRole<Vec<AccountId>>(
+      'StorageProvider'
+    )
+    const accountWorkers: BN = await this.getWorkerIdByRoleAccount(address, WorkingGroups.StorageWorkingGroup)
+    return accountWorkers !== undefined
+  }
+
+  public async addOpening(
+    leader: KeyringPair,
+    actiavteAt: ActivateOpeningAt,
+    commitment: WorkingGroupOpeningPolicyCommitment,
+    text: string,
+    type: string,
+    module: WorkingGroups,
+    expectFailure: boolean
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.createAddOpeningTransaction(actiavteAt, commitment, text, type, module),
+      leader,
+      expectFailure
+    )
+  }
+
+  public async sudoAddOpening(
+    sudo: KeyringPair,
+    actiavteAt: ActivateOpeningAt,
+    commitment: WorkingGroupOpeningPolicyCommitment,
+    text: string,
+    type: string,
+    module: WorkingGroups
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(this.createAddOpeningTransaction(actiavteAt, commitment, text, type, module)),
+      sudo,
+      false
+    )
+  }
+
+  public async proposeCreateWorkingGroupLeaderOpening(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    proposalStake: BN,
+    actiavteAt: ActivateOpeningAt,
+    commitment: WorkingGroupOpeningPolicyCommitment,
+    text: string,
+    workingGroup: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createAddWorkingGroupLeaderOpeningProposal(
+        memberId,
+        title,
+        description,
+        proposalStake,
+        {
+          activate_at: actiavteAt,
+          commitment: commitment,
+          human_readable_text: text,
+          working_group: workingGroup,
+        }
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeFillLeaderOpening(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    proposalStake: BN,
+    fillOpeningParameters: FillOpeningParameters
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createFillWorkingGroupLeaderOpeningProposal(
+        memberId,
+        title,
+        description,
+        proposalStake,
+        fillOpeningParameters
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeTerminateLeaderRole(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    proposalStake: BN,
+    leadWorkerId: BN,
+    rationale: string,
+    slash: boolean,
+    workingGroup: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createTerminateWorkingGroupLeaderRoleProposal(
+        memberId,
+        title,
+        description,
+        proposalStake,
+        {
+          'worker_id': leadWorkerId,
+          rationale,
+          slash,
+          'working_group': workingGroup,
+        }
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeLeaderReward(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    proposalStake: BN,
+    workerId: BN,
+    rewardAmount: BN,
+    workingGroup: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSetWorkingGroupLeaderRewardProposal(
+        memberId,
+        title,
+        description,
+        proposalStake,
+        workerId,
+        rewardAmount,
+        workingGroup
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeDecreaseLeaderStake(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    proposalStake: BN,
+    workerId: BN,
+    rewardAmount: BN,
+    workingGroup: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createDecreaseWorkingGroupLeaderStakeProposal(
+        memberId,
+        title,
+        description,
+        proposalStake,
+        workerId,
+        rewardAmount,
+        workingGroup
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeSlashLeaderStake(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    proposalStake: BN,
+    workerId: BN,
+    rewardAmount: BN,
+    workingGroup: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSlashWorkingGroupLeaderStakeProposal(
+        memberId,
+        title,
+        description,
+        proposalStake,
+        workerId,
+        rewardAmount,
+        workingGroup
+      ),
+      account,
+      false
+    )
+  }
+
+  public async proposeWorkingGroupMintCapacity(
+    account: KeyringPair,
+    title: string,
+    description: string,
+    proposalStake: BN,
+    mintCapacity: BN,
+    workingGroup: string
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+    return this.sender.signAndSend(
+      this.api.tx.proposalsCodex.createSetWorkingGroupMintCapacityProposal(
+        memberId,
+        title,
+        description,
+        proposalStake,
+        mintCapacity,
+        workingGroup
+      ),
+      account,
+      false
+    )
+  }
+
+  private createAddOpeningTransaction(
+    actiavteAt: ActivateOpeningAt,
+    commitment: WorkingGroupOpeningPolicyCommitment,
+    text: string,
+    type: string,
+    module: WorkingGroups
+  ): SubmittableExtrinsic<'promise'> {
+    return this.api.tx[module].addOpening(actiavteAt, commitment, text, type)
+  }
+
+  public async acceptApplications(leader: KeyringPair, openingId: BN, module: WorkingGroups): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].acceptApplications(openingId), leader, false)
+  }
+
+  public async beginApplicantReview(leader: KeyringPair, openingId: BN, module: WorkingGroups): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].beginApplicantReview(openingId), leader, false)
+  }
+
+  public async sudoBeginApplicantReview(sudo: KeyringPair, openingId: BN, module: WorkingGroups): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(this.api.tx[module].beginApplicantReview(openingId)),
+      sudo,
+      false
+    )
+  }
+
+  public async applyOnOpening(
+    account: KeyringPair,
+    roleAccountAddress: string,
+    openingId: BN,
+    roleStake: BN,
+    applicantStake: BN,
+    text: string,
+    expectFailure: boolean,
+    module: WorkingGroups
+  ): Promise<void> {
+    const memberId: BN = (await this.getMemberIds(account.address))[0]
+    return this.sender.signAndSend(
+      this.api.tx[module].applyOnOpening(memberId, openingId, roleAccountAddress, roleStake, applicantStake, text),
+      account,
+      expectFailure
+    )
+  }
+
+  public async batchApplyOnOpening(
+    accounts: KeyringPair[],
+    openingId: BN,
+    roleStake: BN,
+    applicantStake: BN,
+    text: string,
+    module: WorkingGroups,
+    expectFailure: boolean
+  ): Promise<void[]> {
+    return Promise.all(
+      accounts.map(async (keyPair) => {
+        await this.applyOnOpening(
+          keyPair,
+          keyPair.address,
+          openingId,
+          roleStake,
+          applicantStake,
+          text,
+          expectFailure,
+          module
+        )
+      })
+    )
+  }
+
+  public async fillOpening(
+    leader: KeyringPair,
+    openingId: BN,
+    applicationId: BN[],
+    amountPerPayout: BN,
+    nextPaymentBlock: BN,
+    payoutInterval: BN,
+    module: WorkingGroups
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx[module].fillOpening(openingId, applicationId, {
+        'amount_per_payout': amountPerPayout,
+        'next_payment_at_block': nextPaymentBlock,
+        'payout_interval': payoutInterval,
+      }),
+      leader,
+      false
+    )
+  }
+
+  public async sudoFillOpening(
+    sudo: KeyringPair,
+    openingId: BN,
+    applicationId: BN[],
+    amountPerPayout: BN,
+    nextPaymentBlock: BN,
+    payoutInterval: BN,
+    module: WorkingGroups
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(
+        this.api.tx[module].fillOpening(openingId, applicationId, {
+          'amount_per_payout': amountPerPayout,
+          'next_payment_at_block': nextPaymentBlock,
+          'payout_interval': payoutInterval,
+        })
+      ),
+      sudo,
+      false
+    )
+  }
+
+  public async increaseStake(worker: KeyringPair, workerId: BN, stake: BN, module: WorkingGroups): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].increaseStake(workerId, stake), worker, false)
+  }
+
+  public async decreaseStake(
+    leader: KeyringPair,
+    workerId: BN,
+    stake: BN,
+    module: WorkingGroups,
+    expectFailure: boolean
+  ): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].decreaseStake(workerId, stake), leader, expectFailure)
+  }
+
+  public async slashStake(
+    leader: KeyringPair,
+    workerId: BN,
+    stake: BN,
+    module: WorkingGroups,
+    expectFailure: boolean
+  ): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].slashStake(workerId, stake), leader, expectFailure)
+  }
+
+  public async updateRoleAccount(
+    worker: KeyringPair,
+    workerId: BN,
+    newRoleAccount: string,
+    module: WorkingGroups
+  ): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].updateRoleAccount(workerId, newRoleAccount), worker, false)
+  }
+
+  public async updateRewardAccount(
+    worker: KeyringPair,
+    workerId: BN,
+    newRewardAccount: string,
+    module: WorkingGroups
+  ): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].updateRewardAccount(workerId, newRewardAccount), worker, false)
+  }
+
+  public async withdrawApplication(account: KeyringPair, workerId: BN, module: WorkingGroups): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].withdrawApplication(workerId), account, false)
+  }
+
+  public async batchWithdrawApplication(accounts: KeyringPair[], module: WorkingGroups): Promise<void[]> {
+    return Promise.all(
+      accounts.map(async (keyPair) => {
+        const applicationIds: BN[] = await this.getApplicationsIdsByRoleAccount(keyPair.address, module)
+        await this.withdrawApplication(keyPair, applicationIds[0], module)
+      })
+    )
+  }
+
+  public async terminateApplication(leader: KeyringPair, applicationId: BN, module: WorkingGroups): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].terminateApplication(applicationId), leader, false)
+  }
+
+  public async batchTerminateApplication(
+    leader: KeyringPair,
+    roleAccounts: KeyringPair[],
+    module: WorkingGroups
+  ): Promise<void[]> {
+    return Promise.all(
+      roleAccounts.map(async (keyPair) => {
+        const applicationIds: BN[] = await this.getActiveApplicationsIdsByRoleAccount(keyPair.address, module)
+        await this.terminateApplication(leader, applicationIds[0], module)
+      })
+    )
+  }
+
+  public async terminateRole(
+    leader: KeyringPair,
+    applicationId: BN,
+    text: string,
+    module: WorkingGroups,
+    expectFailure: boolean
+  ): Promise<void> {
+    return this.sender.signAndSend(this.api.tx[module].terminateRole(applicationId, text, false), leader, expectFailure)
+  }
+
+  public async leaveRole(
+    account: KeyringPair,
+    text: string,
+    expectFailure: boolean,
+    module: WorkingGroups
+  ): Promise<void> {
+    const workerId: BN = await this.getWorkerIdByRoleAccount(account.address, module)
+    return this.sender.signAndSend(this.api.tx[module].leaveRole(workerId, text), account, expectFailure)
+  }
+
+  public async batchLeaveRole(
+    roleAccounts: KeyringPair[],
+    text: string,
+    expectFailure: boolean,
+    module: WorkingGroups
+  ): Promise<void[]> {
+    return Promise.all(
+      roleAccounts.map(async (keyPair) => {
+        await this.leaveRole(keyPair, text, expectFailure, module)
+      })
+    )
+  }
+
+  public async getStorageRoleParameters(): Promise<RoleParameters> {
+    return (await this.api.query.actors.parameters<Option<RoleParameters>>('StorageProvider')).unwrap()
+  }
+
+  public async getAnnouncingPeriod(): Promise<BN> {
+    return this.api.query.councilElection.announcingPeriod<BlockNumber>()
+  }
+
+  public async getVotingPeriod(): Promise<BN> {
+    return this.api.query.councilElection.votingPeriod<BlockNumber>()
+  }
+
+  public async getRevealingPeriod(): Promise<BN> {
+    return this.api.query.councilElection.revealingPeriod<BlockNumber>()
+  }
+
+  public async getCouncilSize(): Promise<BN> {
+    return this.api.query.councilElection.councilSize<u32>()
+  }
+
+  public async getCandidacyLimit(): Promise<BN> {
+    return this.api.query.councilElection.candidacyLimit<u32>()
+  }
+
+  public async getNewTermDuration(): Promise<BN> {
+    return this.api.query.councilElection.newTermDuration<BlockNumber>()
+  }
+
+  public async getMinCouncilStake(): Promise<BN> {
+    return this.api.query.councilElection.minCouncilStake<BalanceOf>()
+  }
+
+  public async getMinVotingStake(): Promise<BN> {
+    return this.api.query.councilElection.minVotingStake<BalanceOf>()
+  }
+
+  public async getNextOpeningId(module: WorkingGroups): Promise<BN> {
+    return this.api.query[module].nextOpeningId<u32>()
+  }
+
+  public async getNextApplicationId(module: WorkingGroups): Promise<BN> {
+    return this.api.query[module].nextApplicationId<u32>()
+  }
+
+  public async getOpening(id: BN, module: WorkingGroups): Promise<Opening> {
+    return ((await this.api.query[module].openingById<Codec[]>(id))[0] as unknown) as Opening
+  }
+
+  public async getHiringOpening(id: BN): Promise<HiringOpening> {
+    return ((await this.api.query.hiring.openingById<Codec[]>(id))[0] as unknown) as HiringOpening
+  }
+
+  public async getWorkers(module: WorkingGroups): Promise<Worker[]> {
+    return ((await this.api.query[module].workerById<Codec[]>())[1] as unknown) as Worker[]
+  }
+
+  public async getWorkerById(id: BN, module: WorkingGroups): Promise<Worker> {
+    return ((await this.api.query[module].workerById<Codec[]>(id))[0] as unknown) as Worker
+  }
+
+  public async getWorkerIdByRoleAccount(address: string, module: WorkingGroups): Promise<BN> {
+    const workersAndIds = await this.api.query[module].workerById<Codec[]>()
+    const workers: Worker[] = (workersAndIds[1] as unknown) as Worker[]
+    const ids: WorkerId[] = (workersAndIds[0] as unknown) as WorkerId[]
+    const index: number = workers.findIndex((worker) => worker.role_account_id.toString() === address)
+    return ids[index]
+  }
+
+  public async getApplicationsIdsByRoleAccount(address: string, module: WorkingGroups): Promise<BN[]> {
+    const applicationsAndIds = await this.api.query[module].applicationById<Codec[]>()
+    const applications: Application[] = (applicationsAndIds[1] as unknown) as Application[]
+    const ids: ApplicationId[] = (applicationsAndIds[0] as unknown) as ApplicationId[]
+    return applications
+      .map((application, index) => (application.role_account_id.toString() === address ? ids[index] : undefined))
+      .filter((id) => id !== undefined) as BN[]
+  }
+
+  public async getHiringApplicationById(id: BN): Promise<HiringApplication> {
+    return ((await this.api.query.hiring.applicationById<Codec[]>(id))[0] as unknown) as HiringApplication
+  }
+
+  public async getApplicationById(id: BN, module: WorkingGroups): Promise<Application> {
+    return ((await this.api.query[module].applicationById<Codec[]>(id))[0] as unknown) as Application
+  }
+
+  public async getActiveApplicationsIdsByRoleAccount(address: string, module: WorkingGroups): Promise<BN[]> {
+    const applicationsAndIds = await this.api.query[module].applicationById<Codec[]>()
+    const applications: Application[] = (applicationsAndIds[1] as unknown) as Application[]
+    const ids: ApplicationId[] = (applicationsAndIds[0] as unknown) as ApplicationId[]
+    return (
+      await Promise.all(
+        applications.map(async (application, index) => {
+          if (
+            application.role_account_id.toString() === address &&
+            (await this.getHiringApplicationById(application.application_id)).stage.type === 'Active'
+          ) {
+            return ids[index]
+          } else {
+            return undefined
+          }
+        })
+      )
+    ).filter((index) => index !== undefined) as BN[]
+  }
+
+  public async getStake(id: BN): Promise<Stake> {
+    return ((await this.api.query.stake.stakes<Codec[]>(id))[0] as unknown) as Stake
+  }
+
+  public async getWorkerStakeAmount(workerId: BN, module: WorkingGroups): Promise<BN> {
+    const stakeId: BN = (await this.getWorkerById(workerId, module)).role_stake_profile.unwrap().stake_id
+    return (((await this.getStake(stakeId)).staking_status.value as unknown) as StakedState).staked_amount
+  }
+
+  public async getRewardRelationship(id: BN): Promise<RewardRelationship> {
+    return ((
+      await this.api.query.recurringRewards.rewardRelationships<Codec[]>(id)
+    )[0] as unknown) as RewardRelationship
+  }
+
+  public async getWorkerRewardAccount(workerId: BN, module: WorkingGroups): Promise<string> {
+    const rewardRelationshipId: BN = (await this.getWorkerById(workerId, module)).reward_relationship.unwrap()
+    return (await this.getRewardRelationship(rewardRelationshipId)).getField('account').toString()
+  }
+
+  public async getLeadWorkerId(module: WorkingGroups): Promise<BN | undefined> {
+    return (await this.api.query[module].currentLead<Option<WorkerId>>()).unwrapOr(undefined)
+  }
+}

+ 8 - 0
tests/network-tests/src/iznik/utils/closeApi.ts

@@ -0,0 +1,8 @@
+import { ApiWrapper } from './apiWrapper'
+import tap from 'tap'
+
+export function closeApi(apiWrapper: ApiWrapper) {
+  tap.teardown(() => {
+    apiWrapper.close()
+  })
+}

+ 5 - 0
tests/network-tests/src/iznik/utils/config.ts

@@ -0,0 +1,5 @@
+import { config } from 'dotenv'
+
+export function initConfig() {
+  config()
+}

+ 66 - 0
tests/network-tests/src/iznik/utils/sender.ts

@@ -0,0 +1,66 @@
+import BN from 'bn.js'
+import { ApiPromise } from '@polkadot/api'
+import { Index } from '@polkadot/types/interfaces'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { KeyringPair } from '@polkadot/keyring/types'
+
+export class Sender {
+  private readonly api: ApiPromise
+  private static nonceMap: Map<string, BN> = new Map()
+
+  constructor(api: ApiPromise) {
+    this.api = api
+  }
+
+  private async getNonce(address: string): Promise<BN> {
+    let oncahinNonce: BN = new BN(0)
+    if (!Sender.nonceMap.get(address)) {
+      oncahinNonce = await this.api.query.system.accountNonce<Index>(address)
+    }
+    let nonce: BN | undefined = Sender.nonceMap.get(address)
+    if (!nonce) {
+      nonce = oncahinNonce
+    }
+    const nextNonce: BN = nonce.addn(1)
+    Sender.nonceMap.set(address, nextNonce)
+    return nonce
+  }
+
+  private clearNonce(address: string): void {
+    Sender.nonceMap.delete(address)
+  }
+
+  public async signAndSend(
+    tx: SubmittableExtrinsic<'promise'>,
+    account: KeyringPair,
+    expectFailure = false
+  ): Promise<void> {
+    return new Promise(async (resolve, reject) => {
+      const nonce: BN = await this.getNonce(account.address)
+      const signedTx = tx.sign(account, { nonce })
+      await signedTx
+        .send(async (result) => {
+          if (result.status.isFinalized === true && result.events !== undefined) {
+            result.events.forEach((event) => {
+              if (event.event.method === 'ExtrinsicFailed') {
+                if (expectFailure) {
+                  resolve()
+                } else {
+                  reject(new Error('Extrinsic failed unexpectedly'))
+                }
+              }
+            })
+            resolve()
+          }
+          if (result.status.isFuture) {
+            console.log('nonce ' + nonce + ' for account ' + account.address + ' is in future')
+            this.clearNonce(account.address)
+            reject(new Error('Extrinsic nonce is in future'))
+          }
+        })
+        .catch((error) => {
+          reject(error)
+        })
+    })
+  }
+}

+ 7 - 0
tests/network-tests/src/iznik/utils/setTestTimeout.ts

@@ -0,0 +1,7 @@
+import tap from 'tap'
+import { ApiWrapper } from './apiWrapper'
+
+export function setTestTimeout(apiWrapper: ApiWrapper, durationInBlocks: number) {
+  const durationInMillis = apiWrapper.getBlockDuration().muln(durationInBlocks).toNumber()
+  tap.setTimeout(durationInMillis)
+}

+ 63 - 0
tests/network-tests/src/iznik/utils/utils.ts

@@ -0,0 +1,63 @@
+import { IExtrinsic } from '@polkadot/types/types'
+import { compactToU8a, stringToU8a } from '@polkadot/util'
+import { blake2AsHex } from '@polkadot/util-crypto'
+import BN from 'bn.js'
+import fs from 'fs'
+import Keyring, { decodeAddress } from '@polkadot/keyring'
+import { Seat } from '@nicaea/types/council'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { v4 as uuid } from 'uuid'
+
+export class Utils {
+  private static LENGTH_ADDRESS = 32 + 1 // publicKey + prefix
+  private static LENGTH_ERA = 2 // assuming mortals
+  private static LENGTH_SIGNATURE = 64 // assuming ed25519 or sr25519
+  private static LENGTH_VERSION = 1 // 0x80 & version
+
+  public static calcTxLength = (extrinsic?: IExtrinsic | null, nonce?: BN): BN => {
+    return new BN(
+      Utils.LENGTH_VERSION +
+        Utils.LENGTH_ADDRESS +
+        Utils.LENGTH_SIGNATURE +
+        Utils.LENGTH_ERA +
+        compactToU8a(nonce || 0).length +
+        (extrinsic ? extrinsic.encodedLength : 0)
+    )
+  }
+
+  /** hash(accountId + salt) */
+  public static hashVote(accountId: string, salt: string): string {
+    const accountU8a = decodeAddress(accountId)
+    const saltU8a = stringToU8a(salt)
+    const voteU8a = new Uint8Array(accountU8a.length + saltU8a.length)
+    voteU8a.set(accountU8a)
+    voteU8a.set(saltU8a, accountU8a.length)
+
+    const hash = blake2AsHex(voteU8a, 256)
+    return hash
+  }
+
+  public static wait(ms: number): Promise<void> {
+    return new Promise((resolve) => setTimeout(resolve, ms))
+  }
+
+  public static getTotalStake(seat: Seat): BN {
+    return new BN(+seat.stake.toString() + seat.backers.reduce((a, baker) => a + +baker.stake.toString(), 0))
+  }
+
+  public static readRuntimeFromFile(path: string): string {
+    return '0x' + fs.readFileSync(path).toString('hex')
+  }
+
+  public static camelToSnakeCase(key: string): string {
+    return key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
+  }
+
+  public static createKeyPairs(keyring: Keyring, n: number): KeyringPair[] {
+    const nKeyPairs: KeyringPair[] = []
+    for (let i = 0; i < n; i++) {
+      nKeyPairs.push(keyring.addFromUri(i + uuid().substring(0, 8)))
+    }
+    return nKeyPairs
+  }
+}

+ 1 - 1
types/src/working-group/index.ts

@@ -185,7 +185,7 @@ export type IWorkingGroupOpeningPolicyCommitment = {
 // very fragile atm and any change to this type in working-group module could result in "unsolvable" inconsistencies
 // (this won't be an issue after CWG gets refactored to use the working-grpup module too)
 export class WorkingGroupOpeningPolicyCommitment extends JoyStruct<IWorkingGroupOpeningPolicyCommitment> {
-  constructor(value?: WorkingGroupOpeningPolicyCommitment) {
+  constructor(value?: IWorkingGroupOpeningPolicyCommitment) {
     super(
       {
         application_rationing_policy: Option.with(ApplicationRationingPolicy),