Browse Source

Merge branch 'development' into data-object-type-registry

Jens Finkhaeuser 6 years ago
parent
commit
64b79d33fd

+ 2 - 2
Cargo.toml

@@ -75,11 +75,11 @@ rev = 'df5e65927780b323482e2e8b5031822f423a032d'
 
 [dependencies.parity-codec]
 default-features = false
-version = '3.0'
+version = '3.1'
 
 [dependencies.parity-codec-derive]
 default-features = false
-version = '3.0'
+version = '3.1'
 
 [dependencies.primitives]
 default_features = false

+ 9 - 5
src/governance/election.rs

@@ -17,10 +17,14 @@ use super::sealed_vote::SealedVote;
 pub use super::{ GovernanceCurrency, BalanceOf };
 use super::council;
 
+use crate::traits::{IsActiveMember};
+
 pub trait Trait: system::Trait + council::Trait + GovernanceCurrency {
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
 
     type CouncilElected: CouncilElected<Seats<Self::AccountId, BalanceOf<Self>>, Self::BlockNumber>;
+
+    type IsActiveMember: IsActiveMember<Self>;
 }
 
 #[derive(Clone, Copy, Encode, Decode)]
@@ -148,9 +152,9 @@ impl<T: Trait> Module<T> {
         <system::Module<T>>::block_number() + length
     }
 
-    // TODO This method should be moved to Membership module once it's created.
-    fn is_member(sender: T::AccountId) -> bool {
-        !T::Currency::free_balance(&sender).is_zero()
+
+    fn can_participate(sender: &T::AccountId) -> bool {
+        !T::Currency::free_balance(sender).is_zero() && T::IsActiveMember::is_active_member(sender)
     }
 
     // PUBLIC IMMUTABLES
@@ -651,7 +655,7 @@ decl_module! {
         // Member can make subsequent calls during announcing stage to increase their stake.
         fn apply(origin, stake: BalanceOf<T>) {
             let sender = ensure_signed(origin)?;
-            ensure!(Self::is_member(sender.clone()), "Only members can apply to be on council");
+            ensure!(Self::can_participate(&sender), "Only members can apply to be on council");
 
             let stage = Self::stage();
             ensure!(Self::stage().is_some(), "election not running");
@@ -674,7 +678,7 @@ decl_module! {
 
         fn vote(origin, commitment: T::Hash, stake: BalanceOf<T>) {
             let sender = ensure_signed(origin)?;
-            ensure!(Self::is_member(sender.clone()), "Only members can vote for an applicant");
+            ensure!(Self::can_participate(&sender), "Only members can vote for an applicant");
 
             let stage = Self::stage();
             ensure!(Self::stage().is_some(), "election not running");

+ 14 - 4
src/governance/mock.rs

@@ -3,6 +3,7 @@
 use rstd::prelude::*;
 pub use super::{election, council, proposals, GovernanceCurrency};
 pub use system;
+use crate::traits;
 
 pub use primitives::{H256, Blake2Hasher};
 pub use runtime_primitives::{
@@ -17,6 +18,16 @@ impl_outer_origin! {
     pub enum Origin for Test {}
 }
 
+pub struct AnyAccountIsMember {}
+impl<T: system::Trait> traits::IsActiveMember<T> for AnyAccountIsMember {
+    fn is_active_member(who: &T::AccountId) -> bool {
+        true
+    }
+}
+
+// default trait implementation - any account is not a member
+// impl<T: system::Trait> traits::IsActiveMember<T> for () {}
+
 // For testing the module, we construct most of a mock runtime. This means
 // first constructing a configuration type (`Test`) which `impl`s each of the
 // configuration traits of modules we want to use.
@@ -53,10 +64,10 @@ impl election::Trait for Test {
     type Event = ();
 
     type CouncilElected = (Council,);
+
+    type IsActiveMember = AnyAccountIsMember;
 }
-impl proposals::Trait for Test {
-    type Event = ();
-}
+
 impl balances::Trait for Test {
     type Event = ();
 
@@ -92,6 +103,5 @@ pub fn initial_test_ext() -> runtime_io::TestExternalities<Blake2Hasher> {
 
 pub type Election = election::Module<Test>;
 pub type Council = council::Module<Test>;
-pub type Proposals = proposals::Module<Test>;
 pub type System = system::Module<Test>;
 pub type Balances = balances::Module<Test>;

+ 14 - 4
src/governance/proposals.rs

@@ -8,6 +8,7 @@ use rstd::prelude::*;
 
 use super::council;
 pub use super::{ GovernanceCurrency, BalanceOf };
+use crate::traits::{IsActiveMember};
 
 const DEFAULT_APPROVAL_QUORUM: u32 = 60;
 const DEFAULT_MIN_STAKE: u64 = 100;
@@ -115,6 +116,8 @@ pub struct TallyResult<BlockNumber> {
 pub trait Trait: timestamp::Trait + council::Trait + GovernanceCurrency {
     /// The overarching event type.
     type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    type IsActiveMember: IsActiveMember<Self>;
 }
 
 decl_event!(
@@ -221,7 +224,7 @@ decl_module! {
         ) {
 
             let proposer = ensure_signed(origin)?;
-            ensure!(Self::is_member(proposer.clone()), MSG_ONLY_MEMBERS_CAN_PROPOSE);
+            ensure!(Self::can_participate(proposer.clone()), MSG_ONLY_MEMBERS_CAN_PROPOSE);
             ensure!(stake >= Self::min_stake(), MSG_STAKE_IS_TOO_LOW);
 
             ensure!(!name.is_empty(), MSG_EMPTY_NAME_PROVIDED);
@@ -345,9 +348,8 @@ impl<T: Trait> Module<T> {
         <system::Module<T>>::block_number()
     }
 
-    // TODO This method should be moved to Membership module once it's created.
-    fn is_member(sender: T::AccountId) -> bool {
-        !T::Currency::free_balance(&sender).is_zero()
+    fn can_participate(sender: T::AccountId) -> bool {
+        !T::Currency::free_balance(&sender).is_zero() && T::IsActiveMember::is_active_member(&sender)
     }
 
     fn is_councilor(sender: &T::AccountId) -> bool {
@@ -613,6 +615,14 @@ mod tests {
 
     impl Trait for Test {
         type Event = ();
+        type IsActiveMember = AnyAccountIsMember;
+    }
+
+    pub struct AnyAccountIsMember {}
+    impl<T: system::Trait> IsActiveMember<T> for AnyAccountIsMember {
+        fn is_active_member(who: &T::AccountId) -> bool {
+            true
+        }
     }
 
     type System = system::Module<Test>;

+ 19 - 1
src/lib.rs

@@ -20,6 +20,9 @@ pub mod storage;
 use storage::{data_object_type_registry};
 mod memo;
 mod traits;
+mod membership;
+use membership::members;
+mod migration;
 
 use rstd::prelude::*;
 #[cfg(feature = "std")]
@@ -90,7 +93,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
 	spec_name: create_runtime_str!("joystream-node"),
 	impl_name: create_runtime_str!("joystream-node"),
 	authoring_version: 3,
-	spec_version: 4,
+	spec_version: 5,
 	impl_version: 0,
 	apis: RUNTIME_API_VERSIONS,
 };
@@ -211,11 +214,13 @@ impl governance::GovernanceCurrency for Runtime {
 
 impl governance::proposals::Trait for Runtime {
 	type Event = Event;
+	type IsActiveMember = Members;
 }
 
 impl governance::election::Trait for Runtime {
 	type Event = Event;
 	type CouncilElected = (Council,);
+	type IsActiveMember = Members;
 }
 
 impl governance::council::Trait for Runtime {
@@ -232,6 +237,17 @@ impl storage::data_object_type_registry::Trait for Runtime {
 	type DataObjectTypeID = u64;
 }
 
+impl members::Trait for Runtime {
+	type Event = Event;
+	type MemberId = u64;
+	type PaidTermId = u64;
+	type SubscriptionId = u64;
+}
+
+impl migration::Trait for Runtime {
+	type Event = Event;
+}
+
 construct_runtime!(
 	pub enum Runtime with Log(InternalLog: DigestItem<Hash, Ed25519AuthorityId>) where
 		Block = Block,
@@ -252,6 +268,8 @@ construct_runtime!(
 		CouncilElection: election::{Module, Call, Storage, Event<T>, Config<T>},
 		Council: council::{Module, Call, Storage, Event<T>, Config<T>},
 		Memo: memo::{Module, Call, Storage, Event<T>},
+		Members: members::{Module, Call, Storage, Event<T>, Config<T>},
+		Migration: migration::{Module, Call, Storage, Event<T>},
 		DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event<T>, Config<T>},
 	}
 );

+ 411 - 0
src/membership/members.rs

@@ -0,0 +1,411 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use rstd::prelude::*;
+use parity_codec::Codec;
+use parity_codec_derive::{Encode, Decode};
+use srml_support::{StorageMap, StorageValue, dispatch, decl_module, decl_storage, decl_event, ensure, Parameter};
+use srml_support::traits::{Currency};
+use runtime_primitives::traits::{Zero, SimpleArithmetic, As, Member, MaybeSerializeDebug};
+use system::{self, ensure_signed};
+use crate::governance::{GovernanceCurrency, BalanceOf };
+use {timestamp};
+use crate::traits;
+
+pub trait Trait: system::Trait + GovernanceCurrency + timestamp::Trait {
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+
+    type MemberId: Parameter + Member + SimpleArithmetic + Codec + Default + Copy
+        + As<usize> + As<u64> + MaybeSerializeDebug + PartialEq;
+
+    type PaidTermId: Parameter + Member + SimpleArithmetic + Codec + Default + Copy
+        + As<usize> + As<u64> + MaybeSerializeDebug + PartialEq;
+
+    type SubscriptionId: Parameter + Member + SimpleArithmetic + Codec + Default + Copy
+        + As<usize> + As<u64> + MaybeSerializeDebug + PartialEq;
+}
+
+const DEFAULT_FIRST_MEMBER_ID: u64 = 1;
+const FIRST_PAID_TERMS_ID: u64 = 1;
+
+// Default paid membership terms
+const DEFAULT_PAID_TERM_ID: u64 = 0;
+const DEFAULT_PAID_TERM_FEE: u64 = 100; // Can be overidden in genesis config
+const DEFAULT_PAID_TERM_TEXT: &str = "Default Paid Term TOS...";
+
+// Default user info constraints
+const DEFAULT_MIN_HANDLE_LENGTH: u32 = 5;
+const DEFAULT_MAX_HANDLE_LENGTH: u32 = 40;
+const DEFAULT_MAX_AVATAR_URI_LENGTH: u32 = 1024;
+const DEFAULT_MAX_ABOUT_TEXT_LENGTH: u32 = 2048;
+
+//#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode)]
+/// Stored information about a registered user
+pub struct Profile<T: Trait> {
+    pub id: T::MemberId,
+    pub handle: Vec<u8>,
+    pub avatar_uri: Vec<u8>,
+    pub about: Vec<u8>,
+    pub registered_at_block: T::BlockNumber,
+    pub registered_at_time: T::Moment,
+    pub entry: EntryMethod<T>,
+    pub suspended: bool,
+    pub subscription: Option<T::SubscriptionId>,
+}
+
+#[derive(Clone, Debug, Encode, Decode, PartialEq)]
+/// Structure used to batch user configurable profile information when registering or updating info
+pub struct UserInfo {
+    pub handle: Option<Vec<u8>>,
+    pub avatar_uri: Option<Vec<u8>>,
+    pub about: Option<Vec<u8>>,
+}
+
+struct CheckedUserInfo {
+    handle: Vec<u8>,
+    avatar_uri: Vec<u8>,
+    about: Vec<u8>,
+}
+
+//#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Debug, PartialEq)]
+pub enum EntryMethod<T: Trait> {
+    Paid(T::PaidTermId),
+    Screening(T::AccountId),
+}
+
+//#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
+#[derive(Encode, Decode, Eq, PartialEq)]
+pub struct PaidMembershipTerms<T: Trait> {
+    /// Unique identifier - the term id
+    pub id: T::PaidTermId,
+    /// Quantity of native tokens which must be provably burned
+    pub fee: BalanceOf<T>,
+    /// String of capped length describing human readable conditions which are being agreed upon
+    pub text: Vec<u8>
+}
+
+impl<T: Trait> Default for PaidMembershipTerms<T> {
+    fn default() -> Self {
+        PaidMembershipTerms {
+            id: T::PaidTermId::sa(DEFAULT_PAID_TERM_ID),
+            fee: BalanceOf::<T>::sa(DEFAULT_PAID_TERM_FEE),
+            text: DEFAULT_PAID_TERM_TEXT.as_bytes().to_vec()
+        }
+    }
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Membership {
+        /// MemberId's start at this value
+        pub FirstMemberId get(first_member_id) config(first_member_id): T::MemberId = T::MemberId::sa(DEFAULT_FIRST_MEMBER_ID);
+
+        /// MemberId to assign to next member that is added to the registry
+        pub NextMemberId get(next_member_id) build(|config: &GenesisConfig<T>| config.first_member_id): T::MemberId = T::MemberId::sa(DEFAULT_FIRST_MEMBER_ID);
+
+        /// Mapping of member ids to their corresponding primary accountid
+        pub AccountIdByMemberId get(account_id_by_member_id) : map T::MemberId => T::AccountId;
+
+        /// Mapping of members' account ids to their member id.
+        pub MemberIdByAccountId get(member_id_by_account_id) : map T::AccountId => Option<T::MemberId>;
+
+        /// Mapping of member's id to their membership profile
+        // Value is Option<Profile> because it is not meaningful to have a Default value for Profile
+        pub MemberProfile get(member_profile) : map T::MemberId => Option<Profile<T>>;
+
+        /// Registered unique handles and their mapping to their owner
+        pub Handles get(handles) : map Vec<u8> => Option<T::MemberId>;
+
+        /// Next paid membership terms id
+        pub NextPaidMembershipTermsId get(next_paid_membership_terms_id) : T::PaidTermId = T::PaidTermId::sa(FIRST_PAID_TERMS_ID);
+
+        /// Paid membership terms record
+        // Remember to add _genesis_phantom_data: std::marker::PhantomData{} to membership
+        // genesis config if not providing config() or extra_genesis
+        pub PaidMembershipTermsById get(paid_membership_terms_by_id) build(|config: &GenesisConfig<T>| {
+            // This method only gets called when initializing storage, and is
+            // compiled as native code. (Will be called when building `raw` chainspec)
+            // So it can't be relied upon to initialize storage for runtimes updates.
+            // Initialization for updated runtime is done in run_migration()
+            let mut terms: PaidMembershipTerms<T> = Default::default();
+            terms.fee = config.default_paid_membership_fee;
+            vec![(terms.id, terms)]
+        }) : map T::PaidTermId => Option<PaidMembershipTerms<T>>;
+
+        /// Active Paid membership terms
+        pub ActivePaidMembershipTerms get(active_paid_membership_terms) : Vec<T::PaidTermId> = vec![T::PaidTermId::sa(DEFAULT_PAID_TERM_ID)];
+
+        /// Is the platform is accepting new members or not
+        pub NewMembershipsAllowed get(new_memberships_allowed) : bool = true;
+
+        pub ScreeningAuthority get(screening_authority) : Option<T::AccountId>;
+
+        // User Input Validation parameters - do these really need to be state variables
+        // I don't see a need to adjust these in future?
+        pub MinHandleLength get(min_handle_length) : u32 = DEFAULT_MIN_HANDLE_LENGTH;
+        pub MaxHandleLength get(max_handle_length) : u32 = DEFAULT_MAX_HANDLE_LENGTH;
+        pub MaxAvatarUriLength get(max_avatar_uri_length) : u32 = DEFAULT_MAX_AVATAR_URI_LENGTH;
+        pub MaxAboutTextLength get(max_about_text_length) : u32 = DEFAULT_MAX_ABOUT_TEXT_LENGTH;
+    }
+    add_extra_genesis {
+        config(default_paid_membership_fee): BalanceOf<T>;
+    }
+}
+
+decl_event! {
+    pub enum Event<T> where
+      <T as system::Trait>::AccountId,
+      <T as Trait>::MemberId {
+        MemberRegistered(MemberId, AccountId),
+        MemberUpdatedAboutText(MemberId),
+        MemberUpdatedAvatar(MemberId),
+        MemberUpdatedHandle(MemberId),
+    }
+}
+
+/// Initialization step that runs when the runtime is installed as a runtime upgrade
+/// This will and should ONLY be called from the migration module that keeps track of
+/// the store version!
+impl<T: Trait> Module<T> {
+    pub fn initialize_storage() {
+        let default_terms: PaidMembershipTerms<T> = Default::default();
+        <PaidMembershipTermsById<T>>::insert(default_terms.id, default_terms);
+    }
+}
+
+impl<T: Trait> traits::IsActiveMember<T> for Module<T> {
+    fn is_active_member(who: &T::AccountId) -> bool {
+        match Self::ensure_is_member(who)
+            .and_then(|member_id| Self::ensure_profile(member_id))
+        {
+            Ok(profile) => !profile.suspended,
+            Err(err) => false
+        }
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn deposit_event<T>() = default;
+
+        /// Non-members can buy membership
+        pub fn buy_membership(origin, paid_terms_id: T::PaidTermId, user_info: UserInfo) {
+            let who = ensure_signed(origin)?;
+
+            // make sure we are accepting new memberships
+            ensure!(Self::new_memberships_allowed(), "new members not allowed");
+
+            // ensure key not associated with an existing membership
+            Self::ensure_not_member(&who)?;
+
+            // ensure paid_terms_id is active
+            let terms = Self::ensure_active_terms_id(paid_terms_id)?;
+
+            // ensure enough free balance to cover terms fees
+            ensure!(T::Currency::can_slash(&who, terms.fee), "not enough balance to buy membership");
+
+            let user_info = Self::check_user_registration_info(user_info)?;
+
+            // ensure handle is not already registered
+            Self::ensure_unique_handle(&user_info.handle)?;
+
+            let _ = T::Currency::slash(&who, terms.fee);
+            let member_id = Self::insert_member(&who, &user_info, EntryMethod::Paid(paid_terms_id));
+
+            Self::deposit_event(RawEvent::MemberRegistered(member_id, who.clone()));
+        }
+
+        /// Change member's about text
+        pub fn change_member_about_text(origin, text: Vec<u8>) {
+            let who = ensure_signed(origin)?;
+            let member_id = Self::ensure_is_member_primary_account(who.clone())?;
+            Self::_change_member_about_text(member_id, &text)?;
+        }
+
+        /// Change member's avatar
+        pub fn change_member_avatar(origin, uri: Vec<u8>) {
+            let who = ensure_signed(origin)?;
+            let member_id = Self::ensure_is_member_primary_account(who.clone())?;
+            Self::_change_member_avatar(member_id, &uri)?;
+        }
+
+        /// Change member's handle. Will ensure new handle is unique and old one will be available
+        /// for other members to use.
+        pub fn change_member_handle(origin, handle: Vec<u8>) {
+            let who = ensure_signed(origin)?;
+            let member_id = Self::ensure_is_member_primary_account(who.clone())?;
+            Self::_change_member_handle(member_id, handle)?;
+        }
+
+        /// Update member's all or some of handle, avatar and about text.
+        pub fn update_profile(origin, user_info: UserInfo) {
+            let who = ensure_signed(origin)?;
+            let member_id = Self::ensure_is_member_primary_account(who.clone())?;
+
+            if let Some(uri) = user_info.avatar_uri {
+                Self::_change_member_avatar(member_id, &uri)?;
+            }
+            if let Some(about) = user_info.about {
+                Self::_change_member_about_text(member_id, &about)?;
+            }
+            if let Some(handle) = user_info.handle {
+                Self::_change_member_handle(member_id, handle)?;
+            }
+        }
+
+        pub fn add_screened_member(origin, new_member: T::AccountId, user_info: UserInfo) {
+            // ensure sender is screening authority
+            let sender = ensure_signed(origin)?;
+
+            if let Some(screening_authority) = Self::screening_authority() {
+                ensure!(sender == screening_authority, "not screener");
+            } else {
+                // no screening authority defined. Cannot accept this request
+                return Err("no screening authority defined");
+            }
+
+            // make sure we are accepting new memberships
+            ensure!(Self::new_memberships_allowed(), "new members not allowed");
+
+            // ensure key not associated with an existing membership
+            Self::ensure_not_member(&new_member)?;
+
+            let user_info = Self::check_user_registration_info(user_info)?;
+
+            // ensure handle is not already registered
+            Self::ensure_unique_handle(&user_info.handle)?;
+
+            let member_id = Self::insert_member(&new_member, &user_info, EntryMethod::Screening(sender));
+
+            Self::deposit_event(RawEvent::MemberRegistered(member_id, new_member.clone()));
+        }
+
+        pub fn set_screening_authority(authority: T::AccountId) {
+            <ScreeningAuthority<T>>::put(authority);
+        }
+    }
+}
+
+impl<T: Trait> Module<T> {
+    fn ensure_not_member(who: &T::AccountId) -> dispatch::Result {
+        ensure!(!<MemberIdByAccountId<T>>::exists(who), "account already associated with a membership");
+        Ok(())
+    }
+
+    fn ensure_is_member(who: &T::AccountId) -> Result<T::MemberId, &'static str> {
+        let member_id = Self::member_id_by_account_id(who).ok_or("no member id found for accountid")?;
+        Ok(member_id)
+    }
+
+    fn ensure_is_member_primary_account(who: T::AccountId) -> Result<T::MemberId, &'static str> {
+        let member_id = Self::ensure_is_member(&who)?;
+        ensure!(Self::account_id_by_member_id(member_id) == who, "not primary account");
+        Ok(member_id)
+    }
+
+    fn ensure_profile(id: T::MemberId) -> Result<Profile<T>, &'static str> {
+        let profile = Self::member_profile(&id).ok_or("member profile not found")?;
+        Ok(profile)
+    }
+
+    fn ensure_active_terms_id(terms_id: T::PaidTermId) -> Result<PaidMembershipTerms<T>, &'static str> {
+        let active_terms = Self::active_paid_membership_terms();
+        ensure!(active_terms.iter().any(|&id| id == terms_id), "paid terms id not active");
+        let terms = Self::paid_membership_terms_by_id(terms_id).ok_or("paid membership term id does not exist")?;
+        Ok(terms)
+    }
+
+    fn ensure_unique_handle(handle: &Vec<u8> ) -> dispatch::Result {
+        ensure!(!<Handles<T>>::exists(handle), "handle already registered");
+        Ok(())
+    }
+
+    fn validate_handle(handle: &Vec<u8>) -> dispatch::Result {
+        ensure!(handle.len() >= Self::min_handle_length() as usize, "handle too short");
+        ensure!(handle.len() <= Self::max_handle_length() as usize, "handle too long");
+        Ok(())
+    }
+
+    fn validate_text(text: &Vec<u8>) -> Vec<u8> {
+        let mut text = text.clone();
+        text.truncate(Self::max_about_text_length() as usize);
+        text
+    }
+
+    fn validate_avatar(uri: &Vec<u8>) -> dispatch::Result {
+        ensure!(uri.len() <= Self::max_avatar_uri_length() as usize, "avatar uri too long");
+        Ok(())
+    }
+
+    /// Basic user input validation
+    fn check_user_registration_info(user_info: UserInfo) -> Result<CheckedUserInfo, &'static str> {
+        // Handle is required during registration
+        let handle = user_info.handle.ok_or("handle must be provided during registration")?;
+        Self::validate_handle(&handle)?;
+
+        let about = Self::validate_text(&user_info.about.unwrap_or_default());
+        let avatar_uri = user_info.avatar_uri.unwrap_or_default();
+        Self::validate_avatar(&avatar_uri)?;
+
+        Ok(CheckedUserInfo {
+            handle,
+            avatar_uri,
+            about,
+        })
+    }
+
+    // Mutating methods
+    fn insert_member(who: &T::AccountId, user_info: &CheckedUserInfo, entry_method: EntryMethod<T>) -> T::MemberId {
+        let new_member_id = Self::next_member_id();
+
+        let profile: Profile<T> = Profile {
+            id: new_member_id,
+            handle: user_info.handle.clone(),
+            avatar_uri: user_info.avatar_uri.clone(),
+            about: user_info.about.clone(),
+            registered_at_block: <system::Module<T>>::block_number(),
+            registered_at_time: <timestamp::Module<T>>::now(),
+            entry: entry_method,
+            suspended: false,
+            subscription: None,
+        };
+
+        <MemberIdByAccountId<T>>::insert(who.clone(), new_member_id);
+        <AccountIdByMemberId<T>>::insert(new_member_id, who.clone());
+        <MemberProfile<T>>::insert(new_member_id, profile);
+        <Handles<T>>::insert(user_info.handle.clone(), new_member_id);
+        <NextMemberId<T>>::mutate(|n| { *n += T::MemberId::sa(1); });
+
+        new_member_id
+    }
+
+    fn _change_member_about_text (id: T::MemberId, text: &Vec<u8>) -> dispatch::Result {
+        let mut profile = Self::ensure_profile(id)?;
+        let text = Self::validate_text(text);
+        profile.about = text;
+        Self::deposit_event(RawEvent::MemberUpdatedAboutText(id));
+        <MemberProfile<T>>::insert(id, profile);
+        Ok(())
+    }
+
+    fn _change_member_avatar(id: T::MemberId, uri: &Vec<u8>) -> dispatch::Result {
+        let mut profile = Self::ensure_profile(id)?;
+        Self::validate_avatar(uri)?;
+        profile.avatar_uri = uri.clone();
+        Self::deposit_event(RawEvent::MemberUpdatedAvatar(id));
+        <MemberProfile<T>>::insert(id, profile);
+        Ok(())
+    }
+
+    fn _change_member_handle(id: T::MemberId, handle: Vec<u8>) -> dispatch::Result {
+        let mut profile = Self::ensure_profile(id)?;
+        Self::validate_handle(&handle)?;
+        Self::ensure_unique_handle(&handle)?;
+        <Handles<T>>::remove(&profile.handle);
+        <Handles<T>>::insert(handle.clone(), id);
+        profile.handle = handle;
+        Self::deposit_event(RawEvent::MemberUpdatedHandle(id));
+        <MemberProfile<T>>::insert(id, profile);
+        Ok(())
+    }
+}

+ 115 - 0
src/membership/mock.rs

@@ -0,0 +1,115 @@
+#![cfg(test)]
+
+use rstd::prelude::*;
+pub use crate::governance::{GovernanceCurrency};
+pub use super::{members};
+pub use system;
+
+pub use primitives::{H256, Blake2Hasher};
+pub use runtime_primitives::{
+    BuildStorage,
+    traits::{BlakeTwo256, OnFinalise, IdentityLookup},
+    testing::{Digest, DigestItem, Header, UintAuthorityId}
+};
+
+use srml_support::impl_outer_origin;
+
+impl_outer_origin! {
+    pub enum Origin for Test {}
+}
+
+// For testing the module, we construct most of a mock runtime. This means
+// first constructing a configuration type (`Test`) which `impl`s each of the
+// configuration traits of modules we want to use.
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub struct Test;
+impl system::Trait for Test {
+    type Origin = Origin;
+    type Index = u64;
+    type BlockNumber = u64;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type Digest = Digest;
+    type AccountId = u64;
+    type Header = Header;
+    type Event = ();
+    type Log = DigestItem;
+    type Lookup = IdentityLookup<u64>;
+}
+impl timestamp::Trait for Test {
+    type Moment = u64;
+    type OnTimestampSet = ();
+}
+impl consensus::Trait for Test {
+    type SessionKey = UintAuthorityId;
+    type InherentOfflineReport = ();
+    type Log = DigestItem;
+}
+
+impl balances::Trait for Test {
+    type Event = ();
+
+    /// The balance of an account.
+    type Balance = u32;
+
+    /// A function which is invoked when the free-balance has fallen below the existential deposit and
+    /// has been reduced to zero.
+    ///
+    /// Gives a chance to clean up resources associated with the given account.
+    type OnFreeBalanceZero = ();
+
+    /// Handler for when a new account is created.
+    type OnNewAccount = ();
+
+    /// A function that returns true iff a given account can transfer its funds to another account.
+    type EnsureAccountLiquid = ();
+}
+
+impl GovernanceCurrency for Test {
+    type Currency = balances::Module<Self>;
+}
+
+impl members::Trait for Test {
+    type Event = ();
+    type MemberId = u32;
+    type PaidTermId = u32;
+    type SubscriptionId = u32;
+}
+
+pub struct ExtBuilder {
+	first_member_id: u32,
+	default_paid_membership_fee: u32,
+}
+impl Default for ExtBuilder {
+	fn default() -> Self {
+		Self {
+			first_member_id: 1,
+			default_paid_membership_fee: 100,
+		}
+	}
+}
+
+impl ExtBuilder {
+	pub fn first_member_id(mut self, first_member_id: u32) -> Self {
+		self.first_member_id = first_member_id;
+		self
+	}
+	pub fn default_paid_membership_fee(mut self, default_paid_membership_fee: u32) -> Self {
+		self.default_paid_membership_fee = default_paid_membership_fee;
+		self
+	}
+    pub fn build(self) -> runtime_io::TestExternalities<Blake2Hasher> {
+        let mut t = system::GenesisConfig::<Test>::default().build_storage().unwrap().0;
+
+        t.extend(members::GenesisConfig::<Test>{
+            first_member_id: self.first_member_id,
+            default_paid_membership_fee: self.default_paid_membership_fee,
+        }.build_storage().unwrap().0);
+
+        t.into()
+    }
+}
+
+pub type System = system::Module<Test>;
+pub type Balances = balances::Module<Test>;
+pub type Members = members::Module<Test>;

+ 6 - 0
src/membership/mod.rs

@@ -0,0 +1,6 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod members;
+
+mod mock;
+mod tests;

+ 206 - 0
src/membership/tests.rs

@@ -0,0 +1,206 @@
+#![cfg(test)]
+
+use super::*;
+use super::mock::*;
+
+use parity_codec::Encode;
+use runtime_io::with_externalities;
+use srml_support::*;
+
+fn assert_ok_unwrap<T>(value: Option<T>, err: &'static str) -> T {
+    match value {
+        None => { assert!(false, err); value.unwrap() },
+        Some(v) => v
+    }
+}
+
+fn get_alice_info() -> members::UserInfo {
+    members::UserInfo {
+        handle: Some(String::from("alice").as_bytes().to_vec()),
+        avatar_uri: Some(String::from("http://avatar-url.com/alice").as_bytes().to_vec()),
+        about: Some(String::from("my name is alice").as_bytes().to_vec()),
+    }
+}
+
+fn get_bob_info() -> members::UserInfo {
+    members::UserInfo {
+        handle: Some(String::from("bobby").as_bytes().to_vec()),
+        avatar_uri: Some(String::from("http://avatar-url.com/bob").as_bytes().to_vec()),
+        about: Some(String::from("my name is bob").as_bytes().to_vec()),
+    }
+}
+
+const ALICE_ACCOUNT_ID: u64 = 1;
+const DEFAULT_TERMS_ID: u32 = 0;
+
+fn buy_default_membership_as_alice() -> dispatch::Result {
+    Members::buy_membership(Origin::signed(ALICE_ACCOUNT_ID), DEFAULT_TERMS_ID, get_alice_info())
+}
+
+fn set_alice_free_balance(balance: u32) {
+    Balances::set_free_balance(&ALICE_ACCOUNT_ID, balance);
+}
+
+#[test]
+fn initial_state() {
+    const DEFAULT_FEE: u32 = 500;
+    const DEFAULT_FIRST_ID: u32 = 1000;
+
+    with_externalities(&mut ExtBuilder::default()
+        .default_paid_membership_fee(DEFAULT_FEE)
+        .first_member_id(DEFAULT_FIRST_ID).build(), ||
+    {
+        assert_eq!(Members::first_member_id(), DEFAULT_FIRST_ID);
+        assert_eq!(Members::next_member_id(), DEFAULT_FIRST_ID);
+
+        let default_terms = assert_ok_unwrap(Members::paid_membership_terms_by_id(DEFAULT_TERMS_ID), "default terms not initialized");
+
+        assert_eq!(default_terms.id, DEFAULT_TERMS_ID);
+        assert_eq!(default_terms.fee, DEFAULT_FEE);
+    });
+}
+
+#[test]
+fn buy_membership() {
+    const DEFAULT_FEE: u32 = 500;
+    const DEFAULT_FIRST_ID: u32 = 1000;
+    const SURPLUS_BALANCE: u32 = 500;
+
+    with_externalities(&mut ExtBuilder::default()
+        .default_paid_membership_fee(DEFAULT_FEE)
+        .first_member_id(DEFAULT_FIRST_ID).build(), ||
+    {
+        let initial_balance = DEFAULT_FEE + SURPLUS_BALANCE;
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+
+        let member_id = assert_ok_unwrap(Members::member_id_by_account_id(&ALICE_ACCOUNT_ID), "member id not assigned");
+
+        let profile = assert_ok_unwrap(Members::member_profile(&member_id), "member profile not created");
+
+        assert_eq!(Some(profile.handle), get_alice_info().handle);
+        assert_eq!(Some(profile.avatar_uri), get_alice_info().avatar_uri);
+        assert_eq!(Some(profile.about), get_alice_info().about);
+
+        assert_eq!(Balances::free_balance(&ALICE_ACCOUNT_ID), SURPLUS_BALANCE);
+
+    });
+}
+
+#[test]
+fn buy_membership_fails_without_enough_balance() {
+    const DEFAULT_FEE: u32 = 500;
+
+    with_externalities(&mut ExtBuilder::default()
+        .default_paid_membership_fee(DEFAULT_FEE).build(), ||
+    {
+        let initial_balance = DEFAULT_FEE - 1;
+        set_alice_free_balance(initial_balance);
+
+        assert!(buy_default_membership_as_alice().is_err());
+    });
+}
+
+#[test]
+fn new_memberships_allowed_flag() {
+    const DEFAULT_FEE: u32 = 500;
+
+    with_externalities(&mut ExtBuilder::default()
+        .default_paid_membership_fee(DEFAULT_FEE).build(), ||
+    {
+        let initial_balance = DEFAULT_FEE + 1;
+        set_alice_free_balance(initial_balance);
+
+        <members::NewMembershipsAllowed<Test>>::put(false);
+
+        assert!(buy_default_membership_as_alice().is_err());
+    });
+}
+
+#[test]
+fn account_cannot_create_multiple_memberships() {
+    const DEFAULT_FEE: u32 = 500;
+    const SURPLUS_BALANCE: u32 = 500;
+
+    with_externalities(&mut ExtBuilder::default()
+        .default_paid_membership_fee(DEFAULT_FEE).build(), ||
+    {
+        let initial_balance = DEFAULT_FEE + SURPLUS_BALANCE;
+        set_alice_free_balance(initial_balance);
+
+        // First time it works
+        assert_ok!(buy_default_membership_as_alice());
+
+        // second attempt should fail
+        assert!(buy_default_membership_as_alice().is_err());
+
+    });
+}
+
+#[test]
+fn unique_handles() {
+    const DEFAULT_FEE: u32 = 500;
+    const SURPLUS_BALANCE: u32 = 500;
+
+    with_externalities(&mut ExtBuilder::default()
+        .default_paid_membership_fee(DEFAULT_FEE).build(), ||
+    {
+        let initial_balance = DEFAULT_FEE + SURPLUS_BALANCE;
+        set_alice_free_balance(initial_balance);
+
+        // alice's handle already taken
+        <members::Handles<Test>>::insert(get_alice_info().handle.unwrap(), 1);
+
+        // should not be allowed to buy membership with that handle
+        assert!(buy_default_membership_as_alice().is_err());
+
+    });
+}
+
+#[test]
+fn update_profile() {
+    const DEFAULT_FEE: u32 = 500;
+    const SURPLUS_BALANCE: u32 = 500;
+
+    with_externalities(&mut ExtBuilder::default()
+        .default_paid_membership_fee(DEFAULT_FEE).build(), ||
+    {
+        let initial_balance = DEFAULT_FEE + SURPLUS_BALANCE;
+        set_alice_free_balance(initial_balance);
+
+        assert_ok!(buy_default_membership_as_alice());
+
+        assert_ok!(Members::update_profile(Origin::signed(ALICE_ACCOUNT_ID), get_bob_info()));
+
+        let member_id = assert_ok_unwrap(Members::member_id_by_account_id(&ALICE_ACCOUNT_ID), "member id not assigned");
+
+        let profile = assert_ok_unwrap(Members::member_profile(&member_id), "member profile created");
+
+        assert_eq!(Some(profile.handle), get_bob_info().handle);
+        assert_eq!(Some(profile.avatar_uri), get_bob_info().avatar_uri);
+        assert_eq!(Some(profile.about), get_bob_info().about);
+
+    });
+}
+
+#[test]
+fn add_screened_member() {
+    with_externalities(&mut ExtBuilder::default().build(), ||
+    {
+        let screening_authority = 5;
+        <members::ScreeningAuthority<Test>>::put(&screening_authority);
+
+        assert_ok!(Members::add_screened_member(Origin::signed(screening_authority), ALICE_ACCOUNT_ID, get_alice_info()));
+
+        let member_id = assert_ok_unwrap(Members::member_id_by_account_id(&ALICE_ACCOUNT_ID), "member id not assigned");
+
+        let profile = assert_ok_unwrap(Members::member_profile(&member_id), "member profile created");
+
+        assert_eq!(Some(profile.handle), get_alice_info().handle);
+        assert_eq!(Some(profile.avatar_uri), get_alice_info().avatar_uri);
+        assert_eq!(Some(profile.about), get_alice_info().about);
+        assert_eq!(members::EntryMethod::Screening(screening_authority), profile.entry);
+
+    });
+}

+ 65 - 0
src/migration.rs

@@ -0,0 +1,65 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use srml_support::{StorageValue, dispatch::Result, decl_module, decl_storage, decl_event, ensure};
+use system;
+use rstd::prelude::*;
+use runtime_io::print;
+use crate::{VERSION};
+use crate::membership::members;
+
+pub trait Trait: system::Trait + members::Trait {
+    type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
+}
+
+decl_storage! {
+    trait Store for Module<T: Trait> as Migration {
+        /// Records at what runtime spec version the store was initialized. This allows the runtime
+        /// to know when to run initialize code if it was installed as an update.
+        pub SpecVersion get(spec_version) build(|_| Some(VERSION.spec_version)) : Option<u32>;
+    }
+}
+
+decl_event! {
+    pub enum Event<T> where <T as system::Trait>::BlockNumber {
+        Migrated(BlockNumber, u32),
+    }
+}
+
+// When preparing a new major runtime release version bump this value to match it and update
+// the initialization code in runtime_initialization(). Because of the way substrate runs runtime code
+// the runtime doesn't need to maintain any logic for old migrations. All knowledge about state of the chain and runtime
+// prior to the new runtime taking over is implicit in the migration code implementation. If assumptions are incorrect
+// behaviour is undefined.
+const MIGRATION_FOR_SPEC_VERSION: u32 = 5;
+
+impl<T: Trait> Module<T> {
+    fn runtime_initialization() {
+        if VERSION.spec_version != MIGRATION_FOR_SPEC_VERSION { return }
+
+        print("running runtime initializers");
+
+        <members::Module<T>>::initialize_storage();
+
+        // ...
+        // add initialization of other modules introduced in this runtime
+        // ...
+
+        Self::deposit_event(RawEvent::Migrated(<system::Module<T>>::block_number(), VERSION.spec_version));
+    }
+}
+
+decl_module! {
+    pub struct Module<T: Trait> for enum Call where origin: T::Origin {
+        fn deposit_event<T>() = default;
+
+        fn on_initialise(_now: T::BlockNumber) {
+            if Self::spec_version().map_or(true, |spec_version| VERSION.spec_version > spec_version) {
+                // mark store version with current version of the runtime
+                <SpecVersion<T>>::put(VERSION.spec_version);
+
+                // run migrations and store initializers
+                Self::runtime_initialization();
+            }
+        }
+    }
+}

+ 7 - 0
src/traits.rs

@@ -1,6 +1,7 @@
 #![cfg_attr(not(feature = "std"), no_std)]
 
 use crate::storage::data_object_type_registry;
+use system;
 
 pub trait IsActiveDataObjectType<T: data_object_type_registry::Trait>
 {
@@ -9,3 +10,9 @@ pub trait IsActiveDataObjectType<T: data_object_type_registry::Trait>
         false
     }
 }
+
+pub trait IsActiveMember<T: system::Trait> {
+    fn is_active_member(account_id: &T::AccountId) -> bool {
+        false
+    }
+}