Kaynağa Gözat

merged development

Gleb Urvanov 4 yıl önce
ebeveyn
işleme
387b981eb8
37 değiştirilmiş dosya ile 1122 ekleme ve 144 silme
  1. 1 0
      Cargo.lock
  2. 9 4
      node/src/forum_config/from_serialized.rs
  3. 55 36
      runtime-modules/forum/src/lib.rs
  4. 17 10
      runtime-modules/forum/src/mock.rs
  5. 6 1
      runtime-modules/governance/Cargo.toml
  6. 169 9
      runtime-modules/governance/src/council.rs
  7. 5 0
      runtime-modules/governance/src/mock.rs
  8. 2 2
      runtime-modules/proposals/codex/src/tests/mock.rs
  9. 4 4
      runtime-modules/proposals/discussion/src/lib.rs
  10. 2 2
      runtime-modules/proposals/discussion/src/tests/mock.rs
  11. 10 10
      runtime-modules/proposals/discussion/src/tests/mod.rs
  12. 21 3
      runtime/src/lib.rs
  13. 1 36
      runtime/src/migration.rs
  14. 3 1
      tests/network-tests/.env
  15. 5 1
      tests/network-tests/package.json
  16. 4 4
      tests/network-tests/src/tests/constantinople/electingCouncilTest.ts
  17. 2 2
      tests/network-tests/src/tests/constantinople/membershipCreationTest.ts
  18. 2 2
      tests/network-tests/src/tests/constantinople/proposals/electionParametersProposalTest.ts
  19. 3 3
      tests/network-tests/src/tests/constantinople/proposals/evictStoraveProviderTest.ts
  20. 2 2
      tests/network-tests/src/tests/constantinople/proposals/setLeadProposalTest.ts
  21. 2 2
      tests/network-tests/src/tests/constantinople/proposals/spendingProposalTest.ts
  22. 2 2
      tests/network-tests/src/tests/constantinople/proposals/storageRoleParametersProposalTest.ts
  23. 2 2
      tests/network-tests/src/tests/constantinople/proposals/textProposalTest.ts
  24. 2 2
      tests/network-tests/src/tests/constantinople/proposals/updateRuntimeTest.ts
  25. 2 2
      tests/network-tests/src/tests/constantinople/proposals/validatorCountProposal.ts
  26. 2 2
      tests/network-tests/src/tests/constantinople/proposals/workingGroupMintCapacityProposalTest.ts
  27. 0 0
      tests/network-tests/src/tests/constantinople/utils/apiWrapper.ts
  28. 0 0
      tests/network-tests/src/tests/constantinople/utils/config.ts
  29. 0 0
      tests/network-tests/src/tests/constantinople/utils/sender.ts
  30. 0 0
      tests/network-tests/src/tests/constantinople/utils/utils.ts
  31. 127 0
      tests/network-tests/src/tests/rome/electingCouncilTest.ts
  32. 94 0
      tests/network-tests/src/tests/rome/membershipCreationTest.ts
  33. 75 0
      tests/network-tests/src/tests/rome/romeRuntimeUpgradeTest.ts
  34. 370 0
      tests/network-tests/src/tests/rome/utils/apiWrapper.ts
  35. 5 0
      tests/network-tests/src/tests/rome/utils/config.ts
  36. 66 0
      tests/network-tests/src/tests/rome/utils/sender.ts
  37. 50 0
      tests/network-tests/src/tests/rome/utils/utils.ts

+ 1 - 0
Cargo.lock

@@ -4837,6 +4837,7 @@ dependencies = [
  "substrate-common-module",
  "substrate-membership-module",
  "substrate-primitives",
+ "substrate-recurring-reward-module",
  "substrate-token-mint-module",
 ]
 

+ 9 - 4
node/src/forum_config/from_serialized.rs

@@ -1,7 +1,9 @@
+#![allow(clippy::type_complexity)]
+
 use super::new_validation;
 use node_runtime::{
-    forum::{Category, CategoryId, Post, PostId, Thread, ThreadId},
-    AccountId, BlockNumber, ForumConfig, Moment,
+    forum::{Category, CategoryId, Post, Thread},
+    AccountId, BlockNumber, ForumConfig, Moment, PostId, ThreadId,
 };
 use serde::Deserialize;
 use serde_json::Result;
@@ -9,8 +11,11 @@ use serde_json::Result;
 #[derive(Deserialize)]
 struct ForumData {
     categories: Vec<(CategoryId, Category<BlockNumber, Moment, AccountId>)>,
-    posts: Vec<(PostId, Post<BlockNumber, Moment, AccountId>)>,
-    threads: Vec<(ThreadId, Thread<BlockNumber, Moment, AccountId>)>,
+    posts: Vec<(
+        PostId,
+        Post<BlockNumber, Moment, AccountId, ThreadId, PostId>,
+    )>,
+    threads: Vec<(ThreadId, Thread<BlockNumber, Moment, AccountId, ThreadId>)>,
 }
 
 fn parse_forum_json() -> Result<ForumData> {

+ 55 - 36
runtime-modules/forum/src/lib.rs

@@ -12,8 +12,9 @@ use serde_derive::{Deserialize, Serialize};
 use rstd::borrow::ToOwned;
 use rstd::prelude::*;
 
-use codec::{Decode, Encode};
-use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure};
+use codec::{Codec, Decode, Encode};
+use runtime_primitives::traits::{MaybeSerialize, Member, One, SimpleArithmetic};
+use srml_support::{decl_event, decl_module, decl_storage, dispatch, ensure, Parameter};
 
 mod mock;
 mod tests;
@@ -156,13 +157,10 @@ pub struct PostTextChange<BlockNumber, Moment> {
     text: Vec<u8>,
 }
 
-/// Represents a post identifier
-pub type PostId = u64;
-
 /// Represents a thread post
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-pub struct Post<BlockNumber, Moment, AccountId> {
+pub struct Post<BlockNumber, Moment, AccountId, ThreadId, PostId> {
     /// Post identifier
     id: PostId,
 
@@ -192,13 +190,10 @@ pub struct Post<BlockNumber, Moment, AccountId> {
     author_id: AccountId,
 }
 
-/// Represents a thread identifier
-pub type ThreadId = u64;
-
 /// Represents a thread
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
-pub struct Thread<BlockNumber, Moment, AccountId> {
+pub struct Thread<BlockNumber, Moment, AccountId, ThreadId> {
     /// Thread identifier
     id: ThreadId,
 
@@ -238,7 +233,7 @@ pub struct Thread<BlockNumber, Moment, AccountId> {
     author_id: AccountId,
 }
 
-impl<BlockNumber, Moment, AccountId> Thread<BlockNumber, Moment, AccountId> {
+impl<BlockNumber, Moment, AccountId, ThreadId> Thread<BlockNumber, Moment, AccountId, ThreadId> {
     fn num_posts_ever_created(&self) -> u32 {
         self.num_unmoderated_posts + self.num_moderated_posts
     }
@@ -321,6 +316,26 @@ pub trait Trait: system::Trait + timestamp::Trait + Sized {
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
     type MembershipRegistry: ForumUserRegistry<Self::AccountId>;
+
+    /// Thread Id type
+    type ThreadId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
+
+    /// Post Id type
+    type PostId: Parameter
+        + Member
+        + SimpleArithmetic
+        + Codec
+        + Default
+        + Copy
+        + MaybeSerialize
+        + PartialEq;
 }
 
 decl_storage! {
@@ -333,16 +348,16 @@ decl_storage! {
         pub NextCategoryId get(next_category_id) config(): CategoryId;
 
         /// Map thread identifier to corresponding thread.
-        pub ThreadById get(thread_by_id) config(): map ThreadId => Thread<T::BlockNumber, T::Moment, T::AccountId>;
+        pub ThreadById get(thread_by_id) config(): map T::ThreadId => Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>;
 
         /// Thread identifier value to be used for next Thread in threadById.
-        pub NextThreadId get(next_thread_id) config(): ThreadId;
+        pub NextThreadId get(next_thread_id) config(): T::ThreadId;
 
         /// Map post identifier to corresponding post.
-        pub PostById get(post_by_id) config(): map PostId => Post<T::BlockNumber, T::Moment, T::AccountId>;
+        pub PostById get(post_by_id) config(): map T::PostId => Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>;
 
         /// Post identifier value to be used for for next post created.
-        pub NextPostId get(next_post_id) config(): PostId;
+        pub NextPostId get(next_post_id) config(): T::PostId;
 
         /// Account of forum sudo.
         pub ForumSudo get(forum_sudo) config(): Option<T::AccountId>;
@@ -386,6 +401,8 @@ decl_event!(
     pub enum Event<T>
     where
         <T as system::Trait>::AccountId,
+        <T as Trait>::ThreadId,
+        <T as Trait>::PostId,
     {
         /// A category was introduced
         CategoryCreated(CategoryId),
@@ -632,7 +649,7 @@ decl_module! {
         }
 
         /// Moderate thread
-        fn moderate_thread(origin, thread_id: ThreadId, rationale: Vec<u8>) -> dispatch::Result {
+        fn moderate_thread(origin, thread_id: T::ThreadId, rationale: Vec<u8>) -> dispatch::Result {
 
             // Check that its a valid signature
             let who = ensure_signed(origin)?;
@@ -683,7 +700,7 @@ decl_module! {
         }
 
         /// Edit post text
-        fn add_post(origin, thread_id: ThreadId, text: Vec<u8>) -> dispatch::Result {
+        fn add_post(origin, thread_id: T::ThreadId, text: Vec<u8>) -> dispatch::Result {
 
             /*
              * Update SPEC with new errors,
@@ -720,7 +737,7 @@ decl_module! {
         }
 
         /// Edit post text
-        fn edit_post_text(origin, post_id: PostId, new_text: Vec<u8>) -> dispatch::Result {
+        fn edit_post_text(origin, post_id: T::PostId, new_text: Vec<u8>) -> dispatch::Result {
 
             /* Edit spec.
               - forum member guard missing
@@ -767,7 +784,7 @@ decl_module! {
         }
 
         /// Moderate post
-        fn moderate_post(origin, post_id: PostId, rationale: Vec<u8>) -> dispatch::Result {
+        fn moderate_post(origin, post_id: T::PostId, rationale: Vec<u8>) -> dispatch::Result {
 
             // Check that its a valid signature
             let who = ensure_signed(origin)?;
@@ -867,8 +884,9 @@ impl<T: Trait> Module<T> {
     }
 
     fn ensure_post_is_mutable(
-        post_id: PostId,
-    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        post_id: T::PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>, &'static str>
+    {
         // Make sure post exists
         let post = Self::ensure_post_exists(post_id)?;
 
@@ -882,8 +900,9 @@ impl<T: Trait> Module<T> {
     }
 
     fn ensure_post_exists(
-        post_id: PostId,
-    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        post_id: T::PostId,
+    ) -> Result<Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId>, &'static str>
+    {
         if <PostById<T>>::exists(post_id) {
             Ok(<PostById<T>>::get(post_id))
         } else {
@@ -892,8 +911,8 @@ impl<T: Trait> Module<T> {
     }
 
     fn ensure_thread_is_mutable(
-        thread_id: ThreadId,
-    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        thread_id: T::ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>, &'static str> {
         // Make sure thread exists
         let thread = Self::ensure_thread_exists(thread_id)?;
 
@@ -907,8 +926,8 @@ impl<T: Trait> Module<T> {
     }
 
     fn ensure_thread_exists(
-        thread_id: ThreadId,
-    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId>, &'static str> {
+        thread_id: T::ThreadId,
+    ) -> Result<Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId>, &'static str> {
         if <ThreadById<T>>::exists(thread_id) {
             Ok(<ThreadById<T>>::get(thread_id))
         } else {
@@ -1045,12 +1064,12 @@ impl<T: Trait> Module<T> {
         category_id: CategoryId,
         title: &[u8],
         author_id: &T::AccountId,
-    ) -> Thread<T::BlockNumber, T::Moment, T::AccountId> {
+    ) -> Thread<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId> {
         // Get category
         let category = <CategoryById<T>>::get(category_id);
 
         // Create and add new thread
-        let new_thread_id = NextThreadId::get();
+        let new_thread_id = NextThreadId::<T>::get();
 
         let new_thread = Thread {
             id: new_thread_id,
@@ -1068,8 +1087,8 @@ impl<T: Trait> Module<T> {
         <ThreadById<T>>::insert(new_thread_id, new_thread.clone());
 
         // Update next thread id
-        NextThreadId::mutate(|n| {
-            *n += 1;
+        NextThreadId::<T>::mutate(|n| {
+            *n += One::one();
         });
 
         // Update unmoderated thread count in corresponding category
@@ -1083,15 +1102,15 @@ impl<T: Trait> Module<T> {
     /// Creates and ads a new post ot the given thread, and makes all required state updates
     /// `thread_id` must be valid
     fn add_new_post(
-        thread_id: ThreadId,
+        thread_id: T::ThreadId,
         text: &[u8],
         author_id: &T::AccountId,
-    ) -> Post<T::BlockNumber, T::Moment, T::AccountId> {
+    ) -> Post<T::BlockNumber, T::Moment, T::AccountId, T::ThreadId, T::PostId> {
         // Get thread
         let thread = <ThreadById<T>>::get(thread_id);
 
         // Make and add initial post
-        let new_post_id = NextPostId::get();
+        let new_post_id = NextPostId::<T>::get();
 
         let new_post = Post {
             id: new_post_id,
@@ -1108,8 +1127,8 @@ impl<T: Trait> Module<T> {
         <PostById<T>>::insert(new_post_id, new_post.clone());
 
         // Update next post id
-        NextPostId::mutate(|n| {
-            *n += 1;
+        NextPostId::<T>::mutate(|n| {
+            *n += One::one();
         });
 
         // Update unmoderated post count of thread

+ 17 - 10
runtime-modules/forum/src/mock.rs

@@ -100,6 +100,8 @@ impl timestamp::Trait for Runtime {
 impl Trait for Runtime {
     type Event = ();
     type MembershipRegistry = registry::TestMembershipRegistryModule;
+    type ThreadId = u64;
+    type PostId = u64;
 }
 
 #[derive(Clone)]
@@ -123,9 +125,9 @@ pub const NOT_MEMBER_ORIGIN: OriginType = OriginType::Signed(222);
 
 pub const INVLAID_CATEGORY_ID: CategoryId = 333;
 
-pub const INVLAID_THREAD_ID: ThreadId = 444;
+pub const INVLAID_THREAD_ID: RuntimeThreadId = 444;
 
-pub const INVLAID_POST_ID: ThreadId = 555;
+pub const INVLAID_POST_ID: RuntimePostId = 555;
 
 pub fn generate_text(len: usize) -> Vec<u8> {
     vec![b'x'; len]
@@ -228,7 +230,7 @@ impl CreateThreadFixture {
 
 pub struct CreatePostFixture {
     pub origin: OriginType,
-    pub thread_id: ThreadId,
+    pub thread_id: RuntimeThreadId,
     pub text: Vec<u8>,
     pub result: dispatch::Result,
 }
@@ -285,7 +287,7 @@ pub fn assert_create_thread(
 
 pub fn assert_create_post(
     forum_sudo: OriginType,
-    thread_id: ThreadId,
+    thread_id: RuntimeThreadId,
     expected_result: dispatch::Result,
 ) {
     CreatePostFixture {
@@ -312,7 +314,7 @@ pub fn create_root_category(forum_sudo: OriginType) -> CategoryId {
 
 pub fn create_root_category_and_thread(
     forum_sudo: OriginType,
-) -> (OriginType, CategoryId, ThreadId) {
+) -> (OriginType, CategoryId, RuntimeThreadId) {
     let member_origin = create_forum_member();
     let category_id = create_root_category(forum_sudo);
     let thread_id = TestForumModule::next_thread_id();
@@ -331,7 +333,7 @@ pub fn create_root_category_and_thread(
 
 pub fn create_root_category_and_thread_and_post(
     forum_sudo: OriginType,
-) -> (OriginType, CategoryId, ThreadId, PostId) {
+) -> (OriginType, CategoryId, RuntimeThreadId, RuntimePostId) {
     let (member_origin, category_id, thread_id) = create_root_category_and_thread(forum_sudo);
     let post_id = TestForumModule::next_post_id();
 
@@ -348,7 +350,7 @@ pub fn create_root_category_and_thread_and_post(
 
 pub fn moderate_thread(
     forum_sudo: OriginType,
-    thread_id: ThreadId,
+    thread_id: RuntimeThreadId,
     rationale: Vec<u8>,
 ) -> dispatch::Result {
     TestForumModule::moderate_thread(mock_origin(forum_sudo), thread_id, rationale)
@@ -356,7 +358,7 @@ pub fn moderate_thread(
 
 pub fn moderate_post(
     forum_sudo: OriginType,
-    post_id: PostId,
+    post_id: RuntimePostId,
     rationale: Vec<u8>,
 ) -> dispatch::Result {
     TestForumModule::moderate_post(mock_origin(forum_sudo), post_id, rationale)
@@ -456,23 +458,28 @@ pub type RuntimeThread = Thread<
     <Runtime as system::Trait>::BlockNumber,
     <Runtime as timestamp::Trait>::Moment,
     <Runtime as system::Trait>::AccountId,
+    RuntimeThreadId,
 >;
 pub type RuntimePost = Post<
     <Runtime as system::Trait>::BlockNumber,
     <Runtime as timestamp::Trait>::Moment,
     <Runtime as system::Trait>::AccountId,
+    RuntimeThreadId,
+    RuntimePostId,
 >;
 pub type RuntimeBlockchainTimestamp = BlockchainTimestamp<
     <Runtime as system::Trait>::BlockNumber,
     <Runtime as timestamp::Trait>::Moment,
 >;
+pub type RuntimeThreadId = <Runtime as Trait>::ThreadId;
+pub type RuntimePostId = <Runtime as Trait>::PostId;
 
 pub fn genesis_config(
     category_by_id: &RuntimeMap<CategoryId, RuntimeCategory>,
     next_category_id: u64,
-    thread_by_id: &RuntimeMap<ThreadId, RuntimeThread>,
+    thread_by_id: &RuntimeMap<RuntimeThreadId, RuntimeThread>,
     next_thread_id: u64,
-    post_by_id: &RuntimeMap<PostId, RuntimePost>,
+    post_by_id: &RuntimeMap<RuntimePostId, RuntimePost>,
     next_post_id: u64,
     forum_sudo: <Runtime as system::Trait>::AccountId,
     category_title_constraint: &InputValidationLengthConstraint,

+ 6 - 1
runtime-modules/governance/Cargo.toml

@@ -92,4 +92,9 @@ rev = 'c37bb08535c49a12320af7facfd555ce05cce2e8'
 [dependencies.minting]
 default_features = false
 package = 'substrate-token-mint-module'
-path = '../token-minting'
+path = '../token-minting'
+
+[dependencies.recurringrewards]
+default_features = false
+package = 'substrate-recurring-reward-module'
+path = '../recurring-reward'

+ 169 - 9
runtime-modules/governance/src/council.rs

@@ -1,6 +1,6 @@
 use rstd::prelude::*;
-use sr_primitives::traits::Zero;
-use srml_support::{decl_event, decl_module, decl_storage, ensure};
+use sr_primitives::traits::{One, Zero};
+use srml_support::{debug, decl_event, decl_module, decl_storage, ensure};
 use system::{self, ensure_root};
 
 pub use super::election::{self, CouncilElected, Seat, Seats};
@@ -21,7 +21,7 @@ impl<X: CouncilTermEnded> CouncilTermEnded for (X,) {
     }
 }
 
-pub trait Trait: system::Trait + minting::Trait + GovernanceCurrency {
+pub trait Trait: system::Trait + recurringrewards::Trait + GovernanceCurrency {
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
     type CouncilTermEnded: CouncilTermEnded;
@@ -37,6 +37,20 @@ decl_storage! {
         /// because it was introduced in a runtime upgrade. It will be automatically created when
         /// a successful call to set_council_mint_capacity() is made.
         pub CouncilMint get(council_mint) : Option<<T as minting::Trait>::MintId>;
+
+        /// The reward relationships currently in place. There may not necessarily be a 1-1 correspondance with
+        /// the active council, since there are multiple ways of setting/adding/removing council members, some of which
+        /// do not involve creating a relationship.
+        pub RewardRelationships get(reward_relationships) : map T::AccountId => T::RewardRelationshipId;
+
+        /// Reward amount paid out at each PayoutInterval
+        pub AmountPerPayout get(amount_per_payout): minting::BalanceOf<T>;
+
+        /// Optional interval in blocks on which a reward payout will be made to each council member
+        pub PayoutInterval get(payout_interval): Option<T::BlockNumber>;
+
+        /// How many blocks after the reward is created, the first payout will be made
+        pub FirstPayoutAfterRewardCreated get(first_payout_after_reward_created): T::BlockNumber;
     }
 }
 
@@ -50,10 +64,23 @@ decl_event!(
 
 impl<T: Trait> CouncilElected<Seats<T::AccountId, BalanceOf<T>>, T::BlockNumber> for Module<T> {
     fn council_elected(seats: Seats<T::AccountId, BalanceOf<T>>, term: T::BlockNumber) {
-        <ActiveCouncil<T>>::put(seats);
+        <ActiveCouncil<T>>::put(seats.clone());
 
         let next_term_ends_at = <system::Module<T>>::block_number() + term;
+
         <TermEndsAt<T>>::put(next_term_ends_at);
+
+        if let Some(reward_source) = Self::council_mint() {
+            for seat in seats.iter() {
+                Self::add_reward_relationship(&seat.member, reward_source);
+            }
+        } else {
+            // Skip trying to create rewards since no mint has been created yet
+            debug::warn!(
+                "Not creating reward relationship for council seats because no mint exists"
+            );
+        }
+
         Self::deposit_event(RawEvent::NewCouncilTermStarted(next_term_ends_at));
     }
 }
@@ -75,6 +102,51 @@ impl<T: Trait> Module<T> {
         CouncilMint::<T>::put(mint_id);
         Ok(mint_id)
     }
+
+    fn add_reward_relationship(destination: &T::AccountId, reward_source: T::MintId) {
+        let recipient = <recurringrewards::Module<T>>::add_recipient();
+
+        // When calculating when first payout occurs, add minimum of one block interval to ensure rewards module
+        // has a chance to execute its on_finalize routine.
+        let next_payout_at = system::Module::<T>::block_number()
+            + Self::first_payout_after_reward_created()
+            + T::BlockNumber::one();
+
+        if let Ok(relationship_id) = <recurringrewards::Module<T>>::add_reward_relationship(
+            reward_source,
+            recipient,
+            destination.clone(),
+            Self::amount_per_payout(),
+            next_payout_at,
+            Self::payout_interval(),
+        ) {
+            RewardRelationships::<T>::insert(destination, relationship_id);
+        } else {
+            debug::warn!("Failed to create a reward relationship for council seat");
+        }
+    }
+
+    fn remove_reward_relationships() {
+        for seat in Self::active_council().into_iter() {
+            if RewardRelationships::<T>::exists(&seat.member) {
+                let id = Self::reward_relationships(&seat.member);
+                <recurringrewards::Module<T>>::remove_reward_relationship(id);
+            }
+        }
+    }
+
+    fn on_term_ended(now: T::BlockNumber) {
+        // Stop paying out rewards when the term ends.
+        // Note: Is it not simpler to just do a single payout at end of term?
+        // During the term the recurring reward module could unfairly pay some but not all council members
+        // If there is insufficient mint capacity.. so doing it at this point offers more control
+        // and a potentially more fair outcome in such a case.
+        Self::remove_reward_relationships();
+
+        Self::deposit_event(RawEvent::CouncilTermEnded(now));
+
+        T::CouncilTermEnded::council_term_ended();
+    }
 }
 
 decl_module! {
@@ -83,16 +155,28 @@ decl_module! {
 
         fn on_finalize(now: T::BlockNumber) {
             if now == Self::term_ends_at() {
-                Self::deposit_event(RawEvent::CouncilTermEnded(now));
-                T::CouncilTermEnded::council_term_ended();
+                Self::on_term_ended(now);
             }
         }
 
         // Privileged methods
 
-        /// Force set a zero staked council. Stakes in existing council will vanish into thin air!
+        /// Force set a zero staked council. Stakes in existing council seats are not returned.
+        /// Existing council rewards are removed and new council members do NOT get any rewards.
+        /// Avoid using this call if possible, will be deprecated. The term of the new council is
+        /// not extended.
         pub fn set_council(origin, accounts: Vec<T::AccountId>) {
             ensure_root(origin)?;
+
+            // Council is being replaced so remove existing reward relationships if they exist
+            Self::remove_reward_relationships();
+
+            if let Some(reward_source) = Self::council_mint() {
+                for account in accounts.clone() {
+                    Self::add_reward_relationship(&account, reward_source);
+                }
+            }
+
             let new_council: Seats<T::AccountId, BalanceOf<T>> = accounts.into_iter().map(|account| {
                 Seat {
                     member: account,
@@ -100,13 +184,20 @@ decl_module! {
                     backers: vec![]
                 }
             }).collect();
+
             <ActiveCouncil<T>>::put(new_council);
         }
 
-        /// Adds a zero staked council member
+        /// Adds a zero staked council member. A member added in this way does not get a recurring reward.
         fn add_council_member(origin, account: T::AccountId) {
             ensure_root(origin)?;
+
             ensure!(!Self::is_councilor(&account), "cannot add same account multiple times");
+
+            if let Some(reward_source) = Self::council_mint() {
+                Self::add_reward_relationship(&account, reward_source);
+            }
+
             let seat = Seat {
                 member: account,
                 stake: BalanceOf::<T>::zero(),
@@ -117,13 +208,22 @@ decl_module! {
             <ActiveCouncil<T>>::mutate(|council| council.push(seat));
         }
 
+        /// Remove a single council member and their reward.
         fn remove_council_member(origin, account_to_remove: T::AccountId) {
             ensure_root(origin)?;
+
             ensure!(Self::is_councilor(&account_to_remove), "account is not a councilor");
+
+            if RewardRelationships::<T>::exists(&account_to_remove) {
+                let relationship_id = Self::reward_relationships(&account_to_remove);
+                <recurringrewards::Module<T>>::remove_reward_relationship(relationship_id);
+            }
+
             let filtered_council: Seats<T::AccountId, BalanceOf<T>> = Self::active_council()
                 .into_iter()
                 .filter(|c| c.member != account_to_remove)
                 .collect();
+
             <ActiveCouncil<T>>::put(filtered_council);
         }
 
@@ -153,7 +253,26 @@ decl_module! {
             if let Some(mint_id) = Self::council_mint() {
                 minting::Module::<T>::transfer_tokens(mint_id, amount, &destination)?;
             } else {
-                return Err("CouncilHashNoMint")
+                return Err("CouncilHasNoMint")
+            }
+        }
+
+        /// Sets the council rewards which is only applied on new council being elected.
+        fn set_council_rewards(
+            origin,
+            amount_per_payout: minting::BalanceOf<T>,
+            payout_interval: Option<T::BlockNumber>,
+            first_payout_after_reward_created: T::BlockNumber
+        ) {
+            ensure_root(origin)?;
+
+            AmountPerPayout::<T>::put(amount_per_payout);
+            FirstPayoutAfterRewardCreated::<T>::put(first_payout_after_reward_created);
+
+            if let Some(payout_interval) = payout_interval {
+                PayoutInterval::<T>::put(payout_interval);
+            } else {
+                PayoutInterval::<T>::take();
             }
         }
     }
@@ -161,6 +280,7 @@ decl_module! {
 
 #[cfg(test)]
 mod tests {
+    use super::*;
     use crate::mock::*;
     use srml_support::*;
 
@@ -212,4 +332,44 @@ mod tests {
             assert!(Council::is_councilor(&6));
         });
     }
+
+    #[test]
+    fn council_elected_test() {
+        initial_test_ext().execute_with(|| {
+            // Ensure a mint is created so we can create rewards
+            assert_ok!(Council::set_council_mint_capacity(
+                system::RawOrigin::Root.into(),
+                1000
+            ));
+
+            Council::council_elected(
+                vec![
+                    Seat {
+                        member: 5,
+                        stake: 0,
+                        backers: vec![],
+                    },
+                    Seat {
+                        member: 6,
+                        stake: 0,
+                        backers: vec![],
+                    },
+                    Seat {
+                        member: 7,
+                        stake: 0,
+                        backers: vec![],
+                    },
+                ],
+                50 as u64, // <Test as system::Trait>::BlockNumber::from(50)
+            );
+
+            assert!(Council::is_councilor(&5));
+            assert!(Council::is_councilor(&6));
+            assert!(Council::is_councilor(&7));
+
+            assert!(RewardRelationships::<Test>::exists(&5));
+            assert!(RewardRelationships::<Test>::exists(&6));
+            assert!(RewardRelationships::<Test>::exists(&7));
+        });
+    }
 }

+ 5 - 0
runtime-modules/governance/src/mock.rs

@@ -74,6 +74,11 @@ impl minting::Trait for Test {
     type Currency = Balances;
     type MintId = u64;
 }
+impl recurringrewards::Trait for Test {
+    type PayoutStatusHandler = ();
+    type RecipientId = u64;
+    type RewardRelationshipId = u64;
+}
 parameter_types! {
     pub const ExistentialDeposit: u32 = 0;
     pub const TransferFee: u32 = 0;

+ 2 - 2
runtime-modules/proposals/codex/src/tests/mock.rs

@@ -141,8 +141,8 @@ parameter_types! {
 impl proposal_discussion::Trait for Test {
     type Event = ();
     type PostAuthorOriginValidator = ();
-    type ThreadId = u32;
-    type PostId = u32;
+    type ThreadId = u64;
+    type PostId = u64;
     type MaxPostEditionNumber = MaxPostEditionNumber;
     type ThreadTitleLengthLimit = ThreadTitleLengthLimit;
     type PostLengthLimit = PostLengthLimit;

+ 4 - 4
runtime-modules/proposals/discussion/src/lib.rs

@@ -95,10 +95,10 @@ pub trait Trait: system::Trait + membership::members::Trait {
     >;
 
     /// Discussion thread Id type
-    type ThreadId: From<u32> + Into<u32> + Parameter + Default + Copy;
+    type ThreadId: From<u64> + Into<u64> + Parameter + Default + Copy;
 
     /// Post Id type
-    type PostId: From<u32> + Parameter + Default + Copy;
+    type PostId: From<u64> + Parameter + Default + Copy;
 
     /// Defines post edition number limit.
     type MaxPostEditionNumber: Get<u32>;
@@ -166,14 +166,14 @@ decl_storage! {
             Thread<MemberId<T>, T::BlockNumber>;
 
         /// Count of all threads that have been created.
-        pub ThreadCount get(fn thread_count): u32;
+        pub ThreadCount get(fn thread_count): u64;
 
         /// Map thread id and post id to corresponding post.
         pub PostThreadIdByPostId: double_map T::ThreadId, twox_128(T::PostId) =>
              Post<MemberId<T>, T::BlockNumber, T::ThreadId>;
 
         /// Count of all posts that have been created.
-        pub PostCount get(fn post_count): u32;
+        pub PostCount get(fn post_count): u64;
 
         /// Last author thread counter (part of the antispam mechanism)
         pub LastThreadAuthorCounter get(fn last_thread_author_counter):

+ 2 - 2
runtime-modules/proposals/discussion/src/tests/mock.rs

@@ -87,8 +87,8 @@ impl membership::members::Trait for Test {
 impl crate::Trait for Test {
     type Event = TestEvent;
     type PostAuthorOriginValidator = ();
-    type ThreadId = u32;
-    type PostId = u32;
+    type ThreadId = u64;
+    type PostId = u64;
     type MaxPostEditionNumber = MaxPostEditionNumber;
     type ThreadTitleLengthLimit = ThreadTitleLengthLimit;
     type PostLengthLimit = PostLengthLimit;

+ 10 - 10
runtime-modules/proposals/discussion/src/tests/mod.rs

@@ -8,7 +8,7 @@ use system::{EventRecord, Phase};
 
 struct EventFixture;
 impl EventFixture {
-    fn assert_events(expected_raw_events: Vec<RawEvent<u32, u64, u32>>) {
+    fn assert_events(expected_raw_events: Vec<RawEvent<u64, u64, u64>>) {
         let expected_events = expected_raw_events
             .iter()
             .map(|ev| EventRecord {
@@ -23,13 +23,13 @@ impl EventFixture {
 }
 
 struct TestPostEntry {
-    pub post_id: u32,
+    pub post_id: u64,
     pub text: Vec<u8>,
     pub edition_number: u32,
 }
 
 struct TestThreadEntry {
-    pub thread_id: u32,
+    pub thread_id: u64,
     pub title: Vec<u8>,
 }
 
@@ -81,7 +81,7 @@ impl DiscussionFixture {
         DiscussionFixture { title, ..self }
     }
 
-    fn create_discussion_and_assert(&self, result: Result<u32, Error>) -> Option<u32> {
+    fn create_discussion_and_assert(&self, result: Result<u64, Error>) -> Option<u64> {
         let create_discussion_result =
             Discussions::create_thread(self.author_id, self.title.clone());
 
@@ -94,13 +94,13 @@ impl DiscussionFixture {
 struct PostFixture {
     pub text: Vec<u8>,
     pub origin: RawOrigin<u64>,
-    pub thread_id: u32,
-    pub post_id: Option<u32>,
+    pub thread_id: u64,
+    pub post_id: Option<u64>,
     pub author_id: u64,
 }
 
 impl PostFixture {
-    fn default_for_thread(thread_id: u32) -> Self {
+    fn default_for_thread(thread_id: u64) -> Self {
         PostFixture {
             text: b"text".to_vec(),
             author_id: 1,
@@ -122,18 +122,18 @@ impl PostFixture {
         PostFixture { author_id, ..self }
     }
 
-    fn change_thread_id(self, thread_id: u32) -> Self {
+    fn change_thread_id(self, thread_id: u64) -> Self {
         PostFixture { thread_id, ..self }
     }
 
-    fn change_post_id(self, post_id: u32) -> Self {
+    fn change_post_id(self, post_id: u64) -> Self {
         PostFixture {
             post_id: Some(post_id),
             ..self
         }
     }
 
-    fn add_post_and_assert(&mut self, result: Result<(), Error>) -> Option<u32> {
+    fn add_post_and_assert(&mut self, result: Result<(), Error>) -> Option<u64> {
         let add_post_result = Discussions::add_post(
             self.origin.clone().into(),
             self.author_id,

+ 21 - 3
runtime/src/lib.rs

@@ -94,6 +94,22 @@ pub type Moment = u64;
 /// Credential type
 pub type Credential = u64;
 
+/// Represents a thread identifier for both Forum and Proposals Discussion
+///
+/// Note: Both modules expose type names ThreadId and PostId (which are defined on their Trait) and
+/// used in state storage and dispatchable method's argument types,
+/// and are therefore part of the public API/metadata of the runtime.
+/// In the currenlty version the polkadot-js/api that is used and is compatible with the runtime,
+/// the type registry has flat namespace and its not possible
+/// to register identically named types from different modules, separately. And so we MUST configure
+/// the underlying types to be identicaly to avoid issues with encoding/decoding these types on the client side.
+pub type ThreadId = u64;
+
+/// Represents a post identifier for both Forum and Proposals Discussion
+///
+/// See the Note about ThreadId
+pub type PostId = u64;
+
 /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know
 /// the specifics of the runtime. They can then be made to be agnostic over specific formats
 /// of data like extrinsics, allowing for them to continue syncing the network through upgrades
@@ -176,7 +192,7 @@ pub fn native_version() -> NativeVersion {
 
 parameter_types! {
     pub const BlockHashCount: BlockNumber = 250;
-    pub const MaximumBlockWeight: Weight = 1_000_000;
+    pub const MaximumBlockWeight: Weight = 1_000_000_000;
     pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75);
     pub const MaximumBlockLength: u32 = 5 * 1024 * 1024;
     pub const Version: RuntimeVersion = VERSION;
@@ -784,6 +800,8 @@ impl forum::ForumUserRegistry<AccountId> for ShimMembershipRegistry {
 impl forum::Trait for Runtime {
     type Event = Event;
     type MembershipRegistry = ShimMembershipRegistry;
+    type ThreadId = ThreadId;
+    type PostId = PostId;
 }
 
 impl migration::Trait for Runtime {
@@ -845,8 +863,8 @@ parameter_types! {
 impl proposals_discussion::Trait for Runtime {
     type Event = Event;
     type PostAuthorOriginValidator = MembershipOriginValidator<Self>;
-    type ThreadId = u32;
-    type PostId = u32;
+    type ThreadId = ThreadId;
+    type PostId = PostId;
     type MaxPostEditionNumber = ProposalMaxPostEditionNumber;
     type ThreadTitleLengthLimit = ProposalThreadTitleLengthLimit;
     type PostLengthLimit = ProposalPostLengthLimit;

+ 1 - 36
runtime/src/migration.rs

@@ -2,7 +2,6 @@
 #![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design
 
 use crate::VERSION;
-use common::currency::BalanceOf;
 use rstd::prelude::*;
 use sr_primitives::{print, traits::Zero};
 use srml_support::{debug, decl_event, decl_module, decl_storage};
@@ -32,42 +31,8 @@ impl<T: Trait> Module<T> {
             );
         }
 
-        // Reset working group mint capacity
-        if let Err(err) = content_working_group::Module::<T>::set_mint_capacity(
-            system::RawOrigin::Root.into(),
-            minting::BalanceOf::<T>::zero(),
-        ) {
-            debug::warn!(
-                "Failed to reset mint for working group during migration: {:?}",
-                err
-            );
-        }
-
-        // Set Storage Role reward to zero
-        if let Some(parameters) =
-            roles::actors::Parameters::<T>::get(roles::actors::Role::StorageProvider)
-        {
-            if let Err(err) = roles::actors::Module::<T>::set_role_parameters(
-                system::RawOrigin::Root.into(),
-                roles::actors::Role::StorageProvider,
-                roles::actors::RoleParameters {
-                    reward: BalanceOf::<T>::zero(),
-                    ..parameters
-                },
-            ) {
-                debug::warn!(
-                    "Failed to set zero reward for storage role during migration: {:?}",
-                    err
-                );
-            }
-        }
-
+        // Initialise the proposal system various periods
         proposals_codex::Module::<T>::set_default_config_values();
-
-        Self::deposit_event(RawEvent::Migrated(
-            <system::Module<T>>::block_number(),
-            VERSION.spec_version,
-        ));
     }
 }
 

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

@@ -21,4 +21,6 @@ COUNCIL_MINTING_CAPACITY = 100000
 # Stake amount for Rome runtime upgrade proposal
 RUNTIME_UPGRADE_PROPOSAL_STAKE = 100000
 # Validator count increment for Validator count test.
-VALIDATOR_COUNT_INCREMENT = 2
+VALIDATOR_COUNT_INCREMENT = 2
+# Constantinople runtime path
+RUNTIME_WASM_PATH = ../../target/release/wbuild/joystream-node-runtime/joystream_node_runtime.compact.wasm

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

@@ -4,14 +4,18 @@
   "license": "GPL-3.0-only",
   "scripts": {
     "build": "tsc --build tsconfig.json",
+<<<<<<< HEAD
     "test": "mocha -r ts-node/register src/tests/constantinople/proposals/* --reporter tap",
     "test-mocha": "mocha -r ts-node/register src/tests/constantinople/proposals/spendingProposalTest.ts",
+=======
+    "test": "mocha -r ts-node/register src/tests/constantinople/*",
+>>>>>>> development
     "test-migration": "mocha -r ts-node/register src/tests/rome/* && mocha -r ts-node/register src/tests/constantinople/*",
     "lint": "tslint --project tsconfig.json",
     "prettier": "prettier --write ./src"
   },
   "dependencies": {
-    "@joystream/types": "../joystream-apps/packages/joy-types",
+    "@joystream/types": "",
     "@rome/types@npm:@joystream/types": "^0.7.0",
     "@polkadot/api": "^0.96.1",
     "@polkadot/keyring": "^1.7.0-beta.5",

+ 4 - 4
tests/network-tests/src/tests/constantinople/electingCouncilTest.ts

@@ -1,13 +1,13 @@
 import { membershipTest } from './membershipCreationTest';
 import { KeyringPair } from '@polkadot/keyring/types';
-import { ApiWrapper } from '../../utils/apiWrapper';
+import { ApiWrapper } from './utils/apiWrapper';
 import { WsProvider, Keyring } from '@polkadot/api';
-import { initConfig } from '../../utils/config';
+import { initConfig } from './utils/config';
 import BN = require('bn.js');
 import { registerJoystreamTypes, Seat } from '@joystream/types';
 import { assert } from 'chai';
 import { v4 as uuid } from 'uuid';
-import { Utils } from '../../utils/utils';
+import { Utils } from './utils/utils';
 
 export function councilTest(m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[]) {
   initConfig();
@@ -118,7 +118,7 @@ export function councilTest(m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[]
   });
 }
 
-describe.skip('Council integration tests', () => {
+describe('Council integration tests', () => {
   const m1KeyPairs: KeyringPair[] = new Array();
   const m2KeyPairs: KeyringPair[] = new Array();
   membershipTest(m1KeyPairs);

+ 2 - 2
tests/network-tests/src/tests/constantinople/membershipCreationTest.ts

@@ -4,8 +4,8 @@ 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 { ApiWrapper } from './utils/apiWrapper';
+import { initConfig } from './utils/config';
 import { v4 as uuid } from 'uuid';
 
 export function membershipTest(nKeyPairs: KeyringPair[]) {

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/electionParametersProposalTest.ts

@@ -1,10 +1,10 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 import { assert } from 'chai';

+ 3 - 3
tests/network-tests/src/tests/constantinople/proposals/evictStoraveProviderTest.ts

@@ -1,14 +1,14 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 import { assert } from 'chai';
-import { Utils } from '../../../utils/utils';
+import { Utils } from '../utils/utils';
 
 export function evictStorageProviderTest(m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[]) {
   initConfig();

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/setLeadProposalTest.ts

@@ -1,10 +1,10 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 import { assert } from 'chai';

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/spendingProposalTest.ts

@@ -1,10 +1,10 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 import { assert } from 'chai';

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/storageRoleParametersProposalTest.ts

@@ -1,10 +1,10 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 import { assert } from 'chai';

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/textProposalTest.ts

@@ -1,10 +1,10 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/updateRuntimeTest.ts

@@ -1,11 +1,11 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/validatorCountProposal.ts

@@ -1,10 +1,10 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 import { assert } from 'chai';

+ 2 - 2
tests/network-tests/src/tests/constantinople/proposals/workingGroupMintCapacityProposalTest.ts

@@ -1,10 +1,10 @@
-import { initConfig } from '../../../utils/config';
+import { initConfig } from '../utils/config';
 import { Keyring, WsProvider } from '@polkadot/api';
 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 { ApiWrapper } from '../utils/apiWrapper';
 import { v4 as uuid } from 'uuid';
 import BN = require('bn.js');
 import { assert } from 'chai';

+ 0 - 0
tests/network-tests/src/utils/apiWrapper.ts → tests/network-tests/src/tests/constantinople/utils/apiWrapper.ts


+ 0 - 0
tests/network-tests/src/utils/config.ts → tests/network-tests/src/tests/constantinople/utils/config.ts


+ 0 - 0
tests/network-tests/src/utils/sender.ts → tests/network-tests/src/tests/constantinople/utils/sender.ts


+ 0 - 0
tests/network-tests/src/utils/utils.ts → tests/network-tests/src/tests/constantinople/utils/utils.ts


+ 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);
+});

+ 75 - 0
tests/network-tests/src/tests/rome/romeRuntimeUpgradeTest.ts

@@ -0,0 +1,75 @@
+import { initConfig } from './utils/config';
+import { Keyring, WsProvider } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { membershipTest } from './membershipCreationTest';
+import { councilTest } from './electingCouncilTest';
+import { registerJoystreamTypes } from '@rome/types';
+import { ApiWrapper } from './utils/apiWrapper';
+import BN = require('bn.js');
+import { Utils } from './utils/utils';
+
+describe('Runtime upgrade integration tests', () => {
+  initConfig();
+  const keyring = new Keyring({ type: 'sr25519' });
+  const nodeUrl: string = process.env.NODE_URL!;
+  const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+  const proposalStake: BN = new BN(+process.env.RUNTIME_UPGRADE_PROPOSAL_STAKE!);
+  const runtimePath: string = process.env.RUNTIME_WASM_PATH!;
+  const defaultTimeout: number = 180000;
+
+  const m1KeyPairs: KeyringPair[] = new Array();
+  const m2KeyPairs: KeyringPair[] = new Array();
+
+  let apiWrapper: ApiWrapper;
+  let sudo: KeyringPair;
+  let provider: WsProvider;
+
+  before(async function () {
+    this.timeout(defaultTimeout);
+    registerJoystreamTypes();
+    provider = new WsProvider(nodeUrl);
+    apiWrapper = await ApiWrapper.create(provider);
+  });
+
+  membershipTest(m1KeyPairs);
+  membershipTest(m2KeyPairs);
+  councilTest(m1KeyPairs, m2KeyPairs);
+
+  it('Upgrading the runtime test', async () => {
+    // Setup
+    sudo = keyring.addFromUri(sudoUri);
+    const runtime: string = Utils.readRuntimeFromFile(runtimePath);
+    const description: string = 'runtime upgrade proposal which is used for API integration testing';
+    const runtimeProposalFee: BN = apiWrapper.estimateProposeRuntimeUpgradeFee(
+      proposalStake,
+      description,
+      description,
+      runtime
+    );
+    const runtimeVoteFee: BN = apiWrapper.estimateVoteForRuntimeProposalFee();
+
+    // Topping the balances
+    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',
+      'runtime to test proposal functionality',
+      runtime
+    );
+    const proposalNumber = await proposalPromise;
+
+    // Approving runtime update proposal
+    const runtimePromise = apiWrapper.expectRuntimeUpgraded();
+    await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber);
+    await runtimePromise;
+  }).timeout(defaultTimeout);
+
+  after(() => {
+    apiWrapper.close();
+  });
+});

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

@@ -0,0 +1,370 @@
+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: 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 estimateVoteForRuntimeProposalFee(): 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 proposeRuntime(
+    account: KeyringPair,
+    stake: BN,
+    name: string,
+    description: string,
+    runtime: 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, proposal: BN): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.proposals.voteOnProposal(proposal, new VoteKind('Approve')),
+      account,
+      false
+    );
+  }
+
+  public batchApproveProposal(council: KeyringPair[], proposal: BN): Promise<void[]> {
+    return Promise.all(
+      council.map(async keyPair => {
+        await this.approveProposal(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 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 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>();
+  }
+}

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

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

+ 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);
+        });
+    });
+  }
+}

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

@@ -0,0 +1,50 @@
+import { IExtrinsic } from '@polkadot/types/types';
+import { compactToU8a, stringToU8a, u8aToHex } 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 u8aToHex(fs.readFileSync(path));
+  }
+}