Browse Source

Merge pull request #1190 from mnaamani/chainspec-import-members-forum-versioned-store

Chainspec import members forum versioned store
shamil-gadelshin 4 years ago
parent
commit
65cf1e11c2
33 changed files with 903 additions and 371 deletions
  1. 4 0
      Cargo.lock
  2. 2 1
      node/Cargo.toml
  3. 0 0
      node/res/acropolis_members.json
  4. 0 0
      node/res/forum_data_acropolis_encoded.json
  5. 0 0
      node/res/forum_data_acropolis_serialized.json
  6. 0 1
      node/res/forum_data_empty.json
  7. 391 0
      node/src/chain_spec/content_config.rs
  8. 149 0
      node/src/chain_spec/forum_config.rs
  9. 13 0
      node/src/chain_spec/initial_members.rs
  10. 40 52
      node/src/chain_spec/mod.rs
  11. 0 0
      node/src/chain_spec/proposals_config.rs
  12. 0 90
      node/src/forum_config/from_encoded.rs
  13. 0 51
      node/src/forum_config/from_serialized.rs
  14. 0 10
      node/src/forum_config/mod.rs
  15. 0 3
      node/src/lib.rs
  16. 0 38
      node/src/members_config.rs
  17. 3 3
      runtime-modules/forum/src/lib.rs
  18. 30 27
      runtime-modules/governance/src/mock.rs
  19. 38 9
      runtime-modules/membership/src/genesis.rs
  20. 50 26
      runtime-modules/membership/src/lib.rs
  21. 3 3
      runtime-modules/membership/src/tests.rs
  22. 7 2
      runtime-modules/storage/src/data_directory.rs
  23. 9 1
      runtime-modules/storage/src/tests/mock.rs
  24. 3 0
      runtime-modules/versioned-store-permissions/Cargo.toml
  25. 4 0
      runtime-modules/versioned-store-permissions/src/constraint.rs
  26. 5 1
      runtime-modules/versioned-store-permissions/src/credentials.rs
  27. 2 2
      runtime-modules/versioned-store-permissions/src/lib.rs
  28. 1 1
      runtime-modules/versioned-store-permissions/src/mock.rs
  29. 5 0
      runtime-modules/versioned-store-permissions/src/permissions.rs
  30. 2 1
      runtime-modules/versioned-store/Cargo.toml
  31. 1 37
      runtime-modules/versioned-store/src/lib.rs
  32. 8 5
      runtime/src/lib.rs
  33. 133 7
      utils/chain-spec-builder/src/main.rs

+ 4 - 0
Cargo.lock

@@ -1974,6 +1974,7 @@ dependencies = [
  "frame-benchmarking-cli",
  "frame-system",
  "futures 0.3.4",
+ "hex",
  "joystream-node-runtime",
  "jsonrpc-core",
  "node-inspect",
@@ -3805,6 +3806,7 @@ version = "3.0.0"
 dependencies = [
  "frame-support",
  "frame-system",
+ "pallet-common",
  "pallet-timestamp",
  "parity-scale-codec",
  "serde",
@@ -3820,9 +3822,11 @@ version = "3.0.0"
 dependencies = [
  "frame-support",
  "frame-system",
+ "pallet-common",
  "pallet-timestamp",
  "pallet-versioned-store",
  "parity-scale-codec",
+ "serde",
  "sp-arithmetic",
  "sp-core",
  "sp-io",

+ 2 - 1
node/Cargo.toml

@@ -20,6 +20,8 @@ futures = { version = "0.3.1", features = ["compat"] }
 jsonrpc-core = "14.2.0"
 structopt = { version = "0.3.8", optional = true}
 serde_json = '1.0'
+codec = { package = "parity-scale-codec", version = "1.3.1" }
+hex = { package = "hex", version = "0.4.2" }
 
 # primitives
 sp-authority-discovery = { package = 'sp-authority-discovery', git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
@@ -74,7 +76,6 @@ browser-utils = { package = 'substrate-browser-utils', git = 'https://github.com
 
 [dev-dependencies]
 tempfile = "3.1.0"
-codec = { package = "parity-scale-codec", version = "1.3.1" }
 sp-timestamp = { package = 'sp-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
 sp-keyring = { package = 'sp-keyring', git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
 sc-consensus-babe = { git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4', features = ["test-helpers"]}

File diff suppressed because it is too large
+ 0 - 0
node/res/acropolis_members.json


File diff suppressed because it is too large
+ 0 - 0
node/res/forum_data_acropolis_encoded.json


File diff suppressed because it is too large
+ 0 - 0
node/res/forum_data_acropolis_serialized.json


+ 0 - 1
node/res/forum_data_empty.json

@@ -1 +0,0 @@
-{ "categories":[], "posts":[], "threads":[] }

+ 391 - 0
node/src/chain_spec/content_config.rs

@@ -0,0 +1,391 @@
+use codec::Decode;
+use node_runtime::common::constraints::InputValidationLengthConstraint;
+use node_runtime::{
+    content_wg::{Channel, ChannelId, Principal, PrincipalId},
+    data_directory::DataObject,
+    primitives::{AccountId, BlockNumber, Credential},
+    versioned_store::{Class, ClassId, Entity, EntityId},
+    versioned_store_permissions::ClassPermissions,
+    ContentId, ContentWorkingGroupConfig, DataDirectoryConfig, Runtime, VersionedStoreConfig,
+    VersionedStorePermissionsConfig,
+};
+use serde::Deserialize;
+use std::{fs, path::Path};
+
+// Because of the way that the @joystream/types were implemented the getters for
+// the string types return a `string` not the `Text` type so when we are serializing
+// them to json we get a string rather than an array of bytes, so deserializing them
+// is failing. So we are relying on parity codec encoding instead..
+#[derive(Decode)]
+struct ClassAndPermissions {
+    class: Class,
+    permissions: ClassPermissions<ClassId, Credential, u16, BlockNumber>,
+}
+
+#[derive(Decode)]
+struct EntityAndMaintainer {
+    entity: Entity,
+    maintainer: Option<Credential>,
+}
+
+#[derive(Decode)]
+struct DataObjectAndContentId {
+    content_id: ContentId,
+    data_object: DataObject<Runtime>,
+}
+
+#[derive(Decode)]
+struct ContentData {
+    /// classes and their associted permissions
+    classes: Vec<ClassAndPermissions>,
+    /// entities and their associated maintainer
+    entities: Vec<EntityAndMaintainer>,
+    /// DataObject(s) and ContentId
+    data_objects: Vec<DataObjectAndContentId>,
+    /// Media Channels
+    channels: Vec<ChannelAndId>,
+}
+
+#[derive(Deserialize)]
+struct EncodedClassAndPermissions {
+    /// hex encoded Class
+    class: String,
+    /// hex encoded ClassPermissions<ClassId, Credential, u16, BlockNumber>,
+    permissions: String,
+}
+
+impl EncodedClassAndPermissions {
+    fn decode(&self) -> ClassAndPermissions {
+        // hex string must not include '0x' prefix!
+        let encoded_class =
+            hex::decode(&self.class[2..].as_bytes()).expect("failed to parse class hex string");
+        let encoded_permissions = hex::decode(&self.permissions[2..].as_bytes())
+            .expect("failed to parse class permissions hex string");
+        ClassAndPermissions {
+            class: Decode::decode(&mut encoded_class.as_slice()).unwrap(),
+            permissions: Decode::decode(&mut encoded_permissions.as_slice()).unwrap(),
+        }
+    }
+}
+
+#[derive(Deserialize)]
+struct EncodedEntityAndMaintainer {
+    /// hex encoded Entity
+    entity: String,
+    /// hex encoded Option<Credential>
+    maintainer: Option<String>,
+}
+
+impl EncodedEntityAndMaintainer {
+    fn decode(&self) -> EntityAndMaintainer {
+        // hex string must not include '0x' prefix!
+        let encoded_entity =
+            hex::decode(&self.entity[2..].as_bytes()).expect("failed to parse entity hex string");
+        let encoded_maintainer = self.maintainer.as_ref().map(|maintainer| {
+            hex::decode(&maintainer[2..].as_bytes()).expect("failed to parse maintainer hex string")
+        });
+        EntityAndMaintainer {
+            entity: Decode::decode(&mut encoded_entity.as_slice()).unwrap(),
+            maintainer: encoded_maintainer
+                .map(|maintainer| Decode::decode(&mut maintainer.as_slice()).unwrap()),
+        }
+    }
+}
+
+#[derive(Deserialize)]
+struct EncodedDataObjectAndContentId {
+    /// hex encoded ContentId
+    content_id: String,
+    /// hex encoded DataObject<Runtime>
+    data_object: String,
+}
+
+impl EncodedDataObjectAndContentId {
+    fn decode(&self) -> DataObjectAndContentId {
+        // hex string must not include '0x' prefix!
+        let encoded_content_id = hex::decode(&self.content_id[2..].as_bytes())
+            .expect("failed to parse content_id hex string");
+        let encoded_data_object = hex::decode(&self.data_object[2..].as_bytes())
+            .expect("failed to parse data_object hex string");
+        DataObjectAndContentId {
+            content_id: Decode::decode(&mut encoded_content_id.as_slice()).unwrap(),
+            data_object: Decode::decode(&mut encoded_data_object.as_slice()).unwrap(),
+        }
+    }
+}
+
+#[derive(Decode)]
+struct ChannelAndId {
+    id: ChannelId<Runtime>,
+    channel: Channel<u64, AccountId, BlockNumber, PrincipalId<Runtime>>,
+}
+
+#[derive(Deserialize)]
+struct EncodedChannelAndId {
+    /// ChannelId number
+    id: u64,
+    /// hex encoded Channel
+    channel: String,
+}
+
+impl EncodedChannelAndId {
+    fn decode(&self) -> ChannelAndId {
+        let id = self.id;
+        let encoded_channel =
+            hex::decode(&self.channel[2..].as_bytes()).expect("failed to parse channel hex string");
+        ChannelAndId {
+            id: id as ChannelId<Runtime>,
+            channel: Decode::decode(&mut encoded_channel.as_slice()).unwrap(),
+        }
+    }
+}
+
+#[derive(Deserialize)]
+struct EncodedContentData {
+    /// classes and their associted permissions
+    classes: Vec<EncodedClassAndPermissions>,
+    /// entities and their associated maintainer
+    entities: Vec<EncodedEntityAndMaintainer>,
+    /// DataObject(s) and ContentId
+    data_objects: Vec<EncodedDataObjectAndContentId>,
+    /// Media Channels
+    channels: Vec<EncodedChannelAndId>,
+}
+
+fn parse_content_data(data_file: &Path) -> EncodedContentData {
+    let data = fs::read_to_string(data_file).expect("Failed reading file");
+    serde_json::from_str(&data).expect("failed parsing content data")
+}
+
+impl EncodedContentData {
+    pub fn decode(&self) -> ContentData {
+        ContentData {
+            classes: self
+                .classes
+                .iter()
+                .map(|class_and_perm| class_and_perm.decode())
+                .collect(),
+            entities: self
+                .entities
+                .iter()
+                .map(|entities_and_maintainer| entities_and_maintainer.decode())
+                .collect(),
+            data_objects: self
+                .data_objects
+                .iter()
+                .map(|data_objects| data_objects.decode())
+                .collect(),
+            channels: self
+                .channels
+                .iter()
+                .map(|channel_and_id| channel_and_id.decode())
+                .collect(),
+        }
+    }
+}
+
+/// Generates a VersionedStoreConfig genesis config
+/// with pre-populated classes and entities parsed from a json file serialized
+/// as a ContentData struct.
+pub fn versioned_store_config_from_json(data_file: &Path) -> VersionedStoreConfig {
+    let content = parse_content_data(data_file).decode();
+    let base_config = empty_versioned_store_config();
+    let first_id = 1;
+
+    let next_class_id: ClassId = content
+        .classes
+        .last()
+        .map_or(first_id, |class_and_perm| class_and_perm.class.id + 1);
+    assert_eq!(next_class_id, (content.classes.len() + 1) as ClassId);
+
+    let next_entity_id: EntityId = content
+        .entities
+        .last()
+        .map_or(first_id, |entity_and_maintainer| {
+            entity_and_maintainer.entity.id + 1
+        });
+
+    VersionedStoreConfig {
+        class_by_id: content
+            .classes
+            .into_iter()
+            .map(|class_and_permissions| {
+                (class_and_permissions.class.id, class_and_permissions.class)
+            })
+            .collect(),
+        entity_by_id: content
+            .entities
+            .into_iter()
+            .map(|entity_and_maintainer| {
+                (
+                    entity_and_maintainer.entity.id,
+                    entity_and_maintainer.entity,
+                )
+            })
+            .collect(),
+        next_class_id,
+        next_entity_id,
+        ..base_config
+    }
+}
+
+/// Generates basic empty VersionedStoreConfig genesis config
+pub fn empty_versioned_store_config() -> VersionedStoreConfig {
+    VersionedStoreConfig {
+        class_by_id: vec![],
+        entity_by_id: vec![],
+        next_class_id: 1,
+        next_entity_id: 1,
+        property_name_constraint: InputValidationLengthConstraint::new(1, 99),
+        property_description_constraint: InputValidationLengthConstraint::new(1, 999),
+        class_name_constraint: InputValidationLengthConstraint::new(1, 99),
+        class_description_constraint: InputValidationLengthConstraint::new(1, 999),
+    }
+}
+
+/// Generates a basic empty VersionedStorePermissionsConfig genesis config
+pub fn empty_versioned_store_permissions_config() -> VersionedStorePermissionsConfig {
+    VersionedStorePermissionsConfig {
+        class_permissions_by_class_id: vec![],
+        entity_maintainer_by_entity_id: vec![],
+    }
+}
+
+/// Generates a `VersionedStorePermissionsConfig` genesis config
+/// pre-populated with permissions and entity maintainers parsed from
+/// a json file serialized as a `ContentData` struct.
+pub fn versioned_store_permissions_config_from_json(
+    data_file: &Path,
+) -> VersionedStorePermissionsConfig {
+    let content = parse_content_data(data_file).decode();
+
+    VersionedStorePermissionsConfig {
+        class_permissions_by_class_id: content
+            .classes
+            .into_iter()
+            .map(|class_and_perm| (class_and_perm.class.id, class_and_perm.permissions))
+            .collect(),
+        entity_maintainer_by_entity_id: content
+            .entities
+            .into_iter()
+            .filter_map(|entity_and_maintainer| {
+                entity_and_maintainer
+                    .maintainer
+                    .map(|maintainer| (entity_and_maintainer.entity.id, maintainer))
+            })
+            .collect(),
+    }
+}
+
+/// Generates a basic empty `DataDirectoryConfig` genesis config
+pub fn empty_data_directory_config() -> DataDirectoryConfig {
+    DataDirectoryConfig {
+        data_object_by_content_id: vec![],
+        known_content_ids: vec![],
+    }
+}
+
+/// Generates a `DataDirectoryConfig` genesis config
+/// pre-populated with data objects and known content ids parsed from
+/// a json file serialized as a `ContentData` struct
+pub fn data_directory_config_from_json(data_file: &Path) -> DataDirectoryConfig {
+    let content = parse_content_data(data_file).decode();
+
+    DataDirectoryConfig {
+        data_object_by_content_id: content
+            .data_objects
+            .iter()
+            .map(|object| (object.content_id, object.data_object.clone()))
+            .collect(),
+        known_content_ids: content
+            .data_objects
+            .into_iter()
+            .map(|object| object.content_id)
+            .collect(),
+    }
+}
+
+/// Generates a basic `ContentWorkingGroupConfig` genesis config without any active curators
+/// curator lead or channels.
+pub fn empty_content_working_group_config() -> ContentWorkingGroupConfig {
+    ContentWorkingGroupConfig {
+        mint_capacity: 100_000,
+        curator_opening_by_id: vec![],
+        next_curator_opening_id: 0,
+        curator_application_by_id: vec![],
+        next_curator_application_id: 0,
+        channel_by_id: vec![],
+        next_channel_id: 1,
+        channel_id_by_handle: vec![],
+        curator_by_id: vec![],
+        next_curator_id: 0,
+        principal_by_id: vec![],
+        next_principal_id: 0,
+        channel_creation_enabled: true, // there is no extrinsic to change it so enabling at genesis
+        unstaker_by_stake_id: vec![],
+        channel_handle_constraint: InputValidationLengthConstraint::new(5, 20),
+        channel_description_constraint: InputValidationLengthConstraint::new(1, 1024),
+        opening_human_readable_text: InputValidationLengthConstraint::new(1, 2048),
+        curator_application_human_readable_text: InputValidationLengthConstraint::new(1, 2048),
+        curator_exit_rationale_text: InputValidationLengthConstraint::new(1, 2048),
+        channel_avatar_constraint: InputValidationLengthConstraint::new(5, 1024),
+        channel_banner_constraint: InputValidationLengthConstraint::new(5, 1024),
+        channel_title_constraint: InputValidationLengthConstraint::new(5, 1024),
+    }
+}
+
+/// Generates a `ContentWorkingGroupConfig` genesis config
+/// pre-populated with channels and corresponding princial channel owners
+/// parsed from a json file serialized as a `ContentData` struct
+pub fn content_working_group_config_from_json(data_file: &Path) -> ContentWorkingGroupConfig {
+    let content = parse_content_data(data_file).decode();
+    let first_channel_id = 1;
+    let first_principal_id = 0;
+
+    let next_channel_id: ChannelId<Runtime> = content
+        .channels
+        .last()
+        .map_or(first_channel_id, |channel_and_id| channel_and_id.id + 1);
+    assert_eq!(
+        next_channel_id,
+        (content.channels.len() + 1) as ChannelId<Runtime>
+    );
+
+    let base_config = empty_content_working_group_config();
+
+    ContentWorkingGroupConfig {
+        channel_by_id: content
+            .channels
+            .iter()
+            .enumerate()
+            .map(|(ix, channel_and_id)| {
+                (
+                    channel_and_id.id,
+                    Channel {
+                        principal_id: first_principal_id + ix as PrincipalId<Runtime>,
+                        ..channel_and_id.channel.clone()
+                    },
+                )
+            })
+            .collect(),
+        next_channel_id,
+        channel_id_by_handle: content
+            .channels
+            .iter()
+            .map(|channel_and_id| (channel_and_id.channel.handle.clone(), channel_and_id.id))
+            .collect(),
+        principal_by_id: content
+            .channels
+            .iter()
+            .enumerate()
+            .map(|(ix, channel_and_id)| {
+                (
+                    first_principal_id + ix as PrincipalId<Runtime>,
+                    Principal::ChannelOwner(channel_and_id.id),
+                )
+            })
+            .collect(),
+        next_principal_id: first_principal_id + content.channels.len() as PrincipalId<Runtime>,
+        ..base_config
+    }
+}

+ 149 - 0
node/src/chain_spec/forum_config.rs

@@ -0,0 +1,149 @@
+use codec::Decode;
+use node_runtime::{
+    common::constraints::InputValidationLengthConstraint,
+    forum::{Category, CategoryId, Post, Thread},
+    AccountId, BlockNumber, ForumConfig, Moment, PostId, ThreadId,
+};
+use serde::Deserialize;
+use std::{fs, path::Path};
+
+fn new_validation(min: u16, max_min_diff: u16) -> InputValidationLengthConstraint {
+    InputValidationLengthConstraint { min, max_min_diff }
+}
+
+#[derive(Decode)]
+struct ForumData {
+    categories: Vec<Category<BlockNumber, Moment, AccountId>>,
+    posts: Vec<Post<BlockNumber, Moment, AccountId, ThreadId, PostId>>,
+    threads: Vec<Thread<BlockNumber, Moment, AccountId, ThreadId>>,
+}
+
+#[derive(Deserialize)]
+struct EncodedForumData {
+    /// hex encoded categories
+    categories: Vec<String>,
+    /// hex encoded posts
+    posts: Vec<String>,
+    /// hex encoded threads
+    threads: Vec<String>,
+}
+
+impl EncodedForumData {
+    fn decode(&self) -> ForumData {
+        ForumData {
+            categories: self
+                .categories
+                .iter()
+                .map(|category| {
+                    let encoded_category = hex::decode(&category[2..].as_bytes())
+                        .expect("failed to parse category hex string");
+                    Decode::decode(&mut encoded_category.as_slice()).unwrap()
+                })
+                .collect(),
+            posts: self
+                .posts
+                .iter()
+                .map(|post| {
+                    let encoded_post = hex::decode(&post[2..].as_bytes())
+                        .expect("failed to parse post hex string");
+                    Decode::decode(&mut encoded_post.as_slice()).unwrap()
+                })
+                .collect(),
+            threads: self
+                .threads
+                .iter()
+                .map(|thread| {
+                    let encoded_thread = hex::decode(&thread[2..].as_bytes())
+                        .expect("failed to parse thread hex string");
+                    Decode::decode(&mut encoded_thread.as_slice()).unwrap()
+                })
+                .collect(),
+        }
+    }
+}
+
+fn parse_forum_json(data_file: &Path) -> EncodedForumData {
+    let data = fs::read_to_string(data_file).expect("Failed reading file");
+    serde_json::from_str(&data).expect("failed parsing members data")
+}
+
+/// Generates a `ForumConfig` geneis config pre-populated with
+/// categories, threads and posts parsed
+/// from a json file serialized as `EncodedForumData`
+pub fn from_json(forum_sudo: AccountId, data_file: &Path) -> ForumConfig {
+    let forum_data = parse_forum_json(data_file);
+    create(forum_sudo, forum_data)
+}
+
+/// Generates a basic empty `ForumConfig` geneis config
+pub fn empty(forum_sudo: AccountId) -> ForumConfig {
+    let forum_data = EncodedForumData {
+        categories: vec![],
+        threads: vec![],
+        posts: vec![],
+    };
+    create(forum_sudo, forum_data)
+}
+
+fn create(forum_sudo: AccountId, forum_data: EncodedForumData) -> ForumConfig {
+    let first_id = 1;
+    let forum_data = forum_data.decode();
+
+    let next_category_id: CategoryId = forum_data
+        .categories
+        .last()
+        .map_or(first_id, |category| category.id + 1);
+
+    assert_eq!(
+        next_category_id,
+        (forum_data.categories.len() + 1) as CategoryId
+    );
+
+    let next_thread_id: ThreadId = forum_data
+        .threads
+        .last()
+        .map_or(first_id, |thread| thread.id + 1);
+
+    assert_eq!(next_thread_id, (forum_data.threads.len() + 1) as ThreadId);
+
+    let next_post_id: PostId = forum_data.posts.last().map_or(first_id, |post| post.id + 1);
+
+    assert_eq!(next_post_id, (forum_data.posts.len() + 1) as PostId);
+
+    ForumConfig {
+        category_by_id: forum_data
+            .categories
+            .into_iter()
+            .map(|encoded_category| {
+                let category = encoded_category;
+                (category.id, category)
+            })
+            .collect(),
+        thread_by_id: forum_data
+            .threads
+            .into_iter()
+            .map(|encoded_thread| {
+                let thread = encoded_thread;
+                (thread.id, thread)
+            })
+            .collect(),
+        post_by_id: forum_data
+            .posts
+            .into_iter()
+            .map(|encoded_post| {
+                let post = encoded_post;
+                (post.id, post)
+            })
+            .collect(),
+        next_category_id,
+        next_thread_id,
+        next_post_id,
+        forum_sudo,
+        category_title_constraint: new_validation(10, 90),
+        category_description_constraint: new_validation(10, 490),
+        thread_title_constraint: new_validation(10, 90),
+        post_text_constraint: new_validation(10, 990),
+        thread_moderation_rationale_constraint: new_validation(10, 290),
+        post_moderation_rationale_constraint: new_validation(10, 290),
+    }
+}

+ 13 - 0
node/src/chain_spec/initial_members.rs

@@ -0,0 +1,13 @@
+use node_runtime::{membership, AccountId, Moment};
+use std::{fs, path::Path};
+
+/// Generates a Vec of genesis members parsed from a json file
+pub fn from_json(data_file: &Path) -> Vec<membership::genesis::Member<u64, AccountId, Moment>> {
+    let data = fs::read_to_string(data_file).expect("Failed reading file");
+    serde_json::from_str(&data).expect("failed parsing members data")
+}
+
+/// Generates an empty Vec of genesis members
+pub fn none() -> Vec<membership::genesis::Member<u64, AccountId, Moment>> {
+    vec![]
+}

+ 40 - 52
node/src/chain_spec.rs → node/src/chain_spec/mod.rs

@@ -29,22 +29,27 @@ use sp_runtime::traits::{IdentifyAccount, Verify};
 use sp_runtime::Perbill;
 
 use node_runtime::{
-    versioned_store::InputValidationLengthConstraint as VsInputValidation,
-    AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig, ContentWorkingGroupConfig,
-    CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig,
-    DataObjectTypeRegistryConfig, ElectionParameters, GrandpaConfig, ImOnlineConfig, MembersConfig,
-    ProposalsCodexConfig, SessionConfig, SessionKeys, Signature, StakerStatus, StakingConfig,
-    StorageWorkingGroupConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, WASM_BINARY,
+    membership, AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig,
+    ContentWorkingGroupConfig, CouncilConfig, CouncilElectionConfig, DataDirectoryConfig,
+    DataObjectStorageRegistryConfig, DataObjectTypeRegistryConfig, ElectionParameters, ForumConfig,
+    GrandpaConfig, ImOnlineConfig, MembersConfig, Moment, ProposalsCodexConfig, SessionConfig,
+    SessionKeys, Signature, StakerStatus, StakingConfig, StorageWorkingGroupConfig, SudoConfig,
+    SystemConfig, VersionedStoreConfig, VersionedStorePermissionsConfig, DAYS, WASM_BINARY,
 };
 
+// Exported to be used by chain-spec-builder
 pub use node_runtime::{AccountId, GenesisConfig};
 
+pub mod content_config;
+pub mod forum_config;
+pub mod initial_members;
+pub mod proposals_config;
+
 type AccountPublic = <Signature as Verify>::Signer;
 
 /// Specialized `ChainSpec`. This is a specialization of the general Substrate ChainSpec type.
 pub type ChainSpec = sc_service::GenericChainSpec<GenesisConfig>;
 
-use node_runtime::common::constraints::InputValidationLengthConstraint;
 use sc_chain_spec::ChainType;
 
 /// The chain specification option. This is expected to come in from the CLI and
@@ -126,7 +131,13 @@ impl Alternative {
                             get_account_id_from_seed::<sr25519::Public>("Alice//stash"),
                             get_account_id_from_seed::<sr25519::Public>("Bob//stash"),
                         ],
-                        crate::proposals_config::development(),
+                        proposals_config::development(),
+                        initial_members::none(),
+                        forum_config::empty(get_account_id_from_seed::<sr25519::Public>("Alice")),
+                        content_config::empty_versioned_store_config(),
+                        content_config::empty_versioned_store_permissions_config(),
+                        content_config::empty_data_directory_config(),
+                        content_config::empty_content_working_group_config(),
                     )
                 },
                 Vec::new(),
@@ -160,7 +171,13 @@ impl Alternative {
                             get_account_id_from_seed::<sr25519::Public>("Eve//stash"),
                             get_account_id_from_seed::<sr25519::Public>("Ferdie//stash"),
                         ],
-                        crate::proposals_config::development(),
+                        proposals_config::development(),
+                        initial_members::none(),
+                        forum_config::empty(get_account_id_from_seed::<sr25519::Public>("Alice")),
+                        content_config::empty_versioned_store_config(),
+                        content_config::empty_versioned_store_permissions_config(),
+                        content_config::empty_data_directory_config(),
+                        content_config::empty_content_working_group_config(),
                     )
                 },
                 Vec::new(),
@@ -173,10 +190,6 @@ impl Alternative {
     }
 }
 
-fn new_vs_validation(min: u16, max_min_diff: u16) -> VsInputValidation {
-    VsInputValidation { min, max_min_diff }
-}
-
 pub fn chain_spec_properties() -> json::map::Map<String, json::Value> {
     let mut properties: json::map::Map<String, json::Value> = json::map::Map::new();
     properties.insert(
@@ -190,6 +203,7 @@ pub fn chain_spec_properties() -> json::map::Map<String, json::Value> {
     properties
 }
 
+#[allow(clippy::too_many_arguments)]
 pub fn testnet_genesis(
     initial_authorities: Vec<(
         AccountId,
@@ -202,6 +216,12 @@ pub fn testnet_genesis(
     root_key: AccountId,
     endowed_accounts: Vec<AccountId>,
     cpcp: node_runtime::ProposalsConfigParameters,
+    members: Vec<membership::genesis::Member<u64, AccountId, Moment>>,
+    forum_config: ForumConfig,
+    versioned_store_config: VersionedStoreConfig,
+    versioned_store_permissions_config: VersionedStorePermissionsConfig,
+    data_directory_config: DataDirectoryConfig,
+    content_working_group_config: ContentWorkingGroupConfig,
 ) -> GenesisConfig {
     const CENTS: Balance = 1;
     const DOLLARS: Balance = 100 * CENTS;
@@ -234,9 +254,7 @@ pub fn testnet_genesis(
             slash_reward_fraction: Perbill::from_percent(10),
             ..Default::default()
         }),
-        pallet_sudo: Some(SudoConfig {
-            key: root_key.clone(),
-        }),
+        pallet_sudo: Some(SudoConfig { key: root_key }),
         pallet_babe: Some(BabeConfig {
             authorities: vec![],
         }),
@@ -276,9 +294,10 @@ pub fn testnet_genesis(
         }),
         membership: Some(MembersConfig {
             default_paid_membership_fee: 100u128,
-            members: vec![],
+            members,
         }),
-        forum: Some(crate::forum_config::from_serialized::create(root_key)),
+        forum: Some(forum_config),
+        data_directory: Some(data_directory_config),
         data_object_type_registry: Some(DataObjectTypeRegistryConfig {
             first_data_object_type_id: 1,
         }),
@@ -292,40 +311,9 @@ pub fn testnet_genesis(
             worker_application_human_readable_text_constraint: default_text_constraint,
             worker_exit_rationale_text_constraint: default_text_constraint,
         }),
-        versioned_store: Some(VersionedStoreConfig {
-            class_by_id: vec![],
-            entity_by_id: vec![],
-            next_class_id: 1,
-            next_entity_id: 1,
-            property_name_constraint: new_vs_validation(1, 99),
-            property_description_constraint: new_vs_validation(1, 999),
-            class_name_constraint: new_vs_validation(1, 99),
-            class_description_constraint: new_vs_validation(1, 999),
-        }),
-        content_wg: Some(ContentWorkingGroupConfig {
-            mint_capacity: 100_000,
-            curator_opening_by_id: vec![],
-            next_curator_opening_id: 0,
-            curator_application_by_id: vec![],
-            next_curator_application_id: 0,
-            channel_by_id: vec![],
-            next_channel_id: 1,
-            channel_id_by_handle: vec![],
-            curator_by_id: vec![],
-            next_curator_id: 0,
-            principal_by_id: vec![],
-            next_principal_id: 0,
-            channel_creation_enabled: true, // there is no extrinsic to change it so enabling at genesis
-            unstaker_by_stake_id: vec![],
-            channel_handle_constraint: InputValidationLengthConstraint::new(5, 20),
-            channel_description_constraint: InputValidationLengthConstraint::new(1, 1024),
-            opening_human_readable_text: InputValidationLengthConstraint::new(1, 2048),
-            curator_application_human_readable_text: InputValidationLengthConstraint::new(1, 2048),
-            curator_exit_rationale_text: InputValidationLengthConstraint::new(1, 2048),
-            channel_avatar_constraint: InputValidationLengthConstraint::new(5, 1024),
-            channel_banner_constraint: InputValidationLengthConstraint::new(5, 1024),
-            channel_title_constraint: InputValidationLengthConstraint::new(5, 1024),
-        }),
+        versioned_store: Some(versioned_store_config),
+        versioned_store_permissions: Some(versioned_store_permissions_config),
+        content_wg: Some(content_working_group_config),
         proposals_codex: Some(ProposalsCodexConfig {
             set_validator_count_proposal_voting_period: cpcp
                 .set_validator_count_proposal_voting_period,

+ 0 - 0
node/src/proposals_config.rs → node/src/chain_spec/proposals_config.rs


+ 0 - 90
node/src/forum_config/from_encoded.rs

@@ -1,90 +0,0 @@
-// This module is not used but included as sample code
-// and highlights some pitfalls.
-
-use node_runtime::{
-    forum::{
-        Category, CategoryId, Post, PostId, Thread, ThreadId,
-    },
-    AccountId, BlockNumber, ForumConfig, Moment,
-};
-use serde::Deserialize;
-use serde_json::Result;
-use super::new_validation;
-
-use codec::Decode;
-
-#[derive(Deserialize)]
-struct ForumData {
-    /// hex encoded categories
-    categories: Vec<(CategoryId, String)>,
-    /// hex encoded posts
-    posts: Vec<(PostId, String)>,
-    /// hex encoded threads
-    threads: Vec<(ThreadId, String)>,
-}
-
-fn decode_post(encoded: String) -> Post<BlockNumber, Moment, AccountId> {
-    // hex string must not include '0x' prefix!
-    let encoded = hex::decode(encoded.as_bytes()).expect("failed to parse hex string");
-    Decode::decode(&mut encoded.as_slice()).unwrap()
-}
-
-fn decode_category(encoded: String) -> Category<BlockNumber, Moment, AccountId> {
-    // hex string must not include '0x' prefix!
-    let encoded = hex::decode(encoded.as_bytes()).expect("failed to parse hex string");
-    Decode::decode(&mut encoded.as_slice()).unwrap()
-}
-
-fn decode_thread(encoded: String) -> Thread<BlockNumber, Moment, AccountId> {
-    // hex string must not include '0x' prefix!
-    let encoded = hex::decode(encoded.as_bytes()).expect("failed to parse hex string");
-    Decode::decode(&mut encoded.as_slice()).unwrap()
-}
-
-fn parse_forum_json() -> Result<ForumData> {
-    let data = include_str!("../../res/forum_data_acropolis_encoded.json");
-    serde_json::from_str(data)
-}
-
-pub fn create(forum_sudo: AccountId) -> ForumConfig {
-    let forum_data = parse_forum_json().expect("failed loading forum data");
-
-    let next_category_id: CategoryId = forum_data
-        .categories
-        .last()
-        .map_or(1, |category| category.0 + 1);
-    let next_thread_id: ThreadId = forum_data.threads.last().map_or(1, |thread| thread.0 + 1);
-    let next_post_id: PostId = forum_data.posts.last().map_or(1, |post| post.0 + 1);
-
-    ForumConfig {
-        // Decoding will fail because of differnt type used for
-        // BlockNumber between Acropolis (u64) and Rome (u32)
-        // As long as types between chains are identical this approach works nicely
-        // since we don't need to use an intermediate format or do any transformation on source data.
-        category_by_id: forum_data
-            .categories
-            .into_iter()
-            .map(|category| (category.0, decode_category(category.1)))
-            .collect(),
-        thread_by_id: forum_data
-            .threads
-            .into_iter()
-            .map(|thread| (thread.0, decode_thread(thread.1)))
-            .collect(),
-        post_by_id: forum_data
-            .posts
-            .into_iter()
-            .map(|post| (post.0, decode_post(post.1)))
-            .collect(),
-        next_category_id,
-        next_thread_id,
-        next_post_id,
-        forum_sudo,
-        category_title_constraint: new_validation(10, 90),
-        category_description_constraint: new_validation(10, 490),
-        thread_title_constraint: new_validation(10, 90),
-        post_text_constraint: new_validation(10, 990),
-        thread_moderation_rationale_constraint: new_validation(10, 290),
-        post_moderation_rationale_constraint: new_validation(10, 290),
-    }
-}

+ 0 - 51
node/src/forum_config/from_serialized.rs

@@ -1,51 +0,0 @@
-#![allow(clippy::type_complexity)]
-
-use super::new_validation;
-use node_runtime::{
-    forum::{Category, CategoryId, Post, Thread},
-    AccountId, BlockNumber, ForumConfig, Moment, PostId, ThreadId,
-};
-use serde::Deserialize;
-use serde_json::Result;
-
-#[derive(Deserialize)]
-struct ForumData {
-    categories: Vec<(CategoryId, Category<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> {
-    let data = include_str!("../../res/forum_data_empty.json");
-    serde_json::from_str(data)
-}
-
-pub fn create(forum_sudo: AccountId) -> ForumConfig {
-    let forum_data = parse_forum_json().expect("failed loading forum data");
-
-    let next_category_id: CategoryId = forum_data
-        .categories
-        .last()
-        .map_or(1, |category| category.0 + 1);
-    let next_thread_id: ThreadId = forum_data.threads.last().map_or(1, |thread| thread.0 + 1);
-    let next_post_id: PostId = forum_data.posts.last().map_or(1, |post| post.0 + 1);
-
-    ForumConfig {
-        category_by_id: forum_data.categories,
-        thread_by_id: forum_data.threads,
-        post_by_id: forum_data.posts,
-        next_category_id,
-        next_thread_id,
-        next_post_id,
-        category_title_constraint: new_validation(10, 90),
-        category_description_constraint: new_validation(10, 490),
-        thread_title_constraint: new_validation(10, 90),
-        post_text_constraint: new_validation(10, 990),
-        thread_moderation_rationale_constraint: new_validation(10, 290),
-        post_moderation_rationale_constraint: new_validation(10, 290),
-        forum_sudo,
-    }
-}

+ 0 - 10
node/src/forum_config/mod.rs

@@ -1,10 +0,0 @@
-pub mod from_serialized;
-
-// Not exported - only here as sample code
-// mod from_encoded;
-
-use node_runtime::common::constraints::InputValidationLengthConstraint;
-
-pub fn new_validation(min: u16, max_min_diff: u16) -> InputValidationLengthConstraint {
-    InputValidationLengthConstraint { min, max_min_diff }
-}

+ 0 - 3
node/src/lib.rs

@@ -1,8 +1,5 @@
 pub mod chain_spec;
 pub mod cli;
-pub mod forum_config;
-pub mod members_config;
-pub mod proposals_config;
 #[macro_use]
 pub mod service;
 pub mod command;

+ 0 - 38
node/src/members_config.rs

@@ -1,38 +0,0 @@
-use serde::Deserialize;
-use serde_json::Result;
-
-use sp_core::crypto::{AccountId32, Ss58Codec};
-
-#[derive(Deserialize)]
-struct Member {
-    /// SS58 Encoded public key
-    address: String,
-    handle: String,
-    avatar_uri: String,
-    about: String,
-}
-
-fn parse_members_json() -> Result<Vec<Member>> {
-    let data = include_str!("../res/acropolis_members.json");
-    serde_json::from_str(data)
-}
-
-pub fn decode_address(address: String) -> AccountId32 {
-    AccountId32::from_ss58check(address.as_ref()).expect("failed to decode account id")
-}
-
-pub fn initial_members() -> Vec<(AccountId32, String, String, String)> {
-    let members = parse_members_json().expect("failed parsing members data");
-
-    members
-        .into_iter()
-        .map(|member| {
-            (
-                decode_address(member.address),
-                member.handle,
-                member.avatar_uri,
-                member.about,
-            )
-        })
-        .collect()
-}

+ 3 - 3
runtime-modules/forum/src/lib.rs

@@ -111,7 +111,7 @@ pub struct PostTextChange<BlockNumber, Moment> {
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
 pub struct Post<BlockNumber, Moment, AccountId, ThreadId, PostId> {
     /// Post identifier
-    id: PostId,
+    pub id: PostId,
 
     /// Id of thread to which this post corresponds.
     thread_id: ThreadId,
@@ -144,7 +144,7 @@ pub struct Post<BlockNumber, Moment, AccountId, ThreadId, PostId> {
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
 pub struct Thread<BlockNumber, Moment, AccountId, ThreadId> {
     /// Thread identifier
-    id: ThreadId,
+    pub id: ThreadId,
 
     /// Title
     title: Vec<u8>,
@@ -208,7 +208,7 @@ pub struct ChildPositionInParentCategory {
 #[derive(Encode, Decode, Default, Clone, PartialEq, Eq)]
 pub struct Category<BlockNumber, Moment, AccountId> {
     /// Category identifier
-    id: CategoryId,
+    pub id: CategoryId,
 
     /// Title
     title: Vec<u8>,

+ 30 - 27
runtime-modules/governance/src/mock.rs

@@ -110,33 +110,36 @@ pub fn initial_test_ext() -> sp_io::TestExternalities {
         .build_storage::<Test>()
         .unwrap();
 
-    membership::GenesisConfig::<Test> {
-        default_paid_membership_fee: 0,
-        members: vec![
-            (1, "member1".into(), "".into(), "".into()),
-            (2, "member2".into(), "".into(), "".into()),
-            (3, "member3".into(), "".into(), "".into()),
-            (4, "member4".into(), "".into(), "".into()),
-            (5, "member5".into(), "".into(), "".into()),
-            (6, "member6".into(), "".into(), "".into()),
-            (7, "member7".into(), "".into(), "".into()),
-            (8, "member8".into(), "".into(), "".into()),
-            (9, "member9".into(), "".into(), "".into()),
-            (10, "member10".into(), "".into(), "".into()),
-            (11, "member11".into(), "".into(), "".into()),
-            (12, "member12".into(), "".into(), "".into()),
-            (13, "member13".into(), "".into(), "".into()),
-            (14, "member14".into(), "".into(), "".into()),
-            (15, "member15".into(), "".into(), "".into()),
-            (16, "member16".into(), "".into(), "".into()),
-            (17, "member17".into(), "".into(), "".into()),
-            (18, "member18".into(), "".into(), "".into()),
-            (19, "member19".into(), "".into(), "".into()),
-            (20, "member20".into(), "".into(), "".into()),
-        ],
-    }
-    .assimilate_storage(&mut t)
-    .unwrap();
+    let members_config_builder = membership::genesis::GenesisConfigBuilder::<Test>::default()
+        .default_paid_membership_fee(0)
+        .members(vec![
+            // member_id, account_id
+            (0, 1),
+            (1, 2),
+            (2, 3),
+            (3, 4),
+            (4, 5),
+            (5, 6),
+            (6, 7),
+            (7, 8),
+            (8, 9),
+            (9, 10),
+            (10, 11),
+            (11, 12),
+            (12, 13),
+            (13, 14),
+            (14, 15),
+            (15, 16),
+            (16, 17),
+            (17, 18),
+            (18, 19),
+            (19, 20),
+        ]);
+
+    members_config_builder
+        .build()
+        .assimilate_storage(&mut t)
+        .unwrap();
 
     t.into()
 }

+ 38 - 9
runtime-modules/membership/src/genesis.rs

@@ -1,13 +1,24 @@
-#![cfg(test)]
+#![cfg(feature = "std")]
 
+use crate::{GenesisConfig, Trait};
 use common::currency::BalanceOf;
+use serde::{Deserialize, Serialize};
 
-use crate::{GenesisConfig, Trait};
+#[derive(Clone, Serialize, Deserialize)]
+pub struct Member<MemberId, AccountId, Moment> {
+    pub member_id: MemberId,
+    pub root_account: AccountId,
+    pub controller_account: AccountId,
+    pub handle: String,
+    pub avatar_uri: String,
+    pub about: String,
+    pub registered_at_time: Moment,
+}
 
 /// Builder fo membership module genesis configuration.
 pub struct GenesisConfigBuilder<T: Trait> {
     default_paid_membership_fee: BalanceOf<T>,
-    members: Vec<T::AccountId>,
+    members: Vec<(T::MemberId, T::AccountId)>,
 }
 
 impl<T: Trait> Default for GenesisConfigBuilder<T> {
@@ -20,6 +31,7 @@ impl<T: Trait> Default for GenesisConfigBuilder<T> {
 }
 
 impl<T: Trait> GenesisConfigBuilder<T> {
+    /// Assign default paid membeship fee
     pub fn default_paid_membership_fee(
         mut self,
         default_paid_membership_fee: BalanceOf<T>,
@@ -27,19 +39,36 @@ impl<T: Trait> GenesisConfigBuilder<T> {
         self.default_paid_membership_fee = default_paid_membership_fee;
         self
     }
-    pub fn members(mut self, members: Vec<T::AccountId>) -> Self {
+
+    /// Assign a collection of MemberId and AccountId pairs, used to derive mock member at genesis
+    pub fn members(mut self, members: Vec<(T::MemberId, T::AccountId)>) -> Self {
         self.members = members;
         self
     }
 
+    /// Generates a Vec of `Member`s from pairs of MemberId and AccountId
+    fn generate_mock_members(&self) -> Vec<Member<T::MemberId, T::AccountId, T::Moment>> {
+        self.members
+            .iter()
+            .enumerate()
+            .map(|(ix, (ref member_id, ref account_id))| Member {
+                member_id: *member_id,
+                root_account: account_id.clone(),
+                controller_account: account_id.clone(),
+                // hack to get min handle length to 5
+                handle: (10000 + ix).to_string(),
+                avatar_uri: "".into(),
+                about: "".into(),
+                registered_at_time: T::Moment::from(0),
+            })
+            .collect()
+    }
+
+    /// Construct GenesisConfig for mocked testing purposes only
     pub fn build(&self) -> GenesisConfig<T> {
         GenesisConfig::<T> {
             default_paid_membership_fee: self.default_paid_membership_fee,
-            members: self
-                .members
-                .iter()
-                .map(|account_id| (account_id.clone(), "".into(), "".into(), "".into()))
-                .collect(),
+            members: self.generate_mock_members(),
         }
     }
 }

+ 50 - 26
runtime-modules/membership/src/lib.rs

@@ -216,16 +216,26 @@ decl_storage! {
     }
     add_extra_genesis {
         config(default_paid_membership_fee): BalanceOf<T>;
-        config(members) : Vec<(T::AccountId, String, String, String)>;
+        config(members) : Vec<genesis::Member<T::MemberId, T::AccountId, T::Moment>>;
         build(|config: &GenesisConfig<T>| {
-            for (who, handle, avatar_uri, about) in &config.members {
-                let user_info = ValidatedUserInfo {
-                    handle: handle.clone().into_bytes(),
-                    avatar_uri: avatar_uri.clone().into_bytes(),
-                    about: about.clone().into_bytes()
-                };
-
-                <Module<T>>::insert_member(&who, &user_info, EntryMethod::Genesis);
+            for member in &config.members {
+                let checked_user_info = <Module<T>>::check_user_registration_info(
+                    Some(member.handle.clone().into_bytes()),
+                    Some(member.avatar_uri.clone().into_bytes()),
+                    Some(member.about.clone().into_bytes())
+                ).expect("Importing Member Failed");
+
+                let member_id = <Module<T>>::insert_member(
+                    &member.root_account,
+                    &member.controller_account,
+                    &checked_user_info,
+                    EntryMethod::Genesis,
+                    T::BlockNumber::from(1),
+                    member.registered_at_time
+                ).expect("Importing Member Failed");
+
+                // ensure imported member id matches assigned id
+                assert_eq!(member_id, member.member_id, "Import Member Failed: MemberId Incorrect");
             }
         });
     }
@@ -271,11 +281,16 @@ decl_module! {
 
             let user_info = Self::check_user_registration_info(handle, avatar_uri, about)?;
 
-            // ensure handle is not already registered
-            Self::ensure_unique_handle(&user_info.handle)?;
+            let member_id = Self::insert_member(
+                &who,
+                &who,
+                &user_info,
+                EntryMethod::Paid(paid_terms_id),
+                <system::Module<T>>::block_number(),
+                <pallet_timestamp::Module<T>>::now()
+            )?;
 
             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));
         }
@@ -413,10 +428,14 @@ decl_module! {
 
             let user_info = Self::check_user_registration_info(handle, avatar_uri, about)?;
 
-            // ensure handle is not already registered
-            Self::ensure_unique_handle(&user_info.handle)?;
-
-            let member_id = Self::insert_member(&new_member_account, &user_info, EntryMethod::Screening(sender));
+            let member_id = Self::insert_member(
+                &new_member_account,
+                &new_member_account,
+                &user_info,
+                EntryMethod::Screening(sender),
+                <system::Module<T>>::block_number(),
+                <pallet_timestamp::Module<T>>::now()
+            )?;
 
             Self::deposit_event(RawEvent::MemberRegistered(member_id, new_member_account));
         }
@@ -557,37 +576,42 @@ impl<T: Trait> Module<T> {
     }
 
     fn insert_member(
-        who: &T::AccountId,
+        root_account: &T::AccountId,
+        controller_account: &T::AccountId,
         user_info: &ValidatedUserInfo,
         entry_method: EntryMethod<T::PaidTermId, T::AccountId>,
-    ) -> T::MemberId {
+        registered_at_block: T::BlockNumber,
+        registered_at_time: T::Moment,
+    ) -> Result<T::MemberId, &'static str> {
+        Self::ensure_unique_handle(&user_info.handle)?;
+
         let new_member_id = Self::members_created();
 
         let membership: Membership<T> = MembershipObject {
             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: <pallet_timestamp::Module<T>>::now(),
+            registered_at_block,
+            registered_at_time,
             entry: entry_method,
             suspended: false,
             subscription: None,
-            root_account: who.clone(),
-            controller_account: who.clone(),
+            root_account: root_account.clone(),
+            controller_account: controller_account.clone(),
         };
 
-        <MemberIdsByRootAccountId<T>>::mutate(who, |ids| {
+        <MemberIdsByRootAccountId<T>>::mutate(root_account, |ids| {
             ids.push(new_member_id);
         });
-        <MemberIdsByControllerAccountId<T>>::mutate(who, |ids| {
+        <MemberIdsByControllerAccountId<T>>::mutate(controller_account, |ids| {
             ids.push(new_member_id);
         });
 
         <MembershipById<T>>::insert(new_member_id, membership);
         <MemberIdByHandle<T>>::insert(user_info.handle.clone(), new_member_id);
-        <NextMemberId<T>>::put(new_member_id + One::one());
 
-        new_member_id
+        <NextMemberId<T>>::put(new_member_id + One::one());
+        Ok(new_member_id)
     }
 
     fn _change_member_about_text(id: T::MemberId, text: &[u8]) -> DispatchResult {

+ 3 - 3
runtime-modules/membership/src/tests.rs

@@ -71,7 +71,7 @@ fn set_alice_free_balance(balance: u64) {
 #[test]
 fn initial_state() {
     const DEFAULT_FEE: u64 = 500;
-    let initial_members = [1, 2, 3];
+    let initial_members = [(0, 1), (1, 2), (2, 3)];
 
     TestExternalitiesBuilder::<Test>::default()
         .set_membership_config(
@@ -277,7 +277,7 @@ fn add_screened_member() {
 
 #[test]
 fn set_controller_key() {
-    let initial_members = [ALICE_ACCOUNT_ID];
+    let initial_members = [(0, ALICE_ACCOUNT_ID)];
     const ALICE_CONTROLLER_ID: u64 = 2;
 
     TestExternalitiesBuilder::<Test>::default()
@@ -311,7 +311,7 @@ fn set_controller_key() {
 
 #[test]
 fn set_root_account() {
-    let initial_members = [ALICE_ACCOUNT_ID];
+    let initial_members = [(0, ALICE_ACCOUNT_ID)];
     const ALICE_NEW_ROOT_ACCOUNT: u64 = 2;
 
     TestExternalitiesBuilder::<Test>::default()

+ 7 - 2
runtime-modules/storage/src/data_directory.rs

@@ -30,6 +30,9 @@ use sp_std::collections::btree_map::BTreeMap;
 use sp_std::vec::Vec;
 use system::ensure_root;
 
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
 use common::origin::ActorOriginValidator;
 pub(crate) use common::BlockAndTime;
 
@@ -87,6 +90,7 @@ decl_error! {
 }
 
 /// The decision of the storage provider when it acts as liaison.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Clone, Encode, Decode, PartialEq, Debug)]
 pub enum LiaisonJudgement {
     /// Content awaits for a judgment.
@@ -115,6 +119,7 @@ pub type DataObject<T> = DataObjectInternal<
 >;
 
 /// Manages content ids, type and storage provider decision about it.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Clone, Encode, Decode, PartialEq, Debug)]
 pub struct DataObjectInternal<MemberId, BlockNumber, Moment, DataObjectTypeId, StorageProviderId> {
     /// Content owner.
@@ -145,10 +150,10 @@ pub type DataObjectsMap<T> = BTreeMap<<T as Trait>::ContentId, DataObject<T>>;
 decl_storage! {
     trait Store for Module<T: Trait> as DataDirectory {
         /// List of ids known to the system.
-        pub KnownContentIds get(fn known_content_ids): Vec<T::ContentId> = Vec::new();
+        pub KnownContentIds get(fn known_content_ids) config(): Vec<T::ContentId> = Vec::new();
 
         /// Maps data objects by their content id.
-        pub DataObjectByContentId get(fn data_object_by_content_id):
+        pub DataObjectByContentId get(fn data_object_by_content_id) config():
             map hasher(blake2_128_concat) T::ContentId => Option<DataObject<T>>;
     }
 }

+ 9 - 1
runtime-modules/storage/src/tests/mock.rs

@@ -277,7 +277,15 @@ impl ExtBuilder {
 
         membership::GenesisConfig::<Test> {
             default_paid_membership_fee: 0,
-            members: vec![(1, "alice".into(), "".into(), "".into())],
+            members: vec![membership::genesis::Member {
+                member_id: 0,
+                root_account: 1,
+                controller_account: 1,
+                handle: "alice".into(),
+                avatar_uri: "".into(),
+                about: "".into(),
+                registered_at_time: 0,
+            }],
         }
         .assimilate_storage(&mut t)
         .unwrap();

+ 3 - 0
runtime-modules/versioned-store-permissions/Cargo.toml

@@ -5,6 +5,7 @@ authors = ['Joystream contributors']
 edition = '2018'
 
 [dependencies]
+serde = { version = "1.0.101", optional = true, features = ["derive"] }
 codec = { package = 'parity-scale-codec', version = '1.3.1', default-features = false, features = ['derive'] }
 sp-std = { package = 'sp-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
 frame-support = { package = 'frame-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
@@ -17,10 +18,12 @@ versioned-store = { package = 'pallet-versioned-store', default-features = false
 sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
 sp-core = { package = 'sp-core', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
 pallet-timestamp = { package = 'pallet-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
+common = { package = 'pallet-common', default-features = false, path = '../common'}
 
 [features]
 default = ['std']
 std = [
+	'serde',
 	'codec/std',
 	'sp-std/std',
 	'frame-support/std',

+ 4 - 0
runtime-modules/versioned-store-permissions/src/constraint.rs

@@ -1,7 +1,10 @@
 use codec::{Decode, Encode};
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
 use sp_std::collections::btree_set::BTreeSet;
 
 /// Reference to a specific property of a specific class.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
 pub struct PropertyOfClass<ClassId, PropertyIndex> {
     pub class_id: ClassId,
@@ -9,6 +12,7 @@ pub struct PropertyOfClass<ClassId, PropertyIndex> {
 }
 
 /// The type of constraint imposed on referencing a class via class property of type "Internal".
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
 pub enum ReferenceConstraint<ClassId: Ord, PropertyIndex: Ord> {
     /// No property can reference the class.

+ 5 - 1
runtime-modules/versioned-store-permissions/src/credentials.rs

@@ -2,8 +2,12 @@ use codec::{Decode, Encode};
 use sp_std::collections::btree_set::BTreeSet;
 use sp_std::vec::Vec;
 
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Eq, PartialEq, Clone, Debug)]
-pub struct CredentialSet<Credential>(BTreeSet<Credential>);
+pub struct CredentialSet<Credential: Ord>(BTreeSet<Credential>);
 
 impl<Credential> From<Vec<Credential>> for CredentialSet<Credential>
 where

+ 2 - 2
runtime-modules/versioned-store-permissions/src/lib.rs

@@ -88,11 +88,11 @@ pub trait Trait: system::Trait + versioned_store::Trait {
 decl_storage! {
     trait Store for Module<T: Trait> as VersionedStorePermissions {
       /// ClassPermissions of corresponding Classes in the versioned store
-      pub ClassPermissionsByClassId get(fn class_permissions_by_class_id): map hasher(blake2_128_concat)
+      pub ClassPermissionsByClassId get(fn class_permissions_by_class_id) config(): map hasher(blake2_128_concat)
         ClassId => ClassPermissionsType<T>;
 
       /// Owner of an entity in the versioned store. If it is None then it is owned by the system.
-      pub EntityMaintainerByEntityId get(fn entity_maintainer_by_entity_id): map hasher(blake2_128_concat)
+      pub EntityMaintainerByEntityId get(fn entity_maintainer_by_entity_id) config(): map hasher(blake2_128_concat)
         EntityId => Option<T::Credential>;
     }
 }

+ 1 - 1
runtime-modules/versioned-store-permissions/src/mock.rs

@@ -3,6 +3,7 @@
 use crate::*;
 use crate::{Module, Trait};
 
+use common::constraints::InputValidationLengthConstraint;
 use frame_support::{impl_outer_origin, parameter_types};
 use sp_core::H256;
 use sp_runtime::{
@@ -10,7 +11,6 @@ use sp_runtime::{
     traits::{BlakeTwo256, IdentityLookup},
     Perbill,
 };
-use versioned_store::InputValidationLengthConstraint;
 
 impl_outer_origin! {
     pub enum Origin for Runtime {}

+ 5 - 0
runtime-modules/versioned-store-permissions/src/permissions.rs

@@ -4,7 +4,11 @@ use crate::constraint::*;
 use crate::credentials::*;
 use crate::DispatchResult;
 
+#[cfg(feature = "std")]
+use serde::{Deserialize, Serialize};
+
 /// Permissions for an instance of a Class in the versioned store.
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Default, Eq, PartialEq, Clone, Debug)]
 pub struct ClassPermissions<ClassId, Credential, PropertyIndex, BlockNumber>
 where
@@ -133,6 +137,7 @@ where
     }
 }
 
+#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
 #[derive(Encode, Decode, Clone, Debug, Eq, PartialEq)]
 pub struct EntityPermissions<Credential>
 where

+ 2 - 1
runtime-modules/versioned-store/Cargo.toml

@@ -10,6 +10,7 @@ codec = { package = 'parity-scale-codec', version = '1.3.1', default-features =
 sp-std = { package = 'sp-std', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
 frame-support = { package = 'frame-support', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
 system = { package = 'frame-system', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
+common = { package = 'pallet-common', default-features = false, path = '../common'}
 
 [dev-dependencies]
 sp-runtime = { package = 'sp-runtime', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
@@ -17,7 +18,6 @@ sp-io = { package = 'sp-io', default-features = false, git = 'https://github.com
 sp-core = { package = 'sp-core', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
 pallet-timestamp = { package = 'pallet-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4'}
 
-
 [features]
 default = ['std']
 std = [
@@ -26,4 +26,5 @@ std = [
 	'sp-std/std',
 	'frame-support/std',
 	'system/std',
+	'common/std'
 ]

+ 1 - 37
runtime-modules/versioned-store/src/lib.rs

@@ -15,6 +15,7 @@
 use serde::{Deserialize, Serialize};
 
 use codec::{Decode, Encode};
+use common::constraints::InputValidationLengthConstraint;
 use frame_support::{decl_event, decl_module, decl_storage, ensure};
 use sp_std::collections::btree_set::BTreeSet;
 use sp_std::vec;
@@ -67,43 +68,6 @@ const ERROR_VEC_PROP_IS_TOO_LONG: &str = "Vector propery is too long";
 const ERROR_INTERNAL_RPOP_DOES_NOT_MATCH_ITS_CLASS: &str =
     "Internal property does not match its class";
 
-/// Length constraint for input validation
-#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
-pub struct InputValidationLengthConstraint {
-    /// Minimum length
-    pub min: u16,
-
-    /// Difference between minimum length and max length.
-    /// While having max would have been more direct, this
-    /// way makes max < min unrepresentable semantically,
-    /// which is safer.
-    pub max_min_diff: u16,
-}
-
-impl InputValidationLengthConstraint {
-    /// Helper for computing max
-    pub fn max(&self) -> u16 {
-        self.min + self.max_min_diff
-    }
-
-    pub fn ensure_valid(
-        &self,
-        len: usize,
-        too_short_msg: &'static str,
-        too_long_msg: &'static str,
-    ) -> Result<(), &'static str> {
-        let length = len as u16;
-        if length < self.min {
-            Err(too_short_msg)
-        } else if length > self.max() {
-            Err(too_long_msg)
-        } else {
-            Ok(())
-        }
-    }
-}
-
 pub type ClassId = u64;
 pub type EntityId = u64;
 

+ 8 - 5
runtime/src/lib.rs

@@ -15,7 +15,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
 
 mod constants;
 mod integration;
-mod primitives;
+pub mod primitives;
 mod runtime_api;
 #[cfg(test)]
 mod tests; // Runtime integration tests
@@ -47,19 +47,22 @@ pub use runtime_api::*;
 
 use integration::proposals::{CouncilManager, ExtrinsicProposalEncoder, MembershipOriginValidator};
 
-use content_working_group as content_wg;
 use governance::{council, election};
-use storage::{data_directory, data_object_storage_registry, data_object_type_registry};
+use storage::data_object_storage_registry;
 
 // Node dependencies
 pub use common;
+pub use content_working_group as content_wg;
 pub use forum;
 pub use governance::election_params::ElectionParameters;
+pub use membership;
 #[cfg(any(feature = "std", test))]
 pub use pallet_balances::Call as BalancesCall;
 pub use pallet_staking::StakerStatus;
 pub use proposals_codex::ProposalsConfigParameters;
+pub use storage::{data_directory, data_object_type_registry};
 pub use versioned_store;
+pub use versioned_store_permissions;
 pub use working_group;
 
 /// This runtime version.
@@ -597,7 +600,7 @@ construct_runtime!(
         Members: membership::{Module, Call, Storage, Event<T>, Config<T>},
         Forum: forum::{Module, Call, Storage, Event<T>, Config<T>},
         VersionedStore: versioned_store::{Module, Call, Storage, Event<T>, Config},
-        VersionedStorePermissions: versioned_store_permissions::{Module, Call, Storage},
+        VersionedStorePermissions: versioned_store_permissions::{Module, Call, Storage, Config<T>},
         Stake: stake::{Module, Call, Storage},
         Minting: minting::{Module, Call, Storage},
         RecurringRewards: recurring_rewards::{Module, Call, Storage},
@@ -605,7 +608,7 @@ construct_runtime!(
         ContentWorkingGroup: content_wg::{Module, Call, Storage, Event<T>, Config<T>},
         // --- Storage
         DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event<T>, Config<T>},
-        DataDirectory: data_directory::{Module, Call, Storage, Event<T>},
+        DataDirectory: data_directory::{Module, Call, Storage, Event<T>, Config<T>},
         DataObjectStorageRegistry: data_object_storage_registry::{Module, Call, Storage, Event<T>, Config<T>},
         Discovery: service_discovery::{Module, Call, Storage, Event<T>},
         // --- Proposals

+ 133 - 7
utils/chain-spec-builder/src/main.rs

@@ -23,10 +23,11 @@ use ansi_term::Style;
 use rand::{distributions::Alphanumeric, rngs::OsRng, Rng};
 use structopt::StructOpt;
 
-use joystream_node::{
-    chain_spec::{self, chain_spec_properties, AccountId},
-    proposals_config,
+use joystream_node::chain_spec::{
+    self, chain_spec_properties, content_config, forum_config, initial_members, proposals_config,
+    AccountId,
 };
+
 use sc_chain_spec::ChainType;
 use sc_keystore::Store as Keystore;
 use sc_telemetry::TelemetryEndpoints;
@@ -58,6 +59,15 @@ enum ChainSpecBuilder {
         /// The path where the chain spec should be saved.
         #[structopt(long, short, default_value = "./chain_spec.json")]
         chain_spec_path: PathBuf,
+        /// The path to an initial members data
+        #[structopt(long, short)]
+        initial_members_path: Option<PathBuf>,
+        /// The path to an initial forum data
+        #[structopt(long, short)]
+        initial_forum_path: Option<PathBuf>,
+        /// The path to an initial content directory data file
+        #[structopt(long, short)]
+        initial_content_path: Option<PathBuf>,
     },
     /// Create a new chain spec with the given number of authorities and endowed
     /// accounts. Random keys will be generated as required.
@@ -78,6 +88,15 @@ enum ChainSpecBuilder {
         /// `auth-0`, `auth-1`, etc.
         #[structopt(long, short)]
         keystore_path: Option<PathBuf>,
+        /// The path to an initial members data
+        #[structopt(long, short)]
+        initial_members_path: Option<PathBuf>,
+        /// The path to an initial forum data
+        #[structopt(long, short)]
+        initial_forum_path: Option<PathBuf>,
+        /// The path to an initial content directory data file
+        #[structopt(long, short)]
+        initial_content_path: Option<PathBuf>,
     },
 }
 
@@ -93,12 +112,55 @@ impl ChainSpecBuilder {
             } => chain_spec_path.as_path(),
         }
     }
+
+    /// Returns the path to load initial members from
+    fn initial_members_path(&self) -> &Option<PathBuf> {
+        match self {
+            ChainSpecBuilder::New {
+                initial_members_path,
+                ..
+            } => initial_members_path,
+            ChainSpecBuilder::Generate {
+                initial_members_path,
+                ..
+            } => initial_members_path,
+        }
+    }
+
+    /// Returns the path to load initial forum from
+    fn initial_forum_path(&self) -> &Option<PathBuf> {
+        match self {
+            ChainSpecBuilder::New {
+                initial_forum_path, ..
+            } => initial_forum_path,
+            ChainSpecBuilder::Generate {
+                initial_forum_path, ..
+            } => initial_forum_path,
+        }
+    }
+
+    /// Returns the path to load initial platform content from
+    fn initial_content_path(&self) -> &Option<PathBuf> {
+        match self {
+            ChainSpecBuilder::New {
+                initial_content_path,
+                ..
+            } => initial_content_path,
+            ChainSpecBuilder::Generate {
+                initial_content_path,
+                ..
+            } => initial_content_path,
+        }
+    }
 }
 
 fn genesis_constructor(
     authority_seeds: &[String],
     endowed_accounts: &[AccountId],
     sudo_account: &AccountId,
+    initial_members_path: &Option<PathBuf>,
+    initial_forum_path: &Option<PathBuf>,
+    initial_content_path: &Option<PathBuf>,
 ) -> chain_spec::GenesisConfig {
     let authorities = authority_seeds
         .iter()
@@ -106,11 +168,52 @@ fn genesis_constructor(
         .map(chain_spec::get_authority_keys_from_seed)
         .collect::<Vec<_>>();
 
+    let members = if let Some(path) = initial_members_path {
+        initial_members::from_json(path.as_path())
+    } else {
+        initial_members::none()
+    };
+
+    let forum_cfg = if let Some(path) = initial_forum_path {
+        forum_config::from_json(sudo_account.clone(), path.as_path())
+    } else {
+        forum_config::empty(sudo_account.clone())
+    };
+
+    let (
+        versioned_store_cfg,
+        versioned_store_permissions_cfg,
+        data_directory_config,
+        content_working_group_config,
+    ) = if let Some(path) = initial_content_path {
+        let path = path.as_path();
+
+        (
+            content_config::versioned_store_config_from_json(path),
+            content_config::versioned_store_permissions_config_from_json(path),
+            content_config::data_directory_config_from_json(path),
+            content_config::content_working_group_config_from_json(path),
+        )
+    } else {
+        (
+            content_config::empty_versioned_store_config(),
+            content_config::empty_versioned_store_permissions_config(),
+            content_config::empty_data_directory_config(),
+            content_config::empty_content_working_group_config(),
+        )
+    };
+
     chain_spec::testnet_genesis(
         authorities,
         sudo_account.clone(),
         endowed_accounts.to_vec(),
         proposals_config::default(),
+        members,
+        forum_cfg,
+        versioned_store_cfg,
+        versioned_store_permissions_cfg,
+        data_directory_config,
+        content_working_group_config,
     )
 }
 
@@ -118,6 +221,9 @@ fn generate_chain_spec(
     authority_seeds: Vec<String>,
     endowed_accounts: Vec<String>,
     sudo_account: String,
+    initial_members_path: Option<PathBuf>,
+    initial_forum_path: Option<PathBuf>,
+    initial_content_path: Option<PathBuf>,
 ) -> Result<String, String> {
     let parse_account = |address: &String| {
         AccountId::from_string(address)
@@ -142,7 +248,16 @@ fn generate_chain_spec(
         "Joystream Testnet",
         "joy_testnet",
         ChainType::Development,
-        move || genesis_constructor(&authority_seeds, &endowed_accounts, &sudo_account),
+        move || {
+            genesis_constructor(
+                &authority_seeds,
+                &endowed_accounts,
+                &sudo_account,
+                &initial_members_path,
+                &initial_forum_path,
+                &initial_content_path,
+            )
+        },
         vec![],
         Some(telemetry_endpoints),
         Some(&*"/joy/testnet/0"),
@@ -212,6 +327,9 @@ fn main() -> Result<(), String> {
 
     let builder = ChainSpecBuilder::from_args();
     let chain_spec_path = builder.chain_spec_path().to_path_buf();
+    let initial_members_path = builder.initial_members_path().clone();
+    let initial_forum_path = builder.initial_forum_path().clone();
+    let initial_content_path = builder.initial_content_path().clone();
 
     let (authority_seeds, endowed_accounts, sudo_account) = match builder {
         ChainSpecBuilder::Generate {
@@ -240,8 +358,9 @@ fn main() -> Result<(), String> {
                 })
                 .collect();
 
-            let sudo_account =
-                chain_spec::get_account_id_from_seed::<sr25519::Public>(&sudo_seed).to_ss58check();
+            let sudo_account_id =
+                chain_spec::get_account_id_from_seed::<sr25519::Public>(&sudo_seed);
+            let sudo_account = sudo_account_id.to_ss58check();
 
             (authority_seeds, endowed_accounts, sudo_account)
         }
@@ -253,7 +372,14 @@ fn main() -> Result<(), String> {
         } => (authority_seeds, endowed_accounts, sudo_account),
     };
 
-    let json = generate_chain_spec(authority_seeds, endowed_accounts, sudo_account)?;
+    let json = generate_chain_spec(
+        authority_seeds,
+        endowed_accounts,
+        sudo_account,
+        initial_members_path,
+        initial_forum_path,
+        initial_content_path,
+    )?;
 
     fs::write(chain_spec_path, json).map_err(|err| err.to_string())
 }

Some files were not shown because too many files changed in this diff