Эх сурвалжийг харах

Merge latest iznik changes, resolve chain_spec merge conflict

iorveth 4 жил өмнө
parent
commit
4d15ae3ed6
100 өөрчлөгдсөн 2167 нэмэгдсэн , 1372 устгасан
  1. 25 0
      .github/workflows/run-network-tests.yml
  2. 6 5
      .travis.yml
  3. 194 2
      Cargo.lock
  4. 2 2
      README.md
  5. 31 31
      cli/README.md
  6. 2 2
      cli/package.json
  7. 81 108
      cli/src/Api.ts
  8. 101 113
      cli/src/Types.ts
  9. 5 3
      cli/src/base/AccountsCommandBase.ts
  10. 42 27
      cli/src/base/ApiCommandBase.ts
  11. 4 4
      cli/src/base/WorkingGroupsCommandBase.ts
  12. 2 3
      cli/src/commands/account/current.ts
  13. 4 4
      cli/src/commands/account/transferTokens.ts
  14. 11 14
      cli/src/commands/api/inspect.ts
  15. 1 0
      cli/src/commands/working-groups/application.ts
  16. 2 2
      cli/src/commands/working-groups/createOpening.ts
  17. 3 5
      cli/src/commands/working-groups/decreaseWorkerStake.ts
  18. 3 4
      cli/src/commands/working-groups/evictWorker.ts
  19. 4 8
      cli/src/commands/working-groups/fillOpening.ts
  20. 1 0
      cli/src/commands/working-groups/opening.ts
  21. 2 5
      cli/src/commands/working-groups/slashWorker.ts
  22. 2 4
      cli/src/commands/working-groups/startAcceptingApplications.ts
  23. 2 4
      cli/src/commands/working-groups/startReviewPeriod.ts
  24. 3 4
      cli/src/commands/working-groups/terminateApplication.ts
  25. 2 2
      cli/src/commands/working-groups/updateRewardAccount.ts
  26. 2 2
      cli/src/commands/working-groups/updateRoleAccount.ts
  27. 2 2
      cli/src/commands/working-groups/updateWorkerReward.ts
  28. 2 2
      cli/src/helpers/validation.ts
  29. 11 9
      cli/src/promptOptions/addWorkerOpening.ts
  30. 5 1
      cli/tsconfig.json
  31. 22 0
      devops/ansible/build-and-run-tests-exported-chainspec-playbook.yml
  32. 6 1
      devops/ansible/build-and-run-tests-single-node-playbook.yml
  33. 6 1
      devops/ansible/build-and-run-tests-two-nodes-playbook.yml
  34. 6 1
      devops/ansible/build-image-playbook.yml
  35. 2 2
      devops/ansible/docker-compose.yml
  36. 4 0
      devops/ansible/roles/alter_block_creation_time/tasks/main.yml
  37. 1 1
      devops/ansible/roles/build_docker_image/tasks/main.yml
  38. 16 5
      devops/ansible/roles/install_dependencies/tasks/main.yml
  39. 38 0
      devops/ansible/roles/run_tests_exported_chainspec/tasks/main.yml
  40. 16 5
      devops/ansible/roles/run_tests_single_node/tasks/main.yml
  41. 0 37
      devops/dockerfiles/ansible-node/Dockerfile
  42. 1 1
      devops/dockerfiles/node-and-runtime/Dockerfile
  43. 2 2
      devops/dockerfiles/rust-builder/Dockerfile
  44. 5 5
      devops/git-hooks/pre-push
  45. 4 3
      node/Cargo.toml
  46. 3 3
      node/README.md
  47. 0 0
      node/res/acropolis_members.json
  48. 0 0
      node/res/forum_data_acropolis_encoded.json
  49. 0 0
      node/res/forum_data_acropolis_serialized.json
  50. 0 1
      node/res/forum_data_empty.json
  51. 391 0
      node/src/chain_spec/content_config.rs
  52. 149 0
      node/src/chain_spec/forum_config.rs
  53. 18 0
      node/src/chain_spec/initial_balances.rs
  54. 13 0
      node/src/chain_spec/initial_members.rs
  55. 140 132
      node/src/chain_spec/mod.rs
  56. 2 2
      node/src/chain_spec/proposals_config.rs
  57. 0 90
      node/src/forum_config/from_encoded.rs
  58. 0 51
      node/src/forum_config/from_serialized.rs
  59. 0 10
      node/src/forum_config/mod.rs
  60. 0 3
      node/src/lib.rs
  61. 0 38
      node/src/members_config.rs
  62. 244 247
      node/src/service.rs
  63. 7 11
      package.json
  64. 0 9
      pioneer/.eslintignore
  65. 2 0
      pioneer/.eslintrc.js
  66. 39 73
      pioneer/.storybook/webpack.config.js
  67. 5 4
      pioneer/package.json
  68. 90 90
      pioneer/packages/apps-config/src/settings/endpoints.ts
  69. 2 2
      pioneer/packages/apps-routing/src/accounts.ts
  70. 16 0
      pioneer/packages/apps-routing/src/index.ts
  71. 17 0
      pioneer/packages/apps-routing/src/joy-election.ts
  72. 15 0
      pioneer/packages/apps-routing/src/joy-forum.ts
  73. 15 0
      pioneer/packages/apps-routing/src/joy-media.ts
  74. 16 0
      pioneer/packages/apps-routing/src/joy-proposals.ts
  75. 6 8
      pioneer/packages/apps-routing/src/joy-roles.ts
  76. 20 0
      pioneer/packages/apps-routing/src/memo.ts
  77. 1 1
      pioneer/packages/apps-routing/src/staking.ts
  78. 2 0
      pioneer/packages/apps-routing/src/types.ts
  79. 0 0
      pioneer/packages/apps/public/images/default-thumbnail.png
  80. 1 0
      pioneer/packages/apps/public/locales/en/app-accounts.json
  81. 1 0
      pioneer/packages/apps/public/locales/en/index.json
  82. 0 1
      pioneer/packages/apps/public/locales/en/joy-settings.json
  83. 0 1
      pioneer/packages/apps/public/locales/en/joy-utils-old.json
  84. 4 1
      pioneer/packages/apps/public/locales/en/joy-utils.json
  85. 1 1
      pioneer/packages/apps/src/Content/NotFound.tsx
  86. 24 6
      pioneer/packages/apps/src/Content/index.tsx
  87. 5 2
      pioneer/packages/apps/src/SideBar/Item.tsx
  88. 0 2
      pioneer/packages/apps/src/SideBar/index.tsx
  89. 14 7
      pioneer/packages/apps/src/initSettings.ts
  90. 14 0
      pioneer/packages/apps/webpack.base.config.js
  91. 0 0
      pioneer/packages/joy-election/.skip-build
  92. 3 3
      pioneer/packages/joy-election/package.json
  93. 9 6
      pioneer/packages/joy-election/src/Applicant.tsx
  94. 44 24
      pioneer/packages/joy-election/src/Applicants.tsx
  95. 19 21
      pioneer/packages/joy-election/src/ApplyForm.tsx
  96. 2 2
      pioneer/packages/joy-election/src/CandidatePreview.tsx
  97. 8 7
      pioneer/packages/joy-election/src/Council.tsx
  98. 83 53
      pioneer/packages/joy-election/src/Dashboard.tsx
  99. 21 18
      pioneer/packages/joy-election/src/Reveals.tsx
  100. 15 6
      pioneer/packages/joy-election/src/SealedVote.tsx

+ 25 - 0
.github/workflows/run-network-tests.yml

@@ -0,0 +1,25 @@
+name: run-network-tests
+on:
+  pull_request:
+    types: [labeled]
+  workflow_dispatch:
+
+jobs:
+  run_ansible_tests:
+    if: github.event.label.name == 'run-network-tests' || github.event.action == null
+    name: run network tests using ansible
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+      - uses: actions/checkout@v1
+      - uses: actions/setup-node@v1
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: install toolchain
+        run: curl https://getsubstrate.io -sSf | bash -s -- --fast
+      - name: ansible build and tests
+        run: |
+          cd ./devops/ansible
+          ansible-playbook -i hosts build-and-run-tests-single-node-playbook.yml --become -v

+ 6 - 5
.travis.yml

@@ -24,7 +24,7 @@ before_install:
     fi
 
 install:
-  - rustup install nightly-2020-05-23
+  - rustup install nightly-2020-05-23 --force
   - rustup target add wasm32-unknown-unknown --toolchain nightly-2020-05-23
   # travis installs rust using rustup with the "minimal" profile so these tools are not installed by default
   - rustup component add rustfmt
@@ -34,9 +34,10 @@ before_script:
   - cargo fmt --all -- --check
 
 script:
-  # we set release as build type for all steps to benefit from already compiled packages in prior steps
-  - BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release -- -D warnings
-  - BUILD_DUMMY_WASM_BINARY=1 cargo test --release --verbose --all
-  - TRIGGER_WASM_BUILD=1 WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release -p joystream-node
+  - export WASM_BUILD_TOOLCHAIN=nightly-2020-05-23
+  - BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings
+  - travis_wait 75 cargo test --release --verbose --all -- --ignored
+  - cargo build --release
   - ls -l ./target/release/wbuild/joystream-node-runtime/
   - ./target/release/joystream-node --version
+  - ./target/release/chain-spec-builder --version

+ 194 - 2
Cargo.lock

@@ -565,6 +565,7 @@ name = "chain-spec-builder"
 version = "3.0.0"
 dependencies = [
  "ansi_term 0.12.1",
+ "enum-utils",
  "joystream-node",
  "rand 0.7.3",
  "sc-chain-spec",
@@ -927,6 +928,30 @@ dependencies = [
  "syn 0.11.11",
 ]
 
+[[package]]
+name = "enum-utils"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed327f716d0d351d86c9fd3398d20ee39ad8f681873cc081da2ca1c10fed398a"
+dependencies = [
+ "enum-utils-from-str",
+ "failure",
+ "proc-macro2",
+ "quote 1.0.7",
+ "serde_derive_internals",
+ "syn 1.0.17",
+]
+
+[[package]]
+name = "enum-utils-from-str"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49be08bad6e4ca87b2b8e74146987d4e5cb3b7512efa50ef505b51a22227ee1"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.7",
+]
+
 [[package]]
 name = "env_logger"
 version = "0.7.1"
@@ -1968,12 +1993,13 @@ dependencies = [
 
 [[package]]
 name = "joystream-node"
-version = "3.0.0"
+version = "3.1.0"
 dependencies = [
  "frame-benchmarking",
  "frame-benchmarking-cli",
  "frame-system",
  "futures 0.3.4",
+ "hex",
  "joystream-node-runtime",
  "jsonrpc-core",
  "node-inspect",
@@ -1998,6 +2024,7 @@ dependencies = [
  "sc-network",
  "sc-rpc-api",
  "sc-service",
+ "sc-service-test",
  "sc-transaction-pool",
  "serde",
  "serde_json",
@@ -2026,7 +2053,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node-runtime"
-version = "7.0.0"
+version = "7.3.0"
 dependencies = [
  "frame-benchmarking",
  "frame-executive",
@@ -2069,6 +2096,7 @@ dependencies = [
  "pallet-token-mint",
  "pallet-transaction-payment",
  "pallet-transaction-payment-rpc-runtime-api",
+ "pallet-utility",
  "pallet-versioned-store",
  "pallet-versioned-store-permissions",
  "pallet-working-group",
@@ -3815,12 +3843,29 @@ dependencies = [
  "sp-std",
 ]
 
+[[package]]
+name = "pallet-utility"
+version = "2.0.0-rc4"
+source = "git+https://github.com/paritytech/substrate.git?rev=00768a1f21a579c478fe5d4f51e1fa71f7db9fd4#00768a1f21a579c478fe5d4f51e1fa71f7db9fd4"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "parity-scale-codec",
+ "serde",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std",
+]
+
 [[package]]
 name = "pallet-versioned-store"
 version = "3.0.0"
 dependencies = [
  "frame-support",
  "frame-system",
+ "pallet-common",
  "pallet-timestamp",
  "parity-scale-codec",
  "serde",
@@ -3836,9 +3881,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",
@@ -5616,6 +5663,43 @@ dependencies = [
  "wasm-timer",
 ]
 
+[[package]]
+name = "sc-service-test"
+version = "2.0.0-rc4"
+source = "git+https://github.com/paritytech/substrate.git?rev=00768a1f21a579c478fe5d4f51e1fa71f7db9fd4#00768a1f21a579c478fe5d4f51e1fa71f7db9fd4"
+dependencies = [
+ "env_logger",
+ "fdlimit",
+ "futures 0.1.29",
+ "futures 0.3.4",
+ "hex-literal",
+ "log",
+ "parity-scale-codec",
+ "parking_lot 0.10.2",
+ "sc-block-builder",
+ "sc-client-api",
+ "sc-client-db",
+ "sc-executor",
+ "sc-light",
+ "sc-network",
+ "sc-service",
+ "sp-api",
+ "sp-blockchain",
+ "sp-consensus",
+ "sp-core",
+ "sp-externalities",
+ "sp-panic-handler",
+ "sp-runtime",
+ "sp-state-machine",
+ "sp-storage",
+ "sp-transaction-pool",
+ "sp-trie",
+ "substrate-test-runtime",
+ "substrate-test-runtime-client",
+ "tempfile",
+ "tokio 0.1.22",
+]
+
 [[package]]
 name = "sc-state-db"
 version = "0.8.0-rc4"
@@ -5835,6 +5919,17 @@ dependencies = [
  "syn 1.0.17",
 ]
 
+[[package]]
+name = "serde_derive_internals"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.7",
+ "syn 1.0.17",
+]
+
 [[package]]
 name = "serde_json"
 version = "1.0.57"
@@ -6181,6 +6276,20 @@ dependencies = [
  "wasm-timer",
 ]
 
+[[package]]
+name = "sp-consensus-aura"
+version = "0.8.0-rc4"
+source = "git+https://github.com/paritytech/substrate.git?rev=00768a1f21a579c478fe5d4f51e1fa71f7db9fd4#00768a1f21a579c478fe5d4f51e1fa71f7db9fd4"
+dependencies = [
+ "parity-scale-codec",
+ "sp-api",
+ "sp-application-crypto",
+ "sp-inherents",
+ "sp-runtime",
+ "sp-std",
+ "sp-timestamp",
+]
+
 [[package]]
 name = "sp-consensus-babe"
 version = "0.8.0-rc4"
@@ -6792,6 +6901,89 @@ dependencies = [
  "tokio 0.2.22",
 ]
 
+[[package]]
+name = "substrate-test-client"
+version = "2.0.0-rc4"
+source = "git+https://github.com/paritytech/substrate.git?rev=00768a1f21a579c478fe5d4f51e1fa71f7db9fd4#00768a1f21a579c478fe5d4f51e1fa71f7db9fd4"
+dependencies = [
+ "futures 0.3.4",
+ "hash-db",
+ "parity-scale-codec",
+ "sc-client-api",
+ "sc-client-db",
+ "sc-consensus",
+ "sc-executor",
+ "sc-light",
+ "sc-service",
+ "sp-blockchain",
+ "sp-consensus",
+ "sp-core",
+ "sp-keyring",
+ "sp-runtime",
+ "sp-state-machine",
+]
+
+[[package]]
+name = "substrate-test-runtime"
+version = "2.0.0-rc4"
+source = "git+https://github.com/paritytech/substrate.git?rev=00768a1f21a579c478fe5d4f51e1fa71f7db9fd4#00768a1f21a579c478fe5d4f51e1fa71f7db9fd4"
+dependencies = [
+ "cfg-if",
+ "frame-executive",
+ "frame-support",
+ "frame-system",
+ "frame-system-rpc-runtime-api",
+ "log",
+ "memory-db",
+ "pallet-babe",
+ "pallet-timestamp",
+ "parity-scale-codec",
+ "parity-util-mem",
+ "sc-service",
+ "serde",
+ "sp-api",
+ "sp-application-crypto",
+ "sp-block-builder",
+ "sp-consensus-aura",
+ "sp-consensus-babe",
+ "sp-core",
+ "sp-finality-grandpa",
+ "sp-inherents",
+ "sp-io",
+ "sp-keyring",
+ "sp-offchain",
+ "sp-runtime",
+ "sp-runtime-interface",
+ "sp-session",
+ "sp-std",
+ "sp-transaction-pool",
+ "sp-trie",
+ "sp-version",
+ "substrate-wasm-builder-runner",
+ "trie-db",
+]
+
+[[package]]
+name = "substrate-test-runtime-client"
+version = "2.0.0-rc4"
+source = "git+https://github.com/paritytech/substrate.git?rev=00768a1f21a579c478fe5d4f51e1fa71f7db9fd4#00768a1f21a579c478fe5d4f51e1fa71f7db9fd4"
+dependencies = [
+ "futures 0.3.4",
+ "parity-scale-codec",
+ "sc-block-builder",
+ "sc-client-api",
+ "sc-consensus",
+ "sc-light",
+ "sc-service",
+ "sp-api",
+ "sp-blockchain",
+ "sp-consensus",
+ "sp-core",
+ "sp-runtime",
+ "substrate-test-client",
+ "substrate-test-runtime",
+]
+
 [[package]]
 name = "substrate-wasm-builder-runner"
 version = "1.0.6"

+ 2 - 2
README.md

@@ -93,8 +93,8 @@ You can also run your our own joystream-node:
 
 ```sh
 git checkout master
-cargo build --release
-cargo run --release -- --pruning archive --chain testnets/rome.json
+WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release
+./target/release/joystream-node -- --pruning archive --chain testnets/rome.json
 ```
 
 Wait for the node to sync to the latest block, then change pioneer settings "remote node" option to "Local Node", or follow the link below:

+ 31 - 31
cli/README.md

@@ -108,7 +108,7 @@ OPTIONS
   --showSpecial  Whether to show special (DEV chain) accounts
 ```
 
-_See code: [src/commands/account/choose.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/choose.ts)_
+_See code: [src/commands/account/choose.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/choose.ts)_
 
 ## `joystream-cli account:create NAME`
 
@@ -122,7 +122,7 @@ ARGUMENTS
   NAME  Account name
 ```
 
-_See code: [src/commands/account/create.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/create.ts)_
+_See code: [src/commands/account/create.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/create.ts)_
 
 ## `joystream-cli account:current`
 
@@ -137,7 +137,7 @@ ALIASES
   $ joystream-cli account:default
 ```
 
-_See code: [src/commands/account/current.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/current.ts)_
+_See code: [src/commands/account/current.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/current.ts)_
 
 ## `joystream-cli account:export PATH`
 
@@ -154,7 +154,7 @@ OPTIONS
   -a, --all  If provided, exports all existing accounts into "exported_accounts" folder inside given path
 ```
 
-_See code: [src/commands/account/export.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/export.ts)_
+_See code: [src/commands/account/export.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/export.ts)_
 
 ## `joystream-cli account:forget`
 
@@ -165,7 +165,7 @@ USAGE
   $ joystream-cli account:forget
 ```
 
-_See code: [src/commands/account/forget.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/forget.ts)_
+_See code: [src/commands/account/forget.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/forget.ts)_
 
 ## `joystream-cli account:import BACKUPFILEPATH`
 
@@ -179,7 +179,7 @@ ARGUMENTS
   BACKUPFILEPATH  Path to account backup JSON file
 ```
 
-_See code: [src/commands/account/import.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/import.ts)_
+_See code: [src/commands/account/import.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/import.ts)_
 
 ## `joystream-cli account:transferTokens RECIPIENT AMOUNT`
 
@@ -194,7 +194,7 @@ ARGUMENTS
   AMOUNT     Amount of tokens to transfer
 ```
 
-_See code: [src/commands/account/transferTokens.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/account/transferTokens.ts)_
+_See code: [src/commands/account/transferTokens.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/transferTokens.ts)_
 
 ## `joystream-cli api:getUri`
 
@@ -205,7 +205,7 @@ USAGE
   $ joystream-cli api:getUri
 ```
 
-_See code: [src/commands/api/getUri.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/api/getUri.ts)_
+_See code: [src/commands/api/getUri.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/api/getUri.ts)_
 
 ## `joystream-cli api:inspect`
 
@@ -221,15 +221,15 @@ OPTIONS
       If no "--method" flag is provided then all methods in that module will be listed along with the descriptions.
 
   -a, --callArgs=callArgs
-      Specifies the arguments to use when calling a method. Multiple arguments can be separated with a comma, ie.
+      Specifies the arguments to use when calling a method. Multiple arguments can be separated with a comma, ie. 
       "-a=arg1,arg2".
       You can omit this flag even if the method requires some aguments.
       In that case you will be promted to provide value for each required argument.
-      Ommiting this flag is recommended when input parameters are of more complex types (and it's hard to specify them as
+      Ommiting this flag is recommended when input parameters are of more complex types (and it's hard to specify them as 
       just simple comma-separated strings)
 
   -e, --exec
-      Provide this flag if you want to execute the actual call, instead of displaying the method description (which is
+      Provide this flag if you want to execute the actual call, instead of displaying the method description (which is 
       default)
 
   -m, --method=method
@@ -249,7 +249,7 @@ EXAMPLES
   $ api:inspect -t=query -M=members -m=membershipById -e -a=1
 ```
 
-_See code: [src/commands/api/inspect.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/api/inspect.ts)_
+_See code: [src/commands/api/inspect.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/api/inspect.ts)_
 
 ## `joystream-cli api:setUri [URI]`
 
@@ -263,7 +263,7 @@ ARGUMENTS
   URI  Uri of the node api WS provider (if skipped, a prompt will be displayed)
 ```
 
-_See code: [src/commands/api/setUri.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/api/setUri.ts)_
+_See code: [src/commands/api/setUri.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/api/setUri.ts)_
 
 ## `joystream-cli autocomplete [SHELL]`
 
@@ -297,7 +297,7 @@ USAGE
   $ joystream-cli council:info
 ```
 
-_See code: [src/commands/council/info.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/council/info.ts)_
+_See code: [src/commands/council/info.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/council/info.ts)_
 
 ## `joystream-cli help [COMMAND]`
 
@@ -333,7 +333,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/application.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/application.ts)_
+_See code: [src/commands/working-groups/application.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/application.ts)_
 
 ## `joystream-cli working-groups:createOpening`
 
@@ -359,7 +359,7 @@ OPTIONS
   -s, --skipPrompts          Whether to skip all prompts when adding from draft (will use all default values)
 ```
 
-_See code: [src/commands/working-groups/createOpening.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/createOpening.ts)_
+_See code: [src/commands/working-groups/createOpening.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/createOpening.ts)_
 
 ## `joystream-cli working-groups:decreaseWorkerStake WORKERID`
 
@@ -378,7 +378,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/decreaseWorkerStake.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/decreaseWorkerStake.ts)_
+_See code: [src/commands/working-groups/decreaseWorkerStake.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/decreaseWorkerStake.ts)_
 
 ## `joystream-cli working-groups:evictWorker WORKERID`
 
@@ -397,7 +397,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/evictWorker.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/evictWorker.ts)_
+_See code: [src/commands/working-groups/evictWorker.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/evictWorker.ts)_
 
 ## `joystream-cli working-groups:fillOpening WGOPENINGID`
 
@@ -416,7 +416,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/fillOpening.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/fillOpening.ts)_
+_See code: [src/commands/working-groups/fillOpening.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/fillOpening.ts)_
 
 ## `joystream-cli working-groups:increaseStake`
 
@@ -432,7 +432,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/increaseStake.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/increaseStake.ts)_
+_See code: [src/commands/working-groups/increaseStake.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/increaseStake.ts)_
 
 ## `joystream-cli working-groups:leaveRole`
 
@@ -448,7 +448,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/leaveRole.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/leaveRole.ts)_
+_See code: [src/commands/working-groups/leaveRole.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/leaveRole.ts)_
 
 ## `joystream-cli working-groups:opening WGOPENINGID`
 
@@ -467,7 +467,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/opening.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/opening.ts)_
+_See code: [src/commands/working-groups/opening.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/opening.ts)_
 
 ## `joystream-cli working-groups:openings`
 
@@ -483,7 +483,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/openings.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/openings.ts)_
+_See code: [src/commands/working-groups/openings.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/openings.ts)_
 
 ## `joystream-cli working-groups:overview`
 
@@ -499,7 +499,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/overview.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/overview.ts)_
+_See code: [src/commands/working-groups/overview.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/overview.ts)_
 
 ## `joystream-cli working-groups:slashWorker WORKERID`
 
@@ -518,7 +518,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/slashWorker.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/slashWorker.ts)_
+_See code: [src/commands/working-groups/slashWorker.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/slashWorker.ts)_
 
 ## `joystream-cli working-groups:startAcceptingApplications WGOPENINGID`
 
@@ -537,7 +537,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/startAcceptingApplications.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/startAcceptingApplications.ts)_
+_See code: [src/commands/working-groups/startAcceptingApplications.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/startAcceptingApplications.ts)_
 
 ## `joystream-cli working-groups:startReviewPeriod WGOPENINGID`
 
@@ -556,7 +556,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/startReviewPeriod.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/startReviewPeriod.ts)_
+_See code: [src/commands/working-groups/startReviewPeriod.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/startReviewPeriod.ts)_
 
 ## `joystream-cli working-groups:terminateApplication WGAPPLICATIONID`
 
@@ -575,7 +575,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/terminateApplication.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/terminateApplication.ts)_
+_See code: [src/commands/working-groups/terminateApplication.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/terminateApplication.ts)_
 
 ## `joystream-cli working-groups:updateRewardAccount [ACCOUNTADDRESS]`
 
@@ -594,7 +594,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/updateRewardAccount.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/updateRewardAccount.ts)_
+_See code: [src/commands/working-groups/updateRewardAccount.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/updateRewardAccount.ts)_
 
 ## `joystream-cli working-groups:updateRoleAccount [ACCOUNTADDRESS]`
 
@@ -613,7 +613,7 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/updateRoleAccount.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/updateRoleAccount.ts)_
+_See code: [src/commands/working-groups/updateRoleAccount.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/updateRoleAccount.ts)_
 
 ## `joystream-cli working-groups:updateWorkerReward WORKERID`
 
@@ -632,5 +632,5 @@ OPTIONS
                      Available values are: storageProviders.
 ```
 
-_See code: [src/commands/working-groups/updateWorkerReward.ts](https://github.com/Joystream/substrate-runtime-joystream/blob/master/cli/src/commands/working-groups/updateWorkerReward.ts)_
+_See code: [src/commands/working-groups/updateWorkerReward.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/updateWorkerReward.ts)_
 <!-- commandsstop -->

+ 2 - 2
cli/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@joystream/cli",
   "description": "Command Line Interface for Joystream community and governance activities",
-  "version": "0.1.0",
+  "version": "0.2.0",
   "author": "Leszek Wiesner",
   "bin": {
     "joystream-cli": "./bin/run"
@@ -15,7 +15,7 @@
     "@oclif/plugin-help": "^2.2.3",
     "@oclif/plugin-not-found": "^1.2.4",
     "@oclif/plugin-warn-if-update-available": "^1.7.0",
-    "@polkadot/api": "^0.96.1",
+    "@polkadot/api": "1.26.1",
     "@types/inquirer": "^6.5.0",
     "@types/proper-lockfile": "^4.1.1",
     "@types/slug": "^0.9.1",

+ 81 - 108
cli/src/Api.ts

@@ -1,13 +1,12 @@
 import BN from 'bn.js'
-import { registerJoystreamTypes } from '@joystream/types/'
+import { types } from '@joystream/types/'
 import { ApiPromise, WsProvider } from '@polkadot/api'
-import { QueryableStorageMultiArg } from '@polkadot/api/types'
+import { QueryableStorageMultiArg, SubmittableExtrinsic, QueryableStorageEntry } from '@polkadot/api/types'
 import { formatBalance } from '@polkadot/util'
-import { Hash, Balance, Moment, BlockNumber } from '@polkadot/types/interfaces'
+import { Balance, Moment, BlockNumber } from '@polkadot/types/interfaces'
 import { KeyringPair } from '@polkadot/keyring/types'
-import { Codec } from '@polkadot/types/types'
-import { Option, Vec } from '@polkadot/types'
-import { u32 } from '@polkadot/types/primitive'
+import { Codec, CodecArg } from '@polkadot/types/types'
+import { Option, Vec, UInt } from '@polkadot/types'
 import {
   AccountSummary,
   CouncilInfoObj,
@@ -24,7 +23,7 @@ import {
   UnstakingPeriods,
   StakingPolicyUnstakingPeriodKey,
 } from './Types'
-import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types'
+import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { CLIError } from '@oclif/errors'
 import ExitCodes from './ExitCodes'
 import {
@@ -46,12 +45,11 @@ import {
 import { MemberId, Membership } from '@joystream/types/members'
 import { RewardRelationship, RewardRelationshipId } from '@joystream/types/recurring-rewards'
 import { Stake, StakeId } from '@joystream/types/stake'
-import { LinkageResult } from '@polkadot/types/codec/Linkage'
 
 import { InputValidationLengthConstraint } from '@joystream/types/common'
 
 export const DEFAULT_API_URI = 'ws://localhost:9944/'
-const DEFAULT_DECIMALS = new u32(12)
+const DEFAULT_DECIMALS = new BN(12)
 
 // Mapping of working group to api module
 export const apiModuleByGroup: { [key in WorkingGroups]: string } = {
@@ -72,8 +70,7 @@ export default class Api {
 
   private static async initApi(apiUri: string = DEFAULT_API_URI): Promise<ApiPromise> {
     const wsProvider: WsProvider = new WsProvider(apiUri)
-    registerJoystreamTypes()
-    const api = await ApiPromise.create({ provider: wsProvider })
+    const api = await ApiPromise.create({ provider: wsProvider, types })
 
     // Initializing some api params based on pioneer/packages/react-api/Api.tsx
     const [properties] = await Promise.all([api.rpc.system.properties()])
@@ -95,23 +92,27 @@ export default class Api {
     return new Api(originalApi)
   }
 
-  private async queryMultiOnce(queries: Parameters<typeof ApiPromise.prototype.queryMulti>[0]): Promise<Codec[]> {
-    let results: Codec[] = []
-
-    const unsub = await this._api.queryMulti(queries, (res) => {
-      results = res
+  private queryMultiOnce(queries: Parameters<typeof ApiPromise.prototype.queryMulti>[0]): Promise<Codec[]> {
+    return new Promise((resolve, reject) => {
+      let unsub: () => void
+      this._api
+        .queryMulti(queries, (res) => {
+          // unsub should already be set at this point
+          if (!unsub) {
+            reject(new CLIError('API queryMulti issue - unsub method not set!', { exit: ExitCodes.ApiError }))
+          }
+          unsub()
+          resolve(res)
+        })
+        .then((unsubscribe) => (unsub = unsubscribe))
+        .catch((e) => reject(e))
     })
-    unsub()
-
-    if (!results.length || results.length !== queries.length) {
-      throw new CLIError('API querying issue', { exit: ExitCodes.ApiError })
-    }
-
-    return results
   }
 
-  async getAccountsBalancesInfo(accountAddresses: string[]): Promise<DerivedBalances[]> {
-    const accountsBalances: DerivedBalances[] = await this._api.derive.balances.votingBalances(accountAddresses)
+  async getAccountsBalancesInfo(accountAddresses: string[]): Promise<DeriveBalancesAll[]> {
+    const accountsBalances: DeriveBalancesAll[] = await Promise.all(
+      accountAddresses.map((addr) => this._api.derive.balances.all(addr))
+    )
 
     return accountsBalances
   }
@@ -119,7 +120,7 @@ export default class Api {
   // Get on-chain data related to given account.
   // For now it's just account balances
   async getAccountSummary(accountAddresses: string): Promise<AccountSummary> {
-    const balances: DerivedBalances = (await this.getAccountsBalancesInfo([accountAddresses]))[0]
+    const balances: DeriveBalancesAll = (await this.getAccountsBalancesInfo([accountAddresses]))[0]
     // TODO: Some more information can be fetched here in the future
 
     return { balances }
@@ -146,34 +147,29 @@ export default class Api {
     return createCouncilInfoObj(...results)
   }
 
-  // TODO: This formula is probably not too good, so some better implementation will be required in the future
-  async estimateFee(account: KeyringPair, recipientAddr: string, amount: BN): Promise<BN> {
-    const transfer = this._api.tx.balances.transfer(recipientAddr, amount)
-    const signature = account.sign(transfer.toU8a())
-    const transactionByteSize: BN = new BN(transfer.encodedLength + signature.length)
-
-    const fees: DerivedFees = await this._api.derive.balances.fees()
-
-    const estimatedFee = fees.transactionBaseFee.add(fees.transactionByteFee.mul(transactionByteSize))
-
-    return estimatedFee
+  async estimateFee(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<Balance> {
+    const paymentInfo = await tx.paymentInfo(account)
+    return paymentInfo.partialFee
   }
 
-  async transfer(account: KeyringPair, recipientAddr: string, amount: BN): Promise<Hash> {
-    const txHash = await this._api.tx.balances.transfer(recipientAddr, amount).signAndSend(account)
-    return txHash
+  createTransferTx(recipient: string, amount: BN) {
+    return this._api.tx.balances.transfer(recipient, amount)
   }
 
   // Working groups
-  // TODO: This is a lot of repeated logic from "/pioneer/joy-roles/src/transport.substrate.ts"
-  // (although simplified a little bit)
-  // Hopefully this will be refactored to "joystream-js" soon
-  protected singleLinkageResult<T extends Codec>(result: LinkageResult) {
-    return result[0] as T
-  }
+  // TODO: This is a lot of repeated logic from "/pioneer/joy-utils/transport"
+  // It will be refactored to "joystream-js" soon
+  async entriesByIds<IDType extends UInt, ValueType extends Codec>(
+    apiMethod: QueryableStorageEntry<'promise'>,
+    firstKey?: CodecArg // First key in case of double maps
+  ): Promise<[IDType, ValueType][]> {
+    const entries: [IDType, ValueType][] = (await apiMethod.entries<ValueType>(firstKey)).map(([storageKey, value]) => [
+      // If double-map (first key is provided), we map entries by second key
+      storageKey.args[firstKey !== undefined ? 1 : 0] as IDType,
+      value,
+    ])
 
-  protected multiLinkageResult<K extends Codec, V extends Codec>(result: LinkageResult): [Vec<K>, Vec<V>] {
-    return [result[0] as Vec<K>, result[1] as Vec<V>]
+    return entries.sort((a, b) => a[0].toNumber() - b[0].toNumber())
   }
 
   protected async blockHash(height: number): Promise<string> {
@@ -214,7 +210,7 @@ export default class Api {
   }
 
   protected async stakeValue(stakeId: StakeId): Promise<Balance> {
-    const stake = this.singleLinkageResult<Stake>((await this._api.query.stake.stakes(stakeId)) as LinkageResult)
+    const stake = await this._api.query.stake.stakes<Stake>(stakeId)
     return stake.value
   }
 
@@ -223,8 +219,8 @@ export default class Api {
   }
 
   protected async workerReward(relationshipId: RewardRelationshipId): Promise<Reward> {
-    const rewardRelationship = this.singleLinkageResult<RewardRelationship>(
-      (await this._api.query.recurringRewards.rewardRelationships(relationshipId)) as LinkageResult
+    const rewardRelationship = await this._api.query.recurringRewards.rewardRelationships<RewardRelationship>(
+      relationshipId
     )
 
     return {
@@ -266,18 +262,16 @@ export default class Api {
   }
 
   async workerByWorkerId(group: WorkingGroups, workerId: number): Promise<Worker> {
-    const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId
+    const nextId = await this.workingGroupApiQuery(group).nextWorkerId<WorkerId>()
 
     // This is chain specfic, but if next id is still 0, it means no workers have been added yet
     if (workerId < 0 || workerId >= nextId.toNumber()) {
       throw new CLIError('Invalid worker id!')
     }
 
-    const worker = this.singleLinkageResult<Worker>(
-      (await this.workingGroupApiQuery(group).workerById(workerId)) as LinkageResult
-    )
+    const worker = await this.workingGroupApiQuery(group).workerById<Worker>(workerId)
 
-    if (!worker.is_active) {
+    if (worker.isEmpty) {
       throw new CLIError('This worker is not active anymore')
     }
 
@@ -286,67 +280,51 @@ export default class Api {
 
   async groupMember(group: WorkingGroups, workerId: number) {
     const worker = await this.workerByWorkerId(group, workerId)
-    return await this.parseGroupMember(new WorkerId(workerId), worker)
+    return await this.parseGroupMember(this._api.createType('WorkerId', workerId), worker)
   }
 
   async groupMembers(group: WorkingGroups): Promise<GroupMember[]> {
-    const nextId = (await this.workingGroupApiQuery(group).nextWorkerId()) as WorkerId
-
-    // This is chain specfic, but if next id is still 0, it means no workers have been added yet
-    if (nextId.eq(0)) {
-      return []
-    }
+    const workerEntries = await this.entriesByIds<WorkerId, Worker>(this.workingGroupApiQuery(group).workerById)
 
-    const [workerIds, workers] = this.multiLinkageResult<WorkerId, Worker>(
-      (await this.workingGroupApiQuery(group).workerById()) as LinkageResult
+    const groupMembers: GroupMember[] = await Promise.all(
+      workerEntries.map(([id, worker]) => this.parseGroupMember(id, worker))
     )
 
-    const groupMembers: GroupMember[] = []
-    for (const [index, worker] of Object.entries(workers.toArray())) {
-      const workerId = workerIds[parseInt(index)]
-      if (worker.is_active) {
-        groupMembers.push(await this.parseGroupMember(workerId, worker))
-      }
-    }
-
-    return groupMembers.reverse()
+    return groupMembers.reverse() // Sort by newest
   }
 
   async openingsByGroup(group: WorkingGroups): Promise<GroupOpening[]> {
-    const openings: GroupOpening[] = []
-    const nextId = (await this.workingGroupApiQuery(group).nextOpeningId()) as OpeningId
+    let openings: GroupOpening[] = []
+    const nextId = await this.workingGroupApiQuery(group).nextOpeningId<OpeningId>()
 
     // This is chain specfic, but if next id is still 0, it means no openings have been added yet
     if (!nextId.eq(0)) {
-      const highestId = nextId.toNumber() - 1
-      for (let i = highestId; i >= 0; i--) {
-        openings.push(await this.groupOpening(group, i))
-      }
+      const ids = Array.from(Array(nextId.toNumber()).keys()).reverse() // Sort by newest
+      openings = await Promise.all(ids.map((id) => this.groupOpening(group, id)))
     }
 
     return openings
   }
 
   protected async hiringOpeningById(id: number | OpeningId): Promise<Opening> {
-    const result = (await this._api.query.hiring.openingById(id)) as LinkageResult
-    return this.singleLinkageResult<Opening>(result)
+    const result = await this._api.query.hiring.openingById<Opening>(id)
+    return result
   }
 
   protected async hiringApplicationById(id: number | ApplicationId): Promise<Application> {
-    const result = (await this._api.query.hiring.applicationById(id)) as LinkageResult
-    return this.singleLinkageResult<Application>(result)
+    const result = await this._api.query.hiring.applicationById<Application>(id)
+    return result
   }
 
   async wgApplicationById(group: WorkingGroups, wgApplicationId: number): Promise<WGApplication> {
-    const nextAppId = (await this.workingGroupApiQuery(group).nextApplicationId()) as ApplicationId
+    const nextAppId = await this.workingGroupApiQuery(group).nextApplicationId<ApplicationId>()
 
     if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
       throw new CLIError('Invalid working group application ID!')
     }
 
-    return this.singleLinkageResult<WGApplication>(
-      (await this.workingGroupApiQuery(group).applicationById(wgApplicationId)) as LinkageResult
-    )
+    const result = await this.workingGroupApiQuery(group).applicationById<WGApplication>(wgApplicationId)
+    return result
   }
 
   protected async parseApplication(wgApplicationId: number, wgApplication: WGApplication): Promise<GroupApplication> {
@@ -376,18 +354,15 @@ export default class Api {
   }
 
   protected async groupOpeningApplications(group: WorkingGroups, wgOpeningId: number): Promise<GroupApplication[]> {
-    const applications: GroupApplication[] = []
-
-    const nextAppId = (await this.workingGroupApiQuery(group).nextApplicationId()) as ApplicationId
-    for (let i = 0; i < nextAppId.toNumber(); i++) {
-      const wgApplication = await this.wgApplicationById(group, i)
-      if (wgApplication.opening_id.toNumber() !== wgOpeningId) {
-        continue
-      }
-      applications.push(await this.parseApplication(i, wgApplication))
-    }
+    const wgApplicationEntries = await this.entriesByIds<ApplicationId, WGApplication>(
+      this.workingGroupApiQuery(group).applicationById
+    )
 
-    return applications
+    return Promise.all(
+      wgApplicationEntries
+        .filter(([, /* id */ wgApplication]) => wgApplication.opening_id.eqn(wgOpeningId))
+        .map(([id, wgApplication]) => this.parseApplication(id.toNumber(), wgApplication))
+    )
   }
 
   async groupOpening(group: WorkingGroups, wgOpeningId: number): Promise<GroupOpening> {
@@ -397,9 +372,7 @@ export default class Api {
       throw new CLIError('Invalid working group opening ID!')
     }
 
-    const groupOpening = this.singleLinkageResult<WGOpening>(
-      (await this.workingGroupApiQuery(group).openingById(wgOpeningId)) as LinkageResult
-    )
+    const groupOpening = await this.workingGroupApiQuery(group).openingById<WGOpening>(wgOpeningId)
 
     const openingId = groupOpening.hiring_opening_id.toNumber()
     const opening = await this.hiringOpeningById(openingId)
@@ -417,19 +390,19 @@ export default class Api {
       sp.isSome ? unstakingPeriod(sp.unwrap()[key]) : 0
 
     const unstakingPeriods: Partial<UnstakingPeriods> = {
-      ['review_period_expired_application_stake_unstaking_period_length']: spUnstakingPeriod(
+      'review_period_expired_application_stake_unstaking_period_length': spUnstakingPeriod(
         applSP,
         'review_period_expired_unstaking_period_length'
       ),
-      ['crowded_out_application_stake_unstaking_period_length']: spUnstakingPeriod(
+      'crowded_out_application_stake_unstaking_period_length': spUnstakingPeriod(
         applSP,
         'crowded_out_unstaking_period_length'
       ),
-      ['review_period_expired_role_stake_unstaking_period_length']: spUnstakingPeriod(
+      'review_period_expired_role_stake_unstaking_period_length': spUnstakingPeriod(
         roleSP,
         'review_period_expired_unstaking_period_length'
       ),
-      ['crowded_out_role_stake_unstaking_period_length']: spUnstakingPeriod(
+      'crowded_out_role_stake_unstaking_period_length': spUnstakingPeriod(
         roleSP,
         'crowded_out_unstaking_period_length'
       ),
@@ -493,11 +466,11 @@ export default class Api {
   }
 
   async getMemberIdsByControllerAccount(address: string): Promise<MemberId[]> {
-    const ids = (await this._api.query.members.memberIdsByControllerAccountId(address)) as Vec<MemberId>
+    const ids = await this._api.query.members.memberIdsByControllerAccountId<Vec<MemberId>>(address)
     return ids.toArray()
   }
 
   async workerExitRationaleConstraint(group: WorkingGroups): Promise<InputValidationLengthConstraint> {
-    return (await this.workingGroupApiQuery(group).workerExitRationaleText()) as InputValidationLengthConstraint
+    return await this.workingGroupApiQuery(group).workerExitRationaleText<InputValidationLengthConstraint>()
   }
 }

+ 101 - 113
cli/src/Types.ts

@@ -5,7 +5,7 @@ import { Constructor, Codec } from '@polkadot/types/types'
 import { Struct, Vec } from '@polkadot/types/codec'
 import { u32 } from '@polkadot/types/primitive'
 import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces'
-import { DerivedBalances } from '@polkadot/api-derive/types'
+import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { KeyringPair } from '@polkadot/keyring/types'
 import { WorkerId, OpeningType } from '@joystream/types/working-group'
 import { Membership, MemberId } from '@joystream/types/members'
@@ -25,6 +25,7 @@ import {
 import ajv from 'ajv'
 import { Opening, StakingPolicy, ApplicationStageKeys } from '@joystream/types/hiring'
 import { Validator } from 'inquirer'
+import { JoyStructCustom } from '@joystream/types/common'
 
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
@@ -37,7 +38,7 @@ export type NamedKeyringPair = KeyringPair & {
 
 // Summary of the account information fetched from the api for "account:current" purposes (currently just balances)
 export type AccountSummary = {
-  balances: DerivedBalances
+  balances: DeriveBalancesAll
 }
 
 // This function allows us to easily transform the tuple into the object
@@ -186,187 +187,174 @@ export type GroupOpening = {
 // Note those types are not part of the runtime etc., we just use them to simplify prompting for values
 // (since there exists functionality that handles that for substrate types like: Struct, Vec etc.)
 interface WithJSONable<T> {
-  toJSON: () => T
+  toJSONObj: () => T
 }
-export class HRTJobSpecificsStruct extends Struct implements WithJSONable<JobSpecifics> {
-  constructor(value?: JobSpecifics) {
-    super(
-      {
-        title: 'Text',
-        description: 'Text',
-      },
-      value
-    )
-  }
+export class HRTJobSpecificsStruct
+  extends JoyStructCustom({
+    title: Text,
+    description: Text,
+  })
+  implements WithJSONable<JobSpecifics> {
   get title(): string {
-    return (this.get('title') as Text).toString()
+    return this.getField('title').toString()
   }
+
   get description(): string {
-    return (this.get('description') as Text).toString()
+    return this.getField('description').toString()
   }
-  toJSON(): JobSpecifics {
+
+  toJSONObj(): JobSpecifics {
     const { title, description } = this
     return { title, description }
   }
 }
-export class HRTEntryInMembershipModukeStruct extends Struct implements WithJSONable<EntryInMembershipModuke> {
-  constructor(value?: EntryInMembershipModuke) {
-    super(
-      {
-        handle: 'Text',
-      },
-      value
-    )
-  }
+export class HRTEntryInMembershipModukeStruct
+  extends JoyStructCustom({
+    handle: Text,
+  })
+  implements WithJSONable<EntryInMembershipModuke> {
   get handle(): string {
-    return (this.get('handle') as Text).toString()
+    return this.getField('handle').toString()
   }
-  toJSON(): EntryInMembershipModuke {
+
+  toJSONObj(): EntryInMembershipModuke {
     const { handle } = this
     return { handle }
   }
 }
-export class HRTCreatorDetailsStruct extends Struct implements WithJSONable<CreatorDetails> {
-  constructor(value?: CreatorDetails) {
-    super(
-      {
-        membership: HRTEntryInMembershipModukeStruct,
-      },
-      value
-    )
-  }
+export class HRTCreatorDetailsStruct
+  extends JoyStructCustom({
+    membership: HRTEntryInMembershipModukeStruct,
+  })
+  implements WithJSONable<CreatorDetails> {
   get membership(): EntryInMembershipModuke {
-    return (this.get('membership') as HRTEntryInMembershipModukeStruct).toJSON()
+    return this.getField('membership').toJSONObj()
   }
-  toJSON(): CreatorDetails {
+
+  toJSONObj(): CreatorDetails {
     const { membership } = this
     return { membership }
   }
 }
-export class HRTHiringProcessStruct extends Struct implements WithJSONable<HiringProcess> {
-  constructor(value?: HiringProcess) {
-    super(
-      {
-        details: 'Vec<Text>',
-      },
-      value
-    )
-  }
+export class HRTHiringProcessStruct
+  extends JoyStructCustom({
+    details: Vec.with(Text),
+  })
+  implements WithJSONable<HiringProcess> {
   get details(): AdditionalRolehiringProcessDetails {
-    return (this.get('details') as Vec<Text>).toArray().map((v) => v.toString())
+    return this.getField('details')
+      .toArray()
+      .map((v) => v.toString())
   }
-  toJSON(): HiringProcess {
+
+  toJSONObj(): HiringProcess {
     const { details } = this
     return { details }
   }
 }
-export class HRTQuestionFieldStruct extends Struct implements WithJSONable<QuestionField> {
-  constructor(value?: QuestionField) {
-    super(
-      {
-        title: 'Text',
-        type: 'Text',
-      },
-      value
-    )
-  }
+export class HRTQuestionFieldStruct
+  extends JoyStructCustom({
+    title: Text,
+    type: Text,
+  })
+  implements WithJSONable<QuestionField> {
   get title(): string {
-    return (this.get('title') as Text).toString()
+    return this.getField('title').toString()
   }
+
   get type(): string {
-    return (this.get('type') as Text).toString()
+    return this.getField('type').toString()
   }
-  toJSON(): QuestionField {
+
+  toJSONObj(): QuestionField {
     const { title, type } = this
     return { title, type }
   }
 }
 class HRTQuestionsFieldsVec extends Vec.with(HRTQuestionFieldStruct) implements WithJSONable<QuestionsFields> {
-  toJSON(): QuestionsFields {
-    return this.toArray().map((v) => v.toJSON())
+  toJSONObj(): QuestionsFields {
+    return this.toArray().map((v) => v.toJSONObj())
   }
 }
-export class HRTQuestionSectionStruct extends Struct implements WithJSONable<QuestionSection> {
-  constructor(value?: QuestionSection) {
-    super(
-      {
-        title: 'Text',
-        questions: HRTQuestionsFieldsVec,
-      },
-      value
-    )
-  }
+export class HRTQuestionSectionStruct
+  extends JoyStructCustom({
+    title: Text,
+    questions: HRTQuestionsFieldsVec,
+  })
+  implements WithJSONable<QuestionSection> {
   get title(): string {
-    return (this.get('title') as Text).toString()
+    return this.getField('title').toString()
   }
+
   get questions(): QuestionsFields {
-    return (this.get('questions') as HRTQuestionsFieldsVec).toJSON()
+    return this.getField('questions').toJSONObj()
   }
-  toJSON(): QuestionSection {
+
+  toJSONObj(): QuestionSection {
     const { title, questions } = this
     return { title, questions }
   }
 }
 export class HRTQuestionSectionsVec extends Vec.with(HRTQuestionSectionStruct)
   implements WithJSONable<QuestionSections> {
-  toJSON(): QuestionSections {
-    return this.toArray().map((v) => v.toJSON())
+  toJSONObj(): QuestionSections {
+    return this.toArray().map((v) => v.toJSONObj())
   }
 }
-export class HRTApplicationDetailsStruct extends Struct implements WithJSONable<ApplicationDetails> {
-  constructor(value?: ApplicationDetails) {
-    super(
-      {
-        sections: HRTQuestionSectionsVec,
-      },
-      value
-    )
-  }
+export class HRTApplicationDetailsStruct
+  extends JoyStructCustom({
+    sections: HRTQuestionSectionsVec,
+  })
+  implements WithJSONable<ApplicationDetails> {
   get sections(): QuestionSections {
-    return (this.get('sections') as HRTQuestionSectionsVec).toJSON()
+    return this.getField('sections').toJSONObj()
   }
-  toJSON(): ApplicationDetails {
+
+  toJSONObj(): ApplicationDetails {
     const { sections } = this
     return { sections }
   }
 }
-export class HRTStruct extends Struct implements WithJSONable<GenericJoyStreamRoleSchema> {
-  constructor(value?: GenericJoyStreamRoleSchema) {
-    super(
-      {
-        version: 'u32',
-        headline: 'Text',
-        job: HRTJobSpecificsStruct,
-        application: HRTApplicationDetailsStruct,
-        reward: 'Text',
-        creator: HRTCreatorDetailsStruct,
-        process: HRTHiringProcessStruct,
-      },
-      value
-    )
-  }
+export class HRTStruct
+  extends JoyStructCustom({
+    version: u32,
+    headline: Text,
+    job: HRTJobSpecificsStruct,
+    application: HRTApplicationDetailsStruct,
+    reward: Text,
+    creator: HRTCreatorDetailsStruct,
+    process: HRTHiringProcessStruct,
+  })
+  implements WithJSONable<GenericJoyStreamRoleSchema> {
   get version(): number {
-    return (this.get('version') as u32).toNumber()
+    return this.getField('version').toNumber()
   }
+
   get headline(): string {
-    return (this.get('headline') as Text).toString()
+    return this.getField('headline').toString()
   }
+
   get job(): JobSpecifics {
-    return (this.get('job') as HRTJobSpecificsStruct).toJSON()
+    return this.getField('job').toJSONObj()
   }
+
   get application(): ApplicationDetails {
-    return (this.get('application') as HRTApplicationDetailsStruct).toJSON()
+    return this.getField('application').toJSONObj()
   }
+
   get reward(): string {
-    return (this.get('reward') as Text).toString()
+    return this.getField('reward').toString()
   }
+
   get creator(): CreatorDetails {
-    return (this.get('creator') as HRTCreatorDetailsStruct).toJSON()
+    return this.getField('creator').toJSONObj()
   }
+
   get process(): HiringProcess {
-    return (this.get('process') as HRTHiringProcessStruct).toJSON()
+    return this.getField('process').toJSONObj()
   }
-  toJSON(): GenericJoyStreamRoleSchema {
+
+  toJSONObj(): GenericJoyStreamRoleSchema {
     const { version, headline, job, application, reward, creator, process } = this
     return { version, headline, job, application, reward, creator, process }
   }

+ 5 - 3
cli/src/base/AccountsCommandBase.ts

@@ -8,7 +8,7 @@ import ApiCommandBase from './ApiCommandBase'
 import { Keyring } from '@polkadot/api'
 import { formatBalance } from '@polkadot/util'
 import { NamedKeyringPair } from '../Types'
-import { DerivedBalances } from '@polkadot/api-derive/types'
+import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { toFixedLength } from '../helpers/display'
 
 const ACCOUNTS_DIRNAME = 'accounts'
@@ -54,7 +54,9 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     const keyring = new Keyring({ type: 'sr25519' })
     keyring.addFromUri('//Alice', { name: 'Alice' })
     keyring.addFromUri('//Bob', { name: 'Bob' })
-    keyring.getPairs().forEach((pair) => this.saveAccount({ ...pair, meta: { name: pair.meta.name } }, '', true))
+    keyring
+      .getPairs()
+      .forEach((pair) => this.saveAccount({ ...pair, meta: { name: pair.meta.name as string } }, '', true))
   }
 
   fetchAccountFromJsonFile(jsonBackupFilePath: string): NamedKeyringPair {
@@ -186,7 +188,7 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     message = 'Select an account',
     showBalances = true
   ): Promise<NamedKeyringPair> {
-    let balances: DerivedBalances[]
+    let balances: DeriveBalancesAll[]
     if (showBalances) {
       balances = await this.getApi().getAccountsBalancesInfo(accounts.map((acc) => acc.address))
     }

+ 42 - 27
cli/src/base/ApiCommandBase.ts

@@ -2,13 +2,14 @@ import ExitCodes from '../ExitCodes'
 import { CLIError } from '@oclif/errors'
 import StateAwareCommandBase from './StateAwareCommandBase'
 import Api from '../Api'
-import { getTypeDef, createType, Option, Tuple, Bytes } from '@polkadot/types'
-import { Codec, TypeDef, TypeDefInfo, Constructor } from '@polkadot/types/types'
+import { getTypeDef, Option, Tuple, Bytes } from '@polkadot/types'
+import { Registry, Codec, CodecArg, TypeDef, TypeDefInfo, Constructor } from '@polkadot/types/types'
+
 import { Vec, Struct, Enum } from '@polkadot/types/codec'
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { KeyringPair } from '@polkadot/keyring/types'
 import chalk from 'chalk'
-import { SubmittableResultImpl } from '@polkadot/api/types'
+import { InterfaceTypes } from '@polkadot/types/types/registry'
 import ajv from 'ajv'
 import { ApiMethodArg, ApiMethodNamedArgs, ApiParamsOptions, ApiParamOptions } from '../Types'
 import { createParamOptions } from '../helpers/promptOptions'
@@ -32,6 +33,14 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return this.getApi().getOriginalApi()
   }
 
+  getTypesRegistry(): Registry {
+    return this.getOriginalApi().registry
+  }
+
+  createType<K extends keyof InterfaceTypes>(typeName: K, value?: unknown): InterfaceTypes[K] {
+    return this.getOriginalApi().createType(typeName, value)
+  }
+
   async init() {
     await super.init()
     let apiUri: string = this.getPreservedState().apiUri
@@ -81,6 +90,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
   isApiUriValid(uri: string) {
     try {
+      // eslint-disable-next-line no-new
       new WsProvider(uri)
     } catch (e) {
       return false
@@ -90,8 +100,8 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
   // This is needed to correctly handle some structs, enums etc.
   // Where the main typeDef doesn't provide enough information
-  protected getRawTypeDef(type: string) {
-    const instance = createType(type as any)
+  protected getRawTypeDef(type: keyof InterfaceTypes) {
+    const instance = this.createType(type)
     return getTypeDef(instance.toRawType())
   }
 
@@ -120,7 +130,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
   async promptForSimple(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Codec> {
     // If no default provided - get default value resulting from providing empty string
     const defaultValueString =
-      paramOptions?.value?.default?.toString() || createType(typeDef.type as any, '').toString()
+      paramOptions?.value?.default?.toString() || this.createType(typeDef.type as any, '').toString()
     const providedValue = await this.simplePrompt({
       message: `Provide value for ${this.paramName(typeDef)}`,
       type: 'input',
@@ -129,7 +139,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
       default: (defaultValueString === '0x' ? '' : defaultValueString) || undefined,
       validate: paramOptions?.validator,
     })
-    return createType(typeDef.type as any, providedValue)
+    return this.createType(typeDef.type as any, providedValue)
   }
 
   // Prompt for Option<Codec> value
@@ -149,10 +159,10 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         createParamOptions(subtype.name, defaultValue?.unwrapOr(undefined))
       )
       this.closeIndentGroup()
-      return new Option(subtype.type as any, value)
+      return this.createType(`Option<${subtype.type}>` as any, value)
     }
 
-    return new Option(subtype.type as any, null)
+    return this.createType(`Option<${subtype.type}>` as any, null)
   }
 
   // Prompt for Tuple
@@ -173,7 +183,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     }
     this.closeIndentGroup()
 
-    return new Tuple(subtypes.map((subtype) => subtype.type) as any, result)
+    return new Tuple(this.getTypesRegistry(), subtypes.map((subtype) => subtype.type) as any, result)
   }
 
   // Prompt for Struct
@@ -182,7 +192,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
     this.openIndentGroup()
     const structType = typeDef.type
-    const rawTypeDef = this.getRawTypeDef(structType)
+    const rawTypeDef = this.getRawTypeDef(structType as keyof InterfaceTypes)
     // We assume struct typeDef always has array of typeDefs inside ".sub"
     const structSubtypes = rawTypeDef.sub as TypeDef[]
     const structDefault = paramOptions?.value?.default as Struct | undefined
@@ -200,7 +210,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     }
     this.closeIndentGroup()
 
-    return createType(structType as any, structValues)
+    return this.createType(structType as any, structValues)
   }
 
   // Prompt for Vec
@@ -228,12 +238,12 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     } while (addAnother)
     this.closeIndentGroup()
 
-    return new Vec(subtype.type as any, entries)
+    return this.createType(`Vec<${subtype.type}>` as any, entries)
   }
 
   // Prompt for Enum
   async promptForEnum(typeDef: TypeDef, paramOptions?: ApiParamOptions): Promise<Enum> {
-    const enumType = typeDef.type
+    const enumType = typeDef.type as keyof InterfaceTypes
     const rawTypeDef = this.getRawTypeDef(enumType)
     // We assume enum always has array on TypeDefs inside ".sub"
     const enumSubtypes = rawTypeDef.sub as TypeDef[]
@@ -253,12 +263,12 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
     if (enumSubtype.type !== 'Null') {
       const subtypeOptions = createParamOptions(enumSubtype.name, defaultValue?.value)
-      return createType(enumType as any, {
+      return this.createType(enumType as any, {
         [enumSubtype.name!]: await this.promptForParam(enumSubtype.type, subtypeOptions),
       })
     }
 
-    return createType(enumType as any, enumSubtype.name)
+    return this.createType(enumType as any, enumSubtype.name)
   }
 
   // Prompt for param based on "paramType" string (ie. Option<MemeberId>)
@@ -268,7 +278,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     paramOptions?: ApiParamOptions // TODO: This is not fully implemented for all types yet
   ): Promise<ApiMethodArg> {
     const typeDef = getTypeDef(paramType)
-    const rawTypeDef = this.getRawTypeDef(paramType)
+    const rawTypeDef = this.getRawTypeDef(paramType as keyof InterfaceTypes)
 
     if (paramOptions?.forcedName) {
       typeDef.name = paramOptions.forcedName
@@ -309,18 +319,23 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     defaultValue?: Bytes,
     schemaValidator?: ajv.ValidateFunction
   ) {
-    const rawType = new jsonStruct().toRawType()
+    const JsonStructObject = jsonStruct
+    const rawType = new JsonStructObject(this.getTypesRegistry()).toRawType()
     const typeDef = getTypeDef(rawType)
 
     const defaultStruct =
-      defaultValue && new jsonStruct(JSON.parse(Buffer.from(defaultValue.toHex().replace('0x', ''), 'hex').toString()))
+      defaultValue &&
+      new JsonStructObject(
+        this.getTypesRegistry(),
+        JSON.parse(Buffer.from(defaultValue.toHex().replace('0x', ''), 'hex').toString())
+      )
 
     if (argName) {
       typeDef.name = argName
     }
 
-    let isValid = true,
-      jsonText: string
+    let isValid = true
+    let jsonText: string
     do {
       const structVal = await this.promptForStruct(typeDef, createParamOptions(typeDef.name, defaultStruct))
       jsonText = JSON.stringify(structVal.toJSON())
@@ -338,7 +353,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
       }
     } while (!isValid)
 
-    return new Bytes('0x' + Buffer.from(jsonText, 'ascii').toString('hex'))
+    return this.createType('Bytes', '0x' + Buffer.from(jsonText, 'ascii').toString('hex'))
   }
 
   async promptForExtrinsicParams(
@@ -364,18 +379,18 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return values
   }
 
-  sendExtrinsic(account: KeyringPair, module: string, method: string, params: Codec[]) {
+  sendExtrinsic(account: KeyringPair, module: string, method: string, params: CodecArg[]) {
     return new Promise((resolve, reject) => {
       const extrinsicMethod = this.getOriginalApi().tx[module][method]
       let unsubscribe: () => void
       extrinsicMethod(...params)
-        .signAndSend(account, {}, (result: SubmittableResultImpl) => {
+        .signAndSend(account, {}, (result) => {
           // Implementation loosely based on /pioneer/packages/react-signer/src/Modal.tsx
           if (!result || !result.status) {
             return
           }
 
-          if (result.status.isFinalized) {
+          if (result.status.isInBlock) {
             unsubscribe()
             result.events
               .filter(({ event: { section } }): boolean => section === 'system')
@@ -401,7 +416,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     account: KeyringPair,
     module: string,
     method: string,
-    params: Codec[],
+    params: CodecArg[],
     warnOnly = false // If specified - only warning will be displayed (instead of error beeing thrown)
   ) {
     try {
@@ -449,7 +464,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
       const argName = arg.name.toString()
       const argType = arg.type.toString()
       try {
-        parsedArgs.push({ name: argName, value: createType(argType as any, draftJSONObj[parseInt(index)]) })
+        parsedArgs.push({ name: argName, value: this.createType(argType as any, draftJSONObj[parseInt(index)]) })
       } catch (e) {
         throw new CLIError(`Couldn't parse ${argName} value from draft at ${draftFilePath}!`, {
           exit: ExitCodes.InvalidFile,

+ 4 - 4
cli/src/base/WorkingGroupsCommandBase.ts

@@ -110,13 +110,13 @@ export default abstract class WorkingGroupsCommandBase extends AccountsCommandBa
       })),
     })
 
-    return acceptedApplications
+    return acceptedApplications.sort() // Sort just in case, since runtime expects them to be sorted
   }
 
   async promptForNewOpeningDraftName() {
-    let draftName = '',
-      fileExists = false,
-      overrideConfirmed = false
+    let draftName = ''
+    let fileExists = false
+    let overrideConfirmed = false
 
     do {
       draftName = await this.simplePrompt({

+ 2 - 3
cli/src/commands/account/current.ts

@@ -1,6 +1,5 @@
 import AccountsCommandBase from '../../base/AccountsCommandBase'
 import { AccountSummary, NameValueObj, NamedKeyringPair } from '../../Types'
-import { DerivedBalances } from '@polkadot/api-derive/types'
 import { displayHeader, displayNameValueTable } from '../../helpers/display'
 import { formatBalance } from '@polkadot/util'
 import moment from 'moment'
@@ -15,7 +14,7 @@ export default class AccountCurrent extends AccountsCommandBase {
 
     displayHeader('Account information')
     const creationDate: string = currentAccount.meta.whenCreated
-      ? moment(currentAccount.meta.whenCreated).format('YYYY-MM-DD HH:mm:ss')
+      ? moment(currentAccount.meta.whenCreated as string | number).format('YYYY-MM-DD HH:mm:ss')
       : '?'
     const accountRows: NameValueObj[] = [
       { name: 'Account name:', value: currentAccount.meta.name },
@@ -25,7 +24,7 @@ export default class AccountCurrent extends AccountsCommandBase {
     displayNameValueTable(accountRows)
 
     displayHeader('Balances')
-    const balances: DerivedBalances = summary.balances
+    const balances = summary.balances
     const balancesRows: NameValueObj[] = [
       { name: 'Total balance:', value: formatBalance(balances.votingBalance) },
       { name: 'Transferable balance:', value: formatBalance(balances.availableBalance) },

+ 4 - 4
cli/src/commands/account/transferTokens.ts

@@ -6,7 +6,6 @@ import { formatBalance } from '@polkadot/util'
 import { Hash } from '@polkadot/types/interfaces'
 import { NamedKeyringPair } from '../../Types'
 import { checkBalance, validateAddress } from '../../helpers/validation'
-import { DerivedBalances } from '@polkadot/api-derive/types'
 
 type AccountTransferArgs = {
   recipient: string
@@ -36,15 +35,16 @@ export default class AccountTransferTokens extends AccountsCommandBase {
 
     // Initial validation
     validateAddress(args.recipient, 'Invalid recipient address')
-    const accBalances: DerivedBalances = (await this.getApi().getAccountsBalancesInfo([selectedAccount.address]))[0]
+    const accBalances = (await this.getApi().getAccountsBalancesInfo([selectedAccount.address]))[0]
     checkBalance(accBalances, amountBN)
 
     await this.requestAccountDecoding(selectedAccount)
 
     this.log(chalk.white('Estimating fee...'))
+    const tx = await this.getApi().createTransferTx(args.recipient, amountBN)
     let estimatedFee: BN
     try {
-      estimatedFee = await this.getApi().estimateFee(selectedAccount, args.recipient, amountBN)
+      estimatedFee = await this.getApi().estimateFee(selectedAccount, tx)
     } catch (e) {
       this.error('Could not estimate the fee.', { exit: ExitCodes.UnexpectedException })
     }
@@ -57,7 +57,7 @@ export default class AccountTransferTokens extends AccountsCommandBase {
     await this.requireConfirmation('Do you confirm the transfer?')
 
     try {
-      const txHash: Hash = await this.getApi().transfer(selectedAccount, args.recipient, amountBN)
+      const txHash: Hash = await tx.signAndSend(selectedAccount)
       this.log(chalk.greenBright('Transaction succesfully sent!'))
       this.log(chalk.white('Hash:', txHash.toString()))
     } catch (e) {

+ 11 - 14
cli/src/commands/api/inspect.ts

@@ -2,9 +2,8 @@ import { flags } from '@oclif/command'
 import { CLIError } from '@oclif/errors'
 import { displayNameValueTable } from '../../helpers/display'
 import { ApiPromise } from '@polkadot/api'
-import { Option } from '@polkadot/types'
 import { Codec } from '@polkadot/types/types'
-import { ConstantCodec } from '@polkadot/api-metadata/consts/types'
+import { ConstantCodec } from '@polkadot/metadata/Decorated/consts/types'
 import ExitCodes from '../../ExitCodes'
 import chalk from 'chalk'
 import { NameValueObj, ApiMethodArg } from '../../Types'
@@ -99,7 +98,7 @@ export default class ApiInspect extends ApiCommandBase {
       return [type.asDoubleMap.key1.toString(), type.asDoubleMap.key2.toString()]
     }
     if (type.isMap) {
-      return type.asMap.linked.isTrue ? [`Option<${type.asMap.key.toString()}>`] : [type.asMap.key.toString()]
+      return [type.asMap.key.toString()]
     }
     return []
   }
@@ -110,14 +109,17 @@ export default class ApiInspect extends ApiCommandBase {
       const {
         meta: { type, modifier },
       } = method.creator
+      let typeName = type.toString()
       if (type.isDoubleMap) {
-        return type.asDoubleMap.value.toString()
+        typeName = type.asDoubleMap.value.toString()
       }
-      if (modifier.isOptional) {
-        return `Option<${type.toString()}>`
+      if (type.isMap) {
+        typeName = type.asMap.value.toString()
       }
+
+      return modifier.isOptional ? `Option<${typeName}>` : typeName
     }
-    // Fallback for "query" and default for "consts"
+    // Fallback for "consts"
     return this.getMethodMeta(apiType, apiModule, apiMethod).type.toString()
   }
 
@@ -127,7 +129,7 @@ export default class ApiInspect extends ApiCommandBase {
     api: ApiPromise,
     flags: ApiInspectFlags
   ): { apiType: ApiType | undefined; apiModule: string | undefined; apiMethod: string | undefined } {
-    let apiType: ApiType | undefined = undefined
+    let apiType: ApiType | undefined
     const { module: apiModule, method: apiMethod } = flags
 
     if (flags.type !== undefined) {
@@ -155,12 +157,7 @@ export default class ApiInspect extends ApiCommandBase {
     for (const [key, paramType] of Object.entries(paramTypes)) {
       this.log(chalk.bold.white(`Parameter no. ${parseInt(key) + 1} (${paramType}):`))
       const paramValue = await this.promptForParam(paramType)
-      if (paramValue instanceof Option && paramValue.isSome) {
-        result.push(paramValue.unwrap())
-      } else if (!(paramValue instanceof Option)) {
-        result.push(paramValue)
-      }
-      // In case of empty option we MUST NOT add anything to the array (otherwise it causes some error)
+      result.push(paramValue)
     }
 
     return result

+ 1 - 0
cli/src/commands/working-groups/application.ts

@@ -11,6 +11,7 @@ export default class WorkingGroupsApplication extends WorkingGroupsCommandBase {
       description: 'Working Group Application ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }

+ 2 - 2
cli/src/commands/working-groups/createOpening.ts

@@ -53,8 +53,8 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
       const module = apiModuleByGroup[this.group]
       const method = 'addOpening'
 
-      let saveDraft = false,
-        params: ApiMethodArg[]
+      let saveDraft = false
+      let params: ApiMethodArg[]
       if (flags.createDraftOnly) {
         params = await this.promptForExtrinsicParams(module, method, promptOptions)
         saveDraft = true

+ 3 - 5
cli/src/commands/working-groups/decreaseWorkerStake.ts

@@ -1,6 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-import { WorkerId } from '@joystream/types/working-group'
 import { Balance } from '@polkadot/types/interfaces'
 import { formatBalance } from '@polkadot/util'
 import { minMaxInt } from '../../validators/common'
@@ -11,6 +10,7 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
   static description =
     'Decreases given worker stake by an amount that will be returned to the worker role account. ' +
     'Requires lead access.'
+
   static args = [
     {
       name: 'workerId',
@@ -18,6 +18,7 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
       description: 'Worker ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -41,10 +42,7 @@ export default class WorkingGroupsDecreaseWorkerStake extends WorkingGroupsComma
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'decreaseStake', [
-      new WorkerId(workerId),
-      balance,
-    ])
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'decreaseStake', [workerId, balance])
 
     this.log(
       chalk.green(

+ 3 - 4
cli/src/commands/working-groups/evictWorker.ts

@@ -1,7 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-import { WorkerId } from '@joystream/types/working-group'
-import { bool } from '@polkadot/types/primitive'
 import { formatBalance } from '@polkadot/util'
 import chalk from 'chalk'
 import { createParamOptions } from '../../helpers/promptOptions'
@@ -15,6 +13,7 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
       description: 'Worker ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -43,9 +42,9 @@ export default class WorkingGroupsEvictWorker extends WorkingGroupsCommandBase {
     await this.requestAccountDecoding(account)
 
     await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'terminateRole', [
-      new WorkerId(workerId),
+      workerId,
       rationale,
-      new bool(shouldSlash),
+      shouldSlash,
     ])
 
     this.log(chalk.green(`Worker ${chalk.white(workerId)} has been evicted!`))

+ 4 - 8
cli/src/commands/working-groups/fillOpening.ts

@@ -1,8 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { OpeningStatus } from '../../Types'
 import { apiModuleByGroup } from '../../Api'
-import { OpeningId } from '@joystream/types/hiring'
-import { ApplicationIdSet, RewardPolicy } from '@joystream/types/working-group'
 import chalk from 'chalk'
 import { createParamOptions } from '../../helpers/promptOptions'
 
@@ -15,6 +13,7 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
       description: 'Working Group Opening ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -30,16 +29,13 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
     const opening = await this.getOpeningForLeadAction(openingId, OpeningStatus.InReview)
 
     const applicationIds = await this.promptForApplicationsToAccept(opening)
-    const rewardPolicyOpt = await this.promptForParam(
-      `Option<${RewardPolicy.name}>`,
-      createParamOptions('RewardPolicy')
-    )
+    const rewardPolicyOpt = await this.promptForParam(`Option<RewardPolicy>`, createParamOptions('RewardPolicy'))
 
     await this.requestAccountDecoding(account)
 
     await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'fillOpening', [
-      new OpeningId(openingId),
-      new ApplicationIdSet(applicationIds),
+      openingId,
+      applicationIds,
       rewardPolicyOpt,
     ])
 

+ 1 - 0
cli/src/commands/working-groups/opening.ts

@@ -15,6 +15,7 @@ export default class WorkingGroupsOpening extends WorkingGroupsCommandBase {
       description: 'Working Group Opening ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }

+ 2 - 5
cli/src/commands/working-groups/slashWorker.ts

@@ -1,6 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-import { WorkerId } from '@joystream/types/working-group'
 import { Balance } from '@polkadot/types/interfaces'
 import { formatBalance } from '@polkadot/util'
 import { minMaxInt } from '../../validators/common'
@@ -16,6 +15,7 @@ export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
       description: 'Worker ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -39,10 +39,7 @@ export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'slashStake', [
-      new WorkerId(workerId),
-      balance,
-    ])
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'slashStake', [workerId, balance])
 
     this.log(
       chalk.green(

+ 2 - 4
cli/src/commands/working-groups/startAcceptingApplications.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { OpeningStatus } from '../../Types'
 import { apiModuleByGroup } from '../../Api'
-import { OpeningId } from '@joystream/types/hiring'
 import chalk from 'chalk'
 
 export default class WorkingGroupsStartAcceptingApplications extends WorkingGroupsCommandBase {
@@ -13,6 +12,7 @@ export default class WorkingGroupsStartAcceptingApplications extends WorkingGrou
       description: 'Working Group Opening ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -29,9 +29,7 @@ export default class WorkingGroupsStartAcceptingApplications extends WorkingGrou
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'acceptApplications', [
-      new OpeningId(openingId),
-    ])
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'acceptApplications', [openingId])
 
     this.log(
       chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('Accepting Applications')}`)

+ 2 - 4
cli/src/commands/working-groups/startReviewPeriod.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { OpeningStatus } from '../../Types'
 import { apiModuleByGroup } from '../../Api'
-import { OpeningId } from '@joystream/types/hiring'
 import chalk from 'chalk'
 
 export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommandBase {
@@ -13,6 +12,7 @@ export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommand
       description: 'Working Group Opening ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -29,9 +29,7 @@ export default class WorkingGroupsStartReviewPeriod extends WorkingGroupsCommand
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'beginApplicantReview', [
-      new OpeningId(openingId),
-    ])
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'beginApplicantReview', [openingId])
 
     this.log(chalk.green(`Opening ${chalk.white(openingId)} status changed to: ${chalk.white('In Review')}`))
   }

+ 3 - 4
cli/src/commands/working-groups/terminateApplication.ts

@@ -1,6 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-import { ApplicationStageKeys, ApplicationId } from '@joystream/types/hiring'
+import { ApplicationStageKeys } from '@joystream/types/hiring'
 import chalk from 'chalk'
 
 export default class WorkingGroupsTerminateApplication extends WorkingGroupsCommandBase {
@@ -12,6 +12,7 @@ export default class WorkingGroupsTerminateApplication extends WorkingGroupsComm
       description: 'Working Group Application ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -29,9 +30,7 @@ export default class WorkingGroupsTerminateApplication extends WorkingGroupsComm
 
     await this.requestAccountDecoding(account)
 
-    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'terminateApplication', [
-      new ApplicationId(applicationId),
-    ])
+    await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'terminateApplication', [applicationId])
 
     this.log(chalk.green(`Application ${chalk.white(applicationId)} has been succesfully terminated!`))
   }

+ 2 - 2
cli/src/commands/working-groups/updateRewardAccount.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
 import { validateAddress } from '../../helpers/validation'
-import { GenericAccountId } from '@polkadot/types'
 import chalk from 'chalk'
 import ExitCodes from '../../ExitCodes'
 
@@ -14,6 +13,7 @@ export default class WorkingGroupsUpdateRewardAccount extends WorkingGroupsComma
       description: 'New reward account address (if omitted, one of the existing CLI accounts can be selected)',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -40,7 +40,7 @@ export default class WorkingGroupsUpdateRewardAccount extends WorkingGroupsComma
 
     await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRewardAccount', [
       worker.workerId,
-      new GenericAccountId(newRewardAccount),
+      newRewardAccount,
     ])
 
     this.log(chalk.green(`Succesfully updated the reward account to: ${chalk.white(newRewardAccount)})`))

+ 2 - 2
cli/src/commands/working-groups/updateRoleAccount.ts

@@ -1,7 +1,6 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
 import { validateAddress } from '../../helpers/validation'
-import { GenericAccountId } from '@polkadot/types'
 import chalk from 'chalk'
 
 export default class WorkingGroupsUpdateRoleAccount extends WorkingGroupsCommandBase {
@@ -13,6 +12,7 @@ export default class WorkingGroupsUpdateRoleAccount extends WorkingGroupsCommand
       description: 'New role account address (if omitted, one of the existing CLI accounts can be selected)',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -34,7 +34,7 @@ export default class WorkingGroupsUpdateRoleAccount extends WorkingGroupsCommand
 
     await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRoleAccount', [
       worker.workerId,
-      new GenericAccountId(newRoleAccount),
+      newRoleAccount,
     ])
 
     this.log(chalk.green(`Succesfully updated the role account to: ${chalk.white(newRoleAccount)})`))

+ 2 - 2
cli/src/commands/working-groups/updateWorkerReward.ts

@@ -1,6 +1,5 @@
 import WorkingGroupsCommandBase from '../../base/WorkingGroupsCommandBase'
 import { apiModuleByGroup } from '../../Api'
-import { WorkerId } from '@joystream/types/working-group'
 import { formatBalance } from '@polkadot/util'
 import chalk from 'chalk'
 import { Reward } from '../../Types'
@@ -17,6 +16,7 @@ export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsComman
       description: 'Worker ID',
     },
   ]
+
   static flags = {
     ...WorkingGroupsCommandBase.flags,
   }
@@ -56,7 +56,7 @@ export default class WorkingGroupsUpdateWorkerReward extends WorkingGroupsComman
     await this.requestAccountDecoding(account)
 
     await this.sendAndFollowExtrinsic(account, apiModuleByGroup[this.group], 'updateRewardAmount', [
-      new WorkerId(workerId),
+      workerId,
       newRewardValue,
     ])
 

+ 2 - 2
cli/src/helpers/validation.ts

@@ -1,7 +1,7 @@
 import BN from 'bn.js'
 import ExitCodes from '../ExitCodes'
 import { decodeAddress } from '@polkadot/util-crypto'
-import { DerivedBalances } from '@polkadot/api-derive/types'
+import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { CLIError } from '@oclif/errors'
 
 export function validateAddress(address: string, errorMessage = 'Invalid address'): void {
@@ -12,7 +12,7 @@ export function validateAddress(address: string, errorMessage = 'Invalid address
   }
 }
 
-export function checkBalance(accBalances: DerivedBalances, requiredBalance: BN): void {
+export function checkBalance(accBalances: DeriveBalancesAll, requiredBalance: BN): void {
   if (requiredBalance.gt(accBalances.availableBalance)) {
     throw new CLIError('Not enough balance available', { exit: ExitCodes.InvalidInput })
   }

+ 11 - 9
cli/src/promptOptions/addWorkerOpening.ts

@@ -1,32 +1,32 @@
 import { ApiParamsOptions, ApiParamOptions, HRTStruct } from '../Types'
-import {
-  OpeningType,
-  SlashingTerms,
-  UnslashableTerms,
-  OpeningType_Worker as OpeningTypeWorker,
-  WorkingGroupOpeningPolicyCommitment,
-} from '@joystream/types/working-group'
+import { OpeningType, WorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group'
+import { SlashingTerms } from '@joystream/types/common'
 import { Bytes } from '@polkadot/types'
 import { schemaValidator } from '@joystream/types/hiring'
+import { createType } from '@joystream/types'
 
 class OpeningPolicyCommitmentOptions implements ApiParamsOptions {
   [paramName: string]: ApiParamOptions
   public role_slashing_terms: ApiParamOptions<SlashingTerms> = {
     value: {
-      default: SlashingTerms.create('Unslashable', new UnslashableTerms()),
+      default: createType('SlashingTerms', { Unslashable: null }),
       locked: true,
     },
   }
+
   // Rename fields containing "curator" (solivg minor UI issue related to flat namespace)
   public terminate_curator_application_stake_unstaking_period: ApiParamOptions = {
     forcedName: 'terminate_application_stake_unstaking_period',
   }
+
   public terminate_curator_role_stake_unstaking_period: ApiParamOptions = {
     forcedName: 'terminate_role_stake_unstaking_period',
   }
+
   public exit_curator_role_application_stake_unstaking_period: ApiParamOptions = {
     forcedName: 'exit_role_application_stake_unstaking_period',
   }
+
   public exit_curator_role_stake_unstaking_period: ApiParamOptions = {
     forcedName: 'exit_role_stake_unstaking_period',
   }
@@ -37,10 +37,11 @@ class AddWrokerOpeningOptions implements ApiParamsOptions {
   // Lock value for opening_type
   public opening_type: ApiParamOptions<OpeningType> = {
     value: {
-      default: OpeningType.create('Worker', new OpeningTypeWorker()),
+      default: createType('OpeningType', { Worker: null }),
       locked: true,
     },
   }
+
   // Json schema for human_readable_text
   public human_readable_text: ApiParamOptions<Bytes> = {
     jsonSchema: {
@@ -48,6 +49,7 @@ class AddWrokerOpeningOptions implements ApiParamsOptions {
       struct: HRTStruct,
     },
   }
+
   // Lock value for role_slashing_terms
   public commitment: ApiParamOptions<WorkingGroupOpeningPolicyCommitment> = {
     nestedOptions: new OpeningPolicyCommitmentOptions(),

+ 5 - 1
cli/tsconfig.json

@@ -9,7 +9,11 @@
     "target": "es2017",
     "esModuleInterop": true,
     "types" : [ "node" ],
-    "noUnusedLocals": true
+    "noUnusedLocals": true,
+    "baseUrl": ".",
+    "paths": {
+      "@polkadot/types/augment": ["../types/augment-codec/augment-types.ts"],
+    }
   },
   "include": [
     "src/**/*"

+ 22 - 0
devops/ansible/build-and-run-tests-exported-chainspec-playbook.yml

@@ -0,0 +1,22 @@
+- hosts: 127.0.0.1
+  user: root
+  become: yes
+  become_method: sudo
+
+  tasks:
+
+    - name: install dependencies
+      include_role:
+        name: install_dependencies
+
+    - name: alter block creation time
+      include_role:
+        name: alter_block_creation_time
+
+    - name: build node
+      include_role:
+        name: build_docker_image
+
+    - name: run tests
+      include_role:
+        name: run_tests_exported_chainspec

+ 6 - 1
devops/ansible/build-and-run-tests-single-node-playbook.yml

@@ -4,14 +4,19 @@
   become_method: sudo
 
   tasks:
+
     - name: install dependencies
       include_role:
         name: install_dependencies
 
+    - name: alter block creation time
+      include_role:
+        name: alter_block_creation_time
+
     - name: build node
       include_role:
         name: build_docker_image
-        
+
     - name: run tests
       include_role:
         name: run_tests_single_node

+ 6 - 1
devops/ansible/build-and-run-tests-two-nodes-playbook.yml

@@ -4,14 +4,19 @@
   become_method: sudo
 
   tasks:
+
     - name: install dependencies
       include_role:
         name: install_dependencies
 
+    - name: alter block creation time
+      include_role:
+        name: alter_block_creation_time
+
     - name: build node
       include_role:
         name: build_docker_image
-        
+
     - name: run tests
       include_role:
         name: run_tests_two_nodes

+ 6 - 1
devops/ansible/build-image-playbook.yml

@@ -4,10 +4,15 @@
   become_method: sudo
 
   tasks:
+
     - name: install dependencies
       include_role:
         name: install_dependencies
 
+    - name: alter block creation time
+      include_role:
+        name: alter_block_creation_time
+
     - name: build node
       include_role:
-        name: build_docker_image
+        name: build_docker_image

+ 2 - 2
devops/ansible/docker-compose.yml

@@ -3,7 +3,7 @@ services:
   node_alice:
     image: joystream/node-testing
     container_name: alice
-    entrypoint: ./node --chain=chainspec.json --alice --validator --ws-external --rpc-cors=all
+    entrypoint: ./node --dev --alice --validator --unsafe-ws-external --rpc-cors=all
     ports:
       - "30333:30333"
       - "9933:9933"
@@ -15,7 +15,7 @@ services:
   node_bob:
     image: joystream/node-testing
     container_name: bob
-    entrypoint: ./node --chain=chainspec.json --bob --ws-external --rpc-cors=all
+    entrypoint: ./node --dev --bob --validator --unsafe-ws-external --rpc-cors=all
     ports:
       - "30335:30333"
       - "9935:9933"

+ 4 - 0
devops/ansible/roles/alter_block_creation_time/tasks/main.yml

@@ -0,0 +1,4 @@
+- name: alter block creation time
+  shell: ./scripts/alter-block-creation-time.sh
+  args:
+    chdir: ../../

+ 1 - 1
devops/ansible/roles/build_docker_image/tasks/main.yml

@@ -1,4 +1,4 @@
 - name: create testing node docker image
-  shell: ./scripts/build-joystream-testing-node-docker-image.sh
+  shell: ./scripts/build-joystream-node-docker-image.sh
   args:
     chdir: ../../

+ 16 - 5
devops/ansible/roles/install_dependencies/tasks/main.yml

@@ -1,7 +1,13 @@
-- name: install pip on Debian
+- name: install pip and npm on Debian
   block:
+    - name: create temporary folder
+      file:
+        path: ../../.tmp
+        state: directory
     - name: install pip using apt
       apt: name=python-pip state=present
+    - name: install npm using apt
+      apt: name=npm state=present
   when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'
 
 - name: install pip on Mac
@@ -11,15 +17,15 @@
         path: ../../.tmp
         state: directory
     - name: get pip installer using curl
-      get_url: 
+      get_url:
         url: https://bootstrap.pypa.io/get-pip.py
         dest: ../../.tmp/get-pip.py
     - name: install pip
       shell: python ../../.tmp/get-pip.py
   when: ansible_distribution == 'MacOSX'
-  always: 
+  always:
     - name: remove pip installer script
-      file: 
+      file:
         path: ../../.tmp/get-pip.py
         state: absent
 
@@ -31,5 +37,10 @@
     name: yarn
     global: yes
 
+- name: Install pyrsistent
+  pip:
+    name: pyrsistent==0.16.0
+
 - name: Install docker compose
-  pip: name=docker-compose
+  pip:
+    name: docker-compose==1.26.2

+ 38 - 0
devops/ansible/roles/run_tests_exported_chainspec/tasks/main.yml

@@ -0,0 +1,38 @@
+- name: run network
+  block:
+    - name: yarn install for joystream types
+      shell: yarn workspace @joystream/types install
+
+    - name: yarn build for joystream types
+      shell: yarn workspace @joystream/types build
+
+    - name: yarn install for network tests
+      shell: yarn workspace joystream-testing install
+
+    - name: run docker container
+      docker_container:
+        name: "joystream-node"
+        image: "joystream/node"
+        ports:
+          - "9944:9944"
+        mounts:
+          - target: /testnet-state
+            source: "{{ playbook_dir }}/../../testnets/nicaea-exported-state"
+            type: bind
+            read_only: yes
+        entrypoint: ./node --chain ../testnet-state/raw_chain_spec.json --alice --validator --unsafe-ws-external --rpc-cors=all
+        state: started
+
+    - name: execute network tests
+      shell: yarn test >> ../../.tmp/tests.log
+      args:
+        chdir: ../../tests/network-tests/
+
+  always:
+    - name: display tests log
+      shell: cat ../../.tmp/tests.log
+
+    - name: stop docker container
+      docker_container:
+        name: "joystream-node-testing"
+        state: absent

+ 16 - 5
devops/ansible/roles/run_tests_single_node/tasks/main.yml

@@ -1,21 +1,32 @@
 - name: run network
   block:
+    - name: yarn install for joystream types
+      shell: yarn workspace @joystream/types install
+
+    - name: yarn build for joystream types
+      shell: yarn workspace @joystream/types build
+
+    - name: yarn install for network tests
+      shell: yarn workspace joystream-testing install
 
     - name: run docker container
       docker_container:
-        name: "joystream-node-testing"
-        image: "joystream/node-testing"
+        name: "joystream-node"
+        image: "joystream/node"
         ports:
           - "9944:9944"
-        entrypoint: ./node --chain=chainspec.json --alice --validator --ws-external --rpc-cors=all
+        entrypoint: ./node --dev --alice --validator --unsafe-ws-external --rpc-cors=all
         state: started
 
     - name: execute network tests
-      shell: yarn debug >> ../../.tmp/tests.log
+      shell: yarn test >> ../../.tmp/tests.log
       args:
         chdir: ../../tests/network-tests/
-        
+
   always:
+    - name: display tests log
+      shell: cat ../../.tmp/tests.log
+
     - name: stop docker container
       docker_container:
         name: "joystream-node-testing"

+ 0 - 37
devops/dockerfiles/ansible-node/Dockerfile

@@ -1,37 +0,0 @@
-FROM joystream/rust-builder AS builder
-LABEL description="Compiles all workspace artifacts"
-WORKDIR /joystream
-COPY . /joystream
-
-# Build joystream-node and its dependencies - runtime
-RUN cargo build --release -p joystream-node
-RUN /joystream/scripts/create-test-chainspec.sh
-
-FROM debian:stretch
-LABEL description="Joystream node"
-WORKDIR /joystream
-COPY --from=builder /joystream/target/release/joystream-node /joystream/node
-COPY --from=builder /joystream/target/release/wbuild/joystream-node-runtime/joystream_node_runtime.compact.wasm /joystream/runtime.compact.wasm
-COPY --from=builder /joystream/.tmp/chainspec.json /joystream/chainspec.json
-
-# confirm it works
-RUN /joystream/node --version
-
-# https://manpages.debian.org/stretch/coreutils/b2sum.1.en.html
-# RUN apt-get install coreutils
-# print the blake2 256 hash of the wasm blob
-RUN b2sum -l 256 /joystream/runtime.compact.wasm
-# print the blake2 512 hash of the wasm blob
-RUN b2sum -l 512 /joystream/runtime.compact.wasm
-
-EXPOSE 30333 9933 9944
-
-# Use these volumes to persits chain state and keystore, eg.:
-# --base-path /data
-# optionally separate keystore (otherwise it will be stored in the base path)
-# --keystore-path /keystore
-# if base-path isn't specified, chain state is stored inside container in ~/.local/share/joystream-node/
-# which is not ideal
-VOLUME ["/data", "/keystore"]
-
-ENTRYPOINT ["/joystream/node"]

+ 1 - 1
devops/dockerfiles/node-and-runtime/Dockerfile

@@ -4,7 +4,7 @@ WORKDIR /joystream
 COPY . /joystream
 
 # Build joystream-node and its dependencies - runtime
-RUN cargo build --release -p joystream-node
+RUN WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release -p joystream-node
 
 FROM debian:stretch
 LABEL description="Joystream node"

+ 2 - 2
devops/dockerfiles/rust-builder/Dockerfile

@@ -1,8 +1,8 @@
-FROM liuchong/rustup:1.43.0 AS builder
+FROM liuchong/rustup:1.46.0 AS builder
 LABEL description="Rust and WASM build environment for joystream and substrate"
 
 WORKDIR /setup
 COPY setup.sh /setup
 ENV TERM=xterm
 
-RUN ./setup.sh
+RUN ./setup.sh

+ 5 - 5
devops/git-hooks/pre-push

@@ -1,10 +1,10 @@
 #!/bin/sh
 set -e
 
-echo '+cargo test --release --all'
-BUILD_DUMMY_WASM_BINARY=1 cargo test --all
-
-echo '+cargo clippy --release --all -- -D warnings'
-BUILD_DUMMY_WASM_BINARY=1 cargo clippy --all -- -D warnings
+export WASM_BUILD_TOOLCHAIN=nightly-2020-05-23
 
+echo '+cargo clippy --all -- -D warnings'
+BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings
 
+echo '+cargo test --all'
+cargo test --release --all

+ 4 - 3
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream contributors']
 build = 'build.rs'
 edition = '2018'
 name = 'joystream-node'
-version = '3.0.0'
+version = '3.1.0'
 default-run = "joystream-node"
 
 [[bin]]
@@ -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,11 +76,10 @@ 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"]}
-# sc-service-test = { git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
+sc-service-test = { git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
 frame-system = { package = 'frame-system', git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
 pallet-transaction-payment = { package = 'pallet-transaction-payment', git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
 pallet-grandpa = { package = 'pallet-grandpa', git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }

+ 3 - 3
node/README.md

@@ -26,7 +26,7 @@ cd joystream/
 Compile the node and runtime:
 
 ```bash
-cargo build --release
+WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release
 ```
 
 This produces the binary in `./target/release/joystream-node`
@@ -34,7 +34,7 @@ This produces the binary in `./target/release/joystream-node`
 ### Running local development chain
 
 ```bash
-cargo run --release -- --dev
+./target/release/joystream-node --dev
 ```
 
 If you repeatedly need to restart a new chain,
@@ -49,7 +49,7 @@ this script will build and run a fresh new local development chain (purging exis
 Use the `--chain` argument, and specify the path to the genesis `chain.json` file for that public network. The JSON "chain spec" files for Joystream public networks can be found in [../testnets/](../testnets/).
 
 ```bash
-cargo run --release -- --chain testnets/rome.json
+./target/release/joystream-node --chain testnets/rome.json
 ```
 
 ### Tests and code quality

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
node/res/acropolis_members.json


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
node/res/forum_data_acropolis_encoded.json


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 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: 0,
+        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, 2990),
+        thread_moderation_rationale_constraint: new_validation(10, 290),
+        post_moderation_rationale_constraint: new_validation(10, 290),
+    }
+}

+ 18 - 0
node/src/chain_spec/initial_balances.rs

@@ -0,0 +1,18 @@
+use node_runtime::{AccountId, Balance};
+use serde::Deserialize;
+use std::{fs, path::Path};
+
+#[derive(Deserialize)]
+struct SerializedInitialBalances {
+    balances: Vec<(AccountId, Balance)>,
+}
+
+fn parse_json(data_file: &Path) -> SerializedInitialBalances {
+    let data = fs::read_to_string(data_file).expect("Failed reading file");
+    serde_json::from_str(&data).expect("failed parsing balances data")
+}
+
+/// Deserializes initial balances from json file
+pub fn from_json(data_file: &Path) -> Vec<(AccountId, Balance)> {
+    parse_json(data_file).balances
+}

+ 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![]
+}

+ 140 - 132
node/src/chain_spec.rs → node/src/chain_spec/mod.rs

@@ -29,23 +29,29 @@ use sp_runtime::traits::{IdentifyAccount, Verify};
 use sp_runtime::Perbill;
 
 use node_runtime::{
-    versioned_store::InputValidationLengthConstraint as VsInputValidation,
-    AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig, ContentDirectoryConfig,
-    ContentDirectoryWorkingGroupConfig, 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,
+    ContentDirectoryConfig, ContentDirectoryWorkingGroupConfig, 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_balances;
+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
@@ -127,7 +133,14 @@ 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![],
                     )
                 },
                 Vec::new(),
@@ -161,7 +174,14 @@ 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![],
                     )
                 },
                 Vec::new(),
@@ -174,10 +194,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,7 +206,9 @@ pub fn chain_spec_properties() -> json::map::Map<String, json::Value> {
     );
     properties
 }
-
+// This method should be refactored after Alexandria to reduce number of arguments
+// as more args will likely be needed
+#[allow(clippy::too_many_arguments)]
 pub fn testnet_genesis(
     initial_authorities: Vec<(
         AccountId,
@@ -203,11 +221,16 @@ 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,
+    initial_balances: Vec<(AccountId, Balance)>,
 ) -> GenesisConfig {
-    const CENTS: Balance = 1;
-    const DOLLARS: Balance = 100 * CENTS;
-    const STASH: Balance = 20 * DOLLARS;
-    const ENDOWMENT: Balance = 100_000 * DOLLARS;
+    const STASH: Balance = 5_000;
+    const ENDOWMENT: Balance = 100_000_000;
 
     let default_text_constraint = node_runtime::working_group::default_text_constraint();
 
@@ -222,22 +245,26 @@ pub fn testnet_genesis(
                 .cloned()
                 .map(|k| (k, ENDOWMENT))
                 .chain(initial_authorities.iter().map(|x| (x.0.clone(), STASH)))
+                .chain(
+                    initial_balances
+                        .iter()
+                        .map(|(account, balance)| (account.clone(), *balance)),
+                )
                 .collect(),
         }),
         pallet_staking: Some(StakingConfig {
             validator_count: 20,
-            minimum_validator_count: 1,
+            minimum_validator_count: initial_authorities.len() as u32,
             stakers: initial_authorities
                 .iter()
                 .map(|x| (x.0.clone(), x.1.clone(), STASH, StakerStatus::Validator))
                 .collect(),
             invulnerables: initial_authorities.iter().map(|x| x.0.clone()).collect(),
             slash_reward_fraction: Perbill::from_percent(10),
+            history_depth: 336,
             ..Default::default()
         }),
-        pallet_sudo: Some(SudoConfig {
-            key: root_key.clone(),
-        }),
+        pallet_sudo: Some(SudoConfig { key: root_key }),
         pallet_babe: Some(BabeConfig {
             authorities: vec![],
         }),
@@ -265,21 +292,22 @@ pub fn testnet_genesis(
         election: Some(CouncilElectionConfig {
             auto_start: true,
             election_parameters: ElectionParameters {
-                announcing_period: 3 * DAYS,
+                announcing_period: 2 * DAYS,
                 voting_period: 1 * DAYS,
                 revealing_period: 1 * DAYS,
-                council_size: 12,
+                council_size: 6,
                 candidacy_limit: 25,
-                min_council_stake: 10 * DOLLARS,
-                new_term_duration: 14 * DAYS,
-                min_voting_stake: 1 * DOLLARS,
+                min_council_stake: 1_000,
+                new_term_duration: 10 * DAYS,
+                min_voting_stake: 100,
             },
         }),
         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,
         }),
@@ -300,40 +328,6 @@ 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),
-        }),
         content_directory: Some({
             ContentDirectoryConfig {
                 curator_group_by_id: vec![],
@@ -342,6 +336,9 @@ pub fn testnet_genesis(
                 next_curator_group_id: 1,
             }
         }),
+        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,
@@ -399,76 +396,87 @@ pub fn testnet_genesis(
     }
 }
 
-// Tests are commented out until we find a solution to why
-// building dependencies for the tests are taking so long on Travis CI
+#[cfg(test)]
+pub(crate) mod tests {
+    use super::*;
+    use crate::service::{new_full, new_light};
+    use sc_service_test;
 
-// #[cfg(test)]
-// pub(crate) mod tests {
-//     use super::*;
-//     use crate::service::{new_full, new_light};
-//     use sc_service_test;
-
-//     fn local_testnet_genesis_instant_single() -> GenesisConfig {
-//         testnet_genesis(
-//             vec![get_authority_keys_from_seed("Alice")],
-//             get_account_id_from_seed::<sr25519::Public>("Alice"),
-//             vec![get_authority_keys_from_seed("Alice").0],
-//             crate::proposals_config::development(),
-//         )
-//     }
+    fn local_testnet_genesis_instant_single() -> GenesisConfig {
+        testnet_genesis(
+            vec![get_authority_keys_from_seed("Alice")],
+            get_account_id_from_seed::<sr25519::Public>("Alice"),
+            vec![get_authority_keys_from_seed("Alice").0],
+            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![],
+        )
+    }
 
-//     /// Local testnet config (single validator - Alice)
-//     pub fn integration_test_config_with_single_authority() -> ChainSpec {
-//         ChainSpec::from_genesis(
-//             "Integration Test",
-//             "test",
-//             ChainType::Development,
-//             local_testnet_genesis_instant_single,
-//             vec![],
-//             None,
-//             None,
-//             None,
-//             Default::default(),
-//         )
-//     }
+    /// Local testnet config (single validator - Alice)
+    pub fn integration_test_config_with_single_authority() -> ChainSpec {
+        ChainSpec::from_genesis(
+            "Integration Test",
+            "test",
+            ChainType::Development,
+            local_testnet_genesis_instant_single,
+            vec![],
+            None,
+            None,
+            None,
+            Default::default(),
+        )
+    }
 
-//     fn local_testnet_genesis() -> GenesisConfig {
-//         testnet_genesis(
-//             vec![
-//                 get_authority_keys_from_seed("Alice"),
-//                 get_authority_keys_from_seed("Bob"),
-//             ],
-//             get_account_id_from_seed::<sr25519::Public>("Alice"),
-//             vec![
-//                 get_authority_keys_from_seed("Alice").0,
-//                 get_authority_keys_from_seed("Bob").0,
-//             ],
-//             crate::proposals_config::development(),
-//         )
-//     }
+    fn local_testnet_genesis() -> GenesisConfig {
+        testnet_genesis(
+            vec![
+                get_authority_keys_from_seed("Alice"),
+                get_authority_keys_from_seed("Bob"),
+            ],
+            get_account_id_from_seed::<sr25519::Public>("Alice"),
+            vec![
+                get_authority_keys_from_seed("Alice").0,
+                get_authority_keys_from_seed("Bob").0,
+            ],
+            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![],
+        )
+    }
 
-//     /// Local testnet config (multivalidator Alice + Bob)
-//     pub fn integration_test_config_with_two_authorities() -> ChainSpec {
-//         ChainSpec::from_genesis(
-//             "Integration Test",
-//             "test",
-//             ChainType::Development,
-//             local_testnet_genesis,
-//             vec![],
-//             None,
-//             None,
-//             None,
-//             Default::default(),
-//         )
-//     }
+    /// Local testnet config (multivalidator Alice + Bob)
+    pub fn integration_test_config_with_two_authorities() -> ChainSpec {
+        ChainSpec::from_genesis(
+            "Integration Test",
+            "test",
+            ChainType::Development,
+            local_testnet_genesis,
+            vec![],
+            None,
+            None,
+            None,
+            Default::default(),
+        )
+    }
 
-//     #[test]
-//     #[ignore]
-//     fn test_connectivity() {
-//         sc_service_test::connectivity(
-//             integration_test_config_with_two_authorities(),
-//             |config| new_full(config),
-//             |config| new_light(config),
-//         );
-//     }
-// }
+    #[test]
+    #[ignore]
+    fn test_connectivity() {
+        sc_service_test::connectivity(
+            integration_test_config_with_two_authorities(),
+            |config| new_full(config),
+            |config| new_light(config),
+        );
+    }
+}

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

@@ -8,10 +8,10 @@ pub fn development() -> ProposalsConfigParameters {
 
 /// Staging chain config. Shorter grace periods and voting periods than default.
 pub fn staging() -> ProposalsConfigParameters {
-    ProposalsConfigParameters::with_grace_and_voting_periods(200, 600)
+    ProposalsConfigParameters::with_grace_and_voting_periods(20, 30)
 }
 
 /// The default configuration as defined in the runtime module
-pub fn default() -> ProposalsConfigParameters {
+pub fn production() -> ProposalsConfigParameters {
     ProposalsConfigParameters::default()
 }

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

+ 244 - 247
node/src/service.rs

@@ -415,250 +415,247 @@ pub fn new_light(config: Configuration) -> Result<impl AbstractService, ServiceE
     Ok(service)
 }
 
-// Tests are commented out until we find a solution to why
-// building dependencies for the tests are taking so long on Travis CI
-
-// #[cfg(test)]
-// mod tests {
-//     use crate::node_executor;
-//     use crate::node_rpc;
-//     use crate::service::{new_full, new_light};
-//     use codec::{Decode, Encode};
-//     use node_runtime::RuntimeApi;
-//     use node_runtime::{currency::CENTS, SLOT_DURATION};
-//     use node_runtime::{opaque::Block, AccountId, DigestItem, Signature};
-//     use node_runtime::{BalancesCall, Call, UncheckedExtrinsic};
-//     use sc_consensus_babe::{BabeIntermediate, CompatibleDigestItem, INTERMEDIATE_KEY};
-//     use sc_consensus_epochs::descendent_query;
-//     use sc_finality_grandpa::{self as grandpa};
-//     use sc_service::AbstractService;
-//     use sp_consensus::{
-//         BlockImport, BlockImportParams, BlockOrigin, Environment, ForkChoiceStrategy, Proposer,
-//         RecordProof,
-//     };
-//     use sp_core::{crypto::Pair as CryptoPair, H256};
-//     use sp_finality_tracker;
-//     use sp_keyring::AccountKeyring;
-//     use sp_runtime::traits::IdentifyAccount;
-//     use sp_runtime::{
-//         generic::{BlockId, Digest, Era, SignedPayload},
-//         traits::Verify,
-//         traits::{Block as BlockT, Header as HeaderT},
-//         OpaqueExtrinsic,
-//     };
-//     use sp_timestamp;
-//     use sp_transaction_pool::{ChainEvent, MaintainedTransactionPool};
-//     use std::{any::Any, borrow::Cow, sync::Arc};
-
-//     type AccountPublic = <Signature as Verify>::Signer;
-
-//     // Long running test. Run it locally only after the node changes.
-//     #[test]
-//     // It is "ignored", but the node-cli ignored tests are running on the CI.
-//     // This can be run locally with `cargo test --release -p node-cli test_sync -- --ignored`.
-//     #[ignore]
-//     fn test_sync() {
-//         let keystore_path = tempfile::tempdir().expect("Creates keystore path");
-//         let keystore =
-//             sc_keystore::Store::open(keystore_path.path(), None).expect("Creates keystore");
-//         let alice = keystore
-//             .write()
-//             .insert_ephemeral_from_seed::<sc_consensus_babe::AuthorityPair>("//Alice")
-//             .expect("Creates authority pair");
-
-//         let chain_spec = crate::chain_spec::tests::integration_test_config_with_single_authority();
-
-//         // For the block factory
-//         let mut slot_num = 1u64;
-
-//         // For the extrinsics factory
-//         let bob = Arc::new(AccountKeyring::Bob.pair());
-//         let charlie = Arc::new(AccountKeyring::Charlie.pair());
-//         let mut index = 0;
-
-//         sc_service_test::sync(
-//             chain_spec,
-//             |config| {
-//                 let mut setup_handles = None;
-//                 new_full!(
-//                     config,
-//                     |block_import: &sc_consensus_babe::BabeBlockImport<Block, _, _>,
-//                      babe_link: &sc_consensus_babe::BabeLink<Block>| {
-//                         setup_handles = Some((block_import.clone(), babe_link.clone()));
-//                     }
-//                 )
-//                 .map(move |(node, x)| (node, (x, setup_handles.unwrap())))
-//             },
-//             |config| new_light(config),
-//             |service, &mut (ref inherent_data_providers, (ref mut block_import, ref babe_link))| {
-//                 let mut inherent_data = inherent_data_providers
-//                     .create_inherent_data()
-//                     .expect("Creates inherent data.");
-//                 inherent_data.replace_data(sp_finality_tracker::INHERENT_IDENTIFIER, &1u64);
-
-//                 let parent_id = BlockId::number(service.client().chain_info().best_number);
-//                 let parent_header = service.client().header(&parent_id).unwrap().unwrap();
-//                 let parent_hash = parent_header.hash();
-//                 let parent_number = *parent_header.number();
-
-//                 futures::executor::block_on(service.transaction_pool().maintain(
-//                     ChainEvent::NewBlock {
-//                         is_new_best: true,
-//                         hash: parent_header.hash(),
-//                         tree_route: None,
-//                         header: parent_header.clone(),
-//                     },
-//                 ));
-
-//                 let mut proposer_factory = sc_basic_authorship::ProposerFactory::new(
-//                     service.client(),
-//                     service.transaction_pool(),
-//                     None,
-//                 );
-
-//                 let epoch_descriptor = babe_link
-//                     .epoch_changes()
-//                     .lock()
-//                     .epoch_descriptor_for_child_of(
-//                         descendent_query(&*service.client()),
-//                         &parent_hash,
-//                         parent_number,
-//                         slot_num,
-//                     )
-//                     .unwrap()
-//                     .unwrap();
-
-//                 let mut digest = Digest::<H256>::default();
-
-//                 // even though there's only one authority some slots might be empty,
-//                 // so we must keep trying the next slots until we can claim one.
-//                 let babe_pre_digest = loop {
-//                     inherent_data.replace_data(
-//                         sp_timestamp::INHERENT_IDENTIFIER,
-//                         &(slot_num * SLOT_DURATION),
-//                     );
-//                     if let Some(babe_pre_digest) = sc_consensus_babe::test_helpers::claim_slot(
-//                         slot_num,
-//                         &parent_header,
-//                         &*service.client(),
-//                         &keystore,
-//                         &babe_link,
-//                     ) {
-//                         break babe_pre_digest;
-//                     }
-
-//                     slot_num += 1;
-//                 };
-
-//                 digest.push(<DigestItem as CompatibleDigestItem>::babe_pre_digest(
-//                     babe_pre_digest,
-//                 ));
-
-//                 let new_block = futures::executor::block_on(async move {
-//                     let proposer = proposer_factory.init(&parent_header).await;
-//                     proposer
-//                         .unwrap()
-//                         .propose(
-//                             inherent_data,
-//                             digest,
-//                             std::time::Duration::from_secs(1),
-//                             RecordProof::Yes,
-//                         )
-//                         .await
-//                 })
-//                 .expect("Error making test block")
-//                 .block;
-
-//                 let (new_header, new_body) = new_block.deconstruct();
-//                 let pre_hash = new_header.hash();
-//                 // sign the pre-sealed hash of the block and then
-//                 // add it to a digest item.
-//                 let to_sign = pre_hash.encode();
-//                 let signature = alice.sign(&to_sign[..]);
-//                 let item = <DigestItem as CompatibleDigestItem>::babe_seal(signature.into());
-//                 slot_num += 1;
-
-//                 let mut params = BlockImportParams::new(BlockOrigin::File, new_header);
-//                 params.post_digests.push(item);
-//                 params.body = Some(new_body);
-//                 params.intermediates.insert(
-//                     Cow::from(INTERMEDIATE_KEY),
-//                     Box::new(BabeIntermediate::<Block> { epoch_descriptor }) as Box<dyn Any>,
-//                 );
-//                 params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
-
-//                 block_import
-//                     .import_block(params, Default::default())
-//                     .expect("error importing test block");
-//             },
-//             |service, _| {
-//                 let amount = 5 * CENTS;
-//                 let to: AccountId = AccountPublic::from(bob.public()).into_account().into();
-//                 let from: AccountId = AccountPublic::from(charlie.public()).into_account().into();
-//                 let genesis_hash = service.client().block_hash(0).unwrap().unwrap();
-//                 let best_block_id = BlockId::number(service.client().chain_info().best_number);
-//                 let (spec_version, transaction_version) = {
-//                     let version = service.client().runtime_version_at(&best_block_id).unwrap();
-//                     (version.spec_version, version.transaction_version)
-//                 };
-//                 let signer = charlie.clone();
-
-//                 let function = Call::Balances(BalancesCall::transfer(to.into(), amount));
-
-//                 let check_spec_version = frame_system::CheckSpecVersion::new();
-//                 let check_tx_version = frame_system::CheckTxVersion::new();
-//                 let check_genesis = frame_system::CheckGenesis::new();
-//                 let check_era = frame_system::CheckEra::from(Era::Immortal);
-//                 let check_nonce = frame_system::CheckNonce::from(index);
-//                 let check_weight = frame_system::CheckWeight::new();
-//                 let payment = pallet_transaction_payment::ChargeTransactionPayment::from(0);
-//                 let validate_grandpa_equivocation =
-//                     pallet_grandpa::ValidateEquivocationReport::new();
-//                 let extra = (
-//                     check_spec_version,
-//                     check_tx_version,
-//                     check_genesis,
-//                     check_era,
-//                     check_nonce,
-//                     check_weight,
-//                     payment,
-//                     validate_grandpa_equivocation,
-//                 );
-//                 let raw_payload = SignedPayload::from_raw(
-//                     function,
-//                     extra,
-//                     (
-//                         spec_version,
-//                         transaction_version,
-//                         genesis_hash,
-//                         genesis_hash,
-//                         (),
-//                         (),
-//                         (),
-//                         (),
-//                     ),
-//                 );
-//                 let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
-//                 let (function, extra, _) = raw_payload.deconstruct();
-//                 let xt =
-//                     UncheckedExtrinsic::new_signed(function, from.into(), signature.into(), extra)
-//                         .encode();
-//                 let v: Vec<u8> = Decode::decode(&mut xt.as_slice()).unwrap();
-
-//                 index += 1;
-//                 OpaqueExtrinsic(v)
-//             },
-//         );
-//     }
-
-//     #[test]
-//     #[ignore]
-//     fn test_consensus() {
-//         sc_service_test::consensus(
-//             crate::chain_spec::tests::integration_test_config_with_two_authorities(),
-//             |config| new_full(config),
-//             |config| new_light(config),
-//             vec!["//Alice".into(), "//Bob".into()],
-//         )
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use crate::node_executor;
+    use crate::node_rpc;
+    use crate::service::{new_full, new_light};
+    use codec::{Decode, Encode};
+    use node_runtime::RuntimeApi;
+    use node_runtime::{currency::CENTS, SLOT_DURATION};
+    use node_runtime::{opaque::Block, AccountId, DigestItem, Signature};
+    use node_runtime::{BalancesCall, Call, UncheckedExtrinsic};
+    use sc_consensus_babe::{BabeIntermediate, CompatibleDigestItem, INTERMEDIATE_KEY};
+    use sc_consensus_epochs::descendent_query;
+    use sc_finality_grandpa::{self as grandpa};
+    use sc_service::AbstractService;
+    use sp_consensus::{
+        BlockImport, BlockImportParams, BlockOrigin, Environment, ForkChoiceStrategy, Proposer,
+        RecordProof,
+    };
+    use sp_core::{crypto::Pair as CryptoPair, H256};
+    use sp_finality_tracker;
+    use sp_keyring::AccountKeyring;
+    use sp_runtime::traits::IdentifyAccount;
+    use sp_runtime::{
+        generic::{BlockId, Digest, Era, SignedPayload},
+        traits::Verify,
+        traits::{Block as BlockT, Header as HeaderT},
+        OpaqueExtrinsic,
+    };
+    use sp_timestamp;
+    use sp_transaction_pool::{ChainEvent, MaintainedTransactionPool};
+    use std::{any::Any, borrow::Cow, sync::Arc};
+
+    type AccountPublic = <Signature as Verify>::Signer;
+
+    // Long running test. Run it locally only after the node changes.
+    #[test]
+    // It is "ignored", but the node-cli ignored tests are running on the CI.
+    // This can be run locally with `cargo test --release -p node-cli test_sync -- --ignored`.
+    #[ignore]
+    fn test_sync() {
+        let keystore_path = tempfile::tempdir().expect("Creates keystore path");
+        let keystore =
+            sc_keystore::Store::open(keystore_path.path(), None).expect("Creates keystore");
+        let alice = keystore
+            .write()
+            .insert_ephemeral_from_seed::<sc_consensus_babe::AuthorityPair>("//Alice")
+            .expect("Creates authority pair");
+
+        let chain_spec = crate::chain_spec::tests::integration_test_config_with_single_authority();
+
+        // For the block factory
+        let mut slot_num = 1u64;
+
+        // For the extrinsics factory
+        let bob = Arc::new(AccountKeyring::Bob.pair());
+        let charlie = Arc::new(AccountKeyring::Charlie.pair());
+        let mut index = 0;
+
+        sc_service_test::sync(
+            chain_spec,
+            |config| {
+                let mut setup_handles = None;
+                new_full!(
+                    config,
+                    |block_import: &sc_consensus_babe::BabeBlockImport<Block, _, _>,
+                     babe_link: &sc_consensus_babe::BabeLink<Block>| {
+                        setup_handles = Some((block_import.clone(), babe_link.clone()));
+                    }
+                )
+                .map(move |(node, x)| (node, (x, setup_handles.unwrap())))
+            },
+            |config| new_light(config),
+            |service, &mut (ref inherent_data_providers, (ref mut block_import, ref babe_link))| {
+                let mut inherent_data = inherent_data_providers
+                    .create_inherent_data()
+                    .expect("Creates inherent data.");
+                inherent_data.replace_data(sp_finality_tracker::INHERENT_IDENTIFIER, &1u64);
+
+                let parent_id = BlockId::number(service.client().chain_info().best_number);
+                let parent_header = service.client().header(&parent_id).unwrap().unwrap();
+                let parent_hash = parent_header.hash();
+                let parent_number = *parent_header.number();
+
+                futures::executor::block_on(service.transaction_pool().maintain(
+                    ChainEvent::NewBlock {
+                        is_new_best: true,
+                        hash: parent_header.hash(),
+                        tree_route: None,
+                        header: parent_header.clone(),
+                    },
+                ));
+
+                let mut proposer_factory = sc_basic_authorship::ProposerFactory::new(
+                    service.client(),
+                    service.transaction_pool(),
+                    None,
+                );
+
+                let epoch_descriptor = babe_link
+                    .epoch_changes()
+                    .lock()
+                    .epoch_descriptor_for_child_of(
+                        descendent_query(&*service.client()),
+                        &parent_hash,
+                        parent_number,
+                        slot_num,
+                    )
+                    .unwrap()
+                    .unwrap();
+
+                let mut digest = Digest::<H256>::default();
+
+                // even though there's only one authority some slots might be empty,
+                // so we must keep trying the next slots until we can claim one.
+                let babe_pre_digest = loop {
+                    inherent_data.replace_data(
+                        sp_timestamp::INHERENT_IDENTIFIER,
+                        &(slot_num * SLOT_DURATION),
+                    );
+                    if let Some(babe_pre_digest) = sc_consensus_babe::test_helpers::claim_slot(
+                        slot_num,
+                        &parent_header,
+                        &*service.client(),
+                        &keystore,
+                        &babe_link,
+                    ) {
+                        break babe_pre_digest;
+                    }
+
+                    slot_num += 1;
+                };
+
+                digest.push(<DigestItem as CompatibleDigestItem>::babe_pre_digest(
+                    babe_pre_digest,
+                ));
+
+                let new_block = futures::executor::block_on(async move {
+                    let proposer = proposer_factory.init(&parent_header).await;
+                    proposer
+                        .unwrap()
+                        .propose(
+                            inherent_data,
+                            digest,
+                            std::time::Duration::from_secs(1),
+                            RecordProof::Yes,
+                        )
+                        .await
+                })
+                .expect("Error making test block")
+                .block;
+
+                let (new_header, new_body) = new_block.deconstruct();
+                let pre_hash = new_header.hash();
+                // sign the pre-sealed hash of the block and then
+                // add it to a digest item.
+                let to_sign = pre_hash.encode();
+                let signature = alice.sign(&to_sign[..]);
+                let item = <DigestItem as CompatibleDigestItem>::babe_seal(signature.into());
+                slot_num += 1;
+
+                let mut params = BlockImportParams::new(BlockOrigin::File, new_header);
+                params.post_digests.push(item);
+                params.body = Some(new_body);
+                params.intermediates.insert(
+                    Cow::from(INTERMEDIATE_KEY),
+                    Box::new(BabeIntermediate::<Block> { epoch_descriptor }) as Box<dyn Any>,
+                );
+                params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
+
+                block_import
+                    .import_block(params, Default::default())
+                    .expect("error importing test block");
+            },
+            |service, _| {
+                let amount = 5 * CENTS;
+                let to: AccountId = AccountPublic::from(bob.public()).into_account().into();
+                let from: AccountId = AccountPublic::from(charlie.public()).into_account().into();
+                let genesis_hash = service.client().block_hash(0).unwrap().unwrap();
+                let best_block_id = BlockId::number(service.client().chain_info().best_number);
+                let (spec_version, transaction_version) = {
+                    let version = service.client().runtime_version_at(&best_block_id).unwrap();
+                    (version.spec_version, version.transaction_version)
+                };
+                let signer = charlie.clone();
+
+                let function = Call::Balances(BalancesCall::transfer(to.into(), amount));
+
+                let check_spec_version = frame_system::CheckSpecVersion::new();
+                let check_tx_version = frame_system::CheckTxVersion::new();
+                let check_genesis = frame_system::CheckGenesis::new();
+                let check_era = frame_system::CheckEra::from(Era::Immortal);
+                let check_nonce = frame_system::CheckNonce::from(index);
+                let check_weight = frame_system::CheckWeight::new();
+                let payment = pallet_transaction_payment::ChargeTransactionPayment::from(0);
+                let validate_grandpa_equivocation =
+                    pallet_grandpa::ValidateEquivocationReport::new();
+                let extra = (
+                    check_spec_version,
+                    check_tx_version,
+                    check_genesis,
+                    check_era,
+                    check_nonce,
+                    check_weight,
+                    payment,
+                    validate_grandpa_equivocation,
+                );
+                let raw_payload = SignedPayload::from_raw(
+                    function,
+                    extra,
+                    (
+                        spec_version,
+                        transaction_version,
+                        genesis_hash,
+                        genesis_hash,
+                        (),
+                        (),
+                        (),
+                        (),
+                    ),
+                );
+                let signature = raw_payload.using_encoded(|payload| signer.sign(payload));
+                let (function, extra, _) = raw_payload.deconstruct();
+                let xt =
+                    UncheckedExtrinsic::new_signed(function, from.into(), signature.into(), extra)
+                        .encode();
+                let v: Vec<u8> = Decode::decode(&mut xt.as_slice()).unwrap();
+
+                index += 1;
+                OpaqueExtrinsic(v)
+            },
+        );
+    }
+
+    #[test]
+    #[ignore]
+    fn test_consensus() {
+        sc_service_test::consensus(
+            crate::chain_spec::tests::integration_test_config_with_two_authorities(),
+            |config| new_full(config),
+            |config| new_light(config),
+            vec!["//Alice".into(), "//Bob".into()],
+        )
+    }
+}

+ 7 - 11
package.json

@@ -20,24 +20,20 @@
     "devops/eslint-config",
     "devops/prettier-config",
     "pioneer",
-    "pioneer/packages/apps*",
-    "pioneer/packages/page*",
-    "pioneer/packages/react*",
-    "pioneer/packages/joy-utils",
-    "pioneer/packages/joy-members",
-    "pioneer/packages/joy-pages",
+    "pioneer/packages/*",
     "utils/api-examples"
   ],
   "resolutions": {
     "@polkadot/api": "1.26.1",
     "@polkadot/api-contract": "1.26.1",
-    "@polkadot/keyring": "3.0.1",
+    "@polkadot/keyring": "^3.0.1",
     "@polkadot/types": "1.26.1",
-    "@polkadot/util": "3.0.1",
-    "@polkadot/util-crypto": "3.0.1",
-    "@polkadot/wasm-crypto": "1.2.1",
+    "@polkadot/util": "^3.0.1",
+    "@polkadot/util-crypto": "^3.0.1",
+    "@polkadot/wasm-crypto": "^1.2.1",
     "babel-core": "^7.0.0-bridge.0",
-    "typescript": "^3.9.7"
+    "typescript": "^3.9.7",
+    "bn.js": "^5.1.2"
   },
   "devDependencies": {
     "husky": "^4.2.5",

+ 0 - 9
pioneer/.eslintignore

@@ -1,14 +1,5 @@
 **/build/*
 **/coverage/*
 **/node_modules/*
-packages/old-apps/*
-packages/joy-election/*
-packages/joy-forum/*
-packages/joy-help/*
-packages/joy-media/*
-packages/joy-proposals/*
-packages/joy-roles/*
-packages/joy-settings/*
-packages/joy-utils-old/*
 .eslintrc.js
 i18next-scanner.config.js

+ 2 - 0
pioneer/.eslintrc.js

@@ -27,6 +27,8 @@ module.exports = {
     'react/jsx-max-props-per-line': 'off',
     'sort-destructure-keys/sort-destructure-keys': 'off',
     '@typescript-eslint/unbound-method': 'warn', // Doesn't work well with our version of Formik, see: https://github.com/formium/formik/issues/2589
+    'react-hooks/exhaustive-deps': 'warn', // Causes more issues than it solves currently
+    'no-void': 'off' // Otherwise we cannot mark unhandles promises
   },
   // isolate pioneer from monorepo eslint rules
   root: true

+ 39 - 73
pioneer/.storybook/webpack.config.js

@@ -1,81 +1,47 @@
 const path = require('path')
 const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
 module.exports = ({ config }) => {
+  // Styles (replace the provided rule):
+  const originalCssRuleIndex = config.module.rules.findIndex(rule => rule.test.source.includes('.css'));
+  config.module.rules[originalCssRuleIndex] = {
+    test: /\.(sa|sc|c)ss$/i,
+    use: [
+      // Creates `style` nodes from JS strings
+      'style-loader',
+      // Translates CSS into CommonJS
+      'css-loader',
+      // Compiles Sass to CSS
+      'sass-loader'
+    ]
+  };
 
-// Post CSS loader for sources:
-config.module.rules.push({
-  test: /\.css$/,
-  include: path.resolve(__dirname, '../packages'),
-  exclude: /(node_modules)/,
-  use: [
-    {
-      loader: require.resolve('postcss-loader'),
-      options: {
-        // Set postcss.config.js config path && ctx
-        config: {
-          path: '../postcss.config.js',
-        },
-        ident: 'postcss',
-        plugins: () => [
-          require('precss'),
-          require('autoprefixer'),
-          require('postcss-simple-vars'),
-          require('postcss-nested'),
-          require('postcss-import'),
-          require('postcss-clean')(),
-          require('postcss-flexbugs-fixes')
-        ]
-      }
-    }
-  ]
-});
+  // TypeScript loader (via Babel to match polkadot/apps)
+  config.module.rules.push({
+    test: /\.(js|ts|tsx)$/,
+    exclude: /(node_modules)/,
+    use: [
+      {
+        loader: require.resolve('babel-loader'),
+        options: require('@polkadot/dev/config/babel')
+      },
+    ],
+  });
+  config.resolve.extensions.push('.js', '.ts', '.tsx');
 
-// TypeScript loader (via Babel to match polkadot/apps)
-config.module.rules.push({
-  test: /\.(js|ts|tsx)$/,
-  exclude: /(node_modules)/,
-  use: [
-    {
-      loader: require.resolve('babel-loader'),
-      options: require('@polkadot/dev-react/config/babel')
-    },
-  ],
-});
-config.resolve.extensions.push('.js', '.ts', '.tsx');
+  // TSConfig, uses the same file as packages
+  config.resolve.plugins = config.resolve.plugins || [];
+  config.resolve.plugins.push(
+    new TsconfigPathsPlugin({
+      configFile: path.resolve(__dirname, '../tsconfig.json'),
+    })
+  );
 
-// TSConfig, uses the same file as packages
-config.resolve.plugins = config.resolve.plugins || [];
-config.resolve.plugins.push(
-  new TsconfigPathsPlugin({
-    configFile: path.resolve(__dirname, '../tsconfig.json'),
-  })
-);
+  // Stories parser
+  config.module.rules.push({
+      test: /\.stories\.tsx?$/,
+      loaders: [require.resolve('@storybook/source-loader')],
+      enforce: 'pre',
+  });
 
-// Stories parser
-config.module.rules.push({
-    test: /\.stories\.tsx?$/,
-    loaders: [require.resolve('@storybook/source-loader')],
-    enforce: 'pre',
-});
-
-// CSS preprocessors
-config.module.rules.push(
-    {
-        test: /\.s[ac]ss$/i,
-        use: [
-            // Creates `style` nodes from JS strings
-            'style-loader',
-            // Translates CSS into CommonJS
-            'css-loader',
-            // Compiles Sass to CSS
-            'sass-loader',
-        ],
-    },
-    {
-        test: /\.less$/,
-        loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
-    }
-);
-
-return config;
+  return config;
 };

+ 5 - 4
pioneer/package.json

@@ -25,7 +25,6 @@
     "test": "echo \"skipping tests\"",
     "vanitygen": "node packages/app-accounts/scripts/vanitygen.js",
     "start": "yarn clean && cd packages/apps && webpack --config webpack.config.js",
-    "generate-schemas": "json2ts -i packages/joy-types/src/schemas/role.schema.json -o packages/joy-types/src/schemas/role.schema.ts",
     "build-storybook": "build-storybook -c .storybook",
     "storybook": "start-storybook -s ./packages/apps/public -p 3001"
   },
@@ -40,7 +39,7 @@
     "@types/chart.js": "^2.9.23",
     "@types/file-saver": "^2.0.1",
     "@types/i18next": "^13.0.0",
-    "@types/jest": "^26.0.7",
+    "@types/jest": "^26.0.10",
     "@types/react-beautiful-dnd": "^13.0.0",
     "@types/react-copy-to-clipboard": "^4.3.0",
     "@types/react-dom": "^16.9.8",
@@ -74,11 +73,13 @@
     "@storybook/addon-actions": "^5.2.5",
     "@storybook/addon-console": "^1.2.1",
     "@storybook/react": "^5.2.5",
-    "json-schema-to-typescript": "^7.1.0",
     "storybook-react-router": "^1.0.8",
     "typescript": "^3.9.7",
     "eslint-plugin-header": "^3.0.0",
-    "eslint-plugin-sort-destructure-keys": "^1.3.5"
+    "eslint-plugin-sort-destructure-keys": "^1.3.5",
+    "jest": "^26.4.1",
+    "ts-jest": "^26.2.0",
+    "tsconfig-paths-webpack-plugin": "^3.2.0"
   },
   "dependencies": {
     "@types/lodash": "^4.14.138",

+ 90 - 90
pioneer/packages/apps-config/src/settings/endpoints.ts

@@ -33,94 +33,94 @@ function createLive (t: TFunction): LinkOption[] {
       info: 'joystream',
       text: t<string>('rpc.joystream', 'Joystream (Current Testnet, hosted by Jsgenesis)', { ns: 'apps-config' }),
       value: 'wss://rome-rpc-endpoint.joystream.org:9944'
-    },
-    {
-      dnslink: 'polkadot',
-      info: 'polkadot',
-      text: t<string>('rpc.polkadot.parity', 'Polkadot (Live, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://rpc.polkadot.io'
-    },
-    {
-      dnslink: 'polkadot',
-      info: 'polkadot',
-      text: t<string>('rpc.polkadot.w3f', 'Polkadot (Live, hosted by Web3 Foundation)', { ns: 'apps-config' }),
-      value: 'wss://cc1-1.polkadot.network'
-    },
-    {
-      dnslink: 'kusama',
-      info: 'kusama',
-      text: t<string>('rpc.kusama.parity', 'Kusama (Polkadot Canary, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://kusama-rpc.polkadot.io/'
-    },
-    {
-      dnslink: 'kusama',
-      info: 'kusama',
-      text: t<string>('rpc.kusama.w3f', 'Kusama (Polkadot Canary, hosted by Web3 Foundation)', { ns: 'apps-config' }),
-      value: 'wss://cc3-5.kusama.network/'
-    },
-    {
-      dnslink: 'kusama',
-      info: 'kusama',
-      text: t<string>('rpc.kusama.ava', 'Kusama (Polkadot Canary, user-run public nodes; see https://status.cloud.ava.do/)', { ns: 'apps-config' }),
-      value: 'wss://kusama.polkadot.cloud.ava.do/'
-    },
-    {
-      dnslink: 'centrifuge',
-      info: 'centrifuge',
-      text: t<string>('rpc.centrifuge', 'Centrifuge (Mainnet, hosted by Centrifuge)', { ns: 'apps-config' }),
-      value: 'wss://fullnode.centrifuge.io'
-    },
-    {
-      dnslink: 'edgeware',
-      info: 'edgeware',
-      text: t<string>('rpc.edgeware', 'Edgeware (Mainnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
-      value: 'wss://mainnet1.edgewa.re'
-    },
-    {
-      dnslink: 'kulupu',
-      info: 'substrate',
-      text: t<string>('rpc.kulupu', 'Kulupu (Kulupu Mainnet, hosted by Kulupu)', { ns: 'apps-config' }),
-      value: 'wss://rpc.kulupu.network/ws'
     }
+    // {
+    //   dnslink: 'polkadot',
+    //   info: 'polkadot',
+    //   text: t<string>('rpc.polkadot.parity', 'Polkadot (Live, hosted by Parity)', { ns: 'apps-config' }),
+    //   value: 'wss://rpc.polkadot.io'
+    // },
+    // {
+    //   dnslink: 'polkadot',
+    //   info: 'polkadot',
+    //   text: t<string>('rpc.polkadot.w3f', 'Polkadot (Live, hosted by Web3 Foundation)', { ns: 'apps-config' }),
+    //   value: 'wss://cc1-1.polkadot.network'
+    // },
+    // {
+    //   dnslink: 'kusama',
+    //   info: 'kusama',
+    //   text: t<string>('rpc.kusama.parity', 'Kusama (Polkadot Canary, hosted by Parity)', { ns: 'apps-config' }),
+    //   value: 'wss://kusama-rpc.polkadot.io/'
+    // },
+    // {
+    //   dnslink: 'kusama',
+    //   info: 'kusama',
+    //   text: t<string>('rpc.kusama.w3f', 'Kusama (Polkadot Canary, hosted by Web3 Foundation)', { ns: 'apps-config' }),
+    //   value: 'wss://cc3-5.kusama.network/'
+    // },
+    // {
+    //   dnslink: 'kusama',
+    //   info: 'kusama',
+    //   text: t<string>('rpc.kusama.ava', 'Kusama (Polkadot Canary, user-run public nodes; see https://status.cloud.ava.do/)', { ns: 'apps-config' }),
+    //   value: 'wss://kusama.polkadot.cloud.ava.do/'
+    // },
+    // {
+    //   dnslink: 'centrifuge',
+    //   info: 'centrifuge',
+    //   text: t<string>('rpc.centrifuge', 'Centrifuge (Mainnet, hosted by Centrifuge)', { ns: 'apps-config' }),
+    //   value: 'wss://fullnode.centrifuge.io'
+    // },
+    // {
+    //   dnslink: 'edgeware',
+    //   info: 'edgeware',
+    //   text: t<string>('rpc.edgeware', 'Edgeware (Mainnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
+    //   value: 'wss://mainnet1.edgewa.re'
+    // },
+    // {
+    //   dnslink: 'kulupu',
+    //   info: 'substrate',
+    //   text: t<string>('rpc.kulupu', 'Kulupu (Kulupu Mainnet, hosted by Kulupu)', { ns: 'apps-config' }),
+    //   value: 'wss://rpc.kulupu.network/ws'
+    // }
   ];
 }
 
-function createTest (t: TFunction): LinkOption[] {
-  return [
-    {
-      dnslink: 'westend',
-      info: 'westend',
-      text: t<string>('rpc.westend', 'Westend (Polkadot Testnet, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://westend-rpc.polkadot.io'
-    },
-    {
-      info: 'acala',
-      text: t<string>('rpc.mandala', 'Mandala (Acala Testnet, hosted by Acala)', { ns: 'apps-config' }),
-      value: 'wss://node-6684611762228215808.jm.onfinality.io/ws'
-    },
-    {
-      info: 'edgeware',
-      text: t<string>('rpc.berlin', 'Berlin (Edgeware Testnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
-      value: 'wss://berlin1.edgewa.re'
-    },
-    {
-      info: 'substrate',
-      text: t<string>('rpc.flamingfir', 'Flaming Fir (Substrate Testnet, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://substrate-rpc.parity.io/'
-    },
-    {
-      info: 'nodle',
-      text: t<string>('rpc.arcadia', 'Arcadia (Nodle Testnet, hosted by Nodle)', { ns: 'apps-config' }),
-      value: 'wss://arcadia1.nodleprotocol.io/'
-    },
-    {
-      info: 'datahighway',
-      isDisabled: true,
-      text: t<string>('rpc.datahighway.harbour', 'Harbour (DataHighway Testnet, hosted by MXC)', { ns: 'apps-config' }),
-      value: 'wss://testnet-harbour.datahighway.com'
-    }
-  ];
-}
+// function createTest (t: TFunction): LinkOption[] {
+//   return [
+//     {
+//       dnslink: 'westend',
+//       info: 'westend',
+//       text: t<string>('rpc.westend', 'Westend (Polkadot Testnet, hosted by Parity)', { ns: 'apps-config' }),
+//       value: 'wss://westend-rpc.polkadot.io'
+//     },
+//     {
+//       info: 'acala',
+//       text: t<string>('rpc.mandala', 'Mandala (Acala Testnet, hosted by Acala)', { ns: 'apps-config' }),
+//       value: 'wss://node-6684611762228215808.jm.onfinality.io/ws'
+//     },
+//     {
+//       info: 'edgeware',
+//       text: t<string>('rpc.berlin', 'Berlin (Edgeware Testnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
+//       value: 'wss://berlin1.edgewa.re'
+//     },
+//     {
+//       info: 'substrate',
+//       text: t<string>('rpc.flamingfir', 'Flaming Fir (Substrate Testnet, hosted by Parity)', { ns: 'apps-config' }),
+//       value: 'wss://substrate-rpc.parity.io/'
+//     },
+//     {
+//       info: 'nodle',
+//       text: t<string>('rpc.arcadia', 'Arcadia (Nodle Testnet, hosted by Nodle)', { ns: 'apps-config' }),
+//       value: 'wss://arcadia1.nodleprotocol.io/'
+//     },
+//     {
+//       info: 'datahighway',
+//       isDisabled: true,
+//       text: t<string>('rpc.datahighway.harbour', 'Harbour (DataHighway Testnet, hosted by MXC)', { ns: 'apps-config' }),
+//       value: 'wss://testnet-harbour.datahighway.com'
+//     }
+//   ];
+// }
 
 function createCustom (t: TFunction): LinkOption[] {
   const WS_URL = (
@@ -158,12 +158,12 @@ export default function create (t: TFunction): LinkOption[] {
       value: ''
     },
     ...createLive(t),
-    {
-      isHeader: true,
-      text: t<string>('rpc.header.test', 'Test networks', { ns: 'apps-config' }),
-      value: ''
-    },
-    ...createTest(t),
+    // {
+    //   isHeader: true,
+    //   text: t<string>('rpc.header.test', 'Test networks', { ns: 'apps-config' }),
+    //   value: ''
+    // },
+    // ...createTest(t),
     {
       isHeader: true,
       text: t<string>('rpc.header.dev', 'Development', { ns: 'apps-config' }),

+ 2 - 2
pioneer/packages/apps-routing/src/accounts.ts

@@ -12,9 +12,9 @@ export default function create (t: <T = string> (key: string, text: string, opti
     display: {
       needsApi: []
     },
-    icon: 'users',
+    icon: 'key',
     name: 'accounts',
-    text: t<string>('nav.accounts', 'Accounts', { ns: 'apps-routing' }),
+    text: t<string>('nav.accounts', 'My Keys', { ns: 'apps-routing' }),
     useCounter
   };
 }

+ 16 - 0
pioneer/packages/apps-routing/src/index.ts

@@ -18,14 +18,25 @@ import storage from './storage';
 import sudo from './sudo';
 import toolbox from './toolbox';
 import transfer from './transfer';
+// import memo from './memo';
 // Joy packages
 import members from './joy-members';
 import { terms, privacyPolicy } from './joy-pages';
+import election from './joy-election';
+import proposals from './joy-proposals';
+import roles from './joy-roles';
+import media from './joy-media';
+import forum from './joy-forum';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
     ? [
+      media(t),
       members(t),
+      roles(t),
+      election(t),
+      proposals(t),
+      forum(t),
       staking(t),
       null,
       transfer(t),
@@ -36,7 +47,12 @@ export default function create (t: <T = string> (key: string, text: string, opti
       privacyPolicy(t)
     ]
     : [
+      media(t),
       members(t),
+      roles(t),
+      election(t),
+      proposals(t),
+      forum(t),
       staking(t),
       null,
       transfer(t),

+ 17 - 0
pioneer/packages/apps-routing/src/joy-election.ts

@@ -0,0 +1,17 @@
+import { Route } from './types';
+
+import Election from '@polkadot/joy-election/index';
+import SidebarSubtitle from '@polkadot/joy-election/SidebarSubtitle';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Election,
+    display: {
+      needsApi: ['query.council.activeCouncil', 'query.councilElection.stage']
+    },
+    text: t<string>('nav.election', 'Council', { ns: 'apps-routing' }),
+    icon: 'university',
+    name: 'council',
+    SubtitleComponent: SidebarSubtitle
+  };
+}

+ 15 - 0
pioneer/packages/apps-routing/src/joy-forum.ts

@@ -0,0 +1,15 @@
+import { Route } from './types';
+
+import Forum from '@polkadot/joy-forum/index';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Forum,
+    display: {
+      needsApi: ['query.forum.threadById']
+    },
+    text: t<string>('nav.forum', 'Forum', { ns: 'apps-routing' }),
+    icon: 'comment-dots',
+    name: 'forum'
+  };
+}

+ 15 - 0
pioneer/packages/apps-routing/src/joy-media.ts

@@ -0,0 +1,15 @@
+import Media from '@polkadot/joy-media/index';
+
+import { Route } from './types';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Media,
+    display: {
+      needsApi: ['query.storageWorkingGroup.workerById', 'query.dataObjectStorageRegistry.relationshipsByContentId']
+    },
+    text: t<string>('nav.media', 'Media', { ns: 'apps-routing' }),
+    icon: 'play-circle',
+    name: 'media'
+  };
+}

+ 16 - 0
pioneer/packages/apps-routing/src/joy-proposals.ts

@@ -0,0 +1,16 @@
+import { Route } from './types';
+
+import Proposals from '@polkadot/joy-proposals/index';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Proposals,
+    display: {
+      needsApi: ['query.proposalsEngine.proposalCount']
+    },
+    text: t<string>('nav.proposals', 'Proposals', { ns: 'apps-routing' }),
+    icon: 'tasks',
+    name: 'proposals'
+    // TODO: useCounter with active proposals count? (could be a nice addition)
+  };
+}

+ 6 - 8
pioneer/packages/old-apps/apps-routing/src/joy-roles.ts → pioneer/packages/apps-routing/src/joy-roles.ts

@@ -1,9 +1,9 @@
-import { Routes } from './types';
+import { Route } from './types';
 
 import Roles from '@polkadot/joy-roles/index';
 
-export default ([
-  {
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
     Component: Roles,
     display: {
       needsApi: [
@@ -11,10 +11,8 @@ export default ([
         'query.storageWorkingGroup.mint'
       ]
     },
-    i18n: {
-      defaultValue: 'Working groups'
-    },
+    text: t<string>('nav.roles', 'Working groups', { ns: 'apps-routing' }),
     icon: 'users',
     name: 'working-groups'
-  }
-] as Routes);
+  };
+}

+ 20 - 0
pioneer/packages/apps-routing/src/memo.ts

@@ -0,0 +1,20 @@
+import { Route } from './types';
+
+import { MemoModal } from '@polkadot/joy-utils/react/components/Memo';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    // Assert to get around the uncecessary requirement for RouteProps
+    Component: MemoModal as React.ComponentType<any>,
+    Modal: MemoModal,
+    display: {
+      isHidden: false,
+      needsApi: [
+        'tx.memo.updateMemo'
+      ]
+    },
+    icon: 'sticky-note',
+    name: 'memo',
+    text: t<string>('nav.memo', 'My memo', { ns: 'apps-routing' })
+  };
+}

+ 1 - 1
pioneer/packages/apps-routing/src/staking.ts

@@ -16,6 +16,6 @@ export default function create (t: <T = string> (key: string, text: string, opti
     },
     icon: 'certificate',
     name: 'staking',
-    text: t<string>('nav.staking', 'Staking', { ns: 'apps-routing' })
+    text: t<string>('nav.staking', 'Validators', { ns: 'apps-routing' })
   };
 }

+ 2 - 0
pioneer/packages/apps-routing/src/types.ts

@@ -24,6 +24,8 @@ export interface Route {
   name: string;
   text: string;
   useCounter?: () => number | string | null;
+  // Joystream-specific
+  SubtitleComponent?: React.ComponentType<any>;
 }
 
 export type Routes = (Route | null)[];

+ 0 - 0
pioneer/packages/old-apps/apps/public/images/default-thumbnail.png → pioneer/packages/apps/public/images/default-thumbnail.png


+ 1 - 0
pioneer/packages/apps/public/locales/en/app-accounts.json

@@ -75,6 +75,7 @@
   "Multisig approvals": "Multisig approvals",
   "Multisig approvals pending": "Multisig approvals pending",
   "Multisig message with call (for final approval)": "Multisig message with call (for final approval)",
+  "My Memo": "My Memo",
   "My On-Chain Name": "My On-Chain Name",
   "My accounts": "My accounts",
   "My contacts": "My contacts",

+ 1 - 0
pioneer/packages/apps/public/locales/en/index.json

@@ -25,6 +25,7 @@
   "joy-media.json",
   "joy-members.json",
   "joy-roles.json",
+  "joy-utils.json",
   "react-components.json",
   "react-params.json",
   "react-query.json",

+ 0 - 1
pioneer/packages/apps/public/locales/en/joy-settings.json

@@ -1 +0,0 @@
-{}

+ 0 - 1
pioneer/packages/apps/public/locales/en/joy-utils-old.json

@@ -1 +0,0 @@
-{}

+ 4 - 1
pioneer/packages/apps/public/locales/en/joy-utils.json

@@ -1 +1,4 @@
-{}
+{
+  "click to select or drag and drop the file here": "click to select or drag and drop the file here",
+  "{{name}} ({{size}} bytes)": "{{name}} ({{size}} bytes)"
+}

+ 1 - 1
pioneer/packages/apps/src/Content/NotFound.tsx

@@ -7,7 +7,7 @@ import { Redirect } from 'react-router';
 
 function NotFound (): React.ReactElement {
   return (
-    <Redirect to='/staking' />
+    <Redirect to='/media' />
   );
 }
 

+ 24 - 6
pioneer/packages/apps/src/Content/index.tsx

@@ -16,6 +16,11 @@ import { useTranslation } from '../translate';
 import NotFound from './NotFound';
 import Status from './Status';
 
+// Joystream-specific
+// We must use transport provider here instead of /apps/src/index
+// to avoid "Cannot create Transport: The Substrate API is not ready yet." error
+import { TransportProvider } from '@polkadot/joy-utils/react/context';
+
 interface Props {
   className?: string;
 }
@@ -60,11 +65,25 @@ function Content ({ className }: Props): React.ReactElement<Props> {
                 ? <NotFound />
                 : (
                   <ErrorBoundary trigger={name}>
-                    <Component
-                      basePath={`/${name}`}
-                      location={location}
-                      onStatusChange={queueAction}
-                    />
+                    { needsApi
+                      // Add transport provider for routes that need the api
+                      // (the above condition makes sure it's aleady initialized at this point)
+                      ? (
+                        <TransportProvider>
+                          <Component
+                            basePath={`/${name}`}
+                            location={location}
+                            onStatusChange={queueAction}
+                          />
+                        </TransportProvider>
+                      )
+                      : (
+                        <Component
+                          basePath={`/${name}`}
+                          location={location}
+                          onStatusChange={queueAction}
+                        />
+                      ) }
                   </ErrorBoundary>
                 )
               }
@@ -78,7 +97,6 @@ function Content ({ className }: Props): React.ReactElement<Props> {
 }
 
 export default React.memo(styled(Content)`
-  background: rgba(250, 250, 250);
   padding: 0 1.5rem;
   position: relative;
   width: 100%;

+ 5 - 2
pioneer/packages/apps/src/SideBar/Item.tsx

@@ -79,12 +79,15 @@ function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement<Props
     return null;
   }
 
-  const { Modal, icon, name, text } = route;
+  const { Modal, SubtitleComponent, icon, name, text } = route;
 
   const body = (
     <>
       <Icon icon={icon} />
-      <span className='text'>{text}</span>
+      <span className='text'>
+        {text}
+        { SubtitleComponent && <SubtitleComponent/> }
+      </span>
       {!!count && (
         <Badge
           color='counter'

+ 0 - 2
pioneer/packages/apps/src/SideBar/index.tsx

@@ -14,7 +14,6 @@ import NetworkModal from '../modals/Network';
 import { useTranslation } from '../translate';
 import ChainInfo from './ChainInfo';
 import Item from './Item';
-import NodeInfo from './NodeInfo';
 
 interface Props {
   className?: string;
@@ -101,7 +100,6 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
                 )
             ))}
             <Menu.Divider hidden />
-            {!isCollapsed && <NodeInfo />}
           </div>
           <div className={`apps--SideBar-collapse ${isCollapsed ? 'collapsed' : 'expanded'}`}>
             <Button

+ 14 - 7
pioneer/packages/apps/src/initSettings.ts

@@ -8,11 +8,17 @@ import { createEndpoints } from '@polkadot/apps-config/settings';
 import { extractIpfsDetails } from '@polkadot/react-hooks/useIpfs';
 import settings from '@polkadot/ui-settings';
 
-function getApiUrl (): string {
+// Joystream-specific override in order to include default UIMODE
+function getInitSettings () {
   // we split here so that both these forms are allowed
   //  - http://localhost:3000/?rpc=wss://substrate-rpc.parity.io/#/explorer
   //  - http://localhost:3000/#/explorer?rpc=wss://substrate-rpc.parity.io
   const urlOptions = queryString.parse(location.href.split('?')[1]);
+  const stored = store.get('settings') as Record<string, unknown> || {};
+
+  // uiMode - set to "light" if not in storage
+  const uiMode = stored.uiMode ? stored.uiMode as string : 'light';
+  let apiUrl: string;
 
   // if specified, this takes priority
   if (urlOptions.rpc) {
@@ -20,7 +26,7 @@ function getApiUrl (): string {
       throw new Error('Invalid WS endpoint specified');
     }
 
-    return urlOptions.rpc.split('#')[0]; // https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/explorer;
+    apiUrl = urlOptions.rpc.split('#')[0]; // https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/explorer;
   }
 
   const endpoints = createEndpoints(<T = string>(): T => ('' as unknown as T));
@@ -31,24 +37,25 @@ function getApiUrl (): string {
     const option = endpoints.find(({ dnslink }) => dnslink === ipnsChain);
 
     if (option) {
-      return option.value as string;
+      apiUrl = option.value as string;
     }
   }
 
-  const stored = store.get('settings') as Record<string, unknown> || {};
   const fallbackUrl = endpoints.find(({ value }) => !!value);
 
   // via settings, or the default chain
-  return [stored.apiUrl, process.env.WS_URL].includes(settings.apiUrl)
+  apiUrl = [stored.apiUrl, process.env.WS_URL].includes(settings.apiUrl)
     ? settings.apiUrl // keep as-is
     : fallbackUrl
       ? fallbackUrl.value as string // grab the fallback
       : 'ws://127.0.0.1:9944'; // nothing found, go local
+
+  return { apiUrl, uiMode };
 }
 
-const apiUrl = getApiUrl();
+const { apiUrl, uiMode } = getInitSettings();
 
 // set the default as retrieved here
-settings.set({ apiUrl });
+settings.set({ apiUrl, uiMode });
 
 console.log('WS endpoint=', apiUrl);

+ 14 - 0
pioneer/packages/apps/webpack.base.config.js

@@ -48,6 +48,9 @@ function createWebpack (ENV, context) {
     return alias;
   }, {});
 
+  // Add @joystream/types as alias to automatically process any changes:
+  alias['@joystream/types'] = path.resolve(context, '../../../types/src');
+
   return {
     context,
     entry: ['@babel/polyfill', './src/index.tsx'],
@@ -69,6 +72,17 @@ function createWebpack (ENV, context) {
             }
           ]
         },
+        {
+          test: /\.s[ac]ss$/i,
+          use: [
+            // Creates `style` nodes from JS strings
+            'style-loader',
+            // Translates CSS into CommonJS
+            'css-loader',
+            // Compiles Sass to CSS
+            'sass-loader'
+          ]
+        },
         {
           include: /node_modules/,
           test: /\.css$/,

+ 0 - 0
pioneer/packages/joy-election/.skip-build


+ 3 - 3
pioneer/packages/joy-election/package.json

@@ -7,9 +7,9 @@
   "author": "Joystream contributors",
   "maintainers": [],
   "dependencies": {
-    "@babel/runtime": "^7.7.1",
-    "@polkadot/react-components": "0.37.0-beta.63",
-    "@polkadot/react-query": "0.37.0-beta.63",
+    "@babel/runtime": "^7.10.5",
+    "@polkadot/react-components": "0.51.1",
+    "@polkadot/react-query": "0.51.1",
     "@polkadot/joy-utils": "^0.1.1"
   }
 }

+ 9 - 6
pioneer/packages/joy-election/src/Applicant.tsx

@@ -4,24 +4,25 @@ import { Table } from 'semantic-ui-react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
 import CandidatePreview from './CandidatePreview';
 
 import translate from './translate';
-import { calcTotalStake } from '@polkadot/joy-utils/index';
+import { calcTotalStake } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStake } from '@joystream/types/council';
 
 type Props = ApiProps & I18nProps & {
   index: number;
   accountId: AccountId;
   stake?: ElectionStake;
+  isVotingStage: boolean;
 };
 
 class Applicant extends React.PureComponent<Props> {
   render () {
-    const { index, accountId, stake } = this.props;
+    const { index, accountId, stake, isVotingStage } = this.props;
     const voteUrl = `/council/votes?applicantId=${accountId.toString()}`;
 
     return (
@@ -33,9 +34,11 @@ class Applicant extends React.PureComponent<Props> {
         <Table.Cell style={{ textAlign: 'right' }}>
           {formatBalance(calcTotalStake(stake))}
         </Table.Cell>
-        <Table.Cell>
-          <Link to={voteUrl} className='ui button primary inverted'>Vote</Link>
-        </Table.Cell>
+        { isVotingStage && (
+          <Table.Cell>
+            <Link to={voteUrl} className='ui button primary inverted'>Vote</Link>
+          </Table.Cell>
+        ) }
       </Table.Row>
     );
   }

+ 44 - 24
pioneer/packages/joy-election/src/Applicants.tsx

@@ -1,49 +1,68 @@
 import React from 'react';
-import { Table } from 'semantic-ui-react';
+import { Table, Message } from 'semantic-ui-react';
 import BN from 'bn.js';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
+import { Option } from '@polkadot/types';
 import { formatNumber } from '@polkadot/util';
 
 import translate from './translate';
 import Applicant from './Applicant';
 import ApplyForm from './ApplyForm';
-import Section from '@polkadot/joy-utils/Section';
-import { queryToProp } from '@polkadot/joy-utils/index';
-import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { ElectionStage } from '@joystream/types/src/council';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps & I18nProps & MyAccountProps & {
+type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & {
   candidacyLimit?: BN;
   applicants?: Array<AccountId>;
+  stage?: Option<ElectionStage>;
 };
 
 class Applicants extends React.PureComponent<Props> {
-  private renderTable = (applicants: Array<AccountId>) => (
-    <Table celled selectable compact>
-      <Table.Header>
-        <Table.Row>
-          <Table.HeaderCell>#</Table.HeaderCell>
-          <Table.HeaderCell>Applicant</Table.HeaderCell>
-          <Table.HeaderCell>Total stake</Table.HeaderCell>
-          <Table.HeaderCell style={{ width: '1%' }}>Actions</Table.HeaderCell>
-        </Table.Row>
-      </Table.Header>
-      <Table.Body>{applicants.map((accountId, index) => (
-        <Applicant key={index} index={index} accountId={accountId} />
-      ))}</Table.Body>
-    </Table>
-  )
+  private renderTable = (applicants: Array<AccountId>) => {
+    const isVotingStage = this.props.stage?.unwrapOr(undefined)?.isOfType('Voting') || false;
+
+    return (
+      <Table celled selectable compact>
+        <Table.Header>
+          <Table.Row>
+            <Table.HeaderCell>#</Table.HeaderCell>
+            <Table.HeaderCell>Applicant</Table.HeaderCell>
+            <Table.HeaderCell>Total stake</Table.HeaderCell>
+            { isVotingStage && (
+              <Table.HeaderCell style={{ width: '1%' }}>Actions</Table.HeaderCell>
+            ) }
+          </Table.Row>
+        </Table.Header>
+        <Table.Body>{applicants.map((accountId, index) => (
+          <Applicant key={index} index={index} accountId={accountId} isVotingStage={isVotingStage}/>
+        ))}</Table.Body>
+      </Table>
+    );
+  }
 
   render () {
-    const { myAddress, applicants = [], candidacyLimit = new BN(0) } = this.props;
+    const { myAddress, applicants = [], candidacyLimit = new BN(0), stage } = this.props;
     const title = <span>Applicants <sup>{applicants.length}/{formatNumber(candidacyLimit)}</sup></span>;
 
     return <>
       <Section title='My application'>
-        <ApplyForm myAddress={myAddress} />
+        { stage?.unwrapOr(undefined)?.isOfType('Announcing')
+          ? (
+            <ApplyForm myAddress={myAddress} />
+          )
+          : (
+            <Message warning>
+              Applying to council is only possible during <i><b>Announcing</b></i> stage.
+            </Message>
+          )
+        }
       </Section>
       <Section title={title}>
         {!applicants.length
@@ -59,6 +78,7 @@ class Applicants extends React.PureComponent<Props> {
 export default translate(
   withCalls<Props>(
     queryToProp('query.councilElection.candidacyLimit'),
-    queryToProp('query.councilElection.applicants')
+    queryToProp('query.councilElection.applicants'),
+    queryToProp('query.councilElection.stage')
   )(withMyAccount(Applicants))
 );

+ 19 - 21
pioneer/packages/joy-election/src/ApplyForm.tsx

@@ -3,21 +3,21 @@ import React from 'react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { Labelled } from '@polkadot/react-components/index';
 import { Balance } from '@polkadot/types/interfaces';
 
 import translate from './translate';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import InputStake from '@polkadot/joy-utils/InputStake';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
+import InputStake from '@polkadot/joy-utils/react/components/InputStake';
 import { ElectionStake } from '@joystream/types/council';
-import { calcTotalStake, ZERO } from '@polkadot/joy-utils/index';
-import { MyAddressProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
+import { calcTotalStake, ZERO } from '@polkadot/joy-utils/functions/misc';
+import { MyAddressProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
 
 type Props = ApiProps & I18nProps & MyAddressProps & {
   minStake?: Balance;
   alreadyStaked?: ElectionStake;
-  myBalance?: Balance;
 };
 
 type State = {
@@ -48,15 +48,16 @@ class ApplyForm extends React.PureComponent<Props, State> {
           isValid={isStakeValid}
           onChange={this.onChangeStake}
         />
-        <Labelled style={{ marginTop: '.5rem' }}>
-          <TxButton
-            size='large'
-            isDisabled={!isStakeValid}
-            label={buttonLabel}
-            params={[stake]}
-            tx='councilElection.apply'
-          />
-        </Labelled>
+        <div style={{ marginTop: '.5rem' }}>
+          <Labelled>
+            <TxButton
+              isDisabled={!isStakeValid}
+              label={buttonLabel}
+              params={[stake]}
+              tx='councilElection.apply'
+            />
+          </Labelled>
+        </div>
       </div>
     );
   }
@@ -71,10 +72,9 @@ class ApplyForm extends React.PureComponent<Props, State> {
 
   private onChangeStake = (stake?: BN): void => {
     stake = stake || ZERO;
-    const { myBalance = ZERO } = this.props;
-    const isStakeLteBalance = stake.lte(myBalance);
     const isStakeGteMinStake = stake.add(this.alreadyStaked()).gte(this.minStake());
-    const isStakeValid = !stake.isZero() && isStakeGteMinStake && isStakeLteBalance;
+    const isStakeValid = !stake.isZero() && isStakeGteMinStake;
+
     this.setState({ stake, isStakeValid });
   }
 }
@@ -88,8 +88,6 @@ export default withMulti(
     ['query.councilElection.minCouncilStake',
       { propName: 'minStake' }],
     ['query.councilElection.applicantStakes',
-      { paramName: 'myAddress', propName: 'alreadyStaked' }],
-    ['query.balances.freeBalance',
-      { paramName: 'myAddress', propName: 'myBalance' }]
+      { paramName: 'myAddress', propName: 'alreadyStaked' }]
   )
 );

+ 2 - 2
pioneer/packages/joy-election/src/CandidatePreview.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
-import MemberByAccount from '@polkadot/joy-utils/MemberByAccountPreview';
+import AddressMini from '@polkadot/react-components/AddressMini';
+import MemberByAccount from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 import { AccountId } from '@polkadot/types/interfaces';
 
 import styled from 'styled-components';

+ 8 - 7
pioneer/packages/joy-election/src/Council.tsx

@@ -2,22 +2,22 @@ import React from 'react';
 
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Table } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import CouncilCandidate from './CandidatePreview';
 
-import { calcBackersStake } from '@polkadot/joy-utils/index';
+import { calcBackersStake } from '@polkadot/joy-utils/functions/misc';
 import { Seat } from '@joystream/types/council';
 import translate from './translate';
-import Section from '@polkadot/joy-utils/Section';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps &
-I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   council?: Seat[];
 };
 
-type State = {};
+type State = Record<any, never>;
 
 class Council extends React.PureComponent<Props, State> {
   state: State = {};
@@ -53,9 +53,10 @@ class Council extends React.PureComponent<Props, State> {
 
   render () {
     const { council = [] } = this.props;
+
     // console.log({ council });
     return (
-      <Section title="Active council members">
+      <Section title='Active council members'>
         {!council.length ? <em>Council is not elected yet</em> : this.renderTable(council)}
       </Section>
     );

+ 83 - 53
pioneer/packages/joy-election/src/Dashboard.tsx

@@ -3,18 +3,19 @@ import React from 'react';
 
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Option } from '@polkadot/types';
 import { BlockNumber, Balance } from '@polkadot/types/interfaces';
-import { Bubble } from '@polkadot/react-components/index';
+import { Label, Icon } from 'semantic-ui-react';
 import { formatNumber, formatBalance } from '@polkadot/util';
 
-import Section from '@polkadot/joy-utils/Section';
-import { queryToProp } from '@polkadot/joy-utils/index';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStage, Seat } from '@joystream/types/council';
 import translate from './translate';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps & I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   bestNumber?: BN;
 
   activeCouncil?: Seat[];
@@ -34,7 +35,7 @@ type Props = ApiProps & I18nProps & {
   stage?: Option<ElectionStage>;
 };
 
-type State = {};
+type State = Record<any, never>;
 
 class Dashboard extends React.PureComponent<Props, State> {
   state: State = {};
@@ -45,12 +46,17 @@ class Dashboard extends React.PureComponent<Props, State> {
     const title = `Council ${activeCouncil.length > 0 ? '' : '(not elected)'}`;
 
     return <Section title={title}>
-      <Bubble label='Council members'>
-        {activeCouncil.length}
-      </Bubble>
-      <Bubble icon='flag checkered' label='Term ends at block #'>
-        {formatNumber(p.termEndsAt)}
-      </Bubble>
+      <Label.Group color='grey' size='large'>
+        <Label>
+          Council members
+          <Label.Detail>{activeCouncil.length}</Label.Detail>
+        </Label>
+        <Label>
+          <Icon name='flag checkered'/>
+          Term ends at block #
+          <Label.Detail>{formatNumber(p.termEndsAt)}</Label.Detail>
+        </Label>
+      </Label.Group>
     </Section>;
   }
 
@@ -59,13 +65,16 @@ class Dashboard extends React.PureComponent<Props, State> {
 
     let stageName: string | undefined;
     let stageEndsAt: BlockNumber | undefined;
+
     if (stage && stage.isSome) {
       const stageValue = stage.value as ElectionStage;
+
       stageEndsAt = stageValue.value as BlockNumber; // contained value
       stageName = stageValue.type; // name of Enum variant
     }
 
     let leftBlocks: BN | undefined;
+
     if (stageEndsAt && bestNumber) {
       leftBlocks = stageEndsAt.sub(bestNumber);
     }
@@ -76,20 +85,28 @@ class Dashboard extends React.PureComponent<Props, State> {
     const title = <>Election (<span className={stateClass}>{stateText}</span>)</>;
 
     return <Section title={title}>
-      <Bubble icon='target' label='Election round #'>
-        {formatNumber(round)}
-      </Bubble>
-      {isRunning && <>
-        <Bubble label='Stage'>
-          {stageName}
-        </Bubble>
-        <Bubble label='Blocks left'>
-          {formatNumber(leftBlocks)}
-        </Bubble>
-        <Bubble icon='flag checkered' label='Stage ends at block #'>
-          {formatNumber(stageEndsAt)}
-        </Bubble>
-      </>}
+      <Label.Group color='grey' size='large'>
+        <Label>
+          <Icon name='target'/>
+          Election round #
+          <Label.Detail>{formatNumber(round)}</Label.Detail>
+        </Label>
+        {isRunning && <>
+          <Label>
+            Stage
+            <Label.Detail>{stageName}</Label.Detail>
+          </Label>
+          <Label>
+            Blocks left
+            <Label.Detail>{formatNumber(leftBlocks)}</Label.Detail>
+          </Label>
+          <Label>
+            <Icon name='flag checkered'/>
+            Stage ends at block #
+            <Label.Detail>{formatNumber(stageEndsAt)}</Label.Detail>
+          </Label>
+        </>}
+      </Label.Group>
     </Section>;
   }
 
@@ -98,33 +115,46 @@ class Dashboard extends React.PureComponent<Props, State> {
     const isAutoStart = (p.autoStart || false).valueOf();
 
     return <Section title='Configuration'>
-      <Bubble label='Auto-start elections'>
-        {isAutoStart ? 'Yes' : 'No'}
-      </Bubble>
-      <Bubble label='New term duration'>
-        {formatNumber(p.newTermDuration)}
-      </Bubble>
-      <Bubble label='Candidacy limit'>
-        {formatNumber(p.candidacyLimit)}
-      </Bubble>
-      <Bubble label='Council size'>
-        {formatNumber(p.councilSize)}
-      </Bubble>
-      <Bubble label='Min. council stake'>
-        {formatBalance(p.minCouncilStake)}
-      </Bubble>
-      <Bubble label='Min. voting stake'>
-        {formatBalance(p.minVotingStake)}
-      </Bubble>
-      <Bubble label='Announcing period'>
-        {formatNumber(p.announcingPeriod)} blocks
-      </Bubble>
-      <Bubble label='Voting period'>
-        {formatNumber(p.votingPeriod)} blocks
-      </Bubble>
-      <Bubble label='Revealing period'>
-        {formatNumber(p.revealingPeriod)} blocks
-      </Bubble>
+      <Label.Group color='grey' size='large'>
+        <Label>
+          Auto-start elections
+          <Label.Detail>{isAutoStart ? 'Yes' : 'No'}</Label.Detail>
+        </Label>
+        <Label>
+          New term duration
+          <Label.Detail>{formatNumber(p.newTermDuration)}</Label.Detail>
+        </Label>
+        <Label>
+          Candidacy limit
+          <Label.Detail>{formatNumber(p.candidacyLimit)}</Label.Detail>
+        </Label>
+        <Label>
+          Council size
+          <Label.Detail>{formatNumber(p.councilSize)}</Label.Detail>
+        </Label>
+        <Label>
+          Min. council stake
+          <Label.Detail>{formatBalance(p.minCouncilStake)}</Label.Detail>
+        </Label>
+        <Label>
+          Min. voting stake
+          <Label.Detail>{formatBalance(p.minVotingStake)}</Label.Detail>
+        </Label>
+      </Label.Group>
+      <Label.Group color='grey' size='large'>
+        <Label>
+          Announcing period
+          <Label.Detail>{formatNumber(p.announcingPeriod)} blocks</Label.Detail>
+        </Label>
+        <Label>
+          Voting period
+          <Label.Detail>{formatNumber(p.votingPeriod)} blocks</Label.Detail>
+        </Label>
+        <Label>
+          Revealing period
+          <Label.Detail>{formatNumber(p.revealingPeriod)} blocks</Label.Detail>
+        </Label>
+      </Label.Group>
     </Section>;
   }
 

+ 21 - 18
pioneer/packages/joy-election/src/Reveals.tsx

@@ -1,23 +1,23 @@
 import React from 'react';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
 import { Input, Labelled, InputAddress } from '@polkadot/react-components/index';
 
 import translate from './translate';
-import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/index';
+import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/functions/misc';
 import { accountIdsToOptions, hashVote } from './utils';
-import TxButton from '@polkadot/joy-utils/TxButton';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
 import { findVoteByHash } from './myVotesStore';
-import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+import { RouteProps } from 'react-router-dom';
 
 // AppsProps is needed to get a location from the route.
-type Props = AppProps & ApiProps & I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   applicantId?: string | null;
   applicants?: AccountId[];
-  location: any;
 };
 
 type State = {
@@ -30,8 +30,9 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
   constructor (props: Props) {
     super(props);
     let { applicantId, location } = this.props;
-    applicantId = applicantId || getUrlParam(location, 'applicantId');
-    const hashedVote = getUrlParam(location, 'hashedVote');
+
+    applicantId = applicantId || (location && getUrlParam(location, 'applicantId'));
+    const hashedVote = location && getUrlParam(location, 'hashedVote');
 
     this.state = {
       applicantId,
@@ -45,6 +46,7 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
     const applicantOpts = accountIdsToOptions(this.props.applicants || []);
 
     const myVote = hashedVote ? findVoteByHash(hashedVote) : undefined;
+
     if (myVote) {
       // Try to substitue applicantId and salt from local sotre:
       if (!applicantId) applicantId = myVote.applicantId;
@@ -81,15 +83,16 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
             onChange={this.onChangeSalt}
           />
         </div>}
-        <Labelled style={{ marginTop: '.5rem' }}>
-          <TxButton
-            size='large'
-            isDisabled={!isVoteRevealed}
-            label='Reveal this vote'
-            params={[hashedVote, applicantId, salt]}
-            tx='councilElection.reveal'
-          />
-        </Labelled>
+        <div style={{ marginTop: '.5rem' }}>
+          <Labelled>
+            <TxButton
+              isDisabled={!isVoteRevealed}
+              label='Reveal this vote'
+              params={[hashedVote, applicantId, salt]}
+              tx='councilElection.reveal'
+            />
+          </Labelled>
+        </div>
       </div>
     );
   }

+ 15 - 6
pioneer/packages/joy-election/src/SealedVote.tsx

@@ -1,38 +1,47 @@
 import React from 'react';
 import { Link } from 'react-router-dom';
-import { Table } from 'semantic-ui-react';
+import { Table, Message } from 'semantic-ui-react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Hash } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
 
 import translate from './translate';
-import { calcTotalStake } from '@polkadot/joy-utils/index';
+import { calcTotalStake } from '@polkadot/joy-utils/functions/misc';
 import { SealedVote } from '@joystream/types/council';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
+import AddressMini from '@polkadot/react-components/AddressMini';
 import CandidatePreview from './CandidatePreview';
 import { findVoteByHash } from './myVotesStore';
 
 type Props = ApiProps & I18nProps & {
   hash: Hash;
   sealedVote?: SealedVote;
+  isStageRevealing: boolean;
+  isMyVote: boolean;
 };
 
 class Comp extends React.PureComponent<Props> {
   renderCandidateOrAction () {
-    const { hash, sealedVote } = this.props;
+    const { hash, sealedVote, isStageRevealing, isMyVote } = this.props;
+
     if (!sealedVote) {
       return <em>Unknown hashed vote: {hash.toHex()}</em>;
     }
 
     if (sealedVote.vote.isSome) {
       const candidateId = sealedVote.vote.unwrap();
+
       return <CandidatePreview accountId={candidateId} />;
-    } else {
+    } else if (isStageRevealing && isMyVote) {
       const revealUrl = `/council/reveals?hashedVote=${hash.toHex()}`;
+
       return <Link to={revealUrl} className='ui button primary inverted'>Reveal this vote</Link>;
+    } else if (isMyVote) {
+      return <Message warning>Wait until <i><b>Revealing</b></i> stage in order to reveal this vote.</Message>;
+    } else {
+      return <Message info>This vote has not been revealed yet.</Message>;
     }
   }
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно