Przeglądaj źródła

updated runtime from Rome to Constantinople

Gleb Urvanov 4 lat temu
rodzic
commit
b71e471846

+ 3 - 0
.gitignore

@@ -22,3 +22,6 @@ yarn*
 
 # Visual Studio Code
 .vscode
+
+# Compiled WASM code
+*.wasm

+ 3 - 1
tests/network-tests/.env

@@ -15,4 +15,6 @@ COUNCIL_ELECTION_K = 2
 # Balance to spend using spending proposal
 SPENDING_BALANCE = 1000
 # Minting capacity for content working group minting capacity test.
-MINTING_CAPACITY = 100020
+MINTING_CAPACITY = 100020
+# Stake amount for Rome runtime upgrade proposal
+RUNTIME_UPGRADE_PROPOSAL_STAKE = 100000

BIN
tests/network-tests/joystream_node_runtime.wasm


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

@@ -9,7 +9,8 @@
     "prettier": "prettier --write ./src"
   },
   "dependencies": {
-    "@joystream/types": "../joystream-apps/packages/joy-types",
+    "@joystream/types": "^0.7.0",
+    "@rome/types@npm:@joystream/types": "^0.7.0",
     "@polkadot/api": "^0.96.1",
     "@polkadot/keyring": "^1.7.0-beta.5",
     "@types/bn.js": "^4.11.5",

+ 127 - 0
tests/network-tests/src/tests/rome/electingCouncilTest.ts

@@ -0,0 +1,127 @@
+import { membershipTest } from './membershipCreationTest';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { ApiWrapper } from './utils/apiWrapper';
+import { WsProvider, Keyring } from '@polkadot/api';
+import { initConfig } from '../../utils/config';
+import BN = require('bn.js');
+import { registerJoystreamTypes, Seat } from '@rome/types';
+import { assert } from 'chai';
+import { v4 as uuid } from 'uuid';
+import { Utils } from './utils/utils';
+
+export function councilTest(m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[]) {
+  initConfig();
+  const keyring = new Keyring({ type: 'sr25519' });
+  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 defaultTimeout: number = 120000;
+  let sudo: KeyringPair;
+  let apiWrapper: ApiWrapper;
+
+  before(async function () {
+    this.timeout(defaultTimeout);
+    registerJoystreamTypes();
+    const provider = new WsProvider(nodeUrl);
+    apiWrapper = await ApiWrapper.create(provider);
+  });
+
+  it('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)}`
+      )
+    );
+  }).timeout(defaultTimeout);
+
+  after(() => {
+    apiWrapper.close();
+  });
+}
+
+describe.skip('Council integration tests', () => {
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+  membershipTest(m1KeyPairs);
+  membershipTest(m2KeyPairs);
+  councilTest(m1KeyPairs, m2KeyPairs);
+});

+ 94 - 0
tests/network-tests/src/tests/rome/membershipCreationTest.ts

@@ -0,0 +1,94 @@
+import { WsProvider } from '@polkadot/api';
+import { registerJoystreamTypes } from '@rome/types';
+import { Keyring } from '@polkadot/keyring';
+import { assert } from 'chai';
+import { KeyringPair } from '@polkadot/keyring/types';
+import BN = require('bn.js');
+import { ApiWrapper } from './utils/apiWrapper';
+import { initConfig } from '../../utils/config';
+import { v4 as uuid } from 'uuid';
+
+export function membershipTest(nKeyPairs: KeyringPair[]) {
+  initConfig();
+  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 defaultTimeout: number = 30000;
+  let apiWrapper: ApiWrapper;
+  let sudo: KeyringPair;
+  let aKeyPair: KeyringPair;
+  let membershipFee: BN;
+  let membershipTransactionFee: BN;
+
+  before(async function () {
+    this.timeout(defaultTimeout);
+    registerJoystreamTypes();
+    const provider = new WsProvider(nodeUrl);
+    apiWrapper = await ApiWrapper.create(provider);
+    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);
+  });
+
+  it('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`))
+    );
+  }).timeout(defaultTimeout);
+
+  it('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'));
+  }).timeout(defaultTimeout);
+
+  it('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'));
+  }).timeout(defaultTimeout);
+
+  after(() => {
+    apiWrapper.close();
+  });
+}
+
+describe.skip('Membership integration tests', () => {
+  const nKeyPairs: KeyringPair[] = new Array();
+  membershipTest(nKeyPairs);
+});

+ 15 - 13
tests/network-tests/src/tests/upgrade/romeRuntimeUpgradeTest.ts → tests/network-tests/src/tests/rome/romeRuntimeUpgradeTest.ts

@@ -4,10 +4,10 @@ import { Bytes } from '@polkadot/types';
 import { KeyringPair } from '@polkadot/keyring/types';
 import { membershipTest } from '../membershipCreationTest';
 import { councilTest } from '../electingCouncilTest';
-import { registerJoystreamTypes } from '@joystream/types';
-import { ApiWrapper } from '../../utils/apiWrapper';
+import { registerJoystreamTypes } from '@rome/types';
+import { ApiWrapper } from './utils/apiWrapper';
 import BN = require('bn.js');
-import { Utils } from '../../utils/utils';
+import { Utils } from './utils/utils';
 
 describe('Runtime upgrade integration tests', () => {
   initConfig();
@@ -22,31 +22,28 @@ describe('Runtime upgrade integration tests', () => {
 
   let apiWrapper: ApiWrapper;
   let sudo: KeyringPair;
+  let provider: WsProvider;
 
   before(async function () {
-    console.log('before the test');
     this.timeout(defaultTimeout);
     registerJoystreamTypes();
-    const provider = new WsProvider(nodeUrl);
-    console.log('1');
+    provider = new WsProvider(nodeUrl);
     apiWrapper = await ApiWrapper.create(provider);
-    console.log('2');
   });
 
-  console.log('3');
   membershipTest(m1KeyPairs);
-  console.log('4');
   membershipTest(m2KeyPairs);
-  console.log('5');
   councilTest(m1KeyPairs, m2KeyPairs);
-  console.log('6');
 
   it('Upgrading the runtime test', async () => {
     // Setup
     console.log('7');
     sudo = keyring.addFromUri(sudoUri);
+    // const runtime: Bytes = await apiWrapper.getRuntime();
     const runtime: string = Utils.readRuntimeFromFile('joystream_node_runtime.wasm');
-    console.log('runtime read ' + runtime);
+    console.log('runtime length ' + runtime.length);
+    console.log('runtime strart ' + runtime.slice(0, 10));
+    console.log('runtime end ' + runtime.slice(runtime.length - 10));
     const description: string = 'runtime upgrade proposal which is used for API integration testing';
     const runtimeProposalFee: BN = apiWrapper.estimateRomeProposeRuntimeUpgradeFee(
       proposalStake,
@@ -62,6 +59,7 @@ describe('Runtime upgrade integration tests', () => {
 
     // Proposal creation
     const proposalPromise = apiWrapper.expectProposalCreated();
+    console.log('proposal will be sent');
     await apiWrapper.proposeRuntimeRome(
       m1KeyPairs[0],
       proposalStake,
@@ -69,15 +67,19 @@ describe('Runtime upgrade integration tests', () => {
       'runtime to test proposal functionality',
       runtime
     );
+    console.log('proposal sent');
     const proposalNumber = await proposalPromise;
+    console.log('proposal created');
 
     // Approving runtime update proposal
     const runtimePromise = apiWrapper.expectRomeRuntimeUpgraded();
+    console.log('voting');
     await apiWrapper.batchApproveRomeProposal(m2KeyPairs, proposalNumber);
+    // apiWrapper = await ApiWrapper.create(provider);
     await runtimePromise;
   }).timeout(defaultTimeout);
 
-  membershipTest(new Array<KeyringPair>());
+  //membershipTest(new Array<KeyringPair>());
 
   after(() => {
     apiWrapper.close();

+ 433 - 0
tests/network-tests/src/tests/rome/utils/apiWrapper.ts

@@ -0,0 +1,433 @@
+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 '@rome/types/lib/members';
+import { Seat, VoteKind } from '@rome/types';
+import { Balance, EventRecord } from '@polkadot/types/interfaces';
+import BN = require('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 estimateRomeProposeRuntimeUpgradeFee(
+    stake: BN,
+    name: string,
+    description: string,
+    runtime: Bytes | string
+  ): BN {
+    return this.estimateTxFee(this.api.tx.proposals.createProposal(stake, name, description, 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 estimateVoteForProposalFee(): BN {
+    return this.estimateTxFee(this.api.tx.proposalsEngine.vote(0, 0, 'Approve'));
+  }
+
+  public estimateVoteForRomeRuntimeProposalFee(): BN {
+    return this.estimateTxFee(this.api.tx.proposals.voteOnProposal(0, 'Approve'));
+  }
+
+  public newEstimate(): BN {
+    return new BN(100);
+  }
+
+  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 getBestBlock(): Promise<BN> {
+    return this.api.derive.chain.bestNumber();
+  }
+
+  public getCouncil(): Promise<Seat[]> {
+    return this.api.query.council.activeCouncil<Vec<Codec>>().then(seats => {
+      return JSON.parse(seats.toString());
+    });
+  }
+
+  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 proposeRuntimeRome(
+    account: KeyringPair,
+    stake: BN,
+    name: string,
+    description: string,
+    runtime: Bytes | string
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.proposals.createProposal(stake, name, description, 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 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 approveRomeProposal(account: KeyringPair, proposal: BN): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.proposals.voteOnProposal(proposal, new VoteKind('Approve')),
+      account,
+      false
+    );
+  }
+
+  public batchApproveRomeProposal(council: KeyringPair[], proposal: BN): Promise<void[]> {
+    return Promise.all(
+      council.map(async keyPair => {
+        await this.approveRomeProposal(keyPair, 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.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 expectRomeRuntimeUpgraded(): 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') {
+            console.log('Runtime updated!!');
+            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.toString() === 'ProposalStatusUpdated' &&
+            record.event.data[1].toString().includes('Finalized')
+          ) {
+            resolve();
+          }
+        });
+      });
+    });
+  }
+
+  public getTotalIssuance(): Promise<BN> {
+    return this.api.query.balances.totalIssuance<Balance>();
+  }
+
+  public async getProposal(id: BN) {
+    const proposal = await this.api.query.proposalsEngine.proposals(id);
+    console.log('proposal to string ' + proposal.toString());
+    console.log('proposal to raw ' + proposal.toRawType());
+    return;
+  }
+
+  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>();
+  }
+}

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

@@ -0,0 +1,66 @@
+import BN = require('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);
+        });
+    });
+  }
+}

+ 51 - 0
tests/network-tests/src/tests/rome/utils/utils.ts

@@ -0,0 +1,51 @@
+import { IExtrinsic } from '@polkadot/types/types';
+import { Bytes } from '@polkadot/types';
+import { compactToU8a, stringToU8a } from '@polkadot/util';
+import { blake2AsHex } from '@polkadot/util-crypto';
+import BN = require('bn.js');
+import fs = require('fs');
+import { decodeAddress } from '@polkadot/keyring';
+import { Seat } from '@rome/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');
+  }
+}

+ 4 - 0
tests/network-tests/src/utils/apiWrapper.ts

@@ -146,6 +146,10 @@ export class ApiWrapper {
     return this.estimateTxFee(this.api.tx.proposalsEngine.vote(0, 0, 'Approve'));
   }
 
+  public estimateVoteForRomeRuntimeProposalFee(): BN {
+    return this.estimateTxFee(this.api.tx.proposals.voteOnProposal(0, 'Approve'));
+  }
+
   private applyForCouncilElection(account: KeyringPair, amount: BN): Promise<void> {
     return this.sender.signAndSend(this.api.tx.councilElection.apply(amount), account, false);
   }

+ 1 - 0
tests/network-tests/src/utils/utils.ts

@@ -1,4 +1,5 @@
 import { IExtrinsic } from '@polkadot/types/types';
+import { Bytes } from '@polkadot/types';
 import { compactToU8a, stringToU8a } from '@polkadot/util';
 import { blake2AsHex } from '@polkadot/util-crypto';
 import BN = require('bn.js');