Просмотр исходного кода

working group happy case implementation started

Gleb Urvanov 4 лет назад
Родитель
Сommit
6755053de1
34 измененных файлов с 2180 добавлено и 1 удалено
  1. 2 0
      tests/network-tests/.env
  2. 3 1
      tests/network-tests/package.json
  3. 0 0
      tests/network-tests/src/nicaea/tap-parallel-not-ok
  4. 25 0
      tests/network-tests/src/nicaea/tests/bureaucracy/impl/workerApplicationHappyCase.ts
  5. 33 0
      tests/network-tests/src/nicaea/tests/bureaucracy/workerApplicationHappyCaseTest.ts
  6. 38 0
      tests/network-tests/src/nicaea/tests/electingCouncilTest.ts
  7. 8 0
      tests/network-tests/src/nicaea/tests/impl/closeApi.ts
  8. 107 0
      tests/network-tests/src/nicaea/tests/impl/electingCouncil.ts
  9. 78 0
      tests/network-tests/src/nicaea/tests/impl/membershipCreation.ts
  10. 29 0
      tests/network-tests/src/nicaea/tests/membershipCreationTest.ts
  11. 40 0
      tests/network-tests/src/nicaea/tests/proposals/electionParametersProposalTest.ts
  12. 40 0
      tests/network-tests/src/nicaea/tests/proposals/evictStorageProviderTest.ts
  13. 124 0
      tests/network-tests/src/nicaea/tests/proposals/impl/electionParametersProposal.ts
  14. 62 0
      tests/network-tests/src/nicaea/tests/proposals/impl/evictStoraveProvider.ts
  15. 46 0
      tests/network-tests/src/nicaea/tests/proposals/impl/setLeadProposal.ts
  16. 64 0
      tests/network-tests/src/nicaea/tests/proposals/impl/spendingProposal.ts
  17. 120 0
      tests/network-tests/src/nicaea/tests/proposals/impl/storageRoleParametersProposal.ts
  18. 46 0
      tests/network-tests/src/nicaea/tests/proposals/impl/textProposal.ts
  19. 52 0
      tests/network-tests/src/nicaea/tests/proposals/impl/updateRuntime.ts
  20. 55 0
      tests/network-tests/src/nicaea/tests/proposals/impl/validatorCountProposal.ts
  21. 59 0
      tests/network-tests/src/nicaea/tests/proposals/impl/workingGroupMintCapacityProposal.ts
  22. 40 0
      tests/network-tests/src/nicaea/tests/proposals/setLeadProposalTest.ts
  23. 42 0
      tests/network-tests/src/nicaea/tests/proposals/spendingProposalTest.ts
  24. 40 0
      tests/network-tests/src/nicaea/tests/proposals/storageRoleParametersProposalTest.ts
  25. 40 0
      tests/network-tests/src/nicaea/tests/proposals/textProposalTest.ts
  26. 41 0
      tests/network-tests/src/nicaea/tests/proposals/updateRuntimeTest.ts
  27. 41 0
      tests/network-tests/src/nicaea/tests/proposals/validatorCountProposalTest.ts
  28. 41 0
      tests/network-tests/src/nicaea/tests/proposals/workingGroupMintCapacityProposalTest.ts
  29. 728 0
      tests/network-tests/src/nicaea/utils/apiWrapper.ts
  30. 5 0
      tests/network-tests/src/nicaea/utils/config.ts
  31. 66 0
      tests/network-tests/src/nicaea/utils/sender.ts
  32. 7 0
      tests/network-tests/src/nicaea/utils/setTestTimeout.ts
  33. 50 0
      tests/network-tests/src/nicaea/utils/utils.ts
  34. 8 0
      yarn.lock

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

@@ -24,3 +24,5 @@ RUNTIME_UPGRADE_PROPOSAL_STAKE = 100000
 VALIDATOR_COUNT_INCREMENT = 2
 # Constantinople runtime path
 RUNTIME_WASM_PATH = ../../target/release/wbuild/joystream-node-runtime/joystream_node_runtime.compact.wasm
+# Working group size N
+WORKING_GROUP_N = 2

+ 3 - 1
tests/network-tests/package.json

@@ -6,14 +6,16 @@
     "build": "tsc --build tsconfig.json",
     "test": "tap --files ts-node/register src/constantinople/tests/proposals/*Test.ts",
     "test-migration": "tap --files src/rome/tests/romeRuntimeUpgradeTest.ts --files src/constantinople/tests/electingCouncilTest.ts",
+    "debug": "tap --files src/nicaea/tests/bureaucracy/workerApplicationHappyCaseTest.ts -T",
     "lint": "tslint --project tsconfig.json",
     "prettier": "prettier --write ./src"
   },
   "dependencies": {
+    "@rome/types@npm:@joystream/types": "^0.7.0",
     "@constantinople/types@npm:@joystream/types": "^0.10.0",
+    "@nicaea/types": "./types",
     "@polkadot/api": "^0.96.1",
     "@polkadot/keyring": "^1.7.0-beta.5",
-    "@rome/types@npm:@joystream/types": "^0.7.0",
     "@types/bn.js": "^4.11.5",
     "bn.js": "^4.11.8",
     "dotenv": "^8.2.0",

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


+ 25 - 0
tests/network-tests/src/nicaea/tests/bureaucracy/impl/workerApplicationHappyCase.ts

@@ -0,0 +1,25 @@
+import tap from 'tap';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { Keyring } from '@polkadot/api';
+import BN from 'bn.js';
+
+export function workerApplicationHappyCase(
+  apiWrapper: ApiWrapper,
+  membersKeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Set lead test', async () => {
+    sudo = keyring.addFromUri(sudoUri);
+    await apiWrapper.sudoSetLead(sudo, membersKeyPairs[0]);
+  });
+
+  tap.test('Add worker opening', async () => {
+    const addWorkerOpeningFee: BN = apiWrapper.estimateAddWorkerOpeningFee();
+    apiWrapper.transferBalance(sudo, membersKeyPairs[0].address, addWorkerOpeningFee.muln(membersKeyPairs.length));
+    apiWrapper.addWorkerOpening(membersKeyPairs[0], 'CurrentBlock', 'some text');
+  });
+}

+ 33 - 0
tests/network-tests/src/nicaea/tests/bureaucracy/workerApplicationHappyCaseTest.ts

@@ -0,0 +1,33 @@
+import tap from 'tap';
+import { initConfig } from '../../utils/config';
+import { registerJoystreamTypes } from '@nicaea/types';
+import { closeApi } from '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+import { WsProvider, Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { setTestTimeout } from '../../utils/setTestTimeout';
+import { membershipTest } from '../impl/membershipCreation';
+import { workerApplicationHappyCase } from './impl/workerApplicationHappyCase';
+
+tap.mocha.describe('Worker application happy case scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const nKeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.WORKING_GROUP_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  const durationInBlocks: number = 25;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, nKeyPairs, keyring, N, paidTerms, sudoUri);
+  workerApplicationHappyCase(apiWrapper, nKeyPairs, keyring, sudoUri);
+
+  closeApi(apiWrapper);
+});

+ 38 - 0
tests/network-tests/src/nicaea/tests/electingCouncilTest.ts

@@ -0,0 +1,38 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from './impl/membershipCreation';
+import { councilTest } from './impl/electingCouncil';
+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 './impl/closeApi';
+
+tap.mocha.describe('Electing council scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 25;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  closeApi(apiWrapper);
+});

+ 8 - 0
tests/network-tests/src/nicaea/tests/impl/closeApi.ts

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

+ 107 - 0
tests/network-tests/src/nicaea/tests/impl/electingCouncil.ts

@@ -0,0 +1,107 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../utils/apiWrapper';
+import { Keyring } from '@polkadot/api';
+import BN from 'bn.js';
+import { Seat } from '@nicaea/types';
+import { assert } from 'chai';
+import { v4 as uuid } from 'uuid';
+import { Utils } from '../../utils/utils';
+import tap from 'tap';
+
+export function councilTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  K: number,
+  sudoUri: string,
+  greaterStake: BN,
+  lesserStake: BN
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Electing a council test', async () => {
+    // Setup goes here because M keypairs are generated after before() function
+    sudo = keyring.addFromUri(sudoUri);
+    let now = await apiWrapper.getBestBlock();
+    const applyForCouncilFee: BN = apiWrapper.estimateApplyForCouncilFee(greaterStake);
+    const voteForCouncilFee: BN = apiWrapper.estimateVoteForCouncilFee(sudo.address, sudo.address, greaterStake);
+    const salt: string[] = new Array();
+    m1KeyPairs.forEach(() => {
+      salt.push(''.concat(uuid().replace(/-/g, '')));
+    });
+    const revealVoteFee: BN = apiWrapper.estimateRevealVoteFee(sudo.address, salt[0]);
+
+    // Topping the balances
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, applyForCouncilFee.add(greaterStake));
+    await apiWrapper.transferBalanceToAccounts(
+      sudo,
+      m1KeyPairs,
+      voteForCouncilFee.add(revealVoteFee).add(greaterStake)
+    );
+
+    // First K members stake more
+    await apiWrapper.sudoStartAnnouncingPerion(sudo, now.addn(100));
+    await apiWrapper.batchApplyForCouncilElection(m2KeyPairs.slice(0, K), greaterStake);
+    m2KeyPairs.slice(0, K).forEach(keyPair =>
+      apiWrapper.getCouncilElectionStake(keyPair.address).then(stake => {
+        assert(
+          stake.eq(greaterStake),
+          `${keyPair.address} not applied correctrly for council election with stake ${stake} versus expected ${greaterStake}`
+        );
+      })
+    );
+
+    // Last members stake less
+    await apiWrapper.batchApplyForCouncilElection(m2KeyPairs.slice(K), lesserStake);
+    m2KeyPairs.slice(K).forEach(keyPair =>
+      apiWrapper.getCouncilElectionStake(keyPair.address).then(stake => {
+        assert(
+          stake.eq(lesserStake),
+          `${keyPair.address} not applied correctrly for council election with stake ${stake} versus expected ${lesserStake}`
+        );
+      })
+    );
+
+    // Voting
+    await apiWrapper.sudoStartVotingPerion(sudo, now.addn(100));
+    await apiWrapper.batchVoteForCouncilMember(
+      m1KeyPairs.slice(0, K),
+      m2KeyPairs.slice(0, K),
+      salt.slice(0, K),
+      lesserStake
+    );
+    await apiWrapper.batchVoteForCouncilMember(m1KeyPairs.slice(K), m2KeyPairs.slice(K), salt.slice(K), greaterStake);
+
+    // Revealing
+    await apiWrapper.sudoStartRevealingPerion(sudo, now.addn(100));
+    await apiWrapper.batchRevealVote(m1KeyPairs.slice(0, K), m2KeyPairs.slice(0, K), salt.slice(0, K));
+    await apiWrapper.batchRevealVote(m1KeyPairs.slice(K), m2KeyPairs.slice(K), salt.slice(K));
+    now = await apiWrapper.getBestBlock();
+
+    // Resolving election
+    // 3 is to ensure the revealing block is in future
+    await apiWrapper.sudoStartRevealingPerion(sudo, now.addn(3));
+    await Utils.wait(apiWrapper.getBlockDuration().muln(2.5).toNumber());
+    const seats: Seat[] = await apiWrapper.getCouncil();
+
+    // Preparing collections to increase assertion readability
+    const m2addresses: string[] = m2KeyPairs.map(keyPair => keyPair.address);
+    const m1addresses: string[] = m1KeyPairs.map(keyPair => keyPair.address);
+    const members: string[] = seats.map(seat => seat.member.toString());
+    const bakers: string[] = seats.reduce(
+      (array, seat) => array.concat(seat.backers.map(baker => baker.member.toString())),
+      new Array()
+    );
+
+    // 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(greaterStake.add(lesserStake)),
+        `Member ${seat.member} has unexpected stake ${Utils.getTotalStake(seat)}`
+      )
+    );
+  });
+}

+ 78 - 0
tests/network-tests/src/nicaea/tests/impl/membershipCreation.ts

@@ -0,0 +1,78 @@
+import { Keyring } from '@polkadot/keyring';
+import { assert } from 'chai';
+import { KeyringPair } from '@polkadot/keyring/types';
+import BN from 'bn.js';
+import { ApiWrapper } from '../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import tap from 'tap';
+
+export function membershipTest(
+  apiWrapper: ApiWrapper,
+  nKeyPairs: KeyringPair[],
+  keyring: Keyring,
+  N: number,
+  paidTerms: number,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+  let aKeyPair: KeyringPair;
+  let membershipFee: BN;
+  let membershipTransactionFee: BN;
+
+  tap.test('Membership creation test setup', async () => {
+    sudo = keyring.addFromUri(sudoUri);
+    for (let i = 0; i < N; i++) {
+      nKeyPairs.push(keyring.addFromUri(i + uuid().substring(0, 8)));
+    }
+    aKeyPair = keyring.addFromUri(uuid().substring(0, 8));
+    membershipFee = await apiWrapper.getMembershipFee(paidTerms);
+    membershipTransactionFee = apiWrapper.estimateBuyMembershipFee(
+      sudo,
+      paidTerms,
+      'member_name_which_is_longer_than_expected'
+    );
+    await apiWrapper.transferBalanceToAccounts(sudo, nKeyPairs, membershipTransactionFee.add(new BN(membershipFee)));
+    await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipTransactionFee);
+  });
+
+  tap.test('Buy membeship is accepted with sufficient funds', async () => {
+    await Promise.all(
+      nKeyPairs.map(async (keyPair, index) => {
+        await apiWrapper.buyMembership(keyPair, paidTerms, `new_member_${index}${keyPair.address.substring(0, 8)}`);
+      })
+    );
+    nKeyPairs.forEach((keyPair, index) =>
+      apiWrapper
+        .getMemberIds(keyPair.address)
+        .then(membership => assert(membership.length > 0, `Account ${keyPair.address} is not a member`))
+    );
+  });
+
+  tap.test('Account A can not buy the membership with insufficient funds', async () => {
+    await apiWrapper
+      .getBalance(aKeyPair.address)
+      .then(balance =>
+        assert(
+          balance.toBn() < membershipFee.add(membershipTransactionFee),
+          'Account A already have sufficient balance to purchase membership'
+        )
+      );
+    await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`, true);
+    apiWrapper
+      .getMemberIds(aKeyPair.address)
+      .then(membership => assert(membership.length === 0, 'Account A is a member'));
+  });
+
+  tap.test('Account A was able to buy the membership with sufficient funds', async () => {
+    await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipFee.add(membershipTransactionFee));
+    apiWrapper
+      .getBalance(aKeyPair.address)
+      .then(balance =>
+        assert(balance.toBn() >= membershipFee, 'The account balance is insufficient to purchase membership')
+      );
+    await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`);
+    apiWrapper
+      .getMemberIds(aKeyPair.address)
+      .then(membership => assert(membership.length > 0, 'Account A is a not member'));
+  });
+}

+ 29 - 0
tests/network-tests/src/nicaea/tests/membershipCreationTest.ts

@@ -0,0 +1,29 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from './impl/membershipCreation';
+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 './impl/closeApi';
+
+tap.mocha.describe('Membership creation scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const nKeyPairs: KeyringPair[] = new Array();
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  const durationInBlocks: number = 7;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, nKeyPairs, keyring, N, paidTerms, sudoUri);
+  closeApi(apiWrapper);
+});

+ 40 - 0
tests/network-tests/src/nicaea/tests/proposals/electionParametersProposalTest.ts

@@ -0,0 +1,40 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { electionParametersProposalTest } from './impl/electionParametersProposal';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Election parameters proposal scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 29;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  electionParametersProposalTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri);
+  closeApi(apiWrapper);
+});

+ 40 - 0
tests/network-tests/src/nicaea/tests/proposals/evictStorageProviderTest.ts

@@ -0,0 +1,40 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { evictStorageProviderTest } from './impl/evictStoraveProvider';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Evict Storage provider scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 32;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  evictStorageProviderTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri);
+  closeApi(apiWrapper);
+});

+ 124 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/electionParametersProposal.ts

@@ -0,0 +1,124 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import { assert } from 'chai';
+import tap from 'tap';
+
+export function electionParametersProposalTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Election parameters proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8);
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8);
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+    const announcingPeriod: BN = await apiWrapper.getAnnouncingPeriod();
+    const votingPeriod: BN = await apiWrapper.getVotingPeriod();
+    const revealingPeriod: BN = await apiWrapper.getRevealingPeriod();
+    const councilSize: BN = await apiWrapper.getCouncilSize();
+    const candidacyLimit: BN = await apiWrapper.getCandidacyLimit();
+    const newTermDuration: BN = await apiWrapper.getNewTermDuration();
+    const minCouncilStake: BN = await apiWrapper.getMinCouncilStake();
+    const minVotingStake: BN = await apiWrapper.getMinVotingStake();
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(200000);
+    const proposalFee: BN = apiWrapper.estimateProposeElectionParametersFee(
+      description,
+      description,
+      proposalStake,
+      announcingPeriod,
+      votingPeriod,
+      revealingPeriod,
+      councilSize,
+      candidacyLimit,
+      newTermDuration,
+      minCouncilStake,
+      minVotingStake
+    );
+    await apiWrapper.transferBalance(sudo, 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 = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeElectionParameters(
+      m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      proposedAnnouncingPeriod,
+      proposedVotingPeriod,
+      proposedRevealingPeriod,
+      proposedCouncilSize,
+      proposedCandidacyLimit,
+      proposedNewTermDuration,
+      proposedMinCouncilStake,
+      proposedMinVotingStake
+    );
+    const proposalNumber = await proposalPromise;
+
+    // Approving the proposal
+    const proposalExecutionPromise = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await proposalExecutionPromise;
+
+    // Assertions
+    const newAnnouncingPeriod: BN = await apiWrapper.getAnnouncingPeriod();
+    const newVotingPeriod: BN = await apiWrapper.getVotingPeriod();
+    const newRevealingPeriod: BN = await apiWrapper.getRevealingPeriod();
+    const newCouncilSize: BN = await apiWrapper.getCouncilSize();
+    const newCandidacyLimit: BN = await apiWrapper.getCandidacyLimit();
+    const newNewTermDuration: BN = await apiWrapper.getNewTermDuration();
+    const newMinCouncilStake: BN = await apiWrapper.getMinCouncilStake();
+    const newMinVotingStake: BN = await 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}`
+    );
+  });
+}

+ 62 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/evictStoraveProvider.ts

@@ -0,0 +1,62 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import { assert } from 'chai';
+import { Utils } from '../../../utils/utils';
+import tap from 'tap';
+
+export function evictStorageProviderTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Evict storage provider proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8);
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8);
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+    if (!(await apiWrapper.isStorageProvider(sudo.address))) {
+      await apiWrapper.createStorageProvider(sudo);
+    }
+    assert(await apiWrapper.isStorageProvider(sudo.address), `Account ${sudo.address} is not storage provider`);
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(25000);
+    const proposalFee: BN = apiWrapper.estimateProposeEvictStorageProviderFee(
+      description,
+      description,
+      proposalStake,
+      sudo.address
+    );
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, proposalFee.add(proposalStake));
+
+    // Proposal creation
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeEvictStorageProvider(
+      m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      sudo.address
+    );
+    const proposalNumber = await proposalPromise;
+
+    // Approving the proposal
+    const proposalExecutionPromise = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await proposalExecutionPromise;
+    await Utils.wait(apiWrapper.getBlockDuration().toNumber());
+    assert(
+      !(await apiWrapper.isStorageProvider(sudo.address)),
+      `Account ${sudo.address} is storage provider after eviction`
+    );
+  });
+}

+ 46 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/setLeadProposal.ts

@@ -0,0 +1,46 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import { assert } from 'chai';
+import tap from 'tap';
+
+export function setLeadProposalTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Lead proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8);
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8);
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(50000);
+    const proposalFee: BN = apiWrapper.estimateProposeLeadFee(description, description, proposalStake, sudo.address);
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, proposalFee.add(proposalStake));
+
+    // Proposal creation
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeLead(m1KeyPairs[0], proposalTitle, description, proposalStake, m1KeyPairs[1]);
+    const proposalNumber = await proposalPromise;
+
+    // Approving the proposal
+    const proposalExecutionPromise = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await proposalExecutionPromise;
+    const newLead: string = await apiWrapper.getCurrentLeadAddress();
+    assert(
+      newLead === m1KeyPairs[1].address,
+      `New lead has unexpected value ${newLead}, expected ${m1KeyPairs[1].address}`
+    );
+  });
+}

+ 64 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/spendingProposal.ts

@@ -0,0 +1,64 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import { assert } from 'chai';
+import tap from 'tap';
+
+export function spendingProposalTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string,
+  spendingBalance: BN,
+  mintCapacity: BN
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Spending proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const description: string = 'spending proposal which is used for API network testing with some mock data';
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+
+    // Topping the balances
+    const proposalStake: BN = new BN(25000);
+    const runtimeProposalFee: BN = apiWrapper.estimateProposeSpendingFee(
+      description,
+      description,
+      proposalStake,
+      spendingBalance,
+      sudo.address
+    );
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake));
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+    await apiWrapper.sudoSetCouncilMintCapacity(sudo, mintCapacity);
+
+    // Proposal creation
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeSpending(
+      m1KeyPairs[0],
+      'testing spending' + uuid().substring(0, 8),
+      'spending to test proposal functionality' + uuid().substring(0, 8),
+      proposalStake,
+      spendingBalance,
+      sudo.address
+    );
+    const proposalNumber = await proposalPromise;
+
+    // Approving spending proposal
+    const balanceBeforeMinting: BN = await apiWrapper.getBalance(sudo.address);
+    const spendingPromise = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await spendingPromise;
+    const balanceAfterMinting: BN = await apiWrapper.getBalance(sudo.address);
+    assert(
+      balanceAfterMinting.sub(balanceBeforeMinting).eq(spendingBalance),
+      `member ${
+        m1KeyPairs[0].address
+      } has unexpected balance ${balanceAfterMinting}, expected ${balanceBeforeMinting.add(spendingBalance)}`
+    );
+  });
+}

+ 120 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/storageRoleParametersProposal.ts

@@ -0,0 +1,120 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import { assert } from 'chai';
+import { RoleParameters } from '@nicaea/types/lib/roles';
+import tap from 'tap';
+
+export function storageRoleParametersProposalTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Storage role parameters proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8);
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8);
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+    const roleParameters: RoleParameters = ((await apiWrapper.getStorageRoleParameters()) as unknown) as RoleParameters;
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(100000);
+    const proposalFee: BN = apiWrapper.estimateProposeStorageRoleParametersFee(
+      description,
+      description,
+      proposalStake,
+      roleParameters.min_stake.toBn(),
+      roleParameters.min_actors.toBn(),
+      roleParameters.max_actors.toBn(),
+      roleParameters.reward.toBn(),
+      roleParameters.reward_period.toBn(),
+      roleParameters.bonding_period.toBn(),
+      roleParameters.unbonding_period.toBn(),
+      roleParameters.min_service_period.toBn(),
+      roleParameters.startup_grace_period.toBn(),
+      roleParameters.entry_request_fee.toBn()
+    );
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, proposalFee.add(proposalStake));
+
+    // Proposal creation
+    const proposedMinStake: BN = roleParameters.min_stake.toBn().addn(1);
+    const proposedMaxActors: BN = roleParameters.max_actors.toBn().addn(1);
+    const proposedReward: BN = roleParameters.reward.toBn().addn(1);
+    const proposedRewardPeriod: BN = roleParameters.reward_period.toBn().addn(1);
+    const proposedBondingPeriod: BN = roleParameters.bonding_period.toBn().addn(1);
+    const proposedUnbondingPeriod: BN = roleParameters.unbonding_period.toBn().addn(1);
+    const proposedMinServicePeriod: BN = roleParameters.min_service_period.toBn().addn(1);
+    const proposedStartupGracePeriod: BN = roleParameters.startup_grace_period.toBn().addn(1);
+    const proposedEntryRequestFee: BN = roleParameters.entry_request_fee.toBn().addn(1);
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeStorageRoleParameters(
+      m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      proposedMinStake,
+      roleParameters.min_actors.toBn(),
+      proposedMaxActors,
+      proposedReward,
+      proposedRewardPeriod,
+      proposedBondingPeriod,
+      proposedUnbondingPeriod,
+      proposedMinServicePeriod,
+      proposedStartupGracePeriod,
+      proposedEntryRequestFee
+    );
+    const proposalNumber = await proposalPromise;
+
+    // Approving the proposal
+    const proposalExecutionPromise = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await proposalExecutionPromise;
+
+    // Assertions
+    const newRoleParameters: RoleParameters = await apiWrapper.getStorageRoleParameters();
+    assert(
+      proposedMinStake.eq(newRoleParameters.min_stake.toBn()),
+      `Min stake has unexpected value ${newRoleParameters.min_stake.toBn()}, expected ${proposedMinStake}`
+    );
+    assert(
+      proposedMaxActors.eq(newRoleParameters.max_actors.toBn()),
+      `Max actors has unexpected value ${newRoleParameters.max_actors.toBn()}, expected ${proposedMaxActors}`
+    );
+    assert(
+      proposedReward.eq(newRoleParameters.reward.toBn()),
+      `Reward has unexpected value ${newRoleParameters.reward.toBn()}, expected ${proposedReward}`
+    );
+    assert(
+      proposedRewardPeriod.eq(newRoleParameters.reward_period.toBn()),
+      `Reward period has unexpected value ${newRoleParameters.reward_period.toBn()}, expected ${proposedRewardPeriod}`
+    );
+    assert(
+      proposedBondingPeriod.eq(newRoleParameters.bonding_period.toBn()),
+      `Bonding period has unexpected value ${newRoleParameters.bonding_period.toBn()}, expected ${proposedBondingPeriod}`
+    );
+    assert(
+      proposedUnbondingPeriod.eq(newRoleParameters.unbonding_period.toBn()),
+      `Unbonding period has unexpected value ${newRoleParameters.unbonding_period.toBn()}, expected ${proposedUnbondingPeriod}`
+    );
+    assert(
+      proposedMinServicePeriod.eq(newRoleParameters.min_service_period.toBn()),
+      `Min service period has unexpected value ${newRoleParameters.min_service_period.toBn()}, expected ${proposedMinServicePeriod}`
+    );
+    assert(
+      proposedStartupGracePeriod.eq(newRoleParameters.startup_grace_period.toBn()),
+      `Startup grace period has unexpected value ${newRoleParameters.startup_grace_period.toBn()}, expected ${proposedStartupGracePeriod}`
+    );
+    assert(
+      proposedEntryRequestFee.eq(newRoleParameters.entry_request_fee.toBn()),
+      `Entry request fee has unexpected value ${newRoleParameters.entry_request_fee.toBn()}, expected ${proposedEntryRequestFee}`
+    );
+  });
+}

+ 46 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/textProposal.ts

@@ -0,0 +1,46 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import tap from 'tap';
+
+export function textProposalTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Text proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    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 = apiWrapper.estimateVoteForProposalFee();
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(25000);
+    const runtimeProposalFee: BN = apiWrapper.estimateProposeTextFee(
+      proposalStake,
+      description,
+      description,
+      proposalText
+    );
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake));
+
+    // Proposal creation
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeText(m1KeyPairs[0], proposalStake, proposalTitle, description, proposalText);
+    const proposalNumber = await proposalPromise;
+
+    // Approving text proposal
+    const textProposalPromise = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await textProposalPromise;
+  });
+}

+ 52 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/updateRuntime.ts

@@ -0,0 +1,52 @@
+import { Keyring } from '@polkadot/api';
+import { Bytes } from '@polkadot/types';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import tap from 'tap';
+
+export function updateRuntimeTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string
+) {
+  let sudo: KeyringPair;
+
+  tap.test('\n\tUpgrading the runtime test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const runtime: Bytes = await apiWrapper.getRuntime();
+    const description: string = 'runtime upgrade proposal which is used for API network testing';
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+
+    // Topping the balances
+    const proposalStake: BN = new BN(1000000);
+    const runtimeProposalFee: BN = apiWrapper.estimateProposeRuntimeUpgradeFee(
+      proposalStake,
+      description,
+      description,
+      runtime
+    );
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake));
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+
+    // Proposal creation
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeRuntime(
+      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 = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await runtimePromise;
+  });
+}

+ 55 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/validatorCountProposal.ts

@@ -0,0 +1,55 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import { assert } from 'chai';
+import tap from 'tap';
+
+export function validatorCountProposal(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string,
+  validatorCountIncrement: BN
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Validator count proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8);
+    const description: string = 'Testing validator count proposal ' + uuid().substring(0, 8);
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+
+    // Proposal stake calculation
+    const proposalStake: BN = new BN(100000);
+    const proposalFee: BN = apiWrapper.estimateProposeValidatorCountFee(description, description, proposalStake);
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, proposalFee.add(proposalStake));
+    const validatorCount: BN = await apiWrapper.getValidatorCount();
+
+    // Proposal creation
+    const proposedValidatorCount: BN = validatorCount.add(validatorCountIncrement);
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeValidatorCount(
+      m1KeyPairs[0],
+      proposalTitle,
+      description,
+      proposalStake,
+      proposedValidatorCount
+    );
+    const proposalNumber = await proposalPromise;
+
+    // Approving the proposal
+    const proposalExecutionPromise = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await proposalExecutionPromise;
+    const newValidatorCount: BN = await apiWrapper.getValidatorCount();
+    assert(
+      proposedValidatorCount.eq(newValidatorCount),
+      `Validator count has unexpeccted value ${newValidatorCount}, expected ${proposedValidatorCount}`
+    );
+  });
+}

+ 59 - 0
tests/network-tests/src/nicaea/tests/proposals/impl/workingGroupMintCapacityProposal.ts

@@ -0,0 +1,59 @@
+import { Keyring } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from '../../../utils/apiWrapper';
+import { v4 as uuid } from 'uuid';
+import BN from 'bn.js';
+import { assert } from 'chai';
+import tap from 'tap';
+
+export function workingGroupMintCapacityProposalTest(
+  apiWrapper: ApiWrapper,
+  m1KeyPairs: KeyringPair[],
+  m2KeyPairs: KeyringPair[],
+  keyring: Keyring,
+  sudoUri: string,
+  mintingCapacityIncrement: BN
+) {
+  let sudo: KeyringPair;
+
+  tap.test('Mint capacity proposal test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const description: string = 'spending proposal which is used for API network testing';
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee();
+    const initialMintingCapacity: BN = await apiWrapper.getWorkingGroupMintCapacity();
+
+    // Topping the balances
+    const proposalStake: BN = new BN(50000);
+    const runtimeProposalFee: BN = apiWrapper.estimateProposeWorkingGroupMintCapacityFee(
+      description,
+      description,
+      proposalStake,
+      initialMintingCapacity.add(mintingCapacityIncrement)
+    );
+    await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake));
+    await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee);
+
+    // Proposal creation
+    const proposedMintingCapacity: BN = initialMintingCapacity.add(mintingCapacityIncrement);
+    const proposalPromise = apiWrapper.expectProposalCreated();
+    await apiWrapper.proposeWorkingGroupMintCapacity(
+      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 = apiWrapper.expectProposalFinalized();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await mintCapacityPromise;
+    const newMintingCapacity: BN = await apiWrapper.getWorkingGroupMintCapacity();
+    assert(
+      proposedMintingCapacity.eq(newMintingCapacity),
+      `Content working group has unexpected minting capacity ${newMintingCapacity}, expected ${proposedMintingCapacity}`
+    );
+  });
+}

+ 40 - 0
tests/network-tests/src/nicaea/tests/proposals/setLeadProposalTest.ts

@@ -0,0 +1,40 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { setLeadProposalTest } from './impl/setLeadProposal';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Set lead proposal scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 29;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  setLeadProposalTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri);
+  closeApi(apiWrapper);
+});

+ 42 - 0
tests/network-tests/src/nicaea/tests/proposals/spendingProposalTest.ts

@@ -0,0 +1,42 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { spendingProposalTest } from './impl/spendingProposal';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Spending proposal scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 29;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  spendingProposalTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri, spendingBalance, mintCapacity);
+  closeApi(apiWrapper);
+});

+ 40 - 0
tests/network-tests/src/nicaea/tests/proposals/storageRoleParametersProposalTest.ts

@@ -0,0 +1,40 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { storageRoleParametersProposalTest } from './impl/storageRoleParametersProposal';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Storage role parameters proposal scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 29;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  storageRoleParametersProposalTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri);
+  closeApi(apiWrapper);
+});

+ 40 - 0
tests/network-tests/src/nicaea/tests/proposals/textProposalTest.ts

@@ -0,0 +1,40 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { textProposalTest } from './impl/textProposal';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Text proposal scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 28;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  textProposalTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri);
+  closeApi(apiWrapper);
+});

+ 41 - 0
tests/network-tests/src/nicaea/tests/proposals/updateRuntimeTest.ts

@@ -0,0 +1,41 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { updateRuntimeTest } from './impl/updateRuntime';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Update runtime scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 54;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  updateRuntimeTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri);
+  membershipTest(apiWrapper, new Array<KeyringPair>(), keyring, N, paidTerms, sudoUri);
+  closeApi(apiWrapper);
+});

+ 41 - 0
tests/network-tests/src/nicaea/tests/proposals/validatorCountProposalTest.ts

@@ -0,0 +1,41 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { validatorCountProposal } from './impl/validatorCountProposal';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Validator count proposal scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 29;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  validatorCountProposal(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri, validatorCountIncrement);
+  closeApi(apiWrapper);
+});

+ 41 - 0
tests/network-tests/src/nicaea/tests/proposals/workingGroupMintCapacityProposalTest.ts

@@ -0,0 +1,41 @@
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from '../impl/membershipCreation';
+import { councilTest } from '../impl/electingCouncil';
+import { workingGroupMintCapacityProposalTest } from './impl/workingGroupMintCapacityProposal';
+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 '../impl/closeApi';
+import { ApiWrapper } from '../../utils/apiWrapper';
+
+tap.mocha.describe('Validator count proposal scenario', async () => {
+  initConfig();
+  registerJoystreamTypes();
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  const keyring = new Keyring({ type: 'sr25519' });
+  const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+  const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  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: number = 29;
+
+  const provider = new WsProvider(nodeUrl);
+  const apiWrapper: ApiWrapper = await ApiWrapper.create(provider);
+
+  setTestTimeout(apiWrapper, durationInBlocks);
+  membershipTest(apiWrapper, m1KeyPairs, keyring, N, paidTerms, sudoUri);
+  membershipTest(apiWrapper, m2KeyPairs, keyring, N, paidTerms, sudoUri);
+  councilTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, K, sudoUri, greaterStake, lesserStake);
+  workingGroupMintCapacityProposalTest(apiWrapper, m1KeyPairs, m2KeyPairs, keyring, sudoUri, mintingCapacityIncrement);
+  closeApi(apiWrapper);
+});

+ 728 - 0
tests/network-tests/src/nicaea/utils/apiWrapper.ts

@@ -0,0 +1,728 @@
+import { ApiPromise, WsProvider } from '@polkadot/api';
+import { Option, Vec, Bytes, u32 } from '@polkadot/types';
+import { Codec } from '@polkadot/types/types';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { UserInfo, PaidMembershipTerms, MemberId } from '@nicaea/types/lib/members';
+import { Mint, MintId } from '@nicaea/types/lib/mint';
+import { Lead, LeadId } from '@nicaea/types/lib/content-working-group';
+import { RoleParameters } from '@nicaea/types/lib/roles';
+import { Seat } from '@nicaea/types';
+import { Balance, EventRecord, AccountId, BlockNumber, BalanceOf } from '@polkadot/types/interfaces';
+import BN from 'bn.js';
+import { SubmittableExtrinsic } from '@polkadot/api/types';
+import { Sender } from './sender';
+import { Utils } from './utils';
+
+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 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 estimateProposeWorkingGroupMintCapacityFee(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 estimateAddWorkerOpeningFee(): BN {
+    return this.estimateTxFee(
+      this.api.tx.forumBureaucracy.addWorkerOpening(
+        'CurrentBlock',
+        {
+          application_rationing_policy: { max_active_applicants: '32' },
+          max_review_period_length: 2,
+          application_staking_policy: {
+            amount: 0,
+            amount_mode: 'AtLeast',
+            crowded_out_unstaking_period_length: 0,
+            review_period_expired_unstaking_period_length: 0,
+          },
+          role_staking_policy: {
+            amount: 0,
+            amount_mode: 'AtLeast',
+            crowded_out_unstaking_period_length: 0,
+            review_period_expired_unstaking_period_length: 0,
+          },
+          role_slashing_terms: {
+            Slashable: {
+              max_count: 0,
+              max_percent_pts_per_time: 0,
+            },
+          },
+          fill_opening_successful_applicant_application_stake_unstaking_period: 0,
+          fill_opening_failed_applicant_application_stake_unstaking_period: 0,
+          fill_opening_failed_applicant_role_stake_unstaking_period: 0,
+          terminate_curator_application_stake_unstaking_period: 0,
+          terminate_curator_role_stake_unstaking_period: 0,
+          exit_curator_role_application_stake_unstaking_period: 0,
+          exit_curator_role_stake_unstaking_period: 0,
+        },
+        'Opening readable text'
+      )
+    );
+  }
+
+  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 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 proposeWorkingGroupMintCapacity(
+    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 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 => {
+      await this.api.query.system.events<Vec<EventRecord>>(events => {
+        events.forEach(record => {
+          if (record.event.method && record.event.method.toString() === 'ProposalCreated') {
+            resolve(new BN(record.event.data[1].toString()));
+          }
+        });
+      });
+    });
+  }
+
+  public expectRuntimeUpgraded(): Promise<void> {
+    return new Promise(async resolve => {
+      await this.api.query.system.events<Vec<EventRecord>>(events => {
+        events.forEach(record => {
+          if (record.event.method.toString() === 'RuntimeUpdated') {
+            resolve();
+          }
+        });
+      });
+    });
+  }
+
+  public expectProposalFinalized(): Promise<void> {
+    return new Promise(async resolve => {
+      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')
+          ) {
+            resolve();
+          }
+        });
+      });
+    });
+  }
+
+  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 getWorkingGroupMintCapacity(): 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 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'
+    );
+    return storageProviders.map(accountId => accountId.toString()).includes(address);
+  }
+
+  public async sudoSetLead(sudo: KeyringPair, lead: KeyringPair): Promise<void> {
+    const leadMemberId: BN = (await this.getMemberIds(lead.address))[0].toBn();
+    return this.sender.signAndSend(
+      this.api.tx.sudo.sudo(this.api.tx.forumBureaucracy.setLead(leadMemberId, lead.address)),
+      sudo,
+      false
+    );
+  }
+
+  public async addWorkerOpening(account: KeyringPair, activateAt: string, text: string): Promise<void> {
+    const commitment = {
+      application_rationing_policy: { max_active_applicants: '32' },
+      max_review_period_length: 2,
+      application_staking_policy: {
+        amount: 0,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: 0,
+        review_period_expired_unstaking_period_length: 0,
+      },
+      role_staking_policy: {
+        amount: 0,
+        amount_mode: 'AtLeast',
+        crowded_out_unstaking_period_length: 0,
+        review_period_expired_unstaking_period_length: 0,
+      },
+      role_slashing_terms: {
+        Slashable: {
+          max_count: 0,
+          max_percent_pts_per_time: 0,
+        },
+      },
+      fill_opening_successful_applicant_application_stake_unstaking_period: 0,
+      fill_opening_failed_applicant_application_stake_unstaking_period: 0,
+      fill_opening_failed_applicant_role_stake_unstaking_period: 0,
+      terminate_curator_application_stake_unstaking_period: 0,
+      terminate_curator_role_stake_unstaking_period: 0,
+      exit_curator_role_application_stake_unstaking_period: 0,
+      exit_curator_role_stake_unstaking_period: 0,
+    };
+    await this.sender.signAndSend(
+      this.api.tx.forumBureaucracy.addWorkerOpening(activateAt, commitment, text),
+      account,
+      false
+    );
+  }
+
+  public async getStorageRoleParameters(): Promise<RoleParameters> {
+    return (await this.api.query.actors.parameters<Option<RoleParameters>>('StorageProvider')).unwrap();
+  }
+
+  public async getAnnouncingPeriod(): Promise<BN> {
+    return await this.api.query.councilElection.announcingPeriod<BlockNumber>();
+  }
+
+  public async getVotingPeriod(): Promise<BN> {
+    return await this.api.query.councilElection.votingPeriod<BlockNumber>();
+  }
+
+  public async getRevealingPeriod(): Promise<BN> {
+    return await this.api.query.councilElection.revealingPeriod<BlockNumber>();
+  }
+
+  public async getCouncilSize(): Promise<BN> {
+    return await this.api.query.councilElection.councilSize<u32>();
+  }
+
+  public async getCandidacyLimit(): Promise<BN> {
+    return await this.api.query.councilElection.candidacyLimit<u32>();
+  }
+
+  public async getNewTermDuration(): Promise<BN> {
+    return await this.api.query.councilElection.newTermDuration<BlockNumber>();
+  }
+
+  public async getMinCouncilStake(): Promise<BN> {
+    return await this.api.query.councilElection.minCouncilStake<BalanceOf>();
+  }
+
+  public async getMinVotingStake(): Promise<BN> {
+    return await this.api.query.councilElection.minVotingStake<BalanceOf>();
+  }
+}

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

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

+ 66 - 0
tests/network-tests/src/nicaea/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/nicaea/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);
+}

+ 50 - 0
tests/network-tests/src/nicaea/utils/utils.ts

@@ -0,0 +1,50 @@
+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 { decodeAddress } from '@polkadot/keyring';
+import { Seat } from '@nicaea/types';
+
+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);
+    // console.log('Vote hash:', hash, 'for', { accountId, salt });
+    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');
+  }
+}

+ 8 - 0
yarn.lock

@@ -1736,6 +1736,14 @@
     "@types/istanbul-reports" "^1.1.1"
     "@types/yargs" "^13.0.0"
 
+"@joystream/types@./types", "@nicaea/types@./types":
+  version "0.10.0"
+  dependencies:
+    "@polkadot/types" "^0.96.1"
+    "@types/vfile" "^4.0.0"
+    ajv "^6.11.0"
+    lodash "^4.17.15"
+
 "@ledgerhq/devices@^4.78.0":
   version "4.78.0"
   resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-4.78.0.tgz#149b572f0616096e2bd5eb14ce14d0061c432be6"